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

玩转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 核心的对象,敬请期待。

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

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏xingoo, 一个梦想做发明家的程序员

JSP中文乱码问题

之前总是碰到JSP页面乱码的问题,每次都是现在网上搜,然后胡乱改,改完也不明白原因。 这次正好作下总结,中文乱码就是因为编码不符,可能出现乱码有四个地方: ...

2339
来自专栏贾老师の博客

常用 Bash Shell 整理

854
来自专栏Jerry的SAP技术分享

微信小程序开发系列四:微信小程序之控制器的初始化逻辑

这个教程的前两篇文章,介绍了如何用下图所示的微信开发者工具自动生成一个Hello World的微信小程序,并讲解了这个自动生成的微信小程序的视图开发原理。

842
来自专栏用户2442861的专栏

深入讲解GCC和Make的区别(有涉及makefile文件哟!!!)

gcc是编译器 而make不是 make是依赖于Makefile来编译多个源文件的工具 在Makefile里同样是用gcc(或者别的编译器)来编译程序.

1592
来自专栏青玉伏案

JavaEE开发之记事本完整案例(SpringBoot + iOS端)

上篇博客我们聊了《JavaEE开发之SpringBoot整合MyBatis以及Thymeleaf模板引擎》,并且在之前我们也聊了《Swift3.0服务端开发(五...

1785
来自专栏DOTNET

ASP.NET MVC编程——路由

框架自动生成的路由配置 ? 上图中,路由配置文件为App_Start文件夹下的RouteConfig.cs。 代码如下: public class RouteC...

34312
来自专栏史上最简单的Spring Cloud教程

SpringBoot非官方教程 | 第十二篇:springboot集成apidoc

首先声明下,apidoc是基于注释来生成文档的,它不基于任何框架,而且支持大多数编程语言,为了springboot系列的完整性,所以标了个题。 一、apidoc...

1977
来自专栏IMWeb前端团队

FIS源码-fisrelease概览

本文作者:IMWeb 陈映平 原文出处:IMWeb社区 未经同意,禁止转载 前面已经已fis server open为例,讲解了FIS的整体架构设计,...

1948
来自专栏SHERlocked93的前端小站

Vue项目数据动态过滤实践

这个问题是在下在做一个Vue项目中遇到的实际场景,这里记录一下我遇到问题之后的思考和最后怎么解决的(老年程序员记性不好 -。-),过程中会涉及到一些Vue源码的...

913
来自专栏逍遥剑客的游戏开发

C#脚本实践(三): 集成到游戏

1522

扫码关注云+社区