模块化历程
前言
在李录的《文明、现代化、价值投资与中国》里,他把人类的文明史分为三大跃升阶段:即 1.0 狩猎采集文明、2.0 农业文明和 3.0 科技文明。针对 JavaScript 的模块化,笔者认为模块化历程也可类比为这样的历程。在前端页面不复杂的情况下,我们只需引入需要的库、js 文件或模块,这就像“狩猎采集文明”时,饿了去打猎、摘果子一样,及时反馈,速度 NO1;后来前端页面复杂起来,各路前辈将优秀的模块思维带入前端,有了各种模块化方案,这就好比农业文明时代,你需要按照一种规范或说标准来写,它有约束性,但能极高的解决 1.0 时代的缺点——依赖顺序。有代表性的库如 CommonJS,RequireJS,SeaJS。再后来就是目前的 3.0 科技文明阶段,前端需要各种打包器来进行打包,它不再是简简单单的脚本文件,写几个文件就引用那么简单了,在这个阶段最出名是的 webpack,也有 gulp,rollup。当然现在还有 vite 和 snowpack 等。接下来让我们揭开模块化的历程,从历史演变来看清模块化的本质
狩猎采集阶段——IIFE 主导全球化
模块化解决的问题:
命名冲突
文件依赖
CommonJS
- 模块可以 i 多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存
- 模块记载会阻塞接下来代码的执行,需要等到模块加载完成才能继续执行——同步加载
适用场景:服务器环境。nodejs 的模块规范是参考 commonJS 实现的
用法:
1.导入:require("路径")
2.导出:module.exports 和 exports
// a.js
export.a = "hello world";
// b.js
var moduleA = require('./a.js')
console.log(moduleA.a); // hello world
module.exports 和 exports 的区别是 exports 是 module.exports 的一个引用,详单与 Node 为每个模块提供一个 exports 变量,指向 module.exports。这相当于在模块顶部,有一行 var exports = module.exports 这样的命令
以上例子可以理解为:
// 默认执行 var exports = module.exports
exports.a = 'hello world'; // 相当于:module.exports.a = 'hello world'
...
AMD
1.异步加载
2.管理模块之间的依赖性,便于代码的编写和维护
适用场景:浏览器环境,requireJS 是参考 AMD 规范实现的
用法:
1.导入:require(['模块名称'], function())
因为 Node ,有了 CommonJS,所有大家有了想在浏览器端实现模块化的想法,这个时候分为三个分支,amd,cmd 以及 es6,amd 的代表库是 requirejs,cmd 的代表是玉伯的 seajs,后来 es6 才是集大成者。而玉伯的 seajs 是从 requirejs 中分离
无论是 cmd 还是 amd 都是浏览器上的规范,是为了想在浏览器端实现模块化而做的
为什么会有模块化
- 网站正在变成 Web Apps
- 随着站点的扩大,代码的复杂性也在增加
- 需要高度解耦的 JS 文件/模块
- 部署需要几个 HTTP 调用中的优化代码
最开始的模块
从设计模块说起
function foo() {}
function bar() {}
全局被污染,很容易命名冲突
简单封装:命名空间模式
var MYAPP = {
foo: function () {},
bar: function () {},
};
MYAPP.foo();
减少全局上的变量数
本质是对象,一点都不安全
匿名闭包:IIFE 模块
var Module = (function () {
var _private = 'safe now';
var foo = function () {
console.log(_private);
};
return {
foo: foo,
};
})();
Module.foo(); // 私有变量
Module._private; // undefined
函数是 JavaScript 唯一的 Local Scope(作用域)
再增强一点:引入依赖
var Module = (function ($) {
var _$body = $('body'); // we can use jQuery now!
var foo = function () {
console.log(_$body); // 特权方法
};
return {
foo: foo,
};
})(jQuery);
Module.foo();
这就是模块模式,也是现代模块实现的基石
石器时代——脚本加载器
只有封装性可不够,我们还需要加载
让我们回到脚本标签
body script(src="jquery.js") script(src="app.js") // do some $ thins...
顺序是根本
并行加载
DOM 顺序即执行顺序
但现实是这样的...
body script(src="zepto.js") script(src="jhash.js") script(src="fastClick.js")
script(src="iScroll.js") script(src="underscore.js") script(src="handlebar.js")
script(src="datacenter.js") script(src="deferred.js")
script(src="util/wxbridge.js") script(src="util/login.js")
script(src="util/date.js") script(src="app.js") ...
难以维护
依赖模糊 不清楚的依赖关系
请求过多的 HTTP 调用
LABjs-脚本加载器
时间:2009 年
作用:加载和阻止 JavaScript
效果:使用 LABjs 将取代所有难看的“脚本标签”
它是怎么工作的
script(src="LAB.js" async)
$LAB.script("framework.js").wait()
.script("plugin.framework.js")
.script("myplugin.framework.js").wait()
.script("init.js")
先到先得(执行顺序不重要)
另外
$LAB .script(["script1.js", "script2.js", "script3.js"]) .wait(function() { //
wait for all scripts to execute first script1Func(); script2Func();
script3Func(); })
它基于文件的依赖管理
蒸汽朋克——模块加载器
YUI3 Loader - Module Loader
2009
YUI 的轻量级内核和模块化体系结构使其具有可扩展性,快速性和鲁棒性
写法
// YUI - 编写模块
YUI.add("dom", function(Y) {
Y.DOM = { ... }
})
// YUI - 使用模块
YUI().use('dom', function(Y) {
Y.DOM.doSomeThing()
// use some methods DOM attach to Y
})
创建自定义模块
// hello.js
YUI.add(
'hello',
function (Y) {
Y.sayHello = function (msg) {
Y.DOM.set(el, 'innerHTML', 'Hello!');
};
},
'3.0.0',
{
requires: ['dom'],
},
);
// main.js
YUI().use('hello', function (Y) {
Y.sayHello('hey yui loader');
});
基于模块的依赖管理
让我们再深入一下
function Sandbox() {
for (i = 0; i < modulex.length; i += 1) {
Sandbox.modulex[modules[i]](this);
}
}
YUI 其实是一个强沙箱
所有依赖模块通过 attach 的方式被注入沙盒
但还是”脚本标签“
script(src="/path/to/yui-min.js") script(src="/path/to/my/module1.js")
script(src="/path/to/my/module2.js") script(src="/path/to/my/module3.js")
YUI().use('module1', 'module2', 'module3', function (Y) {
// you can use all this module now
});
你不必按照固定顺序写脚本写标签
加载与执行分离
漏了一个问题
HTTP 调用过多
YUI 组合的工作原理
script(src="http://www.baidu.com/build/yui/yui-min.js")
script(src="http://www.baidu.com/build/dom/dom-min.js")
改造成
script(src="http://www.baidu.com/combo?
build/yui/yui-min.js&
build/dom/dom-min.js")
在单个请求中处理多个文件
GET 请求,需要服务器支持
以及
合并 concat
压缩 Minify
混淆 丑化
CommonJS
CommonJS
2009.08
不仅适用于浏览器
用法:
// math.js
exports.add = function (a, b) {
return a + b;
};
// main.js
var math = require('./math.js');
console.log(math.add(1, 2)); // 3
AMD / CMD——浏览器环境预设方案
AMD(Async Module Definition)
RequireJS 对模块定义的规范化产出
CMD(Common Module Definition)
SeaJS 对模块定义的规范化产出
browserify/gulp/webpack
CommonJs in Browser
npm install -g browserify
只需要写 CommonJs 的代码,用 browserify 帮你编译就好
browserify main.js -o bundle.js
Browserify 分析 AST 的 require() 调用,以遍历项目的整个依赖关系图
Webpack-集大成者
几乎对任何资源或资产进行转换,捆绑或打包
Rollup
ES6 模块
用 ES6 的模块来写,然后用 babel 转移成低版本的 JavaScript 语法
Vite
我想说的就是模块化的发展啊
介绍模块化发展历程
IIFE(声明即执行的函数表达式),特点:在一个单独的函数作用于中执行代码,避免变量冲突
(function () {
return {
data: [],
};
})();
AMD: 使用 requireJS 来编写模块化,特点:依赖必须提前声明好
define('./index.js', function (code) {
// code 就是index.Js 返回的内容
});
CMD: 使用 seaJS 来编写模块化,特点: 支持动态引入依赖
define(function (require, exports, moduke) {
var indexCode = require('./index.js');
});
CommonJS: nodejs 中自带的模块化
var fs = require('fs');
exports 指向 module.exports,即 exports = model.exports
UMD: 是 AMD 和 CommonJS 的糅合,跨平台的解决方案
ES Modules: ES6 引入的模块化,支持 import 来引入另一个 js
ES6 模块与 CommonJS 模块的差异
CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
AMD 和 CMD 最大的区别是对依赖模块的执行时机处理不同
AMD:依赖前置,提前执行
CMD:依赖就近,延迟执行
ESM(ES6 module)
export 只支持对象形式导出,不支持值的导出,export default 命令用于指定模块的默认输出,只支持值导出,但是只能指定一个,本质上它就是输出一个叫做 default 的变量或方法
CommonJs 因为是同步执行,所以如果在浏览器上使用 CommonJS,会引起浏览器的假死(卡住)
AMD 规范是异步加载模块,允许指定回调函数,代表函数库:require.js
require.js
主要解决两个问题:
- 异步加载模块
- 模块之间依赖模糊
CMD 是阿里的玉伯提出的,js 的函数为 sea.js
。它与 require.js
最主要的区别是实现了按需加载,推崇依赖就近原则,模块延迟执行
UMD 是 AMD 和 CommonJS 的综合产物
ifelse 的写法
ES6 的模块化,可以使用 import
关键字引入模块, 通过export
关键字导出模块
与 CommonJS 的差异
- CommonJS 模块输出的是一个值的拷贝, ES6 模块输出的是值的引用
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
IIFE
<script>
var module1 = (function () {
var x = 1;
return { a: x };
})();
</script>
<script>
var module2 = (function () {
var a = module1.a;
return { b: a };
})();
</script>
在 ES6 的模块化还没出来之前,一些库或者模块是通过 IIFE(立即执行函数)来实现的。关于 IIFE,我们之前在 JavaScript 篇的 立即执行函数(IIFE) 中提到过
但用这种方式的缺点:扩展的模块无法共享原有模块的内部属性,还有模块本身常常依赖其他模块,模块模式无法实现这些依赖关系
缺点:
- 随着项目扩展,html 文件中会包含大量的 script 标签
- script 的依赖关系难以用 script 标签的先后顺序组织
为了解决这个问题,出现了两个互相竞争的标准,即 Asynchronous Module Definition(AMD)和 CommonJS
AMD 和 CommonJS 是两个相互竞争的标准,均可以定义 JavaScript 模块。除了语法和原理的区别之外,主要的区别是 AMD 的设计理念是明确基于浏览器,而 CommonJS 的设计是面向通用 JavaScript 环境(如 Node.js 服务端),而不局限于浏览器。
AMD 异步
CommonJS 同步
AMD
AMD 可以很容易指定模块及依赖关系。同时,它支持浏览器。目前,AMD 最流行的实现是 RequireJS(http://requirejs.org/)。
使用姿势
define(['jquery', 'common', 'errorMsg'], function($, common, errorMsg) {
var Home = function() {
...
};
return Home;
})
RequireJS 声明一个模块是,必须指定所有的依赖项,这些依赖项会被当做形参传到 factory 中,对于依赖的模块会提前执行(在 RequireJS 2.0 也可以选择延迟执行),这被称为:依赖前置
但是这会带来说明问题呢?
加大了开发过程中的难度,无论是阅读之前的代码还是编写新的内容,会出现这样的情况:引入的另一个模块中的内容是条件性执行的
CMD(Common Module Definition) & SeaJS
CMD 是除 AMD 以外的另外一种模块组织规范。CMD 即 Common Module Definition,意为“通用模块定义”。
针对 AMD 规范中可以优化的部分,CMD 规范 出现了,而 SeaJS 则作为它的具体实现之一,与 AMD 十分相似:
// AMD 的一个例子,当然这是一种极端的情况
define(['header', 'main', 'footer'], function (header, main, footer) {
if (xxx) {
header.setHeader('new-title');
}
if (xxx) {
main.setMain('new-content');
}
if (xxx) {
footer.setFooter('new-footer');
}
});
// 与之对应的 CMD 的写法
define(function (require, exports, module) {
if (xxx) {
var header = require('./header');
header.setHeader('new-title');
}
if (xxx) {
var main = require('./main');
main.setMain('new-content');
}
if (xxx) {
var footer = require('./footer');
footer.setFooter('new-footer');
}
});
AMD 与 CMD 最大的区别:
一方面,在依赖的处理上
- AMD 推崇依赖前置,即通过依赖数组的方式提前声明当前模块的依赖
- CMD 推崇依赖就近,在编译需要用到的时候通过调用 require 方法动态引入
另一方面,在本模块的对外输出上
- AMD 推崇通过返回值的方式对外输出
- CMD 推崇通过给 module.exports 赋值的方式对外输出
AMD && CMD 背后的实现原理
一种解决方 案是采用 UMD(Universal Module Definition, https://github.com/umdjs/umd),这种模式的语法有点复杂,它同时支持 AMD 和 CommonJS。
ES6 模块
Babel 作为 ES6 官方指定的编译器
CommonJS 与 Sea.js
Sea.js 的初衷是为了让 ComoonJS Modules/1.1 的模块能运行在浏览器端,但由于浏览器和服务器的实质差异,实际上,无法达到,也没必要达到