簡單概述 Webpack 總體運行流程
-
讀取參數 -
實例化 Compiler
-
entryOption
階段,讀取入口文件 -
Loader
編譯對應文件,解析成AST
-
找到對應依賴,遞歸編譯處理,生成 chunk
-
輸出到 dist
webpack 打包主流程源碼閱讀
經過打斷點的方式閱讀源碼,來看一下命令行輸入 webpack 的時候都發生了什麼?P.S. 如下的源碼流程分析都基於 webpack4
前端
先附上一張本身繪製的執行流程圖node
初始化階段
-
初始化參數( webpack.config.js+shell options
)
webpack
的幾種啓動方式webpack
-
經過 webpack-cli
執行 會走到./node_modules/.bin/webpack-cli
(執行) -
經過 shell
執行webpack
,會走到./bin/webpack.js
-
經過 require("webpack")
執行 會走到./node_modules/webpack/lib/webpack.js
追加 shell
命令的參數,如-p , -w,
經過 yargs
解析命令行參數convert-yargs
把命令行參數轉換成 Webpack 的配置選項對象 同時實例化插件 new Plugin()
web
-
實例化 Compiler
閱讀完整源碼點擊這裏:webpack.jsshell
// webpack入口
const webpack = (options, callback) => {
let compiler
// 實例Compiler
if (Array.isArray(options)) {
// ...
compiler = createMultiCompiler(options)
} else {
compiler = createCompiler(options)
}
// ...
// 若options.watch === true && callback 則開啓watch線程
if (watch) {
compiler.watch(watchOptions, callback)
} else {
compiler.run((err, stats) => {
compiler.close((err2) => {
callback(err || err2, stats)
})
})
}
return compiler
}
webpack 的入口文件其實就實例了 Compiler
並調用了 run
方法開啓了編譯,數組
-
註冊 NodeEnvironmentPlugin
插件,掛載 plugin 插件,使用 WebpackOptionsApply 初始化基礎插件
在此期間會 apply
全部 webpack
內置的插件,爲 webpack
事件流掛上自定義鉤子瀏覽器
源碼仍然在webpack.js文件緩存
const createCompiler = (rawOptions) => {
// ...省略代碼
const compiler = new Compiler(options.context)
compiler.options = options
//應用Node的文件系統到compiler對象,方便後續的文件查找和讀取
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging,
}).apply(compiler)
// 加載插件
if (Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
// 依次調用插件的apply方法(默認每一個插件對象實例都須要提供一個apply)若爲函數則直接調用,將compiler實例做爲參數傳入,方便插件調用這次構建提供的Webpack API並監聽後續的全部事件Hook。
if (typeof plugin === 'function') {
plugin.call(compiler, compiler)
} else {
plugin.apply(compiler)
}
}
}
// 應用默認的Webpack配置
applyWebpackOptionsDefaults(options)
// 隨即以後,觸發一些Hook
compiler.hooks.environment.call()
compiler.hooks.afterEnvironment.call()
// 內置的Plugin的引入,對webpack options進行初始化
new WebpackOptionsApply().process(options, compiler)
compiler.hooks.initialize.call()
return compiler
}
編譯階段
-
啓動編譯( run/watch
階段)
這裏有個小邏輯區分是不是 watch
,若是是非 watch
,則會正常執行一次 compiler.run()
。微信
若是是監聽文件(如:--watch
)的模式,則會傳遞監聽的 watchOptions
,生成 Watching
實例,每次變化都從新觸發回調。babel
若是不是監視模式就調用 Compiler
對象的 run
方法,befornRun->beforeCompile->compile->thisCompilation->compilation
開始構建整個應用。
const { SyncHook, SyncBailHook, AsyncSeriesHook } = require('tapable')
class Compiler {
constructor() {
// 1. 定義生命週期鉤子
this.hooks = Object.freeze({
// ...只列舉幾個經常使用的常見鉤子,更多hook就不列舉了,有興趣看源碼
done: new AsyncSeriesHook(['stats']), //一次編譯完成後執行,回調參數:stats
beforeRun: new AsyncSeriesHook(['compiler']),
run: new AsyncSeriesHook(['compiler']), //在編譯器開始讀取記錄前執行
emit: new AsyncSeriesHook(['compilation']), //在生成文件到output目錄以前執行,回調參數:compilation
afterEmit: new AsyncSeriesHook(['compilation']), //在生成文件到output目錄以後執行
compilation: new SyncHook(['compilation', 'params']), //在一次compilation建立後執行插件
beforeCompile: new AsyncSeriesHook(['params']),
compile: new SyncHook(['params']), //在一個新的compilation建立以前執行
make: new AsyncParallelHook(['compilation']), //完成一次編譯以前執行
afterCompile: new AsyncSeriesHook(['compilation']),
watchRun: new AsyncSeriesHook(['compiler']),
failed: new SyncHook(['error']),
watchClose: new SyncHook([]),
afterPlugins: new SyncHook(['compiler']),
entryOption: new SyncBailHook(['context', 'entry']),
})
// ...省略代碼
}
newCompilation() {
// 建立Compilation對象回調compilation相關鉤子
const compilation = new Compilation(this)
//...一系列操做
this.hooks.compilation.call(compilation, params) //compilation對象建立完成
return compilation
}
watch() {
//若是運行在watch模式則執行watch方法,不然執行run方法
if (this.running) {
return handler(new ConcurrentCompilationError())
}
this.running = true
this.watchMode = true
return new Watching(this, watchOptions, handler)
}
run(callback) {
if (this.running) {
return callback(new ConcurrentCompilationError())
}
this.running = true
process.nextTick(() => {
this.emitAssets(compilation, (err) => {
if (err) {
// 在編譯和輸出的流程中遇到異常時,會觸發 failed 事件
this.hooks.failed.call(err)
}
if (compilation.hooks.needAdditionalPass.call()) {
// ...
// done:完成編譯
this.hooks.done.callAsync(stats, (err) => {
// 建立compilation對象以前
this.compile(onCompiled)
})
}
this.emitRecords((err) => {
this.hooks.done.callAsync(stats, (err) => {})
})
})
})
this.hooks.beforeRun.callAsync(this, (err) => {
this.hooks.run.callAsync(this, (err) => {
this.readRecords((err) => {
this.compile(onCompiled)
})
})
})
}
compile(callback) {
const params = this.newCompilationParams()
this.hooks.beforeCompile.callAsync(params, (err) => {
this.hooks.compile.call(params)
//已完成complication的實例化
const compilation = this.newCompilation(params)
//觸發make事件並調用addEntry,找到入口js,進行下一步
// make:表示一個新的complication建立完畢
this.hooks.make.callAsync(compilation, (err) => {
process.nextTick(() => {
compilation.finish((err) => {
// 封裝構建結果(seal),逐次對每一個module和chunk進行整理,每一個chunk對應一個入口文件
compilation.seal((err) => {
this.hooks.afterCompile.callAsync(compilation, (err) => {
// 異步的事件須要在插件處理完任務時調用回調函數通知 Webpack 進入下一個流程,
// 否則運行流程將會一直卡在這不往下執行
return callback(null, compilation)
})
})
})
})
})
})
}
emitAssets() {}
}
-
編譯模塊:( make
階段)
-
從 entry 入口配置文件出發, 調用全部配置的 Loader
對模塊進行處理, -
再找出該模塊依賴的模塊, 經過 acorn
庫生成模塊代碼的AST
語法樹,造成依賴關係樹(每一個模塊被處理後的最終內容以及它們之間的依賴關係), -
根據語法樹分析這個模塊是否還有依賴的模塊,若是有則繼續循環每一個依賴;再遞歸本步驟直到全部入口依賴的文件都通過了對應的 loader 處理。 -
解析結束後, webpack
會把全部模塊封裝在一個函數裏,並放入一個名爲modules
的數組裏。 -
將 modules
傳入一個自執行函數中,自執行函數包含一個installedModules
對象,已經執行的代碼模塊會保存在此對象中。 -
最後自執行函數中加載函數( webpack__require
)載入模塊。
class Compilation extends Tapable {
constructor(compiler) {
super();
this.hooks = {};
// ...
this.compiler = compiler;
// ...
// 構建生成的資源
this.chunks = [];
this.chunkGroups = [];
this.modules = [];
this.additionalChunkAssets = [];
this.assets = {};
this.children = [];
// ...
}
//
buildModule(module, optional, origin, dependencies, thisCallback) {
// ...
// 調用module.build方法進行編譯代碼,build中 實際上是利用acorn編譯生成AST
this.hooks.buildModule.call(module);
module.build( /**param*/ );
}
// 將模塊添加到列表中,並編譯模塊
_addModuleChain(context, dependency, onModule, callback) {
// ...
// moduleFactory.create建立模塊,這裏會先利用loader處理文件,而後生成模塊對象
moduleFactory.create({
contextInfo: {
issuer: "",
compiler: this.compiler.name
},
context: context,
dependencies: [dependency]
}, (err, module) = > {
const addModuleResult = this.addModule(module);
module = addModuleResult.module;
onModule(module);
dependency.module = module;
// ...
// 調用buildModule編譯模塊
this.buildModule(module, false, null, null, err = > {});
});
}
// 添加入口模塊,開始編譯&構建
addEntry(context, entry, name, callback) {
// ...
this._addModuleChain( // 調用_addModuleChain添加模塊
context, entry, module = > {
this.entries.push(module);
},
// ...
);
}
seal(callback) {
this.hooks.seal.call();
// ...
//完成了Chunk的構建和依賴、Chunk、module等各方面的優化
const chunk = this.addChunk(name);
const entrypoint = new Entrypoint(name);
entrypoint.setRuntimeChunk(chunk);
entrypoint.addOrigin(null, name, preparedEntrypoint.request);
this.namedChunkGroups.set(name, entrypoint);
this.entrypoints.set(name, entrypoint);
this.chunkGroups.push(entrypoint);
GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk);
GraphHelpers.connectChunkAndModule(chunk, module);
chunk.entryModule = module;
chunk.name = name;
// ...
this.hooks.beforeHash.call();
this.createHash();
this.hooks.afterHash.call();
this.hooks.beforeModuleAssets.call();
this.createModuleAssets();
if (this.hooks.shouldGenerateChunkAssets.call() !== false) {
this.hooks.beforeChunkAssets.call();
this.createChunkAssets();
}
// ...
}
createHash() {
// ...
}
// 生成 assets 資源並 保存到 Compilation.assets 中 給webpack寫插件的時候會用到
createModuleAssets() {
for (let i = 0; i < this.modules.length; i++) {
const module = this.modules[i];
if (module.buildInfo.assets) {
for (const assetName of Object.keys(module.buildInfo.assets)) {
const fileName = this.getPath(assetName);
this.assets[fileName] = module.buildInfo.assets[assetName];
this.hooks.moduleAsset.call(module, fileName);
}
}
}
}
createChunkAssets() {
asyncLib.forEach(
this.chunks, (chunk, callback) = > {
// manifest是數組結構,每一個manifest元素都提供了 `render` 方法,提供後續的源碼字符串生成服務。至於render方法什麼時候初始化的,在`./lib/MainTemplate.js`中
let manifest = this.getRenderManifest()
asyncLib.forEach(
manifest, (fileManifest, callback) = > {...
source = fileManifest.render()
this.emitAsset(file, source, assetInfo)
}, callback)
}, callback)
}
}
class SingleEntryPlugin {
apply(compiler) {
compiler.hooks.compilation.tap(
'SingleEntryPlugin',
(compilation, { normalModuleFactory }) => {
compilation.dependencyFactories.set(
SingleEntryDependency,
normalModuleFactory
)
}
)
compiler.hooks.make.tapAsync(
'SingleEntryPlugin',
(compilation, callback) => {
const { entry, name, context } = this
const dep = SingleEntryPlugin.createDependency(entry, name)
compilation.addEntry(context, dep, name, callback)
}
)
}
static createDependency(entry, name) {
const dep = new SingleEntryDependency(entry)
dep.loc = { name }
return dep
}
}
歸納一下 make
階段單入口打包的流程,大體爲 4 步驟
-
執行 SingleEntryPlugin
(單入口調用SingleEntryPlugin
,多入口調用MultiEntryPlugin
,異步調用DynamicEntryPlugin
),EntryPlugin
方法中調用了Compilation.addEntry
方法,添加入口模塊,開始編譯&構建 -
addEntry
中調用_addModuleChain
,將模塊添加到依賴列表中,並編譯模塊 -
而後在 buildModule
方法中,調用了NormalModule.build
,建立模塊之時,會調用runLoaders
,執行Loader
,利用acorn
編譯生成AST
-
分析文件的依賴關係逐個拉取依賴模塊並重覆上述過程,最後將全部模塊中的 require
語法替換成webpack_require
來模擬模塊化操做。
從源碼的角度,思考一下, loader
爲何是自右向左執行的,loader
中有 pitch
也會從右到左執行的麼?
runLoaders
方法調用 iteratePitchingLoaders
去遞歸查找執行有 pich
屬性的 loader
;若存在多個 pitch
屬性的 loader
則依次執行全部帶 pitch
屬性的 loader
,執行完後逆向執行全部帶 pitch
屬性的 normal
的 normal loader
後返回 result
,沒有 pitch
屬性的 loader
就不會再執行;若 loaders
中沒有 pitch
屬性的 loader
則逆向執行 loader;執行正常
loader 是在 iterateNormalLoaders
方法完成的,處理完全部 loader
後返回 result
。
出自文章你真的掌握了 loader 麼?- loader 十問(https://juejin.im/post/5bc1a73df265da0a8d36b74f)
Compiler 和 Compilation 的區別
webpack
打包離不開 Compiler
和 Compilation
,它們兩個分工明確,理解它們是咱們理清 webpack
構建流程重要的一步。
Compiler
負責監聽文件和啓動編譯 它能夠讀取到 webpack
的 config 信息,整個 Webpack
從啓動到關閉的生命週期,通常只有一個 Compiler 實例,整個生命週期裏暴露了不少方法,常見的 run
,make
,compile
,finish
,seal
,emit
等,咱們寫的插件就是做用在這些暴露方法的 hook 上
Compilation
負責構建編譯。每一次編譯(文件只要發生變化,)就會生成一個 Compilation
實例,Compilation
能夠讀取到當前的模塊資源,編譯生成資源,變化的文件,以及依賴跟蹤等狀態信息。同時也提供不少事件回調給插件進行拓展。
完成編譯
-
輸出資源:(seal 階段)
在編譯完成後,調用 compilation.seal
方法封閉,生成資源,這些資源保存在 compilation.assets
, compilation.chunk
,而後便會調用 emit
鉤子,根據 webpack config
文件的 output
配置的 path
屬性,將文件輸出到指定的 path
.
-
輸出完成: done/failed
階段
done
成功完成一次完成的編譯和輸出流程。failed
編譯失敗,能夠在本事件中獲取到具體的錯誤緣由 在肯定好輸出內容後, 根據配置肯定輸出的路徑和文件名, 把文件內容寫入到文件系統。
emitAssets(compilation, callback) {
const emitFiles = (err) => {
//...省略一系列代碼
// afterEmit:文件已經寫入磁盤完成
this.hooks.afterEmit.callAsync(compilation, (err) => {
if (err) return callback(err)
return callback()
})
}
// emit 事件發生時,能夠讀取到最終輸出的資源、代碼塊、模塊及其依賴,並進行修改(這是最後一次修改最終文件的機會)
this.hooks.emit.callAsync(compilation, (err) => {
if (err) return callback(err)
outputPath = compilation.getPath(this.outputPath, {})
mkdirp(this.outputFileSystem, outputPath, emitFiles)
})
}
而後,咱們來看一下 webpack 打包好的代碼是什麼樣子的。
webpack 輸出文件代碼分析
未壓縮的 bundle.js 文件結構通常以下:
;(function (modules) {
// webpackBootstrap
// 緩存 __webpack_require__ 函數加載過的模塊,提高性能,
var installedModules = {}
/**
* Webpack 加載函數,用來加載 webpack 定義的模塊
* @param {String} moduleId 模塊 ID,通常爲模塊的源碼路徑,如 "./src/index.js"
* @returns {Object} exports 導出對象
*/
function __webpack_require__(moduleId) {
// 重複加載則利用緩存,有則直接從緩存中取得
if (installedModules[moduleId]) {
return installedModules[moduleId].exports
}
// 若是是第一次加載,則初始化模塊對象,並緩存
var module = (installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {},
})
// 把要加載的模塊內容,掛載到module.exports上
modules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
)
module.l = true // 標記爲已加載
// 返回加載的模塊,直接調用便可
return module.exports
}
// 在 __webpack_require__ 函數對象上掛載一些變量及函數 ...
__webpack_require__.m = modules
__webpack_require__.c = installedModules
__webpack_require__.d = function (exports, name, getter) {
if (!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, {
enumerable: true,
get: getter,
})
}
}
__webpack_require__.n = function (module) {
var getter =
module && module.__esModule
? function getDefault() {
return module['default']
}
: function getModuleExports() {
return module
}
__webpack_require__.d(getter, 'a', getter)
return getter
}
// __webpack_require__對象下的r函數
// 在module.exports上定義__esModule爲true,代表是一個模塊對象
__webpack_require__.r = function (exports) {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, {
value: 'Module',
})
}
Object.defineProperty(exports, '__esModule', {
value: true,
})
}
__webpack_require__.o = function (object, property) {
return Object.prototype.hasOwnProperty.call(object, property)
}
// 從入口文件開始執行
return __webpack_require__((__webpack_require__.s = './src/index.js'))
})({
'./src/index.js': function (
module,
__webpack_exports__,
__webpack_require__
) {
'use strict'
eval(
'__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _moduleA__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./moduleA */ "./src/moduleA.js");\n/* harmony import */ var _moduleA__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_moduleA__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var _moduleB__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./moduleB */ "./src/moduleB.js");\n/* harmony import */ var _moduleB__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_moduleB__WEBPACK_IMPORTED_MODULE_1__);\n\n\n\n//# sourceURL=webpack:///./src/index.js?'
)
},
'./src/moduleA.js': function (module, exports) {
eval('console.log("moduleA")\n\n//# sourceURL=webpack:///./src/moduleA.js?')
},
'./src/moduleB.js': function (module, exports) {
// 代碼字符串能夠經過eval 函數運行
eval('console.log("moduleB")\n\n//# sourceURL=webpack:///./src/moduleB.js?')
},
})
上述代碼的實現了⼀個 webpack_require
來實現⾃⼰的模塊化把代碼都緩存在 installedModules
⾥,代碼⽂件以對象傳遞進來,key
是路徑,value
是包裹的代碼字符串,而且代碼內部的 require
,都被替換成了 webpack_require
,代碼字符串能夠經過 eval 函數去執行。
bundle.js
能直接運行在瀏覽器中的緣由在於輸出的文件中經過 webpack_require 函數定義了一個能夠在瀏覽器中執行的加載函數來模擬 Node.js
中的 require
語句。
總結一下,生成的 bundle.js
只包含一個當即調用函數(IIFE),這個函數會接受一個對象爲參數,它其實主要作了兩件事:
-
定義一個模塊加載函數
webpack_require。
-
使用加載函數加載入口模塊
"./src/index.js"
,從入口文件開始遞歸解析依賴,在解析的過程當中,分別對不一樣的模塊進行處理,返回模塊的exports
。
因此咱們只須要實現 2 個功能就能夠實現一個簡單的仿 webpack
打包 js
的編譯工具
-
從入口開始遞歸分析依賴 -
藉助依賴圖譜來生成真正能在瀏覽器上運行的代碼
實現一個簡單的 webpack
接下來從 0 開始實踐一個 Webpack
的雛形,可以讓你們更加深刻了解 Webpack
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser') //解析成ast
const traverse = require('@babel/traverse').default //遍歷ast
const { transformFromAst } = require('@babel/core') //ES6轉換ES5
module.exports = class Webpack {
constructor(options) {
const { entry, output } = options
this.entry = entry
this.output = output
this.modulesArr = []
}
run() {
const info = this.build(this.entry)
this.modulesArr.push(info)
for (let i = 0; i < this.modulesArr.length; i++) {
// 判斷有依賴對象,遞歸解析全部依賴項
const item = this.modulesArr[i]
const { dependencies } = item
if (dependencies) {
for (let j in dependencies) {
this.modulesArr.push(this.build(dependencies[j]))
}
}
}
//數組結構轉換
const obj = {}
this.modulesArr.forEach((item) => {
obj[item.entryFile] = {
dependencies: item.dependencies,
code: item.code,
}
})
this.emitFile(obj)
}
build(entryFile) {
const conts = fs.readFileSync(entryFile, 'utf-8')
const ast = parser.parse(conts, {
sourceType: 'module',
})
// console.log(ast)
// 遍歷全部的 import 模塊,存入dependecies
const dependencies = {}
traverse(ast, {
// 類型爲 ImportDeclaration 的 AST 節點,
// 其實就是咱們的 import xxx from xxxx
ImportDeclaration({ node }) {
const newPath =
'./' + path.join(path.dirname(entryFile), node.source.value)
dependencies[node.source.value] = newPath
// console.log(dependencies)
},
})
// 將轉化後 ast 的代碼從新轉化成代碼
// 並經過配置 @babel/preset-env 預置插件編譯成 es5
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env'],
})
return {
entryFile,
dependencies,
code,
}
}
emitFile(code) {
//生成bundle.js
const filePath = path.join(this.output.path, this.output.filename)
const newCode = JSON.stringify(code)
const bundle = `(function(modules){
// moduleId 爲傳入的 filename ,即模塊的惟一標識符
function require(moduleId){
function localRequire(relativePath){
return require(modules[moduleId].dependencies[relativePath])
}
var exports = {};
(function(require,exports,code){
eval(code)
})(localRequire,exports,modules[moduleId].code)
return exports;
}
require('${this.entry}')
})(${newCode})`
fs.writeFileSync(filePath, bundle, 'utf-8')
}
}
調用
let path = require('path')
let { resolve } = path
let webpackConfig = require(path.resolve('webpack.config.js'))
let Webpack = require('./myWebpack.js')
const defaultConfig = {
entry: 'src/index.js',
output: {
path: resolve(__dirname, '../dist'),
filename: 'bundle.js',
},
}
const config = {
...defaultConfig,
...webpackConfig,
}
const options = require('./webpack.config')
new Webpack(options).run()
輸入到瀏覽器看一下執行結果
參考:
-
快狗打車:實現一個簡單的 webpack
-
tapable
詳解+webpack
流程 -
本文調試以及打斷點用到的源碼
本文分享自微信公衆號 - 前端迷(love_frontend)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。