babel

📝个人主页:爱吃炫迈
💌系列专栏:前端工程化
🧑‍💻座右铭:道阻且长,行则将至💗


Babel是什么

开发中,我们想使用ES6+语法,想要使用TypeScript,开发React项目,它们都是离不开Babel的,所以,学习babel对于我们理解代码从编写到线上的转变过程十分重要的。

官方定义:

babel是一个工具链,主要用于就旧浏览器或者环境中将ECMAScript2015+代码转换为向后兼容版本的JavaScript,包括语法转换、源代码转换、polyfill实现目标环境缺少的功能等,以便能够运行在当前和旧版本的浏览器或其他环境中。

插件

但是默认情况下babel并不会进行转换,只会从一个文件夹里输出到另外一个文件夹里,如果想进行代码转换,就需要使用相关的插件,插件是小型的 JavaScript 程序,用于指导 Babel 如何对代码进行转换。

举个栗子:

  • 比如转换箭头函数,就要使用箭头函数转换的相关插件:

npm install @babel/plugin-transform-arrow-functions -D

npx babelsrc --out-dir dist --plugins=@babel/plugin-transform-arrow-functions

const fu = () => 1;

// converted to

const fn = function fn() {
    return 1;
};
  • 查看转换后的结果:会发现const并没有转成var,这是因为plugin-transform-arrow-functions并没有提供这样的功能,还需要使用plugin-transform-block-scoping来完成这样的功能

npm insatll @babel/plugin-transform-block-scoping -D

npx babel src --out-dir dist --plugins=@babel/plugin-transform-block-scoping,@babel/plugin-transform-arrow-functions

const fu = () => 1;

// converted to

var fn = function fn() {
    return 1;
};

预设preset

如果需要转换的内容过多,一个个设置比较麻烦,这个时候可以使用预设(即一组预先设定的插件)。就像插件一样,你也可以根据自己所需要的插件组合创建一个自己的 preset 并将其分享出去。

常见预设

  1. babel-preset-env

@babel/preset-env是一个智能预设,它可以将我们的高版本JavaScript代码进行转译根据内置的规则转译成为低版本的JavaScript代码。

preset-env内部集成了绝大多数pluginState > 3)的转译插件,它会根据对应的参数进行代码转译。

具体的参数配置可以看官网:https://www.babeljs.cn/docs/babel-preset-env

注意:babel-preset-env仅仅针对语法阶段的转译,比如转译箭头函数,const/let语法。针对一些API或者ES6内置模块的polyfillpreset-env是无法进行转译的。

  1. babel-preset-react

通常我们在使用React中的jsx时,相信大家都明白实质上jsx最终会被编译称为React.createElement()方法。

babel-preset-react这个预设起到的就是将jsx进行转译的作用。

  1. babel-preset-typescript

    对于TypeScript代码,我们有两种方式去编译TypeScript代码成为JavaScript代码。

    1. 使用tsc命令,结合cli命令行参数方式或者tsconfig配置文件进行编译ts代码。
    2. 使用babel,通过babel-preset-typescript代码进行编译ts代码。

安装

对于上面的例子而言,我们可以使用一个名称为 env 的 preset:

  1. 安装@babel/preset-env预设

npm install @babel/preset-env -D

  1. 执行如下命令

npx babel src --out-dir dist --presets=@babel/preset-env

执行顺序

原则如下:

  • Plugin 会运行在 Preset 之前。

  • Plugin 会从前到后顺序执行。

  • Preset 的顺序则从后向前。

preset 的逆向顺序主要是为了保证向后兼容,因为大多数用户的编写顺序是 ['es2015', 'stage-0']。这样必须先执行 stage-0 才能确保 babel 不报错。因为低一级的 stage 会包含所有高级 stage 的内容

结合webpack中使用babel

实际项目中单独使用Babel的命令行工具来实现工作流的情况不是很多,更多的是结合Webpack使用。

在webpack.config.js中添加以下loader配置:

module.exports = {
  module: {
    rules: [
      {
        test: /.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: ["@babel/preset-env"]
            }
          }
        ]
      }
    ]
  }
}

然后安装相应的依赖:babel-loader、@babel/core、@babel/preset-env:

npm install --save-dev babel-loader @babel/core @babel/preset-env
  • 建立.babelrc文件或者babel.config.js文件,添加以下代码,babel会自动寻找这个文件
//.babelrc
{    
	"presets": ["@babel/preset-env"]
}

关于webpack中我们日常使用的babel相关配置主要涉及以下三个相关插件:

  • babel-loader
  • babel-core
  • babel-preset-env

首先我们需要清楚在webpack中loader的本质就是一个函数,接受我们的源代码作为入参同时返回新内容。

babel-loader

babel-loader的本质就是一个函数,我们匹配到对应的jsx?/tsx?的文件交给babel-loader:

/**
 * 
 * @param sourceCode 源代码内容
 * @param options babel-loader相关参数
 * @returns 处理后的代码
 */
function babelLoader (sourceCode,options) {
  // ..
  return targetCode
}

关于optionsbabel-loader支持直接通过loader的参数形式注入,同时也在loader函数内部通过读取.babelrc/babel.config.js/babel.config.json等文件注入配置。

babel-core

babel-loader仅仅是识别匹配文件和接受对应参数的函数,那么babel在编译代码过程中核心的库就是@babel/core这个库。

babel-corebabel最核心的一个编译库,他可以将我们的代码进行词法分析–语法分析–语义分析过程从而生成AST抽象语法树,从而对于“这棵树”的操作之后再通过编译称为新的代码。

babel-core通过transform方法将我们的代码进行编译。

关于babel-core中的编译方法其实有很多种,比如直接接受字符串形式的transform方法或者接受js文件路径的transformFile方法进行文件整体编译。

让我们来完善对应的babel-loader函数:

const core = require('@babel/core')

/**
 * 
 * @param sourceCode 源代码内容
 * @param options babel-loader相关参数
 * @returns 处理后的代码
 */
function babelLoader (sourceCode,options) {
  // 通过transform方法编译传入的源代码
  core.transform(sourceCode)
  return targetCode
}

这里我们在babel-loader中调用了babel-core这个库进行了代码的编译作用。

babel-preset-env

上边我们说到babel-loader本质是一个函数,它在内部通过babel/core这个核心包进行JavaScript代码的转译。

但是针对代码的转译我们需要告诉babel以什么样的规则进行转化,比如我需要告诉babel:“嘿,babel。将我的这段代码转化称为ECMAScript 5版本的内容!”。

此时babel-preset-env在这里充当的就是这个作用:告诉babel我需要以为什么样的规则进行代码转译

const core = require('@babel/core');

/**
 *
 * @param sourceCode 源代码内容
 * @param options babel-loader相关参数
 * @returns 处理后的代码
 */
function babelLoader(sourceCode, options) {
  // 通过transform方法编译传入的源代码
  core.transform(sourceCode, {
    presets: ['babel-preset-env'],
    plugins: [...]
  });
  return targetCode;
}

在命令行中使用babel

babel本身可以作为一个独立的工具,不和webpack等构建工具配置来单独使用。

  1. 需要安装如下库:

npm install @babel/cli @babel/core

  • @babel/core:babel核心代码,必须安装
  • @babel/cli:可以让我们在命令行中使用babel
  1. 使用babel来处理我们的源代码

npx babel src --out-dir dist

  • src:是源文件的目录
  • –out-dir:指定要输出的文件夹dist

Babel配置文件

  1. babel.config.json(或者.js,.cjs,.mjs)文件(相当于将options单独拿出来了)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hsDuwTIQ-1690128623705)(…/AppData/Roaming/Typora/typora-user-images/image-20230723183636666.png)]

  1. .babelrc.json(或者.babelrc,.js,.cjs,mjs)文件

Babel的底层原理

babel是如何将我们的一段代码(ES6、TypeScript、React)转换成另一端代码(ES5)的呢?从一种源代码(原生语言)转换成另一种源代码(目标语言)是怎么工作的呢?这其实是编译器的工作。

事实上我们可以将babel看成是一个编译器,Babel编译器的作用就是将我们的源代码,转换成浏览器可以直接识别的另一段源代码

Babel也拥有编译器的工作流程:

  • 解析阶段(Parsing)

这个阶段将我们的js代码(字符串)进行词法分析生成一系列tokens,之后再进行语法分析将tokens组合称为一颗AST抽象语法树。(比如babel-parser它的作用就是这一步)

  • 转换阶段(Transformation)

这个阶段babel通过对于这棵树的遍历,从而对于旧的AST进行增删改查,将新的js语法节点转化称为浏览器兼容的语法节点。(babel/traverse就是在这一步进行遍历这棵树)

  • 生成阶段(Code Generation)

这个阶段babel会将新的AST转化同样进行深度遍历从而生成新的代码。(@babel/generator)

image-20230722010727620

image-20230722011343855

浏览器兼容性

现在市面上有大量的浏览器:比如chrome、edge、Safari等等,它们的市场占率是多少?我们要不要兼容它们?在开发中,浏览器的兼容性问题,我们应该如何去解决和处理?

注意:这里的兼容性是针对不同的浏览器支持的特性:比如css特性、js语法之间的兼容性。

image-20230723085923532

查询浏览器市场占有率

查询市场占有率,有一个好用的网站:caniuse

browserslist工具

Browerslist是一个在**不同的前端工具之间 **,共享目标浏览器和Node.js版本的配置:

  • Autoprefixer
  • Babel
  • postcss-preset-env
  • eslint-plugin-compat
  • stylelint-no-unsupported-browser-features
  • postcss-normalize
  • obsolete-webpack-plugin

使用browserslist,可以在css兼容性和js兼容性下共享我们配置的兼容性条件。

  • 就是当我们设置了一个条件:> 1%
  • 我们想要的是css要兼容市场占有率大于1%的浏览器,js也要兼容市场占有率大于1%的浏览器
  • 如果我们是通过工具来达到这种兼容性的,比如postcss-preset-env、babel、autoprefixer等
  • 我们可以使用Browserslist共享我们的配置

浏览器查询过程

  • 我们可以编写类似于这样的配置:

image-20230723093314845

  • 那么之后,这些工具会根据我们的配置来获取相关的浏览器信息,以 方便决定是否需要进行兼容性的支持,条件查询使用的是caniuse-lite的工具,这个工具的数据来自于caniuse的网站上。

配置broeserslist

  • 方案一:在package.json中配置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FTlEOCB8-1690128623708)(…/AppData/Roaming/Typora/typora-user-images/image-20230723182035355.png)]

  • 方案二:单独的一个配置文件.browserslistrc文件
// .browserslistrc

> 1%
last 2 versions
not dead

polyfill

Babel默认只转换新的JavaScript语法,而不转换新的API,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。

举例来说,ES6在Array对象上新增了Array.from方法。Babel就不会转码这个方法。如果想让这个方法运行,必须使用babel-polyfill,在Array上添加实现这个方法,为当前环境提供一个垫片。

其实可以简单总结一下,语法层面的转化preset-env完全可以胜任。但是一些内置方法模块,仅仅通过preset-env的语法转化是无法进行识别转化的,所以就需要一系列类似”垫片“的工具进行补充实现这部分内容的低版本代码实现。这就是所谓的polyfill的作用

实现

通过babelPolyfill通过往全局对象上添加属性以及直接修改内置对象的Prototype上添加方法实现polyfill

比如说我们需要支持String.prototype.include,在引入babelPolyfill这个包之后,它会在全局String的原型对象上添加include方法从而支持我们的Js Api

我们说到这种方式本质上是往全局对象/内置对象上挂载属性,所以这种方式难免会造成全局污染

可以通过单独引入core-js和regenerator-runtime来完成polyfilll的使用:

npm install core-js regenerator-runtime --save
// babel.config.js 
module.exports = {
    presets: [
        ["@babel/preset-env",{
            corejs:3,
            useBuiltIns: false
        }]
    ]
    
}

babel-preset-env中存在两个参数:

一个是corejs参数,所谓的core-js就是我们上文讲到的“垫片”的实现。它会实现一系列内置方法或者PromiseApi

一个useBuiltIns参数,这个参数决定了如何在preset-env中使用@babel/polyfilluseBuiltIns属性有三个常见的值:

  1. false
  • 打包之后的文件不使用polyfill来进行适配
  • 并且这个时候是不需要设置corejs属性的
  1. usage
  • 会根据源代码中出现的语言特性,配置的浏览器兼容,以及代码中使用的API,对polyfill按需添加
  • 这样可以确保最终包里的polyfill数量最小化,打包的包相对会小一些
  • 可以设置corejs属性来确定使用的corejs的版本
  1. entry
  • 如果我们依赖的某一库本身使用了某些polyfill的特性,但是因为我们使用的是useage,所以用户浏览器可能会报错,所以,这种情况可以使用entry
  • 并且需要在入口文件中添加import 'core-js/stable';import 'regenerator-runtime/runtime'
  • 这样会根据browserslist目标导入所有的不兼容的polyfill,但是对应的包也会变大

上边我们说到配置为entry时,perset-env会基于我们的浏览器兼容列表进行全量引入polyfill。所谓的全量引入比如说我们代码中仅仅使用了Array.from这个方法。但是polyfill并不仅仅会引入Array.from,同时也会引入PromiseArray.prototype.include等其他并未使用到的方法。这就会造成包中引入的体积太大了。

usage和entry本质上的区别

举个栗子:我们以项目中引入Promise为例

当我们配置useBuintInts:entry时,仅仅会在入口文件全量引入一次polyfill,你可以这样理解:

// 当使用entry配置时
...
// 一系列实现polyfill的方法
global.Promise = promise

// 其他文件使用时
const a = new Promise()

当我们配置useBuintInts:usage时,preset-env只能基于各个模块去分析它们 所使用到的polyfill进而引入。

preset-env会帮助我们智能化的在需要的地方引入,比如:

// a. js 中
import "core-js/modules/es.promise";

...
// b. js 中
import "core-js/modules/es.promise";

...

babel-runtime

上边我们讲到@babel/polyfill是存在污染全局变量的副作用,在实现polyfillBabel还提供了另外一种方式去让我们实现这功能,那就是@babel/runtime

简单来讲,@babel/runtime更像是一种按需加载的解决方案,比如哪里需要使用到Promise@babel/runtime就会在他的文件顶部添加import promise from 'babel-runtime/core-js/promise'

同时上边我们讲到对于preset-envuseBuintIns配置项,我们的polyfillpreset-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你可以理解称为就是一个运行时“哪里需要引哪里”的工具库。

babel-runtime存在的问题

babel-runtime在我们手动引入一些polyfill的时候,它会给我们的代码中注入一些类似_extend(), classCallCheck()之类的工具函数,这些工具函数的代码会包含在编译后的每个文件中,比如:

class Circle {}
// babel-runtime 编译Class需要借助_classCallCheck这个工具函数
function _classCallCheck(instance, Constructor) { //... } 
var Circle = function Circle() { _classCallCheck(this, Circle); };

如果有多个文件都用到了 es6 的 class,则需要在每个文件中都要定义一遍 ,会造成一种浪费

所以针对上述提到的两个问题:

  1. babel-runtime无法做到智能化分析,需要我们手动引入
  2. babel-runtime编译过程中会重复生成冗余代码

此时需要引入我们的主角:@babel/plugin-transform-runtime

@babel/plugin-transform-runtime

作用:为了解决babel-runtime出现的问题

  1. @babel/plugin-transform-runtime插件会智能化的分析我们的项目中所使用到需要转译的js代码,从而实现模块化从babel-runtime中引入所需的polyfill实现。
  2. @babel/plugin-transform-runtime插件提供了一个helpers参数。这个参数开启后就可以将上述提到编译阶段重复的工具函数,比如classCallCheckextends等代码转化为require语句,这些工具函数就不会重复的出现在使用中的模块中了。

比如这样:

// @babel/plugin-transform-runtime会将工具函数转化为require语句进行引入
// 而非runtime那样直接将工具模块代码注入到模块中
var _classCallCheck = require("@babel/runtime/helpers/classCallCheck"); 
var Circle = function Circle() { _classCallCheck(this, Circle); };
n-transform-runtime`插件会智能化的分析我们的项目中所使用到需要转译的`js`代码,从而实现模块化从`babel-runtime`中引入所需的`polyfill`实现。
2. `@babel/plugin-transform-runtime`插件提供了一个`helpers`参数。这个参数开启后就可以将上述提到编译阶段重复的工具函数,比如`classCallCheck``extends`等代码转化为require语句,这些工具函数就不会重复的出现在使用中的模块中了。

比如这样:

```js
// @babel/plugin-transform-runtime会将工具函数转化为require语句进行引入
// 而非runtime那样直接将工具模块代码注入到模块中
var _classCallCheck = require("@babel/runtime/helpers/classCallCheck"); 
var Circle = function Circle() { _classCallCheck(this, Circle); };
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱吃炫迈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值