众所周知,babel作为转换利器,在前端工程中大量使用,从根本上来说,babel是一个ast,再说大点,大家都能写babel。嗯,好,牛吹大了,收。
上面是babel的工作原理,中间 接下来先介绍下babel的生态 先说说几个概念 plugins, preset, preset是plugins的集合,你可以将Preset理解称为就是一些的Plugin整合称为的一个包。
preset
先看看流行的几个preset 首先是stage几兄弟,这里不多说,stage-0包括万象,遇事不决就是它,因为它是在-1,-2,-3基础上再开发的。下面是几个的区别:
- Stage 0 - 稻草人阶段,草案阶段。
- Stage 1 - 提议: 探讨阶段。
- Stage 2 - 草稿: 可以做规范的初始化了
- Stage 3 - 候选: 完整的规范和初始浏览器实现。
- Stage 4 - 结束: 将被添加到下一个年度版本中。所在在一般的项目设置中,直接就上stage-0,诶,用不用无所谓,我都转。 ** 然后是preset-env**,现在看下这位选手,这是babel之前产物preset-es2019,2018的最终版本,抛弃年限之后,集大成者。这个东西相当于通过配置browerlist来自动加载对应plugins,(默认加载了stage-4)但是它只会去转换新的语法,也就是说,还在规范之外的语法,是转换不了的,需要另外加载stage<=3,当然如果觉得引入过大,可以直接分批引入plugin,转换对应的非标准语法;并且对于新的api,env是不会进行转换的,所以还需要进行额外的工具装换。(这里留一个问题,语法阶段,和api啥区别,为啥有些只能转语法,不能转api) 下面就是不用多说的**preset-react,**很简单,顾名思义,就是转换react语法的,预设的就是编译jsx。 preset-typescript,也很容易看出,这就是编译typescript用的, 毕竟这玩意只是编写阶段的一个工具。
plugins
好的,接下来到了plugins,其实大部分的plugins都已经被集成到了上面的preset-env中,假如你发现你的项目不支持某种语法的时候,可以到babel官网中查询具体需要的plugins。 这里再列一些不常见的plugins(或者说packages),比如@babel/register:它会改写require命令,为它加上一个钩子。此后,每当使用require加载.js、.jsx、.es和.es6后缀名的文件,就会先用Babel进行转码。如果接手的项目中碰到了不认识的babel packages,不明所以,不知用法,可以直接在babel官网中查询,毕竟度娘找到的文章很可能已经过时。 先简单说下plugins,@babel/plugin-transform-runtimez,这个是接入后面polyfill的时候需要用到的玩意,后面介绍。
在项目中的配置和搭建
介绍了几个概念之后,我们可以试试把它搭建进我们的项目中。 这里默认大家使用的构建工具都是webpack,谁赞成,谁反对,反对的等下,我日后再更,vite正在路上。 webpack主要使用babel的几个packages,
- babel-loader
- babel-core
- babel-preset-env首先我们需要清楚在 webpack中loader****的本质就是一个函数,接受我们的源代码作为入参同时返回新的内容。
@babel/loader
本质上就是一个函数(柯理化),只负责获得对应的文件,请看伪代码
/**
* @param sourceCode 匹配到的源代码内容
* @param options babel-loader相关参数
* @returns 处理后的代码
*/
function babelLoader (sourceCode,options) {
// 这里是其他的插件处理的事情
// 处理后的代码
return targetCode
}
关于options,babel-loader支持直接通过loader的参数形式注入,同时也在loader函数内部通过读取.babelrc/babel.config.js/babel.config.json等文件注入配置,例如添加一些preset等,一般情况下,笔者更倾向于使用.babelrc文件进行配置,无他,手熟尔。
@babel/core
我们讲到了babel-loader仅仅是识别匹配文件和接受对应参数的函数,那么babel在编译代码过程中核心的库就是@babel/core这个库。 babel-core是babel最核心的一个编译库,他可以将我们的代码进行词法分析--语法分析--语义分析过程从而生成AST抽象语法树,从而对于“这棵树”的操作之后再通过编译称为新的代码。 babel-core其实相当于@babel/parse和@babel/generator这两个包的合体,接触过js编译的同学可能有了解esprima和escodegen这两个库,你可以将babel-core的作用理解称为这两个库的合体。 babel-core通过transform方法将我们的代码进行编译。 关于babel-core中的编译方法其实有很多种,比如直接接受字符串形式的transform方法或者接受js文件路径的transformFile方法进行文件整体编译。 那么根据这个,我们再完善下伪代码
const core = require('@babel/core')
/**
* @param sourceCode 匹配到的源代码内容
* @param options babel-loader相关参数
* @returns 处理后的代码
*/
function babelLoader (sourceCode,options) {
const tCode = core.transform(sourceCode)
// 这里是其他的插件处理的事情
// 处理后的代码
return targetCode
}
如果比作一件事情,那就比作做一个板凳,我们有了木板(源代码),锯子(core),工具台(loader),那么还差什么,做的规则,总不能哗哗一顿乱锯,最后是得不到板凳的。所以我们要有一个规则。 也就是说,我们需要告诉babel以什么样的规则进行转化,比如我需要告诉babel:“嘿,babel。将我的这段代码转化称为EcmaScript 5版本的内容!”。plugins和preset就是干这个用的,把ast再转换,最终成为自己想要的代码,那我们继续完善下伪代码
const core = require('@babel/core')
/**
* @param sourceCode 匹配到的源代码内容
* @param options babel-loader相关参数
* @returns 处理后的代码
*/
function babelLoader (sourceCode,options) {
const tCode = core.transform(sourceCode, {
presets: ['@babel/preset-env'],
plugins: [...], // 注意,这里的plugins可能和会presets中的重合,毕竟这两个本质一样
})
// 这里是其他的插件处理的事情
// 处理后的代码
return targetCode
}
polyfill
好了,主要的功能和工作路径其实都了解的差不多了,我们发现一个问题,之前一直说的,大部分只能转语法,而不能转api的问题还是没能有答案,这就引出了polyfill。 首先我们理清楚几个概念:
-
最新ES语法,比如:箭头函数,let/const。
-
最新ES Api,比如Promise
-
最新ES实例/静态方法,比如Array.prototype.includes第二个可能会稍微比较难理解,可能觉得跟第一个差不多,但是只要再举一个例子, intersectionObserver,也就能理解了。 回到正题,preset-env只能转es语法,所以就需要另外的polyfill来补充api和实例方法了 其实可以稍微简单总结一下,语法层面的转化preset-env完全可以胜任。但是一些内置方法模块,仅仅通过preset-env的语法转化是无法进行识别转化的,所以就需要一系列类似”垫片“的工具进行补充实现这部分内容的低版本代码实现。这就是所谓的polyfill的作用。 针对于polyfill方法的内容,babel中涉及两个方面来解决:
-
@babel/polyfill
-
@babel/runtime
-
@babel/plugin-transform-runtime我们理清了何谓polyfill以及polyfill的作用和含义后,让我们来逐个击破这两个babel包对应的使用方式和区别吧。
@babel/polyfill
详尽的介绍也可以直接到官网中去查看。 比如说我们需要支持String.prototype.include,在引入babelPolyfill这个包之后,它会在全局String的原型对象上添加include方法从而支持我们的Js Api。 我们说到这种方式本质上是往全局对象/内置对象上挂载属性,所以这种方式难免会造成全局污染。 它的引入现在也是十分简单,在babel-preset-env中存在一个useBuiltIns参数,这个参数决定了如何在preset-env中使用@babel/polyfill,也就是说,配置只需要一个参数,舒服
{
"presets": [
["@babel/preset-env", {
"useBuiltIns": false // "usage"| "entry"| false
// entry: 当传入entry时,需要我们在项目入口文件中手动引入一次core-js,它会根据我们配置的浏览器兼容性列表(browserList)然后全量引入不兼容的polyfill。
//
}]
]
}
userBuiltIns默认为false,表示不引入。 entry 当是entry时,需要我们在项目入口文件中手动引入一次core-js,它会根据我们配置的浏览器兼容性列表(browserList)然后全量引入不兼容的polyfill。但是,在 在Babel7.4.0之后,@babel/polyfill被废弃它变成另外两个包的集成: "core-js/stable", "regenerator-runtime/runtime"; 你可以在这里看到变化,但是他们的使用方式是一致的,只是在入口文件中引入的包不同了。也就是说,我们 的引入方式
// 项目入口文件中需要额外引入polyfill
// core-js 2.0中是使用"@babel/polyfill" core-js3.0版本中变化成为了上边两个包
// 2.0
import "@babel/polyfill"
// 3.0
import "core-js/stable"
import "regenerator-runtime/runtime"
// babel
{
"presets": [
["@babel/preset-env", {
"useBuiltIns": "entry"
}]
]
}
同时需要注意的是,在我们使用useBuiltIns:entry/usage时,需要额外指定core-js这个参数。默认为使用core-js 2.0,所谓的core-js就是我们上文讲到的“垫片”的实现。它会实现一系列内置方法或者Promise等Api。core-js 2.0版本是跟随preset-env一起安装的,不需要单独安装。 (既然已经有了3.0,为什么不连着preset-env的内置一起更改呢?) usage 上边我们说到配置为entry时,perset-env会基于我们的浏览器兼容列表进行全量引入polyfill。所谓的全量引入比如说我们代码中仅仅使用了Array.from这个方法。但是polyfill并不仅仅会引入Array.from,同时也会引入Promise、Array.prototype.includes等其他并未使用到的方法。这就会造成包中引入的体积太大了。 这就需要我们用到我们的usage了,按需引入,这个看起来就很帅吧,而且而且,还不需要在入口处再单独引用polyfill,舒服。
{
"presets": [
["@babel/preset-env", {
"useBuiltIns": "usage",
"core-js": 3
}]
]
}
接下来看下这两个配置的区别 我们以项目中引入Promise为例。 当我们配置useBuintInts:entry时,仅仅会在入口文件全量引入一次polyfill。你可以这样理解:
// 当使用entry配置时
...这里表示
// 一系列实现polyfill的方法
global.Promise = promise
// 其他文件使用时
const a = new Promise()
这就直接引入到global里面了,而且还不止是一个promise,是全部的兼容信息。 而当我们使用useBuintIns:usage时,preset-env只能基于各个模块去分析它们使用到的polyfill从而进入引入 preset-env会帮助我们智能化的在需要的地方引入,比如:
// a. js 中
import "core-js/modules/es.promise";
...
// b.js
import "core-js/modules/es.promise";
上面我们也明显看出来了,每个模块都会引入这个,代码十分冗余,并且重复。 所以usageBuintIns不同参数分别有不同场景的适应度,具体参数使用场景还需要大家结合自己的项目实际情况找到最佳方式。
@babel/runtime
充分吸取上面的不足点之后,runtime应运而生,比起polyfill,runtime才更能代表按需加载的解决方案,而且副作用更小。 比如哪里需要使用到Promise,@babel/runtime就会在他的文件顶部添加import promise from 'babel-runtime/core-js/promise'。 同时上边我们讲到对于preset-env的useBuintIns配置项,我们的polyfill是preset-env帮我们智能引入。 而babel-runtime则会将引入方式由智能完全交由我们自己,我们需要什么自己引入什么。 它的用法很简单,只要我们去安装npm install --save @babel/runtime后,在需要使用对应的polyfill的地方去单独引入就可以了。比如:
// a.js 中需要使用Promise 我们需要手动引入对应的运行时polyfill
import Promise from 'babel-runtime/core-js/promise'
const promsies = new Promise()
总而言之,babel/runtime你可以理解称为就是一个运行时“哪里需要引哪里”的工具库。 (重点,这里是重点,我们这时候根本看不出来,runtime和babel-polyfill的区别,举个例子,Object.assign在a文件中用到的时候,babel-polyfill会在a文件下,直接引入,给Object原型下添加assign方法,而runtime是在改文件引入这个polyfill方法,用这个方法实现等同于Object.assign方法,从而不造成全局污染。) 当然这也就带来了一些问题,比如需要频繁的自己定义,不太智能,所以一般情况下,我们都会配合@babel/plugin-transfrom-runtime进行使用达到智能化runtime的polyfill引入。 runtime带来的问题当然显而易见,babel-runtime在我们手动引入一些polyfill的时候,它会给我们的代码中注入一些类似_extend(), classCallCheck()之类的工具函数,这些工具函数的代码会包含在编译后的每个文件中,比如:
class Circle {}
// babel-runtime 编译Class需要借助_classCallCheck这个工具函数
function _classCallCheck(instance, Constructor) { //... }
var Circle = function Circle() { _classCallCheck(this, Circle); };
如果我们项目中存在多个文件使用了class,那么无疑在每个文件中注入这样一段冗余重复的工具函数将是一种灾难。 所以针对上述提到的两个问题:
- babel-runtime无法做到智能化分析,需要我们手动引入。
- babel-runtime编译过程中会重复生成冗余代码。于是就有下文
@babel/plugin-transfrom-runtime
智能化分析和引入runtime,@babel/plugin-transform-runtime插件的作用恰恰就是为了解决上述我们提到的run-time存在的问题而提出的插件。
-
babel-runtime无法做到智能化分析,需要我们手动引入。@babel/plugin-transform-runtime插件会智能化的分析我们的项目中所使用到需要转译的js代码(ast),从而实现模块化从babel-runtime中引入所需的polyfill实现。
-
babel-runtime编译过程中会重复生成冗余代码。(也就是callCheck这类)@babel/plugin-transform-runtime插件提供了一个helpers参数。具体你可以在这里查阅它的所有配置参数。 函数的方法可以在这里 这个helpers参数开启后可以将上边提到编译阶段重复的工具函数,比如classCallCheck, extends等代码转化称为require语句。此时,这些工具函数就不会重复的出现在使用中的模块中了。比如这样:
// @babel/plugin-transform-runtime会将工具函数转化为require语句进行引入
// 而非runtime那样直接将工具模块代码注入到模块中,可以和上面的代码对比一下,搞下立判
var _classCallCheck = require("@babel/runtime/helpers/classCallCheck");
var Circle = function Circle() { _classCallCheck(this, Circle); };
其实用法原理部分已经在上边分析的比较透彻了,可以babel官网查看。 .babelrc
{
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"absoluteRuntime": false,
"corejs": false,
"helpers": true,
"regenerator": true,
"version": "7.0.0-beta.0"
}
]
]
}
总结polyfill
可以看到针对polyfill其实我耗费了不少去将它们之间的区别和联系,让我们来稍微总结一下吧。 在babel中实现polyfill主要有两种方式:
- 一种是通过@babel/polyfill配合preset-env去使用,这种方式可能会存在污染全局作用域。
- 一种是通过@babel/runtime配合@babel/plugin-transform-runtime去使用,这种方式并不会污染作用域。
- 全局引入会污染全局作用域,但是相对于局部引入来说。它会增加很多额外的引入语句,增加包体积。在useBuintIns:usage情况下其实和@babel/plugin-transform-runtime情况下是类似的作用。 其实,可以总结起来,组件开发中(例如前端组内公用的选择器组件),尽量不要污染全局变量,使用runtime,而自己的业务项目内,就可以直接使用polyfill 如有侵权,请联系笔者 参考: 前端基建」带你在Babel的世界中畅游 ast解析