2

欢迎大家前往腾讯云社区,获取更多腾讯海量技术实践干货哦~

作者:QQ会员技术团队

玩转webpack(一)上篇:webpack的基本架构和构建流程

文件生成阶段

这个阶段的主要内容,是根据 chunks 生成最终文件。主要有三个步骤:模板 hash 更新,模板渲染 chunk,生成文件

Compilation 在实例化的时候,就会同时实例化三个对象:MainTemplate, ChunkTemplateModuleTemplate。这三个对象是用来渲染 chunk 对象,得到最终代码的模板。第一个对应了在 entry 配置的入口 chunk 的渲染模板,第二个是动态引入的非入口 chunk 的渲染模板,最后是 chunk 中的 module 的渲染模板。

在开始渲染之前,Compilation 实例会调用 createHash 方法来生成这次构建的 hash。在 webpack 的配置中,我们可以在 output.filename 中配置 [hash] 占位符,最终就会替换成这个 hash。同样,createHash 也会为每一个 chunk 也创建一个 hash,对应 output.filename[chunkhash] 占位符。

每个 hash 的影响因素比较多,首先三个模板对象会调用 updateHash 方法来更新 hash,在内部还会触发任务点 hash,传递 hash 到其他插件。 chunkhash 也是类似的原理:

// https://github.com/webpack/webpack/blob/master/lib/Compilation.js

class Compilation extends Tapable {
    // 其他代码..
    createHash() {
        // 其他代码..
        const hash = crypto.createHash(hashFunction);
        if(outputOptions.hashSalt)
        hash.update(outputOptions.hashSalt);
        this.mainTemplate.updateHash(hash);
        this.chunkTemplate.updateHash(hash);
        this.moduleTemplate.updateHash(hash);
        // 其他代码..
        for(let i = 0; i < chunks.length; i++) {
            const chunk = chunks[i];
            const chunkHash = crypto.createHash(hashFunction);
            if(outputOptions.hashSalt)
            chunkHash.update(outputOptions.hashSalt);
            chunk.updateHash(chunkHash);
            if(chunk.hasRuntime()) {
                this.mainTemplate.updateHashForChunk(chunkHash, chunk);
            } else {
                this.chunkTemplate.updateHashForChunk(chunkHash, chunk);
            }
            this.applyPlugins2("chunk-hash", chunk, chunkHash);
            chunk.hash = chunkHash.digest(hashDigest);
            hash.update(chunk.hash);
            chunk.renderedHash = chunk.hash.substr(0, hashDigestLength);
        }
        this.fullHash = hash.digest(hashDigest);
        this.hash = this.fullHash.substr(0, hashDigestLength);
    }
}

当 hash 都创建完成之后,下一步就会遍历 compilation.chunks 来渲染每一个 chunk。如果一个 chunk 是入口 chunk,那么就会调用 MainTemplate 实例的 render 方法,否则调用 ChunkTemplate 的 render 方法:

// https://github.com/webpack/webpack/blob/master/lib/Compilation.js

class Compilation extends Tapable {
    // 其他代码..
    createChunkAssets() {
        // 其他代码..
        for(let i = 0; i < this.chunks.length; i++) {
            const chunk = this.chunks[i];
            // 其他代码..
            if(chunk.hasRuntime()) {
                source = this.mainTemplate.render(this.hash, chunk, this.moduleTemplate, this.dependencyTemplates);
            } else {
                source = this.chunkTemplate.render(chunk, this.moduleTemplate, this.dependencyTemplates);
            }
            file = this.getPath(filenameTemplate, {
                noChunkHash: !useChunkHash,
                chunk
            });
            this.assets[file] = source;
            // 其他代码..
        }
    }
}

这里注意到 ModuleTemplate 实例会被传递下去,在实际渲染时将会用 ModuleTemplate 来渲染每一个 module,其实更多是往 module 前后添加一些"包装"代码,因为 module 的源码实际上是已经渲染完毕的(还记得前面的 loaders 应用吗?)。

MainTemplate 的渲染跟 ChunkTemplate 的不同点在于,入口 chunk 的源码中会带有启动 webpack 的代码,而非入口 chunk 的源码是不需要的。这个只要查看 webpack 构建后的文件就可以比较清楚地看到区别:

// 入口 chunk
/******/ (function(modules) { // webpackBootstrap
/******/     // install a JSONP callback for chunk loading
/******/     var parentJsonpFunction = window["webpackJsonp"];
/******/     window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
/******/         // add "moreModules" to the modules object,
/******/         // then flag all "chunkIds" as loaded and fire callback
/******/         var moduleId, chunkId, i = 0, resolves = [], result;
/******/         for(;i < chunkIds.length; i++) {
/******/             chunkId = chunkIds[i];
/******/             if(installedChunks[chunkId]) {
/******/                 resolves.push(installedChunks[chunkId][0]);
/******/             }
/******/             installedChunks[chunkId] = 0;
/******/         }
/******/         for(moduleId in moreModules) {
/******/             if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
/******/                 modules[moduleId] = moreModules[moduleId];
/******/             }
/******/         }
/******/         if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules);
/******/         while(resolves.length) {
/******/             resolves.shift()();
/******/         }
/******/         
/******/     };
/******/     // 其他代码..
/******/ })(/* modules代码 */);

// 动态引入的 chunk
webpackJsonp([0],[
    /* modules代码.. */
]);

当每个 chunk 的源码生成之后,就会添加在 Compilation 实例的 assets 属性中。

assets 对象的 key 是最终要生成的文件名称,因此这里要用到前面创建的 hash。调用 Compilation 实例内部的 getPath 方法会根据配置中的 output.filename 来生成文件名称。

assets 对象的 value 是一个对象,对象需要包含两个方法,sourcesize 分别返回文件内容和文件大小。

当所有的 chunk 都渲染完成之后,assets 就是最终更要生成的文件列表。此时 Compilation 实例还会触发几个任务点,例如 addtional-chunk-assetsaddintial-assets等,在这些任务点可以修改 assets 属性来改变最终要生成的文件。

完成上面的操作之后,Compilation 实例的 seal 方法结束,进入到 Compiler 实例的 emitAssets 方法。Compilation 实例的所有工作到此也全部结束,意味着一次构建过程已经结束,接下来只有文件生成的步骤。

Compiler 实例开始生成文件前,最后一个修改最终文件生成的任务点 emit 会被触发:

// 监听 emit 任务点,修改最终文件的最后机会
compiler.plugin("emit", (compilation, callback) => {
    let data = "abcd"
    compilation.assets["newFile.js"] = {
        source() {
            return data
        }
        size() {
            return data.length
        }
    }
})

当任务点 emit 被触发之后,接下来 webpack 会直接遍历 compilation.assets 生成所有文件,然后触发任务点 done,结束构建流程。

总结

经过全文的讨论,我们将 webpack 的基本架构以及核心的构建流程都过了一遍,希望在阅读完全文之后,对大家了解 webpack 原理有所帮助。
最后再次说明,本文内容是由个人理解和整理,如果有不正确的地方欢迎大家指正。如果需要转载,请注明出处。

下一篇文章将会讲解 webpack 核心的对象,敬请期待。

本文来源于 小时光茶社 微信公众号

相关阅读

玩转webpack(一)上篇:webpack的基本架构和构建流程
Webpack + vue 之抽离 CSS 的正确姿势
使用Yeoman generator来规范工程的初始化

此文已由作者授权腾讯云技术社区发布,转载请注明原文出处
原文链接:https://cloud.tencent.com/com...


腾讯云开发者
21.9k 声望17.3k 粉丝