gmfe / Think

观麦前端团队的官方博客
68 stars 3 forks source link

迁移babel7+tree-shaking #67

Open Realwate opened 5 years ago

Realwate commented 5 years ago

babel7主要变化

废弃 babel-preset-stage-x

babel团队认为,非标准的特性是不稳定的(通过stage来推进),按照stage来划分需要花很多精力来维护。

对应 stage < 3plugin 也修改了命名,从@babel/plugin-transform-* 改为 @babel/plugin-proposal-* proposal 表示这只是一个提案中的非标准特性,随时可能变化,使用前要考虑清楚。

废弃 babel-preset-es20xx

使用 babel-preset-env 不需要自己决定去用哪些 preset ,而是由 env 自动的根据要支持的环境启用相应的 preset

使用 Scoped Packages

更加清晰,易于区分 official package 和 community package

如: babel-core -> @babel/core babel-preset-env -> @babel/preset-env

迁移步骤

# 会修改 babelrc 和 package.json
npx babel-upgrade --write
// babelrc before
{
  "presets": [
    "react",
    "es2015",
    "stage-0"
  ],
  "plugins": [
    "transform-decorators-legacy",
    "react-hot-loader/babel"
  ]
}
// after
{
  "presets": [
    "@babel/preset-react",
    "@babel/preset-env"
  ],
  "plugins": [
    [
      "@babel/plugin-proposal-decorators",
      {
        "legacy": true
      }
    ],
    "react-hot-loader/babel",
    "@babel/plugin-syntax-dynamic-import",
    "@babel/plugin-syntax-import-meta",
    "@babel/plugin-proposal-class-properties",
    "@babel/plugin-proposal-json-strings",
    "@babel/plugin-proposal-function-sent",
    "@babel/plugin-proposal-export-namespace-from",
    "@babel/plugin-proposal-numeric-separator",
    "@babel/plugin-proposal-throw-expressions",
    "@babel/plugin-proposal-export-default-from",
    "@babel/plugin-proposal-logical-assignment-operators",
    "@babel/plugin-proposal-optional-chaining",
    [
      "@babel/plugin-proposal-pipeline-operator",
      {
        "proposal": "minimal"
      }
    ],
    "@babel/plugin-proposal-nullish-coalescing-operator",
    "@babel/plugin-proposal-do-expressions",
    "@babel/plugin-proposal-function-bind"
  ]
}

这样就很快完成了 babel7 的升级,但是还不够。

  1. 因为我们之前是直接引用 preset-stage-0,升级后实际上很多 proposal-plugin 是没用到的(扫下每个插件的文档)
  2. babel7config-file 也有一些变化,简单来说,它提供了两种配置 babel.config.js.babelrcbabel.config.js 作为整个项目通用的配置,可作用于 node_modules。而 .babelrc 只作为所在 package 的配置,不会影响到其他package。 这意味着我们项目根目录下配置的 .babelrcnode_modules 是不起作用的,而我们很多库都依赖 babel 的转译。

解决:项目根目录新建个 babel.config.js ,同时删掉没用到的 proposal-plugin (package.json中也对应删除),结果如下。

// babel.config.js
module.exports = (api) => {
  api.cache(true)
  return {
    'presets': [
      '@babel/preset-react',
      '@babel/preset-env'
    ],
    'plugins': [
      [
        '@babel/plugin-proposal-decorators',
        {
          'legacy': true
        }
      ],
      'react-hot-loader/babel',
      '@babel/plugin-syntax-dynamic-import',
      [
        '@babel/plugin-proposal-class-properties',
        { 'loose': true }
      ],
      '@babel/plugin-proposal-function-bind'
    ]
  }
}

babel-transform-runtime

babel 在转换语法的时候会生成一些helper(如 class 的 classCallCheck 等),@babel/plugin-transform-runtime 这个插件会将所有生成 helper 的地方,统一从@babel/runtime引入,减少了冗余代码。

babel.config.js 中添加 @babel/plugin-transform-runtime,然后 install

npm i @babel/plugin-transform-runtime -D
npm i @babel/runtime

因为我们的编译包括 node_modules,要忽略 @babel/runtime 本身的处理。

node_modules下的包可能使用了多种模块格式,而 babel-plugin-transform-runtime 默认是添加 import 语句,wabpack中同一模块下,import 语句不能和 module.exports 同时使用 (babel文档 webpack相关issue) (PS: 原来可以是因为 babel 默认会将所有的模块转化成 cjs ,再传给 webpack。但是这样无法做 tree-shaking,下文会提到)

所以添加配置。

'sourceType': 'unambiguous', // 自动推断编译的模块类型
'ignore': [/@babel[/\\]runtime/], // 忽略 @babel/runtime处理

polyfill

polyfill 部分变化不大,继续走 script 引入,不经过 webpack 打包。

tree-shaking

webpackproduction mode 会自动进行 tree-shaking 优化,消除未使用到的模块代码,原理是通过 es6 module 的静态分析。

需要配置 babel 不转换我们的 module,给 env 加个配置,让 webpack 来处理模块。

['@babel/preset-env', {
    'modules': false
}]

我们的代码中(业务代码、库)可能会混用 importmodules.exports ,这在 webapck 中是不允许的。issue

如下代码会报错

// enum.js 混用了 import 和 modules.exports
import { i18next } from 'gm-i18n'
let enum = {
  a, b
}
modules.exports = enum

// 引用模块
import { a } from 'enum'
// 提示
// export 'a' was not found
// console 输出
// enum.js:347 Uncaught TypeError: Cannot assign to read only property 'exports' of object '#<Object>'

解决:统一只使用es6模块。也可以配置babel把模块先转换成cjs格式,但是这样webpack就不能做tree-shaking,不推荐

一般而言,tree-shaking 对业务代码没多大作用,主要是针对 node_modules 下的库。webpack4 新增了 sideEffects 选项,进一步优化了 tree-shaking 的结果。可以看下Webpack 中的 sideEffects 到底该怎么用? 简单来说,如果一个模块的export没有被任何文件用到 -> 去掉这个模块代码不会产生任何影响(反例是polyfill)-> 那就可以设置 sideEffects:false

对于我们自己发布的库,统一使用 es6 module,并且在 package.json 中加上 sideEffects: false(也可以设置为具体的文件),这样跟着业务代码一起打包,就能通过 tree-shaking 去除没用到的模块代码(例如我们的图标库 gm-svg,针对不同的项目去掉没用到的图标)。

最终...

最后的 babel.config.js 文件内容如下


module.exports = (api) => {
  api.cache(true)
  return {
    'sourceType': 'unambiguous', // 自动推断编译的模块类型(cjs,es6)
    'ignore': [/@babel[/\\]runtime/], // 忽略 @babel/runtime
    'presets': [
      '@babel/preset-react',
      ['@babel/preset-env', {
        'modules': false
      }]
    ],
    'plugins': [
      [
        '@babel/plugin-proposal-decorators',
        {
          'legacy': true
        }
      ],
      'react-hot-loader/babel',
      '@babel/plugin-syntax-dynamic-import',
      [
        '@babel/plugin-proposal-class-properties',
        { 'loose': true }
      ],
      '@babel/plugin-proposal-function-bind',
      '@babel/plugin-transform-runtime'
    ]
  }
}

package.json 新增的依赖如下

  "dependencies": {
    "@babel/runtime": "^7.4.3"
    },
  "devDependencies": {
    "@babel/core": "^7.0.0",
    "@babel/plugin-proposal-class-properties": "^7.0.0",
    "@babel/plugin-proposal-decorators": "^7.0.0",
    "@babel/plugin-proposal-function-bind": "^7.0.0",
    "@babel/plugin-syntax-dynamic-import": "^7.0.0",
    "@babel/plugin-transform-runtime": "^7.4.3",
    "@babel/preset-env": "^7.0.0",
    "@babel/preset-react": "^7.0.0",
    "babel-loader": "^8.0.0",
    }
liyatang commented 5 years ago

学到了