首页 > 其他分享 >一次基于AST的大规模代码迁移实践

一次基于AST的大规模代码迁移实践

时间:2024-09-26 11:46:11浏览次数:10  
标签:Vue const AST 处理 代码 迁移

作者:来自 vivo 互联网大前端团队- Wei Xing

在研发项目过程中,我们经常会遇到技术架构迭代更新的需求,通过技术的迭代更新,让项目从新的技术特性中受益,但由于很多新的技术迭代版本并不能完全向下兼容,包含了很多非兼容性的改变(Breaking Changes),因此我们需要设计一款工具,帮助我们完成大规模代码自动迁移问题。本文简单阐述了基于 AST 的代码迁移概念和大致流程,并通过代码案例带大家了解到了其中的处理细节。

一、背景介绍

在研发项目过程中,我们经常会遇到技术架构迭代更新的需求,通过技术的迭代更新,让项目从新的技术特性中受益。例如将 Vue 2 迁移至 Vue 3、Webpack 4 升级 Webpack 5、构建工具迁移至 Vite 等,这些技术架构的升级能让项目持续受益,获得诸如可维护性、性能、扩展性、编译速度、可读性等等方面的提升,适时的对项目进行技术架构更新是很有必要的。

那既然新特性这么好,有人会说那当然要与时俱进,随时更新了。

但问题在于很多新的技术迭代版本并不能完全向下兼容,包含了很多非兼容性的改变(Breaking Changes),并不是简单升个版本就行了,通常还需要投入不少的人力和学习成本。例如 Vue 3 只能兼容 80%的 Vue 2 代码,对于一些新特性、新语法糖,开发者只能参考官方提供的迁移文档,手动完成迁移。

(图片来源:freecodecamp

1.1 Vue 3 代码迁移案例

来看一个 Vue 3 的代码迁移案例,在 Vue 2 和 Vue 3 中声明一个全局指令(Directive)的差异:

(1)Vue 2:允许直接在 Vue 原型上注册全局指令。而在 Vue 3 中,为了避免多个 Vue 实例产生指令混淆,已经不再支持该写法。

import Vue from 'vue'

Vue.directive('focus', {
  inserted: (el) => el.focus()
})

 

(2)Vue 3:建议通过 createApp 创建 Vue 实例,并直接在实例上注册全局指令。就像这样

import { createApp } from 'vue'

const app = createApp({})

app.directive('focus', {
  inserted: (el) => el.focus() 
})

 

以上是一个大家熟知的 Vue 3 迁移案例,看似简单,动几行代码即可。但当我们的项目规模足够大,或者有大量项目都需要类似代码迁移时,工作量会变得巨大,并且很难规避手动迁移的带来的风险。

因此,一般针对大规模的项目迁移,最好的方式还是写个脚手架工具,协助我们完成自动化迁移。既能提高效率,又能降低人工迁移的风险。

1.2 本文的代码迁移背景

同样地,我在项目中也遇到了相同的技术架构升级难题。简单来说,我需要将基于 Vue 2 的项目迁移到一个我司内部自研的技术栈,这个技术栈的语法结构和 Vue 2 相似,但由于底层的技术原因,有一部分语法上的差异,需要手动去迁移改造兼容(类似 Vue 2 升级至 Vue 3 的过程)。

除了和迁移 Vue 3 一样需要针对 JavaScript、Template 模板做迁移处理之外,我还需要额外去单独处理 CSS、Less、SCSS 等样式文件。

所以,我实现了一个自动化迁移脚手架工具,来协助完成代码的迁移工作,减少人工迁移带来的低效和风险问题。

二、代码迁移思路

刚刚提到我们需要设计一个脚手架来帮助我们完成自动化的代码迁移,那脚手架该如何设计呢?

首先,代码迁移思路可以简单概括为:对原代码做静态代码分析,并按一定规则替换为新代码。那最直观的办法就是利用正则表达式来匹配和替换代码,所以我也做了这样的尝试。

2.1 思路一:利用正则表达式匹配规则和替换代码

例如,将下述代码:

import { toast } from '@vivo/v-jsbridge'

 

import { toast } from '@webf/webf-vue-render'

 

这看起来很简单,似乎用正则匹配即可完成,像这样:

const regx = /\@vivo\/v\-jsbridge/gi

const target = '@webf/webf-vue-render'

sourceCode.replace(regx, target)

 

但在实操过程中,发现正则表达式实在太局限,有几个核心问题:

  • 正则表达式完全基于字符串匹配,对原代码格式的统一性要求很高。空格、换行、单双引号等格式差异都可能引起正则匹配错误;

  • 面对复杂的匹配场景,正则表达式很难写、很晦涩,容易误匹配、误处理;

  • 处理样式文件时,需要兼容 CSS / Less / SCSS / Sass 等语法差异,工作量倍增。

 

简单举个例子,当我需要匹配 import { toast } from '@vivo/v-jsbridge'  字符串时。针对单双引号、空格、分号等细节处理上需要更仔细,稍有不慎就会忽略一些特殊场景,结果就是匹配失败,造成隐蔽的迁移问题。

import { toast } from '@vivo/v-jsbridge'  // 单引号

import { toast } from "@vivo/v-jsbridge"  // 双引号

import { toast } from "@vivo/v-jsbridge";  // 双引号 + 分号

import {toast} from "@vivo/v-jsbridge";  // 无空格

所以,用简单的正则匹配规则是无法帮助我们完成大规模的代码迁移和重构的,我们需要更好的方法:基于 AST 的代码迁移。

2.2 思路二:基于 AST(抽象语法树)的代码迁移

在了解到正则匹配规则的局限性后,我把目光锁定到了基于 AST 的代码迁移上。

那么什么是基于 AST 的代码迁移呢?

2.2.1 Babel 的编译过程

如果你了解过 Babel 的代码编译原理,应该对 AST 代码迁移不陌生。我们知道 Babel 的编译过程大致分为三个步骤:

  • 解析:将源代码解析为 AST(抽象语法树);

  • 变换:对 AST 进行变换;

  • 再建:根据变换后的 AST 重新构建生成新的代码。

(图片来源:Luminosity Blog

 

举个例子,Babel 将一个 ES6 语法转换为 ES5 语法的过程如下:

(1)输入一个简单的 sayHello 箭头函数方法源码:

const sayHello = () => {
    console.log('hello')
}

 

(2)经过 Babel 解析为 AST(可以看到 AST 是一串由 JSON 描述的语法树),并对 AST 进行规则变换:

  • 将 type 字段由 ArrowFunctionExpression 转换为 FunctionExpression

  • 将 kind 字段由 const 转换为 var

{
  "type": "Program",
  "start": 0,
  "end": 228,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 179,
      "end": 227,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 185,
          "end": 227,
          "id": {
            "type": "Identifier",
            "start": 185,
            "end": 193,
            "name": "sayHello"
          },
          "init": {
-            "type": "ArrowFunctionExpression",
+            "type": "FunctionExpression",
            "start": 196,
            "end": 227,
-            "id": null,
+            "id": {
+               "type": "Identifier",
+               "start": 203,
+               "end": 211,
+               "name": "sayHello"
+            },
            "expression": false,
            "generator": false,
            "async": false,
            "params": [],
            "body": {
              "type": "BlockStatement",
              "start": 202,
              "end": 227,
              "body": [
                {
                  "type": "ExpressionStatement",
                  "start": 205,
                  "end": 225,
                  "expression": {
                    "type": "CallExpression",
                    "start": 205,
                    "end": 225,
                    "callee": {
                      "type": "MemberExpression",
                      "start": 205,
                      "end": 216,
                      "object": {
                        "type": "Identifier",
                        "start": 205,
                        "end": 212,
                        "name": "console"
                      },
                      "property": {
                        "type": "Identifier",
                        "start": 213,
                        "end": 216,
                        "name": "log"
                      },
                      "computed": false,
                      "optional": false
                    },
                    "arguments": [
                      {
                        "type": "Literal",
                        "start": 217,
                        "end": 224,
                        "value": "hello",
                        "raw": "'hello'"
                      }
                    ],
                    "optional": false
                  }
                }
              ]
            }
          }
        }
      ],
-      "kind": "const"
+      "kind": "var"
    }
  ],
  "sourceType": "module"
}

 

(3)从 AST 重新构建为 ES5 语法:

var sayHello = function sayHello() {
   console.log('hello');
 };

这样就完成了一个简单的 ES6 到 ES5 的语法转换。我们的脚手架自动代码迁移思路也是如此。

 

对比正则表达式匹配,基于 AST 代码迁移,有几点好处:

  • 比字符串匹配更灵活、涵盖更多复杂场景。

  • 通常 AST 代码迁移工具都提供了方便的解析、查询、匹配、替换的 API,能轻易写出高效的代码转换规则。

  • 方便统一转换后的代码风格。

2.2.2 代码迁移流程设计

了解了 AST 的基本原理和可行性后,我们需要找到合适的工具库来完成代码的 AST 解析、重构、生成。考虑到项目中至少包含了这几种内容(脚本、样式、HTML):

  • 单独的 JS 文件;

  • 单独的样式文件:CSS / Less / SCSS / Sass;

  • Vue 文件:包含 Template、Script、Style 三部分。

 

我们需要分别找到各类文件内容对应的解析和处理工具。

首先,是 JS 文件的解析处理工具的选择。在市面上比较流行的 JS AST 工具有很多种选择,例如最常见的 Babel、jscodeshift 以及 Esprima、Recast、Acorn、estraverse 等。做了一些简单调研后,发现这些工具都有一些共通的缺陷:

  • 上手难度大,有较大的学习成本,要求开发者充分了解 AST 的语法规范;

  • 语法复杂,代码量大;

  • 代码可读性差,不利于维护。

 

以 jscodeshift 为例,如果我们需要匹配一个简单语句:item.update('price')(this, '50'),它的实现代码如下:

const callExpressions = root.find(j.CallExpression, {
  callee: {
    callee: {
      object: {
        name: 'item'
      },
      property: {
        name: 'update'
      }
    },
    arguments: [{
      value: 'price'
    }]
  },
  arguments: [{
    type: 'ThisExpression'
  }, {
    value: '50'
  }]
})

 

其实相比于原始的 Babel 语法,上述的 jscodeshift 语法已经相对简洁,但可以看出依然有较大的代码量,并且要求开发者熟练掌握 AST 的语法结构。

因此我找到了一个更简洁、更高效的 AST 工具:GoGoCode,它是一款阿里开源的 AST 工具,封装了类似 jQuery 的语法,简单易用。一个直观的对比就是,如果用 GoGoCode 同样实现上述的语句匹配,只需要一行代码:

$(code).find(`item.update('price')(this, '50')`)

它直观的语义以及简洁的代码,让我选择了它作为 JS 的 AST 解析工具。

 

其次,是单独的  CSS 样式文件解析工具选择。这个选择很轻易,直接使用通用的 PostCSS 来解析和处理样式即可。

 

最后,是 Vue 文件的解析工具选择。因为 Vue 文件是由 Template、Script、Style 三部分组成,因此需要更复杂的工具进行组合处理。很庆幸的是 GoGoCode 除了能够对单独的 JS 文件进行解析处理,它还封装了对 Vue 文件中的 Template 和 Script 部分的处理能力,因此 Vue 文件中除了 Style 样式部分,我们也可以交由 GoGo Code 来处理。那 Style 样式的部分该如何处理呢?这里我大致看了官方的 vue-loader 源码,发现源码中使用的是 @vue/component-compiler-utils 来解析 Vue 的 SFC 文件,它可以将文件中的 Style 样式内容单独抽离出来。因此思路很简单,我们利用 @vue/component-compiler-utils 将 Vue 文件中的 Style 样式内容抽离出来,再交由 PostCSS 来处理即可。

 

那么,简单总结下找到的几款适合的工具库:

  • GoGoCode:阿里开源的一款抽象语法树处理工具,可用于解析 JS / HTML / Vue 文件并生成抽象语法树(AST),进行代码的规则替换、重构等。封装了类似 jQuery 的语法,简单易用。

  • PostCSS:大家熟悉的开源 CSS 代码迁移工具,可用于解析 Less / CSS / SCSS / Sass 等样式文件并生成语法树(AST),进行代码的规则替换、重构等。

  • @vue/component-compiler-utils:Vue 的开源工具库,可用于解析 Vue 的 SFC 文件,我用它将 SFC 中的 Style 内容单独抽出,并配合 PostCSS 来处理样式代码的规则替换、重构。

 

有了这三个工具,我们就可以梳理出针对不同文件内容的处理思路:

  • JS 文件:交给 GoGoCode 处理。

  • CSS / Less / SCSS / Sass 文件:交给 PostCSS 处理。

  • Vue 文件:

  • Template / Script 部分:交给 GoGoCode 处理。

  • Style 部分:先用 @vue/component-compiler-utils 解析出 Style 部分,再交给 PostCSS 处理。

 

有了处理思路后,接下来进入正文,深入代码细节,详细了解代码迁移流程。

三、代码迁移流程详解

整个代码迁移流程分为几个步骤,分别是:

3.1 遍历和读取文件内容

遍历项目文件内容,根据文件类型交由不同的 transform 函数来处理:

  • transformVue:处理 Vue 文件

  • transformScript:处理 JS 文件

  • transformStyle:处理 CSS 等样式文件

 

const path = require('path')
const fs = require('fs')

const transformFiles = path => {
    const traverse = path => {
        try {
            fs.readdir(path, (err, files) => {
                files.forEach(file => {
                    const filePath = `${path}/${file}`
                    fs.stat(filePath, async function (err, stats) {
                        if (err) {
                            console.error(chalk.red(`  \n

标签:Vue,const,AST,处理,代码,迁移
From: https://www.cnblogs.com/vivotech/p/18433148

相关文章

  • 揭秘 Git-stash:掌握暂存技巧,让代码更整洁!
    stash可以冻结目前的状态‍在gitstash出现之前当我们在开发一个新功能的时候,突然来了一个紧急的bug要修复,此时我们可以创建一个分支去修复它;但如果,切换会导致冲突的话,就会切换失败。我们来模拟下(先确保工作区是干净的):$gitbranchbug02$echo"test">>3-branch/br......
  • 【数据库】生产问题(数据迁移)
    MySQL亿级数据平滑迁移实战(来自vivo)https://www.cnblogs.com/vivotech/p/18373623 1、方案选型常见的迁移方案大致可以分为以下几类:而预约业务有以下特点:读写场景多,频率高,在用户预约/取消预约/福利发放等场景均涉及到大量的读写。不可接受停机,停机不可避免的会造成经济......
  • Git 分支管理全攻略:一篇博客带你玩转代码分支!
    什么是分支?在Git里,分支其实就有点像一个树的枝杈,每个分支上可以有不同的文件的版本,并且不会互相干扰。​分支功能有什么用?在工作中,我们经常是需要和别人一起开发一个项目的,此时可能你开发A功能,别人开发B功能;如果只有一个分支的话,那么所有人都得在这个分支上干活;如果你开发......
  • 一键去水印小程序源码系统 下载无水印的高清图片 带完整的安装代码包以及搭建部署教程
    系统概述一键去水印小程序源码系统是一款专为图片去水印设计的软件开发包(SDK),它集成了先进的图像处理技术和智能识别算法,能够自动识别并去除图片中的水印,同时保持图片的高清画质不受损。该系统支持多种图片格式,包括但不限于JPEG、PNG、GIF等,广泛适用于电商、设计、教育、自媒体......
  • 家庭医生上门服务小程序源码系统 带完整的安装代码包以及搭建部署教程
    系统概述家庭医生上门服务小程序源码系统是一款专为医疗机构、健康服务平台及有意愿涉足健康服务领域的创业者设计的一站式解决方案。该系统集成了预约挂号、在线问诊、健康档案管理、药品配送、健康资讯推送等多种功能于一体,旨在通过移动互联网技术,打破传统医疗服务的时空限......
  • centos7安装elasticsearch6.3.x集群
    一、环境信息及安装前准备主机角色(内存不要小于1G): 软件及版本(百度网盘链接地址和密码:链接:https://pan.baidu.com/s/17bYc8MRw54GWCQCXR6pKjg提取码:f6w8)  部署前操作:关闭防火墙,关闭selinux(生产环境按需关闭或打开)同步服务器时间,选择公网ntpd服务器或者自建ntpd服务器......
  • 微信支付开发-支付工厂AppApi查账代码
    一、JSAPI支付产品、APP支付产品、小程序支付产品流程图二、工厂父类抽象类代码开发<?php/***微信父类抽象类*User:龙哥·三年风水*Date:2024/9/19*Time:11:33*/namespacePayment\WechatPay;abstractclassWechatPaymentHandle{/***下单......
  • Android代码调试与调优指南
    Android代码调试与调优指南在Android应用开发中,代码调试与调优不仅是确保应用稳定运行、提升用户体验的必要环节,也是开发者专业技能的重要体现。随着Android系统版本的更新和硬件设备的多样化,开发者需要掌握更加全面和深入的调试与优化技术。一、深入理解Android架构与性......
  • matlab划分区域的等高线云图(代码)
    出图结果如下:代码如下,按需修改clear;clc;numRows=100;%数据区域,步长,步数numCols=100;ax=-2;bx=2;ay=-2;by=2;hx=(bx-ax)/numCols;hy=(by-ay)/numRows;XXC=ax:hx:bx;%坐标阵,dataYYC=ay:hy:by;CCC=zeros(numRows+1,numCols+1);hold......
  • ELK中日志数据采集器Filebeat的安装和使用、Filebeat结合Logstash进行日志处理入Elast
    一、ELK中日志数据采集器Filebeat的安装和使用    Beats是数据采集的得力工具,Beats能够将数据转发至Logstash进行转换和解析。Filebeat是Beats中的一种,Filebeat是本地文件的日志数据采集器,可监控日志目录或特定日志文件(tailfile),并将它们转发给Elasticsearch或Logstats......