您好,登錄后才能下訂單哦!
?
Javascript 模塊的演化歷史一定程度上代表了前端的發展史。從早期的 對象字面量
、IIFE
到后來的 commonjs
, AMD
等, 再到如今的 ES Module
。這些模塊化方案在互聯網技術發展需求下不斷革新,演進。
本文從四個階段來講述 JS 模塊化的發展歷程,旨在讓大家了解 JS 模塊化是如何發展到今天,每個模塊方案在當時又解決了什么問題。
?
Javascript 早期誕生的目的用于客戶端驗證表單,提高用戶體驗。站在今天的解決方案角度去回顧,在那個無樣式,無交互的,簡單的不能再簡單的 Web 頁面,很難想 JS 的模塊化意義在哪里。
如果非要達到一定程度代碼復用,對象字面量
完全可以滿足 Web 互聯網早期的需求。
?
//Person.js
var Person = {
say: function(words) {
//code
},
run: function() {
//code
}
};
Person.say('say somthing');
Person.run();
歷史總會進步,互聯網上 Web 頁面越來越多樣化,好在人們會不斷的根據變化的需求調整模塊化的方式。
?
當團隊相互合作,去完成某一個項目時,對象字面量
缺點就一覽無遺。命名沖突、作用域隔離等問題就不可避免的會發生,只是一個時間早與晚的問題。
?
javascript 函數,擁有者天然的局部作用域,外界是訪問不到函數內部的作用域。自然而然過渡到IIFE
模塊化。
?
(function(global){
var Person = global.Person || {};
var pritiveFn = function(){
//other code
};
var pritiveName = 'Tom';
Person.say = function(words) {
pritiveFn();
console.log( pritiveName + 'say: ' + words);
//other code
}
Person.run = function() {
pritiveFn();
//other code
}
})(window);
Person.say();
Person.run();
這種模式,能任意定義不會被外界訪問到局部變量,也不會污染全局作用域,同時還能訪問全局中的一些變量。通過傳參命名空間,可將模塊掛在到全局 Person 命名空間上。
IIEF
的模塊化方式,早已***到前端開發的基因。直到今天,在我們的日常開發中,都能見到或用到這種方式。
Web2.0時代的到來,網站應用更加注重用戶與服務的雙向交互,前端開發也逐漸承擔更多的責任。一個網站,可能有成百上千的頁面,而且,javascrpt 不局限于客戶端。
推崇 commonjs
模塊化規范的 Nodejs ,將模塊化推向了一個新的高度。
// path/ModuleA.js
var ModuleA = function(){
//code
}
module.exports = ModuleA;
//-------------------------
// path/ModuleB.js
var ModuleB = function(){
//code
}
module.exports = ModuleB;
//------------------------
// path/index.js
var ModuleA = require('./path/ModuleA');
var ModuleB = require('./path/ModuleB');
ModuleA();
ModuleB();
commonjs
規范提供 module.exports
(或者 exports
)接口用于對外暴露模塊。require
加載模塊。
仔細想想,日常開發中我們理所應當只關心模塊的自由導出和加載。而加載速度、依賴順序、作用域隔離等問題應該交給框架或者其他科學技術來系統解決,讓我們無感知。
但,nodejs 畢竟是運行在服務端的 javascript。
nodejs 中每個文件具有獨立的作用域,所以每個文件可認為是一個模塊。除非你顯示的定義在全局 global 對象上,否則其他文件是訪問不到該作用域的定義的任何數據。
在 nodejs 中,一個 js 文件擁有訪問其他模塊(文件)能力,這就很好的解決模塊間相互依賴的問題。并且所有文件都是在服務器本地加載,速度極快。
但瀏覽器客戶端的現狀是殘酷的。看下面例子,如果某個頁面依賴Slider
, Dialog
, Tab
模塊,而這三個模塊又有一些自身的依賴。
<!-- 模塊自身的依賴 -->
<script src="./util/Animation.js"></script>
<script src="./util/Mask.js"></script>
<!-- 模塊依賴 -->
<script src="./Slider/index.js"></script>
<script src="./Dialog/index.js"></script>
<script src="./Tab/index.js"></script>
<script>
Slider();
Dialog();
Tab();
</script>
上面的例子可以看出:
browserify
,可將commonjs
規范移植到瀏覽器端,本質上。browserify
是將所有被依賴commonjs
的模塊,打包到當前業務代碼中。
瀏覽器中的 js,本身并無加載其他文件(模塊)的接口。聰明的人們用動態創建 script 節點實現了動態加載模塊。AMD
, 異步模塊定義,采用的是異步加載模塊方式。依賴模塊是異步加載,不會阻塞頁面的渲染。
AMD規范中最核心的接口是define
和require
,顧名思義:定義和加載模塊。
其中以requirejs
代表,是AMD
規范的實現。
// 定義模塊
define(['path/util/Animation'], function(Animation){
// Slider code
return Slider;
});
// 加載執行模塊
require(['path/Slider'], function(Slider){
Slider();
})
可以看出,接口的第一個參數,代表模塊的依賴路徑。模塊或業務的代碼,放在 callback 中,其中 callback 參數提供暴露出了各依賴模塊的接口。
此時,模塊規范分成了commonjs
和AMD
兩大陣營。天下大勢分久必合,需要一種解決方案同時兼容這兩種規范。而UMD
規范的誕生就是解決該問題。
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD 規范
define([], factory);
} else if (typeof exports === 'object') {
// commonjs 規范
module.exports = factory();
} else {
// 掛載到全局
root.globalVar = factory();
}
}(this, function () {
return {};
}));
從上面可以看出 UMD 是通過判斷運行環境中是否存在各模塊化的接口來實現的。
不管是 commonjs, AMD, UMD,都是畢竟是為了彌補 javascript 模塊缺陷而衍生出的民間解決方案。2015年es6的發布,讓javascript終于在語言層面上實現了模塊化。
?
// path/ModuleA.js
var ModuleA = function(){
//code
}
exports default ModuleA;
//-------------------------
// path/ModuleB.js
var ModuleB = function(){
//code
}
exports default ModuleB;
//------------------------
// path/index.js
import ModuleA from './path/ModuleA';
import ModuleB from './path/ModuleB';
ModuleA();
ModuleB();
commonjs
已經發展很成熟,也能滿足日常需求。初略看,es module “就像是語法糖”,我們為何還要去使用它呢,換句話說,我們是用它能為我們帶來哪些收益?
不管是 commonjs
, AMD
,他們的模塊架構是 “動態架構”,換句話說,模塊依賴是在程序運行時才能確定。而es module
是 “靜態架構”,也就是模塊依賴在代碼編譯時就獲取到。所以在 commonjs 里能進行 “動態引入” 模塊。
if ( Math.random() > 0.5 ) {
require('./ModuleA');
} else {
require('./ModuleB');
}
而在 es module 中是無法進行類似操作的。從這個角度來看,es6 module 靈活性還不如 commonjs。但事物具有兩面性。es6 module 其實能為我們帶來以下幾個收益。
tree shaking
在我們部署項目時,常常需要將各個模塊文件打包成單個文件,以便瀏覽器一次性加載所有模塊,減少 reqeust 數量。因為在 HTTP/1 中,瀏覽器 request 并發數量有限制。不過隨之帶來的問題是,多個模塊打包成單文件,會造成文件 size 過大。
如果我們能在編譯期時確定好模塊依賴,就可以消除沒有用到的模塊,以便達到一定程度的優化,來看看下面例子。
// moduleA.js
export function moduleX(){
//some code
}
export function moduleY(){
//some code
}
// index.js
import { moduleX, moduleY } from './moduleA';
moduleX();
通過工具 Rollup, 可將 index.js 打包成如下代碼:
'use strict';
function moduleX(){
//some code
}
moduleX();
可以看出,打包的代碼只包含 moduleX,最大限度的減少了打包文件 size,這就是所謂的 'tree shaking', 讀者可以好好品味下這個詞,很傳神。
模塊變量靜態檢查
es6 module由于是“靜態架構”,在編譯時就能確定模塊的依賴樹以及確保模塊一定是被正確的 import/export ,這就為項目質量帶來很大的保障。看下面例子:
// module1.mjs
export function moduleX(){
console.log(1);
}
// index.mjs
// 注意:module1.mjs 中并沒有 export 出 moduleY
import { moduleX, moduleY } from './module1.mjs';
moduleX();
//注意
let randomNum = Math.random();
if (randomNum) > 0.3 && randomNum < 0.4 ) {
moduleY();
}
?
如果沒有靜態檢查,在上面代碼中的條件判斷得出,代碼運行期間,執行 moduleY() 函數報錯的概率是10%,這種風險在線上環境就是一個非常大的隱患,一旦命中條件判斷,你一整年的績效可能就都沒了。
那如果有編譯期間靜態檢查,會是怎樣的結果?
運行 node --experimental-modules index.mjs
命令時,控制臺會報錯:
import { moduleX, moduleY } from './module1.mjs';
^^^^^^^
SyntaxError: The requested module does not provide an export named 'moduleY'
at ModuleJob._instantiate (internal/loader/ModuleJob.js:88:21)
at <anonymous>
這種編譯時靜態檢查對項目的質量把控非常有用。
但 es6 module 有時候也讓我很憂傷。因為它很“靈活”,所以給我帶來了困擾。
來看看 import 語法:
再來看看 export 語法
額,其實我就想簡單的 import/export 而已,“少即使多”。
農業革命是前端史的重大進步,社區各種模塊化解決方案以及事實上的標準,從另一方面也推動著 Javascript 從語言層面對模塊化進行支持。這為我們架構大型項目,保證項目質量提供了機會。
模塊的兼容問題以及重復勞動應該交給工具去做,我們應該留出更多的時間享受”美好生活“。所以,涌現了一大批模塊化工具以及周邊的模塊管理工具。如Browserify、r.js、Webpack、Rollup、 jspm、 npm、yarn等等。
各種工具極大的提高了我們的工作效率,也我們對模塊化有了更多的選擇。
快樂同時也帶來很多的痛,就是因為可選擇工具太多,配置太多,讓你深陷其中無法自拔。要么忙著寫bug, 要么忙著寫配置。
科學革命的時代,還未到來。也許到那時候,模塊化的使用就像var m = 1;語法一樣,它在我們腦海里本應該就是理所當然的存在,而不需借助其他編譯、運行等工具來實現。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。