我怎麼記得我好像寫過相關型別的文章,但是我找遍了我的部落格沒有~那就再寫一遍吧,其實模組化的核心內容也算不上是複雜,只不過需要整理一下,規劃一下罷了。嘻嘻。
開始寫標題的時候我就在糾結一件事情,就是,先吃喜歡吃的,還是後吃喜歡吃的,翻譯過來就是我應該先寫CommonJS和ES6 Module,還是先寫CMD和AMD。嗯,我決定了,誰先做好了我就先吃誰。
其實模組化的緣由很簡單,就一句話,不對,就一個詞,兩個字,分類。如果一定讓我在加一點,那應該是「隔離」。沒了~~但是這麼少不太好,我舉個可能不那麼恰當的例子吧。
剛開始這個世界上只有三個人,起名字的時候會「刻意」的避開彼此已經叫過的名字,他們在一起生活,日子欣欣向榮,一片美好,對未來充滿了期待。
時間飛逝,三個人變成了三十人,他們勉強還是住在一起,起名字的時候雖然費事一點,但是也還能不重複,日子還是欣欣向榮,一片美好,對未來充滿了期待。
時間又飛逝,三十人變成了三百人,那這不太好管理了,於是三位首領就說,你們有領導能力的幾個人站出來,組成各自的部落,去吧,我相信你們可以的。於是每個部落住在一起,部落與部落之間的人可以重名,叫名字的時候再加上一個部落的名稱唄,嗯~又一片欣欣向榮。
時間繼續飛逝,三百人變成了三千人,這三千人住在幾個大部落裡也很不方便,你拿我的蘋果,我偷了你得豬,這肯定不行,有礙於社會的穩定發展,於是三個創始者叫上部落的組長說,我們給每個人分一塊地,蓋一個房子,把三五個人分割成一個家庭,家庭之間由部落作為紐帶,關聯彼此,在形式上又相互獨立,不可以隨便拿別家的蘋果。很完美~
時間飛飛飛飛逝,三千人變成了三百萬人……我們需要法律了。
OK,上面的小例子,人,就是函數,部落就是名稱空間,房子就是IIFE,法律就是後續發展的模組化規範。那麼我們依照上面的描述,如何轉換成程式碼?
最開始的時候,瀏覽器只需要簡單的圖文展示就是可以了,沒什麼複雜的互動和邏輯的處理,所以,當我們只有三個人的時候,我們可以很自由,很隨意:
function a(){}
function b(){}
隨著Web的發展,互動的增多,專案的擴大,很容易有人也宣告了同樣名稱的函數,於是紛爭開始了,那咋解決紛爭呢?嗯,名稱空間也就是拆分部落,就像這樣:
var zaking1 = { a:function(){}, b:function(){} } var zaking2 = { a:function(){}, b:function(){} }
但是這樣並不能真正的解決問題,因為雖然從形式上區分了部落,但是部落之間沒有任何的隔離,部落內部也是混亂的,所以各個首領就制定了一個方案,IIFE,利用閉包的特性,來實現資料的隔離,暴露出對外的入口:
var module = (function () { var name = "zaking"; function getName() { console.log(name); } return { getName }; })(); module.getName();
我們蓋好了房子,還給房子建好了可以出入的門,但是我怎麼邀請別人進來呢?
var module = (function (neighbor) { var name = "zaking"; function getName() { console.log(name + "和鄰居:" + neighbor); } return { getName }; })("xiaowangba"); module.getName();
傳個引數唄,這就是依賴注入。在這個階段,最有代表性的就是jQuery了,它的封閉性的核心實現,跟上面的程式碼幾乎無異,我們可以看下jQuery的模組的實現:
(function (global, factory) { factory(global); })(typeof window !== "undefined" ? window : this, function (window, noGlobal) { if (typeof noGlobal === "undefined") { window.jQuery = window.$ = jQuery; } return jQuery; });
當然我這裡略了很多,你看它,無非就是一個閉包,傳入了window和jQuery本身,然後再繫結到window上,這樣,我們就只能存取到暴露出來的$以及$上的方法和屬性,我們根本無法修改內部的資料。
OK,到了這個階段,其實算是一個轉折點,我們有了初步的法律,還需要後續針對法律的完善,
隨著社會的發展,出現一種規則已成必然,於是commonJs統領舉起模組化的大旗,讓JavaScript邁向了另一個階段。commonJs最初由 JavaScript
社群中的 Mozilla
的工程師Kevin Dangoor
在Google Groups中建立了一個ServerJs小組。該組織的目標是為web伺服器、桌面和命令列應用程式以及瀏覽器構建JavaScript生態系統。嗯,它的野心很大~,後來,他就就把ServerJs改成了commonJs,畢竟ServerJs的範圍有點小,commonJs更符合他們的初衷。
而後,在同一年的年底,NodeJs出現了,Javascript不僅僅可以用於瀏覽器,在伺服器端也開始攻城略地。NodeJs的初衷是基於commonJs社群的模組化規範,但是NodeJs並沒有完全遵循於社群的一些腐朽過時的約束,它實現了自己的想法。
commonJs規範的寫法,如果大家寫過NodeJs一定都有所瞭解,大概是這樣的:
// a.js module.exports = 'zaking' // b.js const a = require("./a"); console.log(a); // zaking
看起來挺簡單的,但是這裡隱藏了一些不那麼容易被理解的特性。
在NodeJs中,一個檔案就是一個模組,有自己的作用域,在一個檔案裡面定義的函數、物件都是私有的,對其他檔案不可見。並且,當第一次載入某個模組的時候,NodeJ會快取該模組,待再次載入的時候,會直接從模組中取出module.exports屬性返回。比如:
// a.js var name = "zaking"; exports.name = name; // b.js var a = require("./a.js"); console.log(a.name); // zaking a.name = "xiaoba"; var b = require("./a.js"); console.log(b.name); // xiaoba
誒?為啥你寫的是「exports.」,不是module.exports?NodeJs在實現CommonJs規範的時候為了方便,給每個模組都提供了一個exports私有變數,指向module.exports。有一點要尤其注意,exports
是模組內的私有區域性變數,它只是指向了 module.exports
,所以直接對 exports
賦值是無效的,這樣只是讓 exports
不再指向 module.exports
了而已。
我們回到上面的程式碼,按理來說,我第二次引入的b的name應該是「zaking」啊。但是實際上,在第一次引入之後的引入,並不會再次執行模組的內容,只是返回了快取的結果。
另外一個核心的點是,我們匯入的是匯出值的拷貝,也就是說一旦引入之後,模組內部關於該值的變化並不會被影響。
// a.js var name = "zaking"; function changeName() { name = "xiaowangba"; } exports.name = name; exports.changeName = changeName; // b.js var a = require("./a.js"); console.log(a.name); // zaking a.changeName(); console.log(a.name); // zaking
嗯,一切看起來都很不錯。
在上一小節,我們簡單介紹了模組化的始祖也就是CommonJs以及實現了該規範的NodeJs的一些核心內容。但是NodeJs的實現的一個關鍵的點是,它在讀取或者說載入模組的時候是同步的,這在伺服器沒什麼問題,但是對於瀏覽器來說,這個問題就很嚴重,因為大量的同步模組載入意味著大量的白屏等待時間。
基於這樣的問題,從CommonJs中獨立出了AMD規範。
AMD,即Asynchronous Module Definition,翻譯過來就是非同步模組化規範,它的主要目的就是解決CommonJs不能在瀏覽器中使用的問題。但是RequireJs在實現上,希望可以通吃,也就是可以在任何宿主環境下使用。
我們先來看個例子:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <script src="https://requirejs.org/docs/release/2.3.6/comments/require.js"></script> <body></body> <script> require(["./a"]); </script> </html>
然後,我們的a.js是這樣的:
define(function () { function fun1() { alert("it works"); } fun1(); });
define用來宣告一個模組,require匯入。我們還可以這樣:
require(["./a"], function () { alert("load finished"); });
匯入前置依賴的模組,在第二個引數也就是回撥中執行。RequireJs會在所有的模組解析完成後執行回撥函數。就算你倒入了一個沒有使用的模組,RequireJs也一樣會載入:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <script src="https://requirejs.org/docs/release/2.3.6/comments/require.js"></script> <body></body> <script> require(["./a", "./b"], function (a, b) { a.fun1(); }); </script> </html>
然後分別是a.js和b.js:
// a.js define(function () { function fun1() { alert("it works fun1"); } return { fun1: fun1, }; }); // b.js define(function () { function fun2() { alert("it works fun2"); } return { fun2: fun2, }; });
結果大家可以試一試~
以上就是RequireJs的簡單用法,我們據此知道了兩個核心內容,RequireJs基於AMD規範,RequireJs會載入你引入的所有模組,哪怕你並不會真的用到它。
由於RequireJs的一些問題,又出現了基於CMD規範的SeaJs,SeaJs和RequireJs有一個最大的不同就是RequireJs希望可以通吃,但是SeaJs則更專注於瀏覽器,哦對了,CMD的全稱叫做:Common Module Definition,即通用模組規範。
SeaJs的簡單用法如下:
// 所有模組都通過 define 來定義 define(function(require, exports, module) { // 通過 require 引入依賴 var a = require('xxx') var b = require('yyy') // 通過 exports 對外提供介面 exports.doSomething = ... // 或者通過 module.exports 提供整個介面 module.exports = ... }) // a.js define(function(require, exports, module){ var name = 'morrain' var age = 18 exports.name = name exports.getAge = () => age }) // b.js define(function(require, exports, module){ var name = 'lilei' var age = 15 var a = require('a.js') console.log(a.name) // 'morrain' console.log(a.getAge()) //18 exports.name = name exports.getAge = () => age })
上面的程式碼是從網上抄的,大概說明白了基本的使用方法。我們可以看到,SeaJs的匯入和匯出的方式,跟NodeJs好像~~而SeaJs從書寫形式上,更像是CommonJs和AMD的結合。當然,我只是說書寫形式上。
而AMD和CMD,RequireJs和SeaJs,都是由社群發起的,並沒有語言層面的規範,包括CommonJs以及NodeJs,所以,這個時代還是一個百花爭豔,沒有統一的時代,不過在現在,這些都不重要了。如果非要我說些什麼,那就是,忘記這兩個東西,去學下面的重點。
百花爭豔的時代確實有些煩人,這個那個那個這個,又都不被官方認可,還好,官方終於還是出手了,ES6的出現,在語言層面上就提出了對於模組化的規範,也就是ES6 Module。它太重要了,具體語法我就不多說了,文末的連結附上了阮一峰大神的《ES6入門指南》關於ES6 Module的地址。
所以到了ES6的時候,你要學習的就是ES6 Module,NodeJs也在逐步實現對ES6 Module的支援。最終,秦始皇會一統天下,這是必然的結果。
這篇文章到這裡就結束了,說實話,模組化的問題和歷史由來已久,從萌芽到統一,至少十幾年的過程,而市面上也已有大量的文章介紹彼此的區別和各自的特點,我寫來寫去,也不過是複製一遍,毫無意義。
但是我又想學一下模組化,以及模組化的歷史,額……,請原諒我的無知,所以才有了這篇文章,但是寫著寫著,發現我能表達出來的東西並不多,因為都是故事,都是歷史,並且對於未來的開發好像也沒什麼實際的意義和價值。
所以,在如此糾結的心態下有了這篇文章,原諒我無知又想逼逼的心情吧。
最後的最後,如果你想學習模組化,在現階段,只需要去深入學習ES6 Module,和學習一下NodeJs的CommonJs,以及瞭解一下各模組化的區別即可,因為現在是即將統一,還未完全統一的時候。
https://wiki.commonjs.org/wiki/CommonJS
https://github.com/seajs/seajs/issues/242
https://es6.ruanyifeng.com/#docs/module