2021.11.01
babel的作用
babel是一个工具链,主要将我们业务里面使用的ES6\ES7\ES8等等语法来编写的代码转换为向后兼容的javascript语法,比如转换成ES5
1. 语法转换
2. 通过垫片(polyfill)方式在目标环境中添加缺失的特性(比如babel-core负责转换语法部分,但是api未转换,此时我们就需要引入第三方polyfill模块,比如core-js)
3. 源码转换
通俗点就是比如ES6+的语法转换(如使用箭头函数->普通函数,let,const->var)
比如ES6+的新方法转换(如使用Array.includes等)
babel7变更
在早期babel6时期,我们经常使用babel-cli这类库(中间使用短线连接),但是到了babel7之后,我们所需要的所有babel模块都是作为独立的npm包发布的,都是以"@babel/xxx"来命名。
核心库(@babel/core)
babel这个工具的核心功能包含在"@babel/core"这个模块中,意味着我们必须安装,但不需要最后打包进bundle,只是在开发下帮我们转换,即:
npm install @babel/core -D
命令行工具(@babel/cli)
它就是一个能够从终端(命令行)使用的工具,类似webpack-cli等等,一般独立转换需要安装,通常在业务中我们都是搭配webpack来进行执行,所以这个工具对项目来说我理解可安可不安,但是不确定webpack或者其他依赖babel的库是否依赖@babel/cli,官网也建议安装在本地项目中,即:
npm install @babel/cli -D
在我们大多数的项目中,经常看到对babel的配置一般可以写在:
-
写在babel-loader的options中,进行配置
{ test:/\.js$/i, loader:'babel-loader', options:{ presets:[], plugins:[] } }
-
项目范围的配置(project-wide-config)
这是babel7.x新配置方式,推荐使用 babel.config.json(.js,.cjs,.mjs)具体内部根据后缀名的对应方式书写,比如.cjs就是个module.exports,.json就是个json格式等等 它是项目的根配置,具体看文档描述
-
文件关联配置(file-relative-config)
推荐使用.babelrc(.babelrc.json)\(.js,.cjs,.mjs) 或者package.json中的bebel关键字配置
具体可以看https://github.com/willson-wang/Blog/issues/100
总结来说就是独立项目(非menorepo)下我们在项目根目录下配置一个根目录配置,如babel.config.json(.js,.cjs,.mjs)
若是一个lerna menorepo项目,也同样在根目录下babel.config.json(.js,.cjs,.mjs),子项目可以使用.babelrc指定编译
而这些配置项中,最常见的就是两个配置"preset"&“plugins”
preset(预设)- 插件集合
1. 最初我们都是通过安装plugins来解决翻译,比如@babel/plugin-transform-arrow-functions这个插件就是让我们写的箭头函数翻译为普通函数,但是若其中我们使用了let,const等新语法并未给我们转换,babel奉行一个插件做一个事,这样的话,es6+新增的东西太多了,岂不是需要一个一个插件的引用?
2. 因此babel提出了preset(预设)-它是一类插件的集合,等于说安装它一个就不需要安装其他相同类型的插件了
3. 即官网提供了常用了环境一些预设preset,如:
3.1 @babel/preset-env(转换ES6+的语法,注意是语法!!!)
3.2 @babel/preset-typescript(转换TS)
3.3 @babel/preset-react(转换React)
3.4 @babel/preset-flow(转换Flow)
4. 如何设置preset(首先preset是一个数组格式)
{
// 4.1 字符串配置(外层数组包裹)
"preset":["@babel/preset-env"]
// 4.2 数组字符串配置((外层数组包裹))
"preset":[
["@babel/preset-env"]
]
// 4.3 数组第0个参数是插件,第1个是配置项(外层数组包裹)
"preset":[
[
"@babel/preset-env",
{
"modules": false
}
]
]
}
同时@babel/preset-env 是根据浏览器的不同版本中缺失的功能确定代码转换规则的,在配置的时候我们只用配置需要支持的浏览器版本就好了,@babel/preset-env 会根据目标浏览器生成对应的插件列表然后进行编译,如:
presets: [
["@babel/preset-env", {
targets: {
browsers: ["last 10 versions", "ie >= 9"]
}
}],
],
5. preset解析顺序
preset顺序是从后往前的,如:
{
"preset":["@babel/preset-env", "@babel/preset-react"]
}
将按如下顺序执行: 首先是 @babel/preset-react,然后是 @babel/preset-env。
刚刚我们利用了@babel/preset-env解决了ES6+的语法问题,但实际中我们可能需要使用ES6+的新方法,那么此时@babel/preset-env不能解析,它只负责解析语法,因为babel将js语法分为了:
syntax(类似箭头函数、let、const、class 等在 JavaScript 运行时无法重写的部分,就是 syntax 句法)
api(类似 Promise、includes 等可以通过函数重新覆盖的语法都可以归类为 api 方法)
因此,针对这种情况,社区提出了使用polyfill垫片解决
实验1(通过@babel/core @babel/preset-env)
npm install @babel/core @babel/preset-env -D
// src/index.js
const fn = () => console.log(1);
fn();
const pro = new Promise()
const isIncludes = [1, 2, 3].includes(2)
// babel.config.json
{
"plugins":[],
"presets":[
"@babel/preset-env"
]
}
// bundle.js
/******/ (() => { // webpackBootstrap
var __webpack_exports__ = {};
/*!*********************!*\
!*** ./src/main.js ***!
\*********************/
var fn = function fn() {
return console.log(1);
};
fn();
var pro = new Promise();
var isIncludes = [1, 2, 3].includes(2);
/******/ })()
;
// 结论
可以看出@babel/preset-env在语法上将const/let -》var,将箭头函数转换成了普通函数,而Promise和includes没转换
@babel/polyfill
1. 作用:Babel默认只转换新的javascript语法,而不转换新的API,比如 Iterator, Generator, Set, Maps, Proxy, Reflect,Symbol,Promise 等全局对象。以及一些在全局对象上的方法(比如 Object.assign)都不会转码。
比如说,ES6在Array对象上新增了Array.form方法,即当运行环境中并没有实现的一些方法,babel-polyfill会做兼容。
2. 原理:通过向全局对象和内置对象的prototype上添加方法来实现的。比如运行环境中不支持Array.prototype.find 方法。
3. 缺点:就是会造成全局空间污染
1. @babel/polyfill由"core-js"和"regenerator-runtime/runtime"两部分组成,其中
"core-js":解决 promise 和 includes 相关的垫片
"regenerator-runtime/runtime":解决await/async和Generator相关的垫片
2. 依赖安装
npm i @babel/polyfill -S(需要安装到dependencies)
3. 搭配使用
3.1 在我们的入口文件中,还必须引入import "@babel/polyfill"(早期)而在babel7.4之后废弃了该直接引入方式,转而必须显式的引入两个依赖库
import 'core-js'
import 'regenerator-runtime/runtime'
3.2 同时需要搭配@babel/preset-env这个preset使用,提供了"corejs"和"useBuiltIns"两个配置项
3.2.1 corejs:默认不设置则为corejs2,有些特性并不包含在2中,因此官方推荐设置为3版本
3.2.2 useBuiltIns:转换方式设置
3.2.2.1 false:默认值。不转化 api,只转化 syntax
3.2.2.2 usage - 转化源码里用到的 api
3.2.2.3 entry - 转化所有的 api
根据官方文档中,针对useBuiltIns设置描述,跟我上面3.1有点冲突,设置成"false"时:需要在webpack的entry设置@babel/polyfill独立打包;设置成"usage"时:则不需要在webpack中配置;
设置成"entry":则需要显示在入口文件顶部引入polyfill
4. 总结来说@babel/polyfill需要与@babel/preset-env的options搭配使用,常用的配置如下,具体可以看文档:
{
"preset":[
["@babel/preset-env",{
corejs:3 // 设置polyfill核心版本 或者"3.8.2"最好指定特定版本 不然设置3就默认为"3.0"
useBuiltIns:'usage' // 设置转换方式
target:{
browsers: ["last 10 versions", "ie >= 9"]
} // 设置目标浏览器以减少兼容代码处理避免过多引入hack代码数量
modules: false // 默认是auto,在设置false情况下,可能有利于进行tree shaking
shippedProposal:true // 当‘usage’时需要设置该项为true
}]
]
}
这里我们指定了corejs@3,那么必须安装依赖
npm install core-js -S
npm uninstall @babel/ployfill
安装完后,我们就可以不需要@babel/polyfill了,也不需要在任何位置引入了,若我们没有对@babel/preset-env设置options时,那么我们想ployfill就只有按上面的显式引入
实验2(加入@babel/polyfill)
npm install @babel/polyfill -S
// src/index.js
import 'core-js';
import 'regenerator-runtime/runtime';
import './b'
const fn = () => console.log(1);
fn();
const pro = new Promise();
const isIncludes = [1, 2, 3].includes(2);
class A {}
// src/b.js
export default class B {}
// bundle.js
引入core-js 导入了./node_modules/core-js/modules/全量包(没用到也导入);
引入regenerator-runtime/runtime 导入了很多没使用的runtime代码;
针对_classCallCheck这个hack函数分别在两个模块里面都声明了,造成浪费,并未提为公共。
// 结论
重复引入;全量引入,造成Bundle过大
实验3(卸载@babel/polyfill,加入core-js更改preset-env配置)
npm uninstall @babel/polyfill
npm install core-js -S
// babel.config.json
{
"plugins":[],
"presets":[
[
"@babel/preset-env",{
"useBuiltIns":"usage",
"corejs":"3.19.0",
"targets": {
"browsers": ["last 10 versions", "ie >= 9"]
}
}
]
]
}
// bundle.js
发现跟polyfill方式差不多,效果不理想
// 结论
但是省去了我们手动在入口文件引入
基于polyfill的弊端,社区又推出了@babel/runtime插件
@babel/runtime配合babel-plugin-transform-runtime
1. @babel/runtime:它是将es6编译成es5去执行。我们使用es6的语法来编写,最终会通过babel-runtime编译成es5.也就是说,不管浏览器是否支持ES6,只要是ES6的语法,它都会进行转码成ES5.所以就有很多冗余的代码。它不会污染全局对象和内置对象的原型,比如说我们需要Promise,我们只需要import Promise from 'babel-runtime/core-js/promise'即可,这样不仅避免污染全局对象,而且可以减少不必要的代码。
2. babel-plugin-transform-runtime:它就可以帮助我们去避免手动引入 import的痛苦,并且它还做了公用方法的抽离。比如说我们有100个模块都使用promise,但是promise的polyfill仅仅存在1份。
注意:@babel/runtime是为了替换@babel/polyfill 全量引入及全局变量覆盖引入,即需要安装-S
babel-plugin-transform-runtime插件是为了提取公共部分的runtime代码,编译阶段-D
npm install @babel/runtime -S
npm install babel-plugin-transform-runtime -D
实验4(安装@babel/runtime及babel-plugin-transform-runtime)
// webpack.config.js
rules:[
{
test:/\.js$/,
use:'babel-loader',
exclude:'/node_modules/' // 注意此处
}
]
// src/index.js
const fn = () => console.log(1);
fn();
Promise.resolve().then(() => {
console.log(123123)
})
const isIncludes = [1, 2, 3].includes(2);
console.log(isIncludes)
class A {}
// babel.config.json
{
"plugins": [
[
"@babel/plugin-transform-runtime"
]
],
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"shippedProposals": true,
"corejs": {
"version": "3.19.0",
"proposals": true
},
"targets": {
"browsers": ["ie >= 9", "android >= 4", "ios >= 9", "and_uc >= 10"]
},
"modules": false
}
]
]
}
// 打包后
在vendor.js中明显的看到引入了很多core-js里面的东西,在作polyfill
源文件中的let -》 var, 箭头函数 -》普通函数 ,class类 -》_classCallCheck(),Promise跟includes没变化
但是加载报错:ES Modules may not assign module.exports or exports.*, Use ESM export syntax
// 结论
看起来像是esmodule跟commonjs不兼容的问题,也是找了很久发现有人提出了在我们的babel-loader配置exclude时,大多数我们都是
exclude:'/node_modules/'这样配置,需要改成:
// webpack.config.js
rules:[
{
test:/\.js$/,
use:'babel-loader',
exclude:'/node_modules[\\\/]core-js/' // 注意此处
}
]
改成这样后,打包成功在google下面跑起来了,但是当我放到IE里面时,居然报错,跑不起来,明明我们设置了browsers ie9兼容,此时我还是以为是babel没处理好编译,结果一直搜索babel转译的问题,直到我想起来我是webpack5最新版本,然后就去了webpack官网配置项查看,终于还是发现了target这个选项,不设置的话,默认webpack5转换成'web'我认为它是现代浏览器适合的Bundle,所以更改了:
// webpack.config.js
module:{
rules:[
{
test:/\.js$/,
use:'babel-loader',
exclude:'/node_modules[\\\/]core-js/' // 注意此处
}
]
},
target:['web','es5']
意思就是让webpack根据我们的target最终打包的目标环境,这里多了个es5,意思打包成web和es5相互结合,具体看文档。最终打包后的包同时跑在google和Ie9上面了,嘻嘻嘻!
实验5(使用@babel/runtime-corejs3及babel-plugin-transform-runtime)
在查找上一个实验过程中,顺便也看到了另一种关于加载polyfill的方式,此时我们
npm uninstall @babel/runtime
npm install @babel/runtime-corejs3
// babel.config.json
{
"plugins": [
[
"@babel/plugin-transform-runtime",{
"corejs":{
"version":3,
"proposals": true
}
}
]
],
"presets": [
[
"@babel/preset-env",
{
// "useBuiltIns": "usage",
// "shippedProposals": true,
// "corejs": {
// "version": "3.19.0",
// "proposals": true
// },
"targets": {
"browsers": ["ie >= 9", "android >= 4", "ios >= 9", "and_uc >= 10"]
},
"modules": false
}
]
]
}
// webpack.config.js
module:{
rules:[
{
test:/\.js$/,
use:'babel-loader',
exclude:'/node_modules[\\\/]core-js/' // 注意此处
}
]
},
target:['web','es5']
// 结论
能同时跑起两个浏览器,但是我发现polyfill的包比实验4中的包大,因此更推荐上面的配置方式
最终完整配置(时刻关注babel和webpack官网最新细节,有时真很影响!!)
// package.json
npm i @babel-core @babel-cli @babel/plugin-transform-runtime @babel/preset-env babel-loader -D
npm i @babel/runtime core-js -S
// babel.config.json
{
"plugins": [
[
"@babel/plugin-transform-runtime"
]
],
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"shippedProposals": true,
"corejs": {
"version": "3.19.0",
"proposals": true
},
"targets": {
"browsers": ["ie >= 9", "android >= 4", "ios >= 9", "and_uc >= 10"]
},
"modules": false
}
]
]
}
其中的target可以分离出来到package.json或者.browserslistrc中或者babel-loader的options中
// webpack.config.js
module:{
rules:[
{
test:/\.js$/,
use:'babel-loader',
exclude:'/node_modules[\\\/]core-js/' // 注意此处
}
]
},
target:['web','es5']