摘要:如果你不太明白模塊化的作用,建議看看玉伯寫的一篇文章。我們可以使用自己的方式去管理代碼,不過有人已經研究處理一套標準,而且是全球統一,那就拿著用吧關于規范,我這里就不多說了,可以去看看草案,玉伯也翻譯了一份。
試發一彈,本文同步自:http://barretlee.com
略蛋疼的是不支持: [title][url reference]
相信很多人都用過 seajs、 requirejs 等這些模塊加載器,他們都是十分便捷的工程管理工具,簡化了代碼的結構,更重要的是消除了各種文件依賴和命名沖突問題,并利用 AMD / CMD 規范統一了格式。如果你不太明白模塊化的作用,建議看看玉伯寫的一篇文章。
為什么他們會想到使用模塊化加載呢,我覺得主要是兩點。
一是按需加載,業務越來越大,基礎代碼也會越來越多,開發人員可能開發了一百個小工具,而且都塞在一個叫做 utils.js 的包里,但是一個頁面可能只需要三到五個小工具,如果直接去加載這個 utils.js 豈不是很大的浪費,PC 端還好,主要是無線端,剩下 1KB 那都是很大的價值啊,所以呢,如今很多框架的開發都體現出細顆粒度的分化,像百度研究比較賣力的 tangram,阿里放滿產品線的 kissy,幾乎是細分到了微粒程度,這種細分方式也促進了模塊化加載技術的發展,比如為了減少請求數量,kissy 的 config 中開啟 combo 就可以合并多個請求為一個等等。
第二點,應該也是從服務器那邊參考而來的,服務器腳本很多都是以文件為單位分離的,如果要利用其它文件的功能,可以輕而易舉的 require 或者 include 進來,我沒有去研究這些加載函數的內部實現原理,稍微猜猜應該是把文件寫入到緩存,遇到 include 之類的加載函數,暫停寫入,找到需要 include 的文件地址,把找到的文件接著上面繼續寫入緩存,以此類推,直到結束,然后編譯器進行統一編譯。
一、模塊化加載的技術原理先不考慮各種模塊定義規范,本文目的只是簡要的分析加載原理, CMD / AMD 規范雖內容然不多,但是要實現起來,工程量還是不小。文章后面會提到。
1. 數據模塊的加載既然是模塊化加載,想辦法把模塊內容拿到當然是重頭戲,無論是 script 還是 css 文件的加載,一個 script 或者 link 標簽就可以搞定問題,不過我這里采用的是 ajax,目的是為了拿到 script 的代碼,也是為了照顧后面要說的 CMD 規范。
var require = function(path){ var xhr = new XMLHttpRequest(), res; xhr.open("GET", path, true); xhr.onreadystatechange = function(){ if(xhr.readyState == 4 && xhr.status == 200){ // 獲取源碼 res = xhr.responseText; } } xhr.send(); };
創建 script 便簽加載腳本不會存在跨域問題,不過拿到的腳本會被瀏覽器立馬解析出來,如果要做同異步的處理就比較麻煩了。沒有跨域的文件我們就通過上面的方式加載,如果腳本跨域了,再去創建標簽,讓文檔自己去加載。
// 跨域處理 if(crossDomain){ var script = document.createElement("script"); script.src = path; (document.getElementsByTagName("head")[0] || document.body).appendChild(script); }2. 解析模塊的層次依賴關系
模塊之間存在依賴關系是十分正常的,如一個工程的文件結構如下:
project/ ├── css/ │ └── main.css ├── js/ │ ├── require.js │ └── modlues/ │ ├── a.js │ ├── b.js │ └── c.js └── index.html
而這里幾個模塊的依賴關系是:
┌> a.js -> b.js index.html -| └> c.js // a.js require("./js/test/b.js"); // b.js console.log("i am b"); // c.js console.log("i am c");
我們要從 index.html 中利用 require.js 獲取這一連串的依賴關系,一般采用的方式就是正則匹配。如下:先拿到 function 的代碼,然后正則匹配出第一層的依賴關系,接著加載匹配到關系的代碼,繼續匹配。
// index.html
整個函數的入口是 start,正則表達式為:
var r = /require((.*))/g; var start = function(str){ while(match = r.exec(str)) { console.log(match[1]); } };
由此我們拿到了第一層的依賴關系,
["./js/modlues/a.js", "./js/modlues/c.js"]
接著要拿到 a.js 和 b.js 的文件層次依賴,之前我們寫了一個 require 函數,這個函數可以拿到腳本的代碼內容,不過這個 require 函數要稍微修改下,遞歸去查詢和下載代碼。
var cache = {}; var start = function(str){ while(match = r.exec(str)) { console.log(match && match[1]); // 如果匹配到了內容,下載 path 對應的源碼 match && match[1] && require(match[1]); } }; var require = function(path){ var xhr = new XMLHttpRequest(), res; xhr.open("GET", path, true); xhr.onreadystatechange = function(){ if(xhr.readyState == 4 && xhr.status == 200){ res = xhr.responseText; // 緩存文件 cache[path] = res; // 繼續遞歸匹配 start(res); } } xhr.send(); };
上面的代碼已經可以很好地拿到文件遞歸關系了。
3. 添加事件機制,優化管理代碼但是我們有必要先把 responseText 緩存起來,如果不緩存文件,直接 eval 得到的 responseText 代碼,想想會發生什么問題~ 如果模塊之間存在循環引用,如:
┌> a.js -> b.js index.html -| └> b.js -> a.js
那 start 和 require 將會陷入死循環,不斷的加載代碼。所以我們需要先拿到依賴關系,然后解構關系,分析出我們需要加載哪些模塊。值得注意的是,我們必須按照加載的順序去 eval 代碼,如果 a 依賴 b,先去執行 a 的話,一定會報錯!
有兩個問題我糾結了半天,上面的請求方式,何時會結束?用什么方式去記錄文件依賴關系?
最后還是決定將 start 和 require 兩個函數的相互遞歸修改成一個函數的遞歸。用一個對象,發起請求時把 URL 作為 key,在這個對象里保存 XHR 對象,XHR 對象請求完成后,把抓取到的新請求再用同樣的方式放入這個對象中,同時從這個對象中把自己刪除掉,然后判斷這個對象上是否存在 key, 如果存在說明還有 XHR 對象沒完成。
var r = /require(s*"(.*)"s*)/g; var cache = {}; // 文件緩存 var relation = []; // 依賴過程控制 var obj = {}; // xhr 管理對象 //輔助函數,獲取鍵值數組 Object.keys = Object.keys || function(obj){ var a = []; for(a[a.length] in obj); return a ; }; // 入口函數 function start(str){ while(match = r.exec(str)){ obj[match[1]] = new XMLHttpRequest(); require(obj[match[1]], match[1]); } } // 遞歸請求 var require = function(xhr, path){ //記錄依賴過程 relation.push(path); xhr.open("GET", path, true); xhr.onreadystatechange = function(){ if(xhr.readyState == 4 && xhr.status == 200){ var res = xhr.responseText; // 緩存文件 cache[path] = res; // 從xhr對象管理器中刪除已經加載完畢的函數 delete obj[path]; // 如果obj為空則觸發 allLoad 事件 Object.keys(obj).length == 0 ? Event.trigger("allLoad") : void 0; //遞歸條件 while(match = r.exec(res)){ obj[match[1]] = new XMLHttpRequest(); require(obj[match[1]], match[1]); } } } xhr.send(); };
上面的代碼已經基本完成了文件依賴分析,文件的加載和緩存工作了,我寫了一個,有興趣可以看一看。這個demo的文件結構為:
project/ ├── js/ │ ├── require.js │ └── test/ │ ├── a.js │ ├── b.js │ ├── c.js │ ├── d.js │ └── e.js └── index.html //文件依賴關系為 ┌> c.js ┌> a.js ->-| index.html -| └> d.js └> b.js -> e.js
戳我 → Demo
4. CMD 規范的介紹上面寫了一大堆內容,也實現了模塊加載器的原型,但是放在實際應用中,他就是個廢品,回到最開始,我們為什么要使用模塊化加載。目的是為了不去使用麻煩的命名空間,把復雜的模塊依賴交給 require 這個函數去管理,但實際上呢,上面拿到的所有模塊都是暴露在全局變量中的,也就是說,如果 a.js 和 b.js 中存在命名相同的變量,后者將會覆蓋前者,這是我們不愿意看到的。為了處理此類問題,我們有必要把所有的模塊都放到一個閉包中,這樣一來,只要不使用 window.vars 命名,閉包之間的變量是不會相互影響的。我們可以使用自己的方式去管理代碼,不過有人已經研究處理一套標準,而且是全球統一,那就拿著用吧~
關于 CMD 規范,我這里就不多說了,可以去看看草案,玉伯也翻譯了一份,。每一模塊有且僅有一個對外公開的接口 exports,如:
define(function(require, exports) { // 對外提供 foo 屬性 exports.foo = "bar"; // 對外提供 doSomething 方法 exports.doSomething = function() {}; });
剩下的工作就是針對 CMD 規范寫一套符合標準的代碼接口,這個比較瑣碎,就不寫了。
二、額外的話題上面的代碼中提到了關于 Event 的事件管理。在模塊全部加在完畢之后,需要有個東西告訴你,所以順手寫了一個 Event 的事件管理器。
// Event var Event = {}; Event.events = []; Event.on = function(evt, func){ for(var i = 0; i < Event.events.length; i++){ if(Event.events[i].evt == evt){ Event.events[i].func.push(func); return; } } Event.events.push({ evt: evt, func: [func] }); }; Event.trigger = function(evt){ for(var i = 0; i < Event.events.length; i++){ if(Event.events[i].evt == evt){ for(var j = 0; j < Event.events[i].func.length; j++){ Event.events[i].func[j](); } return; } } }; Event.off = function(evt){ for(var i = 0; i < Event.events.length; i++){ Event.events.splice(i, 1); } };
我覺得 seajs 是一個很不錯的模塊加載器,如果感興趣,可以去看看他的源碼實現,代碼不長,只有一千多行。模塊的加載它采用的是創建文本節點,讓文檔去加載模塊,實時查看狀態為 interactive 的 script 標簽,如果處于交互狀態就拿到他的代碼,接著刪除節點。當節點數目為 0 的時候,加載工作完成。
本文沒有考慮 css 文件的加載問題,我們可以把它當做一個沒有 require 關鍵詞的 js 文件,或者把它匹配出來之后另作處理,因為他是不可能存在模塊依賴關系的。
然后就是很多很多細節,本文的目的并不是寫一個類似 seajs 的模塊管理工具,只是稍微說幾句自己對這玩意兒的看法,如果說的有錯,請多多吐槽!
三、參考資料https://github.com/seajs/seajs/issues seajs issues
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/87461.html
摘要:聲明的變量不得改變值,這意味著,一旦聲明變量,就必須立即初始化,不能留到以后賦值。 雖然今年沒有換工作的打算 但為了跟上時代的腳步 還是忍不住整理了一份最新前端知識點 知識點匯總 1.HTML HTML5新特性,語義化瀏覽器的標準模式和怪異模式xhtml和html的區別使用data-的好處meta標簽canvasHTML廢棄的標簽IE6 bug,和一些定位寫法css js放置位置和原因...
摘要:聲明的變量不得改變值,這意味著,一旦聲明變量,就必須立即初始化,不能留到以后賦值。 雖然今年沒有換工作的打算 但為了跟上時代的腳步 還是忍不住整理了一份最新前端知識點 知識點匯總 1.HTML HTML5新特性,語義化瀏覽器的標準模式和怪異模式xhtml和html的區別使用data-的好處meta標簽canvasHTML廢棄的標簽IE6 bug,和一些定位寫法css js放置位置和原因...
摘要:聲明的變量不得改變值,這意味著,一旦聲明變量,就必須立即初始化,不能留到以后賦值。 雖然今年沒有換工作的打算 但為了跟上時代的腳步 還是忍不住整理了一份最新前端知識點 知識點匯總 1.HTML HTML5新特性,語義化瀏覽器的標準模式和怪異模式xhtml和html的區別使用data-的好處meta標簽canvasHTML廢棄的標簽IE6 bug,和一些定位寫法css js放置位置和原因...
摘要:前端單元測試,推薦淘寶開源的工具,簡單易用,支持眾多測試框架,也支持調試。這些也是設計前端框架時需要權衡的重要方面。最后,其實大型網站不一定要設計自己的前端框架,完全可以選用現有的框架。 有人在知乎上提問如何設計大型網站的前端 JavaScript 框架,有不少回答,其中得贊較多的兩個回答如下: 相對大型的項目在前端 JS 方面有幾個需要達成的目標: 1. 代碼邏輯分層 ...
摘要:淺談網站性能之前端性能優化性能優化的目的無非是減少用戶流量消耗,提升用戶首屏體驗,提升用戶訪問速度,讓用戶專注內容本身。前端性能優化減少請求數量基本原理在瀏覽器與服務器進行通信時,主要是通過進行通信。 最近項目慢慢走上正軌,需求趨于平穩,這才想起需要對整站進行性能優化。經過一段時間的學習,結合現在項目的實際性能情況,發現確實有許多地方可以進行優化。于是就開始了我的前端性能優化之旅。以下...
閱讀 978·2021-11-04 16:08
閱讀 2958·2021-09-13 10:37
閱讀 492·2019-08-30 15:56
閱讀 1928·2019-08-30 15:55
閱讀 2225·2019-08-30 15:53
閱讀 2071·2019-08-30 13:13
閱讀 2908·2019-08-30 12:51
閱讀 1532·2019-08-29 16:06