webpack工作流程分析

目前,幾乎所有業務的開發構建都會用到 webpack 。的確,作爲模塊加載和打包神器,只需配置幾個文件,加載各種 loader 就可以享受無痛流程化開發。但對於 webpack 這樣一個複雜度較高的插件集合,它的整體流程及思想對我們來說還是很透明的。那麼接下來我會帶你瞭解 webpack 這樣一個構建黑盒,首先來談談它的流程。

準備工作

1. webstorm 中配置 webpack-webstorm-debugger-script

在開始瞭解之前,必須要能對 webpack 整個流程進行 debug ,配置過程比較簡單。
先將 webpack-webstorm-debugger-script 中的 webstorm-debugger.js 置於 webpack.config.js 的同一目錄下,搭建好你的腳手架後就可以直接 Debug 這個 webstorm-debugger.js 文件了。

2. webpack.config.js 配置

估計大家對 webpack.config.js 的配置也嘗試過不少次了,這裏就大致對這個配置文件進行個分析。

var path = require('path');
var node_modules = path.resolve(__dirname, 'node_modules');
var pathToReact = path.resolve(node_modules, 'react/dist/react.min.js');

module.exports = {
    // 入口文件,是模塊構建的起點,同時每一個入口文件對應最後生成的一個 chunk。
    entry: {
        bundle: [
            'webpack/hot/dev-server',
            'webpack-dev-server/client?http://localhost:8080',
            path.resolve(__dirname, 'app/app.js')
        ],
    },
    // 文件路徑指向(可加快打包過程)。
    resolve: {
        alias: {
            'react': pathToReact
        }
    },
    // 生成文件,是模塊構建的終點,包括輸出文件與輸出路徑。
    output: {
        path: path.resolve(__dirname, 'build'),
        filename: '[name].js',
    },
    // 這裏配置了處理各模塊的 loader ,包括 css 預處理 loader ,es6 編譯 loader,圖片處理 loader。
    module: {
        loaders: [
            {
                test: /\.js$/,
                loader: 'babel',
                query: {
                    presets: ['es2015', 'react']
                }  
            }
        ],
        noParse: [pathToReact]
    },
    // webpack 各插件對象,在 webpack 的事件流中執行對應的方法。
    plugins: [
        new webpack.HotModuleReplacementPlugin();
    ]
};

除此之外再大致介紹下 webpack 的一些核心概念:
* loader : 能轉換各類資源,並處理成對應模塊的加載器。loader 間可以串行使用。
* chunk : code splitting後的產物,也就是按需加載的分塊,裝載了不同的module。
對於module和chunk的關係可以參照webpack官方的這張圖:
webpack chunk和module
* plugin : webpack 的插件實體,這裏以 UglifyJsPlugin 爲例。

function UglifyJsPlugin(options) {
    this.options = options;
  }
  module.exports = UglifyJsPlugin;
  UglifyJsPlugin.prototype.apply = function(compiler) {
    compiler.plugin("compilation", function(compilation) {
        compilation.plugin("build-module", function(module) {  
        });
        compilation.plugin("optimize-chunk-assets", function(chunks, callback) {
            // Uglify 邏輯
        });
        compilation.plugin("normal-module-loader", function(context) {
        });
    });
  };

在 webpack 中你經常可以看到 compilation.plugin(‘xxx’, callback) ,你可以把它當作是一個事件的綁定,這些事件在打包時由 webpack 來觸發。

3. 流程總覽

在具體流程學習前,可以先通過這幅 webpack整體流程圖 瞭解一下大致流程(建議保存下來查看)。
這裏寫圖片描述

shell 與 config 解析

每次在命令行輸入 webpack 後,操作系統都會去調用 ./node_modules/.bin/webpack 這個 shell 腳本。這個腳本會去調用./node_modules/webpack/bin/webpack.js 並追加輸入的參數,如 -p , -w 。(圖中 webpack.js 是 webpack 的啓動文件,而 $@ 是後綴參數)
這裏寫圖片描述
在 webpack.js 這個文件中 webpack 通過 optimist 將用戶配置的 webpack.config.js 和 shell 腳本傳過來的參數整合成 options 對象傳到了下一個流程的控制對象中。

1. optimist

和 commander 一樣,optimist 實現了 node 命令行的解析,其 API 調用非常方便。

var optimist = require("optimist");
optimist
    .boolean("json").alias("json", "j").describe("json")
    .boolean("colors").alias("colors", "c").describe("colors")
    .boolean("watch").alias("watch", "w").describe("watch")
    ...

獲取到後綴參數後,optimist 分析參數並以鍵值對的形式把參數對象保存在 optimist.argv 中,來看看 argv 究竟有什麼?

// webpack --hot -w
{
  hot: true,
  profile: false,
  watch: true,
  ...
}

2. config 合併與插件加載

在加載插件之前,webpack 將 webpack.config.js 中的各個配置項拷貝到 options 對象中,並加載用戶配置在 webpack.config.js 的 plugins 。接着 optimist.argv 會被傳入到 ./node_modules/webpack/bin/convert-argv.js 中,通過判斷 argv 中參數的值決定是否去加載對應插件。(至於 webpack 插件運行機制,在之後的運行機制篇會提到)

ifBooleanArg("hot", function() {
    ensureArray(options, "plugins");
    var HotModuleReplacementPlugin = require("../lib/HotModuleReplacementPlugin");
    options.plugins.push(new HotModuleReplacementPlugin());
});
...
return options;

options 作爲最後返回結果,包含了之後構建階段所需的重要信息。

{
  entry: {},//入口配置
  output: {}, //輸出配置
  plugins: [], //插件集合(配置文件 + shell指令)
  module: { loaders: [ [Object] ] }, //模塊配置
  context: //工程路徑
  ...
}

這和 webpack.config.js 的配置非常相似,只是多了一些經 shell 傳入的插件對象。插件對象一初始化完畢, options 也就傳入到了下個流程中。

var webpack = require("../lib/webpack.js");
var compiler = webpack(options);

編譯與構建流程

在加載配置文件和 shell 後綴參數申明的插件,並傳入構建信息 options 對象後,開始整個 webpack 打包最漫長的一步。而這個時候,真正的 webpack 對象纔剛被初始化,具體的初始化邏輯在 lib/webpack.js 中,如下:

function webpack(options) {
    var compiler = new Compiler();
    ...// 檢查options,若watch字段爲true,則開啓watch線程
    return compiler;
}
...

webpack 的實際入口是 Compiler 中的 run 方法,run 一旦執行後,就開始了編譯和構建流程 ,其中有幾個比較關鍵的 webpack 事件節點。
* compile 開始編譯
* make 從入口點分析模塊及其依賴的模塊,創建這些模塊對象
* build-module 構建模塊
* after-compile 完成構建
* seal 封裝構建結果
* emit 把各個chunk輸出到結果文件
* after-emit 完成輸出

1. 核心對象 Compilation

compiler.run 後首先會觸發 compile ,這一步會構建出 Compilation 對象:
這裏寫圖片描述
這個對象有兩個作用,一是負責組織整個打包過程,包含了每個構建環節及輸出環節所對應的方法,可以從圖中看到比較關鍵的步驟,如 addEntry() , _addModuleChain() , buildModule() , seal() , createChunkAssets() (在每一個節點都會觸發 webpack 事件去調用各插件)。二是該對象內部存放着所有 module ,chunk,生成的 asset 以及用來生成最後打包文件的 template 的信息。

2. 編譯與構建主流程

在創建 module 之前,Compiler 會觸發 make,並調用 Compilation.addEntry 方法,通過 options 對象的 entry 字段找到我們的入口js文件。之後,在 addEntry 中調用私有方法 _addModuleChain ,這個方法主要做了兩件事情。一是根據模塊的類型獲取對應的模塊工廠並創建模塊,二是構建模塊。

而構建模塊作爲最耗時的一步,又可細化爲三步:
* 調用各 loader 處理模塊之間的依賴
webpack 提供的一個很大的便利就是能將所有資源都整合成模塊,不僅僅是 js 文件。所以需要一些 loader ,比如 url-loader ,jsx-loader , css-loader 等等來讓我們可以直接在源文件中引用各類資源。webpack 調用 doBuild() ,對每一個 require() 用對應的 loader 進行加工,最後生成一個 js module。

Compilation.prototype._addModuleChain = function process(context, dependency, onModule, callback) {
   var start = this.profile && +new Date();
   ...
     // 根據模塊的類型獲取對應的模塊工廠並創建模塊
   var moduleFactory = this.dependencyFactories.get(dependency.constructor);
   ...
   moduleFactory.create(context, dependency, function(err, module) {
       var result = this.addModule(module);
           ...
       this.buildModule(module, function(err) {
       ...
         // 構建模塊,添加依賴模塊
       }.bind(this));
   }.bind(this));
 };
  • 調用 acorn 解析經 loader 處理後的源文件生成抽象語法樹 AST
Parser.prototype.parse = function parse(source, initialState) {
   var ast;
   if(!ast) {
       // acorn以es6的語法進行解析
       ast = acorn.parse(source, {
           ranges: true,
           locations: true,
           ecmaVersion: 6,
           sourceType: "module"
       });
   }
     ...
 };
  • 遍歷 AST,構建該模塊所依賴的模塊
    對於當前模塊,或許存在着多個依賴模塊。當前模塊會開闢一個依賴模塊的數組,在遍歷 AST 時,將 require() 中的模塊通過addDependency() 添加到數組中。當前模塊構建完成後,webpack 調用 processModuleDependencies 開始遞歸處理依賴的 module,接着就會重複之前的構建步驟。
Compilation.prototype.addModuleDependencies = function(module, dependencies, bail, cacheGroup, recursive, callback) {
    // 根據依賴數組(dependencies)創建依賴模塊對象
    var factories = [];
    for(var i = 0; i < dependencies.length; i++) {
        var factory = _this.dependencyFactories.get(dependencies[i][0].constructor);
        factories[i] = [factory, dependencies[i]];
    }
        ...
      // 與當前模塊構建步驟相同
  }

3. 構建細節

module 是 webpack 構建的核心實體,也是所有 module的 父類,它有幾種不同子類:NormalModule , MultiModule ,ContextModule , DelegatedModule 等。但這些核心實體都是在構建中都會去調用對應方法,也就是 build() 。來看看其中具體做了什麼:

// 初始化module信息,如context,id,chunks,dependencies等。
NormalModule.prototype.build = function build(options, compilation, resolver, fs, callback) {
    this.buildTimestamp = new Date().getTime(); // 構建計時
    this.built = true;
    return this.doBuild(options, compilation, resolver, fs, function(err) {
        // 指定模塊引用,不經acorn解析
        if(options.module && options.module.noParse) {
            if(Array.isArray(options.module.noParse)) {
                if(options.module.noParse.some(function(regExp) {
                        return typeof regExp === "string" ?
                            this.request.indexOf(regExp) === 0 :
                            regExp.test(this.request);
                    }, this)) return callback();
            } else if(typeof options.module.noParse === "string" ?
                this.request.indexOf(options.module.noParse) === 0 :
                options.module.noParse.test(this.request)) {
                return callback();
            }
        }
        // 由acorn解析生成ast
        try {
            this.parser.parse(this._source.source(), {
                current: this,
                module: this,
                compilation: compilation,
                options: options
            });
        } catch(e) {
            var source = this._source.source();
            this._source = null;
            return callback(new ModuleParseError(this, source, e));
        }
        return callback();
    }.bind(this));
};

對於每一個 module ,它都會有這樣一個構建方法。當然,它還包括了從構建到輸出的一系列的有關 module 生命週期的函數,我們通過 module 父類類圖其子類類圖(這裏以 NormalModule 爲例)來觀察其真實形態:
這裏寫圖片描述
可以看到無論是構建流程,處理依賴流程,包括後面的封裝流程都是與 module 密切相關的。

打包輸出

在所有模塊及其依賴模塊 build 完成後,webpack 會監聽 seal 事件調用各插件對構建後的結果進行封裝,要逐次對每個 module 和 chunk 進行整理,生成編譯後的源碼,合併,拆分,生成 hash 。 同時這是我們在開發時進行代碼優化和功能添加的關鍵環節。

Compilation.prototype.seal = function seal(callback) {
    this.applyPlugins("seal"); // 觸發插件的seal事件
    this.preparedChunks.sort(function(a, b) {
        if(a.name < b.name) return -1;
        if(a.name > b.name) return 1;
        return 0;
    });
    this.preparedChunks.forEach(function(preparedChunk) {
        var module = preparedChunk.module;
        var chunk = this.addChunk(preparedChunk.name, module);
        chunk.initial = chunk.entry = true;
        // 整理每個Module和chunk,每個chunk對應一個輸出文件。
        chunk.addModule(module);
        module.addChunk(chunk);
    }, this);
    this.applyPluginsAsync("optimize-tree", this.chunks, this.modules, function(err) {
        if(err) {
            return callback(err);
        }
        ... // 觸發插件的事件
        this.createChunkAssets(); // 生成最終assets
        ... // 觸發插件的事件
    }.bind(this));
};

1. 生成最終 assets

在封裝過程中,webpack 會調用 Compilation 中的 createChunkAssets 方法進行打包後代碼的生成。 createChunkAssets 流程如下:
這裏寫圖片描述
* 不同的 Template
從上圖可以看出通過判斷是入口 js 還是需要異步加載的 js 來選擇不同的模板對象進行封裝,入口 js 會採用webpack 事件流的 render 事件來觸發 Template類 中的 renderChunkModules() (異步加載的 js 會調用 chunkTemplate 中的 render 方法)。

if(chunk.entry) {
    source = this.mainTemplate.render(this.hash, chunk, this.moduleTemplate, this.dependencyTemplates);
} else {
    source = this.chunkTemplate.render(chunk, this.moduleTemplate, this.dependencyTemplates);
}

在 webpack 中有四個 Template 的子類,分別是 MainTemplate.js , ChunkTemplate.js ,ModuleTemplate.js ,HotUpdateChunkTemplate.js ,前兩者先前已大致有介紹,而 ModuleTemplate 是對所有模塊進行一個代碼生成,HotUpdateChunkTemplate 是對熱替換模塊的一個處理。
* 模塊封裝
模塊在封裝的時候和它在構建時一樣,都是調用各模塊類中的方法。封裝通過調用 module.source() 來進行各操作,比如說 require() 的替換。

MainTemplate.prototype.requireFn = "__webpack_require__";
 MainTemplate.prototype.render = function(hash, chunk, moduleTemplate, dependencyTemplates) {
   var buf = [];
       // 每一個module都有一個moduleId,在最後會替換。
   buf.push("function " + this.requireFn + "(moduleId) {");
   buf.push(this.indent(this.applyPluginsWaterfall("require", "", chunk, hash)));
   buf.push("}");
   buf.push("");
   ... // 其餘封裝操作
 };
  • 生成 assets
    各模塊進行 doBlock 後,把 module 的最終代碼循環添加到 source 中。一個 source 對應着一個 asset 對象,該對象保存了單個文件的文件名( name )和最終代碼( value )。

2. 輸出

最後一步,webpack 調用 Compiler 中的 emitAssets() ,按照 output 中的配置項將文件輸出到了對應的 path 中,從而 webpack 整個打包過程結束。要注意的是,若想對結果進行處理,則需要在 emit 觸發後對自定義插件進行擴展。

總結

webpack 的整體流程主要還是依賴於 compilation 和 module 這兩個對象,但其思想遠不止這麼簡單。最開始也說過,webpack 本質是個插件集合,並且由 tapable 控制各插件在 webpack 事件流上運行,至於具體的思想和細節,將會在後一篇文章中提到。同時,在業務開發中,無論是爲了提升構建效率,或是減小打包文件大小,我們都可以通過編寫 webpack 插件來進行流程上的控制,這個也會在之後提到。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章