深圳
Web
前端培训学习:
js
中的模块化
--【千锋】
0.
前言
我们知道最常见的模块化方案有CommonJS
、
AMD
、
CMD
、
ES6
,
AMD
规范一般用于浏览器,异步的,因为模块加载是异步的,
js
解释是同步的,所以有时候导致依赖还没加载完毕,同步的代码运行结束;
CommonJS
规范一般用于服务端,同步的,因为在服务器端所有文件都存储在本地的硬盘上,传输速率快而且稳定。
1.script
标签引入
最开始的时候,多个script
标签引入
js
文件。但是,这种弊端也很明显,很多个
js
文件合并起来,也是相当于一个
script
,造成变量污染。项目大了,不想变量污染也是很难或者不容易做到,开发和维护成本高。 而且对于标签的顺序,也是需要考虑一阵,还有加载的时候同步,更加是一种灾难,幸好后来有了渲染完执行的
defer
和下载完执行的
async
,进入新的时代了。
接着,就有各种各样的动态创建script
标签的方法,最终发展到了上面的几种方案。
2.AMD
与
CMD
2.1AMD
异步模块定义,提供定义模块及异步加载该模块依赖的机制。AMD
遵循依赖前置,代码在一旦运行到需要依赖的地方,就马上知道依赖是什么。而无需遍历整个函数体找到它的依赖,因此性能有所提升。但是开发者必须先前知道依赖具体有什么,并且显式指明依赖,使得开发工作量变大。而且,不能保证模块加载的时候的顺序。 典型代表
requirejs
。
require.js
在声明依赖的模块时会立刻加载并执行模块内的代码。
require
函数让你能够随时去依赖一个模块,即取得模块的引用,从而即使模块没有作为参数定义,也能够被使用。他的风格是依赖注入,比如:
/api.js
define('myMoudle',['foo','bar'],function(foo,bar){
//
引入了
foo
和
bar
,利用
foo
、
bar
来做一些事情
return {
baz:function(){return 'api'}
}
});
require(['api'],function(api) {
console.log(api.baz())
})
复制代码
然后你可以在中间随时引用模块,但是模块第一次初始化的时间比较长。这就像开始的时候很拼搏很辛苦,到最后是美滋滋。
2.2CMD
通用模块定义,提供模块定义及按需执行模块。遵循依赖就近,代码在运行时,最开始的时候是不知道依赖的,需要遍历所有的require
关键字,找出后面的依赖。一个常见的做法是将
function toString
后,用正则匹配出
require
关键字后面的依赖。
CMD
里,每个
API
都简单纯粹。可以让浏览器的模块代码像
node
一样,因为同步所以引入的顺序是能控制的。 对于典型代表
seajs
,一般是这样子:
define(function(require,exports,module){
//...
很多代码略过
var a = require('./a');
//
要用到
a
,于是引入了
a
//
做一些和模块
a
有关的事情
});
复制代码
对于b.js
依赖
a.js
//a.js
define(function(require, exports) {
exports.a = function(){//
也可以把他暴露出去
//
很多代码
};
});
//b.js
define(function(require,exports){
//
前面干了很多事情,突然想要引用
a
了
var fun = require('./a');
console.log(fun.a()); //
就可以调用到及执行
a
函数了。
})
//
或者可以
use
seajs.use(['a.js'], function(a){
//
做一些事情
});
复制代码
AMD
和
CMD
对比:
AMD
推崇依赖前置、提前执行,
CMD
推崇依赖就近、延迟执行。
AMD
需要先列出清单,后面使用的时候随便使用(依赖前置),异步,特别适合浏览器环境下使用(底层其实就是动态创建
script
标签)。而且
API
默认是一个当多个用。
CMD
不需要知道依赖是什么,到了改需要的时候才引入,而且是同步的,就像临时抱佛脚一样。
对于客户端的浏览器,一说到下载、加载,肯定就是和异步脱不了关系了,注定浏览器一般用AMD
更好了。但是,
CMD
的
api
都是有区分的,局部的
require
和全局的
require
不一样。
3.CommonJS
与
ES6
3.1 ES6
ES6
模块的
script
标签有点不同,需要加上
type='module'
<script src='./a.js' type='module'>...</script>
复制代码
对于这种标签都是异步加载,而且是相当于带上defer
属性的
script
标签,不会阻塞页面,渲染完执行。但是你也可以手动加上
defer
或者
async
,实现期望的效果。
ES6
模块的文件后缀是
mjs
,通过
import
引入和
export
导出。我们一般是这样子:
//a.mjs
import b from 'b.js'
//b.mjs
export default b
复制代码
ES6
毕竟是
ES6
,模块内自带严格模式,而且只在自身作用域内运行。在
ES6
模块内引入其他模块就要用
import
引入,暴露也要用
export
暴露。另外,一个模块只会被执行一次。
import
是
ES6
新语法,可静态分析,提前编译。他最终会被
js
引擎编译,也就是可以实现编译后就引入了模块,所以
ES6
模块加载是静态化的,可以在编译的时候确定模块的依赖关系以及输入输出的变量。
ES6
可以做到编译前分析,而
CMD
和
AMD
都只能在运行时确定具体依赖是什么。
3.2CommonJS
一般服务端的文件都在本地的硬盘上面。对于客户,他们用的浏览器是要从这里下载文件的,在服务端一般读取文件非常快,所以同步是不会有太大的问题。require
的时候,马上将
require
的文件代码运行
代表就是nodejs
了。用得最多的,大概就是:
//app.js
var route = require('./route.js')//
读取控制路由的
js
文件
//route.js
var route = {......}
module.exports = route
复制代码
require
第一次加载脚本就会马上执行脚本,生成一个对象
区别: CommonJS
运行时加载,输出的是值的拷贝,是一个对象(都是由
module.export
暴露出去的),可以直接拿去用了,不用再回头找。所以,当
module.export
的源文件里面一些原始类型值发生变化,
require
这边不会随着这个变化而变化的,因为被缓存了。但是有一种常规的操作,写一个返回那个值的函数。就像
angular
里面
$watch
数组里面的每一个对象,旧值是直接写死,新值是写一个返回新值的函数,这样子就不会写死。
module.export
输出一个取值的函数,调用的时候就可以拿到变化的值。
ES6
是编译时输出接口,输出的是值的引用,对外的接口只是一种静态的概念,在静态解释后已经形成。当脚本运行时,根据这个引用去原本的模块内取值。所以不存在缓存的情况,
import
的文件变了,谁发出
import
的也是拿到这个变的值。模块里面的变量绑定着他所在的模块。另外,通过
import
引入的这个变量是只读的,试图进行对他赋值将会报错。
4.
循环依赖
就是a
依赖
b
,
b
依赖
a
,对于不同的规范也有不同的结果。
4.1CommonJS
对于node
,每一个模块的
exports={done:false}
表示一个模块有没有加载完毕,经过一系列的加载最后全部都会变为
true
。 同步,从上到下,只输出已经执行的那部分代码 首先,我们写两个
js
用
node
跑一下:
//a.js
console.log('a.js')
var b = require('./b.js')
console.log(1)
//b.js
console.log('b.js')
var a = require('./a.js')
console.log(2)
//
根据他的特点,
require
一个文件的时候,马上运行内部的代码,所以相当于
console.log('a.js')
console.log('b.js')
console.log(2)
console.log(1)
//
输出是
a.js
、
b.js
、
2
、
1
复制代码
加上export
的时候:
//a.js
module.exports = {val:1}
var b = require('./b.js')
console.log(b.val)
module.exports = {val:2}
b.val = 3
console.log(b)
//b.js
module.exports = {val:1}
var a = require('./a.js')
console.log(a.val)
module.exports = {val:2}
a.val = 3
console.log(a)
//1.
在
a.js
暴露出去一个对象
module.exports = {val:1}
//2.require
了
b
,来到
b
,运行
b
脚本
//3.b
的第一行,把
{val:1}
暴露出去,引入刚刚
a
暴露的
{val:1}
,打印
a.val
的结果肯定是
1
//4.
重新暴露一次,是
{val:2}
,然后做了一件多余的事情,改
a.val
为
3
(反正是拷贝过的了怎么改都不会影响
a.js
),毫无疑问打印出
{ val: 3 }
//5.
回到
a
,继续第三行,打印
b.val
,因为
b
暴露的值是
2
,打印
2
//6.
继续再做一件无意义的事情,打印
{ val: 3 }
复制代码
解决办法:代码合理拆分
4.2ES6
模块
ES6
模块是输出值的引用,是动态引用,等到要用的时候才用,因此可以完美实现相互依赖,在相互依赖的
a.mjs
和
b.mjs
,执行
a
的时候,当发现
import
马上进入
b
并执行
b
的代码。当在
b
发现了
a
的时候,已经知道从
a
输入了接口来到
b
的,不会回到
a
。但是在使用的过程中需要注意,变量的顺序。
如果是单纯的暴露一个基本数据类型,当然会报错not defined
。 因为函数声明会变量提升,所以我们可以改成函数声明(不能用函数表达式)
//a.mjs
import b from './b'
console.log(b())
function a(){return 'a'}
export default a
//b.mjs
import a from './a'
console.log(a())
function b(){return 'b'}
export default b
复制代码
4.3 require
我们一般使用的时候,都是依赖注入,如果是有循环依赖,那么可以直接利用require
解决
define('a',['b'],function(b){
//dosomething
});
define('b',['a'],function(a){
//dosomething
});
//
为了解决循环依赖,在循环依赖发生的时候,引入
require
:
define('a',['b','require'],function(b,require){
//dosomething
require('b')
});
复制代码
4.4 sea
循环依赖,一般就是这样
//a.js
define(function(require, exports, module){
var b = require('./b.js');
//......
});
//b.js
define(function(require, exports, module){
var a = require('./a.js');
//......
});
复制代码
而实际上,并没有问题,因为sea
自己解决了这个问题: 一个模块有几种状态:
'FETCHING':
模块正在下载中
'FETCHED':
模块已下载
'SAVED':
模块信息已保存
'READY':
模块的依赖项都已下载,等待编译
'COMPILING':
模块正在编译中
'COMPILED':
模块已编译
步骤:
1.
模块
a
下载并且下载完成
FETCHED
2.
编译
a
模块(执行回调函数)
3.
遇到了依赖
b
,
b
和自身没有循环依赖,
a
变成
SAVED
4.
模块
b
下载并且下载完成
FETCHED
5.b
遇到了依赖
a
,
a
是
SAVED
,和自身有循环依赖,
b
变成
READY
,编译完成后变成
COMPILED
6.
继续回到
a
,执行剩下的代码,如果有其他依赖继续重复上面步骤,如果所有的依赖都是
READY
,
a
变成
READY
7.
继续编译,当
a
回调函数部分所有的代码运行完毕,
a
变成
COMPILED
对于所有的模块相互依赖的通用的办法,将相互依赖的部分抽取出来,放在一个中间件,利用发布订阅模式解决
5.webpack
是如何处理模块化的
假设我们定义两个js
:
app.js
是主入口文件,
a.js
、
b.js
是
app
依赖文件,用的是
COMMONJS
规范
webpack
首先会从入口模块
app.js
开始,根据引入方法
require
把所有的模块都读取,然后写在一个列表上:
var modules = {
'./b.js': generated_b,
'./a.js': generated_a,
'./app.js': generated_app
}
复制代码
'generated_'+name
是一个
IIFE
,每个模块的源代码都在里面,不会暴露内部的变量。比如对于没有依赖其他模块的
a.js
一般是这样,没有变化:
function generated_a(module, exports, webpack_require) {
// ...a
的全部代码
}
复制代码
对于app.js
则不一样了:
function generated_app(module, exports, webpack_require) {
var a_imported_module = __webpack_require__('./a.js');
var b_imported_module = __webpack_require__('./b.js');
a_imported_module['inc']();
b_imported_module['inc']();
}
复制代码
webpack_require
就是
require
、
exports
、
import
这些的具体实现,够动态地载入模块
a
、
b
,并且将结果返回给
app
对于webpack_require
,大概是这样的流程
var installedModules = {};//
保存已经加载完成的模块
function webpack_require(moduleId) {
if (installedModules[moduleId]) {//
如果已经加载完成直接返回
return installedModules[moduleId].exports;
}
var module = installedModules[moduleId] = {//
如果是第一次加载,则记录在表上
i: moduleId,
l: false,//
没有下载完成
exports: {}
};
//
在模块清单上面读取对应的路径所对应的文件,将模块函数的调用对象绑定为
module.exports
,并返回
modules[moduleId].call(module.exports, module, module.exports,__webpack_require__);
module.l = true;//
下载完成
return module.exports;
}
复制代码
对于webpack
打包后的文件,是一个庞大的
IIFE
,他的内容大概是这样子:
(function(modules) {
var installedModules = {};
function __webpack_require__(moduleId) { /*...*/}
__webpack_require__.m = modules;//
所有的文件依赖列表
__webpack_require__.c = installedModules;//
已经下载完成的列表
__webpack_require__.d = function(exports, name, getter) {//
定义模块对象的
getter
函数
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, {
configurable: false,
enumerable: true,
get: getter
});
}
};
__webpack_require__.n = function(module) {//
当和
ES6
模块混用的时候的处理
var getter = module && module.__esModule ?//
如果是
ES6
模块用
module.default
function getDefault() { return module['default']; } :
function getModuleExports() { return module; };//
是
COMMONJS
则继续用
module
__webpack_require__.d(getter, 'a', getter);
return getter;
};
__webpack_require__.o = function(object, property) { //
判断是否有某种属性(如
exports
)
return Object.prototype.hasOwnProperty.call(object, property);
};
__webpack_require__.p = "";//
默认路径为当前
return __webpack_require__(__webpack_require__.s = 0);//
读取第一个模块
})
/************************************************************************/
//IIFE
第二个括号部分
([
(function(module, exports, __webpack_require__) {
var a = __webpack_require__(1);
var b = __webpack_require__(2);
//
模块
app
代码
}),
(function(module, exports, __webpack_require__) {
//
模块
a
代码
module.exports = ...
}),
(function(module, exports, __webpack_require__) {
//
模块
b
代码
module.exports = ...
})
]);
复制代码
如果是ES6
模块,处理的方法也不一样。还是假设我们定义两个
js
:
app.js
是主入口文件,
a.js
、
b.js
是
app
依赖文件。
(function(modules) {
//
前面这段是一样的
})
([
(function(module, __webpack_exports__, __webpack_require__) {//
入口模块
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
var __WEBPACK_IMPORTED_MODULE_0__m__ = __webpack_require__(1);
var __WEBPACK_IMPORTED_MODULE_1__m__ = __webpack_require__(2);
Object(__WEBPACK_IMPORTED_MODULE_0__m__["a"])();//
用
object
包裹着,使得其他模块
export
的内容即使是基本数据类型,也要让他变成一个引用类型
Object(__WEBPACK_IMPORTED_MODULE_1__m__["b"])();
}),
(function(module, __webpack_exports__, __webpack_require__) {
__webpack_exports__["a"] = a;//
也就是
export xxx
//....
}),
(function(module, __webpack_exports__, __webpack_require__) {
__webpack_exports__["b"] = b;
//....
})
]);