Angular应用程序开发指南

978-7-115-52436-2
作者: 成龙
译者:
编辑: 吴晋瑜

图书目录:

详情

《Angular应用程序开发指南》主要介绍如何用 Web 前端框架 Angular 开发应用程序。书中先介绍Angular的发展历程和基础概念,帮助读者了解Angular框架并打好坚实的基础;然后以“天天菜园”蔬菜订购应用程序为例,将理论讲解落实到代码实现上,带领读者真正体验用Angular开发应用程序的全过程;最后介绍用于实现自动化开发工作流程的命令行接 口Angular CLI,帮助读者用所学知识构建新的应用程序。本书既涵盖Angular的基础内容,又通过真实的项目实例展现了应用程序的开发过程,非常适合对 Angular 框架感兴趣的前端开发人员作为自学教程。

图书摘要

版权信息

书名:Angular应用程序开发指南

ISBN:978-7-115-52436-2

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

著    成 龙

责任编辑 吴晋瑜

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315


本书主要介绍用Angular 框架开发应用程序的方法。全书共包括14 章内容,按照如下思路组织内容:从AngularJS 到Angular 的发展历程讲起,然后介绍Angular 应用程序之前所需做的准备、TypeScript 的基础语法,并在此基础上,介绍TypeScript 中的模块、命名空间和声明文件以及Angular中的基础概念,最后通过使用Angular 开发一个真实的名为“天天菜园”的Web 应用程序,帮助读者系统地掌握Angular 的整个框架,提升构建一个结构清晰且易于开发和维护的Angular 应用程序的 能力。

本书适合Web 前端开发人员和打算从事Web 前端开发的人员阅读,尤其适合想尝试使用Angular但难以入手的开发人员参考。


时至今日,仍然有很多人对Angular的印象停留在AngularJS上,认为它是一个学习曲线极为陡峭并且使用起来极为麻烦的框架。但事实上,Angular和AngularJS不是同一个框架,我们甚至只要掌握Angular提供的不到十个函数或方法的调用,就可以快速地开发出一个成熟的单页面应用程序。

诚然,这些函数或方法不像我们熟悉的jQuery那样直观,但它们背后的理论和思想并不是Angular自创的,而是随着软件工程的发展而诞生的,并在服务器端、桌面和移动端应用程序上运用了长达几十年。

也有很多人说Angular是面向未来的框架,因为科学、高效且成熟的理论和思想从来不会“安分”,它们早已开始从服务器端、桌面和移动端蔓延到Web客户端,而随着富有远见的Web客户端应用程序开发人员对这些理论和思想的逐渐接受,Angular自然会在不久的未来占据国内Web客户端应用程序开发的半壁江山—— 这一事情在国外早已发生。

尽管很多人都明白这一点,但确实也有不少人在走进Angular的世界时会面临一些阻碍,比如很多人并不熟悉Angular所推荐采用的开发语言TypeScript,甚至国内至今还没有一本专门介绍这门正式发布于2014年的“新潮”语言的图书。

本书就是为了帮助读者跨过这些障碍编写的,然后通过一个完整而详细的案例带领读者走进Angular的世界,以帮助读者建立系统的Angular技能框架,从而获得基于这一技能框架搭建并开发Angular应用程序的能力。

具体来说,本书是通过以下内容来实现这一目的的。

本书第1章将带领读者初步认识Angular,使读者了解Angular的前身AngularJS是如何诞生的、AngularJS发展到Angular 2经历了哪些风雨、Angular 2为什么被称为一个框架,以及Angular 2之后的版本直接被命名为Angular的原因。

在对Angular有了一个初步的认识之后,我们本应该着手深入Angular的世界,以早日将其运用到工作中。但工欲善其事必先利其器,在带领读者继续了解Angular中的细节之前,本书第2章将先向读者介绍在学习开发Angular应用程序之前所需做的准备,比如了解Web前端开发常用的Node.js和npm到底是什么、它们和JavaScript的关系以及它们在我们的学习和工作中将发挥什么作用。

除了Node.js和npm,Angular应用程序的开发还离不开相应的代码编辑器和开发语言。本书采用的代码编辑器和开发语言分别为Visual Studio Code和TypeScript,因此第2章随后将介绍Visual Studio Code以及TypeScript编译器的安装和使用方法,其中包括如何使用Visual Studio Code编写TypeScript代码,如何使用TypeScript编译器将TypeScript代码编译成JavaScript代码,以及如何在Node.js、TypeScript代码的编译结果以及如何在Visual Studio Code的帮助下调试TypeScript代码。

除了开发工具,Angular应用程序的开发通常还离不开Web服务器端的支持,因此第2章还将介绍一个基于Node.js的Web服务器端框架Express,并展示如何通过它快速地开发一个可以承载Angular应用程序的服务器端应用程序。

在做好以上所有准备之后,任何具备其他面向对象编程语言使用经验的读者都应该可以开始学习Angular应用程序的开发了。没有这一经验的读者也请不用担心,因为本书第3~5章将详细系统地介绍Angular应用程序的首选开发语言TypeScript。

具体来说,第3章将介绍TypeScript中的模块、基础类型、函数和类等基础语法,以帮助读者快速了解TypeScript。第4章将继续深入介绍TypeScript中的类型、接口、泛型以及装饰器,并带领读者创建基于装饰器的对象工厂和依赖注入器,以帮助读者更加深切地感受TypeScript中的强类型在开发中带来的便利。第5章将对TypeScript中的模块、命名空间和声明文件进行补充,以帮助读者更加系统地认识TypeScript,从而将TypeScript用得更加得心应手。

在读者做好所有准备工作,并对TypeScript有了一个系统的认识之后,本书第6章将开始向读者介绍Angular中的基础概念,带领读者基于对这些概念的了解快速地从零开始创建一个简单的Angular应用程序,并帮助读者梳理Angular和大名鼎鼎的MVC、MVVM以及所谓的MVW之间的关系。

学完第6章的内容,读者会发现Angular绝不像说的那样难以接近—— 事实上,基于Angular开发的应用程序有着非常清晰的结构,并且更加易于团队协作和维护。本书第7~13章能够证明这一点。这7章将带领读者通过Angular开发一个真实的名为“天天菜园”的蔬菜订购Web应用程序,使读者逐步系统地掌握Angular的整个框架,并最终具备构建一个结构清晰且易于开发和维护的Angular应用程序的能力。

值得说明的是,这7章的内容都以“天天菜园”中的真实需求逐步推进,而非简单地罗列Angular中的概念。比如本书第7章将带领读者基于在第6章中掌握的经验创建“天天菜园”应用程序,以商品列表展示需求为驱动更加深入地介绍Angular中的基础概念,展示这些概念在应用程序开发中的合理运用。

第8章则将在商品列表展示的基础上引入商品细节的展示,并以商品细节展示需求为驱动介绍路由器的使用方法,从而将“天天菜园”打造成一个单页面应用程序。单页面应用程序将面临领域划分、视图间的数据传递和共享等问题,因此本章还将介绍领域模块、路由配置模块和依赖注入,并通过它们来解决这些问题。

在将“天天菜园”打造成一个易于维护的单页面应用程序之后,第9章将引入一个独立于商品领域模块的客户领域模块,以实现客户的注册和登录。为了避免客户领域模块降低应用程序的启动效率,本章还将把客户领域模块打造成一个可延迟加载的路由加载模块。此外,为了快速高效地获取用户在视图中输入的注册和登录数据,本章还将介绍Angular表单的使用方法。

用户输入的数据并非总是合法的,因此第10章将继续介绍如何使用Angular表单验证,以阻止用户通过表单提交非法的数据,并给出相应的提示。在获得合法的输入数据之后,这些数据还需要被提交到Web服务器端,以实现真实的客户注册和登录。因此,本章最后还将介绍HttpClient的用法,并说明HttpClient为什么要结合使用RxJS中的可观察对象类Observable,以及Observable和观察者模式的关系。

有了商品领域模块和客户领域模块之后,第11章将带领读者为“天天菜园”创建另一个独立的购物车领域模块,以帮助读者复习并总结实现Angular应用程序中的领域模块的整个过程。此外,为了响应客户对购物车的操作,本章还将介绍如何通过事件绑定来处理用户在视图中触发的点击和输入等事件。为了在不影响购物车现有业务的情况下使客户可以在浏览购物车的同时搜索商品,本章最后还将介绍如何使用嵌套组件,以及如何在父组件和子组件之间进行通信。

经过以上几章的努力,“天天菜园”应有的功能基本得到了实现。接下来,第12章和第13章的职责则是对这些功能进行必要的完善,并对程序架构进行合理的调整。比如第12章将把商品搜索入口共享到商品列表中,为此将用到部件模块;而为了实现购物车商品数量和登录客户姓名的同步,还将用到服务模块。

为了保证应用程序的安全性,第13章将使用路由守卫,并定义自己的客户登录验证指令;为了简化消息提示并实现确认提示,本章将动态地创建组件实例;为了给用户带去良好的使用体验,本章还将使用动画。

第7~13章涉及大量看上去复杂甚至陌生的技术,但Angular中的这些技术使用起来其实相当容易,因此读者会在阅读完这 7 章(并实践了其中的所有案例)之后发现,自己不经意间就系统地掌握了Angular的知识体系,提升了从零开始架构并开发一个完整的Angular应用程序的能力。但目前来说,这样的应用程序仍存在一个严重的问题,那就是体积过于庞大—— “天天菜园”大概会有4.5MB。

为了解决这个问题,本书最后一章(第14章)将介绍如何使用Angular CLI对现有Angular应用程序进行重构,即压缩应用程序中的名称标识符和空格,移除应用程序源代码、Angular源代码以及第三方库源代码中未被使用的代码,并对应用程序进行预先编译。

重构后的“天天菜园”会从大约4.5MB骤减到几百KB,而在完成这一极具意义的构建之后,最后一章还有一个重要的任务,那就是向读者展示如何在Angular CLI的帮助下快速地创建一个可直接运行的Angular应用程序模板,并继续在Angular CLI的帮助下快速地将这个模板扩展成我们要开发的目标应用程序—— 比如“天天菜园”的后台管理应用程序。

所谓的“快速”到底有多快呢?简单来说,它意味着“零配置”以及几行简单的Angular CLI命令。那具体来说它意味着什么呢?这或许就需要读者通过阅读本书的具体内容来寻找答案了。

本书适合所有对Angular感兴趣或存在疑惑的读者,因为本书为读者走进Angular的世界做了充分的准备。读者只需要具备简单的Web前端开发经验,就可以在这些准备的帮助下一步一步地走进Angular的世界,并逐渐了解Angular中的各个细节。

本书也适合已经在使用Angular但对其缺乏全面认识的读者,因为本书循序渐进地将Angular中的各个概念运用到一个完整的案例中,这有助于读者轻松并系统地巩固这些概念。

本书同样适合需要学习TypeScript的读者,因为本书前三分之一的内容系统地介绍了TypeScript中的各个语言特性,尤其是其中的类型系统将对熟悉JavaScript的读者会有不少的帮助。

本书源代码在异步社区本书详情页的配套资源中给出。

尽管笔者用心对待本书的每一章节和每一行代码,并通过多次审读来确保它们的准确性,但因个人能力有限,书中难免会有疏漏和不足之处,敬请广大读者指正。

如果读者在阅读本书内容或运行相关源代码时发现任何问题,或者对本书内容存有疑问,请发送邮件到294867413@qq.com的方式与笔者联系。


本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。

本书为读者提供源代码。

读者可以在异步社区本书页面中单击,跳转到下载界面,按提示进行操作即可。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。

如果读者是教师,希望获得教学配套资源,请在社区本书页面中直接联系本书的责任编辑。

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎读者将发现的问题反馈给我们,帮助我们提升图书的质量。

如果读者发现错误,请登录异步社区,按书名搜索,进入本书页面,单击“提交勘误”,输入勘误信息,单击“提交”按钮即可。本书的作者和编辑会对读者提交的勘误进行审核,确认并接受后,将赠予读者异步社区的100积分(积分可用于在异步社区兑换优惠券、样书或奖品)。

我们的联系邮箱是contact@epubit.com.cn。

如果读者对本书有任何疑问或建议,请发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果读者有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线提交投稿(直接访问www.epubit.com/selfpublish/submission即可)。

如果读者来自学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果读者在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请将怀疑有侵权行为的链接发邮件给我们。这一举动是对作者权益的保护,也是我们持续为广大读者提供有价值的内容的动力之源。

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。

异步社区

微信服务号


1995年,世界上第一款Web浏览器诞生的5年后,有一家公司不再满足于单凭HTML在Web浏览器中带来的静态内容—— 他们希望看到一些动态的效果,比如会说话的画像。

为了将“魔法”带进现实世界,这家公司委托Brendan Eich开发了一门极其简单的“编程”语言,从而使非专业程序员可以直接在其旗下的Web浏览器上通过代码来组织图片和插件。

后来的事实证明,这家名为“网景通信”的公司确实“改变了世界”,他们当时开发的这门因太过简单而被使用其他语言的专业程序员歧视的“非编程语言”,就是当下已经无所不在的JavaScript。

“尴尬”的是,如今的JavaScript违背了网景通信开发它的初衷(使其成为非专业程序员的脚本语言),早已成了一门专业的编程语言。现在,我们可以使用JavaScript开发Web浏览器、桌面、服务器、移动设备、VR设备甚至我们并不熟悉的平台上的应用程序。与其他语言相比,JavaScript有许多独到之处。

接下来,历史又一次重演,我们在2010年看到了与JavaScript同样“尴尬”的一幕。

10年前(2009年),现如今所有Web开发人员耳熟能详的Ajax已经诞生了4年,运行在Web浏览器上的优秀JavaScript框架已是百家争鸣。所有这些框架的主要目的都是一致的,即减轻程序员的痛苦,让程序员写更少的代码,或以更加统一的方式完成不同的开发任务。

很少有人为非开发人员考虑过,而Misko Hevery就是这少数人中的一个。他知道,有些Web设计师很想开发属于他们自己的应用程序,而非一个只包含布局和样式的“半成品”。于是,Misko Hevery开始着手开发一套不一样的JavaScript框架,力求帮助Web设计师在只掌握了HTML标签的使用技能的情况下,也能创建一个简单的应用程序。

这套框架起初叫作GetAngular,它帮助Misko Hevery在3个星期内将他跟另外两名同事在Google写的包含约17000(并且这个数量仍在增长)行代码的Google Feedback缩减到只有1500行。GetAngular因此受到Misko Hevery 上司的注意并最终被Google接管。由于GetAngular是一个运行在Web浏览器中的JavaScript框架,因此与当时很多其他与JavaScript相关的框架一样,GetAngular最终被加上了“.js”扩展名并命名为Angular.js(也叫作AngularJS),并于2010年8月发布了第一个版本。

AngularJS具有当时其他框架或工具包所没有(完整拥有)的优点,在它的帮助下,开发人员可以像使用其他技术(如WPF)开发桌面应用程序那样,运用MVC和MVVM模式,以更加直观、高效的方式开发更加易于测试和维护的Web应用程序。

然而,由于出发点、架构和所处年代(21世纪初期,移动Web应用程序尚未流行)等原因,AngularJS仍有许多不足。比如,它的出发点是运行在桌面Web环境下,而对移动Web没有进行相应的优化;同时要熟练地使用它并非一件易事,scope和digest等概念能让不少对它感兴趣的开发人员望而生畏;不够智能的变化监测导致开发人员有时需要主动更新视图;缺乏对资源的异步加载也让它面临被大型应用程序拒之门外的风险……

为此,开发一个全新的AngularJS显得势在必行。

AngularJS正式发布的4年零2个月之后,2014年10月22日,Misko Hevery及AngularJS的开发团队、贡献者和参与者在巴黎的ng-europe研讨会中正式向外界公布了一个新的框架—— Angular 2。

尽管我们在“Angular”的后面看到一个数字“2”,但Angular 2并不是AngularJS的版本升级,而是一次彻底的重写。这便意味着,Angular 2不再与AngularJS兼容。

看到这个消息之后,众多使用AngularJS的开发人员彻底愤怒了(Angular 2的重写有多彻底,他们的愤怒就有多彻底),并发出了这样的诉求:AngularJS是一个伟大的框架,我们希望看到更好用的版本,但我们需要兼容。有的开发人员将Angular 2正式向外公布的10月22日定为其决定退出Web开发的纪念日,有的则希望这只是一个愚人节的玩笑。

然而,Misko Hevery及Angular 2的开发团队并没有开玩笑。Misko Hevery开发GetAngular的初衷是为Web设计师提供一套只使用HTML标签也能开发一个简单应用程序的JavaScript框架,这导致后来的AngularJS紧密依赖Web浏览器运行。

这种依赖深入AngularJS的每一处,已经阻止了AngularJS运行在非Web浏览器之外的其他环境。而我们知道,JavaScript以其独到的优势跨越了我们所熟知的所有平台,如果要让AngularJS也像JavaScript一样让更多人分享其带来的开发便利,势必要来一次彻底的重写。

顶着众多来自AngularJS开发人员的舆论压力,Angular 2于2015年4月30日发布了开发者预览版,并于同年12月发布Beta版。在此期间,越来越多的AngularJS开发人员意识到一个全新的“AngularJS”的必要性,并积极地向Angular 2开发团队提供反馈。

众人的愤怒早已消散,但随之而来的是一些人的怀疑:Angular 2会有最终产品吗?毕竟距离Angular 2正式公布已经过去1年有余。

显然,这些人已经等不及尝试Angular 2这个曾经让大家愤怒但确实能“改变世界”的产品。但Angular 2的开发团队力求保证开发人员用这个产品能快速高效地开发高性能的跨Web浏览器、桌面及移动设备的应用程序。时间一晃又是半年多。

终于,在经历了多次RC(发布候选)版之后,Angular 2于2016年9月16日发布了它的正式版,并被赋予版本号2.0.0。

同时,开发团队承诺,在绝大多数AngularJS的开发人员转移到Angular2之前,他们将继续维护AngularJS,并不断更新Angular 2。

2016年10月30日,Angular 2已经更新到2.1.0。但Angular 2的开发团队速度惊人,他们于2017年3月23日发布了Angular 4.0.0。

那么,Angular 3哪里去了?答案是,不存在Angular 3。因为Angular 2中路由器包的版本号已经更新到了3.3.0,如果继续发布Angular 3的话,路由器包的版本号过于领先的问题会一直存在。

此外,为了给用户留下统一的印象,Angular 4.0.0对外的正式名称也不再是Angular 4,而是更加简洁的“Angular”。

但这也不意味着4.0.0是Angular的最后一个版本,自这一版本发布之后,Angular的开发团队决定每隔半年发布一个主要版本。截至2018年12月30日,Angular又经历了5.0.0和6.0.0两个版本,并最终被更新至7.1.4。

此处不打算介绍Angular的每一个版本带来的新特性和变化,只想说明这些特性和变化对Angular的用户(也就是我们开发人员)带来的影响是微乎其微的,因为Angular的每一个新版本都对前面的版本有着近乎完美的兼容—— 这不像Angular 2对AngularJS那样。

一个有力的证明是,本书的案例代码起初是基于Angular 2.1.0所写的,但随着Angular新版本的不断发布,本书案例的目标Angular版本也做了相应的更新。而在这个更新过程中,笔者只对案例代码做了三处修改:一是将模板中的<template>换成<ng-template>;二是将HttpModule换成了功能更加丰富的HttpClientModule(这一更换还不是必须做的);三是修改RxJS的模块路径。

这意味着什么呢?这意味着在大部分时候我们可以忽略Angular的版本号,专注于应用程序的实现。

更具体地说,Angular是一个客户端应用程序开发框架(Framework),提供了客户端应用程序开发所需的完整生态。

那么,作为一个应用程序开发框架,Angular做了些什么呢?以Web客户端单页面应用程序(Single Page Application,SPA)开发为例,为了实现页面中某个区域的刷新,我们在传统的开发中大致需要完成以下五步:

(1)创建与这一区域对应的HTML、JavaScript和CSS代码,以实现这一区域负责的显示逻辑、应用逻辑和业务逻辑;

(2)定义一个函数以处理页面中某个链接(或其他元素)的单击事件;

(3)通过Ajax从服务端加载以上HTML、JavaScript和CSS代码;

(4)将以上内容转换成DOM元素,并将它们写入DOM树中;

(5)更新浏览器地址栏中的URL,以使其与被加载的区域对应。

在开发过多个单页面应用程序之后,我们会发现后面四个步骤在所有单页面应用程序的开发中都基本是一样的,因此可以写一个库(Library)来处理这四个步骤,使它可以复用于今后的单页面应用程序开发中。

然而,开发中重复的工作并不只有以上这些(比如,还有用户输入的获取和验证以及动画处理等),并且所有这些重复的工作都有一个特点,那就是与应用程序的运行平台紧密相关。幸运的是,Angular帮助我们完成了所有这些与运行平台紧密相关的重复工作,使我们可以专注于完成只与应用程序本身紧密相关的工作。

但作为一个框架(而非一个库),Angular提供的远不止避免重复的工作这么简单。事实上,Angular本身是一个应用程序,当使用Angular开发应用程序时,我们其实是在对Angular这个应用程序进行扩展,而这些扩展的加载、运行和销毁则完全是由Angular管理的。

本书第6章会介绍这些扩展具体是什么,现在我们只需要知道Angular作为一个框架本身也是一个应用程序,它负责处理与应用程序运行平台紧密相关的工作,从而为专注于应用程序功能的扩展准备一个与运行平台无关的环境。

与运行平台无关?难道Angular是跨平台的?是的,刚刚我们举了一个Web客户端单页面应用程序开发的例子,但事实上和NativeScript一起,Angular也可以用于开发原生的Android和iOS应用程序。

尽管如此,本书只专注于介绍如何使用Angular开发Web客户端应用程序。如果读者已经是一名NativeScript开发人员,那么在看完本书之后,读者会发现自己能轻松地把Angular运用到NativeScript中。


  

说明  

NativeScript官网提供了免费的NativeScript电子书,用于介绍如何使用NativeScript开发原生的Android和iOS应用程序,同时也介绍了如何在NativeScript中使用Angular。


我们不得不感慨历史总是惊人地相似,和JavaScript同样“尴尬”的是,最终的AngularJS/Angular违背了Misko Hevery设计GetAngular的初衷—— 使非专业的Web应用程序开发人员也可以开发简单的Web应用程序。

如今,Angular已成了专业的客户端应用程序开发框架,它使我们可以通过更加轻松和规范的方式开发专业的Web和原生的移动端应用程序。

那么,Angular具体是怎样做到这一点的呢?在回答这个问题之前,我们还需要先做一些准备工作。


看了第1章的介绍,或许读者已经迫不及待地打算创建一个Angular应用程序了,但在此之前,读者还需要花些时间来做一些准备工作。比如,Angular应用程序的首选开发语言是TypeScript,因此我们需要准备一个使用TypeScript开发Web应用程序的环境。

尽管TypeScript是由微软公司开发的,但微软公司没有将TypeScript像.NET Framework那样封闭在Windows平台之下(事实上,.NET Core也早就可以跨平台了),而是将TypeScript开放给了所有平台。

TypeScript能被开放给所有平台归功于Node.js,因为TypeScript的编译器是基于Node.js实现的,而Node.js本身是跨平台的。

不仅TypeScript的编译器,微软公司开发的另一款跨平台的代码编辑器也是基于Node.js实现的,而这款代码编辑器就是接下来我们将要使用的Visual Studio Code。


  

说明  

TypeScript编译器和Visual Studio Code都是开源的,如果读者有兴趣了解它们的实现细节,可以访问它们的Git库。


不熟悉Node.js的读者肯定会问Node.js是什么,本节将让读者对它有一个初步的认识。

简单来说,Node.js是JavaScript的一个运行时(Runtime),这就跟JRE(Java Runtime Environment)是Java的运行时、.NET Framework是C#的运行时一样。此外,Node.js是跨平台的,我们可以基于它开发不同平台下的服务端、桌面甚至移动应用程序。

那么,Node.js是怎么来的呢?Node.js的诞生和发展与Angular颇为相似,2009年11月由Dyan Dahl在欧洲JSConf会议上公布Node.js,目的是展示一款能够比Tomcat处理更多并发连接的Web服务器,但最终意外地“改变了世界”。

为什么这么说呢?Node.js很快实现了它的初衷,即让开发人员可以在Linux和Mac OS X这两个平台下像使用Java和C#那样使用JavaScript访问网络、文件和数据库等I/O资源,从而实现Web服务端应用程序的开发。另外,由于JavaScript与生俱来的单线程和异步的特点,轻量的Node.js在处理高并发的请求时显示出了更高的性能,让人不失所望。

然而,实现这一初衷之后,Node.js并未停滞不前,人们很快意识到了Node.js的潜力,并开始将它迁移到越来越多的平台(包括Windows和其他众多类Linux平台),同时也将它从服务端延伸到了桌面端甚至是移动应用程序中。

现在,基于Node.js除了可以开发Web服务端应用程序,使用基于Node.js的Electron,我们还可以使用JavaScript、HTML和CSS开发跨平台的桌面应用程序—— 随后我们将要使用的Visual Studio Code就是基于Electron开发的。

同时,将Node.js迁移到移动平台的尝试也在不断进行,Node.js for Mobile Apps就是其中比较成熟的一个。通过它,我们可以在原生的移动应用程序中,通过JavaScript来实现应用程序的应用逻辑和业务逻辑,从而实现更多代码的复用。

当然,故事到这里并没有结束,由于Node.js具备体积小、跨平台、学习成本低和高性能等优点,越来越多的开发框架和开发工具都开始基于它来实现。除了刚刚提到的Visual Studio Code,这个世界上众多其他语言或框架的开发人员也都在使用基于Node.js的其他开发工具。

1.模块和包

为了掌握TypeScript中的特性,我们并不需要成为资深的Node.js开发人员,但掌握其中的一些核心概念,会消除我们今后开发中不少的疑虑。为此,让我们再花些时间来了解一下Node.js中的两个核心概念:模块(Module)和包(Package)。

刚刚我们知道了Node.js是JavaScript的一个运行时,我们打算开发运行在Node.js中的应用程序时,需要编写一个个的JavaScript文件,而这些JavaScript文件就是一个个的Node.js模块。也就是说,在Node.js中,一个JavaScript文件就是一个Node.js模块。要在一个模块中使用另一个模块的功能时,我们需要将那个模块导入当前模块中。

当一个Node.js应用程序中的模块数量过多时,这些模块通常会被按照相关性组织到不同的目录中,并且这些目录分别可以被当作一个整体导入有需要的模块中。在Node.js中,这样的目录有一个专有的名称,那就是包(Package)—— 这有些类似于Java中的包或C#中的程序集(Assembly)。

由于Node.js应用程序总是存储在一个自己的根目录中,因此其自身也是一个包。我们可以把自己开发的Node.js应用程序提供给别人下载,也可以向自己的Node.js应用程序中安装别人提供的包。这个时候,我们需要用到npm。

2.npm

狭义上的npm(Node.js Package Manager)是指Node.js应用程序,称为Node.js包管理器。通过它,我们可以方便地下载第三方Node.js包,或发布自己的Node.js包。

广义上的npm则是指开发它的同名公司,以及这家公司提供的第三方Node.js包在线仓库(Registry)等服务。我们在通过npm(Node.js包管理器)下载第三方Node.js包时,就是从它的在线仓库中下载的。

除了下载,任何人都可以注册npm账户,然后向它的在线仓库中发布自己的Node.js包。因此,我们几乎可以从它的在线仓库中找到任何我们想要的Node.js包,包括那些本质上并非运行在Node.js上的资源,比如我们熟悉的jQuery和Bootstrap。

为了确定npm的在线仓库中确实包含我们需要的资源,我们可以通过它主页的搜索框进行相应的搜索。图2-1所示的是在npm的官网搜索“jQuery”后得到的结果列表,其中第一个结果就是我们熟悉的jQuery。

图2-1 使用浏览器搜索npm管理的Node.js包

而打算成为“高级”用户的人,会使用更加高级的npm指令。通过这种指令,他们不仅可以查找所关心的Node.js包,还可以将它们从npm在线仓库中下载并安装到应用程序中。

那么,如何使用npm指令?我们需要先安装Node.js。

3.安装Node.js

访问Node.js的官网,然后下载其中推荐的长期支持(LTS,Long Term Support)版本的安装包,并在下载完成之后双击它以开始Node.js的安装。

目前Node.js的安装包大小不超过20 MB,如果系统盘空间充足,那么只需要单击安装过程中的每一个“Next”按钮,直到安装完成。

这样一来,Node.js会被安装在C:\Program Files\nodejs这个路径下,而这一路径也会被安装程序添加到系统环境变量PATH中。


  

说明  

同一个设备可以安装Node.js的多个版本和实例,一些其他程序也会自动在读者的设备中安装Node.js,在这种情况下,读者仍然要完成以上安装步骤,以确保Node.js的正确安装。

不同平台下Node.js的安装方式不同,本书Node.js及其他工具的安装仅以Windows平台为例,如果读者使用的是其他平台,请使用读者所在平台的方式完成安装。


完成安装之后,我们可以打开“命令提示符”窗口(cmd.exe,其他平台与之对应的为终端),并输入命令:

node -v

此时我们将看到安装成功的Node.js的版本号,如图2-2所示。

图2-2 Node.js的版本号

接着,如果输入另一个命令:

npm -v

我们将看到随Node.js一同安装的npm的版本号,如图2-3所示。

图2-3 npm的版本号

如果成功地执行了以上命令,那么说明读者已经成功地将Node.js和npm安装到了自己的设备中,并且Node.js和npm的路径也成功地被添加到系统环境变量PATH中;反之,读者需要尝试重新安装。


 

说明  

随着时间的推移,所安装的Node.js和npm的版本可能会高于以上版本。只要下载的Node.js是最新的长期支持版本,通常都不会有问题。但如果在后面的开发中遇到npm版本过低的问题,就应使用命令“npm install -g npm”,将npm更新到最新版本。


如果读者在通过以上命令更新npm时遇到问题,那么需要移除npm的安装目录(C:\Users\用户名\AppData\Roaming\npm),并重新安装Node.js。

由于npm的官方在线仓库部署在国外,因此我们并不能在任意时间都可以成功地连接上它。所幸的是,淘宝维护了一个npm在线仓库的镜像。为了保证npm的正常工作,我们需要继续在“命令提示符”窗口中输入并执行以下两条命令:

npm set registry https://registry.npm.taobao.org
npm get registry

第一条命令的作用是将本地安装的npm在线仓库URL设置为淘宝提供的镜像网址,第二条命令的作用是查看这一设置是否成功。在这两条命令成功地执行完成之后,我们将看到与图2-4所示的一致的结果。

图2-4 设置npm镜像

从图2-4中的最后一行可知,镜像设置是成功的,接下来我们便可以通过npm来查看并安装Node.js包了。

首先让我们从查看开始,继续在“命令提示符”窗口中输入并执行以下npm命令:

npm view jquery

命令中的参数view代表的是所要执行的操作,即查看npm在线仓库中指定名称的Node.js包;参数jquery则是所要查看的Node.js包的名字。

如果不出意外的话,执行以上命令之后,“命令提示符”窗口显示的内容将与图2-5所示的一致。

图2-5 通过npm查看在线仓库中的Node.js包

至此,我们便完成了Node.js和npm的安装,并对它们有了一个初步的了解。接下来,我们可以基于Node.js开发一个Web应用程序,通过npm安装基于Node.js的TypeScript编译器,但在此之前,我们需要先获得一个编辑应用程序代码的代码编辑器。

好的跨平台代码编辑器有很多,比如Atom、Sublime Text和WebStorm等,它们任何一个都是很好的一款产品,所以我们可以选择Visual Studio Code。

不同于Visual Studio,Visual Studio Code(后文简称VS Code)是一款免费、开源且跨平台的轻型代码编辑器。使用VS Code,我们可以快速、方便地编写几乎所有主流语言的代码,而我们将要用的TypeScript就是其中之一。

接下来,我们从VS Code的官网下载它的安装文件,然后将它安装到自己的开发设备中。


 

说明  

VS Code支持32位和64位的Windows、Linux以及macOS。本书以64位的Windows作为演示平台,如果读者打算在其他平台下使用VS Code,那么所需要做的操作可能会与本书提供的案例略有不同。


1.安装Visual Studio Code

VS Code安装包的大小目前只有40MB,如果读者希望将它安装到系统盘,那么经过短暂的下载等待之后,双击下载好的安装包并单击安装程序中的所有“下一步”按钮,就可以完成安装。

完成安装之后,读者可以选择立即启动VS Code,或关闭安装程序然后从Windows的开始菜单启动它。如果读者在安装的时候选择了不将VS Code的快捷方式添加到Windows的开始菜单,那么还可以从它的安装目录C:\Program Files\Microsoft VS Code\bin找到它的启动文件Code.exe,然后双击启动它。

成功启动之后,VS Code首次启动后的界面将与图2-6所示的一致。

图2-6 VS Code首次启动后的界面

图2-6中的主要区域是VS Code的欢迎页面,我们在此可以进行一些快速的操作和设置。欢迎页面上方的是几乎所有代码编辑器都有的工具栏,欢迎页面左侧的是VS Code的活动栏,其中列举的是开发中常用的工具,比如资源管理器和搜索工具等。

与了解VS Code中的各种工具相比,现在我们更乐意做的一件事情可能就是更改其主题。单击欢迎页面右侧“自定义”区域的“颜色主题”,弹出主题列表,如图2-7所示。

图2-7 VS Code颜色主题列表

从这个列表中选择读者喜欢的主题,或通过选择列表底部的“安装其他颜色主题”选项来安装更多在线主题。接下来,创建一个Visual Studio Code项目。

2.创建Visual Studio Code项目

不同于Visual Studio或Eclipse,为了保持轻量,VS Code本身没有项目的概念,取而代之的是我们日常使用的文件系统中的文件夹(目录)。在用VS Code打开文件系统中的一个文件夹时,我们就认为打开了一个VS Code项目;而创建一个VS Code项目,事实上就是在文件系统中创建一个文件夹。

因此,为了创建一个VS Code项目,我们需要在Windows资源管理器中找到一个合适的目录,然后在其中为本书创建一个工作目录,比如E:\Work\AngularAppDev。

接着,我们需要在这个工作目录下为本节创建一个项目目录Chapter2-1。然后,我们回到VS Code中,单击其左侧活动栏顶端的“资源管理器”图标,接着单击被打开的资源管理器面板中的“打开文件夹”按钮,随后我们将看到图2-8所示的“打开文件夹”对话框。

图2-8 在VS Code中打开文件夹

从中找到并选中刚刚创建的项目目录“E:\Work\AngularAppDev\Chapter2-1”,然后单击“选择文件夹”按钮。随后我们将看到VS Code将这个目录作为项目打开,并将其显示到资源管理器中,如图2-9所示。

图2-9 打开的VS Code项目

图2-9中的“CHAPTER2-1”就是我们创建的第一个VS Code项目,其右侧的四个图标(这四个图标仅在鼠标指针悬停在VS Code资源管理器中时才会出现)分别用于为当前项目新建文件、新建文件夹、刷新项目内容以及收起项目下所有被展开的文件夹。

至此,我们便完成了一个VS Code项目的创建。但如果就此结束了本节的内容,或许会让读者意犹未尽,为此让我们通过编写一段TypeScript代码来结束本节的学习。

3.编写TypeScript代码

单击VS Code资源管理器中项目名右侧的“新建文件”图标,项目名的下方会出现一个输入框,如图2-10所示。

图2-10 为项目新建文件


 

提示  

也可以通过右击项目名,然后在弹出的快捷菜单中选择“新建文件”命令来创建新的文件。


在输入框中输入文件名“hello-world.ts”(.ts是TypeScript文件的扩展名)并按回车键,将这个文件添加到项目中,并将其打开至可编辑状态,如图2-11所示。

图2-11 新建的TypeScript文件

最后,我们可以尝试向打开的hello-world.ts输入一段TypeScript代码,如代码清单2-1所示。

代码清单2-1 hello-world.ts

function helloWorld() {
   console.log('hello, world');
}

以上是一个JavaScript函数,但TypeScript兼容JavaScript的语法,因此它也是合法的TypeScript代码。

当然,除了我们熟悉的JavaScript语法,TypeScript还引入了JavaScript中没有的众多特性,比如类型检查、接口和装饰器等。但这些特性无法获得JavaScript运行时的支持,因此我们需要把TypeScript代码编译成原生的JavaScript代码,而执行这一编译工作的则是TypeScript编译器。


 

提示  

当在TypeScript文件中输入关键字、变量名和对象成员等标识符时,我们可以通过Tab键或回车键来快速完成输入。我们还可以单击VS Code头部的“帮助”工具,然后在弹出的菜单中选择“交互式演练场”和“快捷键参考”等选项,从打开的网页或视图中了解VS Code提供的更多快捷功能。


尽管TypeScript是一门编程语言,但是微软并未打算为其开发相应的运行时,因此TypeScript代码是无法被直接运行的。为了使所编写的TypeScript代码可以被“运行”,我们需要将它编译成可以直接运行的JavaScript代码。


 

说明  

根据TypeScript代码生成JavaScript代码的过程被称为Transpiling,而由于TypeScript和JavaScript都属于高级语言,因此Transpiling严格来说应该被翻译成“转译”。但人们已经习惯了“编译”一词,因此我们仍然将这个过程称为编译(Compiling)。


如果读者用Visual Studio编写过TypeScript代码,就一定会为其中的一个功能而惊叹,那就是当读者保存正在被编辑的TypeScript代码时,Visual Studio会自动将它编译成对应的JavaScript代码。

作为Visual Studio的跨平台轻型版本,VS Code自然也不会让我们失去这个体验。只是为了保持轻量,VS Code并没有内置TypeScript编译器,如果要获得这一体验,我们还需要将TypeScript编译器安装到自己的设备中。

1.安装TypeScript编译器

在本节开头我们已经知道,TypeScript编译器是基于Node.js实现的。更准确地说,TypeScript编译器是一个运行在Node.js上的应用程序(Node.js包),因此我们可以通过npm命令将它安装到我们的开发设备中。

之前在执行npm命令时,我们总是要打开Windows命令提示符或其他平台下的终端,但现在有了VS Code之后,我们不再需要这么做了,因为VS Code对它们进行了集成。

回到VS Code,单击其菜单栏中的“查看”选项,然后在弹出的下拉菜单中选择“集成终端”命令,随后VS Code将打开一个集成终端面板,如图2-12所示。

图2-12 VS Code集成终端


 

提示  

如果不喜欢使用工具栏,读者也可以通过组合键“Ctrl + `”(键盘中Esc键下方、Tab键上方的那个键)打开或关闭VS Code的集成终端。


图2-12所示的是VS Code集成的Windows PowerShell(可被理解为Windows命令提示符的升级版本),如果读者使用的是非Windows平台,那么将看到特定于自己所处平台的终端。

不管所处的是什么平台,接下来我们都可以放心地通过这个平台下被集成到VS Code中的终端来进行TypeScript编译器的安装。

只是在这么做之前,我们需要先知道一件事,那就是使用npm命令安装Node.js包时,我们有两个选择:一是将其安装到npm在当前设备中的全局安装目录中;二是将其安装到当前正在开发的项目当中。

以上两个选择都可以实现项目中TypeScript文件的编译,但由于TypeScript编译器是一个通用的应用程序,接下来将要开发的所有项目都将通过它来编译项目中的TypeScript代码,因此我们将它安装到全局安装目录中。

为了实现这样的安装,我们需要在VS Code集成终端输入以下npm命令:

npm install -g typescript

我们给npm命令提供了三个参数:

(1)install,固定参数,通知npm安装Node.js包;

(2)-g,固定参数,通知npm将目标Node.js包安装到全局安装目录中,如果不提供这个参数,目标Node.js包会被安装到当前项目中;

(3)typescript,目标Node.js包的名字,不能包含大写字母。

在执行以上命令的过程中,我们将在其下方看到一个进度条,如图2-13所示。

稍等片刻,进度条会在达到100%时消失,此时我们将看到图2-14所示的反馈。

图2-13 TypeScript编译器的安装过程

图2-14 安装TypeScript编译器

从图2-14所示的反馈可以知道,版本为3.2.1的TypeScript编译器已经被成功地安装到了npm全局安装目录(C:\Users\29486\AppData\Roaming\npm,这个目录会随设备的变化而变化)下。

随后我们可能会关闭VS Code集成终端,在这种情况下,如果我们打算查看TypeScript编译器是否已经被安装到全局安装目录中,就可以再次打开VS Code集成终端,输入并执行以下npm命令:

npm ls -g typescript

以上命令与安装TypeScript编译器的命令几乎是一致的,除了其中第二个被换成了ls(list的缩写,表示列举的意思)。

执行以上命令之后,VS Code集成终端将列出TypeScript编译器的安装路径和版本,如图2-15所示。

图2-15 查看TypeScript版本

至此,我们便完成了TypeScript编译器的安装。接下来,回到2.1.2 节的hello-world.ts中,并完成它的编译。

2.手动编译TypeScript代码

让我们再次打开VS Code集成终端,输入并执行以下tsc命令:

tsc hello-world.ts

以上命令不会输出任何内容,但如果我们留意VS Code的资源管理器,就会发现项目中多了一个名为“hello-world.js”的JavaScript文件。如果我们打开这个文件,将会看到图2-16所示的内容。

图2-16 hello-world.js的内容

这个名为“hello-world.js”的JavaScript文件以及其中的内容是从哪儿来的呢?相信聪明的读者已经有了答案—— 它是由同名的TypeScript文件hello-world.ts编译而来的,而这一编译动作就是由前面的tsc命令触发的。

此时读者应该已经知道,tsc命令就是调用TypeScript编译器的命令,其作用是将指定的TypeScript文件编译成同一路径下的同名JavaScript文件。

对比这两个文件的内容,我们会发现它们是一样的,如果向hello-world.ts中添加一些代码并再次对它进行编译,那么hello-world.js中的内容还与之保持一致吗?

不管答案是什么,TypeScript代码的编译都会有些麻烦,因为每编译一次,我们都需要执行一次tsc命令。

好在这种情况不会一直持续下去,因为VS Code可以帮助我们自动编译项目中的TypeScript代码。


 

说明  

tsc命令是随TypeScript编译器一起安装到npm全局安装路径下的,图2-14包含tsc的那条路径就是tsc命令所在的位置。从图2-14中我们还能发现,随TypeScript编译器一起安装的还有另外一个命令tsserver。VS Code和其他代码编辑器会通过这个命令来获得TypeScript编译器的支持,非代码编辑器开发人员很少用到这个命令。

此外,将TypeScript文件编译成同一目录下的同名JavaScript文件是TypeScript编译器的默认行为,我们可以通过向tsc命令传递outDir和outFile参数来修改这一默认行为。但在开发过程中,这一默认行为能给我们带来很多便利,因此本书不会对此进行修改。

最后,目前TypeScript编译器会认为hello-world.ts中重复定义了函数helloWorld(),但随后我们会消除这一误会。


3.自动编译TypeScript代码

默认情况下,VS Code不会自动编译项目中的TypeScript代码。为了使项目中的TypeScript代码获得自动编译的支持,我们需要在项目中新建一个名为tsconfig.json的JSON文件,然后向其中输入一个空白的JSON对象。


 

注意  

这个空白JSON对象是必需的,如果忽略了这个对象(使tsconfig.json中没有任何内容),随后VS Code会报“找不到当前项目的tsconfig.json文件”的错误。


执行以上操作之后,当前项目的文件结构将与图2-17所示的一致。

图2-17 TypeScript项目配置文件tsconfig.json

上述JSON文件tsconfig.json是TypeScript项目的配置文件,稍后我们将通过这个文件来配置当前项目的编译选项,但现在只需要知道VS Code发现当前项目中有这个文件时,会为当前项目创建两个任务,即“监视”和“构建”。


 

说明  

除了以上两个任务的创建,tsconfig.json的加入也会使TypeScript编译器不再认为hello-world.ts重复定义了函数helloWorld()。


接下来,让我们通过组合键“Ctrl + S”来保存tsconfig.json的编辑,然后单击VS Code工具栏中的“终端(T)”选项,接着在弹出的下拉菜单中单击“运行任务(R)”命令。随后,VS Code将弹出一个包含以上两个任务的任务列表,如图2-18所示。

图2-18 VS Code任务列表

单击任务列表中的“tsc:构建-tsconfig.json”任务时,VS Code将对当前项目进行构建,即通过刚刚安装的TypeScript编译器编译当前项目下的所有TypeScript文件;单击其中的“tsc:监视-tsconfig.json”任务时,VS Code将执行对当前项目的“监视”任务,即通过TypeScript编译器监视当前项目的配置文件tsconfig.json和所有TypeScript文件的保存动作。

在监视过程中,如果tsconfig.json文件有修改并被予以保存,那么TypeScript编译器仍然会编辑当前项目中的所有TypeScript文件;如果某个TypeScript文件有修改并被予以保存,那么TypeScript编译器则只会编译这个TypeScript文件—— 这就是所谓的自动编译

通常我们会在开发过程中使TypeScript编译器的监视任务处于执行状态,因此现在要做的就是在任务列表中单击这一任务以开启它。而在这一任务开启之后,VS Code将打开一个新的集成终端,如图2-19所示。

图2-19 TypeScript监视任务

图2-19所示的“Starting compilation in watch mode”表示VS Code已开始通过TypeScript编译器对当前项目进行一次完整的编译;“Fund 0 errors. Watching for file changes.”表示TypeScript编译器已经完成了这次完整的编译,并开始了监视项目中的文件的修改,以在监视到文件的修改时自动对其进行编译。

接下来,为了体验自动编译的功能,让我们在hello-world.ts的底部再添加一行代码,使其完整内容与代码清单2-2所示的一致。

代码清单2-2 hello-world.ts

function helloWorld() {
   console.log('hello, world');
}
helloWorld();// 添加对函数helloWorld()的调用


 

说明  

本书将列出实例中的所有代码,并加粗发生了修改的代码。当代码过长时,本书将以省略号(...)代替多次出现的代码,但保留加粗显示的修改代码。


接着保存对以上代码的编辑,随后我们将看到VS Code集成终端中输出相应的编译提示,如图2-20所示。

图2-20 TypeScript自动编译提示

图2-20所示的第一条信息告诉我们TypeScript编译器监测到了项目中TypeScript文件的修改,并开始了对它的编译;第二条信息我们已经见过了,即TypeScript编译器已经完成了以上编译,并将继续监视项目中tsconfig.json和TypeScript文件的修改。

作为编译结果,hello-world.js的内容将被自动更新,如代码清单2-3所示。

代码清单2-3 hello-world.js

function helloWorld() {
    console.log('hello, world');
}
helloWorld();// 添加对函数helloWorld()的调用

聪明的读者或许已经预料到了这一结果,并同样可以预料以上代码的执行结果,即向控制台输出一条经典的问候信息:

hello, world

那么,怎样才能使以上代码得到执行呢?我们可以把它加载到浏览器中。但有了Node.js之后,我们可以暂时先不考虑浏览器,因为使用Node.js加载并执行JavaScript代码更加方便。

在Node.js、TypeScript编译器和VS Code的帮助下,我们还能方便地调试TypeScript(以及JavaScript)代码。


在开始调试TypeScript代码之前,让我们先来看看如何通过Node.js快速地执行JavaScript代码。为此,我们需要单击VS Code集成终端右上角的“新的终端”图标,让VS Code打开一个新的集成终端,如图2-21所示。

图2-21 打开新的VS Code集成终端

接着在新打开的终端中输入并执行以下node命令:

node hello-world.js

以上命令首先会加载Node.js运行时,接着Node.js运行时会加载当前项目下的JavaScript文件hello-world.js,并开始它的执行。


 

说明  

node命令的第一个参数是需要被执行的JavaScript文件的路径,由于VS Code集成终端当前的工作路径就是当前项目所在的路径(即E:\Work\AngularAppDev\Chapter2-1),因此当执行当前项目中的JavaScript文件时,我们可以使用其相对于项目路径的相对路径。


当以上命令执行完时,hello-world.js中的函数helloWorld()会被调用,并向当前终端输出我们期待已久的问候,如图2-22所示。

图2-22 在Node.js中执行JavaScript代码

是不是很方便?只需要一条命令,就使指定的JavaScript代码得到了执行。

事实上,在VS Code中调试JavaScript代码也是这么方便。为了证明这一点,让我们在编辑器中打开hello-world.js,然后单击行号4左侧的空白区域,随后我们将看到被单击的地方会出现一个调试断点,如图2-23所示。

图2-23 给JavaScript文件添加调试断点


 

注意  

确保hello-world.js处于被打开状态很重要,因为当启动调试时,VS Code会默认启动被打开的(JavaScript)文件的调试。


接着,按下键盘上的F5键(或单击VS Code工具栏中的“调试(D)”选项,然后从弹出的菜单中选择“启动调试(S)”命令),此时VS Code将自动加载Node.js运行时,然后在Node.js中加载并执行hello-world.js。

另外,由于我们在hello-world.js的第四行添加了调试断点,因此VS Code会使hello-world.js的执行停在第四行,如图2-24所示。

图2-24 调试JavaScript代码

可以看到,除了将代码的执行停在了断点处,VS Code还打开了调试面板和调试动作工具条。我们可以通过调试面板查看当前作用域下的变量以及调用堆栈等,通过调试动作工具条则可以对正在调试的代码进行单步跳过、单步调试和单步跳出等操作。

比如,接下来如果我们单击调试动作工具条中的“单步调试”图标(或按下键盘上的F11键),那么将进入函数helloWorld()的内部,然后开始函数helloWorld()内容代码的调试;而如果我们单击的是其中的“单步跳过”图标(或按下键盘上的F10键),那么将直接跳过函数helloWorld()的执行,等等。

除了调试面板和调试动作工具条,VS Code底部还显示了一个调试控制台。这个控制台和浏览器调试工具的控制台类似,调试期间代码中的console.log()函数输出的内容会被输出到其中,而其底部被输入的表达式和语句也会在调试期间得到执行。

为了节省时间,此处不一一演示调试动作工具条和调试控制台的用法。接下来,我们的任务是掌握如何在VS Code中调试TypeScript代码。为了执行这一任务,我们需要打开当前项目中唯一的TypeScript文件hello-world.ts,然后再次按下键盘上的F5键,随后我们将看到不幸的一幕:VS Code没有开启hello-world.ts的调试,而是抛出一个错误,如图2-25所示。

图2-25 因缺少映射文件而无法调试TypeScript代码

图2-25所示的错误信息告诉我们,之所以无法顺利启动hello-world.ts调试,是因为与其对应的JavaScript文件(编辑结果)未被找到。


 

说明  

图2-25中有一个“打开launch.json”的提示,但单击这个提示并不能解决以上问题,随后我们会看到这一提示的作用。


此时读者或许会有两个疑问:一是VS Code为什么要找这么一个文件?二是这个文件不就是hello-world.js吗?为什么会找不到它呢?

对于第一个疑问,答案是:因为TypeScript代码是无法被直接运行的,所以当我们试图通过VS Code调试TypeScript代码时,VS Code必须找到包含目标TypeScript代码的编译结果的JavaScript文件,然后把它加载到Node.js中去运行。

对于第二个疑问,答案是:hello-world.js确实是hello-world.ts的编译结果文件,但这件事情只有TypeScript编译器和开发人员自己知道,VS Code对此其实“一无所知”。

因此,若要解决上述问题,我们必须让VS Code“知道”hello-world.js就是hello-world.ts的编译结果文件。为了实现这一目的,我们需要修改当前TypeScript项目的配置文件tsconfig.json,使其内容与代码清单2-4一致。

代码清单2-4 tsconfig.json

{
 "compilerOptions": { // 编译器选项
 "sourceMap": true // 生成映射文件
 }
}

以上修改添加了TypeScript编译器选项属性compilerOptions,这个属性的值是对象,而这个对象的属性将影响TypeScript编译器对当前项目中的TypeScript文件的编译。

比如,代码中的源文件映射属性sourceMap,它将使TypeScript编译器在对当前项目中的TypeScript文件进行编译时,为它们各生成一个扩展名为.map的映射文件。

为了验证这一点,让我们保存以上代码的编辑,然后执行一次当前项目的“构建”任务,随后我们将在当前项目中看到一个名为hello-world.js.map的文件,如图2-26所示。

图2-26 映射文件


 

说明  

TypeScript编译器只会监听tsconfig.json中的大部分属性的修改,而不会监听其sourceMap属性和outDir属性等的修改,因此在保存以上修改之后,我们还需要执行一次“构建”任务,以使hello-world.ts得到编译。


图2-26所示的hello-world.js.map就是映射文件,我们目前无须关心这个映射文件的太多细节,只需要知道通过其sources属性、sourceRoot属性及file属性,VS Code就能找到与源TypeScript文件对应的那个编译结果文件。


 

说明  

映射文件所在的目录总是和对应的编译结果文件一样,其名字总是在对应的编译结果文件的名字后面加一个.map扩展名。

另外,随后我们也会发现,映射文件也是TypeScript文件在浏览器环境中调试的基础。


也就是说,有了映射文件hello-world.js.map之后,VS Code就能通过它找到hello-world.ts的编译结果文件hello-world.js,从而使hello-world.js被加载到Node.js中,进而使hello-world.ts的调试变得可能。

那么,事实是否是这样呢?让我们将hello-world.ts切换到打开状态,然后按键盘上的F5键,随后我们将再次看到熟悉的调试面板和调试控制台,如图2-27所示。

图2-27 调试TypeScript代码

可以看到,尽管被加载到Node.js中的仍是hello-world.js,但现在被调试的却是hello-world.ts,并且hello-world.js中的断点还对hello-world.ts的调试产生了影响,从而使调试停在了hello-world.ts中对函数helloWorld()的调用这一行。

此外,图2-27左侧还展示了一些列功能强大的调试面板,比如列举当前调试上下文中所有变量的“变量”面板、列举当前函数被调用之前被调用过的所有函数的“调用堆栈”面板和允许我们监视指定变量的“监视”面板等。

根据图2-27的内容,我们可以知道接下来hello-world.ts的调试过程和hello-world.js的调试过程会是一样的,故不再赘述。

接下来,让我们考虑一个更实际的问题,即程序往往不是由一个代码文件组成的,当应用程序中有多个TypeScript文件时,它们是如何协同工作的,我们又该如何对它们进行调试呢?

为了回答这个问题,让我们先对hello-world.ts中的内容做些修改,使其与代码清单2-5一致。

代码清单2-5 hello-world.ts

export function helloWorld() {
   console.log('hello, world');
}

我们删除了代码段最后一行对函数helloWorld()的调用,然后在函数关键字function的左侧添加了一个标识符export。这将使当前TypeScript文件成为一个TypeScript模块(Module),并使其中的函数helloWorld()被导出为公共函数。


 

说明  

模块是TypeScript、Node.js和JavaScript(ECMAScript 2015)中极为重要的概念,本章2.2节将介绍它们的来源、作用和区别,而第3章和第5章还将详细介绍TypeScript中的模块。


接着,让我们在当前项目下新建一个名为index.ts的TypeScript文件,并将代码清单2-6复制到其中。

代码清单2-6 index.ts

import { helloWorld } from './hello-world'; //导入模块hello-world(.ts)内的公共函数helloWorld
debugger;     // 断点标识符
helloWorld(); // 调用helloWorld()函数

其中第一行的标识符import的作用和代码清单2-5的export的作用正好相反,即从当前目录下的TypeScript模块(文件)hello-world.ts(作为模块,扩展名.ts应该被省略)中导入其公共函数helloWorld(),并同时使当前TypeScript文件成为另一个TypeScript模块。

接下来,由于已经导入了函数helloWorld(),当前模块便可以名正言顺地调用它。为了验证这一点,让我们保存所有代码的编辑,然后在合适的VS Code集成终端中输入并执行以下node命令:

node index.js


 

注意  

以上命令不能写成“node index.ts”,因为TypeScript文件不能运行在Node.js中。但这一命令可以被简写成“node index”,因为node命令的目标文件的.js扩展名是可以省略的。


随后,当前终端输出熟悉的问候“hello, world”,如图2-28所示。

图2-28 执行多个文件

从代码中可以看到,TypeScript编译器自动对新增的index.ts(和修改后的hello-world.ts)进行了编译,并为其生成了编译结果文件和映射文件。终端输出问候“hello, world”,则是因为编译结果index.js和hello-world.js先后被加载到了Node.js中,并且hello-world.js中的函数helloWorld()最终被index.js调用了—— 一个复杂的TypeScript应用程序就是基于这样的雏形构建的。

除了探究复杂应用程序的创建,我们的另一个目的是掌握其中多个TypeScript文件的调试,因此接下来我们需要保证index.ts处于打开状态,然后按键盘上的F5键。随后,VS Code将会开始index.ts的调试,并将代码执行停留在其中的断点标识符debugger所在的第二行,如图2-29所示。


 

注意  

如果读者刚刚在尝试调试hello-world.ts时单击过VS Code弹出的“打开launch.json”提示,那么需要在调试index.ts之前删除当前项目下的.vscode文件夹,因为它会使调试总是从hello-world.ts开始调试。


图2-29 调试多个TypeScript文件(一)

接着,我们可以按下F10键跳过图2-29中的断点标识符,然后调用下一行的函数helloWorld(),并按下F11键进入hello-world.ts的函数helloWorld()中,如图2-30所示。

图2-30 调试多个TypeScript文件(二)

此时,我们可以按F5键继续执行剩余的代码,待执行完成之后,我们将再次在调试控制台看到熟悉的问候“hello, world”,如图2-31所示。

图2-31 调试多个TypeScript文件(三)


 

注意  

虽然我们可以在TypeScript文件的编译结果文件中添加断点或断点标识符,但修改编译结果文件这种做法是不被推荐的,因为这些修改很容易被TypeScript编译器覆盖。

另外,相比于断点,断点标识符debugger显得更加可靠。因为当TypeScript被加载到浏览器上时,在VS Code中添加的断点无法被传递到浏览器上,但断点标识符可以。

至此,我们便知道了如何通过export和import来构建一个复杂TypeScript应用程序的雏形,并掌握了对其中多个TypeScript文件的调试方法。

接下来,让我们再考虑另一个实际的问题,即我们将基于Angular创建的都是Web应用程序,那么用于编写Angular应用程序的TypeScript文件怎样才能“运行”在浏览器中?


 

说明  

我们第一次尝试调试hello-world.ts时,VS Code报了一个无法找到对应的hello-world.js的错误,并给出了一个“打开launch.json”的提示。如果我们单击这个提示,VS Code会为当前项目创建一个名为.vscode的文件夹,并在其中创建一个名为launch.json的JSON文件。

这个文件中的以下四个属性会影响VS Code的调试行为。

(1)"type": "node"—— 表示在Node.js运行时上调试程序。

(2)"request": "launch"—— 表示以启动应用程序的方式开始调试程序。

(3)"program": "${workspaceFolder}/hello-world.ts"—— 表示要启动已调试的应用程序是项目根目录下的TypeScript文件hello-world.ts。

(4)"outFiles": ["${workspaceFolder}/**/*.js"]—— 表示相应的输出文件(要加载到Node.js运行时中运行的文件)是项目根目录或其任意子目录中与以上启动文件同名的JavaScript文件,即hello-world.js。

有了以上文件,不管我们当前打开的是哪个TypeScript文件,按下键盘上的F5键时,被调试的都是hello-world.ts。

为了阻止这样的事情发生,我们可以修改以上文件中的program属性的值,或直接删除这个文件。本书选择的是直接删除方式。


Web应用程序离不开Web服务器,因为浏览器需要从Web服务器中加载必需的HTML、JavaScript、CSS文件和图片等资源。因此,在继续后面的内容的学习之前,我们需要准备好一个Web服务器,并且最好是一个跨平台的Web服务器—— 比如Node.js。

作为一个Web服务器,Node.js提供了一些基础的HTTP和文件I/O API,但这些API使用起来有些烦琐,为此我们需要一个基于Node.js的Web应用程序开发框架,这个框架就是Express。

作为一个基于Node.js的Web应用程序开发框架,Express继承了Node.js的优点,即跨平台、轻型且功能强大。通过Express提供的 API,我们能快速地开发出一个功能丰富的Web应用程序或Web API。

由于是基于Node.js实现的,因此Express也是一个Node.js包,并且也可以通过npm命令被安装到开发设备中。接下来,我们要做的就是安装Express。

1.安装Express

打开VS Code集成终端,输入并执行以下命令:

npm install express

以上命令和2.1.4节用于安装TypeScript编译器的命令几乎是一致的,不同的是我们省略了其中的-g参数,然后将目标Node.js包的名字替换成了Express的包名,即express(不能写成Express,因为Node.js包名中不能包含大写字母)。

当以上命令执行完时,VS Code集成终端会输出一些警告信息,并显示安装成功的Express的版本,如图2-32所示。

图2-32 安装Express 

忽略图2-32中的警告信息,我们将看到包括Express在内,共成功安装了48个Node.js包,其中47个是Express所依赖的第三方Node.js包。


 

说明  

图2-32中显示的Express的版本是4.16.4,随着时间的推移,Express的版本可能会升级,并且所依赖的其他Node.js包的数量也可能会发生改变。

另外,如果要查看被安装到当前项目下的Express的版本,我们也可以在VS Code集成终端中输入并执行npm命令:npm ls express。


那么,我们可以在哪里找到Express及其所依赖的第三方Node.js包呢?答案是在当前项目中。由于我们刚刚省略了Express安装命令中的参数-g,因此Express的安装目录不再是npm的全局安装目录,而是npm在当前项目中的本地安装目录。

如图2-32左侧所示,当前项目下多了一个名为node_modules的文件夹,而这个文件夹就是npm为当前项目创建的本地安装目录,展开它,我们将看到大量的子文件夹,其中名为express的子文件夹就是Express的安装目录,其他子文件夹则是Express所依赖的第三方Node.js包的安装目录。

另外,我们还能发现当前项目中还多了一个名为package-lock.json的JSON文件,这个文件是npm自动生成的Node.js包描述文件,用于描述npm本地安装目录(即当前项目中的node_modules文件夹)中各个Node.js包的版本及依赖等信息。

除了package-lock.json,我们在2.2节中还将接触一个类似的Node.js包描述文件,即package.json。通过这个文件我们可以批量地安装Node.js包,并消除图2-32中的警告信息。但在这么做之前,我们先尝试通过Express创建一个基于Node.js的跨平台Web应用程序。

2.创建Web应用程序

在2.1.1节中,我们已经知道Node.js是JavaScript的一个运行时,所有运行在Node.js上的应用程序都是用JavaScript编写的,这对我们要创建的Web应用程序来说也不是例外。

因此,接下来我们要做的就是从创建一个JavaScript文件开始—— 当然,我们也可以创建一个TypeScript文件,然后将它编译成JavaScript文件,但现在来讲,创建一个JavaScript文件可能更加方便一点。

在VS Code资源管理器中选中当前项目,为其新建一个名为server-app.js的JavaScript文件。执行这一操作之后,server-app.js在项目中的位置如图2-33所示。

图2-33 server-app.js在项目中的位置

接下来,将代码清单2-7复制到server-app.js中。

代码清单2-7 server-app.js

var express = require('express'); // 导入Express模块
var app = express();              // 创建基于Express的服务端应用程序

// 处理客户端发起的路径为/的HTTP GET请求
app.get('/', function (req/*请求对象*/, res/*响应对象*/) { // 请求处理函数
    // 向客户端发送字符串响应
    res.send('hello, world');
});

// 让应用程序监听3000端口上的HTTP请求
var server = app.listen(3000, function () { // 监听开始回调函数
    var host = server.address().address;    // 请求的主机名
    var port = server.address().port;       // 请求的端口

    // 向服务端控制台输出应用程序启动成功提示
    console.log('当前应用程序正在监听http://%s:%s', host, port);
});

以上JavaScript代码是一个Node.js模块,其中的第一行通过Node.js内置的require()函数导入了另一个模块,即Express框架中的主模块express。通过express模块,第二行代码创建了一个服务端应用程序对象,然后将这个对象的引用赋给了变量app。

接着,我们调用了服务端应用程序(即变量app)的get()函数,使服务端应用程序接收客户端发起的路径为/的、HTTP方法为GET的请求,并在接收到这一请求时向客户端发送字符串响应,即“hello, world”。

最后,我们调用了变量app的listen()函数,使服务端应用程序开始启动并监听当前设备的3000端口,然后在启动成功时向服务端控制台输出相应的提示信息。

接下来的问题是,如何使以上代码得到执行,从而启动所创建的应用程序呢?答案是使用node命令,因为以上代码已经是一个Node.js应用程序了。

打开一个新的VS Code集成终端,然后输入并执行以下node命令:

node server-app.js

以上命令会加载Node.js运行时,并使Node.js运行时加载并执行JavaScript文件server-app.js中的代码,因此,在以上命令执行完成时,我们将在VS Code集成终端中看到以下信息:

当前应用程序正在监听http://:::3000

这一信息表示刚刚创建的Web应用程序已经启动成功,此时如果我们打开浏览器,在地址栏中输入“http://localhost:3000”并按回车键,就会看到服务端返回的“hello, world”,如图2-34所示。

图2-34 浏览http://localhost:3000


 

说明  

本书案例中所用的浏览器是谷歌的Chrome,所用的端口是3000和50424,但这些都不是强制的。


图2-34所示的效果再次证明,我们通过Express创建的Web应用程序已经运行成功。但目前这个应用程序还不具备太多实际的作用,接下来我们还需要对它做些扩展,使其能够响应静态资源的请求。

3.静态资源服务

在响应静态资源的请求之前,我们需要先有这么一个资源,为此我们可以为当前项目添加一个名为index.html的HTML文件,使其在项目中的位置与图2-35所示的一致。

图2-35 index.html在项目中的位置

接着,让我们将代码清单2-8复制到index.html中。

代码清单2-8 index.html

<html>
<head>
    <title>hello, world</title>
</head>
<body>
    hello, world
</body>
</html>

这是一段简单的HTML代码,它应向浏览器中显示熟悉的“hello, world”,但当我们试图通过浏览器请求http://localhost:3000/index.html时,得到的却是图2-36所示的结果。

图2-36 无法访问index.html

可以看到,图2-36中并没有显示我们所期待的“hello, world”,却显示了 Express输出的错误提示,即无法找到/index.html这个资源。

出现这一错误的原因是Express默认不支持客户端对静态文件的请求,为了覆盖这一默认行为,我们需要对server-app.js做些修改,使其内容与代码清单2-9一致。

代码清单2-9 server-app.js

var express = require('express'); // 导入Express模块
var app = express(); // 创建基于Express的服务端应用程序

// 将项目根目录设置为静态文件目录
app.use(express.static(_ _dirname));

// 让应用程序监听3000端口上的HTTP请求
var server = app.listen(3000, function () { // 监听开始回调函数
    var host = server.address().address;    // 请求的主机名
    var port = server.address().port;       // 请求的端口

    // 向服务端控制台输出应用程序启动成功提示
    console.log('当前应用程序正在监听http:     //%s:%s', host, port);
});

我们仅对server-app.js做了一处修改,即删除了其中app.get()函数的调用,将其替换成了app.use()函数的调用,并向这个函数提供了一个参数express.static(_ _dirname)。


 

说明  

_ _dirname(开头是两个英文下画线)是Node.js中的全局变量,其值是当前Node.js应用程序(server-app.js)所在的路径,即当前项目的根路径。

Express的app.get()方法可以轻松地实现对请求的路由,本书后面的内容还会用到它。


这一修改将使Express把当前项目的根目录当作Web应用程序的静态资源目录,从而使这个目录下的所有静态资源都可以通过它们的路径被访问。为了使这一修改起效,我们需要重启Web应用程序。

回到之前用于启动Web应用程序的VS Code集成终端,按组合键“Ctrl + C”以停止Web应用程序,然后再次输入并执行node命令“node server-app.js”以启动它。

重启后,回到浏览器中并再次请求http://localhost:3000/index.html,我们将看到Express成功返回的index.html的内容,如图2-37所示。

图2-37 成功响应Express服务静态资源的请求


 

说明  

和很多其他Web应用程序开发框架一样,Express会把名为index.html的HTML文件当作一个静态资源目录下的默认首页,所以此时如果我们继续浏览http://localhost:3000,就会得到与图2-37一致的内容。


至此,Web应用程序便具备了提供静态资源服务的功能。接下来,我们要做的是继续2.1.4节结束时留下的任务,即让我们编写的TypeScript代码运行到浏览器中。

当然,前面已经多次提到,TypeScript代码是无法直接被运行的,因此我们只能把由它们编译而来的JavaScript代码加载到浏览器中。

为了将这些JavaScript代码加载到浏览器中,我们需要再次修改index.html的内容,使其与代码清单2-10一致。

代码清单2-10 index.html

<html>
<head>
    <title>hello, world</title>
    <!—加载hello-world.js-->
    <script src="index.js"></script>
</head>
<body>
    hello, world
</body>
</html>

我们向index.html中添加了一个<script>标签,将这个标签的src特性设置为index.js,以使项目根目录下的index.js能够在index.html被加载到浏览器中之后也被加载到浏览器中。

前面我们将index.js加载到Node.js运行时中时,index.js中的代码调用了hello-world.js中定义的函数helloWorld(),因此我们可以想象当将index.js加载到浏览器中时,它也应该会以相同的方式调用hello-world.js中定义的函数helloWorld(),从而向浏览器控制台输出问候“hello, world”。

那么,事实是否就是这样呢?保存以上代码的编辑,然后刷新浏览器,我们将看到图2-38所示的加载失败页面。

图2-38 hello-world.js加载失败

我们之所以会遇到图2-38所示的错误,是因为index.js正在试图以某种方式将自己定义为一个模块,并同时将hello-world.js加载到浏览器中。但不幸的是,浏览器并不认同这种方式—— Node.js却认同这种方式。

那么,这究竟是怎样一种方式呢?我们将在2.2节将给出答案。

Ajax让JavaScript开始受到重视并被用于越来越多的Web应用程序的开发,但随后人们慢慢发现,使用JavaScript开发大型Web应用程序不是一件轻松的事。

为什么这么说呢?原因至少有两个:一是所有JavaScript代码都会共享一个全局的作用域,这使来自于不同组织的JavaScript代码很容易发生冲突和覆盖;二是为了便于测试、开发和维护,大型应用程序通常需要分成多个层(Tier)或类似的架构来开发,但使用JavaScript很难实现应用程序的分层,也很难使层与层之间轻易地进行引用。

以上这些问题在绝大部分其他主流语言中是不存在的,因为它们总是会通过命名空间(Namespace)或类似的概念来创建局部的作用域,并使这些命名空间既相互独立,又可以进行引用。

为此,人们开始尝试借鉴其他语言,将类似于命名空间的模块引入JavaScript中,并最终使它被写进了ECMAScript 2015规范中。在本节中,我们的任务就是了解模块的前世今生,从而为今后的学习奠定一个必备的基础。

在标准的模块被写入ECMAScript 2015中之前,其实人们已经在JavaScript中使用非标准的模块了,只是这种非标准的模块并非由ECMA国际组织官方定义,而是由一些热心的开发人员和社区“私下”定义的。由于热心的开发人员和社区都不止有一个,因此这种非官方定义的模块便有多种,而每一种都必须遵守其定义者提出的规范。

尽管定义模块的规范有很多,但它们都有一个相同的优点,即兼顾多个平台(比如浏览器和Node.js)或同一平台的多个版本(比如IE的多个版本)。因此本书将这些规范称为通用模块规范,而将遵循这些规范的模块称为通用模块。

接下来,就让我们看看热心的人们都提出了哪些通用模块规范。

1.通用模块规范

曾经出现过的通用模块规范有很多,其中有的已经不再活跃了,有的生命力正旺盛,而有的也很有可能在将来的某个时间淡出我们的视野—— 毕竟模块已经被写入了ECMAScript 2015中,并且有很多主流浏览器早已开始原生支持被写入ECMAScript 2015中的模块了。

因此,作为应用程序开发者的我们,根本不需要精通通用模块规范的所有具体细节,只需要知道它们的存在,以及它们是如何在ECMAScript 2015被广泛遵守之前,帮助我们在不同版本的不同平台上实现通用模块的加载的。

接下来,就让我们先来看看名声最为显赫的CommonJS。

(1)CommonJS。CommonJS不像jQuery,它不是一个代码库,我们无法将它加载到浏览器中。CommonJS是一个规范,它规定了怎样的JavaScript代码才是一个CommonJS模块,以及应该如何加载这样的模块。

当然,现如今我们也不需要学着如何编写CommonJS模块,因为TypeScript编译器可以为我们代劳。

打开2.1节的hello-world.ts的编译结果文件hello-world.js,我们将看到如代码清单2-11所示的内容。

代码清单2-11 hello-world.js

"use strict";
exports._ _esModule = true;
function helloWorld() {
    console.log('hello, world');
}
exports.helloWorld = helloWorld;
// helloWorld();// 添加对函数helloWorld()的调用
//# sourceMappingURL=hello-world.js.map

可以看到,在给hello-world.ts添加导出标识符export之前,其编译结果还仅包含一个函数helloWorld()的定义,但在添加导出标识符export之后,其编译结果就变成上面这个样子了。

这是为什么呢?因为添加了导出标识符export之后,hello-world.ts变成了一个TypeScript模块,TypeScript编译器默认将它编译成了一个CommonJS模块。也就是说,以上代码就是一个CommonJS模块,而其中的“exports._ _esModule = true”和“exports.helloWorld = helloWorld”就是CommonJS规范约定的写法,它们的作用是将当前CommonJS模块内定义的函数helloWorld导出为一个公共的函数。


 

说明  

代码底部的“//# sourceMappingURL=hello-world.js.map”不是CommonJS规范的要求,是TypeScript编译器根据tsconfig.json中的sourceMap属性的值为true生成的,用于VS Code和浏览器加载hello-world.js的源映射文件hello-world.js.map。

VS Code和浏览器会通过源映射文件hello-world.js.map加载相应的TypeScript文件hello-world.ts,并建立hello-world.js和hello-world.ts之间的函数和变量的映射关系,从而实现hello-world.ts在VS Code和浏览器中的调试。


既然有导出,那么是不是还有导入呢?如果我们继续打开index.ts的编译结果文件index.js,就将看到与代码清单2-12一致的内容。

代码清单2-12 index.js

"use strict";
exports._ _esModule = true;
// 从当前目录下导入模块hello-world(.ts)内的公共函数helloWorld
var hello_world_1 = require("./hello-world");
debugger; // 断点标识符
hello_world_1.helloWorld(); // 调用hello-world()函数
//# sourceMappingURL=index.js.map

可以想象,以上代码仍然遵循了CommonJS规范,因此,index.js也是一个CommonJS模块。而其中函数调用“require("./hello-world")”的作用就是将hello-world.js这个CommonJS模块导入当前CommonJS模块index.js中。

以上require()函数会执行目标模块hello-world.js中的代码,并返回一个对象给变量hello_world_1,同时将hello-world.js中导出的函数helloWorld()定义成这个对象的一个同名方法—— 因此,以上最后一行代码访问的就是hello-world.js中导出的函数helloWorld()。


 

说明  

require()函数的参数是需要被导入的CommonJS模块的路径,其扩展名.js可以被省略。


此时读者或许会有一个疑问,即以上两个CommonJS模块中的对象exports和函数require()从何而来?答案是由通用模块加载器(Module Loader)提供。


 

提示  

还记得之前我们试图在浏览器上运行index.js时,浏览器抛出的“exports is not defined”这个错误吗?这个错误就是因为通过<script>标签加载的index.js会被执行在JavaScript全局作用域中,而JavaScript全局作用域中根本没有exports对象(和require()函数)造成的。若要消除这个错误,我们需要让浏览器加载一个合适的通用模块加载器。


CommonJS约定:只要是CommonJS模块,就都可以使用exports对象来导出变量和函数等定义,并使用require()函数来导入其他CommonJS模块。但作为一个规范,CommonJS本身并不提供exports对象和require()函数,需要提供它们的是能够加载CommonJS模块的模块加载器。

模块加载器和jQuery一样,是一个JavaScript代码库,之前我们用来成功地执行了index.js的Node.js就内置了这样一个模块加载器,因此它可以很好地处理CommonJS模块的加载和其中的导入及导出。

随后我们会了解可以作用在浏览器中的模块加载器,但在此之前,让我们再花点时间来了解一些其他通用模块规范。

(2)AMD。CommonJS给模块规范起了一个好头,但仍有一些不足,其中最著名的就是它定义的模块加载方式不是异步的。为了弥补CommonJS的不足,人们在它的基础上制订了允许模块异步加载的规范,即异步模块定义(Asynchronous Module Definition,AMD)。

那么,我们应该如何编写AMD模块呢?答案仍然是我们无须自己编写,因为TypeScript编译器同样会代劳。

打开TypeScript项目配置文件tsconfig.json,将其内容修改至与代码清单2-13一致。

代码清单2-13 tsconfig.json

{
    "compilerOptions": {   // 编译器选项
        "sourceMap": true, // 生成映射文件
 "module": "amd" // 指定生成的JavaScript的模块规范
    }
}

我们仅对tsconfig.js做了一处修改,即为其编译器选项compilerOptions添加了一个名为module的属性,然后将这个属性的值设置成了amd。


 

说明  

tsconfig.json中的属性名是大小写敏感的,但(部分)属性的值不是,因此以上module属性的值也可以写成看上去更加正式的AMD。但由于VS Code给出的提示都是小写的,为了便于输入,本书选择了小写的amd。

基于同样的原因,随后我们还会看到module属性的值被设置为commonjs(而非CommonJS),以及同级的target属性的值被设置为es5(而非ES5)等情况。


让我们保存以上代码的编辑,等待TypeScript编译器完成对整个项目的编译,然后将index.js切换到编辑器中,我们会发现其内容已经发生了改变,如代码清单2-14所示。

代码清单2-14 index.js

define(["require", "exports", "./hello-world"], function (require, exports, hello_world_1) {
    "use strict";
    exports._ _esModule = true;
    debugger;                   // 断点标识符
    hello_world_1.helloWorld(); // 调用hello-world()函数
});
//# sourceMappingURL=index.js.map

以上就是由TypeScript生成的一个AMD模块,而它的由来,相信读者已经猜到,即当我们将当前项目的tsconfig.json中的module属性的值设置为amd时,TypeScript便会将项目中的TypeScript模块编译成AMD模块。

也就是说,如果读者有兴趣打开hello-world.js,就会发现它的内容也已经变成了一个AMD模块。但为了节省时间,此处便不再演示了。

接下来,让我们再来看看另一个通用模块规范。

(3)UMD

有了AMD之后,人们面临着一个问题,即同一个项目中可能会同时包含一部分(老的)遵循CommonJS规范的模块,以及另一部分(新的)遵循AMD规范的模块。这种情形会使模块的加载变得难以控制,因为遵循不同规范的模块需要使用遵循不同规范的模块加载器才能加载。

为了解决以上问题,人们又制订了通用模块定义(Universal Module Definition,UMD),使遵循UMD规范的模块加载器可以同时用于加载CommonJS模块和AMD模块。

同样,有了TypeScript编译器之后,我们也不用编写自己的UMD模块。我们只需要再次对当前项目的tsconfig.json进行修改,使其与代码清单2-15一致。

代码清单2-15 tsconfig.json

{
    "compilerOptions": {   // 编译器选项
        "sourceMap": true, // 生成映射文件
        "module": "umd"    // 指定生成的JavaScript的模块规范
    }
}

我们只对以上代码做了一处修改,即将其中module属性的值由之前的amd修改成了umd。

接下来,我们只需要保存以上代码的编辑,等待TypeScript再次完成整个项目的编译,然后再次将index.js切换到编辑器中,那时我们将看到其内容再次发生了改变,如代码清单2-16所示。

代码清单2-16 index.js

(function (factory) {
   if (typeof module === "object" && typeof module.exports === "object") {
        var v = factory(require, exports);
        if (v !== undefined) module.exports = v;
    }
    else if (typeof define === "function" && define.amd) {
        define(["require", "exports", "./hello-world"], factory);
    }
})(function (require, exports) {
    "use strict";
    exports._ _esModule = true;
    // 从当前目录下导入模块hello-world(.ts)内的公共函数helloWorld
    var hello_world_1 = require("./hello-world");
    debugger; // 断点标识符
    hello_world_1.helloWorld(); // 调用hello-world()函数
});
//# sourceMappingURL=index.js.map

以上是由TypeScript编译器生成的一个UMD模块,而它的由来,相信读者也早已猜到,即当TypeScript编译器发现当前项目的tsconfig.json中的module属性被设置为umd时,它会将项目中的所有TypeScript模块编译成UMD模块。

同样,如果打开hello-world.js,我们会发现其中的内容也由之前的AMD模块变成了UMD模块。同样为了节省时间,此处也不进行相应的演示。

接下来,让我们利用节省下来的时间来看看通用模块是如何被加载的。

2.通用模块加载器

前面我们已经知道,CommonJS模块之所以可以被Node.js加载,是因为Node.js中内置了一个遵循CommonJS规范的模块加载器。另外,由于UMD兼容CommonJS,Node.js也能加载UMD模块。


 

说明  

即使读者将当前项目的所有TypeScript模块都编译器成UMD模块,仍然能够通过node命令“node index.js”加载index.js和hello-world.js,并使它们向控制台输出“hello, world”。

但由于CommonJS最为简洁,因此若未特别说明,本书接下来的所有示例也将用CommonJS作为通用模块规范。


现在的问题是,我们开发的Web应用程序除了其中的服务端部分运行在Node.js中,还有很大一部分客户端需要运行在浏览器上,而浏览器上是没有内置任何通用模块加载器的—— 这也是之前当我们试图在浏览器上运行index.js时,浏览器提示“exports is not defined”的原因。

好在一部分热心人定义了通用模块规范之后,另一部分热心人随后就创建了相应的通用模块加载器。

模块加载器和模块规范不同,它们不再仅仅是契约的集合,而是基于这些契约编写的JavaScript代码(库)。这些代码中包含了模块规范中约定的exports、require()、define()和factory()等对象和函数的定义,当这些对象和函数被加载到JavaScript运行环境(如浏览器)中时,我们便能通过它们来将符合指定规范的模块加载到JavaScript运行环境中。

那么,除了Node.js内置的模块加载器,还有哪些著名的模块加载器呢?

(1)RequireJS。RequireJS是遵循AMD规范的模块加载器,可用于在浏览器和Node.js中异步加载AMD模块—— Node.js内置的模块加载器只能加载CommonJS模块和UMD模块。

此外,RequireJS的开发团队还发布了基于RequireJS实现的另一个模块加载器cajon。通过cajon,我们可以在浏览器上加载CommonJS模块。

同时,RequireJS的开发团队还发布了RequireJS的一个适配器r.js。通过r.js,我们可以将CommonJS模块转换成AMD模块,然后将它们加载到Node.js或浏览器中。

有没有觉得很乱?放心,本书不打算对RequireJS进行过多的介绍。更何况在RequireJS之外,我们还有更好的选择,那就是更加简单易用但功能更加强大的SystemJS。

(2)SystemJS。不同于Node.js内置的模块加载器和RequireJS对能够加载的目标模块有所限制,SystemJS在其设计之初的目标就是加载符合任何规范的模块以及任何资源文件,比如CommonJS模块、AMD模块、UMD模块甚至ECMAScript模块,以及图片、文本、CSS、音频和HTML等文件。


 

说明  

SystemJS也有自己的模块规范,这一规范与CommonJS和AMD等规范互不兼容,因此其他模块加载器无法加载SystemJS模块。

另外,SystemJS需要在相应的插件的帮助下才能加载ECMAScript模块及图片和文本等资源。这些插件的用法并不复杂,但由于本书不涉及ECMAScript模块及图片和文本等资源的加载,因此不会介绍这些插件的用法。


因此,本书(以及很长一段时间内的Angular官方案例)采用的模块加载器都是SystemJS,但在使用SystemJS之前,我们需要先将它安装到当前项目中。

为了安装SystemJS,我们本应该在VS Code集成终端输入并执行相应的npm命令,但这一次我们打算换一种更加易于管理的Node.js包安装方式。

首先,我们需要在当前项目下新建一个名为package.json的JSON文件,然后将代码清单2-17复制到其中。

代码清单2-17 package.json

{
    "name": "chapter2-1",
    "version": "1.0.0",
    "private": true,
    "dependencies": {
        "express": "4.16.3",
        "systemjs": "0.21.5"
    }
}


 

说明  

package.json暂时不支持注释,这一文件的存在会消除图2-32所示的警告。


当我们在某个目录中创建一个package.json文件时,这个目录会被当成一个Node.js包,而package.json就是这个Node.js包的描述文件。

因此,经过以上操作之后,当前项目便成了一个Node.js包,而以上代码中的name和version属性则分别描述了当前Node.js包的名字和版本,另一个值为true的private属性则用于说明当前Node.js包是一个不打算发布到npm上的私有包。

将项目描述成一个Node.js包有两个好处:一是可以将它发布到npm的在线仓库中,从而使别人可以下载并使用它;二是可以通过其中的package.json,为其批量下载其所依赖的其他Node.js包。

显然,我们现在更加关心的是第二个好处,即实现当前项目所依赖的Node.js包的批量下载。那么这该如何实现呢?让我们保存以上代码的编辑,然后在VS Code集成终端输入并执行以下npm命令:

npm install

经过几秒的等待之后,npm将通过终端提示我们一个Node.js包已经安装成功,同时当前项目的node_modules目录之下也会多出一个名为systemjs的子目录,如图2-39所示。

图2-39 批量安装Node.js包

显然,被安装成功的那个Node.js包就是SystemJS,而node_modules目录下新增的子目录systemjs就是SystemJS在当前项目中的安装路径。那么,这一切是怎么发生的呢?答案得回到刚刚创建的package.json中去找。


 

说明  

以上被安装的SystemJS的版本是0.21.5,其另一个分支版本是2.0.0。但那个分支不支持CommonJS模块的加载,所以我们需要使用0.21.5这个分支。


在VS Code集成终端中执行npm命令“npm install”时,由于我们没有提供代表目标Node.js包名字的第二个参数,npm便会检查当前工作目录(即当前项目的根目录)下是否存在一个合法的package.json文件,并在发现存在时,将其dependencies属性所描述的所有Node.js包安装(或更新)到当前工作目录的node_modules目录下。

显然,我们已经给当前项目添加了一个合法的package.json文件,并且这个文件的dependencies属性下有以下两个属性。

(1)"express": "4.16.3"—— 指向版本号为4.16.3的Express。

(2)"systemjs": "0.21.5"—— 指向版本号为0.21.5的SystemJS。

这就是说,当前面的“npm install”指令在被执行时,它首先发现了express属性,从而试图将版本号为4.16.3的Express安装到当前项目中,但由于当前项目中已有最新版本的Express了,因此它会跳过Express的安装;接着它又发现了systemjs属性,于是将版本号为0.21.5的SystemJS安装到当前项目中。

以上便是SystemJS被安装到当前项目中的过程,相比直接在npm命令中指定要安装的Node.js包的名字,此方式显然更加便利。因此,在本书后面的内容中,我们将继续使用这种批量安装的方式完成更多Node.js包(比如Angular包等)的安装。

此外,以上方式除了可以实现Node.js包的批量安装,还能实现它们的批量删除。为此我们只需要将相应的Node.js包从package.json的dependencies属性中删除,然后再次在VS Code集成终端中输入并执行命令“npm install”即可。

为了节省时间,此处不再演示Node.js包的批量删除。接下来,让我们看看如何通过SystemJS加载项目中的通用模块。


 

说明  

若要了解更多有关npm和package.json的更多细节,请参考其官方网站。


3.通用模块的综合运用

前面我们已经多次指出了浏览器提示“exports is not defined”的原因,现在终于是消除这个错误的时候了,为此我们需要修改index.html的内容,使其与代码清单2-18一致。

代码清单2-18 index.html

<html>
<head>
    <title>hello, world</title>
 <!—加载模块加载器SystemJS-->
 <script src="/node_modules/systemjs/dist/system.js"></script>
 <script>
 // 使用模块加载器SystemJS加载index.js
 System.import('index.js');
 </script>
</head>
<body>
    hello, world
</body>
</html>

以上代码包含三处修改,它们各自将产生如下影响。

(1)删除了原先通过<script>标签对index.js的加载,这将避免index.js被运行在JavaScript全局作用域中。

(2)新增了一对加载“/node_modules/systemjs/dist/system.js”的<script>标签,这将使SystemJS的源代码被加载到浏览器中,并在浏览器的全局作用域中创建一个名为System的模块加载器—— 其本质是一个JavaScript对象。

(3)新增了一对未设置src特性的<script>标签,并向其中写入了唯一的一行JavaScript代码System.import ('index.js'),这将使index.js被SystemJS加载到浏览器中,并被运行在由SystemJS创建的一个独立作用域中。


 

说明  

当提及SystemJS时,我们可能指的是包含其源代码在内的整个生态,也可能指的只是由其源代码创建的模块加载器。这就跟我们提及jQuery时,有时指的是其源代码库,而有时指的是$对象一样。


SystemJS 创建的独立作用域会被注入一些用于模块的导入和导出的工具,比如前面提到的exports对象和require()函数,因此已经拥有独立作用域的index.js中的exports和require语句不会再引发语法错误。尤其是其中的require('./hello-world')语句还将把hello-world.js加载到浏览器中,并使其运行在另一个独立的作用域中。

那么,事实是否就是这样呢?保存以上代码的编辑并刷新浏览器,随后我们将看到图2-40所示的结果。

图2-40 使用SystemJS加载index.js失败

这是什么?!请不要着急,通过检查图2-40中的错误就会发现,至少“exports is not defined”不见了!

另外,通过仔细分析图2-40中的错误还可以发现,SystemJS已经成功地创建了一个独立的作用域,并成功地将index.js加载到了其中。只是接下来当它试图加载hello-world.js时,使用了一个错误的URL,即http://localhost:3000/hello-world—— 正确的应该是有扩展名的http://localhost:3000/hello-world.js。

或许读者已经知道出错的原因,那就是index.js中的require('./hello-world')的参数本来就没有扩展名,因此SystemJS只能请求没有扩展名的URL。或许读者又想到了解决方案,那就是给函数require()的参数添加上.js扩展名。但我们不需要也不应该这样做,因为对index.js的任何修改都会被index.ts的下一次编译覆盖。

那么,还有别的办法吗?当然有!那就是再次修改index.html的内容,使其与代码清单2-19一致。

代码清单2-19 index.html

<html>
<head>
    <title>hello, world</title>
    <!—加载模块加载器SystemJS-->
    <script src="/node_modules/systemjs/dist/system.js"></script>
    <script>
        (function () {
 System.config({ // SystemJS进行配置
 packages: { // 路径配置
 '/': { // 根路径
 defaultExtension: 'js' // 设置根路径下的资源的默认后缀为.js
 }
 }
 })
 })();

        // 使用模块加载器SystemJS加载index.js
        System.import('index.js');
    </script>
</head>
<body>
    hello, world
</body>
</html>

以上代码在调用System.import()函数之前执行了一个自调用函数,这最终会调用SystemJS的配置函数config(),并向它传递一个配置对象。

SystemJS的配置对象包含map、paths和以上代码中的packages等属性,其中packages属性用于设置指定服务端路径的一些配置,比如,以上代码为服务端根路径(/)设置了一个属性defaultExtension,并将这个属性的值设置为js。其作用就是使SystemJS在加载服务端根路径下的资源时,默认使用.js作为这些资源的扩展名。

也就是说,经过以上修改之后,当index.js中的require('./hello-world')被调用时,SystemJS将自动在hello-world的后面添加.js扩展名,并成功地完成hello-world.js的加载和运行。

因此,如果再次保存index.html的编辑并刷新浏览器,我们将看到图2-41所示的结果。

图2-41 在浏览器中调试TypeScript代码

此时,如果单击继续执行图标(或按键盘上的F8键),然后切换到调试工具的控制台标签,我们将看到图2-42所示的结果。

图2-42 SystemJS加载通用模块

期待已久的问候“hello, world”终于出现在控制台中了!但从以上两个图的变化来看,这一问候出现的过程似乎有些复杂,为此让我们通过以下步骤来看看它出现的背后都发生了什么。

(1)System.import('index.js')被执行,从而创建一个独立的作用域,并成功地将index.js加载到其中。

(2)浏览器发现index.js底部的注释“//# sourceMappingURL=index.js.map”,从而开始加载并分析源映射文件index.js.map,并基于分析结果开始加载TypeScript文件index.ts。

(3)浏览器开始执行index.js的内容,在这个过程中发生了两件事:

(4)接着,我们单击了浏览器调试工具的“继续执行”按钮(或按下F8键),index.js中的函数调用helloWorld()因此而被执行,这将导致自hello-world.js中导入的同名函数helloWorld()被执行,并向控制台输出问候“hello, world”。

(5)最后,我们切换到了控制台,并看到了其中的问候“hello, world”—— 这就是图2-42所示的结果。


 

注意  

在源映射文件的帮助下,(大部分较新的)浏览器会将与JavaScript文件对应的TypeScript文件加载到其中,并使我们像在VS Code中那样调试TypeScript文件。这让我们感觉TypeScript代码可以运行在浏览器中,但事实上被运行的仍是JavaScript代码。


以上便是使用SystemJS 加载通用模块index.js 和 hello-world.js的过程,同时我们还顺便见证了TypeScript在浏览器中也能调试。但到目前为止,我们都还没有感受到通用模块带来的实际好处,为此让我们对index.ts和hello-world.ts再进行一次最后的修改,使它们的内容分别与代码清单2-20和代码清单2-21一致。

代码清单2-20 index.ts

import { helloWorld } from './hello-world'; //导入模块hello-world(.ts)内的公共函数helloWorld
debugger;     // 断点标识符
helloWorld(); // 调用hello-world()函数
var i = 1;
setInterval(function(){
 console.log('奇数i: ' + i);
 i += 2;
}, 3000);

代码清单2-21 hello-world.ts

export function helloWorld() {
    console.log('hello, world');
}
var i = 0;
setInterval(function(){
 console.log('偶数i: ' + i);
 i += 2;
}, 3000);

我们给index.ts和hello-world.ts各添加了一个分别表示奇数和偶数的变量i,还通过两个定时器每隔3秒向控制台输出这两个变量,并使它们自加了2以保证每一次被输出的都是一个更大的奇数或偶数。

保存以上两个文件的编辑,使以上修改可以被编译到相应的通用模块index.js和hello-world.js中,然后刷新浏览器并切换到控制台,我们将看到不断自增的奇数i和偶数i,如图2-43所示。

图2-43 不同模块中的同名变量

可以想象,如果没有模块和模块加载器,当index.js和hello-world.js被同时加载到浏览器中时,其中必有一个文件中的变量i会被另一个文件中的变量i覆盖,因此最后被输出到控制台的只能永远是奇数或偶数。

这就是模块的魅力,每一个模块都有各自的作用域,同时每一个模块也可以导出公共的变量和函数,或导入其他模块导出的公共变量和函数。

基于这样的魅力,用JavaScript创建大型Web应用程序便不再是一件困难的事。


 

说明  

注意到了吗?此刻tsconfig.json中的module属性的值是umd,而以上实践证明了SystemJS可以加载UMD模块。如果读者愿意,还可以将tsconfig.json中的module属性的值先后改成amd和commonjs,完成所有TypeScript代码的编译,然后刷新浏览器,就会发现所有结果都不会发生任何变化。

尽管如此,我们需要保证最后tsconfig.json的module属性的值是commonjs(或umd),因为接下来我们还会使用Node.js加载TypeScript文件的编译结果,而Node.js原生是不支持AMD模块的。


尽管通用模块有着迷人的魅力,但(在不使用TypeScript的情况下)书写通用模块是一件极为烦琐的事,并且通用模块中的各种辅助对象和函数(比如exports和require()等)也使得其可读性急剧下降。

为了弥补以上缺陷,ECMA国际组织在2015年将模块写入了ECMAScript 2015(简称ES 2015)中,从而使支持ES 2015的浏览器(或其他JavaScript运行环境)可以直接加载符合ES 2015规范的模块,即ECMAScript模块。


 

说明  

ECMA国际组织成立于1961年,当时的名字为欧洲计算机制造商协会(European Computer Manufacturers Association,ECMA),但于 1996 年被更名为ECMA国际组织(ECMA International)。此后,ECMA不再代表欧洲计算机制造商协会的首字母缩写,而是一个具有独立意义的名字。

ECMAScript是由ECMA国际组织于1996年根据网景公司提交给它的JavaScript编写的语言规范,其存在的很大一部分意义是指导JavaScript运行环境(比如浏览器和Node.js)厂商开发符合规范的JavaScript运行时,至2018年11月已有九个版本(包括已被弃用的第四个版本)。

从ECMAScript的第六个版本开始,其名称不再以版本序号命名。因此,ECMAScript的第六个版本的正式名称开始是ECMAScript 6(简称ES 6),但随后被更改为ECMAScript 2015(简称ES 2015)。以此类推,ECMAScript的第七个版本的正式名称是ECMAScript 2016(简称ES 2016),第八个版本的正式名称是ECMAScript 2017(简称ES 2017),第九个版本的正式名称是ECMAScript 2018(简称ES 2018)。

另外,在最新的ECMAScript正式定稿之前,它有一个比较特殊的名字,叫作ES.Next。


那么,ECMAScript模块(后文简称ES模块)长什么样子呢?简单来说,它和我们已经见识过的TypeScript模块几乎是一模一样的。为了见识这种“一样”,让我们在当前项目下新建一个名为hello-world.es2015.js的JavaScript文件,然后将代码清单2-22复制到其中。

代码清单2-22 hello-world.es2015.js

// 导出公共函数
export function helloWorld() {
    console.log('hello, world. I am an ES2015 module.');
}
// 模块内的奇数变量,未通过export导出,因此其他模块不能使用这个变量
var i = 1;

以上就是一个ES模块,它和TypeScript模块一样,通过标识符export导出了公共函数helloWorld,并声明了一个未被导出的(模块内私有的)变量i。

接下来,让我们继续新建一个名为index.es2015.js的JavaScript文件,然后将代码清单2-23复制到其中。

代码清单2-23 index.es2015.js

// 从模块hello-world.es2015.js中导入函数helloWorld
import { helloWorld } from './hello-world.es2015.js';
// 调用导入自模块hello-world.es2015.js的函数helloWorld
helloWorld();
// 错误,无法使用其他模块未通过export导出的变量
console.log(i);

以上仍是一个ES模块,它从上一个ES模块hello-world.es2015.js(代码中此处的扩展名.js不能省略)中导入了函数helloWorld,然后调用了这个函数,并试图向控制台输出变量i。

最后,让我们再新建一个名为index.es2015.html的HTML文件,然后将代码清单2-24复制到其中。

代码清单2-24 index.es2015.html

<html>
<head>
    <title>'hello, world' from ECMAScript module</title>
    <!--通过type特性的值为module的<script>标签加载ES模块>-->
    <script type="module" src="/index.es2015.js"></script>
</head>
<body>
    'hello, world' from ECMAScript module
</body>
</html>

以上代码通过一对<script>标签将刚刚创建的JavaScript文件index.es2015.js加载到浏览器中,但由于index.es2015.js是一个ES模块,由此我们将这对<script>标签的type特性的值设置成了module(模块)—— 这是ES 2015规范的要求。

这样一来,(版本较新的)浏览器内置的ES模块加载器便会以加载ES模块的方式加载index.es2015.js,并在遇到其中的import语句时再次以加载ES模块的方式加载hello-world.es2015.js。

因此,当我们保存以上所有代码的编辑,并在浏览器中访问localhost:3000/index.es2015.html时,我们将看到控制台中输出的是来自ES模块的问候,如图2-44所示。

图2-44 使用ES模块

如果没有加框标注的那部分,那么结果堪称完美。但事实上,它从反面说明了ES模块和通用模块一样都有自己的独立作用域—— hello- world.es2015.js中未被导出的(私有)变量i在index.es2015.js中是无法被访问的。

此时,读者是不是在感叹ES模块的简洁和强大,并在憧憬着它被广泛普及?然而,Web领域中新事物(事实上ES 2015的发布已经过去四年了)的普及速度总是远低于我们的期望,我的同事去年还在用“经典、稳定、流畅并且更加称手”的Windows XP。

为了照顾我们念旧的同事、设备确实无法换代的朋友,大量的Web应用程序开发人员不得不将ES 2015(更不用说ES 2016、ES 2017和ES 2018)拒之门外。

好在,现在我们有了TypeScript。我们可以通过TypeScript来编写遵循ES 2015规范的模块,然后将它编译成兼容更多环境的通用模块。

通过对本章内容的学习,我们对Node.js和npm有了一个大概的了解,并准备了一个基于Node.js的跨平台开发环境,最终还基于这一环境创建了一个简单的Web应用程序。

在这个过程中,我们接触了一些命令。为了便于记忆,现将它们的概况描述如下。

(1)查看Node.js版本的命令:node -v。

(2)在Node.js中执行JavaScript模块(文件)的命令:node hello-world.js,其中hello-world.js是文件路径。

(3)查看npm版本的命令:npm -v。

(4)设置npm镜像的命令:npm config set registry https://registry.npm.taobao.org。

(5)查看npm镜像的命令:npm config get registry。

(6)查看npm镜像中是否有指定包的命令:npm view jquery,其中jquery是目标包的名字,且必须小写。

(7)安装TypeScript编译器到当前设备的npm全局安装路径的命令:npm install -g typescript。

(8)查看当前设备的npm全局安装路径下是否有TypeScript编译器的命令:npm ls -g typescript。

(9)安装Express到当前项目的命令:npm install express。

(10)查看当前项目下是否有Express的命令:npm ls express。

此外,我们还接触了一个配置,即TypeScript项目配置tsconfig.json:

{
    "compilerOptions":{    // 编译器选项
        "sourceMap": true  // 生成映射文件
    }
}

这个配置首先是VS Code自动编译项目中的TypeScript文件的基础,而其中的sourceMap属性则是允许VS Code调试TypeScript文件的前提条件。

此外,我们还了解到了模块存在的意义,同时也体会到了其发展的艰辛和混乱。但从第3章开始,我们可以忽略这些混乱,因为其中介绍的TypeScript会处理好这一问题。


相关图书

Dapr与.NET微服务实战
Dapr与.NET微服务实战
区块链国产化实践指南:基于Fabric 2.0
区块链国产化实践指南:基于Fabric 2.0
Metasploit Web渗透测试实战
Metasploit Web渗透测试实战
Eclipse WTP Web应用开发
Eclipse WTP Web应用开发
一个全栈增长工程师的练手项目集
一个全栈增长工程师的练手项目集
2017年异步社区书目
2017年异步社区书目

相关文章

相关课程