大型網站專案中,JavaScript 按需載入是一個常見的需求。幾年前,LABjs曾經流行過一段時間,它的主要原理是建立一個 type=”text/cache” 的 script標籤,並在需要的時候將其更改為 type=”text/javascript”,從而動態並行地載入 JS 並控制其執行時間。
使用 LABjs 時,被引入的 JS 幾乎不需要更改,使用非常方便。但它也有不足,最大的問題是它只是一個載入器,沒有模組管理功能,而後者對大型前端專案非常重要。很快,隨著 CommonJS、AMD、CMD 等規範的流行,Require.JS、SeaJS 等兼顧了 JS 檔案按需載入以及模組化的載入器佔據了更大的市場。LABjs 也在兩三年前宣佈停止開發,後來又說還會維護,只是不再新增新功能。
CommonJS 規範主要在 Node.JS 環境中使用,當然,現在也有browserify、webpack 等工具可以讓瀏覽器端的 JS 直接使用 CommonJS 規範。它們的原理一般是分析依賴關係,然後將所有依賴的 JS 打包為一個檔案。(webpack 也可以實現動態按需載入。)
AMD、CMD 規範則是完全為瀏覽器端 JS 設計的。它們的設計細節不同,不過最基本的原理一樣:通過類似 JSONP 的方式載入 JS 並隔離不同模組的變數。當然,在具體實現過程中還有很多問題需要考慮,比如模組依賴關係等。另外,SeaJS 等還會用正則匹配出使用者在程式碼中直接用 require 等“關鍵字”載入的模組並自動加入依賴。
下面是我參考 AMD 規範實現的一個極簡的 JavaScript 模組載入器(原始碼),去掉註釋和空行差不多100行的樣子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
(function (global) { "use strict"; var LOADER_NAME = "myloader"; var LOADER_FN_DEFINE = "define"; if (global[LOADER_NAME]) return; var loader = {}; var registered_modules = {}; var loaded_modules = {}; var on_modules_loaded = {}; var doc = document; var node_script = doc.getElementsByTagName("script")[0]; var _idx = 0; function Loader(name, deps, callback) { console.log("name", name); this.name = name; this.deps = deps; this.callback = callback; this.deps_left = deps.length; this.init(); } Loader.prototype = { init: function () { if (this.deps_left == 0) { // 沒有依賴,直接載入 this.loaded(this.name); } for (var i = 0; i < this.deps.length; i++) { this.loadModule(this.deps[i]); } }, loadModule: function (name) { var _this = this; if (loaded_modules.hasOwnProperty(name)) { // 該模組已經載入了 this.loaded(name); return; } var m = registered_modules[name]; if (!m) { throw new Error("unregisted module: " + name); } var el = doc.createElement("script"); el.src = m.url; node_script.parentNode.insertBefore(el, node_script); on_modules_loaded[name] = on_modules_loaded[name] || []; on_modules_loaded[name].push(function () { _this.loaded(); }); }, loaded: function () { this.deps_left--; if (this.deps_left <= 0) { this.run(); } }, run: function () { if (loaded_modules[this.name]) return; var modules = []; var i; for (i = 0; i < this.deps.length; i++) { modules.push(loaded_modules[this.deps[i]]); } loaded_modules[this.name] = this.callback.apply(null, modules) || {}; var fns = on_modules_loaded[this.name] || []; var fn; while (fn = fns.shift()) { fn.call(); } } }; global[LOADER_NAME] = loader; global[LOADER_FN_DEFINE] = function (module_name, dependences, fn) { if (typeof dependences == "function") { fn = dependences; dependences = []; } new Loader(module_name, dependences, fn); }; /** * * @param configs * configs 格式: * { * name: "a", * url: "http://xxx/libs/a.js" * } */ loader.register = function (configs) { if (Object.prototype.toString.call(configs) === "[object Array]") { // 傳入的是一個陣列 for (var i = 0; i < configs.length; i++) { loader.register(configs[i]); } } else { registered_modules[configs.name] = configs; } }; loader.use = function (modules, callback) { if (typeof modules == "string") { modules = [modules]; } new Loader(_idx++, modules, callback); }; })(window) |
你可以在 GitHub 上檢視它的原始碼及示例 。需要說明的是,它只實現了 AMD 規範的一個子集,並且把 require 改為了 myloader.use,同時對迴圈依賴等情況也沒有做處理。不過除了這些,它已經是一個可以使用並且相容各大瀏覽器的 JavaScript 模組載入器了。
最後,現在流行的各種 AMD、CMD 載入器,在不久的將來也會像 LABjs 一樣被人慢慢忘記,因為它們都只是為了解決某一個特定歷史階段的某一類技術問題而誕生的,隨著相關技術的發展,它們也將慢慢完成歷史使命,退出前端舞臺。比如,基於 jspm 等專案,我們現在已經可以使用 ES6 中的模組載入方法:
1 2 3 |
System.import('buffer').then(function(buffer) { console.log(new buffer.Buffer('base64 encoded').toString('base64')); }); |