做者:滴滴公共前端團隊 - 水乙html
咱們在使用 webpack 的時候能夠經過 webpack
這個命令配合一些參數來執行咱們打包編譯的任務。咱們想探究它的源碼,從這個命令入手可以比較容易讓咱們瞭解整個代碼的運行過程。那麼在執行這個命令的時候究竟發生了什麼呢?前端
注:本文中的 webpack 源碼版本爲1.13.3。本文中的源碼分析主要關注的是代碼的總體流程,所以一些我認爲不是很重要的細節都會省略,以使得讀者不要陷入到細節中而 get 不到總體。按照官方文檔,
webpack.config.js
會經過 module.exports 暴露一個對象,下文中咱們統一把這個對象稱爲 webpack 編譯對象(Webpack compiler object)。node
bin/webpack.js
// bin/webpack.js
// 引入 nodejs 的 path 模塊
var path = require ("path") ;
// 獲取 /bin/webpack.js 的絕對路徑
try {
var localWebpack = require.resolve (path.join (process.cwd (), "node_modules", "webpack", "bin", "webpack.js")) ;
if (__filename !== localWebpack) {}
} catch (e) {}
// 引入第三方命令行解析庫 optimist
// 解析 webpack 指令後面追加的與輸出顯示相關的參數(Display options)
var optimist = require ("optimist").usage ((("webpack " + require ("../package.json").version) + "\n") + "Usage: https://webpack.github.io/docs/cli.html") ;
require ("./config-optimist") (optimist) ;
optimist
.boolean ("json").alias ("json", "j").describe ("json")
.boolean ("colors").alias ("colors", "c")... ;
// 獲取解析後的參數並轉換格式
var argv = optimist.argv ;
var options = require ("./convert-argv") (optimist, argv) ;
// 判斷是否符合 argv 裏的參數,並執行該參數的回調
function ifArg (name, fn, init) {...}
// 處理輸出相關(output)的配置參數,並執行編譯函數
function processOptions (options) {...}
// 執行
processOptions (options) ;複製代碼
小結1.1:從上面的分析中咱們能夠比較清晰地看到執行
webpack
命令時會作什麼處理,主要就是解析命令行參數以及執行編譯。其中 processOptions 這個函數是整個/bin/webpack.js
裏的核心函數。下面咱們來仔細看一下這個函數:webpack
function processOptions (options) {
// 支持 Promise 風格的異步回調
if ((typeof options.then) === "function") {...}
// 處理傳入一個 webpack 編譯對象是數組時的狀況
var firstOptions = (Array.isArray (options)) ? options[0]: options;
// 設置輸出 options
var outputOptions = Object.create ((options.stats || firstOptions.stats) || ({}));
// 設置輸出的上下文 context
if ((typeof outputOptions.context) === "undefined") outputOptions.context = firstOptions.context ;
// 處理各類顯示相關的參數,從略
ifArg ("json",
function (bool){...}
);
...
// 引入主入口模塊 lib/webpack.js
var webpack = require ("../lib/webpack.js") ;
// 設置錯誤堆棧追蹤上限
Error.stackTraceLimit = 30 ;
var lastHash = null ;
// 執行編譯
var compiler = webpack (options) ;
// 編譯結束後的回調函數
function compilerCallback (err, stats) {...}
// 是否在編譯完成後繼續 watch 文件變動
if (options.watch) {...}
else
// 執行編譯後的回調函數
compiler.run (compilerCallback) ;
}複製代碼
小結1.2:從 processOptions 中咱們看到,最核心的編譯一步,是使用的入口模塊 lib/webpack.js 暴露處理的方法,因此咱們的數據流接下來要從
bin/webpack.js
來到lib/webpack.js
了,接下來咱們看看lib/webpack.js
裏將會發生什麼。git
lib/webpack.js
中的方法開始編譯// lib/webpack.js
// 引入 Compiler 模塊
var Compiler = require ("./Compiler") ;
// 引入 MultiCompiler 模塊,處理多個 webpack 配置文件的狀況
var MultiCompiler = require ("./MultiCompiler") ;
// 引入 node 環境插件
var NodeEnvironmentPlugin = require ("./node/NodeEnvironmentPlugin") ;
// 引入 WebpackOptionsApply 模塊,應用 webpack 配置文件
var WebpackOptionsApply = require ("./WebpackOptionsApply") ;
// 引入 WebpackOptionsDefaulter 模塊,應用 webpack 默認配置
var WebpackOptionsDefaulter = require ("./WebpackOptionsDefaulter") ;
// 核心函數,也是 ./bin/webpack.js 中引用的核心方法
function webpack (options, callback) {...}
exports = module.exports = webpack ;
// 在 webpack 對象上設置一些經常使用屬性
webpack.WebpackOptionsDefaulter = WebpackOptionsDefaulter ;
webpack.WebpackOptionsApply = WebpackOptionsApply ;
webpack.Compiler = Compiler ;
webpack.MultiCompiler = MultiCompiler ;
webpack.NodeEnvironmentPlugin = NodeEnvironmentPlugin ;
// 暴露一些插件
function exportPlugins (exports, path, plugins) {...}
exportPlugins (exports, ".", ["DefinePlugin", "NormalModuleReplacementPlugin", ...]) ;複製代碼
小結2.1:
lib/webpack.js
文件裏的代碼比較清晰,核心函數就是咱們期待已久的 webpack,咱們在 webpack.config.js 裏面引入的 webpack 模塊就是這個文件,下面咱們再來仔細看看這個函數。github
function webpack (options, callback) {
var compiler ;
if (Array.isArray (options)) {
// 若是傳入了數組類型的 webpack 編譯對象,則實例化一個 MultiCompiler 來處理
compiler = new MultiCompiler (options.map(function (options) {
return webpack (options) ; // 遞歸調用 webpack 函數
})) ;
} else if ((typeof options) === "object") {
// 若是傳入了一個對象類型的 webpack 編譯對象
// 實例化一個 WebpackOptionsDefaulter 來處理默認配置項
new WebpackOptionsDefaulter ().process (options) ;
// 實例化一個 Compiler,Compiler 會繼承一個 Tapable 插件框架
// Compiler 實例化後會繼承到 apply、plugin 等調用和綁定插件的方法
compiler = new Compiler () ;
// 實例化一個 WebpackOptionsApply 來編譯處理 webpack 編譯對象
compiler.options = options ; // 疑惑:爲什麼兩次賦值 compiler.options?
compiler.options = new WebpackOptionsApply ().process (options, compiler) ;
// 應用 node 環境插件
new NodeEnvironmentPlugin ().apply (compiler) ;
compiler.applyPlugins ("environment") ;
compiler.applyPlugins ("after-environment") ;
} else {
// 拋出錯誤
throw new Error ("Invalid argument: options") ;
}
}複製代碼
小結2.2:
webpack
函數裏面有兩個地方值得關注一下。web一是
Compiler
,實例化它會繼承 Tapable ,這個 Tapable 是一個插件框架,經過繼承它的一系列方法來實現註冊和調用插件,咱們能夠看到在 webpack 的源碼中,存在大量的 compiler.apply、compiler.applyPlugins、compiler.plugin 等Tapable方法的調用。Webpack 的 plugin 註冊和調用方式,都是源自 Tapable 。Webpack 經過 plugin 的 apply 方法安裝該 plugin,同時傳入一個 webpack 編譯對象(Webpack compiler object)。json二是
WebpackOptionsApply
的實例方法process (options, compiler)
,這個方法將會針對咱們傳進去的webpack 編譯對象進行逐一編譯,接下來咱們再來仔細看看這個模塊。數組
lib/WebpackOptionsApply.js
模塊的 process
方法來逐一編譯 webpack 編譯對象的各項。// lib/WebpackOptionsApply.js
// ...此處省略一堆依賴引入
// 建立構造器函數 WebpackOptionsApply
function WebpackOptionsApply () {
OptionsApply.call (this) ;
}
// 將構造器暴露
module.exports = WebpackOptionsApply ;
// 修改構造器的原型屬性指向
WebpackOptionsApply.prototype = Object.create (OptionsApply.prototype) ;
// 建立 WebpackOptionsApply 的實例方法 process
WebpackOptionsApply.prototype.process = function (options, compiler) {
// 處理 context 屬性,根目錄
compiler.context = options.context ;
// 處理 plugins 屬性
if (options.plugins && (Array.isArray (options.plugins))) {...}
// 緩存輸入輸出的目錄地址等
compiler.outputPath = options.output.path ;
compiler.recordsInputPath = options.recordsInputPath || options.recordsPath ;
compiler.recordsOutputPath = options.recordsOutputPath || options.recordsPath ;
compiler.name = options.name ;
// 處理 target 屬性,該屬性決定包 (bundle) 應該運行的環境
if ((typeof options.target) === "string") {...}
else if (options.target !== false) {...}
else {...}
// 處理 output.library 屬性,該屬性決定導出庫 (exported library) 的名稱
if (options.output.library || (options.output.libraryTarget !== "var")) {...}
// 處理 externals 屬性,告訴 webpack 不要遵循/打包這些模塊,而是在運行時從環境中請求他們
if (options.externals) {...}
// 處理 hot 屬性,它決定 webpack 瞭如何使用熱替換
if (options.hot) {...}
// 處理 devtool 屬性,它決定了 webpack 的 sourceMap 模式
if (options.devtool && (((options.devtool.indexOf ("sourcemap")) >= 0) || ((options.devtool.indexOf ("source-map")) >= 0))) {...}
else if (options.devtool && ((options.devtool.indexOf ("eval")) >= 0)) {...}
// 如下是安裝並調用各類插件 plugin,因爲功能衆多我的閱歷有限,不能面面俱到
compiler.apply (new EntryOptionPlugin ()) ; // 調用處理入口 entry 的插件
compiler.applyPluginsBailResult ("entry-option", options.context, options.entry) ;
if (options.prefetch) {...}
compiler.apply (new CompatibilityPlugin (),
new LoaderPlugin (), // 調用 loader 的插件
new NodeStuffPlugin (options.node), // 調用 nodejs 環境相關的插件
new RequireJsStuffPlugin (), // 調用 RequireJs 的插件
new APIPlugin (), // 調用變量名的替換,webpack 編譯後的文件裏隨處可見的 __webpack_require__ 變量名就是在此處理
new ConstPlugin (), // 調用一些 if 條件語句、三元運算符等語法相關的插件
new RequireIncludePlugin (), // 調用 require.include 函數的插件
new RequireEnsurePlugin (), // 調用 require.ensure 函數的插件
new RequireContextPlugin(options.resolve.modulesDirectories, options.resolve.extensions),
new AMDPlugin (options.module, options.amd || ({})), // 調用處理符合 AMD 規範的插件
new CommonJsPlugin (options.module)) ; // 調用處理符合 CommonJs 規範的插件
compiler.apply (new RemoveParentModulesPlugin (), // 調用移除父 Modules 的插件
new RemoveEmptyChunksPlugin (), // 調用移除空 chunk 的插件
new MergeDuplicateChunksPlugin (), // 調用合併重複多餘 chunk 的插件
new FlagIncludedChunksPlugin ()) ; //
compiler.apply (new TemplatedPathPlugin ()) ;
compiler.apply (new RecordIdsPlugin ()) ; // 調用記錄 Modules 的 Id 的插件
compiler.apply (new WarnCaseSensitiveModulesPlugin ()) ; // 調用警告大小寫敏感的插件
// 處理 webpack.optimize 屬性下的幾個方法
if (options.optimize && options.optimize.occurenceOrder) {...} // 調用 OccurrenceOrderPlugin 插件
if (options.optimize && options.optimize.minChunkSize) {...} // 調用 MinChunkSizePlugin 插件
if (options.optimize && options.optimize.maxChunks) {...} // 調用 LimitChunkCountPlugin 插件
if (options.optimize.minimize) {...} // 調用 UglifyJsPlugin 插件
// 處理cache屬性(緩存),該屬性在watch的模式下默認開啓緩存
if ((options.cache === undefined) ? options.watch: options.cache) {...}
// 處理 provide 屬性,若是有則調用 ProvidePlugin 插件,這個插件可讓一個 module 賦值爲一個變量,從而能在每一個 module 中以變量名訪問它
if ((typeof options.provide) === "object") {...}
// 處理define屬性,若是有這個屬性則調用 DefinePlugin 插件,這個插件能夠定義全局的常量
if (options.define) {...}
// 處理 defineDebug 屬性,調用並開啓 DefinePlugin 插件的 debug 模式?
if (options.defineDebug !== false) compiler.apply (new DefinePlugin ({...})) ; // 處理定義插件的
// 調用一些編譯完後的處理插件
compiler.applyPlugins ("after-plugins", compiler) ;
compiler.resolvers.normal.apply (new UnsafeCachePlugin (options.resolve.unsafeCache)...) ;
compiler.resolvers.context.apply (new UnsafeCachePlugin (options.resolve.unsafeCache)...) ;
compiler.resolvers.loader.apply (new UnsafeCachePlugin (options.resolve.unsafeCache)...) ;
compiler.applyPlugins ("after-resolvers", compiler) ;
// 最後把處理過的 webpack 編譯對象返回
return options;
};複製代碼
小結3.1:咱們能夠在上面的代碼中看到 webpack 文檔中 Configuration 中介紹的各個屬性,同時看到了這些屬性對應的處理插件都是誰。我我的看完這裏以後,熟悉了好幾個日常不怎麼用到,可是感受仍是頗有用的東西,例如 externals 和 define 屬性。緩存
因爲插件繁多,切每一個插件都有不一樣的細節,咱們這裏選擇一個你們可能比較熟悉的插件
UglifyJsPlugin.js
(壓縮代碼插件)來理解 webpack 的流程。
// lib/optimize/UglifyJsPlugin.js
// 引入一些依賴,主要是與 代碼壓縮、sourceMap 相關
var SourceMapConsumer = require("webpack-core/lib/source-map").SourceMapConsumer;
var SourceMapSource = require("webpack-core/lib/SourceMapSource");
var RawSource = require("webpack-core/lib/RawSource");
var RequestShortener = require("../RequestShortener");
var ModuleFilenameHelpers = require("../ModuleFilenameHelpers");
var uglify = require("uglify-js");
// 定義構造器函數
function UglifyJsPlugin(options) {
...
}
// 將構造器暴露出去
module.exports = UglifyJsPlugin;
// 按照 Tapable 風格編寫插件
UglifyJsPlugin.prototype.apply = function(compiler) {
...
// 編譯器開始編譯
compiler.plugin("compilation", function(compilation) {
...
// 編譯器開始調用 "optimize-chunk-assets" 插件編譯
compilation.plugin("optimize-chunk-assets", function(chunks, callback) {
var files = [];
...
files.forEach(function(file) {
...
try {
var asset = compilation.assets[file];
if(asset.__UglifyJsPlugin) {
compilation.assets[file] = asset.__UglifyJsPlugin;
return;
}
if(options.sourceMap !== false) {
// 須要 sourceMap 時要作的一些操做...
} else {
// 獲取讀取到的源文件
var input = asset.source();
...
}
// base54 編碼重置
uglify.base54.reset();
// 將源文件生成語法樹
var ast = uglify.parse(input, {
filename: file
});
// 語法樹轉換爲壓縮後的代碼
if(options.compress !== false) {
ast.figure_out_scope();
var compress = uglify.Compressor(options.compress); // eslint-disable-line new-cap
ast = ast.transform(compress);
}
// 處理混淆變量名
if(options.mangle !== false) {
ast.figure_out_scope();
ast.compute_char_frequency(options.mangle || {});
ast.mangle_names(options.mangle || {});
if(options.mangle && options.mangle.props) {
uglify.mangle_properties(ast, options.mangle.props);
}
}
// 定義輸出變量名
var output = {};
// 處理輸出的註釋
output.comments = Object.prototype.hasOwnProperty.call(options, "comments") ? options.comments : /^\**!|@preserve|@license/;
// 處理輸出的美化
output.beautify = options.beautify;
for(var k in options.output) {
output[k] = options.output[k];
}
// 處理輸出的 sourceMap
if(options.sourceMap !== false) {
var map = uglify.SourceMap({ // eslint-disable-line new-cap
file: file,
root: ""
});
output.source_map = map; // eslint-disable-line camelcase
}
// 將壓縮後的數據輸出
var stream = uglify.OutputStream(output); // eslint-disable-line new-cap
ast.print(stream);
if(map) map = map + "";
stream = stream + "";
asset.__UglifyJsPlugin = compilation.assets[file] = (map ?
new SourceMapSource(stream, file, JSON.parse(map), input, inputSourceMap) :
new RawSource(stream));
if(warnings.length > 0) {
compilation.warnings.push(new Error(file + " from UglifyJs\n" + warnings.join("\n")));
}
} catch(err) {
// 處理異常
...
} finally {
...
}
});
// 回調函數
callback();
});
compilation.plugin("normal-module-loader", function(context) {
context.minimize = true;
});
});
};複製代碼
小結4.1:從這個插件的源碼分析,咱們能夠基本看到 webpack 編譯時的讀寫過程大體是怎麼樣的:實例化插件(如 UglifyJsPlugin )--> 讀取源文件 --> 編譯並輸出
如今咱們回過頭來再看看總體流程,當咱們在命令行輸入 webpack 命令,按下回車時都發生了什麼:
Compiler
,繼承 Tapable 插件框架,實現註冊和調用一系列插件。/WebpackOptionsApply.js
模塊的 process
方法,使用各類各樣的插件來逐一編譯 webpack 編譯對象的各項。歡迎關注DDFE
GITHUB:github.com/DDFE
微信公衆號:微信搜索公衆號「DDFE」或掃描下面的二維碼