webpack 只支持JS模塊,全部其餘類型的模塊,好比圖片,css等,都須要經過對應的loader轉成JS模塊。因此在webpack中不管任何類型的資源,本質上都被當成JS模塊處理。css
Webpack 源碼是一個插件的架構,他的不少功能都是經過諸多的內置插件實現的。node
從配置文件讀取 entry 開始,到最後輸出 bundle.js 的過程,就是主線react
應該關心以下幾點:webpack
webpack 的編譯過程主要有哪些階段?(生命週期)git
webpack 是如何 從 entry 開始解析出整個依賴樹的?github
loaders 是在什麼時候被調用的?web
最終是如何知道要生成幾個文件,以及每一個文件的內容的?json
而其餘一些不重要的問題咱們儘可能忽略,好比如何解析配置,如何處理錯誤,HASH 規則等。數組
編譯源碼分爲如下幾步:promise
根據咱們的webpack配置註冊號對應的插件;
調用 compile.run 進入編譯階段,
在編譯的第一階段是 compilation,他會註冊好不一樣類型的module對應的 factory,否則後面碰到了就不知道如何處理了
進入 make 階段,會從 entry 開始進行兩步操做:
第一步是調用 loaders 對模塊的原始代碼進行編譯,轉換成標準的JS代碼
第二步是調用 acorn 對JS代碼進行語法分析,而後收集其中的依賴關係。每一個模塊都會記錄本身的依賴關係,從而造成一顆關係樹
最後調用 compilation.seal 進入 render 階段,根據以前收集的依賴,決定生成多少文件,每一個文件的內容是什麼
當咱們const webpack = require('webpack')
時:
會從webpack包的package.json
文件的main
字段找到入口文件, 這裏是:
"main": "lib/index.js",
複製代碼
咱們找到lib/index.js
文件: 發現它經過module.exports對外暴露了webpack
及其餘一些方法。
這裏有個有意思的函數:
const exportPlugins = (obj, mappings) => {
for (const name of Object.keys(mappings)) {
Object.defineProperty(obj, name, {
configurable: false,
enumerable: true,
get: mappings[name]
});
}
};
exportPlugins((module.exports.cache = {}), {
MemoryCachePlugin: () => require("./cache/MemoryCachePlugin")
});
複製代碼
經過這個函數, 實現了往module.exports
這個對象上添加屬性, 並設置屬性的configurable
, enumerable
, get
等特性。
咱們順着index.js找到webpack.js
。
const webpack = (options, callback) => {
if (Array.isArray(options)) {
compiler = createMultiCompiler(options);
} else {
compiler = createCompiler(options);
}
if (callback) {}
return compiler;
};
module.exports = webpack;
複製代碼
簡化一下發現webpack是一個方法, 支持兩個參數, callback是一個可選的回調。 options支持數組形式,這裏咱們暫時按只傳一個非數組的options往下走(options就是咱們在項目裏配置的webpack.config.js
文件)
這裏進入 createCompiler
方法, 發現了compiler原來是一個構造函數的實例,
const createCompiler = options => {
options = new WebpackOptionsDefaulter().process(options); //針對webpack的默認設置,主要功能內容都在原型上面
const compiler = new Compiler(options.context); //根據配置的option生成compiler實例, 此時的options.context是process.cwd() 方法返回 Node.js 進程的當前工做目錄。
compiler.options = options;
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
if (Array.isArray(options.plugins)) { ///這裏會解析webpack.config.js的plugins
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
compiler.options = new WebpackOptionsApply().process(options, compiler); ////這裏會解析webpack.config.js的entry
return compiler;
};
複製代碼
咱們進到compiler.js
中來看這個Compiler構造函數。 當咱們執行編譯時, 我從create-react-app
的cli上發現會執行compiler實例的run
方法。
run (callback) { //compiler實例編譯要執行的run方法
this.cache.endIdle(err => {
if (err) return finalCallback(err);
this.hooks.beforeRun.callAsync(this, err => {
if (err) return finalCallback(err);
this.hooks.run.callAsync(this, err => {
if (err) return finalCallback(err);
this.readRecords(err => {
if (err) return finalCallback(err);
this.compile(onCompiled);
});
});
});
});
}
複製代碼
發現這裏出現了hooks
, 在Compiler不難找到hooks在constructor中已經註冊, 來自tapable
這個庫。
beforerun, run都是來自這個庫的AsyncSeriesHook
的實例 compile是來自這個庫的SyncHook
的實例
beforeRun: new AsyncSeriesHook(["compiler"]),
run: new AsyncSeriesHook(["compiler"]),
compile: new SyncHook(["params"]),
複製代碼
爲了下一步的解讀, 咱們首先就要去弄清tapable
這個庫都有些什麼東西。
Webpack爲此專門本身寫一個插件系統,叫 Tapable , 主要提供了註冊和調用插件的功能。
咱們先弄清楚這裏須要用到的 SyncHook
和AsyncSeriesHook
。
SyncHook
爲串行同步執行,不關心事件處理函數的返回值,在觸發事件以後,會按照事件註冊的前後順序執行全部的事件處理函數。
在 tapable 解構的 SyncHook 是一個類,註冊事件需先建立實例,建立實例時支持傳入一個數組,數組內存儲事件觸發時傳入的參數,實例的 tap 方法用於註冊事件,支持傳入兩個參數,第一個參數爲事件名稱,在 Webpack 中通常用於存儲事件對應的插件名稱(名字隨意,只是起到註釋做用), 第二個參數爲事件處理函數,函數參數爲執行 call 方法觸發事件時所傳入的參數的形參。
AsyncSeriesHook 爲異步串行執行,經過 tapAsync 註冊的事件,經過 callAsync 觸發,經過 tapPromise 註冊的事件,經過 promise 觸發,能夠調用 then 方法。
注: 異步串行是指,事件處理函數內三個定時器的異步執行時間分別爲 1s、2s 和 3s,而三個事件處理函數執行完總共用時接近 6s,因此三個事件處理函數執行是須要排隊的,必須一個一個執行,當前事件處理函數執行完才能執行下一個。
AsyncSeriesHook 的 next 執行機制更像 Express 和 Koa 中的中間件,在註冊事件的回調中若是不調用 next,則在觸發事件時會在沒有調用 next 的事件處理函數的位置 「卡死」,即不會繼續執行後面的事件處理函數,只有都調用 next 才能繼續,而最後一個事件處理函數中調用 next 決定是否調用 callAsync 的回調。
在 tapable 源碼中,註冊事件的方法 tab、tapSync、tapPromise 和觸發事件的方法 call、callAsync、promise 都是經過 compile 方法快速編譯出來的,咱們本文中這些方法的實現只是遵守了 tapable 庫這些 「鉤子」 的事件處理機制進行了模擬,以方便咱們瞭解 tapable,爲學習 Webpack 原理作了一個鋪墊,在 Webpack 中,這些 「鉤子」 的真正做用就是將經過配置文件讀取的插件與插件、加載器與加載器之間進行鏈接,「並行」 或 「串行」 執行。
咱們對tapable
的hooks有了必定理解, 繼續往下走:
若是沒有出錯, 咱們會執行下面這個方法
this.compile(onCompiled);
複製代碼
這個compile不是上面的對象實例compile
, 而是在Compiler中定義的compile方法
代碼中的new Compilation(params)
, Compilation
是一個2000多行代碼的構造函數, 獲取到的compilation實例,它就是咱們須要的編譯對象.
compilation 會存儲編譯一個 entry 的全部信息,包括他的依賴,對應的配置等
compile (callback) {
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
if (err) return callback(err);
this.hooks.compile.call(params);
const compilation = this.newCompilation(params); //最主要的在這裏, 獲取compliation實例
const logger = compilation.getLogger("webpack.Compiler");
logger.time("make hook");
this.hooks.make.callAsync(compilation, err => {
logger.timeEnd("make hook");
if (err) return callback(err);
process.nextTick(() => { //將 callback 添加到下一個時間點的隊列。 在 JavaScript 堆棧上的當前操做運行完成以後以及容許事件循環繼續以前,此隊列會被徹底耗盡。
logger.time("finish compilation");
compilation.finish(err => { //下面的finish方法
logger.timeEnd("finish compilation");
if (err) return callback(err);
logger.time("seal compilation");
compilation.seal(err => {
logger.timeEnd("seal compilation");
if (err) return callback(err);
logger.time("afterCompile hook");
this.hooks.afterCompile.callAsync(compilation, err => {
logger.timeEnd("afterCompile hook");
if (err) return callback(err);
return callback(null, compilation);
});
});
});
});
});
});
}
複製代碼
收集依賴
finish(callback) {
const { moduleGraph, modules } = this;
for (const module of modules) {
moduleGraph.finishModule(module);
}
this.hooks.finishModules.callAsync(modules, err => {
if (err) return callback(err);
// extract warnings and errors from modules
for (const module of modules) {
this.reportDependencyErrorsAndWarnings(module, [module]);
const errors = module.getErrors();
if (errors !== undefined) {
if (module.isOptional(this.moduleGraph)) {
for (const error of errors) {
if (!error.module) {
error.module = module;
}
this.warnings.push(error);
}
} else {
for (const error of errors) {
if (!error.module) {
error.module = module;
}
this.errors.push(error);
}
}
}
const warnings = module.getWarnings();
if (warnings !== undefined) {
for (const warning of warnings) {
if (!warning.module) {
warning.module = module;
}
this.warnings.push(warning);
}
}
}
callback();
});
}
複製代碼
把全部依賴的模塊都經過對應的模板 render 出一個拼接好的字符串
seal(callback) {
const chunkGraph = new ChunkGraph(this.moduleGraph);
this.chunkGraph = chunkGraph;
for (const module of this.modules) {
ChunkGraph.setChunkGraphForModule(module, chunkGraph);
}
this.hooks.seal.call();
while (this.hooks.optimizeDependencies.call(this.modules)) {
/* empty */
}
this.hooks.afterOptimizeDependencies.call(this.modules);
this.hooks.beforeChunks.call();
for (const [name, dependencies] of this.entryDependencies) {
const chunk = this.addChunk(name);
chunk.name = name;
const entrypoint = new Entrypoint(name);
entrypoint.setRuntimeChunk(chunk);
this.namedChunkGroups.set(name, entrypoint);
this.entrypoints.set(name, entrypoint);
this.chunkGroups.push(entrypoint);
connectChunkGroupAndChunk(entrypoint, chunk);
for (const dep of dependencies) {
entrypoint.addOrigin(null, { name }, dep.request);
const module = this.moduleGraph.getModule(dep);
if (module) {
chunkGraph.connectChunkAndModule(chunk, module);
chunkGraph.connectChunkAndEntryModule(chunk, module, entrypoint);
this.assignDepth(module);
}
}
}
buildChunkGraph(
this,
/** @type {Entrypoint[]} */ (this.chunkGroups.slice())
);
this.hooks.afterChunks.call(this.chunks);
this.hooks.optimize.call();
while (this.hooks.optimizeModules.call(this.modules)) {
/* empty */
}
this.hooks.afterOptimizeModules.call(this.modules);
while (this.hooks.optimizeChunks.call(this.chunks, this.chunkGroups)) {
/* empty */
}
this.hooks.afterOptimizeChunks.call(this.chunks, this.chunkGroups);
this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => {
if (err) {
return callback(
makeWebpackError(err, "Compilation.hooks.optimizeTree")
);
}
this.hooks.afterOptimizeTree.call(this.chunks, this.modules);
while (this.hooks.optimizeChunkModules.call(this.chunks, this.modules)) {
/* empty */
}
this.hooks.afterOptimizeChunkModules.call(this.chunks, this.modules);
const shouldRecord = this.hooks.shouldRecord.call() !== false;
this.hooks.reviveModules.call(this.modules, this.records);
this.hooks.beforeModuleIds.call(this.modules);
this.hooks.moduleIds.call(this.modules);
this.hooks.optimizeModuleIds.call(this.modules);
this.hooks.afterOptimizeModuleIds.call(this.modules);
this.hooks.reviveChunks.call(this.chunks, this.records);
this.hooks.beforeChunkIds.call(this.chunks);
this.hooks.chunkIds.call(this.chunks);
this.hooks.optimizeChunkIds.call(this.chunks);
this.hooks.afterOptimizeChunkIds.call(this.chunks);
this.sortItemsWithChunkIds();
if (shouldRecord) {
this.hooks.recordModules.call(this.modules, this.records);
this.hooks.recordChunks.call(this.chunks, this.records);
}
this.hooks.optimizeCodeGeneration.call(this.modules);
this.hooks.beforeModuleHash.call();
this.createModuleHashes();
this.hooks.afterModuleHash.call();
this.hooks.beforeCodeGeneration.call();
this.codeGenerationResults = this.codeGeneration();
this.hooks.afterCodeGeneration.call();
this.hooks.beforeRuntimeRequirements.call();
this.processRuntimeRequirements(this.entrypoints.values());
this.hooks.afterRuntimeRequirements.call();
this.hooks.beforeHash.call();
this.createHash();
this.hooks.afterHash.call();
if (shouldRecord) {
this.hooks.recordHash.call(this.records);
}
this.clearAssets();
this.hooks.beforeModuleAssets.call();
this.createModuleAssets();
const cont = () => {
this.hooks.additionalChunkAssets.call(this.chunks);
this.summarizeDependencies();
if (shouldRecord) {
this.hooks.record.call(this, this.records);
}
this.hooks.additionalAssets.callAsync(err => {
if (err) {
return callback(
makeWebpackError(err, "Compilation.hooks.additionalAssets")
);
}
this.hooks.optimizeChunkAssets.callAsync(this.chunks, err => {
if (err) {
return callback(
makeWebpackError(err, "Compilation.hooks.optimizeChunkAssets")
);
}
this.hooks.afterOptimizeChunkAssets.call(this.chunks);
this.hooks.optimizeAssets.callAsync(this.assets, err => {
if (err) {
return callback(
makeWebpackError(err, "Compilation.hooks.optimizeAssets")
);
}
this.hooks.afterOptimizeAssets.call(this.assets);
if (this.hooks.needAdditionalSeal.call()) {
this.unseal();
return this.seal(callback);
}
this.hooks.finishAssets.callAsync(this.assets, err => {
if (err) {
return callback(
makeWebpackError(err, "Compilation.hooks.finishAssets")
);
}
this.hooks.afterFinishAssets.call(this.assets);
this.cache.storeBuildDependencies(
this.buildDependencies,
err => {
if (err) {
return callback(err);
}
return this.hooks.afterSeal.callAsync(callback);
}
);
});
});
});
});
};
if (this.hooks.shouldGenerateChunkAssets.call() !== false) {
this.hooks.beforeChunkAssets.call();
this.createChunkAssets(err => {
if (err) {
return callback(err);
}
cont();
});
} else {
cont();
}
});
}
複製代碼
createNormalModuleFactory () {
const normalModuleFactory = new NormalModuleFactory({
context: this.options.context, //node工做進程的文件目錄
fs: this.inputFileSystem,
resolverFactory: this.resolverFactory,
options: this.options.module || {} //這裏就是webpack.config.js的module對象
});
this.hooks.normalModuleFactory.call(normalModuleFactory);
return normalModuleFactory;
}
createContextModuleFactory () {
const contextModuleFactory = new ContextModuleFactory(this.resolverFactory);
this.hooks.contextModuleFactory.call(contextModuleFactory);
return contextModuleFactory;
}
const params = {
normalModuleFactory: this.createNormalModuleFactory(), //這裏會解析webpack.config.js的loaders, 並經過loaders將它們轉換成js代碼
contextModuleFactory: this.createContextModuleFactory()
};
return params;
複製代碼
const onCompiled = (err, compilation) => {
if (err) return finalCallback(err);
if (this.hooks.shouldEmit.call(compilation) === false) {
const stats = new Stats(compilation);
stats.startTime = startTime;
stats.endTime = Date.now();
this.hooks.done.callAsync(stats, err => {
if (err) return finalCallback(err);
return finalCallback(null, stats);
});
return;
}
process.nextTick(() => {
logger = compilation.getLogger("webpack.Compiler");
logger.time("emitAssets");
this.emitAssets(compilation, err => {
logger.timeEnd("emitAssets");
if (err) return finalCallback(err);
if (compilation.hooks.needAdditionalPass.call()) {
compilation.needAdditionalPass = true;
const stats = new Stats(compilation);
stats.startTime = startTime;
stats.endTime = Date.now();
logger.time("done hook");
this.hooks.done.callAsync(stats, err => {
logger.timeEnd("done hook");
if (err) return finalCallback(err);
this.hooks.additionalPass.callAsync(err => {
if (err) return finalCallback(err);
this.compile(onCompiled); //遞歸調用對下一層進行編譯
});
});
return;
}
logger.time("emitRecords");
this.emitRecords(err => {
logger.timeEnd("emitRecords");
if (err) return finalCallback(err);
const stats = new Stats(compilation);
stats.startTime = startTime;
stats.endTime = Date.now();
logger.time("done hook");
this.hooks.done.callAsync(stats, err => {
logger.timeEnd("done hook");
if (err) return finalCallback(err);
return finalCallback(null, stats);
});
});
});
});
};
複製代碼
大概的理了理webpack編譯的主要思路。 事實上webpack上還有不少其餘的配置, 感受還有不少東西沒搞明白是作什麼用的。
參考: