1模塊化歷史
1.1前言
1.2無模塊化
每次說到JavaScript都會(huì)想到Brendan Eich花了十來天就發(fā)明了它,那就是JS的鴻蒙時(shí)期,混沌初開。
就像當(dāng)年在校初學(xué)前端時(shí)寫的代碼,沒有那么多的套路,就是從上往下碼代碼,沒有想著去聲明函數(shù)神馬的,甚至多少代碼都寫在一個(gè)JS里。現(xiàn)在想來真是慘不忍睹。雖然本人入坑前端距那個(gè)鴻蒙時(shí)代實(shí)在久遠(yuǎn),但是據(jù)各種典籍記載,那時(shí)候的代碼風(fēng)格就差不多這樣子,從上往下一直堆著就好了。
var a = 0;
if (xxx) {
// 省略100L
}
document.getElementById('id').onclick = function(event) {
// 省略若干行
}
......
1.3模塊化冒泡
每個(gè)行業(yè)都有梗,現(xiàn)在和同事聊天有時(shí)候還會(huì)吐槽十幾年前的老網(wǎng)站,真是有幸見過。前輩的聊天更有意思了,當(dāng)年的登錄居然是寫死在前端代碼里,就像這樣子
if (username === 'xxxxx' && password === 'xxxxxx') {
// 登錄成功
}
是不是覺得很無語。當(dāng)年的前端都是靜態(tài)頁面,沒有現(xiàn)在這樣子通過ajax和后端交互神馬的,內(nèi)容更是豐富多彩,更新及時(shí)。
前端代碼愈發(fā)龐大,那么自然而然會(huì)暴露很多問題。
無非就倆個(gè):
- 命名沖突
- 文件依賴
1.3.1命名沖突解決
- java風(fēng)格的namespace,這個(gè)很好理解,在此不贅訴,缺點(diǎn)的話,自行想象,不堪
- 自執(zhí)行函數(shù)(內(nèi)部變量不可見不被污染)
// jQuery式的匿名自執(zhí)行函數(shù)
// 缺點(diǎn)是增添了全局變量、依賴需要提前提供
(function(root) {
root.jQuery = window.$ = jQuery; // 掛載到window之上
})(window)
// 普通自執(zhí)行函數(shù)
// 缺點(diǎn)就是暴露了全局變量,而且隨著模塊的增加,全局變量會(huì)很多
module = function() {
function module() {
}
return module;
}()
- YUI3的沙箱機(jī)制,這個(gè)表示不曉得。
1.3.2文件依賴解決
這個(gè)沒有解決方法,乖乖自行保證順序和不缺漏吧
<script src="https://cdn.bootcss.com/underscore.js/1.8.3/underscore-min.js"></script>
<script src="https://cdn.bootcss.com/backbone.js/1.3.3/backbone-min.js"></script>
1.4CommonJS
1.4.1前言
隨著前端的發(fā)展,到node.js被創(chuàng),js可以用來寫server端代碼。
做過后端的同學(xué)肯定知道沒有模塊化怎么能忍呢?
我們通過上節(jié)所得,我們可以得出以下幾點(diǎn)需待解決
- 模塊代碼安全,不可被污染也不可污染別人,沙箱呀
- 把模塊接口暴露出去(得優(yōu)雅呀,不能增添全局變量)
- 這個(gè)依賴順序管理
1.4.2發(fā)展
這個(gè)還真沒有經(jīng)歷了解過。度娘一番,幸好看到seajs下的一個(gè)issues。
大致就是大牛很牛,推出了Modules/1.0規(guī)范。
之后為了推廣到瀏覽器端,大牛產(chǎn)生分歧,分為三大流派:
- Modules/1.x 流派(Modules/Transport
通過工具轉(zhuǎn)換現(xiàn)有的CommonJS) - Modules/Async 流派(自立門派)
- Modules/2.0 流(Modules/Wrappings
對(duì)1.0的升級(jí))
這里說下為什么不能用在瀏覽器
- 服務(wù)端代碼在硬盤,加載模塊時(shí)間幾乎忽略不計(jì)。瀏覽器端就不成了。
- 模塊引用未被function,所以暴露在了全局之下。
1.4.3番外(AMD、CMD)
AMD是 RequireJS 在推廣過程中對(duì)模塊定義的規(guī)范化產(chǎn)出。
CMD是 SeaJS 在推廣過程中對(duì)模塊定義的規(guī)范化產(chǎn)出。
1.5ES Module
這個(gè)是ECMA搞得一套。和之前的區(qū)別在于人家是官方的,根正苗紅,上文的是社區(qū)搞得,野生。
2 ES Module/CommonJS/AMD/CMD差異
2.1 ES Module與CommonJS的差異
編譯時(shí)和運(yùn)行時(shí)
首先說下編譯時(shí)和運(yùn)行時(shí)。JavaScript有倆種聲明方法(聲明變量和聲明方法)。var/const/let和function
編譯時(shí),對(duì)于聲明變量會(huì)在內(nèi)存中開辟一塊內(nèi)存空間并指向變量名,且指向變量名,賦值為undefined。對(duì)于函數(shù)聲明會(huì)一樣的開啟空間。不過賦值為聲明的函數(shù)體。PS:無論順序如何,都會(huì)先聲明變量
運(yùn)行時(shí),執(zhí)行變量初始化之類的。
// 源碼
var a = 3;
function f() {
return 'f';
}
// 編譯時(shí)
var a = undefined;
var f = function() {
return 'f';
}
// 運(yùn)行時(shí)
a = 3;
CommonJS模塊是對(duì)象,是運(yùn)行時(shí)加載,運(yùn)行時(shí)才把模塊掛載在exports之上(加載整個(gè)模塊的所有),加載模塊其實(shí)就是查找對(duì)象屬性。
ES Module不是對(duì)象,是使用export顯示指定輸出,再通過import輸入。此法為編譯時(shí)加載,編譯時(shí)遇到import就會(huì)生成一個(gè)只讀引用。等到運(yùn)行時(shí)就會(huì)根據(jù)此引用去被加載的模塊取值。所以不會(huì)加載模塊所有方法,僅取所需。
- CommonJS 模塊輸出的是一個(gè)值的拷貝,ES6 模塊輸出的是值的引用。
- CommonJS 模塊是運(yùn)行時(shí)加載,ES6 模塊是編譯時(shí)輸出接口。
詳情參見
2.2CommonJS與AMD/CMD的差異
AMD/CMD是CommonJS在瀏覽器端的解決方案。CommonJS是同步加載(代碼在本地,加載時(shí)間基本等于硬盤讀取時(shí)間)。AMD/CMD是異步加載(瀏覽器必須這么干,代碼在服務(wù)端)
2.3AMD與CMD的差異
- AMD是提前執(zhí)行(RequireJS2.0開始支持延遲執(zhí)行,不過只是支持寫法,實(shí)際上還是會(huì)提前執(zhí)行),CMD是延遲執(zhí)行
- AMD推薦依賴前置,CMD推薦依賴就近
3 用法
3.1 CommonJS
// 導(dǎo)出使用module.exports,也可以exports。exports指向module.exports;即exports = module.exports
// 就是在此對(duì)象上掛屬性
// commonjs
module.exports.add = function add(params) {
return ++params;
}
exports.sub = function sub(params) {
return --params;
}
// 加載模塊使用require('xxx')。相對(duì)、絕對(duì)路徑均可。默認(rèn)引用js,可以不寫.js后綴
// index.js
var common = require('./commonjs');
console.log(common.sub(1));
console.log(common.add(1));
3.2 AMD/RequireJS
- 定義模塊:define(id?, dependencies?, factory)
- 加載模塊:require([module], factory)
// a.js
// 依賴有三個(gè)默認(rèn)的,即"require", "exports", "module"。順序個(gè)數(shù)均可視情況
// 如果忽略則factory默認(rèn)此三個(gè)傳入?yún)?shù)
// id一般是不傳的,默認(rèn)是文件名
define(["b", "require", "exports"], function(b, require, exports) {
console.log("a.js執(zhí)行");
console.log(b);
// 暴露api可以使用exports、module.exports、return
exports.a = function() {
return require("b");
}
})
// b.js
define(function() {
console.log('b.js執(zhí)行');
console.log(require);
console.log(exports);
console.log(module);
return 'b';
})
// index.js
// 支持Modules/Wrappings寫法,注意dependencies得是空的,且factory參數(shù)不可空
define(function(require, exports, module) {
console.log('index.js執(zhí)行');
var a = require('a');
var b = require('b');
})
// index.js
require(['a', 'b'], function(a, b) {
console.log('index.js執(zhí)行');
})
3.3 CMD/SeaJS
SeaJS平時(shí)沒有到,不過了解了下,豐富用法看CMD定義規(guī)范。
- 定義模塊:define(factory)
// a.js
// require, exports, module參數(shù)順序不可亂
// 暴露api方法可以使用exports、module.exports、return
// 與requirejs不同的是,若是未暴露,則返回{},requirejs返回undefined
define(function(require, exports, module) {
console.log('a.js執(zhí)行');
console.log(require);
console.log(exports);
console.log(module);
})
// b.js
//
define(function(require, module, exports) {
console.log('b.js執(zhí)行');
console.log(require);
console.log(exports);
console.log(module);
})
// index.js
define(function(require) {
var a = require('a');
var b = require('b');
console.log(a);
console.log(b);
})
定義模塊無需列依賴,它會(huì)調(diào)用factory的toString方法對(duì)其進(jìn)行正則匹配以此分析依賴。預(yù)先下載,延遲執(zhí)行
3.4 ES Module
輸出/export
// 報(bào)錯(cuò)1
export 1;
// 報(bào)錯(cuò)2
const m = 1;
export m;
// 接口名與模塊內(nèi)部變量之間,建立了一一對(duì)應(yīng)的關(guān)系
// 寫法1
export const m = 1;
// 寫法2
const m = 1;
export { m };
// 寫法3
const m = 1;
export { m as module };
PS:這個(gè)有點(diǎn)不是很明白,大致理解就是不能直接導(dǎo)出變量,但是可以導(dǎo)出聲明(函數(shù)、變量聲明)。這里的接口理解是export之后的變量,它和變量建立了映射關(guān)系??偟亩?,export之后只能接聲明或者語句
輸入/import
基本用法
// 類似于對(duì)象解構(gòu)
// module.js
export const m = 1;
// index.js
// 注意,這里的m得和被加載的模塊輸出的接口名對(duì)應(yīng)
import { m } from './module';
// 若是想為輸入的變量取名
import { m as m1 } './module';
// 值得注意的是,import是編譯階段,所以不能動(dòng)態(tài)加載,比如下面寫法是錯(cuò)誤的。因?yàn)?a' + 'b'在運(yùn)行階段才能取到值,運(yùn)行階段在編譯階段之后
import { 'a' + 'b' } from './module';
// 若是只是想運(yùn)行被加載的模塊,如下
// 值得注意的是,即使加載兩次也只是運(yùn)行一次
import './module';
// 整體加載
import * as module from './module';
PS:CommonJS和ES Module是可以寫一起的,但是最好不要。畢竟一個(gè)是編譯階段一個(gè)是運(yùn)行階段。就在項(xiàng)目中入過坑,自行體會(huì)。
賦值
首先輸入的模塊變量是不可重新賦值的,它只是個(gè)可讀引用,不過卻可以改寫屬性
// 單例
// module.js
export const a = {};
// module2.js
export { a } from './module';
import { a as a1 } from './module';
import { a } from './module2';
a1.e = 3;
console.log(a1) // { e: 3 }
console.log(a) // { e: 3 }
輸出/export default
// module.js
// 其實(shí)export default就是export { xxx as default }
const m = 1;
export default m;
===
export { m as default }
// index.js
// 對(duì)應(yīng)的輸入也得相應(yīng)變化
import module from './module';
===
import { default as module } from './module';
還記得之前export小結(jié)處的倆報(bào)錯(cuò)么?如下寫法正確,因?yàn)樘峁┝薲efault接口
// 寫法1
export default 1;
// 寫法2
const m = 1;
export default m;
// 錯(cuò)誤寫法
export default const m = 1;
PS:export default只能一次
復(fù)合寫法
可用于模塊間繼承。比如在a模塊寫下如下,那么a模塊不就有了./module的方法了
export { a } from './module';
export { a as a1 } from './module';
export * from './module';
動(dòng)態(tài)加載/import()
因?yàn)榫幾g時(shí)加載,所以不能動(dòng)態(tài)加載模塊。不過幸好有import()方法。
這家伙返回值是一個(gè)Promise對(duì)象,所以then、catch你開心就好
// 普通寫法
import('./module').then(({ a }) => {})
// async、await
const { a } = await import('./module');
4 番外自實(shí)現(xiàn)
var MyModules = (function(){
var modules = [];
function define(name, deps, cb) {
deps.forEach(function(dep, i) {
deps[i] = modules[dep];
});
modules[name] = cb.apply(cb, deps);
}
function get(name) {
return modules[name];
}
return {
define: define,
get: get
};
})();
MyModules.define('add', [], function() {
return function(a, b) {
return a + b;
};
})
MyModules.define('foo', ['add'], function(add) {
var a = 3;
var b = 4;
return {
doSomething: function() {
return add(a, b) + a;;
}
};
})
var add = MyModules.get('add');
var foo = MyModules.get('foo');
console.log(add(1, 2));
console.log(foo.doSomething());