webpack源碼分析(一)-流程分析

先上一張流程圖
clipboard.png
通常webpack打包文件是經過cli調用node

webpack.js --config=webpack.build.js

這實際上等同於經過node調用webpack

const Webpack = require('./node_modules/webpack');
const config = require('./config1.js');
const compiler = Webpack(config);
compiler.run();

Webpack(config)源碼以下:git

const webpack = (options, callback) => {
    //將用戶本地的配置文件拼接上webpack內置的參數
    options = new WebpackOptionsDefaulter().process(options);
    //初始化compiler對象(webpack編輯器對象,包含全部webpack主環境相關內容)
    compiler = new Compiler(options.context);
    compiler.options = options;
    //註冊NodeEnvironmentPlugin插件和用戶配置的插件
    new NodeEnvironmentPlugin().apply(compiler);
    if (options.plugins && Array.isArray(options.plugins)) {
        for (const plugin of options.plugins) {
            plugin.apply(compiler);
        }
    }
    //觸發environment和afterEnvironment上註冊的事件
    compiler.hooks.environment.call();
    compiler.hooks.afterEnvironment.call();
    //註冊webpack內置插件,源碼以下
    compiler.options = new WebpackOptionsApply().process(options, compiler);
    return compiler;
})

class WebpackOptionsApply extends OptionsApply {
    process(options, compiler) {
        //註冊EntryOptionPlugin
        new EntryOptionPlugin().apply(compiler);
        //觸發entryOption鉤子
        var a = compiler.hooks.entryOption.call(options.context, options.entry);
        //觸發afterPlugins鉤子
        compiler.hooks.afterPlugins.call(compiler);
        //觸發afterResolvers鉤子
        compiler.hooks.afterResolvers.call(compiler);
    }
}

主要是初始化compiler對象和註冊插件,下面介紹下EntryOptionPlugin插件github

EntryOptionPlugin.apply方法
apply(compiler) {
    //將回調函數註冊到hooks.entryOption上
    //上文調用compiler.hooks.entryOption.call(options.context, options.entry)時觸發
    compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
        //取出entry文件入口配置,判斷是否數組,調用對應的插件
        for (const name of Object.keys(entry)) {
            itemToPlugin(context, entry[name], name).apply(compiler);
        }
    }
}
const itemToPlugin = (context, item, name) => {
    if (Array.isArray(item)) {
        return new MultiEntryPlugin(context, item, name);
    }
    return new SingleEntryPlugin(context, item, name);
}
//本文介紹entry[name]爲字符串的狀況,調用new SingleEntryPlugin().apply方法,源碼以下
apply(compiler) {
    //在compilation鉤子上註冊回調,compilation.call時觸發
    compiler.hooks.compilation.tap(
        "SingleEntryPlugin",
        (compilation, { normalModuleFactory }) => {
            //設置SingleEntryDependency使用normalModuleFactory建立Module
            compilation.dependencyFactories.set(
                SingleEntryDependency,
                normalModuleFactory
            );
        }
    );
    compiler.hooks.make.tapAsync(
        "SingleEntryPlugin",
        (compilation, callback) => {
            const { entry, name, context } = this;

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

通過上一步的分析能夠對webpack的插件機制有必定的瞭解,插件主要是掛載一些回調函數在compiler的生命週期上,當執行到該階段時觸發(事件的發佈訂閱,繼承自tapable)。
compiler的生命週期可參考:webpack hooks,下面再看下compiler.run()方法web

run(callback) {
    this.compile(onCompiled);
}

compile(callback) {
    //初始化compilation,compilation對象表明了一次單一的版本構建和生成資源過程
    const compilation = this.newCompilation(params);
    // 觸發註冊在make上的事件函數,
    this.hooks.make.callAsync(compilation, err => {
        //make上註冊的事件執行完畢後觸發回調,源碼後面給出
    }
}
//觸發上文提到的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);
    }
);

addEntry(context, entry, name, callback) {
        this._addModuleChain(context, dep, ...)
}

_addModuleChain(context, dependency, onModule, callback) {
    //獲取dependency
    const Dep = /** @type {DepConstructor} */ (dependency.constructor);
    //獲取moduleFactory,根據上文的介紹此處是normalModuleFactory
    const moduleFactory = this.dependencyFactories.get(Dep);
    //獲取module
    moduleFactory.create((err, module) => {
        dependency.module = module;
        this.buildModule(module, false, null, null, err => {
            //初始化moudle後生成ast對象,計算依賴,後面介紹
        })
    )
}
//獲取module的實現
//normalModuleFactory.create
create(data, callback) {
    // 獲取在constructor中註冊的factory方法
    const factory = this.hooks.factory.call(null);
    factory(result, (err, module) => {})
}

class NormalModuleFactory extends Tapable {
    constructor(context, resolverFactory, options) {
        this.hooks.factory.tap("NormalModuleFactory", () => (result, callback) => {
            //返回初始的module對象
            callback(null, {
                context: context,
                request: loaders
                    .map(loaderToIdent)
                    .concat([resource])
                    .join("!"),
                dependencies: data.dependencies,
                ...
            });
        }
    }
}

buildModule回調api

this.buildModule(module, false, null, null, err => {
    // 根據js代碼獲取ast語法樹對象
    ast = acorn.parse(code, parserOptions);
    // 根據ast加載模塊的依賴
    this.prewalkStatements(ast.body);
    this.walkStatements(ast.body);

make主要是以entry爲入口,生成一個modoule對象,其中的關鍵是根據js代碼生成ast語法樹對象,同時分析語法樹加載須要使用到的依賴(dependency),若是存在import依賴,就會生成新的modoule,知道全部依賴加在完畢,下圖是部分dependency示例
clipboard.png
clipboard.png數組

make階段完成以後會進入seal階段app

this.hooks.make.callAsync(compilation, err => {
    compilation.seal(err => {})
})
seal() {
    for (const preparedEntrypoint of this._preparedEntrypoints) {
        const module = preparedEntrypoint.module;
        const name = preparedEntrypoint.name;
        const chunk = this.addChunk(name);
        chunk.entryModule = module;
    }
    this.createChunkAssets();
}
createChunkAssets(){
   const manifest = template.getRenderManifest({
        chunk,
        hash: this.hash,
        fullHash: this.fullHash,
        outputOptions,
        moduleTemplates: this.moduleTemplates,
        dependencyTemplates: this.dependencyTemplates
    }); // [{ render(), filenameTemplate, pathOptions, identifier, hash }]
    for (const fileManifest of manifest) {
        source = fileManifest.render();
    }
}

compile結束後調用compiler.emitAssets編輯器

emitAssets() {
    const targetPath = this.outputFileSystem.join(
        outputPath,
        targetFile
    );
    let content = source.source();
    //this.writeFile = fs.writeFile.bind(fs);
    this.outputFileSystem.writeFile(targetPath, content, callback);
}
相關文章
相關標籤/搜索