摘要:是一個極其簡單的打包軟件,使用開發而成,看完本文,你可以實現一個非常簡單的,但是又有實際用途的前端代碼打包工具。誠然,你并不需要了解太多編譯原理之類的事情,如果你在此之前對極為熟悉,那么你對前端打包工具一定能非常好的理解。
roid
roid 是一個極其簡單的打包軟件,使用 node.js 開發而成,看完本文,你可以實現一個非常簡單的,但是又有實際用途的前端代碼打包工具。
如果不想看教程,直接看代碼的(全部注釋):點擊地址
為什么要寫 roid ?我們每天都面對前端的這幾款編譯工具,但是在大量交談中我得知,并不是很多人知道這些打包軟件背后的工作原理,因此有了這個 project 出現。誠然,你并不需要了解太多編譯原理之類的事情,如果你在此之前對 node.js 極為熟悉,那么你對前端打包工具一定能非常好的理解。
弄清楚打包工具的背后原理,有利于我們實現各種神奇的自動化、工程化東西,比如表單的雙向綁定,自創 JavaScript 語法,又如螞蟻金服 ant 中大名鼎鼎的 import 插件,甚至是前端文件自動掃描載入等,能夠極大的提升我們工作效率。
不廢話,我們直接開始。
從一個自增 id 開始const { readFileSync, writeFileSync } = require("fs") const path = require("path") const traverse = require("babel-traverse").default const { transformFromAst, transform } = require("babel-core") let ID = 0 // 當前用戶的操作的目錄 const currentPath = process.cwd()
id:全局的自增 id ,記錄每一個載入的模塊的 id ,我們將所有的模塊都用唯一標識符進行標示,因此自增 id 是最有效也是最直觀的,有多少個模塊,一統計就出來了。
解析單個文件模塊function parseDependecies(filename) { const rawCode = readFileSync(filename, "utf-8") const ast = transform(rawCode).ast const dependencies = [] traverse(ast, { ImportDeclaration(path) { const sourcePath = path.node.source.value dependencies.push(sourcePath) } }) // 當我們完成依賴的收集以后,我們就可以把我們的代碼從 AST 轉換成 CommenJS 的代碼 // 這樣子兼容性更高,更好 const es5Code = transformFromAst(ast, null, { presets: ["env"] }).code // 還記得我們的 webpack-loader 系統嗎? // 具體實現就是在這里可以實現 // 通過將文件名和代碼都傳入 loader 中,進行判斷,甚至用戶定義行為再進行轉換 // 就可以實現 loader 的機制,當然,我們在這里,就做一個弱智版的 loader 就可以了 // parcel 在這里的優化技巧是很有意思的,在 webpack 中,我們每一個 loader 之間傳遞的是轉換好的代碼 // 而不是 AST,那么我們必須要在每一個 loader 進行 code -> AST 的轉換,這樣時非常耗時的 // parcel 的做法其實就是將 AST 直接傳遞,而不是轉換好的代碼,這樣,速度就快起來了 const customCode = loader(filename, es5Code) // 最后模塊導出 return { id: ID++, code: customCode, dependencies, filename } }
首先,我們對每一個文件進行處理。因為這只是一個簡單版本的 bundler ,因此,我們并不考慮如何去解析 css 、md 、txt 等等之類的格式,我們專心處理好 js 文件的打包,因為對于其他文件而言,處理起來過程不太一樣,用文件后綴很容易將他們區分進行不同的處理,在這個版本,我們還是專注 js。
const rawCode = readFileSync(filename, "utf-8") 函數注入一個 filename 顧名思義,就是文件名,讀取其的文件文本內容,然后對其進行 AST 的解析。我們使用 babel 的 transform 方法去轉換我們的原始代碼,通過轉換以后,我們的代碼變成了抽象語法樹( AST ),你可以通過 https://astexplorer.net/, 這個可視化的網站,看看 AST 生成的是什么。
當我們解析完以后,我們就可以提取當前文件中的 dependencies,dependencies 翻譯為依賴,也就是我們文件中所有的 import xxxx from xxxx,我們將這些依賴都放在 dependencies 的數組里面,之后統一進行導出。
然后通過 traverse 遍歷我們的代碼。traverse 函數是一個遍歷 AST 的方法,由 babel-traverse 提供,他的遍歷模式是經典的 visitor 模式
,visitor 模式就是定義一系列的 visitor ,當碰到 AST 的 type === visitor 名字時,就會進入這個 visitor 的函數。類型為 ImportDeclaration 的 AST 節點,其實就是我們的 import xxx from xxxx,最后將地址 push 到 dependencies 中.
最后導出的時候,不要忘記了,每導出一個文件模塊,我們都往全局自增 id 中 + 1,以保證每一個文件模塊的唯一性。
解析所有文件,生成依賴圖function parseGraph(entry) { // 從 entry 出發,首先收集 entry 文件的依賴 const entryAsset = parseDependecies(path.resolve(currentPath, entry)) // graph 其實是一個數組,我們將最開始的入口模塊放在最開頭 const graph = [entryAsset] for (const asset of graph) { if (!asset.idMapping) asset.idMapping = {} // 獲取 asset 中文件對應的文件夾 const dir = path.dirname(asset.filename) // 每個文件都會被 parse 出一個 dependencise,他是一個數組,在之前的函數中已經講到 // 因此,我們要遍歷這個數組,將有用的信息全部取出來 // 值得關注的是 asset.idMapping[dependencyPath] = denpendencyAsset.id 操作 // 我們往下看 asset.dependencies.forEach(dependencyPath => { // 獲取文件中模塊的絕對路徑,比如 import ABC from "./world" // 會轉換成 /User/xxxx/desktop/xproject/world 這樣的形式 const absolutePath = path.resolve(dir, dependencyPath) // 解析這些依賴 const denpendencyAsset = parseDependecies(absolutePath) // 獲取唯一 id const id = denpendencyAsset.id // 這里是重要的點了,我們解析每解析一個模塊,我們就將他記錄在這個文件模塊 asset 下的 idMapping 中 // 之后我們 require 的時候,能夠通過這個 id 值,找到這個模塊對應的代碼,并進行運行 asset.idMapping[dependencyPath] = denpendencyAsset.id // 將解析的模塊推入 graph 中去 graph.push(denpendencyAsset) }) } // 返回這個 graph return graph }
接下來,我們對模塊進行更高級的處理。我們之前已經寫了一個 parseDependecies 函數,那么現在我們要來寫一個 parseGraph 函數,我們將所有文件模塊組成的集合叫做 graph(依賴圖),用于描述我們這個項目的所有的依賴關系,parseGraph 從 entry (入口) 出發,一直手機完所有的以來文件為止.
在這里我們使用 for of 循環而不是 forEach ,原因是因為我們在循環之中會不斷的向 graph 中,push 進東西,graph 會不斷增加,用 for of 會一直持續這個循環直到 graph 不會再被推進去東西,這就意味著,所有的依賴已經解析完畢,graph 數組數量不會繼續增加,但是用 forEach 是不行的,只會遍歷一次。
在 for of 循環中,asset 代表解析好的模塊,里面有 filename , code , dependencies 等東西 asset.idMapping 是一個不太好理解的概念,我們每一個文件都會進行 import 操作,import 操作在之后會被轉換成 require 每一個文件中的 require 的 path 其實會對應一個數字自增 id,這個自增 id 其實就是我們一開始的時候設置的 id,我們通過將 path-id 利用鍵值對,對應起來,之后我們在文件中 require 就能夠輕松的找到文件的代碼,解釋這么啰嗦的原因是往往模塊之間的引用是錯中復雜的,這恰巧是這個概念難以解釋的原因。
最后,生成 bundlefunction build(graph) { // 我們的 modules 就是一個字符串 let modules = "" graph.forEach(asset => { modules += `${asset.id}:[ function(require,module,exports){${asset.code}}, ${JSON.stringify(asset.idMapping)}, ],` }) const wrap = ` (function(modules) { function require(id) { const [fn, idMapping] = modules[id]; function childRequire(filename) { return require(idMapping[filename]); } const newModule = {exports: {}}; fn(childRequire, newModule, newModule.exports); return newModule.exports } require(0); })({${modules}});` // 注意這里需要給 modules 加上一個 {} return wrap } // 這是一個 loader 的最簡單實現 function loader(filename, code) { if (/index/.test(filename)) { console.log("this is loader ") } return code } // 最后我們導出我們的 bundler module.exports = entry => { const graph = parseGraph(entry) const bundle = build(graph) return bundle }
我們完成了 graph 的收集,那么就到我們真正的代碼打包了,這個函數使用了大量的字符串處理,你們不要覺得奇怪,為什么代碼和字符串可以混起來寫,如果你跳出寫代碼的范疇,看我們的代碼,實際上,代碼就是字符串,只不過他通過特殊的語言形式組織起來而已,對于腳本語言 JS 來說,字符串拼接成代碼,然后跑起來,這種操作在前端非常的常見,我認為,這種思維的轉換,是擁有自動化、工程化的第一步。
我們將 graph 中所有的 asset 取出來,然后使用 node.js 制造模塊的方法來將一份代碼包起來,我之前做過一個《庖丁解牛:教你如何實現》node.js 模塊的文章,不懂的可以去看看,https://zhuanlan.zhihu.com/p/...
在這里簡單講述,我們將轉換好的源碼,放進一個 function(require,module,exports){} 函數中,這個函數的參數就是我們隨處可用的 require,module,以及 exports,這就是為什么我們可以隨處使用這三個玩意的原因,因為我們每一個文件的代碼終將被這樣一個函數包裹起來,不過這段代碼中比較奇怪的是,我們將代碼封裝成了 1:[...],2:[...]的形式,我們在最后導入模塊的時候,會為這個字符串加上一個 {},變成 {1:[...],2:[...]},你沒看錯,這是一個對象,這個對象里用數字作為 key,一個二維元組作為值:
[0] 第一個就是我們被包裹的代碼
[1] 第二個就是我們的 mapping
馬上要見到曙光了,這一段代碼實際上才是模塊引入的核心邏輯,我們制造一個頂層的 require 函數,這個函數接收一個 id 作為值,并且返回一個全新的 module 對象,我們倒入我們剛剛制作好的模塊,給他加上 {},使其成為 {1:[...],2:[...]} 這樣一個完整的形式。
然后塞入我們的立即執行函數中(function(modules) {...})(),在 (function(modules) {...})() 中,我們先調用 require(0),理由很簡單,因為我們的主模塊永遠是排在第一位的,緊接著,在我們的 require 函數中,我們拿到外部傳進來的 modules,利用我們一直在說的全局數字 id 獲取我們的模塊,每個模塊獲取出來的就是一個二維元組。
然后,我們要制造一個 子require,這么做的原因是我們在文件中使用 require 時,我們一般 require 的是地址,而頂層的 require 函數參數時 id
不要擔心,我們之前的 idMapping 在這里就用上了,通過用戶 require 進來的地址,在 idMapping 中找到 id。
然后遞歸調用 require(id),就能夠實現模塊的自動倒入了,接下來制造一個 const newModule = {exports: {}};,運行我們的函數 fn(childRequire, newModule, newModule.exports);,將應該丟進去的丟進去,最后 return newModule.exports 這個模塊的 exports 對象。
這里的邏輯其實跟 node.js 差別不太大。
最后寫一點測試測試的代碼,我已經放在了倉庫里,想測試一下的同學可以去倉庫中自行提取。
打滿注釋的代碼也放在倉庫了,點擊地址
git clone https://github.com/Foveluy/roid.git npm i node ./src/_test.js ./example/index.js
輸出
this is loader hello zheng Fang! welcome to roid, I"m zheng Fang if you love roid and learnt any thing, please give me a star https://github.com/Foveluy/roid參考
https://github.com/blackLearn...
https://github.com/ronami/min...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/107938.html
摘要:而且默認帶有執行的順序是,,即便是內聯的,依然具有屬性。模塊腳本只會執行一次必須符合同源策略模塊腳本在跨域的時候默認是不帶的。通常被用作腳本被禁用的回退方案。最后標簽真的令人感到興奮。 窺探 Script 標簽 0x01 什么是 script 標簽? script 標簽允許你包含一些動態腳本或數據塊到文檔中,script 標簽是非閉合的,你也可以將動態腳本或數據塊當做 script 的...
摘要:而且默認帶有執行的順序是,,即便是內聯的,依然具有屬性。模塊腳本只會執行一次必須符合同源策略模塊腳本在跨域的時候默認是不帶的。通常被用作腳本被禁用的回退方案。最后標簽真的令人感到興奮。 窺探 Script 標簽 0x01 什么是 script 標簽? script 標簽允許你包含一些動態腳本或數據塊到文檔中,script 標簽是非閉合的,你也可以將動態腳本或數據塊當做 script 的...
摘要:主要兼容的微信的瀏覽器,因為要在朋友圈來營銷,總體來說,會偏設計以及動畫些。 有一天,我們組內的一個小伙伴突然問我,你知道有一個叫重構工程師的崗位?這是干什么的?重構工程師 這個問題引發了我對前端領域發展的思考,所以我來梳理下前端領域的發展過程,順便小小的預測下2017年的趨勢。不想看回憶的,可以直接跳到后面看展望。 神說,要有光,就有了光 自1991年蒂姆·伯納斯-李公開提及HTML...
摘要:主要兼容的微信的瀏覽器,因為要在朋友圈來營銷,總體來說,會偏設計以及動畫些。 有一天,我們組內的一個小伙伴突然問我,你知道有一個叫重構工程師的崗位?這是干什么的?重構工程師 這個問題引發了我對前端領域發展的思考,所以我來梳理下前端領域的發展過程,順便小小的預測下2017年的趨勢。不想看回憶的,可以直接跳到后面看展望。 神說,要有光,就有了光 自1991年蒂姆·伯納斯-李公開提及HTML...
閱讀 3704·2021-11-22 13:52
閱讀 3602·2019-12-27 12:20
閱讀 2384·2019-08-30 15:55
閱讀 2143·2019-08-30 15:44
閱讀 2261·2019-08-30 13:16
閱讀 573·2019-08-28 18:19
閱讀 1881·2019-08-26 11:58
閱讀 3435·2019-08-26 11:47