不折騰的前端,和鹹魚有什麼區別前端
目錄 |
---|
一 目錄 |
二 前言 |
三 第一步 轉換代碼、生成依賴 |
四 第二步 生成依賴圖譜 |
五 第三步 生成代碼字符串 |
返回目錄node
參考文章:實現一個簡單的Webpackgit
Webpack 的本質就是一個模塊打包器,工做就是將每一個模塊打包成相應的 bundle
。github
首先,咱們須要準備目錄:shell
+ 項目根路徑 || 文件夾
- index.js - 主入口
- message.js - 主入口依賴文件
- word.js - 主入口依賴文件的依賴文件
- bundler.js - 打包器
- bundle.js - 打包後存放代碼的文件
複製代碼
最終的項目地址:all-for-one - 031-手寫 Webpacknpm
若是小夥伴懶得敲,那能夠看上面倉庫的最終代碼。數組
而後,咱們 index.js
、message.js
、word.js
內容以下:babel
index.jsmarkdown
// 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
完成代碼轉換,並生成單個文件的依賴下面分 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 國際 許可協議 進行許可。
基於 github.com/LiangJunron… 上的做品創做。
本許可協議受權以外的使用權限能夠從 creativecommons.org/licenses/by… 處得到。