關於前端JS模塊加載器實現的一些細節

 

最近工做須要,實現一個特定環境的模塊加載方案,實現過程當中有一些技術細節不解,便參考 了一些項目的api設計約定與實現,記錄下來備忘。javascript

本文不探討爲何實現模塊化,以及模塊化相關的規範,直接考慮一些技術實現原理。html

1.簡單實現模塊化

一開始我想若是個人代碼只有一個文件,那幾行不就實現了嗎java

main.jsgit

var modules = {}
var define = function(id,factory){
    moudles[id] = factory
}
var require = function(id){
    return modules[id]
}
define("moduleA",{text:"I am text"})
var moduleA = require("moduleA");
console.log(moduleA)

main.htmlgithub

<script src="main.js"></script>

2.拆多個文件

後來業務需求的增加,個人一個代碼文件逐漸膨脹到了接近2w多行。這個時候每次改動文件找函數找半天啊,俺的編輯器也時不時的開始崩潰了,傳到服務端的時候,也要等很久很久了。。。json

因而我把文件拆成了3個:gulp

1.module.js後端

var modules = {}
var define = function(id,factory){
    moudles[id] = factory
}
var require = function(id){
    return modules[id]
}

2.moduleA.jsapi

define("moduleA",{text:"I am text"})

3.main.js跨域

var A = require("moduleA")
console.log(A)

因而我在html中得這麼寫了

<script src="module.js"><script>
<script src="moduleA.js"><script>
<script src="main.js"><script>

後來瞭解到,我能夠用構件工具gulpconcatwatch模塊,能夠監聽文件改動,自動生成大文件,以便在開發的時候能夠按模塊拆成多個文件,運行的時候倒是在一個文件。詳細能夠了解相關資料。

3.按模塊加載文件

上面提到用構件工具來實現打包成一個文件,這樣作有個缺點,代碼若是有錯誤,報錯的行數沒法與相應文件模塊的行數相對應,debug困難。

這個時候貌似只有不依賴於構件工具,咱們在代碼中實現加載其餘模塊。 貌似也挺簡單的。

咱們得知道,script標籤是能夠用JS動態建立和加載的

var loadScript = function(src){
    var script = document.createElement("script")
    script.src = src
    document.head.appendChild(script)
}

因而咱們能夠在main.js中這樣去加載

loadScript("module.js")
loadScript("moduleA.js")

這樣就能夠在頁面中只引入一個主文件,而後在主文件中引入其餘模塊文件了。

多瞭解一些咱們會知道 loadScript 這樣的代碼加載方法,是並行加載非順序執行的,有可能moduleA的代碼執行的時候module尚未執行,這是就會報錯 variable define is not defined 了。

4.控制文件加載順序

script在加載過程當中會有一些狀態,支持設立回調函數好比 onloadonreadysteadychange 這樣咱們能夠在當一個模塊加載完成後加載另外一個模塊來控制文件加載順序。

咱們經常使用的jsonp技術便也大概是這樣一個原理。

var loadScript = function(src,callback){
    var script = document.createElement("script")
    script.src = src
    script.onload = callback
    document.head.appendChild(script)
}
loadScript("module.js",function(){
    loadScript("moduleA.js",function(){
        var A = require("moduleA")
        console.log(A)
    })
})

這樣的壞處即是,代碼中要寫層層回調,模塊的加載順序須要寫代碼的人本身來管理。

5.XHR加載代碼

script標籤能夠設置src加載遠程代碼,還能夠直接把代碼寫在標籤內。

<script>
    define("A","i am A")
</script>

因而咱們能夠經過XHR對象,加載遠程代碼文本,而後動態的插入進去,好比innerHTML 甚至,XHR有同步的加載方法,來讓咱們串行的加載代碼,避免寫重重回調。固然,同步的XHR請求性能很低

XHR有個硬傷就是受瀏覽器同源策略影響,不能方便的跨域。

6.實現高級API

有了上面的一些基礎,咱們就能夠來封裝一些高級的API了。

通常來講,咱們只須要這樣一個define(id,deps,factory),實現了模塊的定義和加載就基本夠用了。

define("moduleC",["moduleA","moduleB"],function(moduleA,moduleB){
    console.log(moduleA,moduleB)
})

這樣的define作了這麼一些事情

  • 將id 和 factory關聯
  • 用loadscript的方案,去遞歸的加載deps,保證該模塊被依賴時,模塊自己依賴的模塊都加載完畢。
  • 收集完畢後按照deps順序將相關模塊經過apply傳遞給factory

7.自動收集依賴

咱們以爲每次去寫一堆依賴,而後還要保證deps順序和factory的變量順序一致,一一對應着實有些蛋疼,這時候咱們會想要把deps去掉,改爲在factory裏面寫依賴。

moduleC.js

define("moduleC",function(require){
    var moduleA = require("moduleA")
    var moduleB = require("moduleB")
})

這時候須要用到JS的一個神奇的特性,function的toString方法能夠拿到函數的源代碼。 這樣咱們能夠經過一些手段分析出 require 了哪些模塊。能夠看這裏 https://github.com/seajs/seajs/issues/478

固然爲了可以分析出require了哪些模塊,咱們要對require作一些約定,就是但願require有一些特定的標誌,以便於咱們可以經過代碼文本靜態的分析出require項。

好比說 不可以這樣,詳細見 https://github.com/seajs/seajs/issues/259

var req = require
req("moduleA")

而後呢,也不能用通用的壓縮工具壓縮,由於壓縮工具會把require變量壓縮。

8.定義匿名模塊

有時候咱們以爲文件名已經可以表明模塊名字了,咱們連定義模塊名字都不想要了。

moduleC.js

define(function(){
    var moduleA = require("moduleA")
    var moduleB = require("moduleB")
    return {
        A : moduleA,
        B : moduleB
    };
})

當初看到這樣的api用法時都震驚了,由於以前實現define的時候都會把id和factory相關聯,這沒ID怎麼辦?後來冷靜下來,以爲ID必定是有的,只是有辦法不經過函數參數傳遞。

果真,有一個document有一個對象叫作currentScript,能夠得到當前正在執行的script的對象,因而moduleC.js在執行的時候,define是能夠經過document.currentScript拿到src爲moduleC.js的script對象的,進而能夠提取出ID。

這裏關於瀏覽器兼容性有一些細節:

  • document.currentScript只有現代瀏覽器才支持。
  • IE6-10會有一些黑魔法,利用瀏覽器單線程執行的特性,獲取頁面上全部的script標籤,判斷其readystate爲interactive時,該script即是document.currentScript
  • 利用Error.stack,獲得文件調用棧,來分析獲得currentScript

寫匿名模塊方即是方便,可是會帶來一些麻煩。

好比,不能直接打包成一個文件了,由於依賴於模塊的文件名,這個很好理解了。

define(function(){
    return 100
})
define(function(){
    return 200
})

9.加載文本資源

甚是懷念在孢子工做時的那套代碼結構與模塊化方案,開發不須要依賴構建工具,模板直接寫html文件,不用包裝amd。 等等。。,模板直接寫html文件是怎麼作到的,嘗試去看源碼,基本看不懂,孢子源碼太難讀了。後來抓包才知道,原來是後端配合,在特定的目錄名稱下返回的html文件會自動包上define,黑魔法。。

固然也有其餘方法,通常狀況下就是用XHR,加載相應地文本,而後用eval設定執行上下文環境爲global,來包裝define。

10.參考:

  • http://requirejs.org/docs/why.html
  • https://github.com/seajs/seajs/issues/259
  • http://www.cnblogs.com/rubylouvre/archive/2013/01/23/2872618.html
相關文章
相關標籤/搜索