热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

剖析Babel——Babel总览

名词解释AST:AbstractSyntaxTree,抽象语法树DI:DependencyInjection,依赖注入Babel的解析引擎Babel使用的引擎是b

名词解释

AST:Abstract Syntax Tree, 抽象语法树

DI: Dependency Injection, 依赖注入

===============================================================

Babel 的解析引擎

Babel 使用的引擎是 babylon,babylon 并非由 babel 团队自己开发的,而是 fork 的 acorn 项目,acorn 的项目本人在很早之前在兴趣部落 1.0 在构建中使用,为了是做一些代码的转换,是很不错的一款引擎,不过 acorn 引擎只提供基本的解析 ast 的能力,遍历还需要配套的 acorn-travesal, 替换节点需要使用 acorn-,而这些开发,在 Babel 的插件体系开发下,变得一体化了


Babel 的工作过程

Babel 会将源码转换 AST 之后,通过便利 AST 树,对树做一些修改,然后再将 AST 转成 code,即成源码。

上面提到 Babel 是 fork acon 项目,我们先来看一个来自兴趣部落项目的,简单的 ACON 示例

一个简单的 ACON 转换示例

解决的问题

Model.task('getData', function($scope, dbService){});


转换成

Model.task('getData', ['$scope', 'dbService', function($scope, dbService){}]);


熟悉 angular 的同学都能看到这段代码做的是对 DI 的自动提取功能,使用 ACON 手动撸代码

var code = 'let a = 1; // ....';var acorn = require("acorn");var traverse = require("ast-traverse");var alter = require("alter");var ast = acorn.parse(code);var ctx = [];traverse(ast, {pre: function(node, parent, prop, idx){if(node.type === "MemberExpression") {var object = node.object;var objectName = object.name;var property = node.property;var propertyName = property.name;// 这里要进行替换if (objectName === "Model" && (propertyName === "service" || propertyName === "task")) {// 第一个就为serviceName 第二个是functionvar arg = parent.arguments;var serviceName = arg[0];var serviceFunc = arg[1];for (var i = 0; i


具体的流程如下

可以从上面的过程看到 acorn 的特点

1.acorn 做为一款优秀的源码解析器

2.acorn 并不提供对 AST 树的修改能力

3.acorn 并不提供 AST 树的还原能力

4. 修改源码仍然靠源码修改字符串的方式

Babel 正是扩展了 acorn 的能力,使得转换变得更一体化

Babel 的前序工作——Babylon、babel-types:code 转换为 AST

Babel 转 AST 树的过程涉及到语法的问题,转 AST 树一定有对就的语法,如果在解析过程中,出现了不符合 Babel 语法的代码,就会报错,Babel 转 AST 的解析过程在 Babylon 中完成

解析成 AST 树使用 babylon.parse 方法

import babylon from 'babylon';let code = `let a = 1, b = 2;function sum(a, b){return a + b;}sum(a, b);`;let ast = babylon.parse(code);console.log(ast);


结果如下

AST 如下

关于 AST 树的详细定义 Babel 有文档

https://github.com/babel/babylon/blob/master/ast/spec.md

关于 AST 树的定义

interface Node {type: string;loc: SourceLocation | null;}


ast 中的节点都是继承自 Node 节点,Node 节点有 type 和 loc 两个属性,分别代表类型和位置,

其中位置定义如下

interface SourceLocation {source: string | null;start: Position;end: Position;}

位置节点又是由 source(源码 string), 开始位置,结束位置组成,start,end 又是 Position 类型

interface Position {line: number; // >= 1column: number; // >= 0}

节点又包含行号和列号

再看 Program 的定义

interface Program <: Node {type: "Program";sourceType: "script" | "module";body: [ Statement | ModuleDeclaration ];directives: [ Directive ];}


Program 是继承自 Node 节点&#xff0c;类型是 Program, sourceType 有两种&#xff0c;一种是 script&#xff0c;一种是 module&#xff0c;程序体是一个声明体 Statement 或者模块声明体 ModuleDeclaration 节点数组

Babylon 支持的语法

Babel 或者说 Babylon 支持的语法现阶段是不可以第三方扩展的&#xff0c;也就是说我们不可以使用 babylon 做一些奇奇怪的语法&#xff0c;换句话说

不要希望通过 babel 的插件体系来转换自己定义的语法规则

那么 babylon 支持的语法有哪些呢&#xff0c;除了常规的 js 语法之外&#xff0c;babel 暂时只支持如下的语法

Plugins

  • estree
  • jsx
  • flow
  • doExpressions
  • objectRestSpread
  • decorators (Based on an outdated version of the Decorators proposal. Will be removed in a future version of Babylon)
  • classProperties
  • exportExtensions
  • asyncGenerators
  • functionBind
  • functionSent
  • dynamicImport

如果要真要自定义语法&#xff0c;可以在 babylon 的 plugins 目录下自定义语法

https://github.com/babel/babylon/tree/master/src/plugins

Babel-types&#xff0c;扩展的 AST 树

上面提到的 babel 的 AST 文档中&#xff0c;并没有提到 JSX 的语法树&#xff0c;那么 JSX 的语法树在哪里定义呢&#xff0c;同样 jsx 的 AST 树也应该在这个文档中指名&#xff0c;然而 babel 团队还没精力准备出来

实际上&#xff0c;babel-types 有扩展 AST 树&#xff0c;babel-types 的 definitions 就是天然的文档&#xff0c;具体的源码定义在这里

举例一个 AST 节点如查是 JSXElement&#xff0c;那么它的定义可以在 jsx.js 中找到

defineType("JSXElement", {builder: ["openingElement", "closingElement", "children", "selfClosing"],visitor: ["openingElement", "children", "closingElement"],aliases: ["JSX", "Immutable", "Expression"],fields: {openingElement: {validate: assertNodeType("JSXOpeningElement"),},closingElement: {optional: true,validate: assertNodeType("JSXClosingElement"),},children: {validate: chain(assertValueType("array"),assertEach(assertNodeType("JSXText", "JSXExpressionContainer", "JSXSpreadChild", "JSXElement"))),},},});


JSXElement 的 builder 字段指明要构造一个这样的节点需要 4 个参数&#xff0c;这四个参数分别对应在 fields 字段中&#xff0c;四个参数的定义如下

openingElement: 必须是一个 JSXOpeningElement 节点

closingElement: 必须是一个 JSXClosingElement 节点

children: 必须是一个数组&#xff0c;数组元素必须是 JSXText、JSXExpressionContainer、JSXSpreadChild 中的一种类型

selfClosing: 未指明验证

使用 babel-types.[TYPE] 方法就可以构造这样的一个 AST 节点

var types &#61; require(&#39;babel-types&#39;);var jsxElement &#61; types.JSXElement(types.OpeningElement(...),types.JSXClosingElement(...),[...],true);


构造了一个 jsxElement 类型的节点&#xff0c;这在 Babel 插件开发中是很重要的

同样验证是否一个 JSXElement 节点&#xff0c;也可以使用 babel-types.isTYPE 方法

比如

var types &#61; require(&#39;babel-types&#39;);types.isJSXElement(astNode);


所以用 JSXElement 语法定义可以直接看该文件&#xff0c;简单做个梳理如下

其中&#xff0c;斜体代表非终结符&#xff0c;粗体为终结符

Babel 的中序工作——Babel-traverse、遍历 AST 树&#xff0c;插件体系


  • 遍历的方法
    一旦按照 AST 中的定义&#xff0c;解析成一颗 AST 树之后&#xff0c;接下来的工作就是遍历树&#xff0c;并且在遍历的过程中进行转换

Babel 负责便利工作的是 Babel-traverse 包&#xff0c;使用方法

import traverse from "babel-traverse";traverse(ast, {enter(path) {if (path.node.type &#61;&#61;&#61; "Identifier" &&path.node.name &#61;&#61;&#61; "n") {path.node.name &#61; "x";}}});

遍历结点让我们可以获取到我们想要操作的结点的可能&#xff0c;在遍历一个节点时&#xff0c;存在 enter 和 exit 两个时刻&#xff0c;一个是进入结点时&#xff0c;这个时候节点的子节点还没触达&#xff0c;遍历子节点完成的时刻&#xff0c;会离开该节点&#xff0c;所以会有 exit 方法触发

访问节点&#xff0c;可以使用的参数是 path 参数&#xff0c;path 这个参数并不直接等同于节点&#xff0c;path 的属性有几个重要的组成&#xff0c;如下

举个栗子&#xff0c;如下的代码会将所有 function 变成另外的 function

import traverse from "babel-traverse";import types from "babel-types";traverse(ast, {enter(path) {let node &#61; path.node;if(types.isFunctionDeclaration(node)){path.replaceWithSourceString(&#96;function add(a, b) {return a &#43; b;}&#96;);}}});

结果如下

- function square(n) {-   return n * n;&#43; function add(a, b) {&#43;   return a &#43; b;}


注意这里我们使用 babel-types 来判别 node 的类型&#xff0c;使用 path 的 replaceWithSourceString 方法来替换节点

但这里在 babel 的文档中也有提示&#xff0c;尽量少用 replaceWithSourceString 方法&#xff0c;该方法一定会调用 babylon.parse 解析代码&#xff0c;在遍历中解析代码&#xff0c;不如将解析代码放到遍历外面去做

其实上面的过程只是定义了如何遍历节点的时候转换节点

babel 将上面的便利操作对外开放出去了&#xff0c;这就构成了 babel 的插件体系

babel 的插件体系——结点的转换定义

babel 的插件就是定义如何转换当前结点&#xff0c;所以从这里可以看出 babel 的插件能做的事情&#xff0c;只能转换 ast 树&#xff0c;而不能在作用在前序阶段&#xff08;语法分析&#xff09;

这里不得不提下 babel 的插件体系是怎么样的&#xff0c;babel 的插件分为两部分

  • babel-preset-xxx
  • babel-plugin-xxx

preset: 预设, preset 和 plugin 其实是一个东西&#xff0c;preset 定义了一堆 plugin list

这里值得一提的是&#xff0c;preset 的顺序是倒着的&#xff0c;plugin 的顺序是正的&#xff0c;也就是说

preset: [&#39;es2015&#39;, &#39;react&#39;], 其实是先使用 react 插件再用 es2015

plugin: [&#39;transform-react&#39;, &#39;transfrom-async-function&#39;] 的顺序是正的遍历节点的时候先用 transform-react 再用 transfrom-async-function

babel 插件编写

如果是自定义插件&#xff0c;还在开发阶段&#xff0c;要先在 babel 的配置文件指明 babel 插件的路径

{"extensions": [".jsx", ".js"],"presets": ["react", "es2015"],"plugins": [[path.resolve(SERVER_PATH, "pourout/babel-plugin-transform-xxx"),{}],]}


babel 的自定义插件写法是多样&#xff0c;上面只是一个例子&#xff0c;可以传入 option&#xff0c;具体可以参考 babel 的配置文档

上面的代码写成 babel 的插件如下

module.exports &#61;  function(babel) {var types &#61; babel.types;// plugin contentsreturn {visitor: {FunctionDeclaration: {enter: function(path){path.replaceWithSourceString(&#96;function add(a, b){ return a &#43; b}&#96;);}}}};};


Babel 的插件包 return 一个 function, 包含 babel 的参数&#xff0c;function 运行后返回一个包含 visitor 的对象&#xff0c;对象的属性是遍历节点匹配到该类型的处理方法&#xff0c;该方法依然包含 enter 和 exit 方法

一些 AST 树的创建方法

在写插件的过程中&#xff0c;经常要创建一些 AST 树&#xff0c;常用的方法如下

  • 使用 babel-types 定义的创建方法创建
    比如创建一个 var a &#61; 1;

types.VariableDeclaration(&#39;var&#39;,[types.VariableDeclarator(types.Identifier(&#39;a&#39;),types.NumericLiteral(1))])


如果使用这样创建一个 ast 节点&#xff0c;肯定要累死了

  • 使用 replaceWithSourceString 方法创建替换
  • 使用 template 方法来创建 AST 结点
    template 方法其实也是 babel 体系中的一部分&#xff0c;它允许使用一些模板来创建 ast 节点

比如上面的 var a &#61; 1 可以使用

var gen &#61; babel.template(&#96;var NAME &#61; VALUE;&#96;);var ast &#61; gen({NAME: t.Identifier(&#39;a&#39;),VALUE: t.NumberLiteral(1)});


当然也可以简单写

var gen &#61; babel.template(&#96;var a &#61; 1;&#96;);var ast &#61; gen({});


接下来就可以用 path 的增、删、改操作进行转换了

Babel 的后序工作——Babel-generator、AST 树转换成源码

Babel-generator 的工作就是将一颗 ast 树转回来&#xff0c;具体操作如下

import generator from "babel-generator";let code &#61; generator(ast);


至此&#xff0c;代码转换就算完成了

Babel 的外围工作——Babel-register&#xff0c;动态编译

通常我们都是使用 webpack 编译后代码再执行代码的&#xff0c;使用 Babel-register 允许我们不提前编译代码就可以运行代码&#xff0c;这在 node 端是非常便利的

在 node 端&#xff0c;babel-regiser 的核心实现是下面这两个代码

function loader(m, filename) {m._compile(compile(filename), filename);}function registerExtension(ext) {var old &#61; oldHandlers[ext] || oldHandlers[".js"] || require.extensions[".js"];require.extensions[ext] &#61; function (m, filename) {if (shouldIgnore(filename)) {old(m, filename);} else {loader(m, filename, old);}};}

通过定义 require.extensions 方法&#xff0c;可以覆盖 require 方法&#xff0c;这样调用 require 的时候&#xff0c;就可以走 babel 的编译&#xff0c;然后使用 m._compile 方法运行代码

但这个方法在 node 是不稳定的方法

结语

最后&#xff0c;就像 babylon 官网感觉 acorn 一样&#xff0c;babel 为前端界做了一件 awesome 的工作&#xff0c;有了 babel&#xff0c;不仅仅可以让我们的新技术的普及提前几年&#xff0c;我们可以通过写插件做更多的事情&#xff0c;比如做自定义规则的验证&#xff0c;做 node 的直出 node 端的适配工作等等。

参考资料

babel 官网&#xff1a; https://babeljs.io

babel-github: Babel · GitHub

babylon: GitHub - babel/babylon: PSA: moved into babel/babel as &#64;babel/parser -->

acorn: https://github.com/marijnh/acorn

babel-ast 文档&#xff1a; babylon/spec.md at master · babel/babylon · GitHub

babel 插件 cookbook: https://github.com/thejameskyle/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md

babel-packages: https://github.com/babel/babel/tree/7.0/packages

babel-types-definitions: https://github.com/babel/babel/tree/7.0/packages/babel-types/src/definitions


推荐阅读
  • Spring框架的核心组件与架构解析 ... [详细]
  • 单元测试:使用mocha和should.js搭建nodejs的单元测试
    2019独角兽企业重金招聘Python工程师标准BDD测试利器:mochashould.js众所周知对于任何一个项目来说,做好单元测试都是必不可少 ... [详细]
  • 大类|电阻器_使用Requests、Etree、BeautifulSoup、Pandas和Path库进行数据抓取与处理 | 将指定区域内容保存为HTML和Excel格式
    大类|电阻器_使用Requests、Etree、BeautifulSoup、Pandas和Path库进行数据抓取与处理 | 将指定区域内容保存为HTML和Excel格式 ... [详细]
  • 在《Cocos2d-x学习笔记:基础概念解析与内存管理机制深入探讨》中,详细介绍了Cocos2d-x的基础概念,并深入分析了其内存管理机制。特别是针对Boost库引入的智能指针管理方法进行了详细的讲解,例如在处理鱼的运动过程中,可以通过编写自定义函数来动态计算角度变化,利用CallFunc回调机制实现高效的游戏逻辑控制。此外,文章还探讨了如何通过智能指针优化资源管理和避免内存泄漏,为开发者提供了实用的编程技巧和最佳实践。 ... [详细]
  • 当使用 `new` 表达式(即通过 `new` 动态创建对象)时,会发生两件事:首先,内存被分配用于存储新对象;其次,该对象的构造函数被调用以初始化对象。为了确保资源管理的一致性和避免内存泄漏,建议在使用 `new` 和 `delete` 时保持形式一致。例如,如果使用 `new[]` 分配数组,则应使用 `delete[]` 来释放内存;同样,如果使用 `new` 分配单个对象,则应使用 `delete` 来释放内存。这种一致性有助于防止常见的编程错误,提高代码的健壮性和可维护性。 ... [详细]
  • 在使用SSH框架进行项目开发时,经常会遇到一些常见的问题。例如,在Spring配置文件中配置AOP事务声明后,进行单元测试时可能会出现“No Hibernate Session bound to thread”的错误。本文将详细探讨这一问题的原因,并提供有效的解决方案,帮助开发者顺利解决此类问题。 ... [详细]
  • 如何高效启动大数据应用之旅?
    在前一篇文章中,我探讨了大数据的定义及其与数据挖掘的区别。本文将重点介绍如何高效启动大数据应用项目,涵盖关键步骤和最佳实践,帮助读者快速踏上大数据之旅。 ... [详细]
  • 本文作为“实现简易版Spring系列”的第五篇,继前文深入探讨了Spring框架的核心技术之一——控制反转(IoC)之后,将重点转向另一个关键技术——面向切面编程(AOP)。对于使用Spring框架进行开发的开发者来说,AOP是一个不可或缺的概念。了解AOP的背景及其基本原理,对于掌握这一技术至关重要。本文将通过具体示例,详细解析AOP的实现机制,帮助读者更好地理解和应用这一技术。 ... [详细]
  • 本文介绍了在 Java 编程中遇到的一个常见错误:对象无法转换为 long 类型,并提供了详细的解决方案。 ... [详细]
  • 原文网址:https:www.cnblogs.comysoceanp7476379.html目录1、AOP什么?2、需求3、解决办法1:使用静态代理4 ... [详细]
  • Web开发框架概览:Java与JavaScript技术及框架综述
    Web开发涉及服务器端和客户端的协同工作。在服务器端,Java是一种优秀的编程语言,适用于构建各种功能模块,如通过Servlet实现特定服务。客户端则主要依赖HTML进行内容展示,同时借助JavaScript增强交互性和动态效果。此外,现代Web开发还广泛使用各种框架和库,如Spring Boot、React和Vue.js,以提高开发效率和应用性能。 ... [详细]
  • 如何撰写适应变化的高效代码:策略与实践
    编写高质量且适应变化的代码是每位程序员的追求。优质代码的关键在于其可维护性和可扩展性。本文将从面向对象编程的角度出发,探讨实现这一目标的具体策略与实践方法,帮助开发者提升代码效率和灵活性。 ... [详细]
  • 在Django中提交表单时遇到值错误问题如何解决?
    在Django项目中,当用户提交包含多个选择目标的表单时,可能会遇到值错误问题。本文将探讨如何通过优化表单处理逻辑和验证机制来有效解决这一问题,确保表单数据的准确性和完整性。 ... [详细]
  • 如何使用和示例代码解析 org.semanticweb.owlapi.model.OWLSubPropertyChainOfAxiom.getPropertyChain() 方法 ... [详细]
  • 本文深入解析了 Apache 配置文件 `httpd.conf` 和 `.htaccess` 的优化方法,探讨了如何通过合理配置提升服务器性能和安全性。文章详细介绍了这两个文件的关键参数及其作用,并提供了实际应用中的最佳实践,帮助读者更好地理解和运用 Apache 配置。 ... [详细]
author-avatar
Apollo宫保鸡丁
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有