阅读 316
webpack优化生成代码(2)

webpack优化生成代码(2)

优化 babel-loader

babel-loader始终是项目处理任务最多的 loader,尤其是 React 开发过程中,有大量的 JSX 需要去解析,编译。从babel-loader的配置项入手可以进行一些优化。babel-loader使用的插件集合主要是@babel/preset-env@babel/preset-react

@babel/preset-env

@babel/preset-env是一个负责将 JS 代码编译成兼容性更强的低版本 JS 代码的插件集合。

配置项

配置项类型默认值含义
targetsString|Array|Object{}配置浏览器的版本或者 nodejs 的版本
bugfixesBooleanfalse取决于targets设定,根据targets来编译目标浏览器支持的最新版本的语法;babel 8 版本后会自动启用
specBooleanfalse启用更多规范,会导致编译速度减慢
looseBooleanfalse宽松的编译规则,正常情况下,编译会尽可能遵循 ECMAScript 6 的语义,但是宽松模式会不严格,看起来像是手写的代码,见——Babel 6: loose mode
modules"amd"|"umd"|"systemjs"|"commonjs"|"cjs"|"auto"|false"auto"将 ES Module 转换为其他类型模块语法的规则
debugBooleanfalse将 preset 使用的 plugin 和 polyfill 输出到控制台,这个配置不会受到 webpack 的stats的影响
includeArray[]自定义采用的插件名称
excludeArray[]排除使用的插件
useBuiltIns"usage"|"entry"|falsefalse定义如何处理 polyfill
corejs23{version: 2|3,proposals: boolean}2仅在useBuiltIns: usage或者useBuiltIns: entry的时候才生效,定义core-js的版本,version必须是数字 2 或者 3
forceAllTransformsBooleanfalse禁用所有编译
configPathStringNode.js 进程的当前工作目录配置指定了browserslist的目录
ignoreBrowserslistConfigBooleanfalse是否忽略browserslist配置
browserslistEnvObjectundefined配置不同开发环境下的browserslist
shippedProposalsBooleanfalse是否启用浏览器已经支持的提案

配置 modules

modules配置项默认会根据 babel 内部的caller判断浏览器是否支持 ES Modules 的一些特性,例如是否支持静态import或者动态import()等语法;从而选择进一步将 ES Module 转换为 CommonJS 的那种require语法。

由于 webpack 内置的 tree shaking 功能依赖于 ES Modules 的静态语法分析,但是目前modules的支持程度也不是太高,IE 11 是完全不支持的,所以看情况选择是否将modules禁用。

配置 targets

早期的 babel 还有一个 preset,叫babel-preset-latest,这个 preset 的特点是能自动根据 ES 规范的新特性添加对应的 plugin,这样就避免了开发者去单独配置,但是就这样直接添加进去会导致插件越来越多,并且随着时间推移,大部分 ES6 的语法已经被主流浏览器都支持了,没必要再去使用那些插件编译了,这些无用的插件留在 preset 中会导致 babel 的编译流程也越来越慢,于是推荐使用@babel/preset-env去替代这个 preset。

@babel/preset-env帮助开发者从preset-latest进行过渡,如果不在@babel/preset-env中配置targets,那么它就会默认把 ES6+的 JS 代码全部编译成 ES5 的形式。通过开启@babel/preset-envdebug配置项可以清楚的在控制台看到@babel/preset-env使用的 plugin 有多少,如果不设定targets,那么就会像下图这样引入最高兼容到 IE10 的 plugin。

在开发环境下,这些 plugin 基本都是不需要的,而@babel/preset-env并不会去查找browserslist的配置,即使是browserslist的默认配置,也必须在targets中配置,这个设定可能在 Babel 8 重新讨论。

{
  "presets": [["@babel/preset-env", { "targets": "defaults" }]]
}
复制代码

targets字段同样支持上述的browserslist配置形式,在开发环境始终使用最新的 Chrome 版本,这样开发环境引入的 plugin 就减少很多了。

{
  targets: isDevelopment
		? "last 1 chrome version"
		: "> 1%, last 2 versions, Firefox ESR, ie >= 11, not dead",
}
复制代码

targets还支持一些特殊字段的配置:

  • esmodules:默认是false,指定目标浏览器支持 ES Modules 语法,如果设置成true,那么browserslist的配置会失效
  • node"current" | true,指定针对当前 node 的版本进行编译
  • safari"tp",指定针对 safari 的技术预览版进行编译
  • browsers:指定一个browserslist规则数组,不建议使用,因为未来版本可能会删除

配置 bugfixes

默认情况下,@babel/preset-env或者其他的 Babel plugin 会对 ES 语法特性进行相关分组,例如function arguments包含默认参数,剩余参数等内容,如果开启bugfixes@babel/preset-env会根据targets设定的兼容范围,选择将不同的分组编译到目标浏览器支持的最接近的最新现代语法,这将导致已编译应用程序的大小显着减小,不仅优化 webpack 的构建速度,而且优化了生成的代码。

配置 polyfill

core-js 简介

core-js#babelpreset-env

@babel/polyfill已经在 7.4 版本以后被弃用了,目前主流的 polyfill 方案是使用core-js

core-js本身具有三个版本:

  • yarn add core-js@3.6.5:全局注入版本,使用的时候只需要在代码入口点全局引入core-js即可
  • yarn add core-js-pure@3.6.5:模块导入版本,需要在使用的时候单独引入对应的 polyfill 模块文件
  • core-js-bundle:使用 script 注入版本

此外每个版本还具有包含不同 feature 的模块:

  • core-js(-pure)/eses表示稳定的 ES 规范内容
  • core-js(-pure)/stablestable同时包含稳定的 whatwg 的规范内容以及 ES 规范内容
  • core-js(-pure)/featuresfeatures只包含单独语法的特定模块内容,例如core-js(-pure)/features/set表示只包含Set数据集合的相关 polyfill

如果使用全局注入版本,可以只在项目入口文件全局引入,全局引入会自动根据目标环境将 polyfill 引入,不会按需引入。也可以使用features只注入需要的 polyfill。

//全局注入
import 'core-js';

Array.from(new Set([1, 2, 3, 2, 1]));
[1, [2, 3], [4, [5]]].flat(2);
Promise.resolve(32).then(x => console.log(x));

//通过feature引入需要的模块
import 'core-js/features/array/from';
import 'core-js/features/array/flat';
import 'core-js/features/set';
import 'core-js/features/promise';

Array.from(new Set([1, 2, 3, 2, 1]));
[1, [2, 3], [4, [5]]].flat(2);
Promise.resolve(32).then(x => console.log(x));
复制代码

如果使用pure版本,无法使用import 'core-js-pure';这种全局注入的方式,需要根据需要导入特定的模块;pure版本可以做到按需引入,但是使用很麻烦, 比如需要用到SetArray.from两个 feature,需要去找这两个 feature 怎么去引入。

import from from 'core-js-pure/features/array/from';
import flat from 'core-js-pure/features/array/flat';
import Set from 'core-js-pure/features/set';
import Promise from 'core-js-pure/features/promise';

from(new Set([1, 2, 3, 2, 1])); // => [1, 2, 3]
flat([1, [2, 3], [4, [5]]], 2); // => [1, 2, 3, 4, 5]
Promise.resolve(32).then(x => console.log(x));
复制代码

useBuiltIns 和 corejs

Note:如果使用core-js-pure是不需要配置@babel/preset-envuseBuiltInscorejs属性的,可以通过下文介绍的@babel/plugin-transform-runtime让代码编写简化一些。

对于全局注入的版本,例如安装core-js@3版本以后,需要首先设置@babel/preset-envcorejs配置项,并且建议指定较小的core-js版本号,例如使用3.6而不是3,因为对于corejs:3,将不会添加在较小的core-js版本中添加的模块。默认情况下,只会采用稳定的 ES feature,对于尚在提案状态的规范内容不会引入 polyfill,不过可以通过corejs.proposals开启对提案内容的支持。

然后必须指定useBuiltIns,如果不指定不会有任何 polyfill 被添加进来。

core-js的全局注入版本为例,指定corejs: "3.6"useBuiltIns: "entry",配置如下:

module: {
  rules: [
    {
      test: /\.m?js$/,
      exclude: /(node_modules|bower_components)/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: [
            '@babel/preset-env',
            {
              debug: true, // 开启debug模式
              targets: 'ie >= 8',
              bugfixes: true,
              modules: false,
              corejs: {
                version: 3,
                proposals: true,
              },
              useBuiltIns: 'entry',
            },
          ],
        },
      },
    },
  ];
}
复制代码

当指定useBuiltIns:"entry"时,会把core-js所有的针对目标targets的 polyfill 都打包进来,那简直是噩梦!当我只在入口使用了SetArray.from两个 feature 的时候,打包了 1 分钟 生成了一个150KB的 polyfill chunk

import 'core-js';

console.log(Array.from(new Set([1, 2, 3, 2, 1])));
复制代码

当指定useBuiltIns:"usage"时,只会根据代码引入需要的 polyfill,相应的打包时间和 chunk 体积就减小很多了,大概只有20KB

因为直到 Babel 7.3 版本, useBuiltIns: usage还不够稳定,有时候一些需要的 polyfill 并不会自动添加进来,所以可能旧的项目使用会有一点问题。作为兼容性和按需引入更好的选择,可以使用下文core-js-pure@babel/plugin-transform-runtime结合的方案。

@babel/preset-react

@babel/preset-react preset 负责转换 JSX 等语法。

配置项

配置项类型默认值含义
runtime"classic"或"automatic""classic"是否自动导入 JSX 转换后的函数,默认是不会
developmentBooleanfalse是否开启开发环境,针对开发环境会启用辅助开发的插件,例如:@babel/plugin-transform-react-jsx-self@babel/plugin-transform-react-jsx-source
throwIfNamespaceBooleanfalse是否在使用 XML 命名空间的标记名是抛出错误;例如<f:image />形式,虽然 JSX 规范允许这样做,但是默认情况下是被禁止的,因为 React 的 JSX 目前并不支持这种方式
importSourceStringreact设置函数导入来源的名称
pragmaStringReact.createElement替换编译 JSX 表达式时使用的函数
pragmaFragStringReact.Fragment设置 JSX fragments 语法转换后的函数
useBuiltInsBooleanfalse是否使用原生内置的 polyfill,而不是通过其他插件进行 polyfill
useSpreadBooleanfalse当展开props的时候,使用 inline object 形式,而不是使用 Babel 拓展或者Object.assign复制对象

inline object 形式:

{
  (firstName = 'john'), (lastName = 'walter');
}
复制代码

针对不同环境配置@babel/preset-react,以在开发环境使用辅助开发的插件

module: {
  rules: [
    {
      test: /\.m?js$/,
      exclude: /(node_modules|bower_components)/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: [
            '@babel/preset-env',
            [
              '@babel/preset-react',
              {
                development: isDevelopment, //开发环境
                useBuiltIns: true,
              },
            ],
          ],
        },
      },
    },
  ];
}
复制代码

@babel/plugin-transform-runtime

Babel 会使用一些非常小的辅助性的代码插入到需要编译的源代码中,有时候这些代码是重复的,会增加代码体积。通过@babel/plugin-transform-runtime这个 plugin 可以禁用 Babel 自动对每个文件的 runtime 注入;然后通过安装@babel/runtime将 Babel 的辅助代码作为一个独立的依赖模块来引入,这样就可以避免编译后的代码中重复出现辅助性的代码,减小代码体积。

yarn add @babel/plugin-transform-runtime @babel/runtime -D
复制代码
module.exports = {
  module: {
    rules: [
      {
        test: /\.m?js$/,
        exclude: /(node_modules)/,
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env'],
          plugins: ['@babel/plugin-transform-runtime'],
        },
      },
    ],
  },
};
复制代码

@babel/plugin-transform-runtime会进行以下三项任务:

  • 当项目中使用生成器或者异步async函数的时候,自动引入@babel/plugin-transform-runtime
  • 使用core-js polyfill,可以通过corejs配置
  • 默认移除 Babel 的辅助代码,使用@babel/runtime/helpers代替,也就是上文说的减少代码体积的优化,可以通过helpers选择是否开启

配置项

配置项类型默认值含义
corejsfalse
2,3
{version:2或3,proposals:boolean}
false指定core-js库的版本
helpersBooleantrue是否将 Babel 插入的辅助代码切换为模块调用
regeneratorBooleantrue是否将生成器函数转化为生不会污染全局范围的运行时生成器
useESModulesBooleanfalse是否使用辅助代码转换不通过@babel/plugin-transform-modules-commonjs的代码
absoluteRuntimeBooleanStringfalse
versionStringfalse指定@babel/runtime-corejs的版本

polyfill

这个插件的另一个用途就是结合pure版本的core-js-pure库来做 polyfill,简化core-js-pure的导入语法,例如原本需要使用模块导入的语法,会自动进行转换。

// 本来的core-js-pure用法
import from from 'core-js-pure/stable/array/from';
import flat from 'core-js-pure/stable/array/flat';
import Set from 'core-js-pure/stable/set';
import Promise from 'core-js-pure/stable/promise';

from(new Set([1, 2, 3, 2, 1]));
flat([1, [2, 3], [4, [5]]], 2);
Promise.resolve(32).then(x => console.log(x));

// 引入@babel/plugin-transform-runtime的简化写法
Array.from(new Set([1, 2, 3, 2, 1]));
[1, [2, 3], [4, [5]]].flat(2);
Promise.resolve(32).then(x => console.log(x));
复制代码

使用@babel/plugin-transform-runtime结合core-js-pure需要安装对应的@babel/runtime

corejs对应安装的库
falseyarn add @babel/runtime
2yarn add @babel/runtime-corejs2
3yarn add @babel/runtime-corejs3

默认情况下,@babel/plugin-transform-runtime只会采用稳定的 whatwg 规范内容或者 ES 规范内容,但是可以通过corejs.proposals配置项指定其采用尚未稳定的提案。

Note:@babel/plugin-transform-runtime@babel/preset-env都有corejs配置项,不要搞混了,而且不要同时配置,否则会冲突。

module.exports = {
  module: {
    rules: [
      {
        test: /\.m?js$/,
        exclude: /(node_modules)/,
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env'],
          plugins: [
            [
              '@babel/plugin-transform-runtime',
              {
                corejs: {
                  version: 3,
                  proposals: true,
                },
              },
            ],
          ],
        },
      },
    ],
  },
};
复制代码
文章分类
前端
文章标签