模块
理解模块模式
将逻辑分块,各自封装,相互独立,每个块自行决定对外暴露什么,同时自行决定引入执行哪些外部代码
模块标识符
模块标识符是所有模块系统通用的概念;模块系统本质上是键/值实体,每个模块都有个可用于引用它的标识符;这个标识符在模拟模块系统中可能是字符串,在原生实现的模块系统中可能是模块文件的实际路径
原生浏览器模块标识符必须提供实际JavaScript文件的路径;除了文件路径,Node.js还会搜索node_modules目录,用标识符去匹配包含index.js目录
模块依赖
模块系统的核心是管理依赖;指定依赖的模块与周围环境会达成一种契约;本地模块向模块系统声明一组外部模块(依赖);这些外部模块对于当前模块正常运行是必须的;模块系统检视这些依赖,进而保证这些外部模块都能够被加载并在本地模块运行时初始化所有依赖
每个模块都会与唯一的标识符关联,该标识符可用于检索模块;这个标识符通常是js文件路径,但在某些模块系统中,这个标识符也可以是在模块本身内部声明的命名空间路径字符串
模块加载
加载模块的概念派生自依赖契约;当一个外部模块被指定为依赖时,本地模块期望在执行它时,依赖已经准备好并已初始化
所有的模块必须在所有依赖都加载并执行后,加载模块才会涉及执行其中的代码
模块中的依赖也会要保证加载完成,浏览器会递归评估并加载所有依赖
只有整个依赖图都加载完成,才可以执行入口模块
入口
相互依赖的模块必须指定一个模块作为入口,也是代码执行的起点
因为模块中可能嵌套依赖,所以JavaScript应用程序的所有模块会构成依赖图
在JavaScript中,”加载“的概念可以有很多种实现方式;因为模块是作为包含立即执行的JavaScript代码的文件实现的,所以一种可能时按照依赖图的要求依次请求各个脚本
模块加载是阻塞的,这意味着前置操作必须完成才能执行后续操作;手动管理正确的加载顺序也比较棘手,为一个程序按顺序加载js文件也不理想
异步依赖
因为js可以异步执行,可以检测异步js何时加载和初始化,然后再通过回调进行模块的下一步操作
动态依赖
有些模块系统要求开发者在模块开始列出所有依赖,而有些模块系统允许开发者在程序结构中动态添加依赖
if (loadCondition) {
require('./moduleA');
}
动态依赖可以支持更复杂的关系,但代价是增加了对模块进行静态分析的难度
静态分析
模块中包含的发送到浏览器JavaScript代码经常会被静态分析,分析工具会检查到吗结构并在不实际执行代码的情况下推断其行为
对静态分析友好的模块系统可以让模块打包系统更容易将代码处理为较少的文件;它还将支持只能编辑器里智能自动完成
更复杂的模块行为,例如动态依赖,会导致静态分析更困难
循环依赖
包括CommonJS、AMD、ES6在内所有模块系统都支持循环依赖;只要恰当地封装模块,使它们没有副作用,加载顺序就应该不会影响应用程序的运行
凑合的模块系统
为按照模块模式提供必要的封装,ES6之前的模块有时候会使用函数作用域和立即调用函数表达式(IIFE)将模块定义封装在匿名闭包中
相关代码查看红宝书p776
当然自己写模块系统确实非常有意思,但实际开发中并不建议这么做,因为不够可靠
使用ES6之前的模块加载器
CommonJS
该规范概述了同步声明依赖的模块定义;这个规范主要用于在服务器端实现模块化代码组织,但也可以用于定义在浏览器中使用的模块依赖;CommonJS模块语法不能再浏览器中直接运行
Node.js的模块系统使用了CommonJS规范,实际上并不完全正确,实际上使用的是轻微修改版本的CommonJS
因为Node.js主要是在服务器环境下使用,所以不需要考虑网络延迟问题
CommonJS模块定义需要使用require()指定依赖,而使用exports对象定义自己的公共API:
var moduleB = require('./moduleB');
module.export = {
stuff: moduleB.doStuff();
}
调用require()意味着模块会原封不动地加载进来;同一个模块被引用多少次,都只会被加载一次
模块第一次加载后会被缓存,后续加载会取得缓存的模块
在CommonJS中,模块加载是模块系统的执行的同步操作;因此require()可以编程方式嵌入在模块中,这个方法加载是同步的
可以使用module.exports定义公共属性;module.exports对象非常灵活,有多种使用方式;
module.exports = 'foo';
var module = require('./module');
module.exports = {
a: 'a',
b: 'b'
};
module.exports.a = 'a';
module.exports.b = 'b';
模块一个主要用途是托管类定义
class A {}
module.exports = A;
var A = require('./module');
var a = new A();
当然也可以将实例作为导出值;CommonJS支持动态添加依赖
如果想在浏览器中使用CommonJS模块,需要提前把模块文件打包好,把全局属性转换为原生JavaScript结构,将模块代码封装在函数闭包中,最终只提供一个文件
异步模块定义
异步模块定义(AMD,Asynchronous Module Definition)的模块定义系统以浏览器为目标执行环境,这需要考虑网络延迟的问题
AMD一般策略是让模块声明自己的依赖,而运行在浏览器中的模块系统会按需获取依赖,并在依赖加载完后立即执行依赖他们的模块
AMD模块实现的核心是用函数包装模块定义;包装模块的函数是全局define的参数,它是由AMD加载器库是实现定义的
AMD模块可以使用字符串标识符指定自己的依赖,而AMD加载器会在所有依赖模块加载完毕后立即调用模块工厂函数;AMD支持可选的为模块指定字符串标识
define('moduleA', ['moduleB'], function(moduleB) {
return {
stuff: moduleB.doStuff();
};
});
AMD也支持require和exports对象,通过它们可以在AMD模块工厂函数内部定义CommonJS风格的模块;可以像请求模块一样请求它们,但AMD加载器会将它们识别为原生AMD结构
define('moduleA', ['require', 'exports'], function(require, exports) {
var moduleB = require('moduleB');
exports.stuff = moduleB.doStuff();
});
通用模块定义
为了统一CommonJS和AMD生态系统,通用模块定义(UMD,Universal Module Definition)规范应运而生;UMD可用于创建这两个系统都可以使用的模块代码
相关包装函数查阅红宝书p782
开发者不应该期望手写这个包装函数,它应该由构建工具自动生成
模块加载器终将没落
随着ES6模块规范得到越来越广泛的支持,本节展示的模式终将会没落;这也正是ES6模块规范诞生的来由;CommonJS和AMD之间的冲突是ES6模块规范诞生的温床
使用ES6模块
模块标签及定义
ES6模块是作为一整块js代码而存在的;带有type="module" 属性的script标签会告诉浏览器相关代码应该作为模块执行,而不是传统的脚本;模块可以嵌入在网页中,也可以作为外部文件引入
js模块文件也没有专门的内容类型
所有模块都会像<script defer> 加载的脚本一样按顺序执行;解析到模块标签后会立即下载模块文件,但执行会延迟到文档解析完成;嵌入的模块代码和引入的外部模块文件都是这样
也可以给模块标签添加async属性;这样影响就是双重的:不仅模块执行顺序不再与script标签在页面中的顺序绑定,模块也不会等待文档完成解析才执行;不过入口模块仍必须等待其依赖加载完成
与<script type="module"> 标签关联的ES6模块被认为是模块图中的入口模块;一个页面中有多少个入口模块没有限制,重复加载一个模块也没有限制(因为实际上同一个模块只会加载一次)
嵌入模块定义代码不能使用import加载到其它模块;只有通过外部文件加载的模块才可以使用import加载;嵌入模块知识和作为入口模块
模块加载
ES6模块的独特之处在于,既可以通过浏览器原生加载,也可以与第三方加载器和构建工具一起加载;因为有些浏览器原生不支持ES6模块,所以需要用第三方工具;很多时候使用第三方工具可能会更方便
完全支持ES6模块的浏览器可以从顶级模块加载整个依赖图,而且是异步完成的;这个异步递归加载过程会持续到整个应用程序的依赖图都加载完成;解析完依赖图,应用程序就可以正式加载模块了
模块文件是按需加载的,后续模块的请求会因为每个依赖模块的网络延迟而同步延迟;加载大型应用程序的深度依赖图可能要花费很长时间
模块行为
ES6模块借用了CommonJS和AMD很多优秀特性:
? 模块代码只在加载后执行
? 模块代码只能加载一次
? 模块是单例
? 模块可以定义公共接口,其它模块可以基于这个公共接口观察和交互
? 模块可以请求加载其它模块
? 支持循环依赖
ES6模块系统也增加了一些新行为:
? ES6模块默认在严格模式下执行
? ES6模块不共享全局的命名空间
? 模块顶级this的值是undefined
? 模块中var声明不会添加到window对象
? ES6模块是异步加载和执行的
与<script type="module"> 关联或者通过import语句加载的js文件会被认定为模块
模块导出
控制模块的哪些部分对外可见的是export关键字
ES6模块支持两种导出:命名导出和默认导出;不同的导出方式对应不同的导入方式
export关键字用于声明一个值为命名导出;导出语句必须在模块顶级,不能嵌套在某个块中
export ...
if (condition) {
export ...
}
导出值对模块内部js的执行没有直接影响,因此export语句与导出值的相对位置或者export关键字在模块中出现的顺序没有限制
命名导出就好像模块是被导出值的容器;行内命名导出:export const foo = 'foo';
也可以分成两行:
const foo = 'foo';
export { foo };
导出时可以提供别名,别名必须用大括号语法包裹起来:
const foo = 'foo';export { foo as myFoo };
可以在一个模块中声明多个命名导出:
export const foo = 'foo';export const bar = 'bar';const foo = 'foo';const bar = 'bar';export { foo, bar as myBar };
默认导出就好像模块与被导出的值是一回事;默认导出使用default关键字将一个值声明为默认导出,每个模块只能有一个默认导出;重复的默认导出会导致SyntaxError
外部模块可以导入这个模块,而这个模块本身就是这个导出值:
const foo = 'foo';export default foo;
ES6模块系统会识别作为别名提供的default关键字,此时,虽然对应的值是使用命名语法导出的,实际上则会成为默认导出:
const foo = 'foo';export { foo as default };
命名导出和默认导出不会冲突,ES6支持在一个模块中同时定义这两种导出:
const foo = 'foo';const bar = 'bar';export { bar };export default foo;
ES6规范对不同形式的export语句中可以使用什么不可以使用什么规定了限制,详情查看红宝书p787
一般来说声明、赋值、导出标识符最好分开;这样就不容易搞错,同时可以让export语句集中在一块
模块导入
模块可以使用import关键字使用其它模块导出的值,import也必须出现在模块的顶级,不允许在块级中使用
import语句被提升到模块顶部,所以import语句出现的位置并不重要
模块标识符可以是相对路径也可以是绝对路径;它必须是纯字符串,不是动态计算的结果
import ... from './bar.js';
如果使用浏览器通过标识符原生加载模块,则文件必须带有.js扩展名,不然可能无法正确解析
如果不需要模块的特定导出,但仍想加载和执行模块以利用其副作用,可以只通过路径加载它:import './foo.js';
导入对模块而言是只读的,实际上相当于const声明的变量;在使用*执行批量导入时,赋值给别名的命名导出就好像使用Object.freeae()冻结过一样
命名导出和默认导出的区别也反映在它们的导入上;命名导出可以使用*批量获取并赋值给保存导出集合的别名,而无需列出每个标识符:import * as Foo from './bar.js';
要指名导入,需要把标识符放在import子句中:import {foo, bar, baz as myBaz} from './bar.js';
默认导出可以使用default关键字并提供别名来导入;也可以不使用大括号,此时指定地点标识符就是默认到导出的别名:
import { default as foo } from './foo.js';import foo from './foo.js';
如果模块同时导出了命名导出和默认导出,则可以在import中同时取得它们:
import foo, { baz, bar } from './xx.js';import { default as foo, bar, baz } from './xx.js';import foo, * as Foo from './xx.js';
模块转移导出
模块导入也可以直接通过管道转移导出,可以将默认导出转换为命名导出,或者相反;还可以将所有命名导出集中到一块:
export * from './foo.js';
这样不会复制导出的值,只是把导入的引用传给了原始模块
工作者模块
ES6模块与Worker实例完全兼容;在实例化时,可以给工作者传入一个指向模块文件的路径,与传入常规脚本一样;Worker构造函数接收第二个参数,说明传入的是模块文件:
const scriptWorker = new Worker('scriptWorker.js');const moduleWorker = new Worker('moduleWorker.js', { type: 'module' });
在基于模块工作者内部,self.importScripts()方法通常在用于基于脚本的工作者中加载外部脚本,它会抛出错误;因为模块的import行为包含了importScripts()
向后兼容
可以提供两个版本的代码,亦可以使用第三方模块系统或在构建时将ES6模块进行转义
详情查看红宝书p790
|