手写一个简易bundler打包工具带你了解Webpack原理
用原生js手寫(xiě)一個(gè)簡(jiǎn)易的打包工具bundler
- 🥝序言
- 🍉一、模塊分析(入口文件代碼分析)
- 1. 項(xiàng)目結(jié)構(gòu)
- 2. 安裝第三方依賴(lài)
- 3. 業(yè)務(wù)代碼
- 4. 開(kāi)始打包
- 🥑二、依賴(lài)圖譜Dependencies Graph
- 1. 結(jié)果分析
- 2. 分析所有模塊的依賴(lài)關(guān)系
- 🍐三、生成代碼
- 1. 邏輯編寫(xiě)
- 2. 結(jié)果分析
- 🍓四、結(jié)束語(yǔ)
- 🐣彩蛋 One More Thing
- (:往期推薦
- (:番外篇
🥝序言
我們都知道, webpack 是一個(gè)打包工具。在我們對(duì)它進(jìn)行配置之前,它也是經(jīng)過(guò)一系列的代碼編寫(xiě),才生成的打包工具。那這背后,又做了什么事情呢?
今天,我們就來(lái)原生 js ,來(lái)手寫(xiě)一個(gè)簡(jiǎn)易的打包工具 bundler ,實(shí)現(xiàn)對(duì)項(xiàng)目代碼的打包。
下面開(kāi)始進(jìn)行本文的講解~
🍉一、模塊分析(入口文件代碼分析)
1. 項(xiàng)目結(jié)構(gòu)
下面先來(lái)看下我們的項(xiàng)目文件結(jié)構(gòu)。請(qǐng)看下圖👇
2. 安裝第三方依賴(lài)
我們需要用到 4 個(gè)第三方依賴(lài)包,分別是:
- @babel/parser —— 幫助我們分析源代碼并生成抽象語(yǔ)法樹(shù) (AST) ;
- @babel/traverse —— 幫助我們對(duì)抽象語(yǔ)法樹(shù)進(jìn)行遍歷,并分析里語(yǔ)法樹(shù)里面的語(yǔ)句;
- @babel/core —— 將原始代碼打包編譯成瀏覽器能夠運(yùn)行的代碼;
- @babel/preset-env —— 用于在解析抽象語(yǔ)法樹(shù)時(shí)進(jìn)行配置。
下面依次給出安裝這四個(gè)庫(kù)的命令,分別是:
(1)@babel/parser
npm install @babel/parser --save(2)@babel/traverse
npm install @babel/traverse --save(3)@babel/core
npm install @balbel/core --save(4)@babel/preset-env
npm install @babel/preset-env --save3. 業(yè)務(wù)代碼
當(dāng)我們?nèi)プ鲆粋€(gè)項(xiàng)目打包時(shí),首先需要先對(duì)項(xiàng)目中的模塊進(jìn)行分析,現(xiàn)在我們先對(duì)入口文件進(jìn)行分析。假設(shè)我們要實(shí)現(xiàn)一個(gè)業(yè)務(wù),輸出的是 hello monday 。那么我們先來(lái)編寫(xiě)我們的業(yè)務(wù)代碼。
第一步: 編寫(xiě) word.js 文件代碼。具體代碼如下:
export const word = 'monday';第二步: 編寫(xiě) message.js 文件代碼。具體代碼如下:
import { word } from './word.js';const message = `hello ${word}`;export default message;第三步: 編寫(xiě) index.js 文件代碼。具體代碼如下:
import message from "./message.js";console.log(message);4. 開(kāi)始打包
編寫(xiě)完業(yè)務(wù)代碼之后,現(xiàn)在,我們要先來(lái)對(duì)入口文件 index.js 進(jìn)行打包。注意,這里除了 babel 外,我們不使用任何工具,沒(méi)有 webpack 、也沒(méi)有 webpack-cli 等工具。
我們?cè)诟夸浵孪葎?chuàng)建一個(gè)文件,命名為 bundler.js ,用這個(gè)文件來(lái)編寫(xiě)我們的打包邏輯。具體代碼如下:
const fs = require('fs'); const path = require('path'); const parser = require('@babel/parser'); const traverse = require('@babel/traverse').default; const babel = require('@babel/core');const moduleAnalyser = (filename) => {//1. 首先拿到文件名,拿到文件名之后我們?nèi)プx取文件里面的內(nèi)容const content = fs.readFileSync(filename, 'utf-8');//2. 借助Babel-parser,將文件里js的字符串,轉(zhuǎn)化成一個(gè)js的對(duì)象->這個(gè)js對(duì)象就是我們所說(shuō)的抽象語(yǔ)法樹(shù)const ast = parser.parse(content, {// 3. 如果你傳入的ES6的語(yǔ)法,那么需要設(shè)置sourceType為modulesourceType: 'module'});//收集入口文件中的依賴(lài)文件const dependencies = {};traverse(ast, {/*4. 有了抽象語(yǔ)法樹(shù)之后,我們需要去分析,它里面的聲明都在哪些地方,去找到import這些語(yǔ)句對(duì)應(yīng)的內(nèi)容5. 需要借助@babel/traverse這個(gè)工具,這個(gè)工具表明當(dāng)抽象語(yǔ)法樹(shù)有ImportDeclaration這樣的語(yǔ)句時(shí),它就會(huì)繼續(xù)下面的函數(shù)*/ImportDeclaration({ node }) {// console.log(node);const dirname = path.dirname(filename);const newFile = './' + path.join(dirname, node.source.value);// console.log(newFile);//6. 找到import語(yǔ)句之后,將這些語(yǔ)句拼裝成一個(gè)對(duì)象,放在dependencies這個(gè)變量中(以鍵值對(duì)的方式來(lái)進(jìn)行存儲(chǔ))dependencies[node.source.value] = newFile;}});/*7. 分析好了之后,對(duì)模塊的源代碼進(jìn)行一次編譯。通過(guò)使用transformFromAst,把它從一個(gè)ES module,轉(zhuǎn)換成瀏覽器可以執(zhí)行的語(yǔ)法,并將其存儲(chǔ)在code里面,code生成的代碼就是我們可以在瀏覽器上運(yùn)行的代碼*/const { code } = babel.transformFromAst(ast, null, {presets: ["@babel/preset-env"]})return {//返回入口文件的名字filename,//返回入口文件中的依賴(lài)文件dependencies,//返回瀏覽器上可以運(yùn)行的代碼code}// console.log(dependencies); }const moduleInfo = moduleAnalyser('./src/index.js'); console.log(moduleInfo);通過(guò)以上代碼,相信大家對(duì)打包入口文件有一個(gè)基本的了解。之后呢,在控制臺(tái)運(yùn)行 node bundler.js 命令,可以對(duì)打包過(guò)程中的各種分析進(jìn)行查看。
下面我們繼續(xù)第二塊的內(nèi)容~
🥑二、依賴(lài)圖譜Dependencies Graph
對(duì)于上述所講的內(nèi)容,我們談到的,只是對(duì)一個(gè)入口文件進(jìn)行分析。但是呢,這還遠(yuǎn)遠(yuǎn)不夠。所以,現(xiàn)在我們要來(lái)對(duì)整個(gè)工程文件進(jìn)行分析。
1. 結(jié)果分析
我們先來(lái)看下上述代碼中,只分析入口文件時(shí)的打印情況。具體代碼如下:
{filename: './src/index.js',dependencies: { './message.js': './src\\message.js' },code: '"use strict";\n' +'\n' +'var _message = _interopRequireDefault(require("./message.js"));\n' +'\n' +'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +'\n' +'console.log(_message["default"]);' }大家可以看到,入口文件分析完了以后,還有一層一層的依賴(lài)和code。現(xiàn)在,我們需要去順著這些依賴(lài),來(lái)把整個(gè)項(xiàng)目的內(nèi)容分析出來(lái)。
2. 分析所有模塊的依賴(lài)關(guān)系
我們現(xiàn)在來(lái)對(duì) bundler.js 進(jìn)行升級(jí)改造,把所有模塊的依賴(lài)關(guān)系給描繪出來(lái)。具體代碼如下:
const fs = require('fs'); const path = require('path'); const parser = require('@babel/parser'); const traverse = require('@babel/traverse').default; const babel = require('@babel/core');const moduleAnalyser = (filename) => {//1. 首先拿到文件名,拿到文件名之后我們?nèi)プx取文件里面的內(nèi)容const content = fs.readFileSync(filename, 'utf-8');//2. 借助Babel-parser,將文件里js的字符串,轉(zhuǎn)化成一個(gè)js的對(duì)象->這個(gè)js對(duì)象就是我們所說(shuō)的抽象語(yǔ)法樹(shù)const ast = parser.parse(content, {// 3. 如果你傳入的ES6的語(yǔ)法,那么需要設(shè)置sourceType為modulesourceType: 'module'});//收集入口文件中的依賴(lài)文件const dependencies = {};traverse(ast, {/*4. 有了抽象語(yǔ)法樹(shù)之后,我們需要去分析,它里面的聲明都在哪些地方,去找到import這些語(yǔ)句對(duì)應(yīng)的內(nèi)容5. 需要借助@babel/traverse這個(gè)工具,這個(gè)工具表明當(dāng)抽象語(yǔ)法樹(shù)有ImportDeclaration這樣的語(yǔ)句時(shí),它就會(huì)繼續(xù)下面的函數(shù)*/ImportDeclaration({ node }) {// console.log(node);const dirname = path.dirname(filename);const newFile = './' + path.join(dirname, node.source.value);// console.log(newFile);//6. 找到import語(yǔ)句之后,將這些語(yǔ)句拼裝成一個(gè)對(duì)象,放在dependencies這個(gè)變量中(以鍵值對(duì)的方式來(lái)進(jìn)行存儲(chǔ))dependencies[node.source.value] = newFile;}});/*7. 分析好了之后,對(duì)模塊的源代碼進(jìn)行一次編譯。通過(guò)使用transformFromAst,把它從一個(gè)ES module,轉(zhuǎn)換成瀏覽器可以執(zhí)行的語(yǔ)法,并將其存儲(chǔ)在code里面,code生成的代碼就是我們可以在瀏覽器上運(yùn)行的代碼*/const { code } = babel.transformFromAst(ast, null, {presets: ["@babel/preset-env"]})return {//返回入口文件的名字filename,dependencies,code}// console.log(dependencies); }const makeDependenciesGraph = (entry) => {//1. 對(duì)入口模塊進(jìn)行一次分析const entryModule = moduleAnalyser(entry);// console.log(entryModule);//2. 定義一個(gè)數(shù)組,存放入口文件和依賴(lài)const graphArray = [ entryModule ];//3. 對(duì)graphArray進(jìn)行遍歷for(i = 0; i < graphArray.length; i++){//4. 取出graphArray中的每一項(xiàng)const item = graphArray[i];//5. 取出每一項(xiàng)中的依賴(lài)dependenciesconst { dependencies } = item;//6. 如果入口文件有依賴(lài)時(shí),就對(duì)依賴(lài)進(jìn)行循環(huán)if(dependencies) {/*7. 通過(guò)不斷的循環(huán),最終,可以把它的入口文件,以及它的依賴(lài),還有它的依賴(lài)的依賴(lài),一層一層的遍歷出來(lái),并推到graphArray當(dāng)中*/for(let j in dependencies) {/*8. 通過(guò)隊(duì)列(先進(jìn)先出)的方式實(shí)現(xiàn)遞歸的效果;為什么用遞歸?遞歸地進(jìn)行分析,是因?yàn)槊總€(gè)依賴(lài)下面有可能還有依賴(lài)*/graphArray.push(moduleAnalyser(dependencies[j]))}}}//9. 處理后的graphArray是一個(gè)數(shù)組,現(xiàn)在需要對(duì)它進(jìn)行格式上的轉(zhuǎn)換const graph = {};graphArray.forEach(item => {graph[item.filename] = {dependencies: item.dependencies,code: item.code}});return graph; }// './src/index.js' 為入口文件 const graphInfo = makeDependenciesGraph('./src/index.js'); console.log(graphInfo);大家可以看到,我們制造了一個(gè)新的函數(shù) makeDependenciesGraph ,來(lái)描述所有模塊的依賴(lài)關(guān)系,并在最終對(duì)它進(jìn)行格式上的轉(zhuǎn)換,轉(zhuǎn)換成我們理想中的 js 對(duì)象。現(xiàn)在,我們來(lái)看下依賴(lài)關(guān)系的打印結(jié)果。打印結(jié)果如下:
{'./src/index.js': {dependencies: { './message.js': './src\\message.js' },code: '"use strict";\n' +'\n' +'var _message = _interopRequireDefault(require("./message.js"));\n' +'\n' +'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +'\n' +'console.log(_message["default"]);'},'./src\\message.js': {dependencies: { './word.js': './src\\word.js' },code: '"use strict";\n' +'\n' +'Object.defineProperty(exports, "__esModule", {\n' +' value: true\n' +'});\n' +'exports["default"] = void 0;\n' +'\n' +'var _word = require("./word.js");\n' +'\n' +'var message = "hello ".concat(_word.word);\n' +'var _default = message;\n' +'exports["default"] = _default;'},'./src\\word.js': {dependencies: {},code: '"use strict";\n' +'\n' +'Object.defineProperty(exports, "__esModule", {\n' +' value: true\n' +'});\n' +'exports.word = void 0;\n' +"var word = 'monday';\n" +'exports.word = word;'} }大家可以看到,所有模塊的依賴(lài)關(guān)系都給遍歷出來(lái)了。這也就說(shuō)明了,我們成功進(jìn)行了這一步的分析。
🍐三、生成代碼
1. 邏輯編寫(xiě)
上面我們已經(jīng)成功生成了依賴(lài)圖譜,那現(xiàn)在,我們就來(lái)把這個(gè)依賴(lài)圖譜,生成能夠真正在瀏覽器上運(yùn)行的代碼。我們繼續(xù)在 bundle.js 上,編寫(xiě)一個(gè)生成代碼的邏輯。具體代碼如下:
const fs = require('fs'); const path = require('path'); const parser = require('@babel/parser'); const traverse = require('@babel/traverse').default; const babel = require('@babel/core');const moduleAnalyser = (filename) => {//1. 首先拿到文件名,拿到文件名之后我們?nèi)プx取文件里面的內(nèi)容const content = fs.readFileSync(filename, 'utf-8');//2. 借助Babel-parser,將文件里js的字符串,轉(zhuǎn)化成一個(gè)js的對(duì)象->這個(gè)js對(duì)象就是我們所說(shuō)的抽象語(yǔ)法樹(shù)const ast = parser.parse(content, {// 3. 如果你傳入的ES6的語(yǔ)法,那么需要設(shè)置sourceType為modulesourceType: 'module'});//收集入口文件中的依賴(lài)文件const dependencies = {};traverse(ast, {/*4. 有了抽象語(yǔ)法樹(shù)之后,我們需要去分析,它里面的聲明都在哪些地方,去找到import這些語(yǔ)句對(duì)應(yīng)的內(nèi)容5. 需要借助@babel/traverse這個(gè)工具,這個(gè)工具表明當(dāng)抽象語(yǔ)法樹(shù)有ImportDeclaration這樣的語(yǔ)句時(shí),它就會(huì)繼續(xù)下面的函數(shù)*/ImportDeclaration({ node }) {// console.log(node);const dirname = path.dirname(filename);const newFile = './' + path.join(dirname, node.source.value);// console.log(newFile);//6. 找到import語(yǔ)句之后,將這些語(yǔ)句拼裝成一個(gè)對(duì)象,放在dependencies這個(gè)變量中(以鍵值對(duì)的方式來(lái)進(jìn)行存儲(chǔ))dependencies[node.source.value] = newFile;}});/*7. 分析好了之后,對(duì)模塊的源代碼進(jìn)行一次編譯。通過(guò)使用transformFromAst,把它從一個(gè)ES module,轉(zhuǎn)換成瀏覽器可以執(zhí)行的語(yǔ)法,并將其存儲(chǔ)在code里面,code生成的代碼就是我們可以在瀏覽器上運(yùn)行的代碼*/const { code } = babel.transformFromAst(ast, null, {presets: ["@babel/preset-env"]})return {//返回入口文件的名字filename,dependencies,code}// console.log(dependencies); }const makeDependenciesGraph = (entry) => {//1. 對(duì)入口模塊進(jìn)行一次分析const entryModule = moduleAnalyser(entry);// console.log(entryModule);//2. 定義一個(gè)數(shù)組,存放入口文件和依賴(lài)const graphArray = [ entryModule ];//3. 對(duì)graphArray進(jìn)行遍歷for(i = 0; i < graphArray.length; i++){//4. 取出graphArray中的每一項(xiàng)const item = graphArray[i];//5. 取出每一項(xiàng)中的依賴(lài)dependenciesconst { dependencies } = item;//6. 如果入口文件有依賴(lài)時(shí),就對(duì)依賴(lài)進(jìn)行循環(huán)if(dependencies) {/*7. 通過(guò)不斷的循環(huán),最終,可以把它的入口文件,以及它的依賴(lài),還有它的依賴(lài)的依賴(lài),一層一層的遍歷出來(lái),并推到graphArray當(dāng)中*/for(let j in dependencies) {/*8. 通過(guò)隊(duì)列(先進(jìn)先出)的方式實(shí)現(xiàn)遞歸的效果;為什么用遞歸?遞歸地進(jìn)行分析,是因?yàn)槊總€(gè)依賴(lài)下面有可能還有依賴(lài)*/graphArray.push(moduleAnalyser(dependencies[j]))}}}//9. 處理后的graphArray是一個(gè)數(shù)組,現(xiàn)在需要對(duì)它進(jìn)行格式上的轉(zhuǎn)換const graph = {};graphArray.forEach(item => {graph[item.filename] = {dependencies: item.dependencies,code: item.code}});return graph; }const generateCode = (entry) => {//1. 將生成的依賴(lài)圖譜進(jìn)行格式轉(zhuǎn)換const graph = JSON.stringify(makeDependenciesGraph(entry));/** 2. 構(gòu)造require函數(shù)和exports對(duì)象,轉(zhuǎn)化成瀏覽器認(rèn)識(shí)的字符串* return require(graph[module].dependencies[relative]) 目的是為了找到真實(shí)的路徑*/return `(function(graph){function require(module){function localRequire(relativePath) {return require(graph[module].dependencies[relativePath])}var exports = {};(function(require, exports, code){eval(code)})(localRequire, exports, graph[module].code);return exports;};require('${entry}')})(${graph});`; }// './src/index.js' 為入口文件 const code = generateCode('./src/index.js'); console.log(code);通過(guò)上面的代碼我們可以看到,我們先將生成的依賴(lài)圖譜進(jìn)行格式轉(zhuǎn)換,之后呢,構(gòu)造 require 函數(shù)和 exports 對(duì)象,最終轉(zhuǎn)換成瀏覽器認(rèn)識(shí)的字符串。
2. 結(jié)果分析
通過(guò)上面的業(yè)務(wù)編寫(xiě),我們完成了對(duì)整個(gè)項(xiàng)目進(jìn)行打包的過(guò)程。現(xiàn)在,我們來(lái)看一下打印結(jié)果:
(function(graph){function require(module){function localRequire(relativePath) {return require(graph[module].dependencies[relativePath])}var exports = {};(function(require, exports, code){eval(code)})(localRequire, exports, graph[module].code);return exports;};require('./src/index.js')})({"./src/index.js":{"dependencies":{"./message.js":"./src\\message.js"},"code":"\"use strict\";\n\nvar _message = _interopRequireDefault(require(\"./message.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log(_message[\"default\"]);"},"./src\\message.js":{"dependencies":{"./word.js":"./src\\word.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = void 0;\n\nvar _word = require(\"./word.js\");\n\nvar message = \"hello \".concat(_word.word);\nvar _default = message;\nexports[\"default\"] = _default;"},"./src\\word.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.word = void 0;\nvar word = 'monday';\nexports.word = word;"}});接下來(lái),我們把這個(gè)打印結(jié)果,放到瀏覽器上進(jìn)行檢驗(yàn)。檢驗(yàn)結(jié)果如下:
大家可以看到,打包后的結(jié)果,在瀏覽器上成功運(yùn)行了,并顯示除了 hello monday ,至此,說(shuō)明我們的項(xiàng)目打包成功。
🍓四、結(jié)束語(yǔ)
在上面的這篇文章中,從模塊的入口文件分析,再到依賴(lài)圖譜的解析,最后到生成瀏覽器所認(rèn)識(shí)的代碼,我們了解了打包工具的整個(gè)操作流程。
到這里,關(guān)于本文的講解就結(jié)束啦!希望對(duì)大家有幫助~
如文章有誤或有不理解的地方,歡迎小伙伴們?cè)u(píng)論區(qū)留言撒~💬
本文代碼已上傳至公眾號(hào),后臺(tái)回復(fù)關(guān)鍵詞 webpack 即可獲取~
🐣彩蛋 One More Thing
(:往期推薦
webpack入門(mén)核心知識(shí)👉不會(huì)webpack的前端可能是撿來(lái)的,萬(wàn)字總結(jié)webpack的超入門(mén)核心知識(shí)
webpack入門(mén)進(jìn)階知識(shí)👉webpack入門(mén)核心知識(shí)還看不過(guò)癮?速來(lái)圍觀萬(wàn)字進(jìn)階知識(shí)
webpack實(shí)戰(zhàn)案例配置👉[萬(wàn)字總結(jié)]webpack只會(huì)基礎(chǔ)配置可不行!快來(lái)把實(shí)戰(zhàn)案例配置一起打包帶走
手寫(xiě)loader和plugin👉webpack實(shí)戰(zhàn)之手寫(xiě)一個(gè)loader和plugin
(:番外篇
-
關(guān)注公眾號(hào)星期一研究室,第一時(shí)間關(guān)注優(yōu)質(zhì)文章,更多精選專(zhuān)欄待你解鎖~
-
如果這篇文章對(duì)你有用,記得留個(gè)腳印jio再走哦~
-
以上就是本文的全部?jī)?nèi)容!我們下期見(jiàn)!👋👋👋
總結(jié)
以上是生活随笔為你收集整理的手写一个简易bundler打包工具带你了解Webpack原理的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 玉米渣的功效与作用、禁忌和食用方法
- 下一篇: 富硒葡萄的功效与作用、禁忌和食用方法