如果對React
技術棧感興趣的你,可以去閱讀我的前面兩篇文章:
GitHub
上面都有對應的原始碼哦~ 歡迎Star
本專案在Github
上的原始碼地址: mini-webpack
webpack
可以說是目前最火的打包工具,如果用不好他,真的不敢說自己是個合格的前端工程師
本文會先介紹webpack
的打包流程,執行原理,然後去實現一個簡單的webpack
。
本質上,webpack
是一個現代 JavaScript
應用程式的靜態模組打包器(module bundler)。當 webpack 處理應用程式時,它會遞迴地構建一個依賴關係圖(dependency graph
),其中包含應用程式需要的每個模組,然後將所有這些模組打包成一個或多個 bundle
。
webpack打包過程
1.識別入口檔案
2.通過逐層識別模組依賴。(Commonjs、amd或者es6的import,webpack都會對其進行分析。來獲取程式碼的依賴)
3.webpack做的就是分析程式碼。轉換程式碼,編譯程式碼,輸出程式碼
4.最終形成打包後的程式碼
webpack打包原理
1.先逐級遞迴識別依賴,構建依賴圖譜
2.將程式碼轉化成AST
抽象語法樹
下圖是一個抽象語法樹:
]
3.在AST
階段中去處理程式碼
4.把AST
抽象語法樹變成瀏覽器可以識別的程式碼, 然後輸出
準備工作
在編寫自己的構建工具前,需要下載四個包。
1.@babel/parser
: 分析我們通過 fs.readFileSync 讀取的檔案內容,返回 AST (抽象語法樹)
2.@babel/traverse
: 可以遍歷 AST, 拿到必要的資料
3.@babel/core
: babel 核心模組,其有個transformFromAst方法,可以將 AST 轉化為瀏覽器可以執行的程式碼
4.@babel/preset-env
: 將程式碼轉化成 ES5 程式碼
使用yarn
下載:
$ yarn init -y
$ yarn add @babel/parser @babel/traverse @babel/core @babel/preset-env
首先檢視如何將最簡單的一個檔案轉換成AST
目錄結構:
程式碼實現:
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
// traverse 採用的 ES Module 匯出,我們通過 requier 引入的話就加個 .default
const babel = require('@babel/core');
const read = fileName => {
const buffer = fs.readFileSync(fileName, 'utf-8');
const AST = parser.parse(buffer, { sourceType: 'module' });
console.log(AST);
};
read('./test1.js');
上面程式碼:
1.先用同步的Node API
讀取檔案流
2.再將對應的buffer
轉換成下面的AST
Node {
type: 'File',
start: 0,
end: 32,
loc:
SourceLocation {
start: Position { line: 1, column: 0 },
end: Position { line: 1, column: 32 } },
program:
Node {
type: 'Program',
start: 0,
end: 32,
loc: SourceLocation { start: [Position], end: [Position] },
sourceType: 'module',
interpreter: null,
body: [ [Node] ],
directives: [] },
comments: [] }
我們已經將程式碼轉換成了AST
語法樹,那麼還需要遍歷AST
,然後轉換成瀏覽器可以認識的程式碼
在read
函式中加入如下程式碼:
// 依賴收集
const dependencies = {};
// 使用 traverse 來遍歷 AST
traverse(AST, {
ImportDeclaration({ node }) { // 函式名是 AST 中包含的內容,引數是一些節點,node 表示這些節點下的子內容
const dirname = path.dirname(filename); // 我們從抽象語法樹裡面拿到的路徑是相對路徑,然後我們要處理它,在 bundler.js 中才能正確使用
const newDirname = './' + path.join(dirname, node.source.value).replace('\\', '/'); // 將dirname 和 獲取到的依賴聯合生成絕對路徑
dependencies[node.source.value] = newDirname; // 將源路徑和新路徑以 key-value 的形式儲存起來
}
})
// 將抽象語法樹轉換成瀏覽器可以執行的程式碼
const { code } = babel.transformFromAst(AST, null, {
presets: ['@babel/preset-env']
})
return {
filename,
dependencies,
code
}
當我們呼叫read
函式,讀取test1.js
的內容時:
const result = read('./test1.js');
console.log(result);
得到列印的輸出結果:
{ fileName: './test1.js',
dependencies: {},
code: '"use strict";\n\nconsole.log(\'this is test1.js \');' }
原本test1.js
的內容是:
正式開始,下面加入ES6
模組化,重新定義檔案目錄
啟動檔案 index.js
...//一些的邏輯都在這個檔案中,我們只需要傳入一個entry入口
app.js
import test1 from './test1.js'
console.log(test1)
test1.js
import test2 from './test2.js';
console.log('this is test1.js ', test2);
test2.js
function test2() {
console.log('this is test2 ');
}
export default test2;
依賴關係非常清楚:
入口是index.js
- > 依賴test1.js
依賴 - > test2.js
上面僅僅做了一些的處理,如果遇到依賴的檔案還有依賴就不行了。
於是我們需要建立一個可以處理依賴關係的函式:
獲取依賴圖譜
// 建立依賴圖譜函式, 遞迴遍歷所有依賴模組
const makeDependenciesGraph = (entry) => {
const entryModule = read(entry)
const graghArray = [ entryModule ]; // 首先將我們分析的入口檔案結果放入圖譜陣列中
for (let i = 0; i < graghArray.length; i ++) {
const item = graghArray[i];
const { dependencies } = item; // 拿到當前模組所依賴的模組
if (dependencies) {
for ( let j in dependencies ) { // 通過 for-in 遍歷物件
graghArray.push(read(dependencies[j])); // 如果子模組又依賴其它模組,就分析子模組的內容
}
}
}
const gragh = {}; // 將圖譜的陣列形式轉換成物件形式
graghArray.forEach( item => {
gragh[item.filename] = {
dependencies: item.dependencies,
code: item.code
}
})
console.log(gragh)
return gragh;
}
列印gragh
得到的物件:
{ './app.js':
{ dependencies: { './test1.js': './test1.js' },
code:
'"use strict";\n\nvar _test = _interopRequireDefault(require("./test1.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nconsole.log(test
1);' },
'./test1.js':
{ dependencies: { './test2.js': './test2.js' },
code:
'"use strict";\n\nvar _test = _interopRequireDefault(require("./test2.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nconsole.log(\'th
is is test1.js \', _test["default"]);' },
'./test2.js':
{ dependencies: {},
code:
'"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports["default"] = void 0;\n\nfunction test2() {\n console.log(\'this is test2 \');\n}\n\nvar _default = tes
t2;\nexports["default"] = _default;' } }
此時我們已經獲取了所有的依賴,以及依賴的程式碼內容,只需要處理輸出即可
最終處理程式碼輸出
const generateCode = (entry) => {
// 注意:我們的 gragh 是一個物件,key是我們所有模組的絕對路徑,需要通過 JSON.stringify 來轉換
const gragh = JSON.stringify(makeDependenciesGraph(entry));
// 我們知道,webpack 是將我們的所有模組放在閉包裡面執行的,所以我們寫一個自執行的函式
// 注意: 我們生成的程式碼裡面,都是使用的 require 和 exports 來引入匯出模組的,而我們的瀏覽器是不認識的,所以需要構建這樣的函式
return `
(function( gragh ) {
function require( module ) {
// 相對路徑轉換成絕對路徑的方法
function localRequire(relativePath) {
return require(gragh[module].dependencies[relativePath])
}
const exports = {};
(function( require, exports, code ) {
eval(code)
})( localRequire, exports, gragh[module].code )
return exports;
}
require('${ entry }')
})(${ gragh })
`;
}
const code = generateCode('./app.js');
console.log(code)
得到編譯輸出的程式碼code
如下:
(function( gragh ) {
function require( module ) {
// 相對路徑轉換成絕對路徑的方法
function localRequire(relativePath) {
return require(gragh[module].dependencies[relativePath])
}
const exports = {};
(function( require, exports, code ) {
eval(code)
})( localRequire, exports, gragh[module].code )
return exports;
}
require('./app.js')
})({"./app.js":{"dependencies":{"./test1.js":"./test1.js"},"code":"\"use strict\";\n\nvar _test = _interopRequireDefault(require(\"./test1.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log(_test[\"default\"]);"},"./test1.js":{"dependencies":{"./test2.js":"./test2.js"},"code":"\"use strict\";\n\nvar _test = _interopRequireDefault(require(\"./test2.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log('this is test1.js ', _test[\"default\"]);"},"./test2.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = void 0;\n\nfunction test2() {\n console.log('this is test2 ');\n}\n\nvar _default = test2;\nexports[\"default\"] = _default;"}})
複製這段程式碼到瀏覽器中執行:
程式碼是可以執行的,ES6
模組化已經可以被瀏覽器識別
模仿webpack
實現loader
和plugin
:
在開頭那篇文章有介紹到,webpack
的loader
和plugin
本質:
loader
本質是對字串的正則匹配操作
plugin
的本質,是依靠webpack
執行時廣播出來的生命週期事件,再呼叫Node.js
的API
利用webpack
的全域性例項物件進行操作,不論是硬碟檔案的操作,還是記憶體中的資料操作。
webpack
的核心依賴庫:Tapable
tapable
是webpack
的核心依賴庫 想要讀懂webpack原始碼 就必須首先熟悉tapable
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
這些鉤子可分為同步的鉤子和非同步的鉤子,Sync
開頭的都是同步的鉤子,Async
開頭的都是非同步的鉤子。而非同步的鉤子又可分為並行和序列,其實同步的鉤子也可以理解為序列的鉤子。
我的理解:
這是一個釋出-訂閱模式
webpack
執行時廣播出事件,讓之前訂閱這些事件的訂閱者們(其實就是外掛)都觸發對應的事件,並且拿到全域性的webpack
例項物件,再做一系列的處理,就可以完成很複雜的功能
同步的鉤子是序列
非同步的鉤子分為並行和序列的鉤子,並行是指 等待所有併發的非同步事件執行之後再執行最終的非同步回撥。
而序列是值 第一步執行完畢再去執行第二步,以此類推,直到執行完所有回撥再去執行最終的非同步回撥。
拿最簡單的同步鉤子,SyncHook
來說
const { SyncHook } = require('tapable');
class Hook{
constructor(){
/** 1 生成SyncHook例項 */
this.hooks = new SyncHook(['name']);
}
tap(){
/** 2 註冊監聽函式 */
this.hooks.tap('node',function(name){
console.log('node',name);
});
this.hooks.tap('react',function(name){
console.log('react',name);
});
}
start(){
/** 3出發監聽函式 */
this.hooks.call('call end.');
}
}
let h = new Hook();
h.tap();/** 類似訂閱 */
h.start();/** 類似釋出 */
/* 列印順序:
node call end.
react call end.
*/
再看一個非同步鉤子AsyncParallelHook
const { AsyncParallelHook } = require('tapable');
class Hook{
constructor(){
this.hooks = new AsyncParallelHook(['name']);
}
tap(){
/** 非同步的註冊方法是tapAsync()
* 並且有回撥函式cb.
*/
this.hooks.tapAsync('node',function(name,cb){
setTimeout(()=>{
console.log('node',name);
cb();
},1000);
});
this.hooks.tapAsync('react',function(name,cb){
setTimeout(()=>{
console.log('react',name);
cb();
},1000);
});
}
start(){
/** 非同步的觸發方法是callAsync()
* 多了一個最終的回撥函式 fn.
*/
this.hooks.callAsync('call end.',function(){
console.log('最終的回撥');
});
}
}
let h = new Hook();
h.tap();/** 類似訂閱 */
h.start();/** 類似釋出 */
/* 列印順序:
node call end.
react call end.
最終的回撥
*/
當然,作者的能力還沒有到完全解析webpack
的水平,如果有興趣可以深入研究下Tapable這個庫的原始碼
有興趣深入研究的可以看看這兩篇文章
深入理解Webpack核心模組Tapable鉤子---同步版
深入理解Webpack核心模組Tapable鉤子---非同步版
今天先寫到這裡了,如果覺得寫得不錯,別忘了點個贊
歡迎加入segmentFault
前端交流群
我的個人微信:CALASFxiaotan
拉你進群
小姐姐們等你哦~