本系列將會從原理、開發、優化、對比四個方面給大家介紹webpack的工作流程。【預設是以webpack v3為例子】
儲備知識
CommonJS 規範
// 模組引入
let moduleA = require('./a.js')
// 模組匯出
module.exports = () => {}
複製程式碼
es6規範
// 模組引入
import {moduleA} from './a.js'
// 模組匯出
export default () => {}
複製程式碼
黑盒體驗
我們可以把webpack看做一個黑盒,只要會用就可以。先來體驗一次很簡單的webpack打包過程
const webpack = require('webpack')
const path = require('path')
module.exports = {
entry: './index.js',
output: {
filename: 'index.js',
path: path.resolve(__dirname, 'public')
}
}
複製程式碼
啟動編譯,在命令列輸入 node_modules/.bin/webpack 就可看到一次打包過程
關於如何啟動webpack
如果是全域性安裝了webpack,可以在命令列直接輸入 webpack
如果只是專案資料夾安裝,需要輸入 node_modules/.bin/webpack
- npx
在 npmV5版本 會贈送一個npx
npx 會自動查詢當前依賴包中的可執行檔案,如果找不到,就會去 PATH 裡找。如果依然找不到,就會幫你安裝
所以也可以通過npx執行webpack
npx webpack
複製程式碼
require方法
實現一個require方法
common.js的規範中 引入一個模組需要
let getA = require('./a')
複製程式碼
自己寫一個require方法
let fs = require('fs')
// 查詢module
function myReq (myModule) {
// 讀取檔案資訊
let cont = fs.readFileSync(myModule, 'utf-8')
/* function (exports, require, module, __filename, __dirname) {
moduel.exports = {a: 'apple'}
return moduel.exports
} */
let nodeFn = new Function('exports', 'require', 'module', '__filename', '__dirname', cont + 'return module.exports')
let module = {
exports: {}
}
return nodeFn(module.exports, myReq, module, __filename, __dirname)
}
// let getA = require('./a')
let getA = myReq('./a.js')
console.log(getA, 'getA')
複製程式碼
思路:讀取檔案內容,根據node的封裝規範,傳入幾個必須的引數即可。
- 刪減 webpack 編譯後的檔案
把剛剛打包之後的 dist/index.js 刪減掉一些不用的程式碼
(function(modules) {
function myRequire(moduleId) {
var module = {
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, myRequire);
// call 用於讓 modules[moduleId] 函式執行 執行的是傳入後面的引數
return module.exports;
}
return myRequire(/* 下面的第一個函式引數 */);
})
([
(function(module, exports) {
console.log('123')
})
]);
複製程式碼
可以看出來, webpack打包生成之後的檔案內容就和編譯的require方法類似。這就是為什麼打包之後的js檔案可直接在瀏覽器中執行的原因
編譯流程
常見名詞解釋
引數 | 說明 |
---|---|
entry | 專案入口 |
module | 開發中每一個檔案都可以看做module |
chunk | 程式碼塊 |
loader | 模組轉化器 |
plugin | 擴充套件外掛 自定義webpack打包過程 |
bundle | 最終打包完成的檔案 |
打包流程
webpack的執行流程是一個序列的過程,從啟動到結束,會依次執行以下流程
- 引數初始化
從配置檔案 【webpack.config.js】和 shell 語句中讀取與合併引數
- 開始編譯
初始化一個compiler物件 載入所有外掛 執行物件的run方法開始編譯
- 確定入口檔案
根據配置檔案找到專案所有的入口檔案
- 編譯模組
從入口開始 呼叫配置的loader對模組進行編譯 【有一個遞迴尋找依賴模組的流程】
模組編譯完成後 得到模組被轉化後的最後內容以及他們之間的依賴關係
- 資源輸出
根據入口檔案和模組之間的依賴關係 組成chunk檔案 【一個chunk可能包含多個模組】每一個chunk將會被轉化成一個單獨的檔案加入輸出列表中
- 輸出
根據配置的輸出引數 【路徑和檔名】將輸出內容寫入檔案系統
** 在以上的過程 WP會在特定的時間點廣播特定的事件 外掛在監聽到感興趣的事件後會執行特定的邏輯 **
簡化流程
其實以上流程可以簡化為三個階段
原始碼分析
核心庫 tapable
在node中有一個事件發射器 EventEmitter ,可以進行事件監聽與發射。
var EventEmitter = require('events').EventEmitter;
var event = new EventEmitter();
event.on('some_event', function () {
console.log('some_event 事件觸發');
});
setTimeout(function () {
event.emit('some_event');
}, 1000);
複製程式碼
webpack核心庫 tapable 的原理和 EventEmitter 類似,通過事件的註冊和監聽,觸發各個編譯週期中的函式方法. Tapable 還允許你通過回撥函式的引數,訪問事件的“觸發者(emittee)”或“提供者(producer)”
核心物件 compiler
compiler 繼承自 tapable 可以進行事件的廣播和監聽
compiler 進行事件的廣播和監聽的方式為
// 廣播事件 params 為附帶引數
compiler.apply('event-name', params)
// 監聽 名為 event-name 的事件
compiler.plugin('event-name', function (params) {
})
複製程式碼
webpack 在初始化的時候 會將 compiler物件傳入到plugin中 可以使用它來訪問 webpack 的主環境
compiler 物件代表了完整的 webpack 環境配置。這個物件在啟動 webpack 時被一次性建立,並配置好所有可操作的設定,包括 options,loader 和 plugin。
核心物件 compilation
compilation 繼承自 tapable 可以進行事件的廣播和監聽
compilation 物件代表了一次資源版本構建。當執行 webpack 開發環境中介軟體時,每當檢測到一個檔案變化,就會建立一個新的 compilation,從而生成一組新的編譯資源。
一個 compilation 物件表現了當前的模組資源、編譯生成資源、變化的檔案、以及被跟蹤依賴的狀態資訊
plugin 實現機制
作用原理
在webpack的編譯流程,每一個階段都會廣播不同的事件,比如 run, done 等事件。plugin會監聽到這些事件,一旦事件發生,就會執行註冊好的函式方法
plugin分析
每一個plugin都是 一個具有 apply 屬性的 JavaScript 物件
class MPlugin {
// 這裡獲取使用者為外掛傳入的配置引數
constructor (options) {
}
// webpack 會呼叫 MPlugin 例項的apply方法 為外掛例項傳入 compiler 物件
apply (compiler) {
compiler.plugin('compilation', function (compilation) {
// 回撥函式中 傳入了 compilation 物件
})
}
}
複製程式碼
在webpack初始化的階段 會往plugin中傳遞compiler物件
編寫plugin
class StartWp {
constructor(options) {
this.options = options
}
apply(compiler) {
let {name} = this.options
// 監聽事件 這是非同步的 所以要執行cb 不然會卡到這裡不動了
compiler.plugin('run', function (compilation, cb) {
console.log('run', name)
// 每一次重新編譯的時候又會觸發
// compilation.plugin('')
cb();
})
compiler.plugin('done', function (compilation) {
console.log('done', name)
})
}
}
module.exports = StartWp
複製程式碼
-
傳遞給外掛的compiler和compilation是相同的 也就是某一個外掛有修改物件的話會影響後面的外掛的使用
-
有的事件是非同步的,所以在使用的時候,要執行 cb() 去通知webpack 本次事件監聽結束了 要往下繼續執行否則會卡到這裡
如何使用此外掛
plugins: [
new StartWp({
name: 'v3 - plugin '
})
]
複製程式碼
自己來寫一個簡易版本的webpack打包器
實現原理: 根據打包的模板格式 讀取檔案資訊並輸入到指定的位置
-
藉助ejs
-
將簡化的webpack打包結果拿出來作為 字串模板
最簡易的webpack
const fs = require('fs')
// 入口檔案
let input = './index.js'
// 輸出地址
let output = './dist/index.js'
const ejs = require('ejs')
const getIntry = fs.readFileSync(input, 'utf-8')
let template = `(function(modules) {
function __webpack_require__(moduleId) {
var module = {
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
return module.exports;
}
return __webpack_require__(0);
})
([
(function(module, exports) {
<%- getIntry %>
})
])`
let result = ejs.render(template, {
getIntry
})
// 將結果輸出到 dist
fs.writeFileSync(output, result)
複製程式碼
在命令列執行一次 node webpack.0.1.0.js
可以看到在dist目錄有index.js生成 將其引入 html頁面
這樣就完成了一個非常非常簡單的webpack
加入 require 處理
如果入口檔案中 有使用到 require 則需要將其替換為webpack提供的 webpack_require
先看一下如果有使用 require 之後的打包之後的結果 [簡化版本]
bundle.js
(function(modules) {
function __webpack_require__(moduleId) {
var module = {
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
return module.exports;
}
return __webpack_require__(0);
})
([
(function(module, exports, __webpack_require__) {
__webpack_require__(1)
console.log('index.js')
}),
(function(module, exports) {
console.log(123)
})
]);
複製程式碼
我們使用這個模板來重新編寫一個簡易的webpack
const fs = require('fs')
const path = require('path')
// 入口檔案
let input = './index.js'
// 輸出地址
let output = './dist/index.js'
const ejs = require('ejs')
const getIntry = fs.readFileSync(input, 'utf-8')
// 將getIntry 中的 require 進行處理
const contAry = []
let dealIntry = getIntry.replace(/(require)\(['"](.+?)['"]\)/g, ($1, $2, $3, $4) => {
let cont = fs.readFileSync($3, 'utf-8')
contAry.push(cont)
return $2 = `__webpack_require__(${contAry.length})`
})
let template = `(function(modules) {
function __webpack_require__(moduleId) {
var module = {
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
return module.exports;
}
return __webpack_require__(0);
})
([
(function(module, exports, __webpack_require__) {
<%- dealIntry %>
}),
<% for(var i=0;i < contAry.length; i++){ %>
(function(module, exports) {
<%- contAry[i] %>
}),
<%}%>
])`
let result = ejs.render(template, {
dealIntry,
contAry
})
// 將結果輸出到 dist
fs.writeFileSync(output, result)
複製程式碼
在命令列執行一次 node webpack.1.0.0.js
原始碼篇提問
- 在自己構建的plugin中 是否可以進行事件廣播
可以。只要能拿到 compiler或者compilation物件 就可以廣播事件,為其他外掛監聽使用
參考文章
- 《深入淺出webpack》 此書作者掘金地址
- 《細說 webpack 之流程篇》