2.3 SystemJS通用模块加载器
许多现存的web应用用<script>
标签加载js文件和html文件。虽然用相同的方式添加Angular的代码到页面中是可以做到的,但是最推荐的方式是去通过SystemJS库去加载代码。Angular内部也用SystemJS。
在这一章,我们将简单介绍SystemJS所以你可以用来开发Angular应用。SystemJS的详细教程,通过SystemJS网站的GitHub
2.3.1 回顾模块加载器
- 最终的es6规范介绍了模块并涵盖了他们的语法和语意。早期的规范底稿包含了全局System对象,这个对象的作用是 不管是web浏览器还是独立的一个进程,在执行环境中装载模块。但是System对象的定义在最终的es6标准中被删除了,并且最近被Web超文本应用技术工作组(Web Hypertext Application Technology Working Group)提上日程.System对象可能会成为es8的规范.
- es6模块加载垫片提供了一种方式去使用System对象(不用等待未来的Ecma规范)。它努力去迎合未来的标准,但是这个垫片仅支持es6模块。
- 因为es6是新颖的技术,大多数第三方托管在NPM上的包都没有使用es6模块。本书的前九章使用的SystemJS,不仅仅包含ES6模块加载,还允许你以AMD, CommonJS, UMD和全局模块格式去加载模块。对这些格式的支持对SystemJS的用户是显而易见的,因为它自动解析出目标脚本的格式,在第10章,你将会用其他的一个叫做Webpack的模块加载器.
2.3.2 模块加载器 对比 <script>
标签
为什么还是使用模块加载器去加载JavaScript,而不是使用<script>
标签?
<script>
标签有以下几个问题:
- 开发者要负责在HTML文件中维护
<script>
标签.它们中的一些未来可能是多余的,但你忘记去清理它们,它们仍旧会被浏览器加载,增加了加载时间和浪费了网络带宽. - 通常脚本的加载顺序也很重要。浏览器可以仅仅保证执行脚本的顺序如果你把他们放在html文档的
<head>
标签里。但是把所有脚本放在里被认为是一种不好的实践,因为它阻止了页面加载,因为在所有脚本下载完之前页面才开始渲染。
让我们考虑在开发模式用模块加载器的好处和准备应用的生产版本:
- 在开发环境中,代码通常被分割成多个文件,并且每个文件代表着一个模块。每当你在代码中引入了一个模块,加载器会匹配相应文件的模块名,把它加载到浏览器中,和执行剩下的代码。模块让你很好的组织了项目。当你启动应用时,模块加载器自动的把所有东西打包成一个到浏览器中。如果一个模块依赖于其他模块,那么他们都将被加载。
- 当你在程序的生产版本中,一个模块加载器处理main文件,贯穿所有连接着它的模块,并合并他们到一个单独的捆绑块。这样,捆绑块仅仅包含了应用实际用的代码。它还解决了脚本加载顺序和循环引用问题。
以上这些不仅对你的应用代码有好处,而且也对第三方包很有好处(像Angular)。
注意:在这本书中,我们会互换 ‘模块’和‘文件’的概念,一个模块不能跨越多个文件(这句貌似有问题- -原文是:A module can’t span multiple files),一个文件体现一个捆绑块并包含着多着已经注册过的模块。‘模块’和‘文件’的概念是十分重要的,我们稍后细说。
2.3.3 开始于SystemJS
- 当你在html页面上用SystemJS, 这个库成为全局对象并包含了数个静态方法。你会用到的的主要的俩个方法是
System.import()
和System.config()
.
用System.import()
去加载一个模块,这个方法接受一个模块名作为一个参数。一个模块名可以是一个文件的路径或者映射到文件名的逻辑名称
System.import('./my-module.js'); // 文件路径
System.import('@angular2/core'); // 一个逻辑名称
如果模块名以 ./开头并作为文件的路径即使当文件名的类型被省略了,SystemJS还是会首先去匹配文件名对提供的配置映射作为System.config()的参数或者是一个文件(比如system.config.js)。如果名字的映射没有找到,它会被看作为是文件的路径
注意:在此书中,我们会用 ./前缀和映射配置去找出哪个文件应该被加载。如果你看到
System.import('app')
并且不能找到文件名为app.ts的文件,那就检查下项目的映射配置。
- System.import()方法会立刻返回一个Promise对象(见附录A)。当模块对象的promise为resolved,当模块被加载后,then()回调函数会被唤起,如果promise是rejected, 错误会被catch()方法处理。
一个es6模块对象在装载的模块中包含了每个输出值的属性。接下来的一段来自俩个文件的代码段向你展示了如何在模块中输出一个变量并用在其他脚本中:
// lib.js export let foo = 'foo'; // main.js System.import('./lib.js').then(libModule => { libModule.foo === 'foo'; // true });
这里当lib.js被加载后,你用then()方法去指定一个回调函数唤起。被加载的对象以一个参数的形式传进箭头表达式中。
在es5脚本中,你用System.import()方法去加载代码同步或异步(动态的)。例如:如果一个匿名的用户浏览你的网站,你可能不需要一个模块去执行用户简介这一功能(因为用户没登录嘛)。但是当你的用户登录了,你可以动态的加载用户信息模块,这样,你减少了初始化网站所需要的时间和体积。
但是es6 import声明呢?如果在你的第一个Angular应用,你在index.html文件中用system.import()去加载根程序模块, main.ts。相应的,main.ts脚本用它自己import声明引入Angular模块。
当SystemJS加载了main.ts, 它自动的把它转化为ES5兼容代码,所以就不会有import声明在浏览器的代码中执行。未来,es6模块会被主流浏览器原生支持,这一步就不需要了并且import声明将会和System.import()以相同的方式运行,除非他们不会控制模块加载的时刻。
注意:当SystemJS转换文件时,它自动的生成每个.js文件的sourcemap, 这将允许你去浏览器在调试ypeScript代码。