為什么要網頁模塊化?
這篇文章討論的是為什么Web模塊化是很有用的,并介紹了現在可以用來實現Web模塊化的一些機制。這里有另一篇文章介紹了RequireJS使用的函數包裝格式的設計理念。
問題§1
網站逐漸轉化為Web apps
代碼復雜度逐漸提高
組裝變的困難
開發者想要分離的JS文件/模塊
部署時可以把代碼優化成幾個HTTP請求
解決方案§2
前端開發者需要這樣的解決方案:
一些這類的API #include/import/require
有能力加載嵌套的依賴
對開發者來說易于使用,并且有優化工具在后面支持,有助于部署
腳本載入API § 3
首先梳理出腳本載入API。這里有幾個選擇:
-
Dojo: dojo.require("some.module")
-
LABjs: $LAB.script("some/module.js")
-
CommonJS: require("some/module")
所有的都映射到載入 some/path/some/module.js。理想情況下,我們可以選擇CommonJS的語法,因為它很可能會變得更加常見,而且我們想要重用代碼。
當前我們也希望一些語法能夠載入已存在的純文本JavaScript文件,因此開發者不用重寫所有的JavaScript來從腳本載入中獲益。
但是,我們需要一些能在瀏覽器中更好的工作的事物。CommonJS 的require()是一個同步調用,它期望能夠立即返回那個模塊。不過這在瀏覽器中工作的不是很好。
異步與同步§ 4
下面這個例子說明了瀏覽器的基本問題。假設我們有一個Employee對象,我們想要一個派生自Employee對象的Manager對象。獲取該例子,我們可能會用我們的腳步載入API來這樣編碼:
var Employee = require("types/Employee");function Manager () { this.reports = []; }//Error if require call is asyncManager.prototype = new Employee();
如上面注釋中所示,如果require()是異步的,這段代碼不會工作。但是,在瀏覽器中同步載入腳步將會抹殺性能。那么,怎么辦?
腳本載入:XHR§ 5
使用XMLHttpRequest(XHR)載入腳本是很有吸引力的。如果使用XHR,我們就可以觸摸上面的文本,也就是可以通過正則表達式來查找require()調用,以確保我們載入了這些腳本,然后再用eval()或script元素將文本內容傳給使用XHR載入的腳本。
-
使用eval()來評估模塊不太好:
-
開發者已經被告知eval()不好用。
-
有些環境不支持eval()。
-
難以調試。Firebug和WebKit的檢查器有一個//@ sourceURL= 約定,用來給被評估的文本命名,不過這個特性不是所有的瀏覽器都支持。
-
不同的瀏覽器評估上下文環境是不同的。IE中的execScript或許可以做到,但是同時也意味著更多的移動部件。
使用帶文本內容的script標簽來設置為文件文本也不太好:
-
調試的時候,你得到的錯誤行號和源文件對不上號。
XHR 在跨域請求的時候還有問題。一些瀏覽器現在有跨域XHR的支持,但并不是全部。并且 IE 決定創建一個不同的API對象:XDomainRequest來實現跨域請求。出現了更多的需要改動的地方,更容易出錯。特別是,你需要確定不發送任何不標準的HTTPheader或者還需要另外一個"預檢"的請求來保證這次跨域的請求是被允許的。
Dojo 通過eval()使用基于XHR的loader,但是,雖然它能用,但是一直是困擾開發者的源頭。Dojo 有一個 xdomain loader但是它需要通過使用一個函數wrapper來修改require的模塊,所以script src=""標簽可以用來加載模塊了。還有很多邊界情況和變化的地方來給程序員增加困難。
如果我們創建一個新的腳本加載器,我們可以做的更好。
腳本載入:Web Workers § 6
web worker可能是另一個加載腳本的方法,但是:
-
它的跨平臺性不好
-
它是一個消息傳遞API,并且該腳本可能要與DOM交互,它只是使用worker獲取腳本的文本,然后將文本回傳給主窗口,再用eval/script來執行腳本。這種方法帶有上面提到的XHR的全部問題。
腳本載入:document.write()§ 7
document.write()可以用來載入腳本,它可以從其他的域載入腳本并且映射了瀏覽器通常是如何使用腳本的,因此它可以用來進行簡單的調試。
但是,在異步VS同步的例子中,我們不能直接執行腳本。理想情況下,在執行腳本前我們能夠通過require()知道相關依賴項,并且確保這些依賴項被首先載入。但是我們不能在腳本執行前訪問它。
而且,document.write()在頁面載入后就不工作了。對于你的網站,一個好的方法是在用戶需要進行下一步操作時來載入腳本。
最后,通過document.write()載入腳本或阻塞頁面的渲染。要讓你的網站有最佳表現,這個方法是不可取的。
腳本載入:head.appendChild(script)§ 8
我們可以在需要時創建腳本并將它們添加到頭部:
var head = document.getElementsByTagName('head')[0], script = document.createElement('script'); script.src = url; head.appendChild(script);
上面的腳本片段多了一點東西,不過那正是基本的思想。這種方法比document.write要好,因為它不會阻塞頁面的渲染并且在頁面載入后仍能工作。
但是,它仍然有同步VS異步例子的問題:理想情況下,在執行腳本前我們能夠通過require()知道相關依賴項,并且確保這些依賴項被首先載入。
函數封裝 § 9
在執行我們的腳本前,我們需要知道相關依賴項并確保已經將其載入。做這件事的最好方法是通過函數封裝來構造我們的模塊載入API。像這樣:
define( //The name of this module "types/Manager", //The array of dependencies ["types/Employee"], //The function to execute when all dependencies have loaded. The //arguments to this function are the array of dependencies mentioned //above. function (Employee) { function Manager () { this.reports = []; } //This will now work Manager.prototype = new Employee(); //return the Manager constructor function so it can be used by //other modules. return Manager; } );
這是ReguireJS的句法。如果你想載入沒有定義成模塊的純文本的JavaScript的話,有一種簡單的句法:
require(["some/script.js"], function() { //This function is called after some/script.js has loaded. });
選擇這種句法是因為,它足夠簡潔并且允許載入者使用head.appendChild(script)載入類型。
出于在瀏覽器中良好工作的需要,它有不同于普通的CommonJS句法。有建議說普通的CommonJS句法可以使用head.appendChild(script)的載入類型,如果服務器進程有封裝的函數可以將模塊轉換成傳輸格式的話。
我相信不強制使用一個運行時服務器進程來轉換代碼是很重要的事: