若是對React
技術棧感興趣的你,能夠去閱讀個人前面兩篇文章:前端
從零搭建一個React優化版腳手架react
GitHub
上面都有對應的源碼哦~ 歡迎Star
webpack
本項目在Github
上的源碼地址: mini-webpackgit
webpack
能夠說是目前最火的打包工具,若是用很差他,真的不敢說本身是個合格的前端工程師es6
本文會先介紹webpack
的打包流程,運行原理,而後去實現一個簡單的webpack
。github
本質上,webpack
是一個現代 JavaScript
應用程序的靜態模塊打包器(module bundler)。當 webpack 處理應用程序時,它會遞歸地構建一個依賴關係圖(dependency graph
),其中包含應用程序須要的每一個模塊,而後將全部這些模塊打包成一個或多個 bundle
。web
1.識別入口文件segmentfault
2.經過逐層識別模塊依賴。(Commonjs、amd或者es6的import,webpack都會對其進行分析。來獲取代碼的依賴)數組
3.webpack作的就是分析代碼。轉換代碼,編譯代碼,輸出代碼
4.最終造成打包後的代碼
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
拉你進羣
小姐姐們等你哦~