原文连接:http://yanjiie.mejavascript
偶然的一个周末复习了一下 JS 的模块标准,刷新了一下对 JS 模块化的理解。html
从开始 Coding 以来,总会周期性地突发奇想进行 Code Review。既是对一段时期的代码进行总结,也是对那一段时光的怀念。前端
距离上一次 Review 已通过去近两个月,此次居然把两年前在源续写的代码翻了出来,代码杂乱无章的程度就像那时更加浮躁的本身,让人感慨时光流逝之快。java
话很少说,直接上码。webpack
当时在作的是一个境外电商项目(越南天宝商城),做为非 CS 的新手程序员,接触 Coding 时间不长和工程化观念不强,在当时的项目中出现了这样的代码:git
import.js: 程序员
这段代码看起来就是不断地从 DOM 中插进 CSS 和 JS,虽然写得很烂,可是很能反映之前的 Web 开发方式。es6
在 Web 开发中,有一个原则叫“关注点分离(separation of concerns)“,意思是各类技术只负责本身的领域,不互相耦合混合在一块儿,因此催生出了 HTML、CSS 和 JavaScript。github
其中,在 Web 中负责逻辑和交互 的 JavaScript,是一门只用 10 天设计出来的语言,虽然借鉴了许多优秀静态和动态语言的优势,但却一直没有模块 ( module ) 体系。这致使了它将一个大程序拆分红互相依赖的小文件,再用简单的方法拼装起来。其余语言都有这项功能,好比 Ruby
的 require
、Python
的 import
,甚至就连 CSS
都有 @import
,可是 JavaScript 任何这方面的支持都没有。并且 JS 是一种加载即运行的技术,在页面中插入脚本时还须要考虑库的依赖,JS 在这方面的缺陷,对开发大型的、复杂的项目造成了巨大障碍。web
虽然 JS 自己并不支持模块化,可是这并不能阻挡 JS 走向模块化的道路。既然自己不支持,那么就从代码层面解决问题。活跃的社区开始制定了一些模块方案,其中最主要的是 CommonJS 和 AMD,ES6 规范出台以后,以一种更简单的形式制定了 JS 的模块标准 (ES Module),并融合了 CommonJS 和 AMD 的优势。
大体的发展过程:
CommonJS(服务端) => AMD (浏览器端) => CMD / UMD => ES Module
2009年,Node.js 横空出世,JS 得以脱离浏览器运行,咱们可使用 JS 来编写服务端的代码了。对于服务端的 JS,没有模块化简直是不能忍。
CommonJs (前 ServerJS) 在这个阶段应运而生,制定了 Module/1.0 规范,定义了初版模块标准。
标准内容:
exports
来向外暴露 API,exports
只能是一个对象,暴露的 API 须做为此对象的属性。require
,经过传入模块标识来引入其余模块,执行的结果即为别的模块暴露出来的 API。require
函数引入的模块中也包含依赖,那么依次加载这些依赖。特色:
它的语法看起来是这样的:
// a.js
module.exports = {
moduleFunc: function() {
return true;
};
}
// 或
exports.moduleFunc = function() {
return true;
};
// 在 b.js 中引用
var moduleA = require('a.js');
// 或
var moduleFunc = require('a.js').moduleFunc;
console.log(moduleA.moduleFunc());
console.log(moduleFunc())
复制代码
CommonJS 规范出现后,在 Node 开发中产生了很是良好的效果,开发者但愿借鉴这个经验来解决浏览器端 JS 的模块化。
但大部分人认为浏览器和服务器环境差异太大,毕竟浏览器端 JS 是经过网络动态依次加载的,而不是像服务端 JS 存在本地磁盘中。所以,浏览器须要实现的是异步模块,模块在定义的时候就必须先指明它所须要依赖的模块,而后把本模块的代码写在回调函数中去执行,最终衍生出了 AMD 规范。
AMD 的主要思想是异步模块,主逻辑在回调函数中执行,这和浏览器前端所习惯的开发方式不谋而合,RequireJS 应运而生。
标准内容:
define
来定义模块,用法为:define(id?, dependencies?, factory)
;["require", "exports", "module"]
,factory 中也会默认传入 require, exports, module,与 ComminJS 中的实现保持一致特色:
它的用法看起来是这样的:
// 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);
});
复制代码
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 是目前 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 等工具的模块化语法糖转码产生了新的兴趣,但愿有时间能够去分析一下模块化的打包机制和转译代码,而后整理出来加深一下本身对模块化实现原理的认识和理解。
期待下一篇。
参考文章: