Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Babel的另类实践 - 重构古董代码

Babel的另类实践 - 重构古董代码

作者头像
腾讯VTeam技术团队
发布于 2020-10-14 09:41:03
发布于 2020-10-14 09:41:03
92000
代码可运行
举报
文章被收录于专栏:VTeam技术团队VTeam技术团队
运行总次数:0
代码可运行

在最近的工作中,接手了一个古老的项目,其中的 JS 代码是一整坨的面条代码,约 3000 行的代码全写在一个文件里,维护起来着实让人头疼。

想不通为啥之前维护项目的同学能够忍受这么难以维护的代码……既然现在这个锅被我拿下了,怎么着也不能容忍如此丑陋的代码继续存在着,必须把它优化一下。

横竖看了半天,由于逻辑都揉在了一个文件里,看都看得眼花缭乱,当务之急便是把它进行模块化拆分,把这一大坨面条状代码拆分成一个个模块并抽离成文件,这样才方便后续的持续优化。

一、结构分析

说干就干,既然要拆分成模块,首先就要分析源码的结构。虽然源码内容很长很复杂,但万幸的是它还是有一个清晰的结构,简化一下,就是下面这种形式:

很容易看出,这是一种 ES5 时代的经典代码组织方式,在一个 IIFE 里面放一个构造函数,在构造函数的 protorype 上挂载不同的方法,以实现不同的功能。既然代码结构是清晰的,那么我们要做模块化的思路也很清晰,就是想办法把所有绑定在构造函数的 prototype 上的方法抽离出来,以模块文件的形式放置,而源码则使用 ES6 的 import 语句把模块引入进来,完成代码的模块化:

为了完成这个效果,我们可以借助 @babel 全家桶来构造我们的转化脚本。

二、借助 AST 分析代码

关于 AST 的相关资料一搜一大堆,在这里就不赘述了。在本文中,我们会借助 AST 去分析源码,挑选源码中需要被抽离、改造的部分,因此 AST 可以说是本文的核心。在 https://astexplorer.net/ 这个网站,我们可以贴入示例代码,在线查看它的 AST 长什么样:

从右侧的 AST 树中可以很清晰地看到, Demo.prototype.func=function(){} 属于 AssignmentExpression 节点,即为“赋值语句”,拥有左右两个不同的节点( leftright)。

由于一段 JS 代码里可能存在多种赋值语句,而我们只想处理形如 Demo.prototype.func=function(){} 的情况,所以我们需要继续对其左右两侧的节点进行深入分析。

首先看左侧的节点,它属于一个“MemberExpression”,其特征如下图箭头所示:

对于左侧的节点,只要它的 object.property.name 的值为 prototype 即可,那么对应的函数名就是该节点的 property.name

接着看右侧的节点,它属于一个“FunctionExpression”:

我们要做的,就是把它提取出来作为一个独立的文件。

分析完了 AST 以后,我们已经知道需要被处理的代码都有一些什么样的特征,接下来就是针对这些特征进行操作了,这时候就需要我们的 @babel 全家桶出场了!

三、处理代码

首先我们需要安装四个工具,它们分别是:

  • @babel/parser:用于把 JS 源码转化成 AST;
  • @babel/traverse:用于遍历 AST 树,获取当中的节点内容;
  • @babel/generator:把 AST 节点转化成对应的 JS 代码;
  • @babel/types:新建 AST 节点。

接下来新建一个 index.js 文件,引入上面四个工具,并设法加载我们的源码(源码为 demo/es5code.js):

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const fs = require('fs')const { resolve } = require('path')
const parser = require('@babel/parser')const traverse = require('@babel/traverse').defaultconst generator = require('@babel/generator').defaultconst t = require('@babel/types')
const INPUT_CODE = resolve(__dirname, '../demo/es5code.js')
const code = fs.readFileSync(`${INPUT_CODE}`, 'utf-8')

接着使用 @babel/parser 获取源码的 AST:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const ast = parser.parse(code)

拿到 AST 以后,就可以使用 @babel/traverse 来遍历它的节点。从上一节的 AST 分析可以知道,我们只需要关注“AssignmentExpression”节点即可:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
traverse(ast, {  AssignmentExpression ({ node }) {    /* ... */  }})

当前节点即为参数 node,我们需要分析它左右两侧的节点。只有当左侧节点的类型为“MemberExpression”且右侧节点的类型为“FunctionExpression”才需要进入下一步分析(因为形如 a=1 之类的节点也属于 AssignmentExpression 类型,不在我们的处理范围内)。

由于 JS 中可能存在不同的 MemberExpression 节点,如 a.b.c=function(){},但我们现在只需要处理 a.prototype.func 的情况,意味着要盯着关键字 prototype。通过分析 AST 节点,我们知道这个关键字位于左侧节点的 object.property.name 属性中:

同时对应的函数名则藏在左侧节点的 property.name 属性中:

因此便可以很方便地提取出方法名

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
traverse(ast, {  AssignmentExpression ({ node }) {    const { left, right } = node    if (left.type === 'MemberExpression' && right.type === 'FunctionExpression') {      const { object, property } = left      if (object.property.name === 'prototype') {        const funcName = property.name // 提取出方法名        console.log(funcName)      }    }  }})

可以很方便地把方法名打印出来检查:

现在我们已经分析完左侧节点的代码,提取出了方法名。接下来则是处理右侧节点。由于右侧代码直接就是一个 FunctionExpression 节点,因此我们要做的就是通过 @babel/generator 把该节点转化成 JS 代码,并写入文件。

此外,我们也要把原来的代码从 Demo.prototype.func=function(){} 转化成 Demo.prototype.func=func 的形式,因此右侧的节点需要从“FuncitionExpression”类型转化成“Identifier”类型,我们可以借助 @babel/types 来处理。

还有一个事情别忘了,就是我们已经把右侧节点的代码抽离成了 JS 文件,那么我们也应该在最终改造完的源文件里把它们给引入进来,形如 importfunc1from'./func1'这种形式,因此可以继续使用 @babel/typesimportDeclaration() 函数来生成对应的代码。这个函数参数比较复杂,可以封装成一个函数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function createImportDeclaration (funcName) {  return t.importDeclaration([t.importDefaultSpecifier(t.identifier(funcName))], t.stringLiteral(`./${funcName}`))}

只需要传入一个 funcName,就可以生成一段 importfuncNamefrom'./funcName' 代码。

最终整体代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const fs = require('fs')const { resolve } = require('path')
const parser = require('@babel/parser')const traverse = require('@babel/traverse').defaultconst generator = require('@babel/generator').defaultconst t = require('@babel/types')
const INPUT_CODE = resolve(__dirname, '../demo/es5code.js')const OUTPUT_FOLDER = resolve(__dirname, '../output')
const code = fs.readFileSync(`${INPUT_CODE}`, 'utf-8')const ast = parser.parse(code)
function createFile (filename, code) {  fs.writeFileSync(`${OUTPUT_FOLDER}/${filename}.js`, code, 'utf-8')}
function createImportDeclaration (funcName) {  return t.importDeclaration([t.importDefaultSpecifier(t.identifier(funcName))], t.stringLiteral(`./${funcName}`))}
traverse(ast, {  AssignmentExpression ({ node }) {    const { left, right } = node    if (left.type === 'MemberExpression' && right.type === 'FunctionExpression') {      const { object, property } = left      if (object.property.name === 'prototype') {            // 获取左侧节点的方法名        const funcName = property.name        // 获取右侧节点对应的 JS 代码        const { code: funcCode } = generator(right)        // 右侧节点改为 Identifier        const replacedNode = t.identifier(funcName)        node.right = replacedNode               // 借助 `fs.writeFileSync()` 把右侧节点的 JS 代码写入外部文件        createFile(funcName, 'export default ' + funcCode)
        // 在文件头部引入抽离的文件        ast.program.body.unshift(createImportDeclaration(funcName))      }    }  }})
// 输出新的文件createFile('es6code', generate(ast).code)

四、运行脚本

在我们的项目目录中,其结构如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
.├── demo│   └── es5code.js├── output├── package.json└── src    └── index.js

运行脚本, demo/es5code.js 的代码将会被处理,然后输出到 output 目录:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
.├── demo│   └── es5code.js├── output│   ├── es6code.js│   ├── func1.js│   ├── func2.js│   └── func3.js├── package.json└── src    └── index.js

看看我们的代码:

大功告成!把脚本运用到我们的项目中,甚至可以发现原来的约 3000 行代码,已经被整理成了 300 多行:

放到真实环境去跑一遍这段代码,原有功能不受影响!

小结

刚刚接手这个项目,我的内心是一万头神兽奔腾而过,是非常崩溃的。但是既然接手了,就值得好好对待它。借助 AST 和 @babel 全家桶,我们就有了充分改造源码的手段。花半个小时写个脚本,把丑陋的面条代码整理成清晰的模块化代码,内心的阴霾一扫而空,对这个古老的项目更是充满了期待——会不会有更多的地方可以被改造被优化呢?值得拭目以待!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-09-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 腾讯VTeam技术团队 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
使用 AST 实现 babel 插件编写
抽象语法树 (Abstract Syntax Tree) 是源代码语法结构的⼀种抽象表示,以树状描述编程语⾔的语法结构,每个节点表示源代码中的⼀种结构。AST常用于代码语法检查、⻛格检查、格式化、代码提示、混淆压缩、自动补全等,还可以用来优化代码结构,如 webpack 以及 CommonJS、AMD、CMD、UMD等代码规范之间的转化等。
CS逍遥剑仙
2022/08/01
1.3K0
前端AST详解,手写babel插件
抽象语法树(Abstract Syntax Tree,AST),是源代码(不仅限于JavaScript,同时还应用于其他语言,例如: Python,Rust等)语法结构的⼀种抽象表示。它以树状的形式表现编程语⾔的语法结构,树上的每个节点都表示源代码中的⼀种结构。 AST 运⽤⼴泛,⽐如:
can4hou6joeng4
2023/11/29
3710
JS代码之混淆
抽象语法树(Abstract Syntax Tree),简称 AST,初识 AST 是在一门网页逆向的课程,该课程讲述了 js 代码中混淆与还原的对抗,而所使用的技术便是 AST,通过 AST 能很轻松的将 js 源代码混淆成难以辨别的代码。同样的,也可以通过 AST 将其混淆的代码 还原成执行逻辑相对正常的代码。
愧怍
2022/12/27
22.7K0
JS代码之混淆
编译原理工程实践—05使用babel操作AST实现代码转换
babel 是一个 JavaScript 编译器,使用 babel 可以随心所欲地转化和操作 AST,实现对代码的分析、优化、变更等。可以在 https://esprima.org/demo/parse.html 体验转换查看 js 代码的词法、语法和AST。
CS逍遥剑仙
2025/05/12
970
手把手带你走进Babel的编译世界
谈及 Babel,必然离不开 AST。有关 AST 这个知识点其实是很重要的,但由于涉及到代码编译阶段,大多情况都是由各个框架内置相关处理,所以作为开发(使用)者本身,往往会忽视这个过程。希望通过这篇文章,带各位同学走进 AST,借助 AST 发挥更多的想象力。
PHP开发工程师
2022/03/24
5990
手把手带你走进Babel的编译世界
Javascript抽象语法树下篇(实践篇)
目前babel不管是从生态上还是文档上比esprima要好很多,因此推荐大家使用babel工具,本文示例也使用babel来做演示。
WecTeam
2019/12/16
1.8K0
Javascript抽象语法树下篇(实践篇)
Babel原理
Babel是什么?我们为什么要了解它? 1. 什么是babel ? Babel 是一个 JavaScript 编译器。他把最新版的javascript编译成当下可以执行的版本,简言之,利用babel就
null仔
2020/02/28
1.3K0
Babel原理
一文助你搞懂 AST
抽象语法树(Abstract Syntax Tree)简称 AST,是源代码的抽象语法结构的树状表现形式。webpack、eslint 等很多工具库的核心都是通过抽象语法书这个概念来实现对代码的检查、分析等操作。今天我为大家分享一下 JavaScript 这类解释型语言的抽象语法树的概念
coder_koala
2020/06/22
2.7K1
【Web技术】780- AST 实现函数错误自动上报
之前有身边有人问我在错误监控中,如何能实现自动为函数自动添加错误捕获。今天我们来聊一聊技术如何实现。先讲原理:在代码编译时,利用 babel 的 loader,劫持所有函数表达。然后利用 AST(抽象语法树) 修改函数节点,在函数外层包裹 try/catch。然后在 catch 中使用 sdk 将错误信息在运行时捕获上报。如果你对编译打包感兴趣,那么本文就是为你准备的。
pingan8787
2020/11/19
9950
【Web技术】780- AST 实现函数错误自动上报
不一样的JavaScript
导读:本文以JavaScript计算机编程语言为载体,从执行过程去解析它的运行原理,从编译的角度去解析它的结构,最后以AST和产生式作为切入点进行案例分析,目的是为了让读者从更底层去了解计算机编程语言。
@超人
2021/07/05
5080
不一样的JavaScript
掌握 AST,轻松落地关键业务「技术创作101训练营」
如果你查看目前任何主流的项目中的 devDependencies,我们不会在生产环境用到,但是它们在开发过程中充当着重要的角色。归纳一下有:javascript转译、代码压缩、css预处理器、elint、pretiier,postcss等。所有的上述工具,不管怎样,都建立在了AST这个巨人的肩膀上,都是 AST 的运用:
奋飛
2020/09/20
1.1K0
掌握 AST,轻松落地关键业务「技术创作101训练营」
babel源码详解-v1.7.8
继续打开 github 看一下最初的版本的 babel 是怎么实现的,了解它的基本原理。
windliang
2022/09/23
7310
babel源码详解-v1.7.8
一处JS反调试引发的思考
最终这样调用:new _0x4b1809(_0x2ba9)['OsLPar']();
亿人安全
2022/06/30
4030
探索:怎样将单个vue文件转换为小程序所需的四个文件(wxml, wxss, json, js)
比如JavaScript在执行之前,会经过词法分析和语法分析两个步骤之后,得到一个抽象语法树。
极乐君
2019/09/08
5.3K0
AST 实战
最近突然对 AST 产生了兴趣,深入了解后发现它的使用场景还真的不少,很多我们日常开发使用的工具都跟它息息相关,如 Babel、ESLint 和 Prettier 等。本文除了介绍 AST 的一些基本概念外,更偏重实战,讲解如何利用它来对代码进行修改。
Dickensl
2022/06/14
8490
AST 实战
AST抽象语法树——最基础的javascript重点知识,99%的人根本不了解
Javascript就像一台精妙运作的机器,我们可以用它来完成一切天马行空的构思。
Nealyang
2019/09/29
2.4K1
AST抽象语法树——最基础的javascript重点知识,99%的人根本不了解
Babel快速指南
结构上属于编译器,由于输入JS源码,输出也是JS源码(所谓source to source),所以也称为transpiler(转译器)
ayqy贾杰
2019/06/12
1.1K0
深入浅出 Babel 上篇:架构和原理 + 实战
这个文章系列将带大家深入浅出 Babel, 这个系列将分为上下两篇:上篇主要介绍 Babel 的架构和原理,顺便实践一下插件开发的;下篇会介绍 babel-plugin-macros , 利用它来写属于 Javascript 的宏,
Nealyang
2019/10/14
9080
深入浅出 Babel 上篇:架构和原理 + 实战
Web Spider Babel安装 & Ast抽象语法 - 基本使用
Ast反混淆语法在线网址:https://astexplorer.net Babel官方文档:https://www.babeljs.cn/docs/babel-types#stringliteral Babel中文文档:https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/README.md js逆向-ast混淆还原入门案例(1):https://blog.csdn.net/qq_42748190/article/details/106135484
EXI-小洲
2023/03/01
5510
Web Spider Babel安装 & Ast抽象语法 - 基本使用
【编译技术】:解读 Babel AST Format——05
The Babel parser generates AST according to Babel AST format. It is based on ESTree spec with some deviations.
WEBJ2EE
2020/11/05
1.5K1
【编译技术】:解读 Babel AST Format——05
相关推荐
使用 AST 实现 babel 插件编写
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验