导读:本期聚焦于天穹小白创作的《Webpack 中 NormalModule 与 RawModule 有什么区别?各自的使用场景是什么?》,敬请观看详情。翻开Webpack的编译流程会发现,依赖图中的节点并不是同一种对象。NormalModule承担了资源文件的标准编译职责,从loader转换、AST解析到依赖收集一应俱全;RawModule则直接接收一段现成的源码字符串,跳过loader与解析器,把内容原样送入产物。本文从两者的继承关系和构造参数入手,对比它们在构建、代码生成、缓存等环节的行为差异,并结合虚拟模块注入、HMR热更新等实际案例说明各自的适用边界,同时整理了选型建议与常见踩坑点,帮助插件开发者在使用Webpack内部API时做出准确的类型选择。

在 Webpack 的世界里,一切资源皆模块。编译器把入口文件、入口引用的每个资源、甚至运行时注入的代码,统统抽象成依赖图上的节点,而承载这些节点的对象并不只有一种形态。翻开源码目录 lib 会看到 NormalModule、RawModule、ContextModule、DelegatedModule、ExternalModule 等一串实现,其中 NormalModule 和 RawModule 是最基础也最容易混淆的一对:前者是资源文件的标准封装,负责 loader 转换与依赖解析;后者则是一辆跳过所有构建环节的直通车。理解两者的边界,是编写高质量 Webpack 插件、排查构建异常的基本功。

Webpack 中 NormalModule 与 RawModule 有什么区别?各自的使用场景是什么?

一、继承体系与构造参数:两者从出生就不同

NormalModule 与 RawModule 都继承自同一个基类 Module。这个基类定义了模块在依赖图中的通用行为:identifier 返回模块的唯一标识,build 负责构建,codeGeneration 负责生成最终代码,size 用于统计体积,updateHash 参与哈希计算。两个子类的差异,正是从它们对这套接口的不同实现方式开始的。

先看构造函数。NormalModule 的构造参数多达十几个,包括模块类型 type、上下文路径 context、请求字符串 request、用户请求 userRequest、原始请求 rawRequest、loader 列表 loaders、资源路径 resource、解析器 parser、生成器 generator 等。这一长串参数意味着它天然就是为完整编译流程服务的:NormalModuleFactory 在解析阶段会根据配置中的 rules 匹配 loader,确定模块类型,再把所有信息打包交给构造函数。

RawModule 的构造则朴素得多,核心参数只有三个:source、identifier 和 name。第一个是现成的模块源码字符串,第二个是模块标识,第三个是可读名称。不需要 loader,不需要解析器,甚至不需要文件真实存在于磁盘上。换句话说,NormalModule 描述的是“如何把一个资源编译成模块”,而 RawModule 描述的是“我已经有一段代码,请直接把它当作模块”。

// NormalModule 的构造(简化示意,实际参数更多)
new NormalModule({
  type: 'javascript/auto',
  resource: '/project/src/index.js',
  userRequest: './src/index.js',
  rawRequest: './src/index.js',
  loaders: [],
  parser: javascriptParser,
  generator: javascriptGenerator
});

// RawModule 的构造:三个参数就够了
new RawModule(
  'module.exports = 42;',   // source:现成的源码
  'raw-module-42',          // identifier:唯一标识
  './answer.js'             // name:可读名称
);

把两者的行为差异整理成表格会更直观:

对比维度NormalModuleRawModule
创建方式由 NormalModuleFactory 解析请求后创建直接实例化,传入源码字符串
loader 处理完整执行匹配到的 loader 链完全跳过
AST 解析按模块类型选择 parser 生成语法树不解析,不分析语法
依赖收集自动提取 import、require 等依赖自身不产生任何依赖
代码生成由模块类型对应的 Generator 生成原样输出构造时传入的字符串
模块类型js、css、asset 等多种类型固定按 javascript/auto 处理
Source Map可结合 devtool 生成映射基本不支持有意义的映射

二、NormalModule:一条完整的构建流水线

NormalModule 的 build 方法是整个 Webpack 编译流程的核心。它的执行过程可以概括为三步:第一步执行 loader,把磁盘上的原始资源转换成中间代码;第二步调用 parser 把转换结果解析成 AST;第三步遍历 AST 中的语句,把 import、require、动态 import 等依赖语句收集为 Dependency 对象,挂到模块自身。后续流程中,compilation 会根据这些依赖发起新一轮模块解析,依赖图就是这样一层层生长出来的。

以一个 CSS 文件为例,配置了 style-loader 和 css-loader 之后,NormalModule 在构建时会先让 css-loader 把 CSS 转换成 JS 模块形式的字符串,再让 style-loader 包上一层插入 style 标签的运行时代码,最后由解析器扫描转换结果中的 require 调用,继续收集依赖。整个过程对使用者完全透明,这正是 NormalModule 的价值所在:任何能被 loader 处理的资源,都能以统一的方式进入依赖图。

// NormalModule 构建流程的简化示意
class NormalModule extends Module {
  build(options, compilation, resolver, fs, callback) {
    // 第一步:执行 loader 链,得到转换后的源码
    this.doBuild(options, compilation, resolver, fs, (err) => {
      if (err) return callback(err);

      // 第二步:按模块类型获取解析器,生成 AST
      const parser = this.getParser(this.type);
      parser.parse(this._source.source(), {
        current: this,
        module: this,
        compilation: compilation
      });

      // 第三步:解析过程中收集的依赖已被挂到 this.dependencies
      // compilation 会据此继续解析新的模块请求
      callback();
    });
  }
}

值得一提的是,NormalModule 的 identifier 由资源绝对路径、loader 信息、模块类型等共同拼成,这保证了唯一性和稳定性,也让持久化缓存能够通过 identifier 命中历史构建结果。当文件内容与相关配置都没有变化时,模块可以跳过重新构建,这是 Webpack 增量编译效率的基础。

三、RawModule:把现成代码直接送进产物

RawModule 的 build 方法几乎是空的:它不读文件,不跑 loader,不解析语法,只是初始化一些构建元数据就宣告完成。代码生成阶段,它把构造函数里传入的那个字符串包装成 Source 对象直接返回。也就是说,你在构造时给它什么,最终产物里就是什么,一个字节都不会变。

这种设计看似简单,却解决了一个很实际的问题:有些代码根本不是从文件来的。HMR 热更新在生成补丁模块时,需要把运行时拼装的更新代码作为一个模块塞进 chunk;某些构建工具需要把配置序列化成一段 JS 代码注入产物;DLL、外部模块等场景也存在“代码是生成的,不是读来的”需求。这些场景如果强行走 NormalModule,就得先把字符串写到磁盘再让编译器读回来,既别扭又低效。

// RawModule 的核心逻辑(简化示意)
class RawModule extends Module {
  constructor(source, identifier, name, readableIdentifier) {
    super('javascript/auto', null);
    this.sourceStr = source;
    this.identifierStr = identifier || 'raw-module';
    this.nameStr = name || 'raw-module';
    this.readableIdentifierStr = readableIdentifier || this.nameStr;
  }

  identifier() {
    return this.identifierStr;
  }

  build(options, compilation, resolver, fs, callback) {
    // 没有任何编译动作,直接完成
    this.buildInfo = {};
    this.buildMeta = {};
    callback();
  }

  codeGeneration() {
    // 把构造时传入的字符串原样作为产物代码
    const sources = new Map();
    sources.set('javascript', new RawSource(this.sourceStr));
    return { sources };
  }
}

需要注意两个隐含限制。其一,RawModule 的模块类型固定为 javascript/auto,传入的代码会被当作 JS 处理,如果你想注入 CSS 或其他资源形态,它帮不上忙。其二,identifier 完全由调用方决定,如果两个不同的 RawModule 使用了相同的 identifier,依赖图和缓存都会出现混乱,这是插件开发中最常见的翻车点之一。

四、使用场景对比与插件实战

NormalModule 的适用场景无需多言:项目中所有真实存在的资源文件,JS、CSS、图片、字体、JSON,只要经过 rules 匹配和 loader 转换,最终都会以 NormalModule 的形态参与编译。它是构建流水线上的主力,绝大多数开发者日常打交道的就是它。

RawModule 的舞台则在标准流程之外,典型的有三类:一是虚拟模块,比如把环境配置、构建元信息序列化成一个模块供业务代码引用;二是运行时代码注入,HMR 的补丁模块就是现成的例子;三是桥接外部产物,把已经生成好的代码片段纳入依赖图统一管理。下面用一个虚拟配置模块的插件演示具体做法:拦截 NormalModuleFactory 的 factorize 钩子,当请求命中约定路径时直接返回 RawModule,绕过文件解析。

const webpack = require('webpack');
const RawModule = webpack.RawModule;

class VirtualConfigPlugin {
  apply(compiler) {
    compiler.hooks.normalModuleFactory.tap('VirtualConfigPlugin', (factory) => {
      factory.hooks.factorize.tapAsync(
        'VirtualConfigPlugin',
        (resolveData, callback) => {
          if (resolveData.request === 'virtual:config') {
            // 业务代码里 import 'virtual:config' 即可拿到这份配置
            const config = { api: 'https://api.ipipp.com', debug: false };
            const source = 'module.exports = ' + JSON.stringify(config) + ';';
            const module = new RawModule(source, 'virtual:config', 'virtual:config');
            return callback(null, module);
          }
          // 其他请求走正常流程
          callback();
        }
      );
    });
  }
}

module.exports = VirtualConfigPlugin;

这个插件生效后,业务代码中的 import 'virtual:config' 会被直接解析为一个内容固定的模块,磁盘上不需要存在任何对应文件。如果改用 NormalModule 实现同样效果,就得借助虚拟文件系统伪装出真实文件,复杂度立刻上升一个量级。反过来,如果模块内容来自真实文件且需要 loader 参与,比如 TS 转译、CSS 处理,那就必须使用 NormalModule,RawModule 完全无法胜任。

五、选型建议与常见踩坑点

选型的判断标准其实只有一条:代码是否需要经过编译。需要 loader 转换、语法分析、依赖收集的,交给 NormalModule;内容已经确定、拿来即用的字符串,用 RawModule。介于两者之间的情况,优先考虑 NormalModule,因为它对缓存、Source Map、模块类型系统的支持都更完整。

使用 RawModule 时有几个坑值得提前记下。第一,identifier 必须全局唯一且跨构建稳定,否则持久化缓存可能返回错误内容,排查起来非常隐蔽。第二,传入的代码必须是最终形态,没有 loader 意味着没有 babel 转译,如果目标运行环境较老,语法层面要自己把关。第三,RawModule 不会自动产生依赖,代码里如果引用了其他模块,需要手动构造 Dependency 并正确挂接,否则产物运行时会报找不到模块。第四,它对 Source Map 基本无能为力,产物报错时定位会直接指向生成字符串,调试体验要提前做好心理预期。

最后补充一点版本层面的差异。Webpack 4 时代,HMR 补丁、部分运行时代码都直接使用 RawModule;到了 Webpack 5,运行时体系被重构成了独立的 RuntimeModule,职责划分更清晰,但 RawModule 作为公开导出的内部类依然保留,插件生态中虚拟模块类方案也依然大量依赖它。无论版本如何演进,理解“完整流水线”与“直通车”这两种模块形态的分野,都能帮助你在阅读源码和编写插件时快速定位问题所在。

NormalModuleRawModuleWebpack插件开发修改时间:2026-09-26 05:39:18

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0926/62008.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。