以前寫的文章急速Js全棧教程獲得了不錯的閱讀量,霸屏掘金頭條3天,點贊過千,閱讀近萬,甚至還有人在評論區打廣告,可見也是一個小小的生態了;)。看來和JS全棧有關的內容,仍是有人很有興趣的。javascript
此次文章的內容,是JavaScript模塊。JavaScript Module 真是很討厭,可是不得不瞭解的話題。奇葩在於:css
最近的ES2017,終於在前端也有了媲美后端的模塊,可是你們並不許備把它用起來,不少人表示須要繼續Webpack玩轉ES6模塊。html
把ES6模塊真用的起來,能夠不在意Webpack等打包工具帶來的加載優化,各類小文件沒必要打包這點來講,我看還得加上HTTP/2的配合就好不少了。這也是文章將要介紹的一個主旨吧。ES6模塊的引入,確實有可能對當前主流的打包模式有些影響,參考文章6內有所論述前端
文章天然也很多,可是寫做此文的理由仍是存在:java
<script>
標籤一切從Javascript的加載開始,自有Javascript依賴,第一個加載模塊的方案就是使用HTML標籤,也就是<script>
標籤。也就是說,Javascript自己根本就沒有模塊和加載的定義,它是利用HTML來完成本該本身作的工做。node
這是初學者遇到的第一個使人困惑的問題。這樣的語言,根本就是一個玩具!也許有些尷尬,可是現實就是如此。而且如此的加載方案,在稍微大點的工程中,會遇到幾個違反常識的問題:webpack
仍是從一個案例開始。這個案例,不會作任何有意義的工做,也不會作什麼功能演示,而只是驗證古典的Javascript加載的能力和限定,驗證這些問題的存在,進而找到解決問題的方法。git
假設咱們如今有一個主程序,它在index.html內,一個模塊dep1,一個模塊dep2,依賴關係是index.html依賴dep1,dep1依賴dep2。代碼都不復雜,就是直接列表以下:es6
文件index.htmlgithub
// index.html
<script src="dep2.js"></script>
<script src="dep1.js"></script>
<script type="text/javascript">
console.log(dep1())
</script>
複製代碼
文件dep1.js
var v1 = "dep1"
function dep1(){
return v1+"-"+dep2()
}
複製代碼
文件dep2.js
var v2 = "dep2"
function dep2(){
return v2
}
複製代碼
當使用瀏覽器加載index.html文件時,如我所願,它會隨後加載dep2,dep1,並調用函數dep1,後者調用dep2,而後在控制檯輸出:
dep1+dep2
複製代碼
功能是有效的,依賴關係是是對的,輸出也是如指望的。可是它也帶來了額外的問題:
討論到此,我感受我在重複先輩們的話,實際上1960年代,第一屆軟件工程會議,就提出了模塊化編程的概念,而且在以後多年一直努力的批評全局變量和Goto語句了。有時候,你會發現,這樣看起來很是不濟的語言,卻能夠在現實的項目中如魚得水,發展的很是的好。而軟件工程思想指導下的一些名流語言卻早早夭折。這是另一個有趣的話題了,或許之後有機會談到。
後端Nodejs乾淨利索的解決了此問題。作法就是對每個裝入的模塊都注入一個require函數和一個exports對象。其中require函數能夠被模塊用來引入其餘模塊,而exports對象則被用來引出當前模塊的功能接口。仍是之前文提到的做爲案例,作法就是:
文件index.js
// index.js
var d = require('./dep1')
console.log(d.dep1())
複製代碼
文件dep1.js
var d = require('./dep2')
var v1 = "dep1"
function dep1(){
return v1+"-"+d.dep2()
}
exports.dep1 = dep1
複製代碼
文件dep2.js
var v2 = "dep2"
function dep2(){
return v2
}
exports.dep2 = dep2
複製代碼
執行命令:
$ node index.js
dep1-dep2
複製代碼
這裏有一點變化,就是在nodejs內使用index.js代替了index.html。能夠看到:
在傳統的服務器開發的諸多語言中,模塊都是最基礎也是最必備的,像是JavaScript連個內置模塊支持都沒有的是不常見的(或者說根本沒有?)。使用諸如的require和exports,就在後端乾淨利索的解決了困惱前端的模塊問題,這難免讓前端以爲應該效仿之。固然,Nodejs加載模塊是同步的,這個是不能在前端效仿的。由於後端從磁盤加載代碼,速度根本不是問題,而前端加載的都是從網絡上進行的, 若是同步的話,加上Javas自己的單線程限定,整個UI就會由於加載代碼而被卡死的。對比下二者的速度差別,你就明白了:
硬盤 I/O
-----------------
HDD: 100 MB/s
SSD: 600 MB/s
SATA-III: 6000 Mb/s
-----------------
網速 I/O
ADSL: 4 Mb/s
4G: 100 Mb/s
Fiber: 100 Mb/s
複製代碼
思路倒也簡單,只要本身編寫一個庫,有它來異步加載其餘模塊,並在加載時注入須要的require和exports便可。這方面的庫有幾個,好比requirejs,sea.js等。由於咱們只是爲了講清楚概念和思路,所以會那概念上最清晰,和Nodejs最爲一致的庫來講明問題,並不會由於那個更加主流而去選擇它。從這個標準看,sea.js是說明概念問題的最佳模塊裝入庫。
sea.js 是一個模塊加載器,模塊加載器須要實現兩個基本功能:
核心落腳點,就在規範二字上。sea.js要求模塊編寫必須在真正的代碼以外套上一層規定的代碼包裝,樣子看起來是這樣的:
define(function(require, exports, module) {
// 模塊代碼
});
複製代碼
經過傳遞一個簽名爲function(require, exports, module)
的回調函數給define函數,就能夠把須要注入的變量和函數注入到模塊代碼內。以前的實例代碼,在這裏寫成:
文件index.js
// index.js
define(function(require, exports, module) {
var d = require('./dep1')
console.log(d.dep1())
});
複製代碼
文件dep1.js
define(function(require, exports, module) {
var d = require('./dep2')
var v1 = "dep1"
function dep1(){
return v1+"-"+d.dep2()
}
exports.dep1 = dep1
});
複製代碼
文件dep2.js
define(function(require, exports, module) {
var v2 = "dep2"
function dep2(){
return v2
}
exports.dep2 = dep2
});
複製代碼
除了加上一層有點看起來莫名其妙的外套代碼,其餘的模塊代碼,你該怎麼寫就怎麼寫。假若不是那麼潔癖,這樣的代碼確實解決了以前使用script標籤加載代碼帶來的全局變量污染等問題,而且仍是能夠異步加載的,那些看起來不錯的依賴關係,也如Nodejs同樣。以上代碼,能夠直接把nodejs對應的代碼拷貝過來,加上外套便可運行。
咱們不妨加入seajs文件,來看看實際的使用效果:
//index.html
<script type="text/javascript" src="https://cdn.bootcss.com/seajs/3.0.2/sea.js"></script>
<script>
seajs.use('./index.js');
</script>
複製代碼
這裏爲了偷懶,我使用了seajs的CDN文件。若是有遇到什麼問題,你不妨本身下載一個seajs文件,而後改爲你的URL便可。
加載此HTML文件,能夠在控制檯看到輸出:
dep1+dep2
複製代碼
說明seajs執行效果不錯!
document.createElement(‘script’)
,這些標籤的建立會致使瀏覽器加載對應的腳本對模塊的價值,都是異步加載,瀏覽器不會失去響應,它指定的回調函數,只有前面的模塊都加載成功後,纔會運行,解決了依賴性的問題。
能夠在控制檯輸入:
seajs.data.fetchedList
複製代碼
查看文件加載清單。
由於不是語言自帶,而是社區經過現有的語言特性,硬造出來的一個模塊系統,由於看起來代碼難免累贅。可是在沒有原生模塊的狀況下,這樣作確實是管用的。要知道真正的原生模塊,在ES6標準以後纔出現,這都是2015年的事兒了。在一些有名的應用如Gmail、Google Map的推進下,Web從簡單的展現到App的變化,迫切須要這樣相似的模塊技術,你們等不了那麼久,先弄一個能用的是很重要的。
爲何要套這層外殼呢?就是爲了解決全局變量污染問題。在JavaScript語言內,惟一提供本地做用域的就是函數和閉包,經過閉包function(require, exports, module)
,模塊加載器給模塊注入了必要的函數和變量。看起來在模塊以內的任何地方均可以使用require和exports,可是他沒都不是全局變量,而是閉包內變量。這些變量都是局部化的,絕對不會污染全局空間。
使用require函數,能夠就近指定對其餘模塊的依賴,函數自己是由sea.js這樣的模塊加載器提供,它會內部構造依賴關係圖譜,並根據依賴關係,設置加入script標籤的次序。
更加詳細的理解這層外殼,能夠閱讀seajs源代碼,代碼量並不大,值得一讀。或者看看此問答
固然Seajs也引入了本身的規範,叫作CMD規範。它的前身是Modules/Wrappings規範。SeaJS更多地來自 Modules/2.0 的觀點,同時借鑑了 RequireJS 的很多東西,好比將Modules/Wrappings規範裏的 module.declare改成define等。 說是規範,卻不像是通常的規範那麼冗長,可能打印出來也就一兩頁的紙張而已,這也是JavaScript社區的一個特色吧。Modules/Wrappings
seajs的做者在一篇文章中提到了業界在開發前端模塊加載器時的場景:
大概 09 年 - 10 年期間,CommonJS 社區大牛雲集。CommonJS 原來叫 ServerJS,推出 Modules/1.0 規範後,在 Node.js 等環境下取得了很不錯的實踐。09年下半年這幫充滿幹勁的小夥子們想把 ServerJS 的成功經驗進一步推廣到瀏覽器端,因而將社區更名叫 CommonJS,同時激烈爭論 Modules 的下一版規範。分歧和衝突由此誕生,逐步造成了三大流派:
這個模塊加載器是更加主流的。之因此不是首先提到它,是由於概念上來講seajs更加簡明。和seajs相比,requirejs是更加主流的框架。它的差別主要是一些零零散散的不一樣,好比模塊代碼的外套是不太同樣的:
require(['moduleA', 'moduleB', 'moduleC'], function (moduleA, moduleB, moduleC){
複製代碼
// some code here });
導出模塊變量和函數的方式,也是不一樣的。requirejs的引出方式是直接返回:
return {foo:foo}
複製代碼
同樣的案例,使用requirejs的話,代碼是這樣的:
index.html文件
<script data-main="index"
src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.js" ></script>
複製代碼
index.js文件:
require(['./dep1'], function (d){
console.log(d.dep1())
});
複製代碼
dep1.js文件:
define(['./dep2'], function (d){
var v1 = "dep1"
function dep1(){
return v1+"-"+d.dep2()
}
return {dep1:dep1}
});
複製代碼
dep2.js文件:
define(function() {
var v2 = "dep2"
function dep2(){
return v2
}
return {dep2:dep2}
});
複製代碼
瀏覽器打開文件index.html,能夠看到控制檯輸出同樣的結果。
稍加對比require.js和sea.js。使用Require.js,默認推薦的模塊格式是:
define(['a','b'], function(a, b) {
// do sth
})
複製代碼
使用seajs的時候,相似的功能,代碼這樣寫:
define(function(require, exports, module) {
var a = require('a')
var b = require('b')
// do sth
...
})
複製代碼
Seajs的作法是更加現代的。我須要用的時候,我纔去引用它,而不是實現什麼都引用好,而後用的時候直接用就好。
以require.js爲表明的Modules/Async流派,尊重了瀏覽器的特殊性,代價是無論寫什麼模塊,都得本身給本身穿上一層外套,對於有代碼潔癖的人來講,這樣的狀況是看不下去的。最好是開發人員編寫乾乾淨淨的模塊代碼,框架開發者作一個工具,這個工具自動的把這些代碼轉義成客戶端承認的異步代碼。即在瀏覽器上運行前,先經過轉換工具將模塊轉換爲符合規範的代碼。這就是Modules/1.x 流派的作法。須要注意的是1.x和2.0還有Async流派不能簡單的認爲版本號大的就更好。卻是理解成各自不一樣的解決方案爲好。
以咱們本身的案例來講,就是能夠直接把nodejs代碼那裏,使用一個工具作一個轉換,便可獲得符合前端須要的代碼,這些代碼是異步加載的、是能夠保證模塊變量局部化的、是能夠由良好的依賴關係定義的。工具browerfy就是作這個的。咱們來試試具體是怎麼玩的。
首先安裝此工具:
npm install --global browserify
複製代碼
到你的nodejs代碼內,而後轉換此代碼,生成一個新的js文件,通常命名爲bundle.js:
browserify index.js -o bundle.js
複製代碼
而後建立index.html並引入bundle.js:
<script type="text/javascript" src="./bundle.js"></script>
複製代碼
使用瀏覽器打開此HTML文件,能夠在控制檯看到熟悉的輸出,這說明轉換是有效的:
dep1+dep2
複製代碼
自己nodejs的代碼,是不能在瀏覽器執行的,瀏覽器內也沒有什麼require函數,可是轉換後就能夠執行了。那麼,轉換的過程,到底玩了什麼魔術?
像是browserify這樣的工具,就是找到全面被引入的代碼,解析它的依賴關係,而且自動的加入咱們在requirejs裏面須要的外套代碼。儘管bundle.js文件並非爲了閱讀優化的,可是能夠取出其中的代碼片斷來證明咱們的觀點:
{"./dep2":2}],2:[function(require,module,exports){
var v2 = "dep2"
function dep2(){
return v2
}
exports.dep2 = dep2
},{}],3:[function(require,module,exports){
var d = require('./dep1')
console.log(d.dep1())
},{"./dep1":1}]},{},[3]);
複製代碼
咱們能夠看到原本的nodejs代碼,以及它們對應的外套。仍是比較簡單,就不進一步解釋了。browserify不但完成了加外套代碼的工做,還同時把若干小文件打成一個大的文件,對於當前使用的HTTP主流版本1.1來講,這樣作會讓加載效率更高。可是對於HTTP/2.0來講,它已經支持了多個文件在一個鏈接內交錯傳遞,所以再作打包的意義就不大了。只是...HTTP/2.0的普及還須要時日。
browerify完成的工做簡明而單一。另一個主流的同類工具叫作webpack,不但能夠轉換js代碼,還能夠打包css文件、圖片文件,而且能夠作一些工程化的管理,代價就是webpack學起來也困難的多。實際上像是Vuejs這樣的UI開發框架,內部就是使用了webpack作工程化管理和代碼轉譯的。可是在模塊化方面,二者是差很少的。就不另外介紹了。
時間到了May 9, 2018,我看到了阮一峯發佈了這樣的微博:
今天 Firefox 60發佈,默認打開了ES6 模塊支持,至此全部瀏覽器都默認支持ES6模塊。前端開發模式可能所以大變。如今的方案是全部模塊發到npm,本地寫好入口文件,再用webpack打包成一個腳本。可是若是瀏覽器原生支持,爲何還要打包呢?至少簡單的應用能夠直接加載入口文件,瀏覽器本身去抓取依賴。
複製代碼
這裏全部瀏覽器指的是Edge、Firefox、Chrome、Safari。固然,再一次沒有IE。若是想要支持IE或者比較老的版本的話,仍是須要使用打包器來完成代碼的轉譯。另外不少人表示會繼續使用Webpack,緣由很簡單,Webpack不只僅是完成模塊打包工做,還有壓縮、混淆等,而且不少框架還須要依賴它。因此遷移並不是一朝一夕之功。而無需考慮老版本瀏覽器的兼容的代碼,是徹底能夠大量的使用它了。了不得在把Webpack加起來轉換ES Module到加外套的代碼就是了。
ES6 Module不是requirejs那樣加外套的樣子,也不是Nodejs使用require函數的樣子,而是另一套有官方提出的模塊模式。它使用的是import、export關鍵字。官方的就是不同,社區是加不了關鍵字的。一樣的案例,使用ES6 Module就是這樣的了。
index.html文件:
<script type="module">
import {dep1 } from './dep1.js'
console.log(dep1())
</script>
複製代碼
dep1.js文件
import {dep2} from './dep2.js'
var v1 = "dep1"
export function dep1(){
return v1+"-"+dep2()
}
複製代碼
dep2.js文件:
var v2 = "dep2"
export function dep2(){
return v2
}
複製代碼
ES6 Module要求必須有後臺的HTTP服務器,而不能直接在文件系統上完成Demo的測試。所幸使用Nodejs搭建一個服務器也很是簡單直接:
npm i http-server -g
http-server
複製代碼
在瀏覽器內訪問此HTML文件的URL,能夠看到控制檯輸出:dep1+dep2。這個輸出,已是你的老朋友了。
Nodejs在10.9才支持實驗版本的ES6 Module,是落後了點,可是對於Nodejs來講,新的模塊技術原本也就並不迫切。
綜合以上的內容,我認爲,在沒必要考慮古老的瀏覽器兼容的狀況下,最好的實踐是這樣的:
這樣的實踐,會隨着HTTP/2的逐步普及和ES6被更多的開發者採用,而成爲更好的選擇。
使用ES6 Module的壞處是沒法像require那樣動態的加載。可是好處是能夠精確指明對於一個庫,咱們使用的是那些,這就給工具提供了優化的可能,就是說若是我引入了一個庫,可是這個庫內有些我不會用的,那麼這些不會被用到的代碼也不會加載到前端了。這個功能對於後端來講意義不大,可是對於前端來講,就是很是使人喜歡的功能了。實際上,這樣的工具已經有了,比較知名的就是rollup,它屬於了一種被稱爲tree-shaking的技術優化使用代碼。
而以往作模塊打包,不少的緣由是HTTP/1.1傳遞大量小文件的時候開銷比較大,而打包成單一的問題,就能夠更好的利用HTTP/1.1的傳輸特性。可是HTTP/2.0的一個大的特點就是能夠在單一的鏈接內,併發和交錯的傳遞多個流,由於在一個鏈接內交錯的傳遞多個文件,就能夠再也不有HTTP/1.1的鏈接開銷了。所以,在HTTP/2.0被採納的網絡裏面,打包單一文件的價值幾乎沒有了。直接使用小文件默認狀況下就能夠獲得比較好的優化傳輸。
按照如今的技術發展的勢頭,要不了幾年,打包器將再也不那麼必要,使用原生代碼編寫模塊將會成爲主流的。
參考文章很多,其中模塊歷史和選型以下:
這篇文章預計想要編寫的YUI方法,YUI Combo方法,想了想仍是算了,由於這樣的恐龍代碼,已經在平常的代碼實踐中逐步消失,做爲一個曾經比較重要,如今則退居二線的代碼庫,對它最好的讚許就是讓它退休,也沒必要給讀者增長額外的閱讀負擔了。畢竟require.js、browerify、webpack都工做的不錯,在此基礎上發展的Vuejs、React.js也的獲得了更多的承認。
本文講到的模塊規範和實踐工具,爲編寫一個廣爲社區承認的模塊起到了最基礎的規範做用。可是,JavaScript社區最爲使人稱道的就是代碼庫倉庫。包括NPM倉庫,Bower倉庫。在這些倉庫內,有模塊依賴管理工具,還有工程化工具。這些內容,它們固然是重要的,不在本文的範圍內。
做爲前端開發者,有人採用Bower管理組件依賴,也有人使用Npm作相似的工做。有不少時候,這樣的實踐是使人困惑的。還有這裏npm and the front end,NPM官方也對npm在前端的使用,提出了[本身的見解][blog.npmjs.org/post/101775…]。
這些未盡的內容,或許在將來的文章中表達之。