项目产物兼容性问题 
问题描述 
项目中遇到一个问题,项目中使用了 String.replaceAll 方法,并且在 Webpack 中配置了 targets: { chrome: '54' },但是打包后的产物在 54 版本的 Chrome 浏览器中 replaceAll 还是会报错。 webpack 配置如下:
const config = {
  test: /\.(ts|js)x?$/,
  exclude: /node_modules/,
  use: [
    {
      loader: 'babel-loader',
      options: {
        cacheDirectory: true,
        presets: [
          [
            '@babel/preset-env',
            {
              targets: { chrome: '54' },
            }
          ]
        ],
      }
    },
    'ts-loader'
  ]
},// 实际代码
const str = 'aaabbb'.replaceAll('a', 'b')问题原因 
@babel/preset-env 其实做的是两件事:
一件事是语法转换,当设置
targets时,会根据浏览器版本将高级js语法转换为低版本js语法。比如const转换为var,箭头函数转换为function。第二件事就是
API补齐。因为一些语法并不能直接转换为低版本语法,比如replaceAll/Promise.finally等。所以需要增加相关 API 定义,比如转换后在文件头部添加replaceAll的定义就可以解决了:js// 在文件头部添加 // eslint-disable-next-line no-extend-native String.prototype.replaceAll = function () { // ... }
实际上,设置 targets 只是做了第一件事,第二件事则需要设置 useBuiltIns 和 corejs 参数。
corejs是一些低版本js语法定义的集合,corejs3相较于corejs2支持更多的语法,体积更小,性能更好,所以一般采用corejs3。最近 corejs 的作者发了一篇他的 开源经历,感兴趣可以看看。useBuiltIns表示是否使用corejs中的API补齐。默认不使用,为usage是为按需引入。
[
  '@babel/preset-env',
  {
    targets: { chrome: '54' },
    useBuiltIns: 'usage',
    corejs: '3'
  }
]当设置以上两个参数后,就能确保 replaceAll 方法在 Chrome54 版本浏览器上正常使用了。
但是实际项目在生产环境中又会报 exports is not defined 错误,排查原因是因为 import 和 required 共存导致 Webpack 打包时按 es 模块语法生成,未生成 cjs 语法相关的定义。但是在加入 corejs 相关配置前是没有这个问题的。
那么,为什么加入 corejs 配置后会出现 es 和 cjs 共存的情况呢?首先猜测是追加的 corejs 代码的引入方式与实际代码模块不一致。比如我们 tsconfig module 设置的是 cjs,那么 babel-loader 接收到的是 cjs 代码,此时如果 corejs 通过 es module 引入,就会导致该问题。
那么 corejs 代码是在哪注入的呢,查找到代码:
function injectGlobalImport(url) {
  cache.storeAnonymous(prog, url, (isScript, source) => {
    return isScript
      ? template.statement.ast`require(${source})`
      : t.importDeclaration([], source)
  })
}当 isScript 为 true 时,使用的是 cjs,否则使用 es 语法。继续查找:
// isScript 参数
programPath.node.sourceType === 'script'isScript 与 sourceType 相关,查找 babel 文档 sourceType,默认情况下 sourceType 为 module,因此默认情况下注入的是 es 模块语法。证实了之前的猜想,因为 tsconfig.json 的 module 设置为了 commonjs 将代码转为 cjs 格式。在 babel-loader 中添加 es 模块的 corejs 代码,从而导致打包后的产物运行报错。
如何解决这个问题呢?答案是另外一个参数:modules。modules 表示是否允许将 es 模块语法转换为 cjs,为 false 时表示不将 es 语法转换为 cjs 语法,默认值为 auto。
shouldTransformESM: modules !== 'auto' || !api.caller?.(supportsStaticESM)babel-loader 默认会设置 supportsStaticESM: true,也就以为着不会将 es 模块语法转换为 cjs 模块语法,导致最终打包后的产物运行报错。
所以解决方法可以有:
- tsconfig.json 中 module 改为 esnext,让 ts 生成的为 es 代码。
 - 将 modules 设置为 cjs,这样 es 代码就可以全部转换为 cjs 代码了。
 
