不折騰的前端,和鹹魚有什麼區別前端
目錄 |
---|
一 目錄 |
二 前言 |
三 第一步 轉換代碼、生成依賴 |
四 第二步 生成依賴圖譜 |
五 第三步 生成代碼字符串 |
返回目錄
參考文章:實現一個簡單的Webpacknode
Webpack 的本質就是一個模塊打包器,工做就是將每一個模塊打包成相應的 bundle
。git
首先,咱們須要準備目錄:github
+ 項目根路徑 || 文件夾 - index.js - 主入口 - message.js - 主入口依賴文件 - word.js - 主入口依賴文件的依賴文件 - bundler.js - 打包器 - bundle.js - 打包後存放代碼的文件
最終的項目地址:all-for-one - 031-手寫 Webpackshell
若是小夥伴懶得敲,那能夠看上面倉庫的最終代碼。npm
而後,咱們 index.js
、message.js
、word.js
內容以下:數組
index.js
// index.js import message from "./message.js"; console.log(message);
message.js
// message.js import { word } from "./word.js"; const message = `say ${word}`; export default message;
word.js
// word.js export const word = "hello";
最後,咱們實現一個 bundler.js
文件,將 index.js
當成入口,將裏面牽扯的文件都轉義並執行便可!babel
實現思路:閉包
babel
完成代碼轉換,並生成單個文件的依賴下面分 3 章嘗試這個內容。函數
返回目錄
這一步須要利用 babel
幫助咱們進行轉換,因此先裝包:
npm i @babel/parser @babel/traverse @babel/core @babel/preset-env -D
轉換代碼須要:
@babel/parser
生成 AST 抽象語法樹@babel/traverse
進行 AST 遍歷,記錄依賴關係@babel/core
和 @babel/preset-env
進行代碼的轉換而後添加內容:
bundler.js
const fs = require("fs"); const path = require("path"); const parser = require("@babel/parser"); const traverse = require("@babel/traverse").default; const babel = require("@babel/core"); // 第一步:轉換代碼、生成依賴 function stepOne(filename) { // 讀入文件 const content = fs.readFileSync(filename, "utf-8"); const ast = parser.parse(content, { sourceType: "module", // babel 官方規定必須加這個參數,否則沒法識別 ES Module }); const dependencies = {}; // 遍歷 AST 抽象語法樹 traverse(ast, { // 獲取經過 import 引入的模塊 ImportDeclaration({ node }) { const dirname = path.dirname(filename); const newFile = "./" + path.join(dirname, node.source.value); // 保存所依賴的模塊 dependencies[node.source.value] = newFile; }, }); //經過 @babel/core 和 @babel/preset-env 進行代碼的轉換 const { code } = babel.transformFromAst(ast, null, { presets: ["@babel/preset-env"], }); return { filename, // 該文件名 dependencies, // 該文件所依賴的模塊集合(鍵值對存儲) code, // 轉換後的代碼 }; } console.log('--- step one ---'); const one = stepOne('./index.js'); console.log(one); fs.writeFile('bundle.js', one.code, () => { console.log('寫入成功'); });
經過 Node 的方式運行這段代碼:node bundler.js
:
--- step one --- { filename: './index.js', dependencies: { './message.js': './message.js' }, code:` "use strict"; var _message = _interopRequireDefault(require("./message.js")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } // index.js console.log(_message["default"]); `, }
filename
:index.js
message.js
code
因此 jsliang 將 code
提取到 bundle.js
中進行查看:
bundler.js
// ...代碼省略 fs.writeFile('bundle.js', one.code, () => { console.log('寫入成功'); });
bundle.js
"use strict"; var _message = _interopRequireDefault(require("./message.js")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } // index.js console.log(_message["default"]);
解讀下這個文件內容:
use strict
:使用嚴格模式_interopRequireDefault
:對不符合 babel
標準的模塊添加 default
屬性,並指向自身對象以免 exports.default
出錯因此如今這份文件的內容是能夠運行的了,可是你運行的時候會報錯,報錯內容以下:
import { word } from "./word.js"; ^ SyntaxError: Unexpected token {
也就是說咱們執行到 message.js
,可是它裏面的內容無法運行,由於 import
是 ES6
內容嘛。
咋整,繼續看下面內容。
返回目錄
既然咱們只生成了一份轉義後的文件:
--- step one --- { filename: './index.js', dependencies: { './message.js': './message.js' }, code:` "use strict"; var _message = _interopRequireDefault(require("./message.js")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } // index.js console.log(_message["default"]); `, }
那麼咱們能夠根據其中的 dependencies
進行遞歸,將整個依賴圖譜都找出來:
bundler.js
// ...省略前面內容 // 第二步:生成依賴圖譜 // entry 爲入口文件 function stepTwo(entry) { const entryModule = stepOne(entry); // 這個數組是核心,雖然如今只有一個元素,日後看你就會明白 const graphArray = [entryModule]; for (let i = 0; i < graphArray.length; i++) { const item = graphArray[i]; const { dependencies } = item; // 拿到文件所依賴的模塊集合(鍵值對存儲) for (let j in dependencies) { graphArray.push(stepOne(dependencies[j])); // 敲黑板!關鍵代碼,目的是將入口模塊及其全部相關的模塊放入數組 } } // 接下來生成圖譜 const graph = {}; graphArray.forEach((item) => { graph[item.filename] = { dependencies: item.dependencies, code: item.code, }; }); return graph; } console.log('--- step two ---'); const two = stepTwo('./index.js'); console.log(two); let word = ''; for (let i in two) { word = word + two[i].code + '\n\n'; } fs.writeFile('bundle.js', word, () => { console.log('寫入成功'); });
因此當咱們 node bundler.js
的時候,會打印內容出來:
--- step two --- { './index.js': { dependencies: { './message.js': './message.js' }, code: '"use strict";\n\nvar _message = _interopRequireDefault(require("./message.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\n// index.js\nconsole.log(_message["default"]);' }, './message.js': { dependencies: { './word.js': './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\n// message.js\nvar message = "say ".concat(_word.word);\nvar _default = message;\nexports["default"] = _default;' }, './word.js': { dependencies: {}, code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports.word = void 0;\n// word.js\nvar word = "hello";\nexports.word = word;' } }
能夠看到咱們將整個依賴關係中的文件都搜索出來,並經過 babel
進行了轉換,而後 jsliang 經過 Node
的 fs
模塊將其寫進了 bundle.js
中:
bundler.js
let word = ''; for (let i in two) { word = word + two[i].code + '\n\n'; } fs.writeFile('bundle.js', word, () => { console.log('寫入成功'); });
再來看 bundle.js
內容:
bundle.js
"use strict"; var _message = _interopRequireDefault(require("./message.js")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } // index.js console.log(_message["default"]); "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports["default"] = void 0; var _word = require("./word.js"); // message.js var message = "say ".concat(_word.word); var _default = message; exports["default"] = _default; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.word = void 0; // word.js var word = "hello"; exports.word = word;
跟步驟一的解析差很少,不過這樣子的內容是無法運行的,畢竟咱們塞到同一個文件中了,因此須要步驟三咯。
返回目錄
最後一步咱們實現下面代碼:
bundler.js
// 下面是生成代碼字符串的操做 function stepThree(entry){ // 要先把對象轉換爲字符串,否則在下面的模板字符串中會默認調取對象的 toString 方法,參數變成 [Object object],顯然不行 const graph = JSON.stringify(stepTwo(entry)) return `(function(graph) { // require 函數的本質是執行一個模塊的代碼,而後將相應變量掛載到 exports 對象上 function require(module) { // localRequire 的本質是拿到依賴包的 exports 變量 function localRequire(relativePath) { return require(graph[module].dependencies[relativePath]); } var exports = {}; (function(require, exports, code) { eval(code); })(localRequire, exports, graph[module].code); return exports; // 函數返回指向局部變量,造成閉包,exports 變量在函數執行後不會被摧毀 } require('${entry}') })(${graph}) `; }; console.log('--- step three ---'); const three = stepThree('./index.js'); console.log(three); fs.writeFile('bundle.js', three, () => { console.log('寫入成功'); });
能夠看到,stepThree
返回的是一個當即執行函數,須要傳遞 graph
:
(function(graph) { // 具體內容 })(graph)
那麼圖譜(graph
)怎麼來?須要經過 stepTwo(entry)
拿到了依賴圖譜。
可是,由於步驟二返回的是對象啊,若是直接傳進去對象,那麼就會被轉義,因此須要 JSON.stringify()
:
const graph = JSON.stringify(stepTwo(entry)); (function(graph) { // 具體內容 })(graph)
那爲何這個函數(stepThree
)須要傳遞 entry
?緣由在於咱們須要一個主入口,就比如 Webpack 單入口形式:
轉變先後
// 轉變前 const graph = JSON.stringify(stepTwo(entry)); (function(graph) { function require(module) { // ...具體內容 } require('${entry}') })(graph) /* --- 分界線 --- */ // 轉變後 const graph = JSON.stringify(stepTwo(entry)); (function(graph) { function require(module) { // ...具體內容 } require('./index.js') })(graph)
這樣咱們就清楚了,從 index.js
入手,而後再看裏面具體內容:
function require(module) { // localRequire 的本質是拿到依賴包的 exports 變量 function localRequire(relativePath) { return require(graph[module].dependencies[relativePath]); } var exports = {}; (function(require, exports, code) { eval(code); })(localRequire, exports, graph[module].code); return exports; // 函數返回指向局部變量,造成閉包,exports 變量在函數執行後不會被摧毀 } require('./index.js')
eval
是指 JavaScript 能夠運行裏面的字符串代碼,eval('2 + 2')
會出來結果 4
,因此 eval(code)
就跟咱們第一步的時候,node bundle.js
同樣,執行 code
裏面的代碼。
因此咱們執行 require(module)
裏面的代碼,先走:
(function(require, exports, code) { eval(code); })(localRequire, exports, graph[module].code);
此刻這個代碼中,傳遞的參數有 3 個:
require
:若是在 eval(code)
執行代碼期間,碰到 require
就調用 localRequire
方法exports
:若是在 eval(code)
執行代碼期間,碰到 exports
就將裏面內容設置到對象 exports
中graph[module].code
:一開始 module
是 './index.js'
,因此查找 graph
中 './index.js'
對應的 code
,將其傳遞進 eval(code)
裏面有的小夥伴會好奇這代碼怎麼走的,咱們能夠先看下面一段代碼:
const localRequire = (abc) => { console.log(abc); }; const code = ` console.log(456); doRequire(123) `; (function(doRequire, code) { eval(code); })(localRequire, code);
這段代碼中,執行的 doRequire
其實就是傳入進來的 localRequire
方法,最終輸出 456
和 123
。
如今,再回頭來看:
區塊一:
bundle.js
function require(module) { // localRequire 的本質是拿到依賴包的 exports 變量 function localRequire(relativePath) { return require(graph[module].dependencies[relativePath]); } var exports = {}; (function (require, exports, code) { eval(code); })(localRequire, exports, graph[module].code); return exports; // 函數返回指向局部變量,造成閉包,exports 變量在函數執行後不會被摧毀 } require("./index.js");
它先執行 當即執行函數 (function (require, exports, code) {})()
,再到 eval(code)
,從而執行下面代碼:
區塊二:
graph['./index.js'].code
"use strict"; var _message = _interopRequireDefault(require("./message.js")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } // index.js console.log(_message["default"]);
在碰到 require("./message.js")
的時候,繼續進去上面【區塊一】的代碼,由於此刻的 require
是:
function localRequire(relativePath) { return require(graph[module].dependencies[relativePath]); }
因此咱們再調用本身的 require()
方法,將內容傳遞進去,變成:require('./message.js')
。
……以此類推,直到 './word.js'
裏面沒有 require()
方法體了,咱們再執行下面內容,將 exports
導出去。
這就是這段內容的運行流程。
至於其中細節咱們就不一一贅述了,小夥伴們若是還沒看懂能夠自行斷點調試,這裏面的代碼口頭描述的話 jsliang 講得不是清楚。
最後咱們看看輸出整理後的 bundle.js
:
bundle.js
(function (graph) { // require 函數的本質是執行一個模塊的代碼,而後將相應變量掛載到 exports 對象上 function require(module) { // localRequire 的本質是拿到依賴包的 exports 變量 function localRequire(relativePath) { return require(graph[module].dependencies[relativePath]); } var exports = {}; (function (require, exports, code) { eval(code); })(localRequire, exports, graph[module].code); return exports; // 函數返回指向局部變量,造成閉包,exports 變量在函數執行後不會被摧毀 } require("./index.js"); })({ "./index.js": { dependencies: { "./message.js": "./message.js" }, code: ` "use strict"; var _message = _interopRequireDefault(require("./message.js")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } // index.js console.log(_message["default"]); `, }, "./message.js": { dependencies: { "./word.js": "./word.js" }, code: ` "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports["default"] = void 0; var _word = require("./word.js"); // message.js var message = "say ".concat(_word.word); var _default = message; exports["default"] = _default; `, }, "./word.js": { dependencies: {}, code: ` "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.word = void 0; // word.js var word = "hello"; exports.word = word;', }, });
此時咱們 node bundle.js
,就能夠獲取到:
say hello
這樣咱們就手擼完成了單入口的 Webpack 簡單實現。
jsliang 的文檔庫由 梁峻榮 採用 知識共享 署名-非商業性使用-相同方式共享 4.0 國際 許可協議 進行許可。<br/>基於 https://github.com/LiangJunrong/document-library 上的做品創做。<br/>本許可協議受權以外的使用權限能夠從 https://creativecommons.org/licenses/by-nc-sa/2.5/cn/ 處得到。