Babel 7.4.0

491 阅读9分钟

github.com/willson-wan… 深入理解babel生态

polyfill

polyfill的英文意思是填充工具,意义就是兜底的东西;为什么会有polyfill这个概念,因为ECMASCRIPT一直在发布新的api,当我们使用这些新的api的时候,在旧版本的浏览器上是无法使用的,因为旧的版本上是没有提供这些新的api的,所以为了让代码也能在旧的浏览器上跑起来,于是手动添加对应的api,这就是polyfill;

core-js2.x常用的引入polyfill方式

require('core-js'); // 全局polyfill
var core = require('core-js/library'); // 没有全局污染的polyfill
require('core-js/shim'); // 仅有Shim

require('core-js/fn/array/find-index'); // 单个api polyfill
var findIndex = require('core-js/library/fn/array/find-index'); // 无全局污染的单个api polyfill

全局polyfill与单个api polyfill的区别:

全局是引入所有api的polyfill;而单个只是引入某一个api的polyfill;

所以在项目中如果知道使用了哪些api,那么仅引入对应的api的polyfill即可,这样可以减少包的体积;

全局污染

全局polyfill与没有全局污染的polyfill的区别:有污染的指的是直接在window上添加静态方法or属性,在Array等构造函数上添加静态方法or原型方法,如findIndex方法,会直接添加到Array.prototype的原型上,这就直接污染了Array.prototype;

而无污染指的是: 放在了core全局变量or直接export该api;

browserslist

browserslist用于在不同前端工具(如Autoprefixer、babel)之间共享目标浏览器和Node.js的版本配置;

告诉前端工具,当前项目运行的目标浏览器及node版本是多少,这样工具可以根据目标浏览器及node版本添加对应的css前缀及语法是否需要转化;

browserslist的配置方式:

  1. 在package.json内添加browserslist字段
 "browserslist": [
    "last 1 version",
    "> 1%",
    "maintained node versions",
    "not dead"
  ]
  1. 单独的.browserslistrc配置文件
    last 1 version
    > 1%
    maintained node versions
    not dead
  1. 各个工具中对应的属性,如babel中的targets属性
{
    "presets": [
        [
            "@babel/preset-env",
            {
                "targets": {
                    "node": "4",
                    "browsers": "edge >= 13"
                }
            }
        ]
    ]
}

github.com/willson-wan…

preset runtime

@babel/preset-env 和 @babel/transform-runtime 现在都支持 core-js@3,@babel/template 也新增一些新语法!

useBuiltIns 和 @babel/plugin-transform-runtime冲突?

两者在引入polyfills方面是冲突的,不建议同时使用。均为了引入polyfill,前者在全局范围内引入polyfills,后者只在引入的文件内作用。

transform-runtime和环境无关,不会因为你的目标环境动态调整polyfill的内容;而useBuildInts会。

使用场景:前者用于项目;后者用于开发第三方库。

改进: babel作者对useBuiltIns做了改进,使它也能像transform-runtime一样,局部引入polyfills,但同时能使用preset-env's的targets,对目标环境配置。

下面是引入polyfill的方案:

方案1 preset-env + useBuildIns配置:实现polyfill

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ]
  ]
}

上面配置既能转换高级语法,又能按需引入polyfill来转换api。

缺点是:会产生重复的helper冗余代码,且引入的polyfill会破环全局环境。

方案2 transform-runtime + runtime-corejs3 :实现polyfill

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": false
      }
    ]
  ],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": 3
      }
    ]
  ]
}

缺点是:针对polyfill的targets或者browserslist配置会失效。因为transform-runtime环境无关,不会因为你的页面的目标浏览器动态调整polyfill的内容;而useBuildInts会。

测试代码

1, helper函数相关的处理

首先我们需要了解什么是 helpers?

是辅助函数,是帮助 babel transform 的时候用的,帮助语法降级,都放在 babel-helpers这个包中。如果 babe 编译的时候,检测到某个文件需要这些 helpers,在编译成模块的时候,会放到模块的顶部。

(注: helpers和polyfill没有关系。)

如下配置babel:(关闭了preset-env的polyfill的功能, 未启用transform-runtime的配置)

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": false
      }
    ]
  ]
}

然后准备2个ES文件,运行babel,对该文件转码,最后结果为:

截屏2021-06-08 上午11.24.24.png 上面转码后的结果中:_classCallCheck、_defineProperties等都属于babel内部的helper函数。 这些函数在babel转码后的每个文件中都会重复存在。

2,通过transform-runtime引入runtime中的modules来优化helper函数

修改babel配置如下:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": false
      }
    ]
  ],
  "plugins": [
    "@babel/plugin-transform-runtime"
  ]
}

重新运行babel转码,结果为:

截屏2021-06-08 上午11.48.27.png

从这个结果可以清晰地看到,transform-runtime把helper函数,都转换成了对@babel/runtime内modules的引用,@babel/runtime将被编译到输出文件里。

同时也能看出,为什么@babel/runtime要安装在dependencies下。

总结: transform-runtime插件可以把这些重复的helper函数,转换成公共的、单独的依赖引入,从而节省转码后的文件大小,这也是@babel/plugin-transform-runtime插件出现的主要原因。

3,@babel/preset-env + cojs3 + useBuiltIns 实现polyfill

修改babel配置如下:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ]
  ]
}

重新运行babel转码,结果为:

截屏2021-06-08 下午3.04.04.png

上面的 babel 配置,可以实现按需引入polyfill,还可以针对目标环境精准转换 。那么,还存在那些问题呢?有2个问题

第一个问题:就是上面已经提到过的helper函数,存在重复定义,代码冗余问题。

第二个问题是:从上面的转译结果可以看到,includes 这个 api 直接是 require 了一下:

require("core-js/modules/es.array.includes.js");

并不是另一种更符合直觉的方式:

var includes = require('xxx/includes')

这种机制,对于例如 includes 等实例方法,直接在 global.Array.prototype 上添加。这样直接修改了全局变量的原型,有可能会带来意想不到的问题。

这个问题在开发第三方库的时候尤其重要,因为我们开发的第三方库修改了全局变量,有可能和另一个也修改了全局变量的第三方库发生冲突,或者和使用我们的第三方库的使用者发生冲突。公认的较好的编程范式中,也不鼓励直接修改全局变量、全局变量原型。

4,@babel/plugin-transform-runtime + @babel/runtime-corejs3 实现polyfill

babel.config.json配置为:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": false
      }
    ]
  ],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": 3
      }
    ]
  ]
}

转换结果为:

截屏2021-06-08 下午3.19.50.png 可以看出:

  • 转换预发需要的辅助函数在@babel/runtime-corejs3/helpers/xxx下;
  • 转换api需要要的polyfills在@babel/runtime-corejs3/core-js-stable/xxx下。
  • 避免了对全局变量及其原型的污染;
  • helpers 从之前的原地定义改为了从一个统一的模块中引入,使得打包的结果中每个 helper 只会存在一个。

5,同时开启preset-env和transform-runtime的polyfill功能 ?

配置如下:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ]
  ],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": 3
      }
    ]
  ]
}

转换结果:

截屏2021-06-08 下午3.34.10.png 会出现:preset-env的polyfill与transform-runtime的polyfill并存的现象。但helper函数的引入只有transform-runtime有效。 这样的转码结果肯定是有问题的,这两个属于不同的polyfill做法,有不同的应用场景。 所以这两种polyfill不能同时启用。

6,开发应用,最佳配置???

常规业务项目,推荐使用env-useBuiltIns-usage、corejs3,且配合@babel/plugin-transform-runtime,corejs-false,因为我们不需要考虑全局污染带给我们的影响,因为这些都是可控的,babel配置如下所示:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ]
  ],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": false,
        "regenerator": false
      }
    ]
  ]
}

截屏2021-06-08 下午3.41.13.png helpers函数的引入是@babel/plugin-transform-runtime生效;polyfills的引入是"useBuiltIns": "usage"生效。

安装

npm install --save core-js@3

yarn add core-js@3

yarn add @babel/runtime

yarn add @babel/runtime-corejs3

或者:

npm install --save-dev @babel/core @babel/cli

其他:

@babel/preset-flow、 @babel/preset-react、 @babel/preset-typescript

注意: corejs和runtime包,是安装在dependencies里面的。

部分package.json

{
  "browserslist": [
        "last 2 version",
        "> 1%",
        "IE 10"
  ],
  "devDependencies": {
        "@babel/cli": "^7.14.3",
        "@babel/core": "^7.14.3",
        "@babel/plugin-syntax-dynamic-import": "^7.8.3",
        "@babel/preset-env": "^7.14.2",
        "@babel/preset-react": "^7.13.13",
        "babel-loader": "^8.0.0-beta.4",
        "clean-webpack-plugin": "^0.1.19",
        "copy-webpack-plugin": "^4.6.0",
        "css-loader": "^1.0.0",
        "cssnano": "^4.1.4",
        "file-loader": "^1.1.11",
        "html-webpack-plugin": "^3.2.0",
        "optimize-css-assets-webpack-plugin": "^5.0.1",
        "sass-loader": "^7.0.3",
        "style-loader": "^0.21.0",
        "webpack": "^4.43.0",
        "webpack-bundle-analyzer": "^4.4.0",
        "webpack-cli": "^3.3.11",
        "webpack-dev-server": "^3.11.2",
        "webpack-merge": "^5.8.0"
  },
   "dependencies": {
    "@ant-design/icons": "^4.6.0",
    "@babel/runtime": "^7.14.0",
    "@babel/runtime-corejs3": "^7.14.0",
    "antd": "^4.16.0",
    "axios": "^0.21.1",
    "classnames": "^2.3.1",
    "core-js": "3",
    "dayjs": "^1.8.30",
    "history": "^5.0.0",
    "lodash": "^4.17.20",
    "mobx": "^6.1.4",
    "mobx-react": "^7.1.0",
    "react": "^17.0.1",
    "react-dom": "^17.0.1",
    "react-router": "^5.2.0",
    "react-router-dom": "^5.2.0"
  },
}

配置文件

v7.7.0之后,建议配置文件名:babel.config.json 或 .babelrc.json:

 {
  "presets": [
    [
      "@babel/preset-env",
      {
       "targets": {
            ios: 8,
            android: 4
        },
        "useBuiltIns": "usage",
        "debug": true,
        "corejs": 3,
      }
    ]
  ],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": false,
        "regenerator": false
      }
    ]
  ]
}

Babel api

常用参数和方法:path、Scope

插件(设计模式中称为‘具体访问者’)只需要定义自己感兴趣的节点类型,当访问者访问到对应节点时,就调用插件的访问(visit)方法。这时babel会为我们的访问者方法传入一个path参数,那么这个path参数的作用是什么呢?

path

  • path参数的作用就是关联节点,是表示两个节点之间连接的对象;
  • 把AST看成一棵树,树上节点之间的关联关系通过path来表示;这样最终是用一个可操作和访问的巨大可变对象表示节点之间的关联关系。

常用Path操作方法:

1, 新增节点 :

insertBefore(nodes: [Object]) // 当前节点之前插入新节点
 insertAfter() // 当前节点之后插入新节点

2, 删除节点:

BooleanLiteral(path) {
  path.remove(); // remove 删除当前节点
}

3, 替换节点

 // 单节点替换 replaceWith(replacement: Object)
BooleanLiteral(path) {
  path.replaceWith(t.identifier("bar"));
}

// 多节点替换 replaceWithMultiple(nodes: [Object])
BooleanLiteral(path) {
  const nodes = [
    t.identifier("foo"),
    t.identifier("bar")
  ];

  path.replaceWithMultiple(nodes);
}

scope

当我们在操作AST时,需要考虑作用域Scope;babel中为了我们能够更好的操作AST,提供了Scope,每个Scope包含如下信息

{
  path: path,
  block: path.node, 
  parentBlock: path.parent,  // 父节点
  parent: parentScope, // 父作用域
  bindings: [...]  // 当前作用域的所有绑定
}

Scope 对象和 Path 对象差不多,它包含了作用域之间的关联关系(通过parent指向父作用域),收集了作用域下面的所有绑定(bindings), 另外还提供了丰富的方法来对作用域仅限操作。

常用Scope操作方法

  1. generateUidIdentifier(name: string = "temp") 生成一个uniq ID并返回一个标识符
  2. generateUid(name: string = "temp")生成一个uniq ID并返回一个字符串
  3. rename(oldName: string, newName?: string, block?: Object) 重命名当前作用域内的某个变量名
  4. getBinding(name: string) 获取某个name对应的绑定关系
  5. getBindingIdentifier(name: string) 获取某个name对应的绑定关系的标识
  6. getOwnBindingIdentifier(name: string) 获取name所在作用域对应的绑定关系的标识
  7. hasOwnBinding(name: string) 判断name是否定义在当前作用域
  8. parentHasBinding(name: string, noGlobals?: boolean) 判断name是否定义父前作用域

怎样写一个babel插件

下面让我们一起写个插件

针对传入的library进行按需编译

module.exports = function ({ types: t }) {
    return {
        visitor: {
            ImportDeclaration(path, {opts}) {
                if (!opts.library) return
                let librarys = []
                if (Array.isArray(opts.library)) {
                    librarys = opts.library
                } else {
                    librarys = opts.library.split(',')
                }
                // get方法获取某个属性的路径,如果是数组的话,就直接是一个数组
                const specifiers = path.get('specifiers');
                if (!specifiers.length) return;
                const source = path.get('source');
                const library = source.node.value
                if (librarys.indexOf(library) > -1) {
                    const nodes = specifiers.map((it) => {
                        const localNode = it.get("local").node
                        const importedNode = it.get("imported").node
                        return t.ImportDeclaration(
                            [t.importDefaultSpecifier(localNode)], 
                            t.StringLiteral(`${library}/${importedNode.name}`)
                        )
                    })
                    path.replaceWithMultiple(nodes)
                }
            }
        }
    }
}

参考链接

@babel/plugin-transform-runtime 到底是什么? : zhuanlan.zhihu.com/p/147083132

blog.liuyunzhuge.com/2019/09/04/… 详解 流云诸葛

blog.windstone.cc/es6/babel/@… 风动之石的博客

segmentfault.com/a/119000002…

segmentfault.com/a/119000002… # Babel7 转码(五)- corejs3 的更新