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