速来报名!AICon北京站鸿蒙专场~ 了解详情
写点什么

前端工程师需要了解的 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:381860

评论 1 条评论

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

恒源云(GPUSHARE)_CV领域之几何变化

恒源云

深度学习 CV

12. 《重学 JAVA》-- 面向对象

杨鹏Geek

Java 25 周年 28天写作 12月日更

架构训练营 week2 学习总结

红莲疾风

「架构实战营」

43 K8S之节点/Pod亲和调度

穿过生命散发芬芳

k8s 28天写作 12月日更

一个小插曲

Tiger

28天写作

AOP在JavaScript和TypeScript中的应用

RingCentral铃盛

英特尔技术发力:着眼10倍封装密度提升,30%~50%晶体管密度提升,布局非硅基半导体

科技新消息

Dubbo 框架学习笔记二

风翱

dubbo 12月日更

TDengine入驻Rainbond开源应用商店

北京好雨科技有限公司

时序数据库 #Kubernetes# rainbond

【等保小知识】安全等保是什么意思?是ccrc吗?

行云管家

等保 等级保护 ccrc

利用漏洞修复漏洞:青藤提供的「Log4j命令注入漏洞(CVE-2021-44228)」【免重启】在线热补丁服务现已上线!

青藤云安全

网络安全 漏洞修复

如何高效完成HarmonyOS分布式应用测试?|HDC2021技术分论坛

HarmonyOS开发者

HarmonyOS ArKUI 3.0

如何知道页面浏览时长?

神策技术社区

采集 iOS SDK 页面浏览

如何避免移动研发的一些坑

Speedoooo

研发效能 ios开发 APP开发 APP软件开发 Andriod开发

超写实“龚俊数字人”上线,百度智能云助力各行业打造定制数字人

百度开发者中心

人工智能

使用HTML,CSS和Javascript构建响应式导航栏和面包屑菜单

海拥(haiyong.site)

响应式 大前端 28天写作 签约计划第二季 12月日更

[架构实战营] 模块六作业

张祥

架构实战营

百度开源一款基于 Git 的多仓库管理工具:MGit

百度开发者中心

git 开源

架构实战营模块六作业

渐行渐远

架构实战营

How to construct a Playground Project

Changing Lin

12月日更

使用WT工具恢复MongoDB数据

MongoDB中文社区

mongodb

Go语言学习查缺补漏ing Day8

恒生LIGHT云社区

golang 编程语言 Go 语言

十年所学,终成《代码随想录》!

博文视点Broadview

JetBrains又出神器啦!Fleet,体验飞一般的感觉

程序那些事

Java ide JetBrains 程序那些事 12月日更

CSS之选择器(五)

Augus

CSS 12月日更

【等保测评】山东省9家等保测评机构名单汇总

行云管家

等保 等级保护 等保测评 等保评测

Linux一学就会之RAID磁盘阵列的原理与搭建

学神来啦

Linux 运维 linux运维 raid linux云计算

如何在建木CI中使用Vault管理密钥

Jianmu

DevOps CI/CD 开源软件

如何使用会声会影标题工具制作弹幕效果

懒得勤快

MongoDB在 AWS Marketplace 中推出即用即付产品,有力提升客户体验

MongoDB中文社区

mongodb

node.js中利用IPC和共享内存机制实现计算密集型任务转移

RingCentral铃盛

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