在前端圈子里,对于 Babel,大家肯定都比较熟悉了。如果哪天少了它,对于前端工程师来说肯定是个噩梦。Babel 的工作原理是怎样的可能了解的人就不太多了。
本文将主要介绍 Babel 的工作原理以及怎么写一个 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 描述了源代码的每个部分以及它们之间的关系。

AST 是怎么来的?

整个解析过程分为两个步骤:

  • 分词:将整个代码字符串分割成语法单元数组
  • 语法分析:建立分析语法单元之间的关系

分词语法单元通俗点说就是代码中的最小单元,不能再被分割,就像原子是化学变化中的最小粒子一样。Javascript 代码中的语法单元主要包括以下这么几种:

  • 关键字:const、 let、  var 等
  • 标识符:可能是一个变量,也可能是 if、else 这些关键字,又或者是 true、false 这些常量
  • 运算符
  • 数字
  • 空格
  • 注释:对于计算机来说,知道是这段代码是注释就行了,不关心其具体内容

其实分词说白了就是简单粗暴地对字符串一个个遍历。为了模拟分词的过程,写了一个简单的 Demo,仅仅适用于和上面一样的简单代码。Babel 的实现比这要复杂得多,但是思路大体上是相同的。

function tokenizer(input) {
const tokens = [];
const punctuators = [',', '.', '(', ')', '=', ';']; let current = ;
while (current < input.length) { let char = input[current]; if (punctuators.indexOf(char) !== -) { 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-\$\_]/.test(char)) {
value += char;
char = input[++current];
}
tokens.push({ type: 'Identifier', value });
continue;
} // 数字从0-9开始,不止一位
const NUMBERS = /[-]/;
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 官方的介绍。

插件格式

先从一个接收了当前 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 ,先看下 var a = 1的 AST

{
"type": "Program",
"start": ,
"end": ,
"body": [
{
"type": "VariableDeclaration",
"start": ,
"end": ,
"declarations": [
{
"type": "VariableDeclarator",
"start": ,
"end": ,
"id": {
"type": "Identifier",
"start": ,
"end": ,
"name": "a"
},
"init": {
"type": "Literal",
"start": ,
"end": ,
"value": ,
"raw": ""
}
}
],
"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 = `; 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[])) { // 对 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 知识的更多相关文章

  1. 前端工程师应该知道的yarn知识

    yarn 是在工作中离不开的工具,但在工作中,很多人基本只会使用 yarn install,而且会手动删除 node-modules,或删除 yarn.lock 文件等不规范操作.本文将从一些基础的知 ...

  2. 作为一个有B格的前端工程师需要掌握的一些知识

    如果说你3年还在不停地切页面的... 那么你对http协议的了解程度 你的原生的javascript的掌握程度 你的页面的优化的理念 你在写页面是否会有什么独特地技巧 你对ajax的get和post方 ...

  3. 前端需要掌握的Babel知识

    Babel 是怎么工作的 Babel 是一个 JavaScript 编译器. 做与不做 注意很重要的一点就是,Babel 只是转译新标准引入的语法,比如: 箭头函数 let / const 解构 哪些 ...

  4. 淘宝前端工程师:国内WEB前端开发十日谈

    一直想写这篇"十日谈",聊聊我对Web前端开发的体会,顺便解答下周围不少人的困惑和迷惘.我不打算聊太多技术,我想,通过技术的历练,得到的反思应当更重要. 我一直认为自己是" ...

  5. web前端工程师入门须知

    本文是写给那些想要入门web前端工程的初学者,高手请路过,也欢迎高手们拍砖. 先说下web前端工程师的价值,目前web产品交互越来越复杂,用户使用体验和网站前端性能优化这些都得靠web前端工程师去做w ...

  6. Web前端工程师成长之路——知识汇总

    一.何为Web前端工程师?          前端工程师,也叫Web前端开发工程师.他是随着web发展,细分出来的行业.Web前端开发工程师,主要职责是利用(X)HTML/CSS/JavaScript ...

  7. 小白到web前端工程师需要学习哪些知识?

    随着web3.0时代,那么web前端开发技术人才越来越吃香,而且web前端领域划分越来越细,对技术的需求越来越高,想学习web前端的人也是越来越多.那么,如何学习web前端知识?从哪开始?转型成为we ...

  8. 如何才能成为一个合格的web前端工程师

    转载原文地址:https://juejin.im/post/5cc1da82f265da036023b628 开篇前端开发是一个非常特殊的行业,它的历史实际上不是很长,但是知识之繁杂,技术迭代速度之快 ...

  9. 如何面试前端工程师:GitHub 很重要

    编者注:下面这篇文章从面试官的角度介绍到面试时可能会问到的一些问题. 我在Twitter和Stripe的一部分工作内容是面试前端工程师.其实关于面试你可能很有自己的一套,这里我想跟你们分享一下我常用的 ...

随机推荐

  1. docker 入门5 - 栈 【翻译】

    入门,第 5 部分:堆栈 先决条件 安装 Docker 版本 1.13 或更高版本. 获取第 3 部分先决条件中所述的 Docker Compose. 获取 Docker Machine,如第 4 部 ...

  2. 安装 node.js npm,cnpm

    参考:https://blog.csdn.net/suiyuehuimou/article/details/74143436 https://www.liaoxuefeng.com/wiki/0014 ...

  3. Visual Studio container tools require Docker to be running

    处理项目在生成时报错"Visual Studio container tools require Docker to be running" 最初win10上安装docker,项目 ...

  4. NetScaler Logs Collection Guide

    NetScaler Logs Collection Guide 来源  https://support.citrix.com/article/CTX227560 Article | Authentic ...

  5. js之数据类型(对象类型——单体内置对象——JSON)

    JSON(Java Script Object Notation)使用JavaScript语法,是用于存储和传输数据的格式,通常用于服务端向网页传递数据.JSON格式仅仅是一个文本,文本可以被任何编程 ...

  6. kali linux 安装 qq (deepin-wine)

    添加deeepin-wine 依赖 /etc/apt/sources.list: # Generated by deepin-installer deb http://mirrors.aliyun.c ...

  7. 【Distributed】CDN

    一.概述 1.1 Web前端优化 1.2 DNS域名解析过程 1.3 传统方式请求静态资源 二.CDN内容分发 2.1 什么是CDN 2.2 CDN内容分发原理 2.3 阿里云环境实战搭建CDN内容分 ...

  8. web开发: css高级与盒模型

    一.组合选择器 二.复制选择器优先级 三.伪类选择器 四.盒模型 五.盒模型显示区域 六.盒模型布局 一.组合选择器 <!DOCTYPE html> <html> <he ...

  9. drone 更新仓库为truested

    drone 更新仓库为truested drone repo update -trusted=true my_org/repository DRONE_USER_CREATE=username:oct ...

  10. wampserver apache 500 Internal Server Error解决办法

    Internal Server ErrorThe server encountered an internal error or misconfiguration and was unable to ...