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