webpack 格式化模块 第 二 节

一、getCompilationHooks(compilation) 静态方法

用于为某个 Compilation 实例附加或获取一组 模块构建过程的钩子NormalModuleCompilationHooks):

  • 参数校验 :如果不是 Compilation 实例,抛出类型错误。

  • 缓存机制 :利用 WeakMap 存储和获取绑定在 compilation 上的钩子。

  • 注册钩子

    • loader:同步钩子,用于设置 loaderContext
    • beforeLoaders:同步钩子,在加载器设置前调用。
    • beforeParsebeforeSnapshot:解析/快照前调用。
    • readResourceForScheme:适配旧的资源读取方式,为兼容性设计(即将弃用)。
    • readResource:根据 URL scheme 注册的资源读取钩子。
    • needBuild:异步钩子,用于判断模块是否需要重新构建。

二、构造函数 constructor({...})

负责初始化一个 NormalModule 实例。接收构建时从 NormalModuleFactory 提供的参数:

模块来源信息(从工厂获得):

  • requestuserRequestrawRequest:模块请求路径的不同形式。
  • type:模块类型(如 javascript/auto)。
  • binary:是否为二进制模块(如 asset/webassembly)。
  • parsergenerator:用于解析与生成模块代码的实例。
  • resource:资源路径。
  • matchResource:匹配用资源路径。
  • loaders:对应的 loader 列表。
  • resolveOptions:模块的解析选项。

构建相关的缓存和状态信息:

  • this._source:模块源代码。
  • this._sourceSizes:源代码大小缓存。
  • this._sourceTypes:源类型缓存。
  • _forceBuild:是否强制构建。
  • _codeGeneratorData:代码生成相关的数据缓存。
  • _lastSuccessfulBuildMeta:记录上一次成功构建的元数据。
  • _isEvaluatingSideEffects:是否正在评估副作用。

三、核心方法

identifier()

返回模块的唯一标识符(作为缓存键使用):

  • 格式:type|requesttype|request|layer

readableIdentifier(requestShortener)

返回模块的可读标识符,通常用于日志和调试信息。

libIdent(options)

生成用于 library export 的标识符,常用于 DllPlugin 或模块缓存。

nameForCondition()

返回模块用于条件匹配的路径(主要是用于配置中 test/resource/include 等字段的匹配判断)。


四、缓存相关方法

updateCacheModule(module)

当模块来自缓存时更新其引用属性(如 loader、parser、request 等),以同步当前实例的状态。

cleanupForCache()

清理当前模块中的引用,释放内存以优化缓存大小。特别保留 sourceTypessize() 缓存数据,确保构建统计正常。

js 复制代码
class NormalModule extends Module {
    /**
     * 获取当前 compilation 实例对应的 NormalModule 的 hooks。
     * 如果之前未创建过,则初始化一组 Hook 并缓存。
     *
     * @param {Compilation} compilation 当前编译过程中的 Compilation 实例
     * @returns {NormalModuleCompilationHooks} 当前 compilation 的 hook 集合
     */
    static getCompilationHooks(compilation) {
            if (!(compilation instanceof Compilation)) {
                    throw new TypeError("The 'compilation' argument must be an instance of Compilation");
            }

            // 从缓存中尝试获取 hooks
            let hooks = compilationHooksMap.get(compilation);
            if (hooks === undefined) {
                    // 首次获取,为该 compilation 创建一套新的 hooks
                    hooks = {
                            // 在执行 loader 函数时调用
                            loader: new SyncHook(["loaderContext", "module"]),
                            // 在设置 loaders 之前调用,可修改 loader 列表
                            beforeLoaders: new SyncHook(["loaders", "module", "loaderContext"]),
                            // 在资源被解析前调用
                            beforeParse: new SyncHook(["module"]),
                            // 在创建文件系统快照之前调用(主要用于缓存判断)
                            beforeSnapshot: new SyncHook(["module"]),
                            // 为向后兼容保留(将在 webpack 6 中废弃)
                            readResourceForScheme: new HookMap(scheme => {
                                    const hook = hooks.readResource.for(scheme);
                                    return createFakeHook({
                                            // 将回调包装为使用 resource 和 module 的方式
                                            tap: (options, fn) =>
                                                    hook.tap(options, loaderContext =>
                                                            fn(loaderContext.resource, loaderContext._module)
                                                    ),
                                            tapAsync: (options, fn) =>
                                                    hook.tapAsync(options, (loaderContext, callback) =>
                                                            fn(loaderContext.resource, loaderContext._module, callback)
                                                    ),
                                            tapPromise: (options, fn) =>
                                                    hook.tapPromise(options, loaderContext =>
                                                            fn(loaderContext.resource, loaderContext._module)
                                                    )
                                    });
                            }),
                            // 真正用于读取资源(根据 scheme 分类)
                            readResource: new HookMap(() => new AsyncSeriesBailHook(["loaderContext"])),
                            // 判断是否需要重新构建模块
                            needBuild: new AsyncSeriesBailHook(["module", "context"])
                    };

                    // 缓存 hook 集合
                    compilationHooksMap.set(compilation, hooks);
            }

            return hooks;
    }

    /**
     * NormalModule 构造函数,表示 Webpack 中最常见的模块类型。
     * 用于处理像 JS、CSS 等非内建模块。
     *
     * @param {NormalModuleCreateData} options 创建模块时传入的数据
     */
    constructor({
            layer,
            type,
            request,
            userRequest,
            rawRequest,
            loaders,
            resource,
            resourceResolveData,
            context,
            matchResource,
            parser,
            parserOptions,
            generator,
            generatorOptions,
            resolveOptions
    }) {
            // 初始化父类 Module
            super(type, context || getContext(resource), layer);

            // 模块请求信息(来自工厂)
            this.request = request; // 带 loader 前缀的请求字符串
            this.userRequest = userRequest; // 用户可读的请求路径
            this.rawRequest = rawRequest; // 原始请求路径(不含 loader)
            this.binary = /^(asset|webassembly)\b/.test(type); // 是否为二进制模块(如 wasm 或 asset)
            this.parser = parser; // 模块使用的解析器
            this.parserOptions = parserOptions; // 解析器选项
            this.generator = generator; // 生成器(生成最终的输出资源)
            this.generatorOptions = generatorOptions; // 生成器选项
            this.resource = resource; // 资源的绝对路径
            this.resourceResolveData = resourceResolveData; // 资源解析过程中的数据
            this.matchResource = matchResource; // 指定替换资源的路径(通常用于 loader 特殊场景)
            this.loaders = loaders; // 模块使用的 loader 列表

            if (resolveOptions !== undefined) {
                    this.resolveOptions = resolveOptions; // 自定义的解析选项
            }

            // 构建过程状态
            this.error = null; // 构建中出现的错误
            this._source = null; // 构建生成的源码
            this._sourceSizes = undefined; // 缓存各类型源码大小
            this._sourceTypes = undefined; // 模块输出的资源类型(如 javascript, asset)

            // 缓存控制与构建优化
            this._lastSuccessfulBuildMeta = {}; // 上一次成功构建的元信息
            this._forceBuild = true; // 是否强制重新构建
            this._isEvaluatingSideEffects = false; // 是否正在评估副作用
            this._addedSideEffectsBailout = undefined; // 判断副作用跳过的缓存
            this._codeGeneratorData = new Map(); // 存储代码生成相关的缓存数据
    }

    /**
     * 获取模块的唯一标识符(用于模块映射与缓存键)
     * @returns {string}
     */
    identifier() {
            if (this.layer === null) {
                    // 默认情况下,如果是 JS 模块,直接返回 request
                    if (this.type === JAVASCRIPT_MODULE_TYPE_AUTO) {
                            return this.request;
                    }
                    return `${this.type}|${this.request}`;
            }
            return `${this.type}|${this.request}|${this.layer}`;
    }

    /**
     * 获取用户可读的模块标识符(用于日志、错误等输出)
     *
     * @param {RequestShortener} requestShortener
     * @returns {string}
     */
    readableIdentifier(requestShortener) {
            return requestShortener.shorten(this.userRequest);
    }

    /**
     * 获取模块在 library 构建中的唯一标识符(用于构建 library 结构时的依赖引用)
     *
     * @param {LibIdentOptions} options
     * @returns {string | null}
     */
    libIdent(options) {
            let ident = contextify(
                    options.context,
                    this.userRequest,
                    options.associatedObjectForCache
            );
            if (this.layer) ident = `(${this.layer})/${ident}`;
            return ident;
    }

    /**
     * 获取可用于 chunk 条件匹配的名称(通常是资源路径,不含 query)
     *
     * @returns {string | null}
     */
    nameForCondition() {
            const resource = this.matchResource || this.resource;
            const idx = resource.indexOf("?");
            if (idx >= 0) return resource.slice(0, idx);
            return resource;
    }

    /**
     * 使用缓存模块替换当前模块的内部状态(例如编译过程中重新利用缓存)
     *
     * @param {Module} module 缓存中已有的模块
     */
    updateCacheModule(module) {
            super.updateCacheModule(module);
            const m = /** @type {NormalModule} */ (module);

            this.binary = m.binary;
            this.request = m.request;
            this.userRequest = m.userRequest;
            this.rawRequest = m.rawRequest;
            this.parser = m.parser;
            this.parserOptions = m.parserOptions;
            this.generator = m.generator;
            this.generatorOptions = m.generatorOptions;
            this.resource = m.resource;
            this.resourceResolveData = m.resourceResolveData;
            this.context = m.context;
            this.matchResource = m.matchResource;
            this.loaders = m.loaders;
    }

    /**
     * 清理模块内部无关缓存的数据(以释放内存)
     * 通常用于构建后阶段,例如缓存写入前的内存优化
     */
    cleanupForCache() {
            // 构建过的模块,在清理前需要缓存类型和大小信息供 stats 使用
            if (this.buildInfo) {
                    if (this._sourceTypes === undefined) this.getSourceTypes();
                    for (const type of this._sourceTypes) {
                            this.size(type); // 缓存 size
                    }
            }

            // 调用父类的清理逻辑
            super.cleanupForCache();

            // 清除内部无关状态
            this.parser = undefined;
            this.parserOptions = undefined;
            this.generator = undefined;
            this.generatorOptions = undefined;
    }

}

这段代码展示了 NormalModule 的核心职责:

  1. 构建相关的状态和属性管理(如 parser、generator、resource 等)。
  2. 模块唯一性和可读性处理(如 identifier、libIdent)。
  3. 模块缓存机制支持 (如 updateCacheModulecleanupForCache)。
  4. 钩子机制支持模块构建生命周期扩展getCompilationHooks)。
相关推荐
_请输入用户名1 分钟前
husky 切换 simlple-git-hook 失效解决方法
前端
前端九哥2 分钟前
🚀Vue 3 hooks 每次使用都是新建一个实例?一文彻底搞懂!🎉
前端·vue.js
盏灯2 分钟前
尤雨溪搞响应式为什么要从 Object.defineProperty 换成 Proxy❓
前端·vue.js
爱上大树的小猪2 分钟前
【前端样式】使用CSS Grid打造完美响应式卡片布局:auto-fill与minmax深度指南
前端·css·面试
代码小学僧3 分钟前
🤗 赛博佛祖 Cloudflare 初体验托管自定义域名与无限邮箱注册
前端·serverless·云计算
晴殇i3 分钟前
一行代码解决深拷贝问题,JavaScript新特性解析
前端
天天扭码13 分钟前
零基础入门 | 超详细讲解 | 小白也能看懂的爬虫程序——爬取微博热搜榜
前端·爬虫·cursor
小兔崽子去哪了29 分钟前
微信小程序入门
前端·vue.js·微信小程序
独立开阀者_FwtCoder32 分钟前
# 白嫖千刀亲测可行——200刀拿下 Cursor、V0、Bolt和Perplexity 等等 1 年会员
前端·javascript·面试
不和乔治玩的佩奇39 分钟前
【 React 】useState (温故知新)
前端