写点什么

前端工程师需要了解的 Babel 知识

  • 2021-01-26
  • 本文字数:5884 字

    阅读完需:约 19 分钟

前端工程师需要了解的 Babel 知识

Babel 是怎么工作的


Babel 是一个 JavaScript 编译器。

做与不做


注意很重要的一点就是,Babel 只是转译新标准引入的语法,比如:


  • 箭头函数

  • let / const

  • 解构


哪些在 Babel 范围外?对于新标准引入的全局变量、部分原生对象新增的原型链上的方法,Babel 表示超纲了。


  • 全局变量

    Promise

    Symbol

    WeakMap

    Set

  • includes

  • generator 函数


对于上面的这些 API,Babel 是不会转译的,需要引入 polyfill 来解决。


Babel 编译的三个阶段


Babel 的编译过程和大多数其他语言的编译器相似,可以分为三个阶段:


  • 解析(Parsing):将代码字符串解析成抽象语法树。

  • 转换(Transformation):对抽象语法树进行转换操作。

  • 生成(Code Generation): 根据变换后的抽象语法树再生成代码字符串。



为了理解 Babel,我们从最简单一句 console 命令下手


解析(Parsing)


Babel 拿到源代码会把代码抽象出来,变成 AST (抽象语法树),学过编译原理的同学应该都听过这个词,全称是 Abstract Syntax Tree


抽象语法树是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,只所以说是抽象的,是因为抽象语法树并不会表示出真实语法出现的每一个细节,比如说,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现,它们主要用于源代码的简单转换。


console.log('zcy'); 的 AST 长这样:


{    "type": "Program",    "body": [        {            "type": "ExpressionStatement",            "expression": {                "type": "CallExpression",                "callee": {                    "type": "MemberExpression",                    "computed": false,                    "object": {                        "type": "Identifier",                        "name": "console"                    },                    "property": {                        "type": "Identifier",                        "name": "log"                    }                },                "arguments": [                    {                        "type": "Literal",                        "value": "zcy",                        "raw": "'zcy'"                    }                ]            }        }    ],    "sourceType": "script"}
复制代码


上面的 AST 描述了源代码的每个部分以及它们之间的关系,可以自己在这里试一下 astexplorer


AST 是怎么来的?整个解析过程分为两个步骤:


分词:将整个代码字符串分割成语法单元数组 在线分词工具语法单元通俗点说就是代码中的最小单元,不能再被分割,就像原子是化学变化中的最小粒子一样。


Javascript 代码中的语法单元主要包括以下这么几种:


关键字:const、 let、 var 等标识符:可能是一个变量,也可能是 if、else 这些关键字,又或者是 true、false 这些常量运算符数字空格注释:对于计算机来说,知道是这段代码是注释就行了,不关心其具体内容其实分词说白了就是简单粗暴地对字符串一个个遍历。为了模拟分词的过程,写了一个简单的 Demo,仅仅适用于和上面一样的简单代码。Babel 的实现比这要复杂得多,但是思路大体上是相同的。对于一些好奇心比较强的同学,可以看下具体是怎么实现的,链接在文章底部。


function tokenizer(input) {  const tokens = [];  const punctuators = [',', '.', '(', ')', '=', ';'];
let current = 0; while (current < input.length) {
let char = input[current];
if (punctuators.indexOf(char) !== -1) {
tokens.push({ type: 'Punctuator', value: char, }); current++; continue; } // 检查空格,连续的空格放到一起 let WHITESPACE = /\s/; if (WHITESPACE.test(char)) { current++; continue; }
// 标识符是字母、$、_开始的 if (/[a-zA-Z\$\_]/.test(char)) { let value = '';
while(/[a-zA-Z0-9\$\_]/.test(char)) { value += char; char = input[++current]; } tokens.push({ type: 'Identifier', value }); continue; }
// 数字从0-9开始,不止一位 const NUMBERS = /[0-9]/; if (NUMBERS.test(char)) { let value = ''; while (NUMBERS.test(char)) { value += char; char = input[++current]; } tokens.push({ type: 'Numeric', value }); continue; }
// 处理字符串 if (char === '"') { let value = ''; char = input[++current];
while (char !== '"') { value += char; char = input[++current]; }
char = input[++current];
tokens.push({ type: 'String', value });
continue; } // 最后遇到不认识到字符就抛个异常出来 throw new TypeError('Unexpected charactor: ' + char); }
return tokens;}
const input = `console.log("zcy");`
console.log(tokenizer(input));
复制代码


结果如下:


[    {        "type": "Identifier",        "value": "console"    },    {        "type": "Punctuator",        "value": "."    },    {        "type": "Identifier",        "value": "log"    },    {        "type": "Punctuator",        "value": "("    },    {        "type": "String",        "value": "'zcy'"    },    {        "type": "Punctuator",        "value": ")"    },    {        "type": "Punctuator",        "value": ";"    }]
复制代码


  • 语法分析:建立分析语法单元之间的关系


语义分析则是将得到的词汇进行一个立体的组合,确定词语之间的关系。考虑到编程语言的各种从属关系的复杂性,语义分析的过程又是在遍历得到的语法单元组,相对而言就会变得更复杂。


简单来说语法分析是对语句和表达式识别,这是个递归过程,在解析中,Babel 会在解析每个语句和表达式的过程中设置一个暂存器,用来暂存当前读取到的语法单元,如果解析失败,就会返回之前的暂存点,再按照另一种方式进行解析,如果解析成功,则将暂存点销毁,不断重复以上操作,直到最后生成对应的语法树。


转换(Transformation)


Plugins

插件应用于 babel 的转译过程,尤其是第二个阶段 Transformation,如果这个阶段不使用任何插件,那么 babel 会原样输出代码。


Presets

Babel 官方帮我们做了一些预设的插件集,称之为 Preset,这样我们只需要使用对应的 Preset 就可以了。每年每个 Preset 只编译当年批准的内容。 而 babel-preset-env 相当于 ES2015 ,ES2016 ,ES2017 及最新版本。


Plugin/Preset 路径

如果 Plugin 是通过 npm 安装,可以传入 Plugin 名字给 Babel,Babel 将检查它是否安装在 node_modules 中


"plugins": ["babel-plugin-myPlugin"]
复制代码

也可以指定你的 Plugin/Preset 的相对或绝对路径。

"plugins": ["./node_modules/asdf/plugin"]
复制代码


Plugin/Preset 排序

如果两次转译都访问相同的节点,则转译将按照 Plugin 或 Preset 的规则进行排序然后执行。


  • Plugin 会运行在 Preset 之前。

  • Plugin 会从第一个开始顺序执行。

  • Preset 的顺序则刚好相反(从最后一个逆序执行)。


例如:

{  "plugins": [    "transform-decorators-legacy",    "transform-class-properties"  ]}
复制代码


将先执行 transform-decorators-legacy 再执行 transform-class-properties

但 preset 是反向的


{  "presets": [    "es2015",    "react",    "stage-2"  ]}
复制代码


会按以下顺序运行: stage-2, react, 最后 es2015


那么问题来了,如果 presets 和 plugins 同时存在,那执行顺序又是怎样的呢?答案是先执行 plugins 的配置,再执行 presets 的配置。


所以以下代码的执行顺序为


  1. @babel/plugin-proposal-decorators

  2. @babel/plugin-proposal-class-properties

  3. @babel/plugin-transform-runtime

  4. @babel/preset-env

// .babelrc 文件{  "presets": [    [      "@babel/preset-env"    ]  ],  "plugins": [    ["@babel/plugin-proposal-decorators", { "legacy": true }],    ["@babel/plugin-proposal-class-properties", { "loose": true }],    "@babel/plugin-transform-runtime",  ]}
复制代码
生成(Code Generation)


用 babel-generator 通过 AST 树生成 ES5 代码


如何编写一个 Babel 插件


基础的东西讲了些,下面说下具体如何写插件,只做简单的介绍,感兴趣的同学可以看 Babel 官方的介绍。Plugin Development


插件格式


先从一个接收了当前 Babel 对象作为参数的 Function 开始。


export default function(babel) {  // plugin contents}
复制代码


我们经常会这样写


export default function({ types: t }) {    //}
复制代码


接着返回一个对象,其 visitor 属性是这个插件的主要访问者。


export default function({ types: t }) {  return {    visitor: {      // visitor contents    }  };};
复制代码


visitor 中的每个函数接收 2 个参数:path 和 state



export default function({ types: t }) { return { visitor: { CallExpression(path, state) {} } };};
复制代码


写一个简单的插件


我们先写一个简单的插件,把所有定义变量名为 a 的换成 b ,先从 astexplorer 看下 var a = 1 的 AST


{  "type": "Program",  "start": 0,  "end": 10,  "body": [    {      "type": "VariableDeclaration",      "start": 0,      "end": 9,      "declarations": [        {          "type": "VariableDeclarator",          "start": 4,          "end": 9,          "id": {            "type": "Identifier",            "start": 4,            "end": 5,            "name": "a"          },          "init": {            "type": "Literal",            "start": 8,            "end": 9,            "value": 1,            "raw": "1"          }        }      ],      "kind": "var"    }  ],  "sourceType": "module"}
复制代码


从这里看,要找的节点类型就是 VariableDeclarator ,下面开始撸代码


export default function({ types: t }) {  return {    visitor: {      VariableDeclarator(path, state) {        if (path.node.id.name == 'a') {          path.node.id = t.identifier('b')        }      }    }  }}
复制代码


我们要把 id 属性是 a 的替换成 b 就好了。但是这里不能直接 path.node.id.name = 'b' 。如果操作的是 Object,就没问题,但是这里是 AST 语法树,所以想改变某个值,就是用对应的 AST 来替换,现在我们用新的标识符来替换这个属性。


最后测试一下


import * as babel from '@babel/core';const c = `var a = 1`;
const { code } = babel.transform(c, { plugins: [ function({ types: t }) { return { visitor: { VariableDeclarator(path, state) { if (path.node.id.name == 'a') { path.node.id = t.identifier('b') } } } } } ]})
console.log(code); // var b = 1
复制代码


实现一个简单的按需打包功能


例如我们要实现把 import { Button } from 'antd' 转成 import Button from 'antd/lib/button'


通过对比 AST 发现,specifiers 里的 type 和 source 不同。


// import { Button } from 'antd'"specifiers": [    {        "type": "ImportSpecifier",        ...    }]
复制代码


// import Button from 'antd/lib/button'"specifiers": [    {        "type": "ImportDefaultSpecifier",        ...    }]
复制代码


import * as babel from '@babel/core';const c = `import { Button } from 'antd'`;
const { code } = babel.transform(c, { plugins: [ function({ types: t }) { return { visitor: { ImportDeclaration(path) { const { node: { specifiers, source } } = path; if (!t.isImportDefaultSpecifier(specifiers[0])) { // 对 specifiers 进行判断,是否默认倒入 const newImport = specifiers.map(specifier => ( t.importDeclaration( [t.ImportDefaultSpecifier(specifier.local)], t.stringLiteral(`${source.value}/lib/${specifier.local.name}`) ) )) path.replaceWithMultiple(newImport) } } } } } ]})
console.log(code); // import Button from "antd/lib/Button";
复制代码


当然 babel-plugin-import 这个插件是有配置项的,我们可以对代码做以下更改

export default function({ types: t }) {  return {    visitor: {      ImportDeclaration(path, { opts }) {        const { node: { specifiers, source } } = path;        if (source.value === opts.libraryName) {          // ...        }      }    }  }}
复制代码


Babel 常用 API

@babel/core

Babel 的编译器,核心 API 都在这里面,比如常见的 transformparse

@babel/cli

cli 是命令行工具, 安装了 @babel/cli 就能够在命令行中使用 babel 命令来编译文件。当然我们一般不会用到,打包工具已经帮我们做好了。

@babel/node

直接在 node 环境中,运行 ES6 的代码

babylon

Babel 的解析器

babel-traverse

用于对 AST 的遍历,维护了整棵树的状态,并且负责替换、移除和添加节点。

babel-types

用于 AST 节点的 Lodash 式工具库, 它包含了构造、验证以及变换 AST 节点的方法,对编写处理 AST 逻辑非常有用。

babel-generator

Babel 的代码生成器,它读取 AST 并将其转换为代码和源码映射(sourcemaps)

总结

文章主要介绍了一下几个 Babel 的 API,和 Babel 编译代码的过程以及简单编写了一个 babel 插件

参考文档



头图:Unsplash

作者:天泽

原文:https://mp.weixin.qq.com/s/zbWfxfnGWF5MY2qBDgtJbw

原文:前端工程师需要了解的 Babel 知识

来源:政采云前端团队 - 微信公众号 [ID:Zoo-Team]

转载:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

2021-01-26 23:381880

评论 1 条评论

发布
用户头像
可以简单了解AST、babel插件的运行机制,对新手入门很友好,赞!
2021-02-07 10:10
回复
没有更多了
发现更多内容

Java并发编程必备:分布式锁的选型和性能对比

做梦都在改BUG

Java 数据库 分布式锁

责任链模式在复杂数据处理场景中的实战

阿里技术

设计模式 技术实践

用户分享 | Dockquery,一个国产数据库客户端的初体验

BinTools图尔兹

用户体验 国产数据库工具

一路同行,端点科技与海尔集团相伴十年的数字之旅

科技热闻

牛掰!阿里架构师熬夜肝了一份JVM必知必会,哪里不会查哪里

做梦都在改BUG

Java 性能优化 JVM

并发编程-ReentrantLook底层设计

Java你猿哥

Java ssm 重入锁 lock锁 底层实现原理

硬核!阿里最新出品架构核心场景实战手册,解决99%的架构问题

Java你猿哥

微服务 架构设计 架构师 架构场景实战 微服务实战

震撼来袭!最具中国特色的微服务组件:新一代SpringCloud Alibaba

做梦都在改BUG

Java 架构 微服务 Spring Cloud spring cloud alibaba

基于图神经网络的推荐算法

TiAmo

神经网络 算法 推荐算法

新技术越来越多,作为程序员,我们应该怎么规划职业生涯? | 社区征文

wljslmz

三周年征文

sysMaster: 全新1号进程实现方案,秒级自愈,保障系统全天在线

openEuler

Linux rust 操作系统 openEuler init

云数据库技术沙龙|多云多源下的数据复制技术解读-NineData

NineData

MySQL Clickhouse 数据管理 多云多源 数据存取

Java 面试八股文合集:涵盖大厂必考的核心知识点

采菊东篱下

Java 程序员 面试

AI 大底座,大模型时代的答卷

百度Geek说

人工智能 百度 文心一言 企业号 5 月 PK 榜

阿里官方上线!号称Java面试八股文天花板(2023最新版)首次开源

Java你猿哥

Java redis Spring Boot JVM java面试

从浏览器输入域名开始分析DNS解析过程

华为云开发者联盟

开发 华为云 DNS 华为云开发者联盟 企业号 5 月 PK 榜

重磅发布!阿里巴巴专家亲自撰写,Dubbo 3.0 分布式实战(彩印版)

做梦都在改BUG

Java 分布式 微服务 dubbo

精选!字节大佬带你一周刷完Java面试八股文,比啃书效果好多了

Java你猿哥

Java 算法 ssm java面试 java知识点

独家巨献!阿里专家兼Github贡献者,整理的SpringBoot入门到成神

做梦都在改BUG

Java spring 架构 微服务 Spring Boot

IT知识百科:什么是下一代防火墙和IPS?

wljslmz

防火墙 三周年连更 入侵防御系统

k8s+Docker部署方法

Java你猿哥

Java Docker k8s ssm 架构师

本周日直播,全链路数据治理实践论坛开放报名

阿里云大数据AI技术

大数据 数据治理

阿里全新推出:微服务突击手册,把所有操作都写出来了|超清PDF

做梦都在改BUG

Java 架构 微服务 Spring Cloud spring cloud alibaba

如何选择最优权限框架?Sa-Token 和 Shiro 对比

做梦都在改BUG

shiro Sa-Token

开源之夏 2023 | 与 Databend 一同探索云数仓的魅力

Databend

硬核Prompt赏析:HuggingGPT告诉你Prompt可以有多“工程”

无人之路

ChatGPT Prompt

一周吃透Java面试八股文(2023最新整理)

Java你猿哥

Java kafka Spring Boot JVM java面试

MySQL百万数据深度分页优化思路分析

Java你猿哥

Java MySQL 数据库 ssm 优化技术

鲸鸿动能广告接入如何高效变现流量?

HarmonyOS SDK

HMS Core

来了!昇腾MindStudio全流程工具链分论坛精彩回顾,助力高效开发和迁移效率提升

科技热闻

Redis和MySQL扛不住,B站分布式存储系统如何演进?

Java你猿哥

Java MySQL redis ssm kv

前端工程师需要了解的 Babel 知识_编程语言_政采云前端团队_InfoQ精选文章