前端模塊化簡單總結

來源 |?http://www.fly63.com/article/detial/9827
前言
前端模塊化
模塊化的價值
模塊化的進化過程
1、函數(shù)封裝
//函數(shù)1function fn1(){//statement}//函數(shù)2function fn2(){//statement}
2、namespace模式?
也可以理解為對象封裝,其實就是把相關函數(shù)、變量在外面加了一層。
let module1 = {let tag : 1,let name:'module1',fun1(){console.log('this is fun1')},fun2(){console.log('this is fun2')}}
我們在使用的時候呢,就直接
module1.fun2();
優(yōu)點
一定程度上優(yōu)化了命名沖突,降低了全局變量污染的風險
有一定的模塊封裝和隔離,并且還可以進一步語義化一些
缺點
并沒有實質(zhì)上改變命名沖突的問題
外部可以隨意修改內(nèi)部成員變量,還是容易產(chǎn)生意外風險
3、IIFE模式:立即執(zhí)行匿名函數(shù)(閉包)
let global = 'Hello, I am a global variable :)';(function () {// 在函數(shù)的作用域中下面的變量是私有的const myGrades = [93, 95, 88, 0, 55, 91];let average = function() {let total = myGrades.reduce(function(accumulator, item) {return accumulator + item}, 0);return 'Your average grade is ' + total / myGrades.length + '.';}let failing = function(){let failingGrades = myGrades.filter(function(item) {return item < 70;});return 'You failed ' + failingGrades.length + ' times.';}console.log(failing());console.log(global);// 需要暴露的apireturn {// something}}());// 控制臺顯示:'You failed 2 times.'// 控制臺顯示:'Hello, I am a global variable :)'
這種方法的好處在于,你可以在函數(shù)內(nèi)部使用局部變量,而不會意外覆蓋同名全局變量,但仍然能夠訪問到全局變量
類似如上的?IIFE?,還有非常多的演進寫法
比如引入依賴:
把內(nèi)部需要的變量傳進去。
// module.js文件(function(window, $) {let data = 'www.baidu.com'//操作數(shù)據(jù)的函數(shù)function foo() {//用于暴露有函數(shù)console.log(`foo() ${data}`)$('body').css('background', 'red')}function bar() {//用于暴露有函數(shù)console.log(`bar() ${data}`)otherFun() //內(nèi)部調(diào)用}function otherFun() {//內(nèi)部私有的函數(shù)console.log('otherFun()')}//暴露行為window.myModule = { foo, bar }})(window, jQuery)
使用
// index.html文件<script type="text/javascript" src="jquery-1.10.1.js">script><script type="text/javascript" src="module.js">script><script type="text/javascript">myModule.foo()script>
優(yōu)點
實現(xiàn)了基本的封裝
只暴露對外的方法操作,利用閉包實現(xiàn)了類似?public?和?private?的概念
缺點
模塊依賴關系模糊
模塊與模塊之間不好管理
上述的所有解決方案,雖然每種方法都可行,但是始終沒有哪一種可以很好的解決變量污染、各個模塊之間依賴關系清晰、方便管理、與第三方代碼方便集成。
隨著大前端時代的到來,在2009年提出了 CommonJS 規(guī)范,并且nodeJs 直接用該規(guī)范來管理模塊化,隨著時間的遷移,現(xiàn)在 JavaScript 模塊規(guī)范也就有了:CommonJS、AMD、CMD、UMD、ES6 模塊化。
4、CommonJS
CommonJS 是 JavaScript 的一個模塊化規(guī)范(http://www.commonjs.org/),主要用于服務端Nodejs 中。
根據(jù)規(guī)范,每一個文件既是一個模塊,其內(nèi)部定義的變量是屬于這個模塊的,不會污染全局變量。每個模塊內(nèi)部,module變量代表當前模塊,這個變量是一個對象,它的exports屬性(即module.exports)是對外的接口。
加載某個模塊,其實是加載該模塊的module.exports屬性。
CommonJS 的核心思想是通過 require 方法來同步加載所依賴的模塊,然后通過 exports 或者 module.exprots 來導出對外暴露的接口。?
基本用法
暴露模塊:module.exports = value 或 exports.xxx = value
引入模塊:require(xxx),如果是第三方模塊,xxx為模塊名;如果是自定義模塊,xxx為模塊文件路徑。
// example.jslet x = 5;let addX = function (value) {return value + x;};module.exports.x = x;module.exports.addX = addX;
let example = require('./example.js');console.log(example.x); // 5console.log(example.addX(1)); // 6
require命令用于加載模塊文件。require命令的基本功能是,讀入并執(zhí)行一個JavaScript文件,然后返回該模塊的exports對象,如果沒有發(fā)現(xiàn)指定模塊,會報錯,如果存在多個 exports ?只有第一個exports 有效。
require 是加載這個文件然后執(zhí)行,在返回、輸出 exports 這個對象。
console.log('開始加載了') // 會輸出 開始加載了function run (val) {console.log(val)}
特點
以文件為一個單元模塊,代碼運行在模塊作用域內(nèi),不會污染全局變量
同步加載模塊,在服務端直接讀取本地磁盤沒問題,不太適用于瀏覽器
模塊可以加載多次,但是只會在第一次加載時運行,然后在加載,就是讀取的緩存文件。需清理緩存后才可再次讀取文件內(nèi)容
模塊加載的順序,按照其在代碼中出現(xiàn)的順序
導出的是值的拷貝,這一點和 ES6 有著很大的不同(后面會介紹到)
補充知識點
Node 中,一個文件是一個模塊->module
源碼定義如下:
function Module(id = '', parent) {this.id = id;this.path = path.dirname(id);this.exports = {};this.parent = parent;updateChildren(parent, this, false);this.filename = null;this.loaded = false;this.children = [];}
//實例化一個模塊var module = new Module(filename, parent);
CommonJS 的一個模塊,就是一個腳本文件。require命令第一次加載該腳本,就會執(zhí)行整個腳本,然后在內(nèi)存生成一個對象。
{id: '...',exports: { ... },loaded: true,...}
上面代碼就是 Node 內(nèi)部加載模塊后生成的一個對象。該對象的id屬性是模塊名,exports屬性是模塊輸出的各個接口,loaded屬性是一個布爾值,表示該模塊的腳本是否執(zhí)行完畢。其他還有很多屬性。
以后需要用到這個模塊的時候,就會到exports屬性上面取值。即使再次執(zhí)行require命令,也不會再次執(zhí)行該模塊,而是到緩存之中取值。
也就是說,CommonJS 模塊無論加載多少次,都只會在第一次加載時運行一次,以后再加載,就返回第一次運行的結果,除非手動清除系統(tǒng)緩存。
關于AMD、CMD
CommonJS 在 Node.js 環(huán)境下取得了很不錯的成功,很多人想把commonJs 規(guī)范推向瀏覽器端,但是瀏覽器不能像服務器那樣直接讀取磁盤上的內(nèi)容所以又有了后面的AMD、CMD規(guī)范。
ES6 在語言標準的層面上,實現(xiàn)了模塊功能,而且實現(xiàn)得相當簡單,完全可以取代現(xiàn)有的 CommonJS 和 AMD 規(guī)范,成為瀏覽器和服務器通用的模塊解決方案,因為我自己也是在近幾年才做前端, AMD、CMD 并沒有太多使用,所以AMD、CMD 這里只是做簡單的介紹。
5、AMD
AMD 全稱為?Asynchromous Module Definition(異步模塊定義)
AMD 是?RequireJS?在推廣過程中對模塊定義的規(guī)范化產(chǎn)出,它是一個在瀏覽器端模塊化開發(fā)的規(guī)范。
AMD 模式可以用于瀏覽器環(huán)境并且允許異步加載模塊,同時又能保證正確的順序,也可以按需動態(tài)加載模塊。
特點
異步加載模塊,不會造成因網(wǎng)絡問題而出現(xiàn)的假死
顯式地列出其依賴關系,并以函數(shù)(定義此模塊的那個函數(shù))參數(shù)的形式將這些依賴進行注入
在模塊開始時,加載所有所需依賴
定義模塊
define(id?: String, dependencies?: String[], callback: Function|Object);
id,一個可選參數(shù),類似給模塊取個名字,但是卻是模塊的唯一標識。如果沒有提供則取腳本的文件名
dependence,依賴的模塊數(shù)組
callback,工廠方法,模塊初始化的一些操作。如果是函數(shù),只被執(zhí)行一次。如果是對象,則為模塊的輸出值
使用模塊
require([moduleName],callback);
使用
//article.js文件// 定義有依賴的模塊define(['user'], function(user) {let name = 'THE LAST TIME'function consoleMsg() {console.log(`${name} by ${user.getAuthor()}`);}// 暴露模塊return { consoleMsg }})
// 調(diào)用 article 模塊種的 consoleMsgrequire(['article'], function(alerter) {article.consoleMsg()})
關于 require.js 的使用,仔細看文檔,其實還是有很多知識點的。但是鑒于我們著實現(xiàn)在使用不多(我也不熟),所以這里也就參考網(wǎng)上優(yōu)秀文章和自己實踐,拋磚引玉。
6、CMD
CMD 即Common Module Definition通用模塊定義,CMD 是 SeaJS 在推廣過程中對模塊定義的規(guī)范化產(chǎn)出,是阿里的玉伯提出來,它和 AMD 其實非常的相似,文件即為模塊。
CMD最主要的區(qū)別是實現(xiàn)了按需加載,推崇依賴就近的原則,模塊延遲執(zhí)行,而 AMD 所依賴模塊式提前執(zhí)行(requireJS 2.0 后也改為了延遲執(zhí)行)
所以AMD和CMD最大的區(qū)別是對依賴模塊的執(zhí)行時機處理不同,注意不是加載的時機或者方式不同。
CMD 規(guī)范盡量保持簡單,并與 CommonJS 規(guī)范保持了很大的兼容性。
通過 CMD 規(guī)范書寫的模塊,可以很容易在 Node.js 中運行。
在 CMD 規(guī)范中,一個模塊就是一個文件。格式如下:
define(factory);
define?是一個全局函數(shù),用來定義模塊,參數(shù)factory可以是對象、字符串、函數(shù)
factory 為對象、字符串時,表示模塊的接口就是該對象、字符串。比如可以如下定義一個 JSON 數(shù)據(jù)模塊:
define({ "foo": "bar" });
也可以通過字符串定義模板模塊:
define('I am a template. My name is {{name}}.');
factory 為函數(shù)時,表示是模塊的構造方法。
執(zhí)行該構造方法,可以得到模塊向外提供的接口。factory 方法在執(zhí)行時,默認會傳入三個參數(shù):require、exports 和 module:
define(function(require, exports, module) {// 模塊代碼});
使用sea.js
/** sea.js **/// 定義模塊 math.jsdefine(function(require, exports, module) {var $ = require('jquery.js');var add = function(a,b){return a+b;}exports.add = add;});// 加載模塊seajs.use(['math.js'], function(math){var sum = math.add(1+2);});
關于 sea.js 的使用,仔細看文檔,其實還是有很多知識點的。但是鑒于我們著實現(xiàn)在使用不多(我也不熟),所以這里也就參考網(wǎng)上優(yōu)秀文章和自己實踐,拋磚引玉。
?7、UMD
UMD 是 AMD 和 CommonJS 的綜合產(chǎn)物。如上所說,AMD 的用武之地是瀏覽器,非阻塞式加載。CommonJS 主要用于服務端 Nodejs 中使用。所以人們就想到了一個通用的模式 UMD(universal module definition)。來解決跨平臺的問題。
沒錯!就是 ifElse 的寫法。
核心思想就是:先判斷是否支持Node.js的模塊(exports)是否存在,存在則使用Node.js模塊模式。
在判斷是否支持AMD(define是否存在),存在則使用AMD方式加載模塊。
(function (root, factory) {if (typeof define === 'function' && define.amd) {//AMDdefine(['jquery'], factory);} else if (typeof exports === 'object') {//Node, CommonJS之類的module.exports = factory(require('jquery'));} else {//瀏覽器全局變量(root 即 window)root.returnExports = factory(root.jQuery);}}(this, function ($) {//方法function myFunc(){};//暴露公共方法return myFunc;}));
8、ES Module
在 ES Module 之前,社區(qū)制定了一些模塊加載方案,最主要的有 CommonJS 和 AMD 兩種。前者用于服務器,后者用于瀏覽器。
ES Module 在語言標準的層面上,實現(xiàn)了模塊功能,而且實現(xiàn)得相當簡單,完全可以取代 CommonJS 和 AMD 規(guī)范,成為瀏覽器和服務器通用的模塊解決方案。
ES Module 的設計思想是盡量的靜態(tài)化,使得編譯時就能確定模塊的依賴關系,以及輸入和輸出的變量。CommonJS 和 AMD 模塊,都只能在運行時確定這些東西。
CommonJS 和 AMD 模塊,其本質(zhì)是在運行時生成一個對象進行導出,稱為“運行時加載”,沒法進行“編譯優(yōu)化”,而 ES Module 不是對象,而是通過 export 命令顯式指定輸出的代碼,再通過 import 命令輸入。
這稱為“編譯時加載”或者靜態(tài)加載,即 ES Module 可以在編譯時就完成模塊加載,效率要比 CommonJS 模塊的加載方式高。當然,這也導致了沒法引用 ES Module 模塊本身,因為它不是對象。
由于 ES Module 是編譯時加載,使得靜態(tài)分析成為可能。有了它,就能進一步拓寬 JavaScript 的語法,比如引入宏(macro)和類型檢驗(type system)這些只能靠靜態(tài)分析實現(xiàn)的功能。
特點
靜態(tài)編譯
輸出的值引用,而非值拷貝
import?只能寫在頂層,因為是靜態(tài)語法
9、CommonJs、ESM 區(qū)別
| CommonJs | ES6 Module |
| 運行時加載;CommonJs模塊就是對象(module.exports屬性)),即在輸入時是先加載整個模塊、執(zhí)行模塊,生成一個對象,然后再從這個對象上面讀取方法。 | 編譯時加載;ES6 模塊不是對象,而是通過 export 命令顯式指定輸出的代碼,import時采用靜態(tài)命令的形式。即在import時可以指定加載某個輸出值,而不是加載整個模塊。 |
| 輸出的是值的拷貝(一旦輸出一個值,模塊內(nèi)部的變化就影響不到這個值。) | 輸出的是值的引用(JS 引擎對腳本靜態(tài)分析的時候,遇到模塊加載命令import,就會生成一個只讀引用。等到腳本真正執(zhí)行時,再根據(jù)這個只讀引用,到被加載的那個模塊里面去取值。即原始值變了,import加載的值也會跟著變。因此,ES6 模塊是動態(tài)引用,并且不會緩存值,模塊里面的變量綁定其所在的模塊。) |
差異
CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。
CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口。
加載 & 編譯
因為 CommonJS 加載的是一個對象(module.exports),對象只有在有腳本運行的時候才能生成。而 ES6 模塊不是一個對象,只是一個靜態(tài)的定義。在代碼解析階段就會生成。
ES6 模塊是編譯時輸出接口,因此有如下2個特點:
import?命令會被 JS 引擎靜態(tài)分析,優(yōu)先于模塊內(nèi)的其他內(nèi)容執(zhí)行
export 命令會有變量聲明提升的效果,所以import 和 export 命令在模塊中的位置并不影響程序的輸出。
異步加載模塊,不會造成因網(wǎng)絡問題而出現(xiàn)的假死。
顯式地列出其依賴關系,并以函數(shù)(定義此模塊的那個函數(shù))參數(shù)的形式將這些依賴進行注入。
在模塊開始時,加載所有所需依賴。

