webpack 很强大,提供的能力选项配置也很多好让你满足各种不同的打包场景。

但学习成本也跟着上去了,其中一件让人头疼的是输出时的配置,特别容易让人迷惑;

平时用 webpack 可能不会有太大问题,可一旦你开发的包被别人引用的时候,就会存在问题;

我最近遇到这么一个场景耗费我很多时间去重新学习 webpack 的打包输出。

1、场景

当你开发的包依赖较大的第三方包(reactreact-dom) 的时候,你一般是把这些大的第三方包 externals 出去:

{
  externals: {
    'react': 'React',
    'react-dom': 'ReactDOM'
  }
}

假如你开发了 A、B 两个插件

  • A 依赖 B 、reactreact-dom
  • B 只依赖 reactreact-dom

deps

如果你想发布 A 的话,有两种策略,要么直接依赖(将 B 写到 dependencies 中),要么像 reactreact-dom 一样 externals 掉 B 包(有可能 B 的包也很大)。

请问此时该如何 webpack 配置来支持常用的两种模式?

2、如何解决?

先看一下 webpack 官方在进行打包时候,是根据 package.json 中的 mainFields 字段指定依据哪个字段中的 路径 找到第三方包的,而 externals 字段则是指定以何种方式引入第三方包

客观情况如下:

  • 我们通常的是会把打出来的包放在 browser 字段中;
  • webpack 打包时,选择依赖包是根据 resolve.mainFields 字段找到指定的路径把代码打进去的;默认配置是 ['browser', 'module', 'main'],也就是说会优先找 browser 的字段指定的路径
  • libraryTarget 配置如何暴露 library。如果不设置 library, 那这个library就不暴露。就相当于一个自执行函数
  • libraryTarget 决定了你的 library 运行在哪个环境,哪个环境也就决定了你哪种模式去加载所引入的额外的包。也就是说,externals 应该和 libraryTarget 保持一致。library 运行在浏览器中的,你设置 externals 的模式为 commonjs,那代码肯定就运行不了了。

我们一般容易混淆的是 externals 的使用,比如对 react-dom 的 externals,经常会看到两种写法:

  • {'react-dom': 'reactDom'}
  • {'react-dom': 'react-dom'}

这两种的区别,其实是和你想将第三方以怎样的方式打入到你最终代码有关

还有一种 对象形式,那种是只应用在 umd 的打包方式中

2.1、externals 资料

2.2、mainFields 参考资料

先通读一下官方文档中的 resolve.mainFields,想看中文的可以看这个链接 解析(resolve)

附其他参考资料:

顺带收集几个相关 issue,看看别人遇到的问题现在你是否可以解决:

3、解决方案

使用两份输出配置项,主要更改 webpack 的打包的配置项中的 externalsoutput 这两个字段。

3.1、B 包的配置如下

第一份配置是针对 .umd.js 文件的(别人用于 externals ,然后通过 script 脚本标签)

{
    externals: {
      'react': 'React',
      'react-dom': 'ReactDOM'
    },
    output: {
      filename: 'index.umd.js',
      libraryTarget: 'umd',
      library: 'extB',
      path: path.resolve(__dirname, 'dist'),
      umdNamedDefine: true
    }
}

第二份配置是针对 .browser.js 文件的(别人使用的时候直接放在 dependencies中,也会最终打包进去)

{
    externals: {
      'react': 'react', // 这里更改了
      'react-dom': 'react-dom' // 这里更改了
    },
    output: {
      filename: 'index.browser.js', // 这里更改了
      libraryTarget: 'umd',
      library: 'extB',
      path: path.resolve(__dirname, 'dist'),
      umdNamedDefine: true
    }
}

同时还得更改当前文件夹的 package.json 的对应字段如下:

{
  ...
  "main": "dist/index.umd.js",
  "module": "dist/index.umd.js",
  "browser": "dist/index.browser.js",
  ...
}

3.2、A 包的配置

首先,无论是否 externals,都需要在 package.json 中填完对 B 依赖的信息(可以根据实际情况放在 dependencies 字段或者 peerDependencies 字段)

情况 1 :A 包最终要把 B 包打入到最终代码中去,那么和 B 包的 webpack 配置是一样的;

情况 2:A 包最终要把 B 包 externals 掉,基本配置是一样的,只不过有额外的两部需要操作:

  1. 在上述的 externals 中新增 B 包的 externals 配置项(需要区别 {'B': 'B'} 和 {'B': 'extB'})
  2. 在页面中引入 cdn 资源 http://xxx/index.umd.js (注意不是 http://xxx/index.browser.js

4、简化的写法

我们看到上述这么写是能成功的,官方考虑到了这种情况,所以针对 umd 的打包方式,推出 对象形式](https://webpack.js.org/configuration/externals/#object),让你统一上述两种配置文件(但这种配置只能应用在 umd 的打包方式中);

最终我们把上述两份打包配置合并成一份:

{
    externals: {
      'react': 'react'
      'react-dom': {
        commonjs: 'react-dom', // 这里更改了
        commonjs2: 'react-dom', // 这里更改了
        amd: 'react-dom', // 这里更改了
        root: 'reactDom'
      }
    },
    output: {
      filename: 'index.umd.js', // 这里更改了
      libraryTarget: 'umd',
      library: 'extB',
      path: path.resolve(__dirname, 'dist'),
      umdNamedDefine: true
    }
}

这样就省去了 index.browser.js 这个文件,增加了兼容性。

5、总结

这篇文章是我一整天掉在 webpack 坑里,来回调试了半天所得出的经验总结,特此总结形成文章,方便以后查找。

具体案例参考 ide-context-menu 中的配置