【JS】简简单单的一次babel大扫盲!

848 阅读8分钟

祖传开头

早睡早起保平安

这篇文章算是一次关于babel的一次扫盲。整理出一份babel的基础概念扫盲,写给自己,也写需要这种基础梳理的小伙伴吧。希望大家都不要因为非常基础的问题,比如babel版本之类的小问题绊住了手脚哈哈哈!另外babel也是一个非常好的学习工具,可以把es6的代码转化成es5,抛开各种语法糖,探究js的本质。总而言之,希望这篇文章能对各位有所助益吧~

一、Babel基本概念

Babel 是一个通用的多功能的 JavaScript 编译器。将ECMAScript 2015+ 的代码转化成能适配各种低端游览器或者其他环境(Node)的代码。

特点:

  • 静态分析,在不需要执行代码的情况下,对代码分析处理。
  • 通过组合各种模块,生一个能实现特定功能的集合。
  • @babel/core 包含了所有babel的 核心功能,使用babel前必须先引入。

🌰一个简单的例子


第一步:在 .babelrc 文件中进行babel的配置。

当然还有其他的配置方式,比如创建一个babel.config.js文件或者直接在package.json中的babel字段下进行配置。

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

**第二步:安装 @babel/core 、 @babel/cli 和 ****@babel/preset-env** 
@babel/core是babel的和核心库。@babel/cli是babel能在终端环境中可以运行的工具库。

第三步:创建源代码文件和babel运行命令。

src/index.js

const newArys = [1, 2, 3, 4, 5].map((n) => n * n);


package.json

"scripts": {
    "compiler": "babel src --out-dir lib",
    "watch": "npm run compiler -- --watch"
 }

命令 compiler 的作用就是将 src 目录下所有的JavaScript文件都进行解析,最后输出到 lib 目录下。
命令 watch 的作用就是监听 src 目录下是否有进行改动,若有就重新进行编译。

tips:向 npm 脚本传入参数,要使用--标明。

**
更多babel-cli命令
以下列举了几个比较常见的配置命令。

说明使用
--out-file/-o指定输出文件babel src --out-file index.js
--out-dir/-o指定输出目录babel src --out-dir lib
--watch/-w监听文件改动并且进行重新编译babel src --out-dir lib --watch
--source-maps/-s生成映射代码babel src --out-file script-compiled.js --source-maps
--ignore指定不需要转换的文件babel src --out-dir lib ---ignore "src/**/*.spec.js",

www.babeljs.cn/docs/babel-…

二、Babel原理

三个步骤

babel的处理步骤主要分为以下步骤:

第一步:解析(parse)

将原代码转换成AST树,也就是抽象语法树。这个步骤分为两个阶段:

  • 词法分析
  • 语法分析


词法分析 简单的来理解,就是把原代码(字符串形式)进行分割,生成一个个最小单位——令牌(token),方便后续的组装。
一个简单的表达式:

a * b

经过词法分析后,以上表达式分割成3个部分。变成一组 令牌流(tokens) 。形式如下:

[
  { type: { ... }, value: "a", start: 0, end: 1, loc: { ... } },
  { type: { ... }, value: "*", start: 2, end: 3, loc: { ... } },
  { type: { ... }, value: "b", start: 4, end: 5, loc: { ... } },
  ...
]

词法分析 ,就是把每个token的关系描述清楚,把有关系的token组合在一起。比如上述三个token组合起来就是个获取乘积的表达式。经过词法分析后,就会生成AST树。

🎄先来看一个具体的AST树:(通过AST Explorer模拟AST树的生成)
源码:

function add(a, b) {
  return a + b
}
{
  "type": "Program",
  "start": 0,
  "end": 37,
  "body": [
    {
      "type": "FunctionDeclaration",
      "start": 0,
      "end": 37,
      "id": {
        "type": "Identifier",
        "start": 9,
        "end": 12,
        "name": "add"
      },
      "expression": false,
      "generator": false,
      "async": false,
      "params": [
        {
          "type": "Identifier",
          "start": 13,
          "end": 14,
          "name": "a"
        },
        {
          "type": "Identifier",
          "start": 16,
          "end": 17,
          "name": "b"
        }
      ],
      "body": {
        "type": "BlockStatement",
        "start": 19,
        "end": 37,
        "body": [
          {
            "type": "ReturnStatement",
            "start": 23,
            "end": 35,
            "argument": {
              "type": "BinaryExpression",
              "start": 30,
              "end": 35,
              "left": {
                "type": "Identifier",
                "start": 30,
                "end": 31,
                "name": "a"
              },
              "operator": "+",
              "right": {
                "type": "Identifier",
                "start": 34,
                "end": 35,
                "name": "b"
              }
            }
          }
        ]
      }
    }
  ],
  "sourceType": "module"
}

AST树的每一层结构都比较相似,每一层树都会由一个个节点(Node)构成。每个节点的type都会对这个节点的类型进行说明:

  • Program :一个程序的根节点类型
  • FunctionDeclaration :函数声明
  • Identifier :变量声明
  • BinaryExpression :表达式
  • ...
{
   type:"",
   id:"",
   params:{},
   body:{}
}

每个节点都可以是一个树形结构,成千上万的节点组成的一个AST树。这样就完成了一次对代码的静态分析。

第二步:转换(transform)

这个步骤就是接收到第一步生成的AST树后进行遍历,对AST的节点进行更新、添加、删除等操作。也是整个编译过程中最复杂的部分,也是babel插件介入工作并完成一些特定功能的过程。

babel插件原理:

  • 创建一个访问者,这个访问者对象会提供具体的获取树节点信息的方法
  • 实际访问的是一个路径

**
🌰看一个简单的babel插件例子

// babel-plugins-mm
module.exports = function () {
    return {
        visitor:{
            Identifier: {
                enter(path) {
                    console.log("Entered!"+ path.node.name);
                },
                exit(path) {
                    console.log("Exited!"+ path.node.name);
                }
            }
        }
    };
}

使用插件:(只写插件名称,默认会从node-modules里查找)
mm 是 babel-plugins-mm 的简写。

{
    "presets": [
        ["@babel/preset-env"]
    ],
    "plugins": [
        ["mm"]
    ]
}

第三步:生成(generate)

最后一步就是就是将处理过后的AST再次转化后字符串形式的代码。(深度优先的遍历AST树,并生成字符串形式的代码。)

三、Babel配置

{
  // 预设
  "presets":[...],
  // 插件
  "plugins":[...],
  // 环境配置
  "env": {
     "development": {
       "plugins": [...]
     },
     "production": {
       "plugins": [...]
     }
    }
}

4、Babel预设

就是把零零散散的babel插件组合起来,只要简单的引入一个babel预设。 每个预设都会有自己对应的配置项,具体可以查阅babel官网。


目前官方的推荐的一些预设:

@babel/preset-env
@babel/preset-flow
@babel/preset-react
@babel/preset-typescript
...


注意:babel @7.0.0之后对标准阶段提案的一些预设都会废弃,比如以下预设不推荐使用了。

@babel/preset-stage-0
@babel/preset-stage-1
@babel/preset-stage-2
@babel/preset-stage-3

@babel/preset-env

对ES2015和ES2016进行转换,并且处理polyfill。

preset-env有以下配置,具体可以访问官网查看。

targets
spec
loose
modules
debug
include
exclude
useBuiltIns
corejs
forceAllTransforms
configPath
ignoreBrowserslistConfig
shippedProposals

tagets

指定适配游览器范围,如果设置了该参数,优先级会比.browserslist 或者 package.json/browserslist设置的高。如果不设置,则默认会获取 .browserslist 或者 package.json/browserslist指定的游览器适配范围。
但是在项目中,还是建议使用 .browserslistrc  或者 package.json/browserslist指定目标。

useBuiltIns

"usage" | "entry" | false, defaults to false.
这个配置参数主要的作用是明确如何处理polyfill。

**polyfill
先简单的了解一下 polyfill ,中文成为垫片,形象一点描述用一个个垫片铺平低端游览器的坑,使代码运行的和在高端游览器中一样。对ES2015+的一些语法进行转化适配(除了一些实验性的Js特性),比如一些新的内置对象和实例方法等(Promise、Array.from、Object.assign...)

需要注意的是,@babel/polyfill在babel v7.4.0之后版本会被废弃,官方建议直接引入corejs,并且在options中指定corejs版本。

@babel/polyfill 其实就是由 core-js/stable  和 regenerator-runtime/runtime  组合而成。

  • core-js:@babel/polyfill的底层依赖库,为ES2015+的JS特性提供编译转化功能。
  • **regenerator-runtime : **专门为 Generator 函数提供编译转化功能,比如在项目中使用了async/await就需要引入这个插件。


以下两种写法其实是一样,而且只能两种方式取一种,否则会报重复引入的报错。

import "@babel/polyfill";
// ====等同于====
import "core-js/stable";
import "regenerator-runtime/runtime";

但是由于polyfill的体积比较大,通常情况下我们不需要引入完成的polyfill,而且全部引入也会导致打包后体积变大。所以此处useBuiltIns就开始发挥作用。

useBuiltIns:usage
不需要在文件入口处引入polyfill,env会提供一个插件,根据每个文件的需求进行特殊引入垫片。

此时需要你在babel配置中指定corejs的版本的版本。

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

比如promise-a.js文件只使用到promise,就只会在该文件入口引入promise相关的垫片。
src/promise-a.js

// src/promise-a.js
new Promise((resolve)=>{
    console.log("promise A");
    resolve()
})

lib/promise-a.js

"use strict";

require("core-js/modules/es.object.to-string");

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

new Promise(function (resolve) {
  console.log("promise A");
  resolve();
});

useBuiltIns: 'entry'
需要在文件入口处手动引入polyfill,然后根据当前的环境尽可能的引入需要的功能。
比如引入一个array相关的垫片。
src/entry.js

import "core-js/es/array";

lib/entry.js

"use strict";
require("core-js/modules/es.array.concat");
require("core-js/modules/es.array.copy-within");
require("core-js/modules/es.array.every");
require("core-js/modules/es.array.fill");
require("core-js/modules/es.array.filter");
require("core-js/modules/es.array.find");
require("core-js/modules/es.array.find-index");
require("core-js/modules/es.array.flat");
require("core-js/modules/es.array.flat-map");
...


useBuiltIns: false
不为文件自动添加垫片,也不会对于自行引入的core-js或者@babel/polyfill有任何转化。

corejs

23 or { version: 2 | 3, proposals: boolean }, defaults to 2.

这个选项需要和useBuiltIns结合起来使用。当useBuiltIns: usage或者useBuiltIns: entry时才会起效。
core-js目前存在两个版本,分别是coreJs2和coreJs3。coreJs2目前已经不再更新,所以对于一些特别新的特性只会在coreJs3去更新。

如果使用coreJs3时只想注入稳定的ECMAScript功能,有以下两种方式:
1.对于useBuiltIns: usage 
corejs: { version: 3, proposals: true }<br />
2.对于useBuiltIns: entry
corejs配置为 3 ,另外在项目引入import "core-js/proposals/string-replace-all"

✍其他小知识点:


1.插件名称简写

@babel/plugin-XXX 等同于 @babel/XXX

2.babel命名
@7.0.0以上的包都是以@开头命名,用于区分6.x版本

3.执行顺序

  • 插件在 Presets 前运行。
  • 插件顺序从前往后排列。
  • Preset 顺序是颠倒的(从后往前)。


参考文档
Babel用户手册