為什麼要網頁模組化?

oschina發表於2014-11-17

  這篇文章討論的是為什麼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)的載入型別,如果伺服器程式有封裝的函式可以將模組轉換成傳輸格式的話。

  我相信不強制使用一個執行時伺服器程式來轉換程式碼是很重要的事:

  • 一是除錯變的很怪異,因為伺服器在注入封裝函式時會導致原始檔的行號關閉。

  • 二是需要做更多的工作。前端開發應該儘可能的使用靜態檔案。

  關於設計的力量和功能封裝格式的使用案例的更多細節,被叫做非同步模組定義(Asynchronous Module Definition (AMD)),請前往為什麼是AMD?

  原文地址:http://requirejs.org/docs/why.html

相關文章