以史为镜,可以知兴替。
什么是模块化
按照惯例,先对我们讨论的事物找个定义或者下个定义,毕竟掌控定义权才掌控话语权。
定义
模块化编程是编程范式中结构化编程范式中的一种,是强调将计算机程序的功能分离成独立的、可相互改变的“模块”(module)的软件设计技术,它使得每个模块都包含着执行预期功能的一个唯一方面(aspect)所必需的所有东西。
模块接口表达了这个模块所提供的和所要求的元素。这些在接口中定义的元素可以被其他模块检测。模块实现包含了工作代码,它们对应于在接口中声明的元素。模块化编程密切相关于结构化编程和面向对象编程,他们有着通过分解成更小部分的方式,促进大型软件和系统的建构的相同目标,并且都大致起源于1960年代。尽管这些术语的历史上的用法曾经是不兼容的,“模块化编程”现在指称将整个程序的代码分开成各部分的高层分解:结构化编程是采用结构化控制流的低层代码使用,而面向对象编程是对象)的“数据”使用,对象是某种数据结构。
历史
内联脚本
绝大部分前端开发者,应该都是从这类代码开始写的,在一个 HTML,写个 script 标签,并在 script 标签内编写所需的逻辑。
这样的编写方式完全无需担心外部依赖可能带来的影响,但是却有如下的缺陷:
- 复用性低:如果我们试图将页面中的一些逻辑复用到其他页面,我们只能通过复制粘贴的方式来完成。
- 依赖顺序:你必须确保使用前定义好的想使用的方法或变量
- 全局作用域的污染:任何在 Script 标签内直接定义的变量都将直接注入到全局变量中。
<h1>
The Answer is
<span id="answer"></span>
</h1>
<script type="text/javascript">
function add(a, b) {
return a + b;
}
function reduce(arr, iteratee) {
var index = 0,
length = arr.length,
memo = arr[index];
for(index += 1; index < length; index += 1){
memo = iteratee(memo, arr[index])
}
return memo;
}
function sum(arr){
return reduce(arr, add);
}
/* Main Function */
var values = [ 1, 2, 4, 5, 6, 7, 8, 9 ];
var answer = sum(values)
document.getElementById("answer").innerHTML = answer;
</script>
Script 标签
从内联脚本过渡,我们为了解决复用性问题,将不同独立功能都封装到不同的 js 文件中,并使用 <script/>
引入。
但依旧无法解决一些问题:
- 依赖顺序:你必须确保使用前引入你想使用的方法或变量
- 全局作用域的污染
但好处是,解决了复用性问题,复用性不仅以为着,你当前编写的代码可以在想用的地方使用,同时你也可以使用其他你想使用的功能工具,比如 JQuery
<h1>
The Answer is
<span id="answer"></span>
</h1>
<script type="text/javascript" src="./add.js"></script>
<script type="text/javascript" src="./reduce.js"></script>
<script type="text/javascript" src="./sum.js"></script>
<script type="text/javascript" src="./main.js"></script>
// -------------add.js--------------
function add(a, b) {
return a + b;
}
// -------------reduce.js--------------
function reduce(arr, iteratee) {
var index = 0,
length = arr.length,
memo = arr[index];
index += 1;
for(; index < length; index += 1) {
memo = iteratee(memo, arr[index])
}
return memo;
}
// -------------sum.js--------------
function sum(arr){
return reduce(arr, add);
}
// -------------main.js--------------
var values = [ 1, 2, 4, 5, 6, 7, 8, 9 ];
var answer = sum(values)
document.getElementById("answer").innerHTML = answer;
模块对象和IIFE(立即执行函数)
为了解决全局变量污染的问题,我们使用对象和立即执行函数来减少对全局变量的污染。我们使用 IIFE 将方法注入到一个全局对象中,使用区域只需要从这个全局变量中取出需要的方法即可。
虽然它的使用上比上一个方式优雅的多,但还是有上面的问题:
- 依赖顺序:你必须确保使用前引入你想使用的方法或变量
- 全局作用域的污染:但没有上面方法的污染程度高
<h1>
The Answer is
<span id="answer"></span>
</h1>
<script type="text/javascript" src="./my-app.js"></script>
<script type="text/javascript" src="./add.js"></script>
<script type="text/javascript" src="./reduce.js"></script>
<script type="text/javascript" src="./sum.js"></script>
<script type="text/javascript" src="./main.js"></script>
// -------------my-app.js--------------
var myApp = {}
// -------------add.js--------------
(function(){
myApp.reduce = function(arr, iteratee) {
var index = 0,
length = arr.length,
memo = arr[index];
index += 1;
for(; index < length; index += 1){
memo = iteratee(memo, arr[index])
}
return memo;
}
})();
// -------------reduce.js--------------
(function(){
myApp.sum = function(arr){
return myApp.reduce(arr, myUtil.add);
}
})();
// -------------sum.js--------------
(function(app){
var values = [ 1, 2, 4, 5, 6, 7, 8, 9 ];
var answer = app.sum(values)
document.getElementById("answer").innerHTML = answer;
})(myApp);
CommonJS
规范:https://javascript.ruanyifeng.com/nodejs/module.html
CommonJS是一个项目,其目标是为JavaScript在网页浏览器之外创建模块约定。创建这个项目的主要原因是当时缺乏普遍可接受形式的JavaScript脚本模块单元,模块在与运行JavaScript脚本的常规网页浏览器所提供的不同的环境下可以重复使用。 这个项目由Mozilla工程师Kevin Dangoor于2009年1月发起,最初名为ServerJS[1]。在2009年8月,这个项目被改名为“CommonJS”来展示其API的广泛的应用性[2]。有关规定在一个开放进程中被创建和认可,一个规定只有在已经被多个实现完成之后才被认为是最终的[3]。 CommonJS不隶属于致力于ECMAScript的Ecma国际的工作组 TC39,但是TC39的一些成员参与了这个项目[4]。 在2013年5月,Node.js包管理器npm的作者Isaac Z. Schlueter,宣布Node.js已经废弃了CommonJS,Node.js核心开发者应避免使用它[5]。
CommonJS 是为了非浏览器环境(服务端、指令行工具、混合应用)所准备的,所以他跟上面的方式不同,上面是基于浏览器自身所创造出来的解决方案,而 CommonJS 则是一个规范。
CommonJS规范规定,每个模块内部,module
变量代表当前模块。这个变量是一个对象,它的exports
属性(即module.exports
)是对外的接口。加载某个模块,其实是加载该模块的module.exports
属性。
特点
- 所有代码都运行在模块作用域,不会污染全局作用域。
- 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
- 删除缓存:
delete require.cache[moduleName]
- 删除缓存:
- 模块加载的顺序,按照其在代码中出现的顺序。
Module
Node 内部提供一个 Module
构建函数。所有模块都是 Module
的实例。
module.id // 模块的识别符,通常是带有绝对路径的模块文件名。
module.filename // 模块的文件名,带有绝对路径。
module.loaded // 返回一个布尔值,表示模块是否已经完成加载。
module.parent // 返回一个对象,表示调用该模块的模块。 如果当前文件是入口文件则为 null
module.children // 返回一个数组,表示该模块要用到的其他模块。
module.exports // 表示模块对外输出的值。
const eg = {
id: '.',
exports: { '$': [Function] },
parent: null,
filename: '/path/to/example.js',
loaded: false,
children:
[ { id: '/path/to/node_modules/jquery/dist/jquery.js',
exports: [Function],
parent: [Circular],
filename: '/path/to/node_modules/jquery/dist/jquery.js',
loaded: true,
children: [],
paths: [Object] } ],
paths:
[ '/home/user/deleted/node_modules',
'/home/user/node_modules',
'/home/node_modules',
'/node_modules' ]
}
Require
require
命令的基本功能是,读入并执行一个 JavaScript 文件,然后返回该模块的 exports 对象。如果没有发现指定模块,会报错。
加载规则
**
- 如果参数字符串以“/”开头,则表示加载的是一个位于绝对路径的模块文件。比如,
require('/home/marco/foo.js')
将加载/home/marco/foo.js
。 - 如果参数字符串以“./”开头,则表示加载的是一个位于相对路径(跟当前执行脚本的位置相比)的模块文件。比如,
require('./circle')
将加载当前脚本同一目录的circle.js
。 - 如果参数字符串不以“./“或”/“开头,则表示加载的是一个默认提供的核心模块(位于Node的系统安装目录中),或者一个位于各级node_modules目录的已安装模块(全局安装或局部安装)。
- 如果参数字符串不以“./“或”/“开头,而且是一个路径,比如
require('example-module/path/to/file')
,则将先找到example-module
的位置,然后再以它为参数,找到后续路径。 - 如果指定的模块文件没有发现,Node会尝试为文件名添加
.js
、.json
、.node
后,再去搜索。.js
件会以文本格式的JavaScript脚本文件解析,.json
文件会以JSON格式的文本文件解析,.node
文件会以编译后的二进制文件解析。 - 如果想得到
require
命令加载的确切文件名,使用require.resolve()
方法。**
AMD(Asynchronous Module Definition)
规范:https://github.com/amdjs/amdjs-api/wiki/AMD-(%E4%B8%AD%E6%96%87%E7%89%88))
相较于 CommonJS 是为了非浏览器而设计的模块化标准,AMD 则是为了浏览器而设计的模块化标准,前者在模块的加载上是同步的,而后者的加载是异步的,异步的加载方式能更好的适应浏览器的业务场景,防止模块的加载使得浏览器假死或者阻塞。
define
AMD 之定义了一个函数 “define”,它是全局变量。函数描述为
define(id?: string, dependencies?: string[], factory: Function | object);
- id:当前模块的唯一 ID,可忽略
- dependencies:当前模块所需要引入的依赖
- factory:为模块初始化要执行的函数或对象。如果为函数,它应该只被执行一次。如果是对象,此对象应该为模块的输出值
如果前两个参数都被忽略,则切换至 CommonJS 的模式,会对 factory 传入 require
, exports
, module
三个参数来实现模块化的使用
define.amd
AMD 规范中指出,为了清晰的标识全局函数(为浏览器加载script必须的)遵从AMD编程接口,任何全局函数应该有一个”amd”的属性,它的值为一个对象。这样可以防止与现有的定义了define函数但不遵从AMD编程接口的代码相冲突。define.amd对象的属性没有包含在本规范中。实现本规范的作者,可以用它通知超出本规范编程接口基本实现的额外能力。
define.amd 的存在表明函数遵循本规范。如果有另外一个版本的编程接口,那么应该定义另外一个属性,如define.amd2,表明实现只遵循该版本的编程接口。
CommonJS 和 AMD 两个规范的出现,以及基于两者规范的模块化实现终于提出了有效的依赖解决方案以及解决了全局变量污染的问题,我们只需要去关注每个文件或者每个模块的依赖性就可以。
RequireJS
AMD 规范可以帮助我们在浏览器端解决传统依赖分解及全局变量污染的问题,但我们该怎么使用它呢?因为 AMD 只是一个规范,而不是一个解决方案。
RequireJS 就是一个基于 AMD 规范的模块加载器,并且它可以异步的加载我们所需要的依赖
虽然 RequireJS 的名字容易和 CommonJS 中的 require
混淆,但可以明确地是,RequireJS 设计之初并不是为了 CommonJS 而出现的(虽然是 CommonJS 规范衍生出来的),当然在后续的更迭中,他也是可以支持 CommonJS 规范的使用。
基本使用
使用 script 标签引入 require.js
, 并使用自定义标签属性 data-main
告诉 require.js
,文件入口在哪里,比如下方的例子:
用户请求 index.html
, 通过 script 标签去请求 require.js
, require.js
通过入口文件去解析,使得工程能完整运行
<h1>
The Answer is
<span id="answer"></span>
</h1>
<script data-main="main" src="require.js"></script>
// -------------main.js--------------
define(['sum'], function(sum){
var values = [ 1, 2, 4, 5, 6, 7, 8, 9 ];
var answer = sum(values)
document.getElementById("answer").innerHTML = answer;
})
// -------------sum.js--------------
define(['add', 'reduce'], function(add, reduce){
var sum = function(arr){
return reduce(arr, add);
};
return sum;
})
// -------------add.js--------------
define([], function(){
var add = function(a, b){
return a + b;
};
return add;
});
// -------------reduce.js--------------
define([], function(){
var reduce = function(arr, iteratee) {
var index = 0,
length = arr.length,
memo = arr[index];
index += 1;
for(; index < length; index += 1){
memo = iteratee(memo, arr[index])
}
return memo;
}
return reduce;
})
虽然 RequireJS 和 AMD 解决了我们之前所描述的绝大多数问题,但它其实还是有不少小问题需要我们去注意
- AMD 的语法过于冗长,他会将我们 factory 中的代码全部存下来,包括缩进。对于小一点的文件当然没有什么太大的问题,但是如果遇到大一点的模块或者文件,其中无用的消耗则会大大增多
- AMD 的依赖引入不够优雅,当项目逐渐变大,单个模块的引入也会开始变得复杂,模块与模块之间依赖关系的维护也会变得十分繁重。
- 在当前主流的浏览器中(HTTP1.1),加载太多的小文件会导致性能有所降低
CMD(Common Module Definition)
规范:https://github.com/cmdjs/specification/blob/master/draft/module.md
CMD 与 AMD 的目标一致,都是针对浏览器的模块化标准, 但是 API 心智负担更低,同时更契合 CommonJS 的模块化设计方案。
define
CMD 与 AMD 的 define 有一些细微的差异,那就是 CMD 只能接收一个工厂函数或者直接的值
define(factory: object | string | (require, exports, module) => any)
require
**require
用来获取指定模块的接口
define(function(require) {
// 获取模块 a 的接口
var a = require('./a');
// 调用模块 a 的方法
a.doSomething();
});
require.async
**
用来在模块内部异步加载一个或多个模块。
define(function(require) {
// 异步加载一个模块,在加载完成时,执行回调
require.async('./b', function(b) {
b.doSomething();
});
// 异步加载多个模块,在加载完成时,执行回调
require.async(['./c', './d'], function(c, d) {
c.doSomething();
d.doSomething();
});
});
exports
用来在模块内部对外提供接口。
define(function(require, exports) {
// 对外提供 foo 属性
exports.foo = 'bar';
// 对外提供 doSomething 方法
exports.doSomething = function() {};
});
module.export
**
与 exports
类似,用来在模块内部对外提供接口。
define(function(require, exports, module) {
// 对外提供接口
module.exports = {
name: 'a',
doSomething: function() {};
};
});
Sea.js
Sea.js 是 CMD 规范的产物(当然也可以将 CMD 理解为 Sea.js 诞生后的产物),所以自然而然的完全遵循 CMD 规范设计。
有一点比较特殊,因为 sea.js 实际上也是跟 AMD 一样提前加载依赖,只不过通过正则去获取 require 的模块,所以 require 这个关键字不能变形或者重命名
直接示例吧
<h1>
The Answer is
<span id="answer"></span>
</h1>
<script src="sea.js"></script>
<script>
seajs.config({base: './'})
seajs.use('./main.js')
</script>
// -------------main.js--------------
define(function(require, exports, module){
const sum = require('./sum')
var values = [ 1, 2, 4, 5, 6, 7, 8, 9 ];
var answer = sum(values)
document.getElementById("answer").innerHTML = answer;
})
// -------------sum.js--------------
define(function(require, exports, module){
const add = require('./add')
const reduce = require('./reduce')
module.export = function(arr){
return reduce(arr, add);
};
})
// -------------add.js--------------
define(function(require, exports, module) {
module.export function(a, b){
return a + b;
};
});
// -------------reduce.js--------------
define(function(require, exports, module){
module.exports = function(arr, iteratee) {
var index = 0,
length = arr.length,
memo = arr[index];
index += 1;
for(; index < length; index += 1){
memo = iteratee(memo, arr[index])
}
return memo;
}
})
Browserify
由于一些原因,开发者们希望将 CommonJS 规范运用于浏览器中,但由于 CommonJS 设计之初是为了非浏览器端,也就是服务端等环境所准备的。
后来 Browserify 出现,解决了开发们这一需求,在浏览器中使用 CommonJS。
Browserify 是一个模块打包工具,Browserify 会遍历模块,并将需要的模块打包到一个文件中。
不像 RequireJS 这样的模块加载库,Browserify 是一个指令行工具,我们需要 NodeJS 和 npm 来安装它
$ npm install -g browserify
让我们尝试用之前的案例来编写一边
<h1>
The Answer is
<span id="answer"></span>
</h1>
<script src="bundle.js"></script>
// -------------main.js--------------
var sum = require('./sum');
var values = [ 1, 2, 4, 5, 6, 7, 8, 9 ];
var answer = sum(values)
document.getElementById("answer").innerHTML = answer;
// -------------sum.js--------------
var reduce = require('./reduce');
var add = require('./add');
module.exports = function(arr){
return reduce(arr, add);
};
// -------------add.js--------------
module.exports = function add(a,b){
return a + b;
};
// -------------reduce.js--------------
module.exports = function reduce(arr, iteratee) {
var index = 0,
length = arr.length,
memo = arr[index];
index += 1;
for(; index < length; index += 1){
memo = iteratee(memo, arr[index])
}
return memo;
};
其中 index.html
中引入的 bundle.js
是 Browserify 打包后的结果
$ browserify main.js -o bundle.js
Browserify 会解析 main.js
通过 CommonJS 规范的语法来获取项目的依赖关系树,并将他们打包成一个文件,这就是 bundle.js
(function e(t,n,r){
function s(o,u){
if(!n[o]){
if(!t[o]){
var a=typeof require=="function"&&require;
if(!u&&a)return a(o,!0);
if(i)return i(o,!0);
var f=new Error("Cannot find module '"+o+"'");
throw f.code="MODULE_NOT_FOUND",f
}
var l=n[o]={exports:{}};
t[o][0].call(l.exports,function(e){
var n=t[o][1][e];
return s(n?n:e)
},l,l.exports,e,t,n,r)
}
return n[o].exports
}
var i=typeof require=="function"&&require;
for(var o=0;o<r.length;o++)
s(r[o]);
return s
})({
1:[function(require,module,exports){
module.exports = function add(a,b){
return a + b;
};
},{}],
2:[function(require,module,exports){
var sum = require('./sum');
var values = [ 1, 2, 4, 5, 6, 7, 8, 9 ];
var answer = sum(values)
document.getElementById("answer").innerHTML = answer;
},{"./sum":4}],
3:[function(require,module,exports){
module.exports = function reduce(arr, iteratee) {
var index = 0,
length = arr.length,
memo = arr[index];
index += 1;
for(; index < length; index += 1){
memo = iteratee(memo, arr[index])
}
return memo;
};
},{}],
4:[function(require,module,exports){
var reduce = require('./reduce');
var add = require('./add');
module.exports = function(arr){
return reduce(arr, add);
};
},{"./add":1,"./reduce":3}]
},{},[2]);
打包后的内容很简单,先将所需的模块都已键值对的形式获取,模块获取其他模块的内容也用键值的方式获取。
UMD(Universal Module Definition)
现在我们对于模块化的解决方案已经有了,全局变量,CommonJS 和 AMD。并且每个方案都有了具体的实现。
那么问题来了,如果我们要写一个模块或者说是库的话,我们应该怎么写?
一种方式是,我们将上面描述的模块化方案都写一遍,但是这样写实在是太麻烦了,于是就有了 UMD 的诞生。
UMD(Universal Module Definition / 通用模块定义方案)就是来解决上述解决方案的兼容性问题,原理上其实就是一端 if/else
用于判断环境是什么方案,然后以对应的方案执行模块。
//sum.umd.js
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['add', 'reduce'], factory);
} else if (typeof exports === 'object') {
// Node, CommonJS-like
module.exports = factory(require('add'), require('reduce'));
} else {
// Browser globals (root is window)
root.sum = factory(root.add, root.reduce);
}
}(this, function (add, reduce) {
// private methods
// exposed public methods
return function(arr) {
return reduce(arr, add);
}
}));
ES6 module
截止目前,JS 的模块化方案已经有了 全局变量、CommonJS、AMD、UMD 等其他方案。如此多的方案一定会使人迷惑项目中应该使用哪一种解决方案,而导致这一现状的主要原因是因为 JavaScript 语言本身并没有官方的模块话解决方案,只能靠百花齐放的开源社区来实现。
但幸运的是随着 ES6 的公布,ES6 module 从莫种意义上来讲成为了官方支持的模块化解决方案。
ES6 module 主要使用 import
和 export
两个关键词来实现模块的引入导出。
// -------------main.js--------------
import sum from "./sum";
var values = [ 1, 2, 4, 5, 6, 7, 8, 9 ];
var answer = sum(values);
document.getElementById("answer").innerHTML = answer;
// -------------sum.js--------------
import add from './add';
import reduce from './reduce';
export default function sum(arr){
return reduce(arr, add);
}
// -------------add.js--------------
export default function add(a,b){
return a + b;
}
// -------------reduce.js--------------
export default function reduce(arr, iteratee) {
let index = 0,
length = arr.length,
memo = arr[index];
index += 1;
for(; index < length; index += 1){
memo = iteratee(memo, arr[index]);
}
return memo;
}
ES6 module 的语法简洁且优雅,心智成本低。但它有个核心的问题就是大多浏览器并不支持原生的 ES6 module,当然这一问题会在不远的未来被解决,同时现在我们也可以使用模块打包工具来使得 ES6 module 在浏览器中生效。
Webpack
Webpack 是一个模块打包工具,类似于 Browserify,但它比 Browserify 更强大更灵活,支持的模块化方案更多。它拥有如下特性:
- Code Split:Webpack 可以实现非常友好的代码拆分,而不是跟 Browserify 一样只能耿直的打包成一个文件。比如我们有 A、B 两个应用,同时他们有一个公共模块 Shared,在 Browserify 中你只能将 Shared 分别打包到 A、B 中,但 Webpack 却可以分别打包 A、B、Shared 三个模块。
- Loader:自定义 Loader 可以帮助开发者加载除 js、json 以外的文件,比如我们可以使用 Webpack 自身的
require()
来实现模块的异步加载。 - Plugin:Webpack 的插件可以在打包的结果写入文件之前进行个性化操作
WebpackDevServer 是一个用于开发者开发的开发服务器,主要作用是监听文件的变化并即时的响应到视图上进行 debugger。
Rollup
Rollup 跟 Webpack、Browserify 一样是模块打包工具,他跟 Webpack 一样是目前主流的打包工具,他们两者之间并没有谁更好谁更强的问题。因为他们两者解决不同的问题。
Rollup 主要面向的是 JavaScript 库,而 Webpack 主要面向的是应用程序。
Rollup 可以使用任何上述的模块化方案,但官方推荐使用 ES6 module 来进行模块化编写,主要的原因是 ES6 module 支持静态分析,可以帮助打包工具进行摇树优化,进而减少生成的代码体积。
System.js
System.js 是一个遵循 UMD 的通用模块加载器。它支持上述的模块化解决方案,同时它不仅可以加在 JavaScript,还可以借助运行时编译器加载 CoffeeScript 和 TypeScript 等更多类型的资源。
System.js 的一个有点是它建立在 ES6 module loader polyfill 之上,使得模块化的使用更具兼容性且面向未来。
System.js 还支持加载远程模块,也是这一特点比大多数 UMD 模块加载器更好。
基本使用
<script src="//cdn.jsdelivr.net/npm/systemjs/dist/system.js"></script>
<script type="systemjs-module" src="./entry.js"></script>
<script type="systemjs-importmap">
{
"imports": {
"module-name": "module-url"
}
}
</script>
JSPM
JSPM 是一个不用基于 node 的包管理工具,直接面向浏览器。在现代开发中,我们需要使用 node 和 npm 来完成依赖的管理。而 JSPM 则是直接脱离 node 和 npm 实现的依赖管理。
同时他还是一个 模块加载器、模块打包器,全部过程离开了 node,哪怕是 debug 也只需要打开 index.html 即可。
但是项目的初始化还是需要 npm 帮助一下的
$ npm init -y
$ npm install jspm & jspn init
包的安装
$ jspm install npm:package-name or github:package-name
打包
$ jspm bundle main.js bundle.js
其他
AMD、CMD 的区别
AMD、CMD 都是针对浏览器设计的模块加载规范,两者都有对应的模块加载器的实现,分别是 RequireJS 和 SeaJS
两者其实都脱胎于先行的 CommonJS,所以有不少思想和顶层设计都有 CommonJS 的影子。
两者仅在规范上来说,只是 API 设计上有较大的差异,AMD 推崇 依赖前置,CMD 推崇 依赖就近
前者性能高,但是开发成本,心智成本较高
后者性能相对较低,但是开发成本,心智成本低
AMD 的 API 设计相对复杂,CMD 的 API 设计精简(所以我更喜欢 CMD)
CommonJS、ES Module 的区别
两者严格来说其实是两个时代的产物,CommonJS 是由社区在 2009~2010 设计的,ES module 则是由标准化组织 ESMA 在 2015 年制定的。
前者从诞生之初就是为非浏览器端的 JS 设计的,而 ES module 则不对具体环境区分。
CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
CommonJS 是单个值导出,ES6 Module可以导出多个
CommonJS 是动态语法可以写在判断里,ES6 Module 静态语法只能写在顶层
CommonJS 的 this 是当前模块,ES6 Module的 this 是 undefined
ES6 module 支持 top-level await,CommonJS 却不行
Webpack、Rollup 的区别
webpack 强大在于 loader 和 plugin,前者使得 webpack 可以以各种方式加载各式各样的资源,后者可以使得打包过程自定义。所以往往被用于应用程序的搭建。
rollup 默认支持 ES6 module,并有强悍的优化,往往被用于纯 js 类库的构建打包。
什么是 tree-shaking,它的原理是什么
tree-shaking的目的,就是通过减少web项目中JavaScript的无用代码,以达到减少用户打开页面所需的等待时间,来增强用户体验
由于 ES6 module 是静态的,但 CommonJS 可以是动态的,所以 tree-shaking目前支持 ES6 module 组织的代码
其原理主要是通过静态的词法分析和语法分析来判断代码是否是无用的,该过程十分谨慎,如不能百分之百保证代码是无用的,则不会轻易删除
将代码内聚性提高,耦合度变低,尽量少的写一些容易产生副作用的代码,可以让 tree-shaking 发挥最大的作用
同时 babel 其实是 tree-shaking 一大阻碍之一,虽然前者可以帮助我们十分痛快的使用最新特性的 JS,但是各种 polyfill 让代码会产生各种可能的副作用,让 tree-shaking 的作用变小
Systemjs 和 JSPM 对前端的影响
SystemJS 是一个原生的浏览器的模块加载器,由于浏览器 ES6 module 还没有广泛支持,所以 SystemJS 莫种意义上可以理解为浏览器 ES6 module 的 polyfill。
JSPM 是一个基于 SystemJS 的浏览器端的包管理工具,我们现在使用的 npm 是 nodejs 环境下的包管理工具,我们无法直接使用在浏览器上。而 JSPM 的出现可以直接跳过现在普遍的方案,通过 npm 包管理,然后使用打包工具再将应用到浏览器。现在可以直接安装包,然后直接应用在浏览器上。
前者现在广泛应用于通用微前端的解决方案,后者还未可知。
模块化的未来发展趋势
以史为镜,可以知兴替。
从历史的发展角度我们可以发现,每一次前端模块化的进步都是基础建设的提升而带来。
个人认为未来的模块化一定是随着浏览器对 ES module 的支持越来越高而会发生巨大的变化,从此衍生出来的包管理工具,模块加载器,模块打包器将会逐渐专精于某一领域的工程优化,从而降低开发人员的心智成本。
也随着游览器对 ES module 的不断支持,微前端这类抽象模块化方案也有可能有新的突破。
参考
- 模块化编程
- Brief history of JavaScript Modules
- 立即调用函数表达式
- CommonJS规范
- module 对象
- AMD)
- CMD
- SeaJS从入门到原理
- ES module
- systemjs
- JavaScript模块化编程简史(2009-2016)
- 前端模块化开发那点历史
- AMD 和 CMD 的区别有哪些?
- Module 的语法
- CommonJs和ES6 module的区别是什么呢?
- Node Modules at War: Why CommonJS and ES Modules Can’t Get Along
- Module 的语法
- Module 的加载实现
- Tree-Shaking性能优化实践 - 原理篇