但大部分人认为浏览器和服务器环境差别太大,毕竟浏览器端 JS 是通过网络动态依次加载的,而不是像服务端 JS 存在本地磁盘中。因此,浏览器需要实现的是异步模块,模块在定义的时候就必须先指明它所需要依赖的模块,然后把本模块的代码写在回调函数中去执行,最终衍生出了 AMD 规范。
特点:
- 前置依赖,异步加载
- 便于管理模块之间的依赖性,有利于代码的编写和维护。
它的用法看起来是这样的:
// a.js
define(function (require, exports, module) {
console.log('a.js');
exports.name = 'Jack';
});
// b.js
define(function (require, exports, module) {
console.log('b.js');
exports.desc = 'Hello World';
});
// main.js
require(['a', 'b'], function (moduleA, moduleB) {
console.log('main.js');
console.log(moduleA.name + ', ' + moduleB.desc);
});
// 执行顺序:
// a.js
// b.js
// main.js
人无完人,AMD/RequireJS 也存在饱受诟病的缺点。按照 AMD 的规范,在定义模块的时候需要把所有依赖模块都罗列一遍(前置依赖),而且在使用时还需要在 factory 中作为形参传进去。
define(['a', 'b', 'c', 'd', 'e', 'f', 'g'], function(a, b, c, d, e, f, g){ ..... });
看起来略微不爽 ...
RequireJS 模块化的顺序是这样的:模块预加载 => 全部模块预执行 => 主逻辑中调用模块,所以实质是依赖加载完成后还会预先一一将模块执行一遍,这种方式会使得程序效率有点低。
所以 RequireJS 也提供了就近依赖,会在执行至 require 方法才会去进行依赖加载和执行,但这种方式的用户体验不是很好,用户的操作会有明显的延迟(下载依赖过程),虽然可以通过各种 loading 去解决。
// 就近依赖
define(function () {
setTimeout(function () {
require(['a'], function (moduleA) {
console.log(moduleA.name);
});
}, 1000);
});
CMD 规范(Common Module Definition)
AMD/RequireJS 的 JS 模块实现上有很多不优雅的地方,长期以来在开发者中广受诟病,原因主要是不能以一种更好的管理模块的依赖加载和执行,虽然有不足的地方,但它提出的思想在当时是非常先进的。
既然优缺点那么必然有人出来完善它,SeaJS 在这个时候出现。
SeaJS 遵循的是 CMD 规范,CMD 是在 AMD 基础上改进的一种规范,解决了 AMD 对依赖模块的执行时机处理问题。
SeaJS 模块化的顺序是这样的:模块预加载 => 主逻辑调用模块前才执行模块中的代码,通过依赖的延迟执行,很好解决了 RequireJS 被诟病的缺点。
SeaJS 用法和 AMD 基本相同,并且融合了 CommonJS 的写法:
// a.js
define(function (require, exports, module) {
console.log('a.js');
exports.name = 'Jack';
});
// main.js
define(function (require, exports, module) {
console.log('main.js');
var moduleA = require('a');
console.log(moduleA.name);
});
// 执行顺序
// main.js
// a.js
除此之外,SeaJS 还提供了 async API,实现依赖的延迟加载。
// main.js
define(function (require, exports, module) {
var moduleA = require.async('a');
console.log(moduleA.name);
});
SeaJS 的出现,貌似以一种比较完美的形式解决了 JS 模块化的问题,是 CommonJS 在浏览器端的践行者,并吸收了 RequestJS 的优点。
ES Module
ES Module 是目前 web 开发中使用率最高的模块化标准。
随着 JS 模块化开发的呼声越来越高,作为 JS 语言规范的官方组织 ECMA 也开始将 JS 模块化纳入 TC39 提案中,并在 ECMAScript 6.0 中得到实践。
ES Module 吸收了其他方案的优点并以更优雅的形式实现模块化,它的思想是尽量的静态化,即在编译时就确定所有模块的依赖关系,以及输入和输出的变量,和 CommonJS 和 AMD/CMD 这些标准不同的是,它们都是在运行时才能确定需要依赖哪一些模块并且执行它。ES Module 使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,实现一些只能靠静态分析实现的功能(比如引入宏(macro)和类型检验(type system)。
标准内容:
- 模块功能主要由两个命令构成:
export 和 import。export 命令用于规定模块的对外接口,import 命令用于输入其他模块提供的功能。
- 通过
export 命令定义了模块的对外接口,其他 JS 文件就可以通过 import 命令加载这个模块。
ES Module 可以有多种用法:
模块的定义:
/**
* export 只支持对象形式导出,不支持值的导出,export default 命令用于指定模块的默认输出,
* 只支持值导出,但是只能指定一个,本质上它就是输出一个叫做 default 的变量或方法
*/
// 写法 1
export var m = 1;
// 写法 2
var m = 1;
export { m };
// 写法 3
var n = 1;
export { n as m };
// 写法 4
var n = 1;
export default n;
模块的引入:
// 解构引入
import { firstName, lastName, year } from 'a-module';
// 为输入的变量重新命名
import { lastName as surname } from 'a-module';
// 引出模块对象(引入所有)
import * as ModuleA from 'a-module';
在使用 ES Module 值得注意的是:import 和 export 命令只能在模块的顶层,在代码块中将会报错,这是因为 ES Module 需要在编译时期进行模块静态优化,import 和 export 命令会被 JavaScript 引擎静态分析,先于模块内的其他语句执行,这种设计有利于编译器提高效率,但也导致无法在运行时加载模块(动态加载)。
对于这个缺点,TC39 有了一个新的提案 -- Dynamic Import,提案的内容是建议引入 import()方法,实现模块动态加载。
// specifier: 指定所要加载的模块的位置
import(specifier)
import() 方法返回的是一个 Promise 对象。
import('b-module')
.then(module => {
module.helloWorld();
})
.catch(err => {
console.log(err.message);
});
import() 函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用。它是运行时执行,也就是说,什么时候运行到这一句,就会加载指定的模块。另外,import() 函数与所加载的模块没有静态连接关系,这点也是与 import 语句不相同。import() 类似于 Node 的 require 方法,区别主要是前者是异步加载,后者是同步加载。
通过 import 和 export 命令以及 import() 方法,ES Module 几乎实现了 CommonJS/AMD/CMD 方案的所有功能,更重要的是它是作为 ECMAScript 标准出现的,带有正统基因,这也是它在现在 Web 开发中广泛应用的原因之一。
但 ES Module 是在 ECMAScript 6.0 标准中的,而目前绝大多数的浏览器并直接支持 ES6 语法,ES Module 并不能直接使用在浏览器上,所以需要 Babel 先进行转码,将 import 和 export 命令转译成 ES2015 语法才能被浏览器解析。
总结
JS 模块化的出现使得前端工程化程度越来越高,让使用 JS 开发大型应用成为触手可及的现实(VScode)。纵观 JS 模块化的发展,其中很多思想都借鉴了其他优秀的动态语言(Python),然后结合 JS 运行环境的特点,衍生出符合自身的标准。但其实在本质上,浏览器端的 JS 仍没有真正意义上的支持模块化,只能通过工具库(RequireJS、SeaJS)或者语法糖(ES Module)去 Hack 实现模块化。随着 Node 前端工程化工具的繁荣发展(Grunt/Gulp/webpack),使我们可以不关注模块化的实现过程,直接享受 JS 模块化编程的快感。
在复习 JS 模块化的过程中,对 Webpack 等工具的模块化语法糖转码产生了新的兴趣,希望有时间可以去分析一下模块化的打包机制和转译代码,然后整理出来加深一下自己对模块化实现原理的认识和理解。
期待下一篇。
参考文章:
原文发布时间为:2018年07月02日
原文作者:掘金
本文来源:
掘金
如需转载请联系原作者