【webpack源码7】- 生成资源

webpack | 2020-07-09 13:37:13 327次 1次

之前的文章大致上了解了 webpack 的运行然后生成了 hash 后,到这里基本上就是构建资源然后输出到文件,生成我们最终需要的运行文件。执行 createChunkAssets

createChunkAssets() {
    ...
    for (let i = 0; i < this.chunks.length; i++) {
        const chunk = this.chunks[i];
        chunk.files = [];
        let source;
        let file;
        let filenameTemplate;
        try {
            //判断使用模板
            const template = chunk.hasRuntime()
                ? this.mainTemplate //主模块
                : this.chunkTemplate; //比如有动态引入的模块或者不包含runtime部分的
            //获取  render 需要的内容
            const manifest = template.getRenderManifest({
                chunk,
                hash: this.hash,
                fullHash: this.fullHash,
                outputOptions,
                moduleTemplates: this.moduleTemplates,
                dependencyTemplates: this.dependencyTemplates
            }); // [{ render(), filenameTemplate, pathOptions, identifier, hash }]
            ...
        } catch (err) {
            ...
        }
    }
}

首先生成 manifest ,不同的模板方法生成的 manifest 中的每一项的 render 方法是不同的,接下来进行不同的模板方法分析。


一、mainTemplate 

主模块的生成:

mainTemplate.js
getRenderManifest(options) {
    const result = [];
    this.hooks.renderManifest.call(result, options);
    return result;
}

触发一个钩子,在 JavascriptModulesPlugin 插件中去创建相关的 render 方法:

compilation.mainTemplate.hooks.renderManifest.tap(
    "JavascriptModulesPlugin",
    (result, options) => {
        ...
        const useChunkHash = compilation.mainTemplate.useChunkHash(chunk);
        result.push({
            render: () =>
                compilation.mainTemplate.render(
                    hash,
                    chunk,
                    moduleTemplates.javascript,
                    dependencyTemplates
                ),
            filenameTemplate,
            pathOptions: {
                noChunkHash: !useChunkHash,
                contentHashType: "javascript",
                chunk
            },
            identifier: `chunk${chunk.id}`,
            hash: useChunkHash ? chunk.hash : fullHash
        });
        return result;
    }
);

主要看下这个 render 方法,来自 mainTemplate 中:

MainTemplate.js
...
render(hash, chunk, moduleTemplate, dependencyTemplates) {
    // bundle.js 中的runtime部分
    // (function(modules) { ... }) 这里面内容生成
    const buf = this.renderBootstrap(
        hash,
        chunk,
        moduleTemplate,
        dependencyTemplates
    );
    //触发自身中绑定的事件
    let source = this.hooks.render.call(
        new OriginalSource(
            Template.prefix(buf, " \t") + "\n",
            "webpack/bootstrap"
        ),
        chunk,
        hash,
        moduleTemplate,
        dependencyTemplates
    );
    if (chunk.hasEntryModule()) {
        source = this.hooks.renderWithEntry.call(source, chunk, hash);
    }
    ...
    chunk.rendered = true;
    return new ConcatSource(source, ";");
}
...

然后触发在自身的 constructor 中注册事件,生成一个数组,这里面包含最终我们看到 bundle.js 中对应的自执行中的参数部分需要的相关信息,但是这里没有转换,而是将开发的代码原封不动的生成,在最后输出静态文件的时候根据这个数组和源码来生成最终代码,样保证了整个过程中,我们可以追溯对源码做了那些改变,并且在一些 hook 中,我们可以灵活的修改这些操作:

this.hooks.render.tap(
    "MainTemplate",
    (bootstrapSource, chunk, hash, moduleTemplate, dependencyTemplates) => {
        const source = new ConcatSource();
        source.add("/******/ (function(modules) { // webpackBootstrap\n");
        source.add(new PrefixSource("/******/", bootstrapSource));
        source.add("/******/ })\n");
        source.add(
            "/************************************************************************/\n"
        );
        source.add("/******/ (");
        source.add(
            this.hooks.modules.call(
                new RawSource(""),
                chunk,
                hash,
                moduleTemplate,
                dependencyTemplates
            )
        );
        source.add(")");
        return source;
    }
);

如果配置了 optimization runtimeChunk true(默认false),则会继续进行分包,比如 a 文件动态引入了 b,此时不分包,bundle.js 中的自执行参数会引入 a 相关的逻辑,b 会被单独打包。但此时有个问题,修改 a 文件后,由于会触发 bundle 改变,同时 b 文件虽然是分离出来,但是也会影响 hash 改变,所以,需要进行 runtimewebpack 中加载解析模块的逻辑部分,浏览器运行时)分包处理,这样所有的 mainfest (资源清单)的代码都是单独的包(更准确的说应该是 installedChunks 加载好的模块),runtime 也是一个单独的包,为了页面缓存优化而生,避免修改一个文件而导致所有文件 hash 都发生改变。 


二、chunkTemplate

动态导入的模块,同样也是触发 hooks 事件,在 ChunkTemplate

JavascriptModulesPlugin.js 中对应 chunkTemplate,上面的是 mainTemplate

compilation.chunkTemplate.hooks.renderManifest.tap(
    "JavascriptModulesPlugin",
    (result, options) => {
        const chunk = options.chunk;
        const outputOptions = options.outputOptions;
        const moduleTemplates = options.moduleTemplates;
        const dependencyTemplates = options.dependencyTemplates;
        const filenameTemplate =
            chunk.filenameTemplate || outputOptions.chunkFilename;
        //render 方法不同 renderJavascript 
        result.push({
            render: () =>
                this.renderJavascript(
                    compilation.chunkTemplate, // chunk模板
                    chunk,  // 需要生成的 chunk 实例
                    moduleTemplates.javascript, // 模块类型
                    dependencyTemplates // 不同依赖所对应的渲染模板
                ),
            filenameTemplate,
            pathOptions: {
                chunk,
                contentHashType: "javascript"
            },
            identifier: `chunk${chunk.id}`,
            hash: chunk.hash
        });

        return result;
    }
);

这里的 render 方法和上面的不同,分为三个步骤来创建。 

renderJavascript(chunkTemplate, chunk, moduleTemplate, dependencyTemplates) {
    //获取每个 chunk 当中所依赖的所有 module 最终需要渲染的代码
    const moduleSources = Template.renderChunkModules(
        chunk,
        m => typeof m.source === "function",
        moduleTemplate,
        dependencyTemplates
    );
    // 触发 hooks.modules 钩子,用以在最终生成 chunk 代码前对 chunk 最修改
    const core = chunkTemplate.hooks.modules.call(
        moduleSources,
        chunk,
        moduleTemplate,
        dependencyTemplates
    );
    //调用 hooks.render 钩子函数,完成这个 chunk 最终的渲染,即在外层添加包裹函数。
    let source = chunkTemplate.hooks.render.call(
        core,
        chunk,
        moduleTemplate,
        dependencyTemplates
    );
    if (chunk.hasEntryModule()) {
        source = chunkTemplate.hooks.renderWithEntry.call(source, chunk);
    }
    chunk.rendered = true;
    return new ConcatSource(source, ";");
}

进入到 Template.js 中执行它的自身静态方法 renderChunkModules,获取转换后的所有模块:

static renderChunkModules() {
    ...
    const allModules = modules.map(module => {
        return {
            id: module.id,
            source: moduleTemplate.render(module, dependencyTemplates, {
                chunk
            })
        };
    });
    ...
    return source;
}

其中 source 的生成,通过 ModuleTemplate 中的 render 生成,有三种模板方法:

RuntimeTemplate这个模板类主要是提供了和 module 运行时相关的代码输出方法,例如你的 module 使用的是 esModule 类型,那么导出的代码模块会带有__esModule标识,而通过 import 语法引入的外部模块都会通过/* harmony import */注释来进行标识。

dependencyTemplates保存了每个 module 不同依赖的模板,在输出最终代码的时候会通过 dependencyTemplates 来完成模板代码的替换工作。

ModuleTemplate模板类主要是对外暴露了 render 方法,通过调用 moduleTemplate 实例上的 render 方法,即完成每个 module 的代码渲染工作,这也是每个 module 输出最终代码的入口方法。

tag2
render(module, dependencyTemplates, options) {
    try {
        // 资源替换
        const moduleSource = module.source(
            dependencyTemplates,
            this.runtimeTemplate,
            this.type
        );
        //content、module主要是用来对于 module 完成依赖代码替换后的代码处理工作,
        //编写插件时可以通过注册相关的钩子完成对于 module 代码的改造,
        //因为这个时候得到代码还没有在外层包裹 webpack runtime 的代码
        const moduleSourcePostContent = this.hooks.content.call(
            moduleSource,
            module,
            options,
            dependencyTemplates
        );
        const moduleSourcePostModule = this.hooks.module.call(
            moduleSourcePostContent,
            module,
            options,
            dependencyTemplates
        );
        // 添加编译 module 外层包裹的函数
        const moduleSourcePostRender = this.hooks.render.call(
            moduleSourcePostModule,
            module,
            options,
            dependencyTemplates
        );
        return this.hooks.package.call(
            moduleSourcePostRender,
            module,
            options,
            dependencyTemplates
        );
    } catch (e) {
        e.message = `${module.identifier()}\n${e.message}`;
        throw e;
    }
}

首先进入了 module.source,这个方法中执行生成方法:

NormalModule.js
source(dependencyTemplates, runtimeTemplate, type = "javascript") {
    ...
    //生成
    const source = this.generator.generate(
        this,
        dependencyTemplates,
        runtimeTemplate,
        type
    );
    //缓存模块
    const cachedSource = new CachedSource(source);
    this._cachedSources.set(type, {
        source: cachedSource,
        hash: hashDigest
    });
    return cachedSource;
}

这里的 generate 来自 JavascriptGenerator 插件,第五篇中有介绍一个 resolver 通过 hooks.resolver.call 触发生成,这里面最终是触发了一系列操作创建了 generate,方法主要流程如下:

① 生成一个 ReplaceSource 对象,并将源码保存到对象中。包含模块的替换的内容、替换源码的终止位置、 优先级、 替换源码的起始位置等信息,这个对象还会包含一个 replacements 数组和 source 方法(source 方法会在生成最终代码时被调用,根据 replacements 的内容,将源码 _source 中对应位置代码进行替换,从而得到最终代码)。

② 根据每一个依赖对源码做相应的处理,可能是替换某些代码,也可能是插入一些代码。这些对源码转化的操作,将保存在 ReplaceSource 的 replacements 的数组中。

③ 处理 webpack 内部特有的变量

④ 如有有 block ,则对每个 block 做 1-4 的处理(异步加载对应的 import 的内容会被放在 block 中)。

render 中剩余的两个钩子被注册在 FunctionModuleTemplatePlugin 进行一些 webpack 自身的代码包裹和注释添加,这个方法不管哪种模板方式都是会被触发,其中 mainTemplate 情况下会多传递一个 "/******/ " 参数。

回到 renderJavascript 方法中的 hooks.modules 和 hooks.render 方法,主要用来生成最终的 jsonp 格式包裹代码:

(window['webpackJsonp'] = window['webpackJsonp'] || []).push([
  [0]
  // 前面生成的 chunk 代码
 ]);


三、写入文件

    经历了上面所有的阶段之后,所有的最终代码信息已经保存在了 Compilation assets 中。然后代码片段会被拼合起来,并且上一步 generator.generate 得到的 ReplaceSource (webpack-sources中)结果中,会遍历 replacement 中的操作,按照要替换的源码的先后位置(同一位置的话,按照 replacement 中的最后一个参数优先级先后)来一一对源码进行替换,然后代码最终代码。

执行完之后,回到 Compiler 中 run 方法的 onCompiled 回调

//输出构建资源信息
const onCompiled = (err, compilation) => {
    if (err) return finalCallback(err);
    // 不需要发送
    if (this.hooks.shouldEmit.call(compilation) === false) {
        const stats = new Stats(compilation);
        stats.startTime = startTime;
        stats.endTime = Date.now();
        //编译完成钩子
        this.hooks.done.callAsync(stats, err => {
            if (err) return finalCallback(err);
            return finalCallback(null, stats);
        });
        return;
    }
    this.emitAssets(compilation, err => {
        if (err) return finalCallback(err);
        //compilation上的钩子  暂时未知用意
        ...
        //记录输出
        this.emitRecords(err => {
            if (err) return finalCallback(err);

            const stats = new Stats(compilation);
            stats.startTime = startTime;
            stats.endTime = Date.now();
            this.hooks.done.callAsync(stats, err => {
                if (err) return finalCallback(err);
                return finalCallback(null, stats);
            });
        });
    });
};

然后里面进行 emitAssets  调用发布资源,这里面调用了 上一小节中提到的 source 触发( ReplaceSource )生成最终的资源内容:

// 发布资源
emitAssets(compilation, callback) {
    let outputPath;
    // 输出打包结果文件
    const emitFiles = err => {
        if (err) return callback(err);
        // 异步的forEach方法
        asyncLib.forEachLimit(
            compilation.getAssets(),
            15, // 最多同时执行15个异步任务
            ({ name: file, source }, callback) => {
                let targetFile = file;
                const queryStringIdx = targetFile.indexOf("?");
                if (queryStringIdx >= 0) {
                    targetFile = targetFile.substr(0, queryStringIdx);
                }
                // 执行写文件操作
                const writeOut = err => {
                    if (err) return callback(err);
                    // 路径拼接,得到真实路径
                    const targetPath = this.outputFileSystem.join(
                        outputPath,
                        targetFile
                    );
                    // TODO webpack 5 remove futureEmitAssets option and make it on by default
                    if (this.options.output.futureEmitAssets) {
                        // 用于标记目标路径已经被写入的次数,
                        // key是targetPath。每次targetPath被文件写入,其对应的value会自增。
                        const targetFileGeneration = this._assetEmittingWrittenFiles.get(
                            targetPath
                        );
                        // 用于记录资源在不同目标路径被写入的次数。
                        let cacheEntry = this._assetEmittingSourceCache.get(source);
                        if (cacheEntry === undefined) {
                            cacheEntry = {
                                sizeOnlySource: undefined,
                                // 存储资源被写入的目标路径及其次数,对应this._assetEmittingWrittenFiles的格式
                                writtenTo: new Map()
                            };
                            this._assetEmittingSourceCache.set(source, cacheEntry);
                        }

                        // if the target file has already been written
                        if (targetFileGeneration !== undefined) {
                            // check if the Source has been written to this target file
                            const writtenGeneration = cacheEntry.writtenTo.get(targetPath);
                            if (writtenGeneration === targetFileGeneration) {
                                // 如果等式成立,我们跳过写入当前文件,因为它已经被写入过
                                // (我们假设Compiler在running过程中文件不会被删除)
                                compilation.updateAsset(file, cacheEntry.sizeOnlySource, {
                                    size: cacheEntry.sizeOnlySource.size()
                                });

                                return callback();
                            }
                        }

                        // TODO webpack 5: if info.immutable check if file already exists in output
                        // skip emitting if it's already there

                        // get the binary (Buffer) content from the Source
                        /** @type {Buffer} */
                        let content;
                        if (typeof source.buffer === "function") {
                            content = source.buffer();
                        } else {
                            const bufferOrString = source.source();
                            if (Buffer.isBuffer(bufferOrString)) {
                                content = bufferOrString;
                            } else {
                                content = Buffer.from(bufferOrString, "utf8");
                            }
                        }

                        /**
                         * 创建一个source的代替资源,其只有一个size方法返回size属性(sizeOnlySource)
                         * 这步操作是为了让垃圾回收机制能回收由source创建的内存资源
                         */
                        cacheEntry.sizeOnlySource = new SizeOnlySource(content.length);
                        compilation.updateAsset(file, cacheEntry.sizeOnlySource, {
                            size: content.length
                        });

                        // 将content写到目标路径targetPath
                        this.outputFileSystem.writeFile(targetPath, content, err => {
                            if (err) return callback(err);

                            // information marker that the asset has been emitted
                            compilation.emittedAssets.add(file);

                            // 缓存source已经被写入目标路径,写入次数自增
                            const newGeneration =
                                targetFileGeneration === undefined
                                    ? 1
                                    : targetFileGeneration + 1;
                            //将这个自增的值写入cacheEntry.writtenTo和this._assetEmittingWrittenFiles两个Map中
                            cacheEntry.writtenTo.set(targetPath, newGeneration);
                            this._assetEmittingWrittenFiles.set(targetPath, newGeneration);
                            this.hooks.assetEmitted.callAsync(file, content, callback);
                        });
                    } else {
                        //若资源已存在在目标路径 则跳过
                        if (source.existsAt === targetPath) {
                            source.emitted = false;
                            return callback();
                        }
                        // 获取资源内容,这里开始调用之前提到的source方法
                        let content = source.source();

                        if (!Buffer.isBuffer(content)) {
                            content = Buffer.from(content, "utf8");
                        }
                        // 写入目标路径并标记
                        source.existsAt = targetPath;
                        source.emitted = true;
                        this.outputFileSystem.writeFile(targetPath, content, err => {
                            if (err) return callback(err);
                            this.hooks.assetEmitted.callAsync(file, content, callback);
                        });
                    }
                };
                // 若目标文件路径包含/或\,先创建文件夹再写入
                if (targetFile.match(/\/|\\/)) {
                    const dir = path.dirname(targetFile);
                    this.outputFileSystem.mkdirp(
                        this.outputFileSystem.join(outputPath, dir),
                        writeOut
                    );
                } else {
                    writeOut();
                }
            },
            err => {
                ...
            }
        );
    };

    this.hooks.emit.callAsync(compilation, err => {
        if (err) return callback(err);
        outputPath = compilation.getPath(this.outputPath);
        // 递归创建输出目录,并输出资源
        this.outputFileSystem.mkdirp(outputPath, emitFiles);
    });
}

到这里基本上大致梳理了 webpack 从初始到资源生成,当然只是粗糙的分析了流程,里面具体的很深入的细节没有去认真阅读,接下来开始进行生成模板的分析,以及中间使用的其他功能,比如热更新、watchtree-shakingsplitChunk 等功能的分析。

引用:https://www.cnblogs.com/yanze/p/7999550.html
webpack是一个插件架构,所有功能都以插件的形式集成在构建流程中,并通过发布订阅事件来触发各个插件的执行。
核心类是Tapable,用它来实现插件的实例化及挂载。
 
optimist是node的工具库,根据webpack.config.js及shell options生成option,options包含构建需要的重要信息;
    (entry-options)
webpack创建compiler实例,如果options是数组,则创建多个compiler(compiler包含compiler与watching两个对象),
初始化compiler,为compiler添加上下文context和options,初始化基本插件,把options对应的选项进行require;
compiler调用run,run内调用compile方法,开始编译;(make)
compiler内创建compilation对象,并将this传入,compilation就包含了对compiler的引用;
compiler调用addEntry,addEntry调用_addModuleChain();
_addModuleChain查询合适的工厂函数创建模板,并将其加入module链当中,调用buildModule(),对模块进行build(build-module);
buildModule是核心,包括**module.js内调用的build(),build调用doBuild来查找合适的loader,并在回调函数内解析源文件,生成AST,来记录源码间的依赖行为,创建depedency加入依赖数组(期间调用addModuleDepedencies);
module创建完毕;
compilation调用seal(after-compile),添加hash,调用addModule、addChunk(将module装入chunk)对chunk和module开始封装、合并、抽取公共模块,生成编译后的源码
compilation调用createChunkAsset,开始生成最终代码;
createChunkAsset内根据不同的module,调用MainModule.render,chunkTemplate.render进行进一步处理
MainModule.render,chunkTemplate.render内调用moduleTemplate.render
最终生成_webpack_require格式。
moduleTemplate.render调用module.source
module.source将生成好的代码放入compilation.assets中
Compiler.emitAssets将compilation的assets输出到目录中
 
webpack的template有四种子类
MainTemplate.js 生成项目入口文件
ChunkTemplate.js 异步加载的js
ModuleTemplate.js 对所有模块的一个代码生成
HotUpdateChunkTemplate.js 对热替换的处理
 
依赖(dependency)
每一个module都有dependencies字段,这是它的依赖数组,而每一个依赖对象都有一个module字段,指向被依赖的module,
这样module就能找到它依赖的那些module。dependency有许多子类,
如AMDRequireDependency、CommonJsRequireDependency、SystemImportDependency等,分别对应AMD、commonJs、es6等的加载规范。


1人赞

分享到: