webpack源碼之運行流程

引言

經過前面幾張的鋪墊,下面開始分析webpack源碼核心流程,大致上能夠分爲初始化,編譯,輸出三個階段,下面開始分析webpack

初始化

這個階段總體流程作了什麼? 啓動構建,讀取與合併配置參數,加載 Plugin,實例化 Compiler。

詳細分析

//經過yargs得到shell中的參數
yargs.parse(process.argv.slice(2), (err, argv, output) => {
    //把webpack.config.js中的參數和shell參數整合到options對象上
    let options;
        options = require("./convert-argv")(argv);

    function processOptions(options) {

        const firstOptions = [].concat(options)[0];
        const webpack = require("webpack");

        let compiler;
            //經過webpack方法建立compile對象,Compiler 負責文件監聽和啓動編譯。
            //Compiler 實例中包含了完整的 Webpack 配置,全局只有一個 Compiler 實例。
            compiler = webpack(options);


        if (firstOptions.watch || options.watch) {

            compiler.watch(watchOptions, compilerCallback);
            //啓動一次新的編譯。
        } else compiler.run(compilerCallback);
    }

    processOptions(options);
});

說明 從源碼中摘取了初始化的的第一步,作了簡化,當運行webpack命令的的時候,運行的是webpack-cli下webpack.js,其內容是一個自執行函數,上面是執行的第一步,進行參數的解析合併處理,並建立compiler實例,而後啓動編譯運行run方法,其中關鍵步驟 compiler = webpack(options); 詳細展開以下所示web

const webpack = (options, callback) => {
    //參數合法性校驗
    const webpackOptionsValidationErrors = validateSchema(
        webpackOptionsSchema,
        options
    );

    let compiler;
    if (Array.isArray(options)) {
        compiler = new MultiCompiler(options.map(options => webpack(options)));
    } else if (typeof options === "object") {
        options = new WebpackOptionsDefaulter().process(options);
        //建立compiler對象
        compiler = new Compiler(options.context);
        compiler.options = options;
        new NodeEnvironmentPlugin().apply(compiler);
        //註冊配置文件中的插件,依次調用插件的 apply 方法,讓插件能夠監聽後續的全部事件節點。同時給插件傳入 compiler 實例的引用,以方便插件經過 compiler 調用 Webpack 提供的 API。
        if (options.plugins && Array.isArray(options.plugins)) {
            for (const plugin of options.plugins) {
                plugin.apply(compiler);
            }
        }
        //開始應用 Node.js 風格的文件系統到 compiler 對象,以方便後續的文件尋找和讀取。
        compiler.hooks.environment.call();
        compiler.hooks.afterEnvironment.call();
        //註冊內部插件
        compiler.options = new WebpackOptionsApply().process(options, compiler);
    }

    return compiler;
};

說明 註冊插件過程不在展開,webpack內置插件真的不少啊shell

編譯

這個階段總體流程作了什麼? 從 Entry 發出,針對每一個 Module 串行調用對應的 Loader 去翻譯文件內容,再找到該 Module 依賴的 Module,遞歸地進行編譯處理。

詳細分析

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);
                });
            });
        });

說明 從執行run方法開始,開始執行編譯流程,run方法觸發了before-run、run兩個事件,而後經過readRecords讀取文件,經過compile進行打包,該方法中實例化了一個Compilation類segmentfault

compile(callback) {
        const params = this.newCompilationParams();
        this.hooks.beforeCompile.callAsync(params, err => {
            if (err) return callback(err);

            this.hooks.compile.call(params);
// 每編譯一次都會建立一個compilation對象(好比watch 文件時,一改動就會執行),可是compile只會建立一次
            const compilation = this.newCompilation(params);
// make事件觸發了  事件會觸發SingleEntryPlugin監聽函數,調用compilation.addEntry方法
            this.hooks.make.callAsync(compilation, err => {
                if (err) return callback(err);
                
            });
        });
    }

說明 打包時觸發before-compile、compile、make等事件,同時建立很是重要的compilation對象,內部有聲明瞭不少鉤子,初始化模板等等app

this.hooks = {
    buildModule: new SyncHook(["module"]),
    seal: new SyncHook([]),
    optimize: new SyncHook([]),
};
//拼接最終生成代碼的主模板會用到
this.mainTemplate = new MainTemplate(this.outputOptions);
//拼接最終生成代碼的chunk模板會用到
this.chunkTemplate = new ChunkTemplate(this.outputOptions); 
 //拼接最終生成代碼的熱更新模板會用到
this.hotUpdateChunkTemplate = new HotUpdateChunkTemplate()
//監聽comple的make hooks事件,經過內部的 SingleEntryPlugin 從入口文件開始執行編譯
        compiler.hooks.make.tapAsync(
            "SingleEntryPlugin",
            (compilation, callback) => {
                const { entry, name, context } = this;

                const dep = SingleEntryPlugin.createDependency(entry, name);
                compilation.addEntry(context, dep, name, callback);
            }
        );

說明 監聽compile的make hooks事件,經過內部的 SingleEntryPlugin 從入口文件開始執行編譯,調用compilation.addEntry方法,根據模塊的類型獲取對應的模塊工廠並建立模塊,開始構建模塊async

doBuild(options, compilation, resolver, fs, callback) {
    const loaderContext = this.createLoaderContext(
        resolver,
        options,
        compilation,
        fs
    );
    //調用loader處理模塊
    runLoaders(
        {
            resource: this.resource,
            loaders: this.loaders,
            context: loaderContext,
            readResource: fs.readFile.bind(fs)
        },
        (err, result) => {
           
            
            const resourceBuffer = result.resourceBuffer;
            const source = result.result[0];
            const sourceMap = result.result.length >= 1 ? result.result[1] : null;
            const extraInfo = result.result.length >= 2 ? result.result[2] : null;
            

            this._source = this.createSource(
                this.binary ? asBuffer(source) : asString(source),
                resourceBuffer,
                sourceMap
            );
            //loader處理完以後 獲得_source  而後ast接着處理
            this._ast =
                typeof extraInfo === "object" &&
                extraInfo !== null &&
                extraInfo.webpackAST !== undefined
                    ? extraInfo.webpackAST
                    : null;
            return callback();
        }
    );
}

說明 SingleEntryPlugin這個內存插件主要做用是從entry讀取文件,根據文件類型和配置的 Loader 執行runLoaders,而後將loader處理後的文件經過acorn抽象成抽象語法樹AST,遍歷AST,構建該模塊的全部依賴。函數

輸出

這個階段總體流程作了什麼? 把編譯後的 Module 組合成 Chunk,把 Chunk 轉換成文件,輸出到文件系統。

詳細分析

//全部依賴build完成,開始對chunk進行優化(抽取公共模塊、加hash等)
compilation.seal(err => {
    if (err) return callback(err);

    this.hooks.afterCompile.callAsync(compilation, err => {
        if (err) return callback(err);

        return callback(null, compilation);
    });
});

說明 compilation.seal主要是對chunk進行優化,生成編譯後的源碼,比較重要,詳細展開以下所示工具

//代碼生成前面優化
this.hooks.optimize.call();
this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => {
 
    this.hooks.beforeHash.call();
    this.createHash();
    this.hooks.afterHash.call();

    if (shouldRecord) this.hooks.recordHash.call(this.records);

    this.hooks.beforeModuleAssets.call();
    this.createModuleAssets();
    if (this.hooks.shouldGenerateChunkAssets.call() !== false) {
        this.hooks.beforeChunkAssets.call();
        //生成最終打包輸出的chunk資源,根據template文件,詳細步驟以下所示
        this.createChunkAssets();
    }
    
});
--------------------------------------
//取出最後文件須要的模板
const template = chunk.hasRuntime()
                    ? this.mainTemplate
                    : this.chunkTemplate;
//經過模板最終生成webpack_require格式的內容,他這個是內部封裝的拼接渲染邏輯,也沒用什麼ejs,handlebar等這些模板工具
source = fileManifest.render();
//生成的資源保存在compilation.assets,方便下一步emitAssets步驟中,把文件輸出到硬盤
this.assets[file] = source;
//把處理好的assets輸出到output的path中
    emitAssets(compilation, callback) {
        let outputPath;
    
        const emitFiles = err => {
            if (err) return callback(err);
    
            asyncLib.forEach(
                compilation.assets,
                (source, file, callback) => {
                    const writeOut = err => {
                        //輸出打包後的文件到配置中指定的目錄下
                        this.outputFileSystem.writeFile(targetPath, content, callback);
                    };
    
                    writeOut();
                }
            );
        };
    
        this.hooks.emit.callAsync(compilation, err => {
            if (err) return callback(err);
            outputPath = compilation.getPath(this.outputPath);
            this.outputFileSystem.mkdirp(outputPath, emitFiles);
        });
    }

總結

若是單獨看這篇文章的話,理解起來會比較困難,推薦一下與之相關的系列鋪墊文章,上面是我對webpack源碼運行流程的總結, 整個流程已經跑通了,不過還有蠻多點值得深刻挖掘的。清明在家宅了3天,過得好快,明天公司組織去奧森公園尋寶行動,期待ing 。優化

推薦
webpack源碼之tapable
webpack源碼之plugin機制
webpack源碼之ast簡介
webpack源碼之loader機制ui

參考源碼webpack: "4.4.1"webpack-cli: "2.0.13"

相關文章
相關標籤/搜索