资讯专栏INFORMATION COLUMN

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

codecraft / 423人阅读

摘要:先上一张流程图一般打包文件是通过调用这实际上等同于通过调用源码如下将用户本地的配置文件拼接上内置的参数初始化对象编辑器对象,包含所有主环境相关内容注册插件和用户配置的插件触发和上注册的事件注册内置插件源码如下注册触发钩子触发钩子触发钩子

先上一张流程图

一般webpack打包文件是通过cli调用

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

这实际上等同于通过node调用

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

Webpack(config)源码如下:

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插件

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()方法

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回调

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示例

make阶段完成之后会进入seal阶段

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/96681.html

相关文章

  • webpack源码之运行流程

    摘要:引言通过前面几张的铺垫下面开始分析源码核心流程大体上可以分为初始化编译输出三个阶段下面开始分析初始化这个阶段整体流程做了什么启动构建,读取与合并配置参数,加载,实例化。推荐源码之源码之机制源码之简介源码之机制参考源码 引言 通过前面几张的铺垫,下面开始分析webpack源码核心流程,大体上可以分为初始化,编译,输出三个阶段,下面开始分析 初始化 这个阶段整体流程做了什么? 启动构建,读...

    kviccn 评论0 收藏0
  • Webpack 源码(二)—— 如何阅读源码

    摘要:正所谓四两拨千斤,找对要分析的对象以及它的关系网,就找到了正确的分析源码的方法下面的是我的公众号二维码图片,欢迎关注。 1、如何调试阅读源码 如果想要了解 Webpack 的流程,只要阅读 @七珏 细说 webpack 之流程篇 所述的内容就够了,讲解地比较全面了;本文就不对 Webpack 流程再做重复的描述,而是从另外一个角度补充分析 Webpack 源码; Webpack 中最为...

    wing324 评论0 收藏0
  • 用十分之的构建时间做场页面静态资源依赖分析

    摘要:不直接使用的原因很简单首先构建一次实在太慢了,特别是有几十个页面存在的情况下,另一个原因是我只是想拿到资源依赖,我根本不想对整个前端进行一次构建,也不想生成任何。这就达到了本文题目中目的,用十分之一的构建时间做一场页面静态资源依赖分析。原文链接 作者:梯田 前言: 所谓【静态资源依赖分析】,指的是可以通过分析页面资源后,可以以 json 数据或者图表的方式拿到页面资源间的依赖关系。 比如 c...

    B0B0 评论0 收藏0
  • webpack源码分析之四:plugin

    摘要:流程划分纵观整个打包过程,可以流程划分为四块。核心类关系图功能实现模块通过将源码解析为树并拆分,以及直至基础模块。通过的依赖和切割文件构建出含有和包含关系的对象。通过模版完成主入口文件的写入,模版完成切割文件的写入。 前言 插件plugin,webpack重要的组成部分。它以事件流的方式让用户可以直接接触到webpack的整个编译过程。plugin在编译的关键地方触发对应的事件,极大的...

    yhaolpz 评论0 收藏0

发表评论

0条评论

codecraft

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<