Shimming:本质、方法及工作原理

作者: IT Sectr 发布日期: 2026-05-19 阅读时间: 8 分钟

Shimming 是一种确保期望特定全局变量或 API 的模块兼容性的技术。在 Webpack 生态系统中,shimming 通过 ProvidePlugin、imports-loader 和 exports-loader 实现,可以在不修改其源代码的情况下接入旧版库。根据 Webpack Documentation (2026),shimming 仍然是集成 jQuery 插件和其他不支持模块系统的依赖的关键工具。

要点

  • Shimming — 一种在构建包中替换全局变量和 API 以确保模块兼容性的技术。
  • ProvidePlugin 在代码中检测到对全局变量的引用时会自动导入该模块。
  • imports-loaderexports-loader 管理模块的作用域,添加或修改它们的接口。
  • Shim 与 polyfill 的区别在于,它不实现缺失的功能,而是重定向现有的调用。
  • Webpack 提供内置的 shimming 机制,无需安装额外的包。

什么是 Shimming?

Shimming 是一种软件技术,它在代码与环境之间嵌入一层兼容层,而不修改模块的源代码。在 JavaScript 构建的上下文中,shimming 解决的是模块引用模块环境中不存在的全局变量(window.$global.process)的问题。

Shim 和 polyfill:主要区别

Polyfill 从零开始实现缺失的功能,为环境添加新的能力。例如,core-js 为旧浏览器添加 Array.prototype.flatMap。而 shim 则将现有的调用重定向到可用的实现,或替换期望的全局对象。在 Webpack 中,ProvidePlugin 会在任何出现对全局变量 $ 的引用之处自动插入 import $ from 'jquery',无需修改代码。

主要区别在于目标。Polyfill 添加所缺失的内容,而 shim 使现有代码与其运行环境兼容。两者之间的选择取决于要解决的问题:是缺少 API,还是接口不兼容。

Shimming 在 Webpack 中如何工作

Webpack 将每个模块视为具有自身作用域的隔离单元。如果库像 window.$ 那样引用全局变量 jQuery,构建将因模块上下文中不存在该变量而以错误结束。ProvidePlugin 在编译阶段解决这个问题:当在代码中检测到标识符 $ 时,插件会自动在文件开头插入 import $ from 'jquery'

js
// 源代码(旧版模块引用全局 jQuery)
$('.element').hide();

// 经过 ProvidePlugin 处理后(Webpack 插入 import)
import $ from 'jquery';
$('.element').hide();

此外,imports-loader 允许显式指定模块应获得哪些依赖。当库在顶层使用 this 并期望 this 指向 window 而非 module.exports 时,这很有用。

ProvidePlugin:为模块提供全局变量

ProvidePlugin 是 Webpack 的内置插件,当检测到对指定标识符的引用时自动加载模块。配置是一个对象,其中键是变量名,值是模块路径和导出的字段。

插件配置

js
// webpack.config.js
const webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.ProvidePlugin({
      $: 'jquery',
      jQuery: 'jquery',
      _: 'lodash',
      'window.$': 'jquery',
    }),
  ],
};

ProvidePlugin 支持通过数组语法进行定向导入。例如,[lodash, debounce] 只从 lodash 导入 debounce 函数,从而减小最终包的大小。这对于移动项目尤其重要,因为每个千字节都会影响加载时间。

imports-loader 和 exports-loader

imports-loader 在模块开头添加必要的导入,而 exports-loader 为不显式使用 module.exports 的模块设置导出的值。这些 loader 在单个文件的层面工作,而不是像 ProvidePlugin 那样全局工作。

使用 imports-loader 修复依赖

js
// webpack.config.js — imports-loader 配置
module.exports = {
  module: {
    rules: [
      {
        test: /legacy-module\.js$/,
        use: [
          {
            loader: 'imports-loader',
            options: {
              imports: [
                'jquery',
                '$',
              ],
            },
          },
        ],
      },
    ],
  },
};

当库将值赋给全局变量但不通过模块系统导出它时,使用 exports-loader。loader 提取该值并将其转换为模块导出,从而使其他模块能够通过 import 导入它。

在 Webpack 配置中设置 shimming

Shimmingwebpack.config.js 中通过插件和 loader 的组合进行配置。典型方案包括用于全局变量的 ProvidePlugin 和用于需要更改作用域的特定模块的 imports-loader。

用于 shimming 的基本 Webpack 配置

js
const webpack = require('webpack');
const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
    globalObject: 'this',
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules\/(?!legacy-lib)/,
        use: [
          {
            loader: 'imports-loader',
            options: {
              type: 'commonjs',
              imports: ['jquery', '$'],
            },
          },
        ],
      },
    ],
  },
  plugins: [
    new webpack.ProvidePlugin({
      $: 'jquery',
      jQuery: 'jquery',
    }),
  ],
};

output 中的 globalObject 字段设置顶层对 this 的引用的上下文。对于浏览器环境,值 'this' 指向 window,而对于 React Native 或 Node.js — 指向 global。选择正确的值可以防止目标环境中的运行时错误。

shimming 中的常见错误

Shimming 是一个强大但危险的工具。错误的配置会导致包中的代码重复、命名冲突和意外的运行时错误。开发者常常忘记 ProvidePlugin 在编译阶段工作,无法处理对变量的动态引用。

全局变量冲突

如果两个插件使用不同版本的 jQuery,ProvidePlugin 只会插入配置中首先指定的那一个。第二个库将获得不兼容的版本,从而引发难以调试的错误。解决方案是使用 exports-loader 并为每个库明确指定版本,或者应用 webpack.IgnorePlugin 排除重复的模块。

另一个常见错误是试图对在动态上下文中使用 CommonJS 同步 require 调用的模块进行 shimming。ProvidePlugin 只处理静态标识符,因此动态引用必须手动替换,或使用 NormalModuleReplacementPlugin

错误 shimming 导致的性能问题

错误的 shimming 配置可能导致包的大小显著增加。如果 ProvidePlugin 配置了数十个全局变量,Webpack 会将相应的导入插入项目的所有文件,无论这些变量是否在每个特定文件中被使用。这会产生冗余代码,尤其是在包含数千个模块的大型项目中。

要诊断 shimming 问题,请使用 webpack-bundle-analyzer — 一种可视化包组成的工具。如果 jQuery 或其他库多次出现在包中,可能是不同版本发生冲突,或者 ProvidePlugin 配置了多个指向包不同版本的标识符。解决方案是通过 resolve.alias 统一依赖版本,并确保所有被 shim 的标识符都指向同一个模块。

shimming 的替代方案:重构和更新依赖

在应用 shimming 之前,评估将库更新到支持模块系统的版本的可能性。许多旧版包都有不需要 shimming 的现代替代品。例如,jQuery 插件可以用浏览器的原生 API 替换:$.ajaxfetch$.eachArray.forEach。重构在维护方面带来长期收益,而 shimming 则是一种使配置复杂化的临时解决方案。

如果无法更新,请考虑 NormalModuleReplacementPlugin,它允许在不修改源代码的情况下在解析层面将一个模块替换为另一个。该插件在构建依赖关系图的阶段、在应用 loader 之前工作,并且无论上下文如何都处理对该模块的所有引用。与定向 loader 相比,这是替换整个库的更干净的解决方案。

现代 JavaScript 中的 Shimming:ESM 和 import maps

随着浏览器中原生 ES 模块的发展以及 import maps 的出现,某些 shimming 场景可以在没有 Webpack 的情况下解决。import maps 允许在浏览器层面即时重新分配模块名称,无需构建阶段。然而,这种方法在 React Native 和其他没有浏览器 ESM 的环境中不受支持,因此通过 Webpack 进行 shimming 对于需要完全控制依赖及其版本的 production 构建仍然适用。import maps 与 Webpack shim 之间的选择取决于目标平台以及与旧浏览器兼容性的要求。

常见问题

shimming 与 tree shaking 有什么区别?

Shimming 添加代码以确保兼容性,而 tree shaking 移除未使用的代码。这两种技术在目标上是对立的:shimming 增大包的大小,tree shaking 减小包的大小。在 production 构建中,两者会依次应用。

可以不用 Webpack 使用 shimming 吗?

可以,shimming 作为一种独立于 Webpack 的技术存在 — 例如,通过 HTML 中的全局脚本或通过带重新导出的 ES 模块。然而,Webpack 提供了最方便的自动化工具:ProvidePlugin 和不需要手动修改代码的 loader。

shimming 如何影响构建性能?

ProvidePlugin 不会影响构建速度,因为它在 AST 编译阶段工作。imports-loaderexports-loader 为每个文件的处理增加少量时间。当在数百个文件上使用时,差异可能占完整构建时间的 5–15%。

什么时候应该放弃 shimming?

如果所有依赖都支持 ES 模块和模块系统,shimming 就是多余的。放弃 shimming 可以简化配置、减小包的大小并降低命名冲突的风险。建议在 caniuse.com 上检查依赖。

shimming 如何与 TypeScript 配合?

TypeScript 要求为被 shim 的变量提供额外的类型声明。需要添加 declare const $: any 或通过 @types/jquery 安装类型。ProvidePlugin 在 TypeScript 编译之后于 JavaScript 层面插入导入,因此类型会被单独检查。

结论

  • Shimming — 一种通过替换全局变量和 API 来确保模块与环境兼容性的技术。
  • ProvidePlugin 在代码中检测到对指定标识符的引用时自动导入模块。
  • imports-loader 在特定文件的开头添加导入,而 exports-loader 设置导出的值。
  • Shim 与 polyfill 的区别在于,它不实现功能,而是将调用重定向到现有的实现。
  • ProvidePlugin 在编译阶段工作,不处理对变量的动态引用。
  • output 中的 globalObject 字段 为目标环境中的顶层设置正确的上下文。
  • 只对不支持现代模块系统的模块使用 shimming,并在 ES 模块得到完整支持时放弃它。

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读