[webpack]源碼解讀:命令行輸入webpack的時候都發生了什麼?

作者:滴滴前端公共團隊——鍾宇飛

個人知乎鏈接:水乙 - 知乎

我們在使用 webpack 的時候可以通過 webpack 這個命令配合一些參數來執行我們打包編譯的任務。我們想探究它的源碼,從這個命令入手能夠比較容易讓我們了解整個代碼的運行過程。那麼在執行這個命令的時候究竟發生了什麼呢?

註:本文中的 webpack 源碼版本為1.13.3。本文中的源碼分析主要關注的是代碼的整體流程,因此一些我認為不是很重要的細節都會省略,以使得讀者不要陷入到細節中而 get 不到整體。按照官方文檔,webpack.config.js 會通過 module.exports 暴露一個對象,下文中我們統一把這個對象稱為 webpack 編譯對象(Webpack compiler object)。

Step1:執行腳本 bin/webpack.js

// bin/webpack.jsnn// 引入 nodejs 的 path 模塊nvar path = require ("path") ;nn// 獲取 /bin/webpack.js 的絕對路徑ntry {n var localWebpack = require.resolve (path.join (process.cwd (), "node_modules", "webpack", "bin", "webpack.js")) ;n if (__filename !== localWebpack) {}n} catch (e) {}nn// 引入第三方命令行解析庫 optimistn// 解析 webpack 指令後面追加的與輸出顯示相關的參數(Display options)nvar optimist = require ("optimist").usage ((("webpack " + require ("../package.json").version) + "n") + "Usage: https://webpack.github.io/docs/cli.html") ;nrequire ("./config-optimist") (optimist) ;noptimistn .boolean ("json").alias ("json", "j").describe ("json")n .boolean ("colors").alias ("colors", "c")... ;nn// 獲取解析後的參數並轉換格式nvar argv = optimist.argv ;nvar options = require ("./convert-argv") (optimist, argv) ;nn// 判斷是否符合 argv 里的參數,並執行該參數的回調nfunction ifArg (name, fn, init) {...}nn// 處理輸出相關(output)的配置參數,並執行編譯函數nfunction processOptions (options) {...}n// 執行nprocessOptions (options) ;n

小結1.1:從上面的分析中我們可以比較清晰地看到執行 webpack 命令時會做什麼處理,主要就是解析命令行參數以及執行編譯。其中 processOptions 這個函數是整個 /bin/webpack.js 里的核心函數。下面我們來仔細看一下這個函數:

function processOptions (options) {n // 支持 Promise 風格的非同步回調n if ((typeof options.then) === "function") {...}nn // 處理傳入一個 webpack 編譯對象是數組時的情況n var firstOptions = (Array.isArray (options)) ? options[0]: options;nn // 設置輸出 optionsn var outputOptions = Object.create ((options.stats || firstOptions.stats) || ({}));nn // 設置輸出的上下文 contextn if ((typeof outputOptions.context) === "undefined") outputOptions.context = firstOptions.context ;nn // 處理各種顯示相關的參數,從略n ifArg ("json", n function (bool){...}n );n ...nn // 引入主入口模塊 lib/webpack.jsn var webpack = require ("../lib/webpack.js") ;nn // 設置錯誤堆棧追蹤上限n Error.stackTraceLimit = 30 ;n var lastHash = null ;nn // 執行編譯n var compiler = webpack (options) ;nn // 編譯結束後的回調函數n function compilerCallback (err, stats) {...}nn // 是否在編譯完成後繼續 watch 文件變更n if (options.watch) {...}n else n // 執行編譯後的回調函數n compiler.run (compilerCallback) ;n}n

小結1.2:從 processOptions 中我們看到,最核心的編譯一步,是使用的入口模塊 lib/webpack.js 暴露處理的方法,所以我們的數據流接下來要從 bin/webpack.js 來到 lib/webpack.js 了,接下來我們看看 lib/webpack.js 里將會發生什麼。

step2:執行 lib/webpack.js 中的方法開始編譯

// lib/webpack.jsnn// 引入 Compiler 模塊nvar Compiler = require ("./Compiler") ;nn// 引入 MultiCompiler 模塊,處理多個 webpack 配置文件的情況nvar MultiCompiler = require ("./MultiCompiler") ;nn// 引入 node 環境插件nvar NodeEnvironmentPlugin = require ("./node/NodeEnvironmentPlugin") ;nn// 引入 WebpackOptionsApply 模塊,應用 webpack 配置文件nvar WebpackOptionsApply = require ("./WebpackOptionsApply") ;nn// 引入 WebpackOptionsDefaulter 模塊,應用 webpack 默認配置nvar WebpackOptionsDefaulter = require ("./WebpackOptionsDefaulter") ;nn// 核心函數,也是 ./bin/webpack.js 中引用的核心方法nfunction webpack (options, callback) {...}nexports = module.exports = webpack ;nn// 在 webpack 對象上設置一些常用屬性nwebpack.WebpackOptionsDefaulter = WebpackOptionsDefaulter ;nwebpack.WebpackOptionsApply = WebpackOptionsApply ;nwebpack.Compiler = Compiler ;nwebpack.MultiCompiler = MultiCompiler ;nwebpack.NodeEnvironmentPlugin = NodeEnvironmentPlugin ;nn// 暴露一些插件nfunction exportPlugins (exports, path, plugins) {...}nexportPlugins (exports, ".", ["DefinePlugin", "NormalModuleReplacementPlugin", ...]) ;n

小結2.1:lib/webpack.js 文件里的代碼比較清晰,核心函數就是我們期待已久的 webpack,我們在 webpack.config.js 裡面引入的 webpack 模塊就是這個文件,下面我們再來仔細看看這個函數。

function webpack (options, callback) {n var compiler ;n if (Array.isArray (options)) {n // 如果傳入了數組類型的 webpack 編譯對象,則實例化一個 MultiCompiler 來處理n compiler = new MultiCompiler (options.map(function (options) {n return webpack (options) ; // 遞歸調用 webpack 函數n })) ;n } else if ((typeof options) === "object") {n // 如果傳入了一個對象類型的 webpack 編譯對象n n // 實例化一個 WebpackOptionsDefaulter 來處理默認配置項n new WebpackOptionsDefaulter ().process (options) ;nn // 實例化一個 Compiler,Compiler 會繼承一個 Tapable 插件框架n // Compiler 實例化後會繼承到 apply、plugin 等調用和綁定插件的方法n compiler = new Compiler () ;nn // 實例化一個 WebpackOptionsApply 來編譯處理 webpack 編譯對象n compiler.options = options ; // 疑惑:為何兩次賦值 compiler.options?n compiler.options = new WebpackOptionsApply ().process (options, compiler) ;nn // 應用 node 環境插件n new NodeEnvironmentPlugin ().apply (compiler) ;n compiler.applyPlugins ("environment") ;n compiler.applyPlugins ("after-environment") ;n } else {n // 拋出錯誤n throw new Error ("Invalid argument: options") ;n }n}n

小結2.2:webpack 函數裡面有兩個地方值得關注一下。

一是 Compiler,實例化它會繼承 Tapable ,這個 Tapable 是一個插件框架,通過繼承它的一系列方法來實現註冊和調用插件,我們可以看到在 webpack 的源碼中,存在大量的 compiler.apply、compiler.applyPlugins、compiler.plugin 等Tapable方法的調用。Webpack 的 plugin 註冊和調用方式,都是源自 Tapable 。Webpack 通過 plugin 的 apply 方法安裝該 plugin,同時傳入一個 webpack 編譯對象(Webpack compiler object)。

二是 WebpackOptionsApply 的實例方法 process (options, compiler),這個方法將會針對我們傳進去的webpack 編譯對象進行逐一編譯,接下來我們再來仔細看看這個模塊。

step3:調用 lib/WebpackOptionsApply.js 模塊的 process 方法來逐一編譯 webpack 編譯對象的各項。

// lib/WebpackOptionsApply.jsn// ...此處省略一堆依賴引入nn// 創建構造器函數 WebpackOptionsApplynfunction WebpackOptionsApply () {n OptionsApply.call (this) ;n}nn// 將構造器暴露nmodule.exports = WebpackOptionsApply ;nn// 修改構造器的原型屬性指向nWebpackOptionsApply.prototype = Object.create (OptionsApply.prototype) ;nn// 創建 WebpackOptionsApply 的實例方法 processnWebpackOptionsApply.prototype.process = function (options, compiler) {n // 處理 context 屬性,根目錄n compiler.context = options.context ;n // 處理 plugins 屬性n if (options.plugins && (Array.isArray (options.plugins))) {...}n// 緩存輸入輸出的目錄地址等n compiler.outputPath = options.output.path ;n compiler.recordsInputPath = options.recordsInputPath || options.recordsPath ;n compiler.recordsOutputPath = options.recordsOutputPath || options.recordsPath ;n compiler.name = options.name ;n// 處理 target 屬性,該屬性決定包 (bundle) 應該運行的環境n if ((typeof options.target) === "string") {...}n else if (options.target !== false) {...}n else {...}n // 處理 output.library 屬性,該屬性決定導出庫 (exported library) 的名稱n if (options.output.library || (options.output.libraryTarget !== "var")) {...}n // 處理 externals 屬性,告訴 webpack 不要遵循/打包這些模塊,而是在運行時從環境中請求他們n if (options.externals) {...}n // 處理 hot 屬性,它決定 webpack 了如何使用熱替換n if (options.hot) {...}n// 處理 devtool 屬性,它決定了 webpack 的 sourceMap 模式n if (options.devtool && (((options.devtool.indexOf ("sourcemap")) >= 0) || ((options.devtool.indexOf ("source-map")) >= 0))) {...}n else if (options.devtool && ((options.devtool.indexOf ("eval")) >= 0)) {...}nn// 以下是安裝並調用各種插件 plugin,由於功能眾多個人閱歷有限,不能面面俱到nn compiler.apply (new EntryOptionPlugin ()) ; // 調用處理入口 entry 的插件n compiler.applyPluginsBailResult ("entry-option", options.context, options.entry) ;n if (options.prefetch) {...}n n compiler.apply (new CompatibilityPlugin (),n new LoaderPlugin (), // 調用 loader 的插件n new NodeStuffPlugin (options.node), // 調用 nodejs 環境相關的插件n new RequireJsStuffPlugin (), // 調用 RequireJs 的插件n new APIPlugin (), // 調用變數名的替換,webpack 編譯後的文件里隨處可見的 __webpack_require__ 變數名就是在此處理n new ConstPlugin (), // 調用一些 if 條件語句、三元運算符等語法相關的插件n new RequireIncludePlugin (), // 調用 require.include 函數的插件n new RequireEnsurePlugin (), // 調用 require.ensure 函數的插件n new RequireContextPlugin(options.resolve.modulesDirectories, options.resolve.extensions),n new AMDPlugin (options.module, options.amd || ({})), // 調用處理符合 AMD 規範的插件n new CommonJsPlugin (options.module)) ; // 調用處理符合 CommonJs 規範的插件nn compiler.apply (new RemoveParentModulesPlugin (), // 調用移除父 Modules 的插件n new RemoveEmptyChunksPlugin (), // 調用移除空 chunk 的插件n new MergeDuplicateChunksPlugin (), // 調用合併重複多餘 chunk 的插件n new FlagIncludedChunksPlugin ()) ; // nn compiler.apply (new TemplatedPathPlugin ()) ;n compiler.apply (new RecordIdsPlugin ()) ; // 調用記錄 Modules 的 Id 的插件n compiler.apply (new WarnCaseSensitiveModulesPlugin ()) ; // 調用警告大小寫敏感的插件nn // 處理 webpack.optimize 屬性下的幾個方法n if (options.optimize && options.optimize.occurenceOrder) {...} // 調用 OccurrenceOrderPlugin 插件n if (options.optimize && options.optimize.minChunkSize) {...} // 調用 MinChunkSizePlugin 插件n if (options.optimize && options.optimize.maxChunks) {...} // 調用 LimitChunkCountPlugin 插件n if (options.optimize.minimize) {...} // 調用 UglifyJsPlugin 插件nn // 處理cache屬性(緩存),該屬性在watch的模式下默認開啟緩存n if ((options.cache === undefined) ? options.watch: options.cache) {...}n // 處理 provide 屬性,如果有則調用 ProvidePlugin 插件,這個插件可以讓一個 module 賦值為一個變數,從而能在每個 module 中以變數名訪問它n if ((typeof options.provide) === "object") {...}n // 處理define屬性,如果有這個屬性則調用 DefinePlugin 插件,這個插件可以定義全局的常量n if (options.define) {...}n // 處理 defineDebug 屬性,調用並開啟 DefinePlugin 插件的 debug 模式?n if (options.defineDebug !== false) compiler.apply (new DefinePlugin ({...})) ; // 處理定義插件的n n // 調用一些編譯完後的處理插件n compiler.applyPlugins ("after-plugins", compiler) ;n compiler.resolvers.normal.apply (new UnsafeCachePlugin (options.resolve.unsafeCache)...) ;n compiler.resolvers.context.apply (new UnsafeCachePlugin (options.resolve.unsafeCache)...) ;n compiler.resolvers.loader.apply (new UnsafeCachePlugin (options.resolve.unsafeCache)...) ;n compiler.applyPlugins ("after-resolvers", compiler) ;nn // 最後把處理過的 webpack 編譯對象返回n return options;n};n

小結3.1:我們可以在上面的代碼中看到 webpack 文檔中 Configuration 中介紹的各個屬性,同時看到了這些屬性對應的處理插件都是誰。我個人看完這裡之後,熟悉了好幾個平常不怎麼用到,但是感覺還是很有用的東西,例如 externals 和 define 屬性。

step4:在 step3 中調用的各種插件會按照 webpack 編譯對象的配置來構建出文件

由於插件繁多,切每個插件都有不同的細節,我們這裡選擇一個大家可能比較熟悉的插件 UglifyJsPlugin.js(壓縮代碼插件)來理解 webpack 的流程。

// lib/optimize/UglifyJsPlugin.jsnn// 引入一些依賴,主要是與 sourceMap 相關nvar SourceMapConsumer = require("webpack-core/lib/source-map").SourceMapConsumer;nvar SourceMapSource = require("webpack-core/lib/SourceMapSource");nvar RawSource = require("webpack-core/lib/RawSource");nvar RequestShortener = require("../RequestShortener");nvar ModuleFilenameHelpers = require("../ModuleFilenameHelpers");nvar uglify = require("uglify-js");nn// 定義構造器函數nfunction UglifyJsPlugin(options) {n ...n}n// 將構造器暴露出去nmodule.exports = UglifyJsPlugin;nn// 按照 Tapable 風格編寫插件nUglifyJsPlugin.prototype.apply = function(compiler) {n ...n // 編譯器開始編譯n compiler.plugin("compilation", function(compilation) {n ...n // 編譯器開始調用 "optimize-chunk-assets" 插件編譯n compilation.plugin("optimize-chunk-assets", function(chunks, callback) {n var files = [];n ...n files.forEach(function(file) {n ...n try {n var asset = compilation.assets[file];n if(asset.__UglifyJsPlugin) {n compilation.assets[file] = asset.__UglifyJsPlugin;n return;n }n if(options.sourceMap !== false) {n // 需要 sourceMap 時要做的一些操作...n } else {n // 獲取讀取到的源文件n var input = asset.source(); n ...n }n // base54 編碼重置n uglify.base54.reset(); n // 將源文件生成語法樹n var ast = uglify.parse(input, {n filename: filen });n // 語法樹轉換為壓縮後的代碼n if(options.compress !== false) {n ast.figure_out_scope();n var compress = uglify.Compressor(options.compress); // eslint-disable-line new-capn ast = ast.transform(compress);n }n // 處理混淆變數名n if(options.mangle !== false) {n ast.figure_out_scope();n ast.compute_char_frequency(options.mangle || {});n ast.mangle_names(options.mangle || {});n if(options.mangle && options.mangle.props) {n uglify.mangle_properties(ast, options.mangle.props);n }n }n // 定義輸出變數名n var output = {};n // 處理輸出的注釋n output.comments = Object.prototype.hasOwnProperty.call(options, "comments") ? options.comments : /^**!|@preserve|@license/;n // 處理輸出的美化n output.beautify = options.beautify;n for(var k in options.output) {n output[k] = options.output[k];n }n // 處理輸出的 sourceMapn if(options.sourceMap !== false) {n var map = uglify.SourceMap({ // eslint-disable-line new-capn file: file,n root: ""n });n output.source_map = map; // eslint-disable-line camelcasen }n // 將壓縮後的數據輸出n var stream = uglify.OutputStream(output); // eslint-disable-line new-capn ast.print(stream);n if(map) map = map + "";n stream = stream + "";n asset.__UglifyJsPlugin = compilation.assets[file] = (map ?n new SourceMapSource(stream, file, JSON.parse(map), input, inputSourceMap) :n new RawSource(stream));n if(warnings.length > 0) {n compilation.warnings.push(new Error(file + " from UglifyJsn" + warnings.join("n")));n }n } catch(err) {n // 處理異常n ...n } finally {n ...n }n });n // 回調函數n callback();n });n compilation.plugin("normal-module-loader", function(context) {n context.minimize = true;n });n });n};n

小結4.1:從這個插件的源碼分析,我們可以基本看到 webpack 編譯時的讀寫過程大致是怎麼樣的:實例化插件(如 UglifyJsPlugin )--> 讀取源文件 --> 編譯並輸出

總結

現在我們回過頭來再看看整體流程,當我們在命令行輸入 webpack 命令,按下回車時都發生了什麼: 1. 執行 bin 目錄下的 webpack.js 腳本,解析命令行參數以及開始執行編譯。 2. 調用 lib 目錄下的 webpack.js 文件的核心函數 webpack ,實例化一個 Compiler,繼承 Tapable 插件框架,實現註冊和調用一系列插件。 3. 調用 lib 目錄下的 /WebpackOptionsApply.js 模塊的 process 方法,使用各種各樣的插件來逐一編譯 webpack 編譯對象的各項。 4. 在3中調用的各種插件編譯並輸出新文件。

推薦閱讀:

基於 webpack 的持久化緩存方案
徹底解決Webpack打包性能問題

TAG:前端工程化 | webpack | 源码阅读 |