我在团队帮助开发 Node 工具时,遇到了需要对多份相似的代码进行一定的处理,但又不能改变原本仓库的代码,这个非常像我们的编译工具做的事情。在一开始的时候,参考了类似 FIS 的功能,简单参照使用代码的 HOOK 配合正则来进行代码的操作。后面觉得实在难以拓展且无法确保修正后的代码可用性,还是觉得必须使用 Babel 来实现源码的操作。

什么是 Babel

babel 可以说是前端工程化用的最多的工具之一。在 ES6 仅仅只有标准时,我们就开始利用 Babel 的 ES6 转换到 ES5 的功能来使用新的语法特性。包括后续大热的React 制定的 JSX 的语法,“凭空”创建出来的语法,为什么能这么快且自然的就融入到前端体系里,这都离不开 Babel 这个编译器的功劳(compiler)。除了编译器功能外,还可以对代码进行各类的静态分析。

原理

babel 利用 babylon 解析器进行语法解析。解析成 AST(抽象语法树)后,经过一定的规则对这个树进行处理后再生成新的语法树。最终新的语法树在生成对应的合法的 Javascript 语法。

AST

AST,就是编译时常说的抽象语法树。树状结构可以帮助我们更好的索引和操作数据结构。例如我们前端三板斧 HTML/CSS/Javascript,而我们平常认知最深的 HTML DOM 树,通过一棵树就可以表达整个页面结构,而 CSS 和 Javascript 都可以通过编译器将语法解析成 AST 抽象语法树。

代码生成 AST 需要进行词法分析和语法分析。

我们可以看一个最简单的程序语法来管中窥豹一下,我们利用 AST Expoler 看看下面这段语法的 AST 到底是长什么样的。

  1. if(3 > 5) {
  2. console.log('3 大于 5')
  3. } else {
  4. console.log('5 大于等于 3')
  5. }

简单画个示意图,部分属性也精简了一下。

我们可以看到,所有的 AST 树的根结点都是 Program 节点。但即使一个非常简单的 if 判断语法对应的都是一支比较复杂的树结构。而 if 语句对应的最重要的三个属性就是 consequent/test/alternate。判断分支内是一个 BinaryExpression(二元表达式),而在 ture/false 分支内则是 CallExpression(函数调用表达式),而调用的函数来自 MemberExpression(成员表达式)。

下面这个表上面这段代码用到的

属性 描述
BlockStatement 块级表达式
BinaryExpression 二元表达式
CallExpression 函数调用表达式
MemberExpression 成员表达式
Literal 文字

更多详细的 AST 类型都可以查看 Babylon 提供的 Spec 文档

babel 插件

在我们简单了解 AST 树后,就可以具体看看如何利用 Babel 插件来玩转我们的代码。Babel 提供了几个核心的 Lib,我们可以关心以下几个,babel-corebabel-typesbabel-template

  • babel-core。Babel 的核心库,提供了将代码编译转化的能力。
  • babel-types。提供 AST 树节点的类型。
  • babel-template。可以将普通字符串转化成 AST,提供更便捷的使用。

Visitors

Visitors 访问者模式,是我们访问 AST 树上的节点时所用到的方法。

  1. const visitors = {
  2. BinaryExpression(path) {
  3. console.log('访问二元表达式的节点')
  4. },
  5. Literal(path) {
  6. console.log('访问文本')
  7. }
  8. }

例如上图就可以定义不同节点类型的访问者,对节点路径进行访问并对节点进行操作并转化。

Paths

我们在 我们定义成树状结构,除了可以完整描述整个代码的结构,也方便让节点和节点之间进行索引和操作。我们通过 visitor 访问节点时,其实访问的是节点的 path,并不是实际上的节点。path 上有该节点信息 path.node,也有类似 DOM 树上一样的可以访问到父节点的信息 path.parent,方便后续的操作。

  1. // 我们通过 visitor 访问到的 AST 节点后,回调参数 path 示例
  2. {
  3. "parent": {
  4. "type": "BinaryExpression",
  5. "operator": ">"
  6. ....
  7. },
  8. "node": {
  9. "type": "Literal",
  10. "name": "3",
  11. ....
  12. }
  13. }
  14. // in vistors
  15. const visitors = {
  16. Literal(path) {
  17. console.log(path.node.name)
  18. console.log(path.parent.operator)
  19. }
  20. }

示例

我们在了解 visitors 和 paths 后,就可以正式看看如何编写自己的 babel 插件了。

替换操作符

我们最开头的例子的判断条件是一个二元表达式,我们希望将大于号(>)都转换成小于等于(<=)号。

  1. const babel = require('babel-core')
  2. const t = require('babel-types')
  3. const code = `
  4. if(3>5) {}
  5. `
  6. const visitor = {
  7. BinaryExpression(path) {
  8. if(path.node.operator === '>') {
  9. path.replaceWith(
  10. t.binaryExpression(
  11. '<=',
  12. path.node.left,
  13. path.node.right
  14. )
  15. );
  16. }
  17. }
  18. }
  19. const result = babel.transform(code, {
  20. plugins: [
  21. { visitor }
  22. ]
  23. })
  24. console.log(result.code)

import 拆分

在网上很多有关 babel 插件的例子,都会提到这个 import 语句拆分的案例,我也觉得这个案例非常实用且有意义。

我们在使用 lodash 时,如果是用普通的方式引入时,会引入非常大的包,有的时候你只需要使用其中一两个小函数,所以 lodash 提供了分包按需引入的方式。但两者的写法不一样,所以我们需要使用 babel 插件来实现这个功能。

  1. // 转化前,bad
  2. import { uniq, sort as _sort } from 'lodash'
  3. // 转化后,better
  4. import uniq from 'lodash/uniq'
  5. import _sort from 'lodash/sort'

我们可以先去 AST explore 先看看具体 AST 的树结构。然后再来写出我们的插件。

  1. const babel = require('babel-core')
  2. const t = require('babel-types')
  3. const code = `import { unqi, sort as _sort } from 'lodash'`
  4. const visitor = {
  5. ImportDeclaration(path) {
  6. const specifiers = path.node.specifiers
  7. const source = path.node.source
  8. if(!t.isImportDefaultSpecifier(specifiers[0])) {
  9. const myImportDeclaration = specifiers.map(specifier => {
  10. return t.importDeclaration(
  11. [t.importDefaultSpecifier(specifier.local)],
  12. t.stringLiteral(`${source.value}/${specifier.imported.name}`)
  13. )
  14. })
  15. path.replaceWithMultiple(myImportDeclaration)
  16. }
  17. }
  18. }
  19. const result = babel.transform(code, {
  20. plugins: [
  21. { visitor }
  22. ]
  23. })
  24. console.log(result.code)

为你的函数插入一行代码

我们如何对一个函数内部新增 AST 节点,“凭空”插入一行正确无误的代码呢。

  1. const babel = require('babel-core')
  2. const t = require('babel-types')
  3. const code = `
  4. class Page {
  5. onLoad() {
  6. console.log('Hello')
  7. }
  8. }
  9. `
  10. const visitor = {
  11. BlockStatement(path) {
  12. if(path.parent.key.name === 'onLoad' && path.node.body.length === 1) {
  13. const body = path.node.body
  14. const newBlockStatement = t.blockStatement(body.concat(
  15. t.expressionStatement(
  16. t.callExpression(
  17. t.memberExpression(
  18. t.identifier('console'),
  19. t.identifier('log')
  20. ),
  21. [t.stringLiteral('World!')]
  22. )
  23. )
  24. ))
  25. path.replaceWith(newBlockStatement)
  26. }
  27. }
  28. }
  29. const result = babel.transform(code, {
  30. plugins: [
  31. { visitor }
  32. ]
  33. })
  34. console.log(result.code)

其实思路是一样的,只要我们构建出一个 AST 节点,再把这个节点再插到原本的 AST 树上,那么这个 AST 树就可以转化成我们想要的代码了。

最后

这里我们了解了 AST 树,理解了我们学习工作中常用的 Babel 到底背后做了哪些事,我们就可以更好的融入到前端开发的社区,去做更多的事情。更多更复杂更详细的用法,都可以参照 Babel 官网,或 Babel 插件手册。当然有兴趣的同学也可以把玩一下 CSS 的相关 AST 编译,原理都是一样的。

利用 Babel 玩转你的代码的更多相关文章

  1. 利用FFmpeg玩转Android视频录制与压缩(二)<转>

    转载出处:http://blog.csdn.net/mabeijianxi/article/details/72983362 预热 时光荏苒,光阴如梭,离上一次吹牛逼已经过去了两三个月,身边很多人的女 ...

  2. XSS漏洞的渗透利用另类玩法

    XSS漏洞的渗透利用另类玩法 2017-08-08 18:20程序设计/微软/手机 作者:色豹 i春秋社区 今天就来讲一下大家都熟悉的 xss漏洞的渗透利用.相信大家对xss已经很熟悉了,但是很多安全 ...

  3. 逆向进阶,利用 AST 技术还原 JavaScript 混淆代码

    什么是 AST AST(Abstract Syntax Tree),中文抽象语法树,简称语法树(Syntax Tree),是源代码的抽象语法结构的树状表现形式,树上的每个节点都表示源代码中的一种结构. ...

  4. 利用Babel来转化你的ES2015脚本初步

    我们在前面已经安装和学习过babel 安装babel-cli 这是babel解释器的客户端主程序 npm install -g babel-cli 安装”编译”插件(babel的JSX语法转换器) n ...

  5. 【更新WordPress 4.6漏洞利用PoC】PHPMailer曝远程代码执行高危漏洞(CVE-2016-10033)

    [2017.5.4更新] 昨天曝出了两个比较热门的漏洞,一个是CVE-2016-10033,另一个则为CVE-2017-8295.从描述来看,前者是WordPress Core 4.6一个未经授权的R ...

  6. 理解Babel是如何编译JS代码的及理解抽象语法树(AST)

    Babel是如何编译JS代码的及理解抽象语法树(AST) 1. Babel的作用是?   很多浏览器目前还不支持ES6的代码,但是我们可以通过Babel将ES6的代码转译成ES5代码,让所有的浏览器都 ...

  7. git 利用hook 实现服务器自动更新代码

    如何利用git的hook实现提交代码后自动更新? 因为个人开发经常需要提交代码,每次都需要连接服务器去pull代码,重启服务器就显得十分繁琐,因此github提供了一个时间钩子,用户push代码后可以 ...

  8. 利用Python进行异常值分析实例代码

    利用Python进行异常值分析实例代码 异常值是指样本中的个别值,也称为离群点,其数值明显偏离其余的观测值.常用检测方法3σ原则和箱型图.其中,3σ原则只适用服从正态分布的数据.在3σ原则下,异常值被 ...

  9. 利用封装、继承对Java代码进行优化

    注:本文实例分别可以在oldcastle(未优化的代码)和newcastle(优化后的代码)中查看,网址见文末 城堡游戏: 城堡中有多个房间,用户通过输入north, south, east, wes ...

随机推荐

  1. python学习笔记(locust性能测试模块)

    locust是基于python的性能测试工具.支持python2.7及其以上的版本.相对于主流的LR与Jmeter工具使用的方式不一样.locust是通过编写python代码来完成性能测试的. 通过L ...

  2. Android 之低版本高版本实现沉浸式状态栏

    沉浸式状态栏确切的说应该叫做透明状态栏.一般情况下,状态栏的底色都为黑色,而沉浸式状态栏则是把状态栏设置为透明或者半透明. 沉浸式状态栏是从android Kitkat(Android 4.4)开始出 ...

  3. Mybatis之SSM配置

    applicationContext-dao.xml <?xml version="1.0" encoding="UTF-8"?> <bean ...

  4. ASP.NET ValidationSummary 控件

    ASP.NET ValidationSummary 控件 Validation 服务器控件 定义和用法 ValidationSummary 控件用于在网页.消息框或在这两者中内联显示所有验证错误的摘要 ...

  5. HDU 5186 zhx's submissions 模拟,细节 难度:1

    http://acm.hdu.edu.cn/showproblem.php?pid=5186 题意是分别对每一位做b进制加法,但是不要进位 模拟,注意:1 去掉前置0 2 当结果为0时输出0,而不是全 ...

  6. [转载]java正则表达式

    转载自:http://butter.iteye.com/blog/1189600 1.正则表达式的知识要点1.正则表达式是什么?正则表达式是一种可以用于模式匹配和替换的强有力的工具.2.正则表达式的优 ...

  7. Can't create session svn: Unable to connect to a repository at URL “...”的解决方案

    Can't create sessionsvn: Unable to connect to a repository at URL '...' Cannot negotiate authenticat ...

  8. 面筋: 奇虎360 c++ 后台开发 实习生 面试

    投的是360上海的商业化部门,岗位是C++服务端开发实习生,记录一下面试历程: 视频面试,但是是有代码框让你写代码的. 一面: Q:先说一下个人信息,做过的项目 A:.......... Q:先写个翻 ...

  9. Http协议——Header说明

    下图是我用IE的开发人员工具截取的一个Http Request请求的Header. 下图是我用IE的开发人员工具截取的一个Http Response的Header. header常用指令 header ...

  10. HTML页面每次打开的时候都清除页面缓存

    解决办法为: (1) 用HTML标签设置HTTP头信息 <HEAD> <META    HTTP-EQUIV="Pragma" CONTENT="no- ...