Webpack 默认支持 ES2015 的模块定义,但是在进行代码转换的时候,像 const 这样的语法并不会被转换。这样的话,在一些老旧浏览器中就会产生很多问题。

为了能更好的理解 Webpack 的默认输出,我们将 Webpack 的 mode 参数设置为 none, 然后观察一下构建输出:

dist/main.js

...
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "default": () => __WEBPACK_DEFAULT_EXPORT__
/* harmony export */ });
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ((text = "Hello world") => {
  const element = document.createElement("div");
  element.className = "rounded bg-red-100 border max-w-md m-4 p-4";
  element.innerHTML = text;
  return element;
});
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14

为了解决上面的语法转换的问题,我们可以借助于 Babelopen in new window. Babel 是一个 JavaScript 语法转译器,支持 ES2015 以及更新版本的各种特性。

在 Webpack 中使用 Babel

在 Webpack 中,我们可以通过 babel-loaderopen in new window 来使用 Babel。

首先安装 babel-loader.

npm add babel-loader @babel/core --develop
1

webpack.parts.js

const APP_SOURCE = path.join(__dirname, 'src');

exports.loadJavaScript = () => ({
  module: {
    rules: [{ test: /\.js$/, include: APP_SOURCE, use: 'babel-loader' }],
  },
});
1
2
3
4
5
6
7

webpack.config.js

const commonConfig = merge([...parts.loadJavaScript()]);
1

设置完 babel-loader 的配置以后,我们还需要添加一个 babel 的配置,我们把配置放在 .babelrc 里面。

通常情况下,我们至少需要安装 @babel/preset-envopen in new window. 这个包是 Babel 的一个插件预置包,它通过 browserslist 中的定义来加载需要的插件。

browserslist 我们在前面已经讲过,这里不再赘述。

安装预置包,

npm add @babel/preset-env --develop
1

.babelrc 定义如下:

{
  "presets": [["@babel/preset-env", { "modules": false }]]
}
1
2
3

这时候执行 npm run build -- --mode none,然后查看 dist/main.js 的内容,你会发现,当 .browserslistrc 内的定义变化的时候,dist/main.js 也会发生变化.

我们将 .browserslistrc 中的定义修改为只包含 IE8,则输出的 dist/main.js 内容为:

...
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "default": () => __WEBPACK_DEFAULT_EXPORT__
/* harmony export */ });
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (function () {
  var text = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "Hello world";
  var element = document.createElement("div");
  element.className = "rounded bg-red-100 border max-w-md m-4 p-4";
  element.innerHTML = text;
  return element;
});
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

对比前面的 dist/main.js 输出,我们可以看到函数的语法发生了变化。

使用 Polyfill

@babel/preset-env 可以针对一些老旧浏览器填补语法功能的缺失。要使用这个功能,需要开启 useBuiltIns 参数并安装 core-jsopen in new window.如果代码中使用了 async 函数,那么还需要安装 regenerator-runtimeopen in new window 这个包。

除非我们使用 useBuiltIns: 'usage' 来配置 @babel/preset-env, 否则我们需要在代码中显示的引入 core-js 或者在 Webpack 的 entry 中配置,app: ["core-js", "./src"]

注意,core-js 会污染全局变量,比如 Promise. 对于一个三方库开发者来说,引入 core-js 会引发许多问题。Babel 有一个插件 @babel/plugin-transform-runtimeopen in new window 可以很好的解决这个问题。

关于 Babel 的一些小提示

.babelrc 还有很多其他的选项可以使用,可以参考官网说明open in new window.babelrc 支持 JSON5open in new window 格式,也就是可以在 .babelrc 中添加注释、使用单引号字符串等。

有时候如果你要使用一些新特性或者实验性的特性,如果项目是一个长期迭代的项目,那么注释是非常必要的。

Babel 的一些插件

Babel 有很多插件可以使用。

在 NodeJS 中,我们可以通过 babel-registeropen in new window 或者 babel-cliopen in new window 来使用 Babel。

针对不同浏览器输出不同的构建结果

为了更好的利用现代浏览器对一些语法的支持,同时支持老旧浏览器,我们需要构建两份输出,然后在启动代码中根据浏览器加载不同的构建结果。这样的话,在现代浏览器中,加载的代码体积更小,同时解析时间也更短。

我们可以像下面这样,在 HTML 中使用不同的构建结果。

<!-- 支持 ES module 语法的加载这个文件 -->
<script type="module" src="main.mjs"></script>

<!-- 老浏览器加载这个文件 (同时支持 module 语法的浏览器知道不去加载这个文件(nomodule)). -->
<script nomodule src="main.es5.js"></script>
1
2
3
4
5

我们可以通过下面的方式配置 Webpack 输出两份构建结果。

.browserslistrc

# 支持老的 IE
[legacy]
IE 8

# 新浏览器,可以根据需要自行决定
[modern]
> 1% # Browser usage over 1%
1
2
3
4
5
6
7

webpack.config.js

// 记住在生产构建中设置 "mode": "production"
const getConfig = (mode) => {
  switch (mode) {
    case "prod:legacy":
      process.env.BROWSERSLIST_ENV = "legacy";
      return merge(commonConfig, productionConfig);
    case "prod:modern":
      process.env.BROWSERSLIST_ENV = "modern";
      return merge(commonConfig, productionConfig);
    ...
    default:
      throw new Error(`Trying to use an unknown mode, ${mode}`);
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14

package.json

{
  "scripts": {
    "build": "wp --mode prod:legacy && wp --mode prod:modern"
  }
}
1
2
3
4
5

上面的两次 Webpack 构建是串行执行,我们可以通过 concurrentlyopen in new window 并行支持 Webpack 构建。

使用 TypeScript

微软的 TypeScript 是一门编译型语言,使用模式与 Babel 类似。TypeScript 除了有 JavaScript 能力以外,还有类型定义。

在 Webpack 中,我们可以通过 ts-loaderopen in new window 来使用 TypeScript。在使用 ts-loader 的时候,我们可以仅仅使用它的代码编译功能,把类型检查能力通过编辑器来实现,或者通过 fork-ts-checker-webpack-pluginopen in new window 这个插件在另一个进程中实现。

我们也可以通过 @babel/plugin-transform-typescriptopen in new window 来使用 TypeScript。

Webpack 5 已经默认支持了 TypeScript。在使用 Webpack 5 的时候要确保项目中不要安装 @types/webpack 这个包,因为会有冲突。

有时候项目中的 TypeScript 配置有多个,比如使用 extends 属性 "extends": "./tsconfig.common" 配置了多个配置,这时候,我们可以通过 ts-loader 的 configFile 来指定一个具体配置文件。

使用 TypeScript 来编写 Webpack 配置文件

如果项目中使用 TypeScript,那么 Webpack 的配置文件也可以使用 TypeScript 来编写,webpack.config.ts。 Webpack 会自动检测和执行这个配置文件。

要让 TypeScript 编辑的配置文件能够正确执行,我们需要安装 ts-nodeopen in new windowts-node-devopen in new window 这两个包,因为 Webpack 需要这两个包来执行配置文件。

默认情况下,在 Webpack watc 模式或者 webpack-dev-server 中,编译错误会导致构建失败。为了避免这个问题,我们可以使用下面的配置:

tsconfig.json

{ "ts-node": { "logError": true, "transpileOnly": true } }
1

logError 非常重要,因为没有这个选项的话,ts-node 在遇到错误的时候会直接退出。transpileOnly 只编译,不做类型检查。因为很多时候编辑器已经可以帮助我们做了类型检查。

WebAssembly

WebAssemblyopen in new window 让开发者在浏览器上运行一些非 JavaScript 代码,比如 C++。

wasm-simpleopen in new windowwasm-complexopen in new window 是两个官方示例。

总结

Webpack 默认支持 JavaScript,Babel 工具可以帮助我们根据不同浏览器定制构建输出。

Babel 给我们提供了针对不同浏览器定制转换代码的能力。@babel/preset-env 可以根据 .browserslistrc 的定义决定哪些特性要编译,需要使用哪些 polyfill.

借助于 Babel,我们可以使用还处在试验阶段的语言特性。Babel 有很多 preset 和 插件来定制使用。

我们可以根据不同环境定制不同的 Babel 能力。我们需要在不同环境中确保使用正确的 preset 和插件。

关注微信公众号,获取最新推送~