摘要:而在編譯過程中通過語法和詞法的分析得出一顆語法樹,我們可以將它稱為抽象語法樹也稱為語法樹,指的是源代碼語法所對(duì)應(yīng)的樹狀結(jié)構(gòu)。而這個(gè)卻恰恰使我們分析打包工具的重點(diǎn)核心。
概述
眼下wepack似乎已經(jīng)成了前端開發(fā)中不可缺少的工具之一,而他的一切皆模塊的思想隨著webpack版本不斷的迭代(webpack 4)使其打包速度更快,效率更高的為我們的前端工程化服務(wù)
相信大家使用webpack已經(jīng)很熟練了,他通過一個(gè)配置對(duì)象,其中包括對(duì)入口,出口,插件的配置等,然后內(nèi)部根據(jù)這個(gè)配置對(duì)象去對(duì)整個(gè)項(xiàng)目工程進(jìn)行打包,從一個(gè)js文件切入(此為單入口,當(dāng)然也可以設(shè)置多入口文件打包),將該文件中所有的依賴的文件通過特定的loader和插件都會(huì)按照我們的需求為我們打包出來,這樣在面對(duì)當(dāng)前的ES6、scss、less、postcss就可以暢快的盡管使用,打包工具會(huì)幫助我們讓他們正確的運(yùn)行在瀏覽器上。可謂是省時(shí)省力還省心啊。
那當(dāng)下的打包工具的核心原理是什么呢?今天就來通過模擬實(shí)現(xiàn)一個(gè)小小的打包工具來為探究一下他的核心原理嘍。文中有些知識(shí)是點(diǎn)到,沒有深挖,如果有興趣的可以自行查閱資料。
功力尚淺,只是入門級(jí)的了解打包工具的核心原理,簡(jiǎn)單的功能項(xiàng)目地址
Pack:點(diǎn)擊github
原理當(dāng)我們更加深入的去了解javascript這門語言時(shí),去知道javascript更底層的一些實(shí)現(xiàn),對(duì)我們理解好的開源項(xiàng)目是由很多幫助的,當(dāng)然對(duì)我們自身技術(shù)提高會(huì)有更大的幫助。
javascript是一門弱類型的解釋型語言,也就是說在我們執(zhí)行前不需要編譯器來編譯出一個(gè)版本供我們執(zhí)行,對(duì)于javascript來說也有編譯的過程,只不過大部分情況下編譯發(fā)生在代碼執(zhí)行前的幾微秒,編譯完成后會(huì)盡快的執(zhí)行。也就是根據(jù)代碼的執(zhí)行去動(dòng)態(tài)的編譯。而在編譯過程中通過語法和詞法的分析得出一顆語法樹,我們可以將它稱為AST【抽象語法樹(Abstract Syntax Tree)也稱為AST語法樹,指的是源代碼語法所對(duì)應(yīng)的樹狀結(jié)構(gòu)。也就是說,一種編程語言的源代碼,通過構(gòu)建語法樹的形式將源代碼中的語句映射到樹中的每一個(gè)節(jié)點(diǎn)上。】。而這個(gè)AST卻恰恰使我們分析打包工具的重點(diǎn)核心。
我們都熟悉babel,他讓前端程序員很爽的地方在于他可以讓我們暢快的去書寫ES6、ES7、ES8.....等等,而他會(huì)幫我們統(tǒng)統(tǒng)都轉(zhuǎn)成瀏覽器能夠執(zhí)行的ES5版本,它的核心就是通過一個(gè)babylon的js詞法解析引擎來分析我們寫的ES6以上的版本語法來得到AST(抽象語法樹),再通過對(duì)這個(gè)語法樹的深度遍歷來對(duì)這棵樹的結(jié)構(gòu)和數(shù)據(jù)進(jìn)行修改。最終轉(zhuǎn)通過整理和修改后的AST生成ES5的語法。這也就是我們使用babel的主要核心。一下是語法樹的示例
需要轉(zhuǎn)換的文件(index.js)
// es6 index.js import add from "./add.js" let sum = add(1, 2); export default sum // ndoe build.js const fs = require("fs") const babylon = require("babylon") // 讀取文件內(nèi)容 const content = fs.readFileSync(filePath, "utf-8") // 生成 AST 通過babylon const ast = babylon.parse(content, { sourceType: "module" }) console.log(ast)
執(zhí)行文件(在node環(huán)境下build.js)
// node build.js // 引入fs 和 babylon引擎 const fs = require("fs") const babylon = require("babylon") // 讀取文件內(nèi)容 const content = fs.readFileSync(filePath, "utf-8") // 生成 AST 通過babylon const ast = babylon.parse(content, { sourceType: "module" }) console.log(ast)
生成的AST
ast = { ... ... comments:[], tokens:[Token { type: [KeywordTokenType], value: "import", start: 0, end: 6, loc: [SourceLocation] }, Token { type: [TokenType], value: "add", start: 7, end: 10, loc: [SourceLocation] }, Token { type: [TokenType], value: "from", start: 11, end: 15, loc: [SourceLocation] }, Token { type: [TokenType], value: "./add.js", start: 16, end: 26, loc: [SourceLocation] }, Token { type: [KeywordTokenType], value: "let", start: 27, end: 30, loc: [SourceLocation] }, Token { type: [TokenType], value: "sum", start: 31, end: 34, loc: [SourceLocation] }, ... ... Token { type: [KeywordTokenType], value: "export", start: 48, end: 54, loc: [SourceLocation] }, Token { type: [KeywordTokenType], value: "default", start: 55, end: 62, loc: [SourceLocation] }, Token { type: [TokenType], value: "sum", start: 63, end: 66, loc: [SourceLocation] }, ] }
上面的示例就是分析出來的AST語法樹。babylon在分析源代碼的時(shí)候,會(huì)逐個(gè)字母的像掃描機(jī)一樣讀取,然后分析得出語法樹。(關(guān)于語法樹和babylon可以參考 https://www.jianshu.com/p/019...。通過遍歷對(duì)他的屬性或者值進(jìn)行修改根據(jù)相應(yīng)的算法規(guī)則重新組成代碼。當(dāng)分析我們正常的js文件時(shí),往往得到的AST會(huì)很大甚至幾萬、幾十萬行,所以需要很優(yōu)秀的算法才能保證速度和效率。下面本項(xiàng)目中用到的是babel-traverse來解析AST。對(duì)算法的感興趣的可以去了解一下。以上部分講述的知識(shí)點(diǎn)并沒有深入,原因如題目,只是要探索出打包工具的原理,具體知識(shí)點(diǎn)感興趣的自己去了解下吧。原理部分大概介紹到這里吧,下面開始施實(shí)戰(zhàn)。
項(xiàng)目目錄├── README.md ├── package.json ├── src │?? ├── lib │?? │?? ├── bundle.js // 生成打包后的文件 │?? │?? ├── getdep.js // 從AST中獲得文件依賴關(guān)系 │?? │?? └── readcode.js //讀取文件代碼,生成AST,處理AST,并且轉(zhuǎn)換ES6代碼 │?? └── pack.js // 向外暴露工具入口方法 └── yarn.lock
思維導(dǎo)圖
通過思維導(dǎo)圖可以更清楚羅列出來思路
具體實(shí)現(xiàn)流程梳理中發(fā)現(xiàn),重點(diǎn)是找到每個(gè)文件中的依賴關(guān)系,我們用deps來收集依賴。從而通過依賴關(guān)系來模塊化的把依賴關(guān)系中一層一層的打包。下面一步步的來實(shí)現(xiàn)
主要通過 代碼 + 解釋 的梳理過程讀取文件代碼
首先,我們需要一個(gè)入口文件的路徑,通過node的fs模塊來讀取指定文件中的代碼,然后通過以上提到的babylon來分析代碼得到AST語法樹,然后通過babel-traverse庫來從AST中獲得代碼中含有import的模塊(路徑)信息,也就是依賴關(guān)系。我們把當(dāng)前模塊的所有依賴文件的相對(duì)路徑都push到一個(gè)deps的數(shù)組中。以便后面去遍歷查找依賴。
const fs = require("fs") // 分析引擎 const babylon = require("babylon") // traverse 對(duì)語法樹遍歷等操作 const traverse = require("babel-traverse").default // babel提供的語法轉(zhuǎn)換 const { transformFromAst } = require("babel-core") // 讀取文件代碼函數(shù) const readCode = function (filePath) { if(!filePath) { throw new Error("No entry file path") return } // 當(dāng)前模塊的依賴收集 const deps = [] const content = fs.readFileSync(filePath, "utf-8") const ast = babylon.parse(content, { sourceType: "module" }) // 分析AST,從中得到import的模塊信息(路徑) // 其中ImportDeclaration方法為當(dāng)遍歷到import時(shí)的一個(gè)回調(diào) traverse(ast, { ImportDeclaration: ({ node }) => { // 將依賴push到deps中 // 如果有多個(gè)依賴,所以用數(shù)組 deps.push(node.source.value) } }) // es6 轉(zhuǎn)化為 es5 const {code} = transformFromAst(ast, null, {presets: ["env"]}) // 返回一個(gè)對(duì)象 // 有路徑,依賴,轉(zhuǎn)化后的es5代碼 // 以及一個(gè)模塊的id(自定義) return { filePath, deps, code, id: deps.length > 0 ? deps.length - 1 : 0 } } module.exports = readCode
相信上述代碼是可以理解的,代碼中的注釋寫的很詳細(xì),這里就不在多啰嗦了。需要注意的是,babel-traverse這個(gè)庫關(guān)于api以及詳細(xì)的介紹很少,可以通過其他途徑去了解這個(gè)庫的用法。
另外需要在強(qiáng)調(diào)一下的是最后函數(shù)的返回值,是一個(gè)對(duì)象,該對(duì)象中包含的是當(dāng)前這個(gè)文件(模塊)中的一些重要信息,deps中存放的就是當(dāng)前模塊分析得到的所有依賴文件路徑。最后我們需要去遞歸遍歷每個(gè)模塊的所有依賴,以及代碼。后面的依賴收集的時(shí)候會(huì)用到。
通過上面的讀取文件方法我們得到返回了一個(gè)關(guān)于單個(gè)文件(模塊)的一些重要信息。filePath(文件路徑),deps(該模塊的所有依賴),code(轉(zhuǎn)化后的代碼),id(該對(duì)象模塊的id)
我們通過定義deps為一個(gè)數(shù)組,來存放所有依賴關(guān)系中每一個(gè)文件(模塊)的以上重要信息對(duì)象
接下來我們通過這個(gè)單文件入口的依賴關(guān)系去搜集該模塊的依賴模塊的依賴,以及該模塊的依賴模塊的依賴模塊的依賴......我們通過遞歸和循環(huán)的方式去執(zhí)行readCode方法,每執(zhí)行一次將readCode返回的對(duì)象push到deps數(shù)組中,最終得到了所有的在依賴關(guān)系鏈中的每一個(gè)模塊的重要信息以及依賴。
const readCode = require("./readcode.js") const fs = require("fs") const path = require("path") const getDeps = function (entry) { // 通過讀取文件分析返回的主入口文件模塊的重要信息 對(duì)象 const entryFileObject = readCode(entry) // deps 為每一個(gè)依賴關(guān)系或者每一個(gè)模塊的重要信息對(duì)象 合成的數(shù)組 // deps 就是我們提到的最終的核心數(shù)據(jù),通過他來構(gòu)建整個(gè)打包文件 const deps = [entryFileObject ? entryFileObject : null] // 對(duì)deps進(jìn)行遍歷 // 拿到filePath信息,判斷是css文件還是js文件 for (let obj of deps) { const dirname = path.dirname(obj.filePath) obj.deps.forEach(rPath => { const aPath = path.join(dirname, rPath) if (/.css/.test(aPath)) { // 如果是css文件,則不進(jìn)行遞歸readCode分析代碼, // 直接將代碼改寫成通過js操作寫入到style標(biāo)簽中 const content = fs.readFileSync(aPath, "utf-8") const code = ` var style = document.createElement("style") style.innerText = ${JSON.stringify(content).replace(/ /g, "")} document.head.appendChild(style) ` deps.push({ filePath: aPath, reletivePaht: rPath, deps, code, id: deps.length > 0 ? deps.length : 0 }) } else { // 如果是js文件 則繼續(xù)調(diào)用readCode分析該代碼 let obj = readCode(aPath) obj.reletivePaht = rPath obj.id = deps.length > 0 ? deps.length : 0 deps.push(obj) } }) } // 返回deps return deps } module.exports = getDeps
可能在上述代碼中有疑問也許是在對(duì)deps遍歷收集全部依賴的時(shí)候,又循環(huán)又重復(fù)調(diào)用的可能有一點(diǎn)繞,還有一點(diǎn)可能就是對(duì)于deps這個(gè)數(shù)組最后究竟要干什么用,沒關(guān)系,繼續(xù)往下看,后面就會(huì)懂了。
輸出文件到現(xiàn)在,我們已經(jīng)可以拿到了所有文件以及對(duì)應(yīng)的依賴以及文件中的轉(zhuǎn)換后的代碼以及id,是的,就是我們上一節(jié)中返回的deps(就靠它了),可能在上一節(jié)還會(huì)有人產(chǎn)生疑問,接下來,我們就直接上代碼,慢慢道來慢慢解開你的疑惑。
const fs = require("fs") // 壓縮代碼的庫 const uglify = require("uglify-js") // 四個(gè)參數(shù) // 1. 所有依賴的數(shù)組 上一節(jié)中返回值 // 2. 主入口文件路徑 // 3. 出口文件路徑 // 4. 是否壓縮輸出文件的代碼 // 以上三個(gè)參數(shù),除了第一個(gè)deps之外,其他三個(gè)都需要在該項(xiàng)目主入口方法中傳入?yún)?shù),配置對(duì)象 const bundle = function (deps, entry, outPath, isCompress) { let modules = "" let moduleId deps.forEach(dep => { var id = dep.id // 重點(diǎn)來了 // 此處,通過deps的模塊「id」作為屬性,而其屬性值為一個(gè)函數(shù) // 函數(shù)體為 當(dāng)前遍歷到的模塊的「code」,也就是轉(zhuǎn)換后的代碼 // 產(chǎn)生一個(gè)長(zhǎng)字符 // 0:function(......){......}, // 1: function(......){......} // ... modules = modules + `${id}: function (module, exports, require) {${dep.code}},` }); // 自執(zhí)行函數(shù),傳入的剛才拼接的對(duì)象,以及deps // 其中require使我們自定義的,模擬commonjs中的模塊化 let result = ` (function (modules, mType) { function require (id) { var module = { exports: {}} var module_id = require_moduleId(mType, id) modules[module_id](module, module.exports, require) return module.exports } require("${entry}") })({${modules}},${JSON.stringify(deps)}); function require_moduleId (typelist, id) { var module_id typelist.forEach(function (item) { if(id === item.filePath || id === item.reletivePaht){ module_id = item.id } }) return module_id } ` // 判斷是否壓縮 if(isCompress) { result = uglify.minify(result,{ mangle: { toplevel: true } }).code } // 寫入文件 輸出 fs.writeFileSync(outPath + "/bundle.js", result) console.log("打包完成【success】(./bundle.js)") } module.exports = bundle
這里還是要在詳細(xì)的敘述一下。因?yàn)槲覀円敵鑫募?,顧出現(xiàn)了大量的字符串。
解釋1:modules字符串
modules字符串最后通過遍歷deps得到的字符串為
modules = ` 0:function (module, module.exports, require){相應(yīng)模塊的代碼}, 1: function (module, module.exports, require){相應(yīng)模塊的代碼}, 2: function (module, module.exports, require){相應(yīng)模塊的代碼}, 3: function (module, module.exports, require){相應(yīng)模塊的代碼}, ... ... `
如果我們?cè)谧址膬啥朔謩e加上”{“和”}“,如果當(dāng)成代碼執(zhí)行的話那不就是一個(gè)對(duì)象了嗎?對(duì)啊,這樣0,1,2,3...就變成了屬性,而屬性的值就是一個(gè)函數(shù),這樣就可以通過屬性直接調(diào)用函數(shù)了。而這個(gè)函數(shù)的內(nèi)容就是我們需要打包的每個(gè)模塊的代碼經(jīng)過babel轉(zhuǎn)換之后的代碼啊。
解釋2:result字符串
// 自執(zhí)行函數(shù) 將上面的modules字符串加上{}后傳入(對(duì)象) (function (modules, mType) { // 自定義require函數(shù),模擬commonjs中的模塊化 function require (id) { // 定義module對(duì)象,以及他的exports屬性 var module = { exports: {}} // 轉(zhuǎn)化路徑和id,已調(diào)用相關(guān)函數(shù) var module_id = require_moduleId(mType, id) // 調(diào)用傳進(jìn)來modules對(duì)象的屬性的函數(shù) modules[module_id](module, module.exports, require) return module.exports } require("${entry}") })({${modules}},${JSON.stringify(deps)}); // 路徑和id對(duì)應(yīng)轉(zhuǎn)換,目的是為了調(diào)用相應(yīng)路徑下對(duì)應(yīng)的id屬性的函數(shù) function require_moduleId (typelist, id) { var module_id typelist.forEach(function (item) { if(id === item.filePath || id === item.reletivePaht){ module_id = item.id } }) return module_id }
至于為什么我們要通過require_modulesId函數(shù)來轉(zhuǎn)換路徑和id的關(guān)系呢,這要先從babel吧ES6轉(zhuǎn)成ES5說起,下面列出一個(gè)ES6轉(zhuǎn)ES5的例子
ES6代碼:
import a from "./a.js" let b = a + a export default b
ES5代碼:
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _a = require("./a.js"); var _a2 = _interopRequireDefault(_a); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var b = _a2.default + _a2.default; exports.default = b;
1.以上代碼為轉(zhuǎn)化前和轉(zhuǎn)換后,有興趣的可以去babel官網(wǎng)試試,可以發(fā)現(xiàn)轉(zhuǎn)換后的這一行代碼var _a = require("./a.js");,他為我們轉(zhuǎn)換出來的require的參數(shù)是文件的路徑,而我們需要調(diào)用的相對(duì)應(yīng)的模塊的函數(shù)其屬性值都是以id(0,1,2,3...)命名的,所以需要轉(zhuǎn)換
2.還有一點(diǎn)可能有疑問的就是為什么會(huì)用function (module, module.exports, require){...}這樣的commonjs模塊化的形式呢,原因是babel為我們轉(zhuǎn)后后的代碼模塊化采用的就是commonjs的規(guī)范。
最后一步就是我們?nèi)シ庋b一下,向外暴露一個(gè)入口函數(shù)就可以了。這一步效仿一下webpack的api,一個(gè)pack方法傳入一個(gè)config配置對(duì)象。這樣就可以在package.json中寫scripts腳本來npm/yarn來執(zhí)行了。
const getDeps = require("./lib/getdep") const bundle = require("./lib/bundle") const pack = function (config) { if(!config.entryPath || !config.outPath) { throw new Error("pack工具:請(qǐng)配置入口和出口路徑") return } let entryPath = config.entryPath let outPath = config.outPath let isCompress = config.isCompression || false let deps = getDeps(entryPath) bundle(deps, entryPath, outPath, isCompress) } module.exports = pack
傳入的config只有是三個(gè)屬性,entryPath,outPath,isCompression。
總結(jié)一個(gè)簡(jiǎn)單的實(shí)現(xiàn),只為了探究一下原理,并沒有完善的功能和穩(wěn)定性。希望對(duì)看到的人能有幫助
打包工具,首先通過我們代碼文件進(jìn)行詞法和語法的分析,生成AST,再通過處理AST,最終變換成我們想要的以及瀏覽器能兼容的代碼,收集每一個(gè)文件的依賴,最終形成一個(gè)依賴鏈,然后通過這個(gè)依賴關(guān)系最后輸出打包后的文件。
初來乍到,穩(wěn)重有解釋不當(dāng)或錯(cuò)的地方,還請(qǐng)多理解,有問題可以在評(píng)論區(qū)交流。還有別忘了你的
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/103142.html
摘要:首先一段代碼轉(zhuǎn)化成的抽象語法樹是一個(gè)對(duì)象,該對(duì)象會(huì)有一個(gè)頂級(jí)的屬性第二個(gè)屬性是是一個(gè)數(shù)組。最終完成整個(gè)文件依賴的處理。參考文章抽象語法樹一看就懂的抽象語法樹源碼所有的源碼已經(jīng)上傳 背景 隨著前端復(fù)雜度的不斷提升,誕生出很多打包工具,比如最先的grunt,gulp。到后來的webpack和 Parcel。但是目前很多腳手架工具,比如vue-cli已經(jīng)幫我們集成了一些構(gòu)建工具的使用。有的時(shí)...
摘要:前端模塊化成為了主流的今天,離不開各種打包工具的貢獻(xiàn)。與此同時(shí),打包工具也會(huì)處理好模塊之間的依賴關(guān)系,最終這個(gè)大模塊將可以被運(yùn)行在合適的平臺(tái)中。至此,整一個(gè)打包工具已經(jīng)完成。明白了當(dāng)中每一步的目的,便能夠明白一個(gè)打包工具的運(yùn)行原理。 showImg(https://segmentfault.com/img/bVbckjY?w=900&h=565); 前端模塊化成為了主流的今天,離不開各...
摘要:五六月份推薦集合查看最新的請(qǐng)點(diǎn)擊集前端最近很火的框架資源定時(shí)更新,歡迎一下。蘇幕遮燎沈香宋周邦彥燎沈香,消溽暑。鳥雀呼晴,侵曉窺檐語。葉上初陽乾宿雨,水面清圓,一一風(fēng)荷舉。家住吳門,久作長(zhǎng)安旅。五月漁郎相憶否。小楫輕舟,夢(mèng)入芙蓉浦。 五、六月份推薦集合 查看github最新的Vue weekly;請(qǐng)::點(diǎn)擊::集web前端最近很火的vue2框架資源;定時(shí)更新,歡迎 Star 一下。 蘇...
摘要:五六月份推薦集合查看最新的請(qǐng)點(diǎn)擊集前端最近很火的框架資源定時(shí)更新,歡迎一下。蘇幕遮燎沈香宋周邦彥燎沈香,消溽暑。鳥雀呼晴,侵曉窺檐語。葉上初陽乾宿雨,水面清圓,一一風(fēng)荷舉。家住吳門,久作長(zhǎng)安旅。五月漁郎相憶否。小楫輕舟,夢(mèng)入芙蓉浦。 五、六月份推薦集合 查看github最新的Vue weekly;請(qǐng)::點(diǎn)擊::集web前端最近很火的vue2框架資源;定時(shí)更新,歡迎 Star 一下。 蘇...
閱讀 3801·2023-04-26 02:00
閱讀 3167·2021-11-22 13:54
閱讀 1778·2021-08-03 14:03
閱讀 769·2019-08-30 15:52
閱讀 3186·2019-08-29 12:30
閱讀 2477·2019-08-26 13:35
閱讀 3437·2019-08-26 13:25
閱讀 3056·2019-08-26 11:39