第 71 期 - Webpack 结构与源码剖析
logoFRONTALK AI/1月2日 16:32/阅读原文

摘要

本文阐述了 Webpack 在前端开发工具链中的重要性,剖析其结构与源码,包括核心流程、重要对象以及插件和加载器的工作原理,还总结了相关常见问题。

一、Webpack 的重要性及学习必要性

Webpack 在现代前端开发工具链中地位不可替代,具有优秀的灵活性和强大的生态系统。然而随着版本更新,其功能增多、代码量庞大,学习难度提高。很多人对 Webpack 的使用仅停留在配置层面,这会导致在实现功能时不清楚原理,面试时被追问就难以作答等问题。所以深入学习 Webpack 的底层原理和架构设计非常必要。本文将从 JS 打包核心流程、Plugin 的作用与原理、Loader 的作用与原理这三方面构建读者对 Webpack 的完整认知体系,且不会涉及代码分割、按需加载等功能实现。文中涉及大量源码,已做压缩处理并添加注释方便阅读。

二、Webpack 的基本概念

(一)入口(Entry)

Entry是构建的起点,Webpack 从这里开始执行构建,通过Entry配置能确定开始构建的文件,进而识别出应用程序的依赖图谱。例如在webpack.config.js中配置entry: './src/index.js',Webpack 就会从index.js开始分析依赖关系。

// 示例配置
module.exports = {
    entry: './src/index.js'
};

(二)模块(Module)

在 Webpack 视角下,一切文件皆可视为模块,像 JavaScript、CSS、图片等类型的文件都是。Webpack 从Entry出发,递归地构建出包含所有依赖文件的模块网络。

(三)代码块(Chunk)

Chunk是代码的集合体,由模块合并而成,可优化输出文件的结构,支持代码的懒加载、拆分等高级功能。

(四)加载器(Loader)

Loader是模块的转换器,由于 Webpack 本身只理解 JavaScript,Loader使 Webpack 能够处理非 JavaScript 文件。例如将 CSS 转换为 JS 模块或者将高版本 JavaScript 转换为兼容性更好的形式。

// 示例,使用 babel - loader 处理 JavaScript 文件
module.exports = {
    module: {
        rules: [
            {
                test: /.js$/,
                use: 'babel - loader'
            }
        ]
    }
};

(五)插件(Plugin)

Plugin是构建流程的参与者,Webpack 的构建流程中有很多事件钩子(hooks),Plugin可监听这些事件触发,在触发时加入自定义构建行为,如自动压缩打包后的文件、生成 HTML 文件等。

三、JS 打包的核心流程

(一)Webpack 执行入口

Webpack 的执行入口在./lib/webpack.js,核心函数接收webpack配置和可选的回调函数。其中compiler.run(callback)的执行正式开启了 Webpack 的编译过程。

// 核心函数部分代码
const webpack = (
    // 接收 webpack 配置和可选的回调函数
    (options, callback) => {
        // 根据配置创建编译器的简化版函数
        const create = () => {
            let compiler;
            let watch = false;
            let watchOptions;
            return { compiler, watch, watchOptions };
        };
        const { compiler, watch, watchOptions } = create();
        if (watch) {
            compiler.watch(watchOptions, callback);
        } else if (callback) {
            compiler.run(callback);
        }
        return compiler;
    }
);

(二)compiler 和 compilation

  1. Compiler
    • 是 Webpack 的核心,贯穿整个构建周期,封装了 Webpack 环境的全局配置,如配置信息、输出路径等。
    • compiler的创建过程在createCompiler函数中,这个函数标准化配置、应用默认值、创建Compiler实例、注册插件、触发相关钩子等操作。
  2. Compilation
    • 表示单次的构建过程及其产出,每次构建都会新创建,描述构建的具体过程,包括模块资源、编译后的产出资源、文件变化和依赖关系状态等。在watch mode下,文件变化触发重新构建时会生成新的Compilation实例。

(三)编译阶段

  1. compiler.run()的实现
    • ./lib/Compiler.js中的compiler.run()函数实现里,触发了很多钩子,如beforeRunrunafterDone等,核心是run周期中的回调函数this.compile(onCompiled)
  2. compiler.compile()
    • compile函数中,按照beforeCompile - compile - make - finishMake - afterCompile(不完全,还有seal等)的钩子执行顺序进行操作。其中make阶段实现了整个编译过程,相关编译逻辑在EntryPlugin中,而EntryPluginEntryOptionPlugin中被实例化,在WebpackOptionsApply中被引入,在compiler.run()之前就被注册好了。
  3. 添加 Entry
    • EntryPlugin中的compilation.addEntry主要处理Entry,添加过程中会调用addModuleTree(),依据代码依赖关系递归构建模块树。
  4. 添加 Module
    • 涉及addModuleTree()及后续进程,是生成Module的过程。其中addModule函数将模块添加到编译过程,构建内容在addModule回调中的_handleModuleBuildAndDependencies()中执行。在buildModule函数里,会调用构建模块钩子、实际进行模块构建、将构建后的模块存储到缓存等操作。并且build方法存在继承问题,在./lib/NormalModule.jsbuild方法中还有_doBuild(),这里会调用加载器之前的钩子、执行加载器处理流程等。

四、Loader 处理

  1. loader - runner 中的处理逻辑
    • loader - runner包中导入runLoaders作为loader的处理函数。在loader - runner中,通过迭代处理每一个loader,在loadLoader中检验并加载好loader后,在其回调中执行loaderpitch方法完成loader处理。

五、Parse 处理

  1. 解析模块依赖关系
    • Parse主要是对模块代码进行解析,构建出抽象语法树(AST)来描述模块依赖关系。Webpack 会分析代码中的importrequire等语句找出依赖关系构建模块依赖图,这对后续优化操作很重要。Parse的主要作用是处理模块间依赖关系并将关系数据存储在module.dependencies数组中。
    • 借助第三方库acorn实现AST转换,在this.parser.parse函数中完成相关操作。

六、打包封装模块

(一)封装 Chunk

  1. seal 的核心处理过程
    • compilation.seal()中有chunk处理逻辑,其核心处理过程包括创建ChunkGraph、遍历入口点、构建Chunk Graph、生成Chunk Assets等步骤。
    • 关于chunk类型,有Entry Chunks(每个入口点至少生成一个entry chunk,确保入口有对应的chunk包含启动代码,通过entry配置指定)和Async Chunks(使用import()语句导入的模块会被封装到新的async chunk中,实现代码拆分和懒加载)。

(二)通过 emit 将 Assets 输出

  1. createChunkAssets 中的逻辑
    • createChunkAssets函数中,核心处理在renderemitAsset中,render触发对不同资源的处理打出最终资源内容SourceSource被传入emitAsset用来将生成的资源添加到最终输出部分。而最终的输出在compiler.emitAssets中触发,compiler.emitAssetscompiler.runonCompiled中被调用,它会将资源写入文件系统。

七、常见问题总结

(一)Asset 和 Bundle 的区别

  1. Bundle
    • 主要是 JavaScript 文件,也可包含其他类型文件(如通过插件或loader生成的 CSS、HTML)。
  2. Asset
    • 指构建过程中生成的任何类型的文件,包括Bundle本身和其他资源(如图片、字体、样式表等)。

(二)如何手写 Webpack 插件

(三)构建工具的横向对比

(四)阅读源码的方法

  1. 调整状态、心态
    • 要有自驱力,明确读源码的目的;保持好奇心和探究心,对不懂的部分积极探索;要有耐力,耐心分析代码;保持安静,专注阅读。
  2. 掌握工具
    • 可使用debugger调试源码、查阅文档、结合 AI 精简源码、进行文档记录等,还可根据目标源码特点协助阅读,如 Webpack 可先从整体了解其hooks机制。
 

扩展阅读

Made by 捣鼓键盘的小麦 / © 2025 Front Talk 版权所有