1、理解模块模式
- 逻辑分块、各自封装、相互独立、自行决定暴露、自行决定引入执行
1、模块标识符
- 模块系统本质上是键/值实体,其中每个模块都有个可用 于引用它的标识符。
2、模块依赖
- 模块系统的核心是管理依赖
- 模块系统检视这些依赖, 进而保证这些外部模块能够被加载并在本地模块运行时初始化所有依赖
- 每个模块都会与某个唯一的标识符关联,该标识符可用于检索模块
3、模块加载
- 当一个外部模块被指定为依赖时,本地模块期望在执行它时,依 赖已准备好并已初始化。
4、入口
- 相互依赖的模块必须指定一个模块作为入口,这也是代码执行的起点。
5、异步依赖
- 让 JavaScript 通知模块 系统在必要时加载新模块,并在模块加载完成后提供回调
// 在模块 A 里面
load('moduleB').then(function(moduleB) {
moduleB.doStuff();
});
复制代码
6、动态依赖
- 有些模块系统要求开发者在模块开始列出所有依赖
- 有些模块系统则允许开发者在程序结构中动态添加依赖
// 下面是动态依赖加载的例子:
if (loadCondition) {
require('./moduleA');
}
复制代码
- 动态依赖可以支持更复杂的依赖关系,但代价是增加了对模块进行静态分析的难度
7、静态分析
- 模块中包含的发送到浏览器的 JavaScript 代码经常会被静态分析,分析工具会检查代码结构并在不实际执行代码的情况下推断其行为
8、循环依赖
- 此包括 CommonJS、AMD 和 ES6 在内的所有模块系统都支持循环依赖。
- 在包含循环依赖的应用程序中,模块加载顺序可能会出人意料
- 只要恰当地封装模块,使它们没有副作用,加载顺序就应该不会影响应用程序的运行
2、凑合的模块系统
- ES6 之前的模块有时候会使用函数作用域和立即调用函数表达式将模块定义封装在匿名闭包中
(function() {
// 私有 Foo 模块的代码
console.log('bar');
})();
// bar
复制代码
- 把这个模块的返回值赋给一个变量,那么实际上就为模块创建了命名空间
- 暴露公共 API,模块 IIFE 会返回一个对象,其属性就是模块命名空间中的公共成员
var Foo = (function () {
return {
bar: 'baz',
baz: function () {
console.log(this.bar);
}
};
})();
console.log(Foo.bar); // 'baz'
Foo.baz(); // 'baz'
复制代码
- 还有一种模式叫作“泄露模块模式”,这种模式只返回一个对象, 其属性是私有数据和成员的引用
var Foo = (function () {
var bar = 'baz';
var baz = function () {
console.log(bar);
};
return {
bar: bar,
baz: baz
};
})();
console.log(Foo.bar); // 'baz'
Foo.baz(); // 'baz'
复制代码
- 在模块内部也可以定义模块,这样可以实现命名空间嵌套
var Foo = (function () {
return {
bar: 'baz'
};
})();
Foo.baz = (function () {
return {
qux: function () {
console.log('baz');
}
};
})();
console.log(Foo.bar); // 'baz'
Foo.baz.qux(); // 'baz'
复制代码
- 让模块正确使用外部的值,可以将它们作为参数传给 IIFE
var globalBar = 'baz';
var Foo = (function (bar) {
return {
bar: bar,
baz: function () {
console.log(bar);
}
};
})(globalBar);
console.log(Foo.bar); // 'baz'
Foo.baz(); // 'baz'
复制代码
- 可以在定义之后再扩展模块
// 原始的 Foo
var Foo = (function (bar) {
var bar = 'baz';
return {
bar: bar
};
})();
// 扩展 Foo
var Foo = (function (FooModule) {
FooModule.baz = function () {
console.log(FooModule.bar);
}
return FooModule;
})(Foo);
console.log(Foo.bar); // 'baz'
Foo.baz(); // 'baz'
复制代码
- 无论模块是否存在,配置模块扩展以执行扩展也很有用
// 扩展 Foo 以增加新方法
var Foo = (function (FooModule) {
FooModule.baz = function () {
console.log(FooModule.bar);
}
return FooModule;
})(Foo || {});
// 扩展 Foo 以增加新数据
var Foo = (function (FooModule) {
FooModule.bar = 'baz';
return FooModule;
})(Foo || {});
console.log(Foo.bar); // 'baz'
Foo.baz(); // 'baz'
复制代码
3、使用ES6之前的模块加载器
1、CommonJS
- 概述了同步声明依赖的模块定义。主要用于在服务器端实现模块化代码组织,但也可用于定义在浏览器中使用的模块依赖。CommonJS 模块语法不能在浏览器中直接运行
- CommonJS模块定义需要使用require() 指定依赖,使用exports对象定义自己公共API
var moduleB = require('./moduleB')
module.exports = {
stuff: mouduleB.doStuff();
}
复制代码
- 赋值给变量不是必需的,调用require()意味着模块会原封不动加载进来。
- 无论一个模块在require()中被引用了多少次,,模块永远是单例。
- 模块第一次加载后会被缓存,后续加载会取得缓存的模块。模块加载顺序由依赖图决定。
- CommonJS 中,模块加载是模块系统执行的同步操作。
console.log('moduleA')
if(loadCondition) {
require('./moduleA')
}
复制代码
-
module.exports
-
导出一个实体
module.exports = 'foo'
-
导出多个值
module.exports = { a: 'A', b: 'B' } module.exports.a = 'A' module.exports.b = 'B' 复制代码
-
模块主要用途是托管类定义
class A {} module.exports = A; var A = require('./moduleA'); var a = new A(); 复制代码
-
也可以将类实例作为导出值
class A {} module.exports = new A() 复制代码
-
支持动态依赖
-
2、异步模块定义
- CommonJS以服务端为目标环境,一次性把所有模块都加载到内存
- 异步模块定义AMD以浏览器为目标执行环境,需要考虑网络延迟问题
- AMD一般策略是让模块声明自己的依赖,运行在浏览器的模块系统会按需获取依赖,并在依赖加载完成后立即执行依赖它们的模块
- AMD模块核心是用函数包装模块定义。防止声明全局变量,允许加载器控制何时加载模块。
- 包装模块的函数是全局define的参数,而AMD加载器会在所有依赖模块加载完毕后立即调用工厂函数。支持为模块指定字符串标识符
// ID 为'moduleA'的模块定义。moduleA 依赖 moduleB,
// moduleB 会异步加载
define('moduleA', ['moduleB'], function(moduleB) {
return {
stuff: moduleB.doStuff();
};
});
复制代码
- 支持require和exports对象。可以在AMD模块工厂函数内部定义CommonJS风格的模块。
- 这样可以像请求模块一样请求它们,但 AMD 加载器会将它们识别为原生 AMD 结构,而不是模块定义
define('moduleA', ['require', 'exports'], function(require, exports) {
var moduleB = require('moduleB');
exports.stuff = moduleB.doStuff();
});
define('moduleA', ['require'], function(require) {
if (condition) {
var moduleB = require('moduleB');
}
});
复制代码
3、通用模块定义
- UMD 为了统一CommonJS和AMD生态系统。
- 可以用于创建这两个系统都可以使用的模块代码。
- 本质上,UMD 定义的模块会在启动时 检测要使用哪个模块系统,然后进行适当配置,并把所有逻辑包装在一个立即调用的函数表达式(IIFE) 中。
- 这种组合不完美,但很多场景下足以实现两个生态的共存。
(function (root, factory) {
if(typeof define === 'function' && define.amd) {
// AMD 注册为匿名函数
define(['moduleB'], factory);
}else if(typeof module === 'object' && module.exports) {
// Node 不支持严格CommonJS
// 但可以在Node这样支持module.exports的
// 类CommonJS环境下使用
module.exports = factory(require('moduleB'));
}else {
// 浏览器全局上下文(root是window)
root.returnExports = factory(root.moduleB)
}(this, function(moduleB) {
// 以某种方式使用moduleB
// 将返回值作为模块的导出
// 这个例子返回了一个对象
// 但是模块也可以返回函数作为导出值
return {}
})
})
复制代码
- 此模式有支持严格 CommonJS 和浏览器全局上下文的变体。不应该期望手写这个包装函数,它应该 由构建工具自动生成。
4、模块加载器终将没落
- CommonJS与AMD之间的冲突正是我们现在享用的ECMAScript 6模块规范诞生的温床
4、使用ES6模块
1、模块标签及定义
- 带有
type="module"
属性的<script>
标签会告诉浏览器相关代码应该作为模块执行,而不是作为传统的脚本执行。
<script type="module">
// 模块代码
</script>
<script type="module" src="path/to/module.js"></script>
复制代码
- 所有模块都会像
<script defer>
加载的脚本一样按顺序执行。 - 解析到
<script type="module">
的标签后会立即下载模块文件,但执行会延迟到文档解析完成。 <script type="module">
在页面中出现的顺序就是它们的执行循序。- 修改模块标签的位置,无论是在
<head>
还是在<body>
中,只会影响文件什么时候加载,而不会影响模块什么时候加载 - 也可以给模块标签添加async属性,这样是双重影响:
- 不仅模块执行顺序不再与
<script>
标签在页面中的顺序绑定 - 模块也不会等待文档完成解析才执行。
- 入口模块仍必须等待其依赖加载完成
- 不仅模块执行顺序不再与
- 一个页面上有 多少个入口模块没有限制,重复加载同一个模块也没有限制。
- 同一个模块无论在一个页面中被加载多少次,也不管它是如何加载的,实际上都只会加载一次。
- 嵌入的模块定义代码不能使用 import 加载到其他模块。只有通过外部文件加载的模块才可以使用 import 加载。因此,嵌入模块只适合作为入口模块
2、模块加载
- 既可以通过浏览器原生加载,也可以与第三方加载器和构建工 具一起加载。
- 模块文件按需加载,且后续模块的请求会因为每个依赖模块的网络延迟而同步延迟
3、模块行为
- ECMAScript6 借用CommonJS 和 AMD的优秀特征
- 模块代码只在加载后执行
- 模块只能加载一次
- 模块是单例
- 模块可以定义公共接口,其他模块可以基于这个公共接口观察和交互
- 模块可以请求加载其他模块
- 支持循环依赖
- ES6 模块系统也增加了一些新行为
- ES6模块默认在严格模式下执行
- ES6模块不共享全局命名空间
- 定义模块this的值是undefined (常规脚本是window)
- 模块中的var声明不会添加到window对象
- ES6是异步加载和执行的
- 浏览器在运行时在知道应该把某个文件当成模块时,会有条件地按照上述 ECMAScript 6 模块行为来施加限制
- 与
<script type="module">
关联或者通过import语句加载的JS文件也会被认定为模块
4、模块导出
- ES6 模块的公共导出系统与 CommonJS 非常相似。
- 控制模块的哪些部分对外部可见的是 export 关键字
- 两种导出模式:命名导出和默认导出
- export 关键字用于声明一个值为命名导出。导出语句必须在模块顶级,不能嵌套在某个块中
// 允许
export ...
// 不允许
if (condition) {
export ...
}
复制代码
- 导出值对模块内部JavaScript执行没有直接影响。export 语句与导出值的相对位置或者export关键字在模块中出现的顺序没有限制。export语句甚至可以出现在它要导出的值之前
// 允许
const foo = 'foo'
export { foo }
// 允许
export const foo = 'foo'
// 允许 但不好
export { foo }
const foo = 'foo'
复制代码
-
命名导出,就好像模块是被导出值的容器。
-
行内命名导出,顾名思义,可以在同一 行执行变量声明。
export const foo = 'foo' 复制代码
-
不在一行导出
const foo = 'foo'; export { foo }; 复制代码
-
导出时提供别名
const foo = 'foo' export { foo as myFoo } 复制代码
-
命名导出可以将模块作为容器,所以可以在一个模块中声明多个命名导出。
export const foo = 'foo'; export const bar = 'bar'; export const baz = 'baz'; 复制代码
-
支持对导出声明分组,可以同时为部分或全部导出值 指定别名
const foo = 'foo'; const bar = 'bar'; const baz = 'baz'; export { foo, bar as myBar, baz }; 复制代码
-
-
默认导出,好像模块与被导出的值是一回事。默认导出使用 default 关键字将一个值声明为默认导出,每个模块只能有一个默认导出。重复的默认导出会导致 SyntaxError。
-
外部模块可以导入这个模块
const foo = 'foo' export default foo 复制代码
-
ES6 模块系统会识别作为别名提供的 default 关键字。虽然对应的值是使用命名语 法导出的,实际上则会成为默认导出
const foo = 'foo'; // 等同于 export default foo; export { foo as default }; 复制代码
-
命名导出和默认导出不会冲突
const foo = 'foo'; const bar = 'bar'; export { bar }; export default foo; 复制代码
-
这两个 export 语句可以组合为一行
const foo = 'foo'; const bar = 'bar'; export { foo as default, bar }; 复制代码
-
ES6 规范对不同形式的 export 语句中可以使用什么不可以使用什么规定了限制
- 某些形式允许声明和赋值
- 某些形式只允许表达式
- 某些形式则只允许简单标识符
// 命名行内导出 export const baz = 'baz'; export const foo = 'foo', bar = 'bar'; export function foo() {} export function* foo() {} export class Foo {} // 命名子句导出 export { foo }; export { foo, bar }; export { foo as myFoo, bar }; // 默认导出 export default 'foo'; export default 123; export default /[a-z]*/; export default { foo: 'foo' }; export { foo, bar as default }; export default foo export default function() {} export default function foo() {} export default function*() {} export default class {} // 会导致错误的不同形式: // 行内默认导出中不能出现变量声明 export default const foo = 'bar'; // 只有标识符可以出现在 export 子句中 export { 123 as foo } // 别名只能在 export 子句中出现 export const foo = 'foo' as myFoo; 复制代码
-
-
声明、赋值和导出标识符最好分开。这样就不容易搞错了.
5、模块导入
- 通过使用import 关键字使用其他模块导出的值
- 与export类似,import必须出现在模块顶级
// 允许
import ...
// 不允许
if (condition) {
import ...
}
复制代码
- import 语句被提升到模块顶部。所以import语句与使用导入值的语句的相对位置并不重要。
// 允许
import { foo } from './fooModule.js'
console.log(foo) // 'foo'
// 允许 不好
console.log(foo) // 'foo'
import { foo } from './fooModule.js'
复制代码
- 模块标识符可以是相对于当前模块的相对路径,也可以是指向模块文件的绝对路径。
- 必须是纯字符串,不能是动态计算的结果。
- 原生加载模块,文件必须带有.js扩展名,不然可能无法正确解析。如果是通过构建工具或第三方模块加载器打包或解析的 ES6 模块,则可能不需要包含文件扩展名
// 解析为/components/bar.js
import ... from './bar.js';
// 解析为/bar.js
import ... from '../bar.js';
// 解析为/bar.js
import ... from '/bar.js';
复制代码
- 不是必须通过导出的成员才能导入模块。
import './foo.js'
复制代码
- 导入对模块而言是只读的,相当于const声明的变量,在使用*批量导入时,赋值给别名的命名导出就好像使用 Object.freeze()冻结过一样。直接修改导出的值是不可能的,但可以修改导出对象的属性。
- 不能给导出的集合添加或删除导出的属性。要修改导出的值,必须使用有内部变量和属性访问权限的导出方法
import foo, * as Foo from './foo.js';
foo = 'foo'; // 错误
Foo.foo = 'foo'; // 错误
foo.bar = 'bar'; // 允许
复制代码
- 命名导出和默认导出的区别反映在他们的导入上。
const foo = 'foo', bar = 'bar', baz = 'baz';
export { foo, bar, baz }
import * as Foo from './foo.js';
console.log(Foo.foo); // foo
console.log(Foo.bar); // bar
console.log(Foo.baz); // baz
复制代码
- 要指名导入,需要把标识符放在 import 子句中。使用 import 子句可以为导入的值指定别名
import { foo, bar, baz as myBaz } from './foo.js'
console.log(foo); // foo
console.log(bar); // bar
console.log(myBaz); // baz
复制代码
- 默认导出就好像整个模块就是导出的值一样, 可以使用 default 关键字并提供别名来导入
// 等效
import { default as foo } from './foo.js';
import foo from './foo.js';
复制代码
- 如果模块同时导出了命名导出和默认导出,则可以在import语句中同时取得它们
import foo, { bar, baz } from './foo.js'
import { default as foo, bar, baz } from './foo.js'
import foo, * as Foo from './foo.js'
复制代码
6、模块转移导出
-
模块导入的值可以直接通过管道转移到导出,也可以将默认导出转换为命名导出
-
想把一个模块的所有命名导出集中在一块,可以像下面这样在 bar.js 中使用*导出
export * from './foo.js'
复制代码
- 要注意是否有重写风险。
// foo.js
export const baz = 'origin:foo'
// bar.js
export * from './foo.js'
export const baz = 'origin:bar'
// main.js
import { baz } from './bar.js'
console.log(baz) // origin:bar
复制代码
- 可以明确列出要从外部模块转移本地导出的值
export { foo, bar as myBar } from './foo.js'
复制代码
- 外部模块的默认导出可以重用为当前模块的默认导出
- 只是把导入的引用传给了原始模块
- 原始模块中,导入值仍是可用的,与修改导入相关的限制也适用于再次导出的导入
export { default } from './foo.js'
复制代码
- 重新导出时,还可以在导入模块修改命名或默认导出的角色。
export { foo as default } from './foo.js'
复制代码
7、工作者模块
- ES6模块与 Worker 实例完全兼容。在实例化时,可以给工作者传入一个指向模块文件的 路径,与传入常规脚本文件一样。
- Worker 构造函数接收第二个参数,用于说明传入的是模块文件。
// 第二个参数默认为{ type: 'classic' }
const scriptWorker = new Worker('scriptWorker.js');
const moduleWorker = new Worker('moduleWorker.js', { type: 'module' });
复制代码
8、向后兼容
- 基于模块的版本与基于脚本的版本,使用第三方模块系统(如 SystemJS)或在构建时 将 ES6 模块进行转译
- 第一种方案涉及在服务器上检查浏览器的用户代理,与支持模块的浏览器名单进行匹配
- 不可靠,比较麻烦,不推荐。
- 更好、更 优雅的方案是利用脚本的 type 属性和 nomodule 属性
- 浏览器在遇到
<script>
标签上无法识别的 type 属性时会拒绝执行其内容 - 原生支持 ECMAScript 6 模块的浏览器也会识别 nomodule 属性。此属性通 知支持 ES6 模块的浏览器不执行脚本
// 支持模块的浏览器会执行这段脚本
// 不支持模块的浏览器不会执行这段脚本
<script type="module" src="module.js"></script>
// 支持模块的浏览器不会执行这段脚本
// 不支持模块的浏览器会执行这段脚本
<script nomodule src="script.js"></script>
复制代码
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END