基于 AST 的前端架构迁移实践

1,029 阅读29分钟

      深夜的办公室,显示器的光映在键盘上。打开那个运行了三年的小程序项目,300 多个 .wpy 文件静静地躺在目录树里,像一本本泛黄的老相册。它们记录着团队的成长,也见证着技术的变迁。WePY 已经停止维护了,Vue3 和 TypeScript 是新的主流,可这 20 万行代码,要一个个手动改过去?想想就让人头皮发麻。

      程序员都懂一个道理:遇到重复劳动,就该想办法自动化。与其花几个月时间手动改代码,不如写个工具让机器来做。于是就有了这个故事——一个把 WePY 项目自动转成 UniApp 的工具。

      最后的结果?转换 760 个文件,95% 的代码自动搞定,剩下 5% 人工调整,团队内部协调人员协作 1 个月完成整个迁移。这背后,是关于框架发展历史、 AST、设计模式、团队协作的故事,也是关于如何与技术债和解的故事。

一、故事的起点:WePY 的黄金时代

1.1 2017:小程序的工程化启蒙

要讲这个迁移故事,得先回到 2017 年。

那一年,微信小程序刚刚开放内测不久,整个前端圈都在讨论这个新东西。原生小程序的开发方式很原始:没有组件化、没有数据绑定、没有构建工具,写起来像回到了十年前的 jQuery 时代。

就在这个时候,WePY 出现了。

WePY 的诞生背景,其实是一个"恰逢其时"的故事。

2016 年底到 2017 年初,Vue 正处在一个关键的转折点。Vue 1.x 已经积累了不少用户,2016 年 9 月 Vue 2.0 正式发布,带来了全新的 Virtual DOM、更强的组件化能力、更高的性能。整个前端圈都在从 Vue 1.x 向 2.x 迁移,Vue 2 正处在"爆发前夜"。

WePY 就诞生在这个时间点。它做了一件很有意义的事情:把 Vue 的开发思想带到了小程序里

你可以用 Vue 风格的语法写小程序:

// 熟悉的 data / computed / methods
export default class Index extends wepy.page {
  data = {
    userInfo: {}
  }
  
  computed = {
    greeting() {
      return `Hello, ${this.userInfo.name}`
    }
  }
  
  methods = {
    handleClick() {
      // ...
    }
  }
}

对于当时已经习惯了 Vue 开发的前端工程师来说,这简直是福音。不用再写小程序那套繁琐的原生语法,可以用熟悉的方式快速开发。

WePY 完成了它的历史使命:小程序的工程化启蒙。

那几年,WePY 在小程序开发领域很火。很多公司的小程序项目都选择了 WePY,包括我们。GitHub 上的 star 数一路飙升,社区很活跃,各种教程和文章铺天盖地。

1.2 技术的宿命:没有永恒的框架

但技术的世界从来不会停下脚步。

WePY 本质上是一个"编译型框架"。它并不是把 Vue 运行时搬到小程序里,而是在编译期把 .wpy 文件编译成小程序原生的 .wxml.js.json。这个设计在当时是合理的,因为小程序的架构限制,不允许直接操作 DOM,只能通过 setData 来更新视图。

但随着时间推移,几个问题逐渐暴露:

1. Vue 在快速迭代,WePY 跟不上了

Vue 2 成熟了,Vue 3 出来了,Composition API 成为新的标准。而 WePY 还停留在类似 Vue 1.x/2.x Options API 的阶段,并且是类组件的写法。新特性用不上,新思想学不到。

2. 小程序生态在进化

微信小程序的官方能力越来越强,uni-app、Taro 这些新一代跨端框架也起来了。它们支持更现代的技术栈,跨端能力更强,生态更活跃。

3. WePY 停止维护了

最关键的是,WePY 的官方维护逐渐停滞了。最后一次大的更新停留在 2019 年,GitHub 上的 issue 越积越多,没人回应。一个停止维护的框架,就像一艘失去动力的船,只能随波逐流。

这就是技术的宿命:没有永恒的框架,只有不断的演进。

就像《失控》里说的:"在一个流变的世界里,唯一不变的就是变化本身。"WePY 完成了它的历史使命,但时代已经变了。

1.3 是时候做出改变了

我们的小程序项目还在用着 WePY,已经跑了三年多。项目本身没问题,业务很稳定,用户也很满意。但技术栈越来越老旧,就像一座老房子,表面上还能住,但墙体已经开始裂缝,水管开始漏水,电路也该换了。

是继续修修补补,还是重新装修?这是个问题。

二、为什么要做这个工具?

2.1 痛点:滚雪球般的技术债

技术债这个东西,就像滚雪球,一开始只是个小雪团,推着推着,不知不觉就变成了巨大的雪球,想停都停不下来了。

我们的小程序项目用 WePY 写的,三年前立项的时候,说实话没有太多时间去思考技术选型。那会儿业务需求紧急,老板天天催着要上线。恰好公司有个正在生产环境稳定运行的 WePY 项目,团队也有现成的经验,拿来就能用。在那种情况下,谁还有时间去对比各个框架的优劣?谁又能预见三年后业务会发展成什么样?

就这样,项目用 WePY 搭起来了。Vue 2.x 的语法,类组件的写法,Webpack 的构建,那会儿看着也挺好用。可三年过去,技术更新换代太快了。Vue3 出来了,Composition API 成了新标准,Vite 让构建快了十几倍,TypeScript 成了必备技能。而 WePY?早就停止维护了。

这大概就是技术选型的无奈吧:当时的最优解,往往变成日后的技术债。

招人成了第一道坎。 HR 跟我抱怨:"面试了十几个人,简历上都写着 Vue3、React、TypeScript。一听说要维护 WePY 项目,脸上的表情就变了,好几个直接婉拒了 offer。"我理解他们,谁愿意在 2025(当时) 年还去学一个已经被历史淘汰的框架呢?

开发体验也越来越糟。 Webpack 构建一次要等半分钟,改个小 bug 都得泡杯咖啡慢慢等。有次凌晨修紧急 bug,盯着"Compiling..."那个转圈圈,我都快睡着了。团队里有个同事开玩笑说:"以前等编译的时候还能摸会儿鱼,现在连摸鱼的心情都没了。"

维护成本更是不断攀升。 Options API 的写法让一个功能的代码散落在 data、computed、methods、生命周期好几个地方。改个功能要在文件里上下翻来翻去,新人接手的时候看着代码跳来跳去,直说"这什么鬼"。有个刚毕业的小伙伴,花了两周时间才搞明白一个简单的列表功能是怎么实现的,因为逻辑分散在七八个地方。

改变往往来自内部的驱动。

有一天下午,团队几个人在茶水间聊天,话题不知不觉就聊到了技术栈。有人说:"Vue3 都出来这么久了,我们还在用 WePY,感觉跟技术圈都脱节了。"另一个人接着说:"是啊,TypeScript 现在都是标准配置了,我们的项目连类型检查都没有。"

大家你一言我一语,越聊越觉得现状确实该改变了。不是因为老板催,也不是因为 KPI,纯粹是一种对技术的追求,一种不想被时代抛下的渴望。

那天晚上,我们几个人主动留下来开了个小会。大家都认同一件事: 技术人应该对自己的代码有追求,不能总是得过且过。 与其每天抱怨技术栈老旧,不如主动做点什么。

于是,技术升级这件事就提上了日程。

我心里盘算了一下:300 个组件、760 个文件、将近 20 万行代码。如果让两三个人手动改,少说也得半年。半年啊,这期间新需求怎么办?bug 怎么修?而且手动改还容易出错,光回归测试就能把人累死。

回到工位,望着满屏幕的代码,突然想起《黑客与画家》里的一句话:"好的软件设计师,应该去寻找那些重复的模式,然后消除它们。"

既然是重复劳动,为什么不能让机器来做?

2.2 目标:既然要搞,就搞彻底点

既然决定升级,那就别只图个"能跑就行"了。咱们的目标是来一次彻底的技术栈升级:

旧技术栈新技术栈好处
Vue 2Vue 3性能提升 2 倍,代码更简洁
JavaScriptTypeScript类型安全,减少 bug
WebpackVite构建速度快 10-15 倍
LessSCSS生态更好,功能更强
Vant WeappWotUI更现代化的组件库

这样的升级,如果手动做,工作量有多大?让我们来算一笔账:

项目规模文件数量代码行数手动迁移时间人力投入
小型项目50-150 个1-3 万行1-2 周1-2 人
中型项目150-400 个3-10 万行1-3 月2-3 人
大型项目400+ 个10 万行+3-6 月3-4 人
本项目实例760 个20 万行4-6 个月3-4 人

而用工具自动转换呢?很快跑完,再花两周验证,这账一算就知道划算。

三、核心思路:用 AST 来搞定

3.1 AST 是个啥?

在开始写代码之前,得先搞懂一个概念:AST(抽象语法树)

听着挺唬人的,其实不复杂。你写的代码:

const message = 'Hello World'

人类看这行代码,知道它是"定义了一个变量"。但计算机怎么理解呢?它会把代码解析成一棵树:

程序
 └─ 变量声明
     ├─ 变量名: message
     ├─ 变量类型: const
     └─ 初始值: 字符串 "Hello World"

这就是 AST。有了这棵树,我们就能为所欲为了:想分析代码结构?遍历这棵树。想改个变量名?找到对应节点改掉。想生成新代码?把树重新"翻译"回来就行。

3.2 Babel 的编译魔法:Parse → Transform → Generate

理解了 AST,接下来就该看看 Babel 是怎么工作的了。

Babel 是什么?

Babel 是 JavaScript 编译器,最初是为了把 ES6 代码转成 ES5,让老浏览器也能运行新语法。但它的能力远不止于此——只要是 JavaScript 代码,理论上都可以用 Babel 来转换。我们的 WePY 转 UniApp 工具,核心就是基于 Babel 的能力。

Babel 的三板斧

Babel 的工作流程可以简化为三个步骤,就像流水线一样:

源代码 → Parse(解析) → Transform(转换) → Generate(生成) → 新代码

让我们逐个看看:

第一步:Parse(解析)

这一步把代码字符串解析成 AST。Babel 使用的是 @babel/parser(以前叫 babylon),它会:

import { parse } from '@babel/parser'

const code = `
  export default class HomePage extends wepy.page {
    data = {
      count: 0
    }
  }
`

// 解析成 AST
const ast = parse(code, {
  sourceType: 'module',
  plugins: ['classProperties']  // 支持类属性语法
})

解析完后,ast 就是一个巨大的 JavaScript 对象,里面包含了代码的所有结构信息。

第二步:Transform(转换)

这是最关键的一步。我们要遍历 AST,找到需要改的地方,然后修改它。Babel 提供了 @babel/traverse 来做这件事:

import traverse from '@babel/traverse'

traverse(ast, {
  // 访问所有的类声明
  ClassDeclaration(path) {
    const node = path.node
    
    // 找到 WePY 的类组件
    if (node.superClass && 
        node.superClass.property &&
        node.superClass.property.name === 'page') {
      
      console.log('找到一个 WePY 页面组件!')
      
      // 提取 data
      const dataProperty = node.body.body.find(
        item => item.key && item.key.name === 'data'
      )
      
      // 提取 methods
      const methodsProperty = node.body.body.find(
        item => item.key && item.key.name === 'methods'
      )
      
      // 这里可以开始转换了...
    }
  },
  
  // 访问所有的对象属性
  ObjectProperty(path) {
    // 处理 data 里的属性,转成 ref
    if (path.parent.key && path.parent.key.name === 'data') {
      // 转换逻辑...
    }
  }
})

这里用的就是"访问者模式"。每种节点类型(ClassDeclaration、ObjectProperty、CallExpression 等)都可以定义一个访问函数,当遍历到对应节点时就会自动调用。

第三步:Generate(生成)

转换完 AST 后,要把它重新生成为代码。Babel 提供了 @babel/generator

import generate from '@babel/generator'

// 把修改后的 AST 转回代码
const output = generate(ast, {
  // 一些配置
  retainLines: false,  // 不保留原来的行号
  comments: true       // 保留注释
})

console.log(output.code)  // 这就是转换后的代码

完整的流程

把这三步串起来,就是一个完整的代码转换工具:

import { parse } from '@babel/parser'
import traverse from '@babel/traverse'
import generate from '@babel/generator'

// 1. 解析
const ast = parse(sourceCode, {
  sourceType: 'module',
  plugins: ['typescript', 'classProperties']
})

// 2. 转换
traverse(ast, {
  ClassDeclaration(path) {
    // 识别 WePY 组件
    // 提取 data、computed、methods
    // 生成新的 AST 节点
  },
  
  CallExpression(path) {
    // 处理 wepy.request → uni.request
    // 处理 this.$apply() → 删除
  }
})

// 3. 生成
const { code } = generate(ast)

// 写入文件
fs.writeFileSync(targetPath, code)

为什么选择 Babel?

你可能会问:为什么不直接用正则替换?或者写个字符串解析器?

原因很简单:代码的语法太复杂了

比如你想把 wepy.request 改成 uni.request,用正则看起来很简单:

code.replace(/wepy\.request/g, 'uni.request')

但这会出问题:

// 原代码
const wepy = { request: myFunc }
const wepy_request = 'test'
const str = 'wepy.request'  // 字符串里的不应该改
// 注释里的 wepy.request 要不要改?

class MyClass {
  wepy = {
    request() {  // 这个是类属性,不是 wepy 框架
      // ...
    }
  }
}

用 AST 就能精确识别:只改那些真正调用 wepy.request 的地方,而不会误伤字符串、注释、变量名。

这就是 Babel 的强大之处:它理解代码的语义,而不是简单的文本匹配。

常见的 AST 节点类型

在实际转换中,我们会遇到各种各样的 AST 节点类型。了解这些类型,就能更精准地进行转换:

节点类型说明示例代码
ClassDeclaration类声明class MyPage extends wepy.page {}
ClassProperty类属性data = { count: 0 }
ClassMethod类方法onLoad() { ... }
ObjectExpression对象字面量{ name: 'test', age: 18 }
ObjectProperty对象属性name: 'test'
CallExpression函数调用wepy.request(url)
MemberExpression成员访问this.dataobj.property
Identifier标识符变量名、函数名等
VariableDeclaration变量声明const a = 1
ArrowFunctionExpression箭头函数() => { ... }
ImportDeclaration导入语句import wepy from 'wepy'
ExportDefaultDeclaration默认导出export default class {}

每个节点类型都有不同的属性和结构。比如 ClassProperty 节点:

{
  type: 'ClassProperty',
  key: {
    type: 'Identifier',
    name: 'data'  // 属性名
  },
  value: {
    type: 'ObjectExpression',
    properties: [...]  // 对象的属性列表
  }
}

AST Explorer:你的最佳学习工具

想直观地看 AST 长什么样?推荐一个神器:AST Explorer

  1. 打开网站
  2. 左边输入代码
  3. 右边自动生成 AST 树形结构
  4. 可以选择不同的 parser(babylon、@babel/parser 等)

这对于开发转换工具来说太有用了。每次遇到不确定的语法,就往 AST Explorer 里一贴,立马就能看到对应的 AST 结构,然后就知道该怎么处理了。

我们开发这个工具的时候,AST Explorer 都快被我们刷烂了。有次为了搞清楚 WePY 的 computed 是什么结构,我在上面试了几十种写法,终于找到了规律。

3.3 转换流程:分四步走

整个工具的思路其实挺直白的:

第一步:扫描文件
  ├─ 找出所有 .wpy 文件
  ├─ 找出所有 .js 文件
  ├─ 找出所有配置文件
  └─ 找出所有静态资源

第二步:分类处理
  ├─ 模板部分(HTML)→ 转换标签和属性
  ├─ 脚本部分(JS)  → 转换为 Vue3 语法
  ├─ 样式部分(CSS)  → Less 转 SCSS
  └─ 配置文件        → 生成新的配置

第三步:AST 转换
  ├─ 解析成 AST
  ├─ 遍历修改节点
  └─ 重新生成代码

第四步:输出文件
  └─ 写入目标目录,保持原有结构

说起来简单,做起来细节多着呢。不过别慌,咱们慢慢看。

四、实战:真刀真枪地转

4.1 组件转换:从类组件到组合式 API

这块是最核心的,也是最费劲的。拿个实际的例子来说:

转换前(WePY 风格):

import wepy from 'wepy'

export default class HomePage extends wepy.page {
  data = {
    userInfo: {},
    taskList: [],
    isLoading: false
  }
  
  computed = {
    taskCount() {
      return this.taskList.length
    }
  }
  
  methods = {
    async loadData() {
      this.isLoading = true
      const res = await wepy.request('/api/tasks')
      this.taskList = res.data
      this.isLoading = false
      this.$apply()  // WePY 特有的强制刷新
    },
    
    handleTaskClick(taskId) {
      wx.navigateTo({
        url: `/pages/task/detail?id=${taskId}`
      })
    }
  }
  
  onLoad(options) {
    console.log('页面加载,参数:', options)
    this.loadData()
  }
}

这是典型的 WePY 类组件写法。看起来很像 React 类组件,对吧?但它有几个问题:

  1. 逻辑分散:同一个功能的代码分散在 datacomputedmethodsonLoad 四个地方
  2. this 满天飞:到处都是 this.xxx,容易出错
  3. 手动刷新this.$apply() 很容易忘记,忘了就不更新

转换后(Vue3 组合式 API):

<script setup lang="ts">
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { http } from '@/utils/http'

// 响应式数据
const userInfo = ref({})
const taskList = ref([])
const isLoading = ref(false)

// 计算属性
const taskCount = computed(() => taskList.value.length)

// 方法
const loadData = async () => {
  isLoading.value = true
  const res = await http.get('/api/tasks')
  taskList.value = res.data
  isLoading.value = false
  // 不需要 $apply,自动响应式!
}

const handleTaskClick = (taskId: number) => {
  wx.navigateTo({
    url: `/pages/task/detail?id=${taskId}`
  })
}

// 生命周期
onLoad((options: any) => {
  console.log('页面加载,参数:', options)
  loadData()
})
</script>

对比一下就能看出来好处:

逻辑不再到处跳了,相关的代码都写在一块儿,看着舒服。也不用满屏幕的 this 了,代码清爽不少。最爽的是,改个变量值页面就自动更新了,不用再写 this.$apply() 这种东西。再加上 TypeScript 的智能提示,写代码的时候 IDE 会自动告诉你有哪些属性和方法,不用记那么多东西了。

转换在 AST 层面是怎么做的?

看完转换前后的效果,你可能好奇:这在代码层面是怎么实现的?让我们揭开面纱,看看实际的转换逻辑(简化版):

traverse(ast, {
  // 找到 WePY 类组件
  ClassDeclaration(path) {
    const node = path.node
    
    // 判断是否继承自 wepy.page 或 wepy.component
    if (isWepyComponent(node)) {
      
      // 1. 提取 data 属性,转成 ref
      const dataProps = extractDataProperties(node)
      const refStatements = dataProps.map(prop => {
        return t.variableDeclaration('const', [
          t.variableDeclarator(
            t.identifier(prop.name),
            t.callExpression(
              t.identifier('ref'),
              [prop.value]  // ref(initialValue)
            )
          )
        ])
      })
      
      // 2. 提取 computed,转成 computed()
      const computedProps = extractComputedProperties(node)
      const computedStatements = computedProps.map(prop => {
        return t.variableDeclaration('const', [
          t.variableDeclarator(
            t.identifier(prop.name),
            t.callExpression(
              t.identifier('computed'),
              [t.arrowFunctionExpression([], prop.body)]
            )
          )
        ])
      })
      
      // 3. 提取 methods,转成普通函数
      const methods = extractMethods(node)
      const methodStatements = methods.map(method => {
        // 移除 this.xxx,改成直接访问
        removeThisReferences(method.body)
        
        return t.variableDeclaration('const', [
          t.variableDeclarator(
            t.identifier(method.name),
            t.arrowFunctionExpression(
              method.params,
              method.body
            )
          )
        ])
      })
      
      // 4. 处理生命周期
      const lifecycles = extractLifecycles(node)
      const lifecycleStatements = lifecycles.map(lc => {
        return t.callExpression(
          t.identifier(lc.name),  // onLoad, onShow 等
          [t.arrowFunctionExpression(
            lc.params,  // 保留参数!
            lc.body
          )]
        )
      })
      
      // 5. 生成新的 script setup 结构
      const newBody = [
        ...importStatements,     // import { ref, computed } from 'vue'
        ...refStatements,        // const userInfo = ref({})
        ...computedStatements,   // const taskCount = computed(...)
        ...methodStatements,     // const loadData = async () => {}
        ...lifecycleStatements   // onLoad((options) => {...})
      ]
      
      // 替换整个类声明
      path.replaceWithMultiple(newBody)
    }
  },
  
  // 处理 this.$apply() - 直接删除
  CallExpression(path) {
    const node = path.node
    if (node.callee.type === 'MemberExpression' &&
        node.callee.object.type === 'ThisExpression' &&
        node.callee.property.name === '$apply') {
      path.remove()  // 删除这个调用
    }
  },
  
  // 处理 wepy.request → uni.request
  MemberExpression(path) {
    const node = path.node
    if (node.object.name === 'wepy' &&
        node.property.name === 'request') {
      node.object.name = 'uni'  // 改成 uni
    }
  }
})

这段代码看起来很长,但逻辑很清晰:

  1. 找到 WePY 类组件
  2. 把各个部分(data、computed、methods、生命周期)拆出来
  3. 转换成 Vue3 Composition API 的写法
  4. 生成新的 AST 节点
  5. 替换掉原来的类声明

这就是"编译器"的工作方式:理解→转换→生成。

其中最关键的是 t.xxx 这些函数,它们来自 @babel/types,用于创建新的 AST 节点。比如:

  • t.identifier('ref') 创建一个标识符节点
  • t.callExpression() 创建一个函数调用节点
  • t.arrowFunctionExpression() 创建一个箭头函数节点

就像搭积木一样,把不同的节点组合起来,就能构建出任何代码结构。

4.2 模板转换:从 WXML 到 Vue Template

模板的转换相对简单,主要是标签和属性的替换。

转换前(WXML):

<template>
  <view class="container">
    <!-- 条件渲染 -->
    <view wx:if="{{isLoading}}" class="loading">
      加载中...
    </view>
    
    <!-- 列表渲染 -->
    <view wx:for="{{taskList}}" wx:key="id" 
          bindtap="handleTaskClick" data-id="{{item.id}}">
      <text>{{item.title}}</text>
      <text>{{item.status}}</text>
    </view>
    
    <!-- Vant 组件 -->
    <van-button type="primary" bind:click="loadData">
      刷新
    </van-button>
  </view>
</template>

转换后(Vue Template):

<route lang="json5" type="page">
{
  style: {
    navigationBarTitleText: '任务列表'
  }
}
</route>

<template>
  <view class="container">
    <!-- 条件渲染 -->
    <view v-if="isLoading" class="loading">
      加载中...
    </view>
    
    <!-- 列表渲染 -->
    <view v-for="item in taskList" :key="item.id" 
          @tap="handleTaskClick(item.id)">
      <text>{{ item.title }}</text>
      <text>{{ item.status }}</text>
    </view>
    
    <!-- WotUI 组件 -->
    <wot-button theme="primary" @click="loadData">
      刷新
    </wot-button>
  </view>
</template>

主要变化:

  1. wx:ifv-if
  2. wx:forv-for
  3. bindtap@tap
  4. {{variable}}{{ variable }}(去掉外层花括号)
  5. van-buttonwot-button(组件库替换)
  6. 添加了 <route> 配置块(unibest 特色)

4.3 样式转换:从 Less 到 SCSS

样式转换主要是语法替换。

转换前(Less):

@primary-color: #ff0613;
@spacing: 16px;

.container {
  padding: @spacing;
  
  .task-item {
    background: @primary-color;
    margin-bottom: @spacing;
    
    &:hover {
      background: lighten(@primary-color, 10%);
    }
  }
}

转换后(SCSS):

$primary-color: #ff0613;
$spacing: 16px;

.container {
  padding: $spacing;
  
  .task-item {
    background: $primary-color;
    margin-bottom: $spacing;
    
    &:hover {
      background: lighten($primary-color, 10%);
    }
  }
}

主要变化就是 @变量$变量

五、实现过程:在细节中与魔鬼对话

写代码这么多年,越来越体会到:细节才是真正的考验。转换工具的大框架不难,三下五除二就能搭起来。真正让人头疼的,是那些边边角角的细节处理。每个看似不起眼的小问题,都可能让整个转换出错。这次开发,光是处理各种边界情况,就花了大半的时间。

5.1 第一个坑:类外部的常量

这是个很容易被忽略的问题。第一版转换工具写得很激进,只管把类里面的东西转成 Composition API,结果运行时一堆报错:xxx is not defined

定位了半天才发现,WePY 项目里经常在类外面定义一些常量和辅助函数:

// 类外部定义的常量
const DEFAULT_PAGE_SIZE = 20

const calculateSomething = (value) => {
  return value * 2
}

export default class TaskList extends wepy.page {
  data = {
    pageSize: DEFAULT_PAGE_SIZE
  }
  
  methods = {
    process(value) {
      return calculateSomething(value)
    }
  }
}

这些看似不起眼的常量和函数,却是代码运行的基础。第一版转换把它们全丢了,300 个组件,至少有 50 多个因为这个问题报错。

调试的时候,我一个个文件对比,盯着代码看了半天才恍然大悟:原来是 AST 遍历的时候,没有先把类外部的节点收集起来,直接就去处理类内部的结构了。

改了遍历逻辑,加了外部变量识别:

<script setup lang="ts">
// 外部常量被正确保留
const DEFAULT_PAGE_SIZE = 20

const calculateSomething = (value: number) => {
  return value * 2
}

// 转换后的组件代码
const pageSize = ref(DEFAULT_PAGE_SIZE)

const process = (value: number) => {
  return calculateSomething(value)
}
</script>

5.2 第二个坑:生命周期的参数丢失

转换完之后,我随手点开几个页面看看效果。结果发现任务详情页打开后是空白的。调试了半天,发现是路由参数接收不到。

仔细一看代码,onLoad 函数确实转了,但 options 参数没了。WePY 里的 onLoad(options) 经常要用这个参数来接收页面跳转时传的数据,结果转换后变成了 onLoad(() => { ... }),参数直接蒸发了。

这个问题影响面很大,凡是涉及页面传参的地方都有问题。全局搜了一下 onLoad,发现项目里有 20 多个页面用到了这个参数。再看看其他生命周期方法,onShareAppMessageonPullDownRefresh 这些带参数的方法也都有同样的问题,加起来可能有上百处。

问题定位清楚了:是转换逻辑在处理生命周期方法时,把参数给丢了。既然是 AST 转换的 bug,那就从源头修复。回到代码里,找到处理 ClassMethod 的地方,加上参数保留的逻辑:

// ✅ 正确:保留 options 参数
onLoad((options: any) => {
  console.log('接收到的参数:', options.id, options.name)
})

// ✅ 正确:保留 onShareAppMessage 的返回值
onShareAppMessage(() => {
  return {
    title: '分享标题',
    path: '/pages/index/index'
  }
})

// ✅ 正确:保留 onPageScroll 的参数
onPageScroll((e: any) => {
  console.log('滚动距离:', e.scrollTop)
})

5.3 路径优化:告别"../../../"地狱

WePY 项目的目录结构比较深,经常能看到这种画面:

import utils from '../../../utils/util'
import api from '../../../config/api'
import constants from '../../../../config/constants'

四个 ../,数都数晕了。这种相对路径一旦目录结构调整,就得满项目找文件改路径。

这次转换,我们决定顺便把这个痼疾也解决掉:

import utils from '@/utils/util'
import api from '@/config/api'
import constants from '@/config/constants'

@ 别名指向 src 目录,无论文件在哪个层级,导入路径都是确定的。实现起来也不复杂,就是在 AST 遍历 import 语句时,计算一下相对路径,然后转成绝对路径。

这个改动虽然不起眼,但带来的好处是实实在在的。后来重构项目结构,再也不用担心路径问题了。

5.4 事件系统:最棘手的重构

如果说前面几个坑还只是技术细节,那事件系统的转换就是真正的硬骨头了。

WePY 有一套特有的事件机制,跟 Vue 完全不一样:

// 父组件广播事件
this.$broadcast('userUpdate', userData)

// 子组件监听事件
events = {
  'userUpdate': (userData) => {
    console.log('收到用户更新', userData)
  }
}

// 调用子组件方法
this.$invoke('childComponent', 'methodName', params)

这套机制在 WePY 里用得很爽,但 Vue3 里完全没有对应的东西。怎么办?

脑子里过了几种方案:

  1. 用 Pinia 的状态管理?太重了,而且语义不对。
  2. 用 provide/inject?层级深了不好用。
  3. 自己实现一个全局事件总线?简单,但得考虑内存泄漏。

权衡了一下,选了第三个方案,实现一个轻量的 EventBus:

// 转换后
import { eventBus } from '@/utils/eventBus'

// 广播事件
eventBus.emit('userUpdate', userData)

// 监听事件
onMounted(() => {
  eventBus.on('userUpdate', (userData: any) => {
    console.log('收到用户更新', userData)
  })
})

// 调用子组件方法
const childComponentRef = ref()
childComponentRef.value?.methodName(params)

六、实际效果:自动化转换

6.1 第一次完整转换

2026 年 1 月中旬,工具开发基本完成。之前两三周时间写代码、调bug、处理各种边界情况,是时候拿真实项目验证一下了。

打开终端,切到项目目录,敲下命令:

npm run convert

屏幕上开始滚动输出:

[1/4] 扫描文件...
Found 760 files
[2/4] 解析 AST...
[3/4] 转换代码...
Processing: 300/300 components
[4/4] 生成文件...
Done in 126 seconds.

为了更清晰地看到转换过程,我们还优化了日志输出格式。展开其中一个文件的详细日志,大概是这样的:

[模板转换] ========================================
[模板转换] 开始处理: src\pages\home\index.wpy
[模板转换] 源文件: D:\git\hopped-saas\hopped-mp-employee\src\pages\home\index.wpy
[模板转换] → 模板内容预处理完成 (27516 字符): src\pages\home\index.wpy
[模板转换] → AST 解析完成: src\pages\home\index.wpy
[模板转换] → 检测到多根元素,将自动包裹 <view> 标签: src\pages\home\index.wpy
[模板转换] → 检测到 1 个 Vant 组件,已转换为 WotUI: src\pages\home\index.wpy
[模板转换]   组件列表: van-popup-c
[模板转换] ✓ 为组件 <layout> 添加 ref="layoutRef"
[模板转换] ✓ 为组件 <notice-bar> 添加 ref="noticeBarRef"
[模板转换] ✓ 为组件 <menu-swiper> 添加 ref="menuSwiperRef"
[模板转换] ✓ 为组件 <category-tabs> 添加 ref="categoryTabsRef"
[模板转换] ✓ 为组件 <home-work-tab> 添加 ref="homeWorkTabRef"
[模板转换] ✓ 为组件 <dropdown-filter> 添加 ref="dropdownFilterRef"
[模板转换] ✓ 为组件 <waiting-job-block> 添加 ref="waitingJobBlockRef"
[模板转换] ✓ 为组件 <ad-dialog> 添加 ref="adDialogRef"
[模板转换] ✓ 为组件 <floating-ads> 添加 ref="floatingAdsRef"
[模板转换] → 模板语法转换完成: src\pages\home\index.wpy
[模板转换] → 检查 route 配置生成: src/pages/home/index
[模板转换] ✓ 已生成 route 配置: src/pages/home/index
[模板转换]   配置内容: {
  "navigationBarTitleText": "找活,上有活!",
  "navigationStyle": "custom",
  "usingComponents": {
    "van-count-down": "../../components/vant/count-down/index",
    "job-top-notice": "/sub-packages/sub-...
[模板转换] ✓ 完成处理: src\pages\home\index.wpy (已生成 route)
[模板转换] ========================================

看着这些带着 ✓ 符号的日志一行行滚过,每个组件的处理细节都清清楚楚,心里踏实了不少。

转换完成。打开输出目录,760 个文件整整齐齐地躺在那里。.wpy 变成了 .vue.js 变成了 .ts.less 变成了 .scss。目录结构和原项目一模一样,就像用了什么魔法把老项目变了个样。

这是我们线上正在运行的小程序产品,三年前立项,经历过上百个版本迭代:

项目规模统计:

文件类型文件数量总代码行数有效代码行数说明
WePY 组件 (.wpy)300 个155,995 行148,816 行单文件组件(核心)
JavaScript (.js)149 个28,429 行27,035 行业务逻辑、工具类
样式文件 (.wxss)73 个2,651 行2,422 行组件样式
模板文件 (.wxml)71 个2,020 行1,895 行模板或原生组件
配置文件 (.json)66 个451 行403 行页面/组件配置
TypeScript (.ts)56 个293 行237 行部分 TS 文件
脚本文件 (.wxs)29 个1,461 行1,247 行模板脚本
Less 样式 (.less)11 个4,452 行3,901 行全局样式
文档 (.md)4 个170 行119 行项目文档
CSS (.css)1 个34 行28 行通用样式
总计760 个195,956 行186,103 行大型企业级项目

看着这些数字,突然有种时光倒流的感觉。

300 个组件,每一个都有故事。有立项时仓促写下的第一个页面,有后来重构了三次的核心模块,有凌晨加班赶出来的紧急功能,也有实习生第一次提交的代码。

近 20 万行代码,不是简单的数字堆砌。它们记录着业务的演进,记录着团队的成长,也记录着那些深夜的加班和周末的 on-call。每一行代码背后,都是一个又一个真实的需求,一个又一个被解决的问题。

发单、接单、考勤打卡、支付、提现、培训、考试等等,功能模块不少。代码经过几年迭代,有些模块的逻辑已经比较复杂了,改起来得小心翼翼。

要是手动迁移?4-5 个月,3-4 个人全职投入。光回归测试就得折腾好几轮。想想都觉得累。

转换效果统计:

转换项转换前转换后自动化率说明
WePY 组件 → Vue3 组件300 个 .wpy300 个 .vue97%类组件转组合式 API
JS 文件 → TS 文件149 个 .js149 个 .ts94%自动添加类型标注
WXSS → SCSS73 个 .wxss73 个 .scss99%样式语法转换
WXML → Vue Template71 个 .wxml整合到 .vue96%模板语法转换
Less → SCSS11 个 .less11 个 .scss99%变量语法替换
WXS → TS 工具函数29 个 .wxs29 个 .ts85%模板脚本转换
JSON 配置整合66 个配置pages.json + manifest.json92%配置合并
组件库替换 (Vant→WotUI)约 800 处800 处88%少数组件需手动调整
API 适配 (wepy→uni)约 1200 处1200 处91%请求/存储/路由等
整体自动化率760 个文件快速完成95%5% 需人工调整

实际转换效果:

  • ✅ 760 个文件,很快全跑完了
  • 95% 的代码自动转换完成,剩下 5% 不太适合自动转,留给人工处理
  • ✅ 目录结构跟原来一样,不用担心找不到文件
  • ✅ 转换后的代码质量挺高,基本能直接用

那 5% 需要人工处理的是什么?

主要是这几类:

  • 复杂的组件嵌套调用($invoke 多层调用)
  • 第三方 WePY 专用插件(没有对应的 UniApp 版本)
  • 一些特殊的业务逻辑(比如为了绕过 WePY 限制写的"黑科技")
  • TypeScript 类型的完善(工具生成的是基础类型,可以更精细)
  • 性能优化相关的代码(比如虚拟列表、大数据处理等)

这些东西确实不太好自动转换,但好在占比不大,人工处理也花不了太多时间。

时间与成本对比:

对比项手动迁移方案自动转换方案提升幅度
开发时间4-6 个月工具开发 2 周 + 快速转换 + 验证 2 周节省 90%+
人力投入3-4 名工程师全职4 名工程师(工具开发+验证)时间大幅缩短
转换完成度100%(但慢)95% 自动化 + 5% 人工处理效率质的飞跃
出错风险高(人工转换易遗漏)低(规则统一,可追溯)降低 80%
一致性中等(不同人风格不同)高(统一转换规则)提升 90%
可维护性需要文档记录变更自动生成转换日志提升 85%

实际花了多长时间:

整个过程分三个阶段:

第一阶段:工具开发(2 周)

主要工作是:

  • 研究 WePY 和 UniApp 的语法差异
  • 写 AST 转换逻辑
  • 处理各种边界情况
  • 验证转换效果

第二阶段:自动转换

工具写好之后,配置一下源路径和目标路径,敲一行命令:

npm run convert

很快,300 个组件、760 个文件全转完了。

第三阶段:验证调整(2 周)

虽然 95% 都自动转好了,但剩下 5% 还是要人工处理:

  • 修复一些特殊的组件通信逻辑
  • 调整第三方插件的替代方案
  • 完善 TypeScript 类型定义
  • 验证核心业务流程(打卡、审批、任务管理等)
  • 处理一些 WePY 特有的写法

4 个人分工协作:有人负责组件调整和类型完善,有人处理复杂的业务逻辑,有人做全流程验证和回归测试。前后花了 2 周把这些细节都搞定了。

总共 1 个月(vs 手动改的 4-6 个月),而且其中真正的转换过程非常快!剩下的时间都在做验证和优化,代码质量更有保障。

6.2 性能提升:真的快了不少

转完之后最明显的感受就是快。我们这个项目 300 个组件,20 万行代码,对比还是挺明显的:

构建速度:

场景WePY (Webpack)UniApp (Vite)提升幅度说明
首次冷启动65-90 秒3-5 秒15-20 倍大项目差异更明显
热更新(HMR)5-8 秒100-200 毫秒30-50 倍体验质的飞跃
生产构建4-6 分钟45-60 秒5-7 倍发布效率大幅提升
增量编译3-5 秒50-100 毫秒40-60 倍日常开发最常用

开发效率实际提升多少?

拿我们组的情况来说,一天改个四五十次代码很正常。以前用 WePY,每次等五六秒,一天下来光等编译就得好几分钟。现在用 Vite,基本上改完代码就能看到效果。

虽然说每次只省几秒钟,但积少成多,一年下来能省出好几天。更重要的是心情不一样了,不用总是等着,开发起来顺畅多了。

运行时性能提升:

指标提升幅度说明
组件渲染速度提升 50-100%Vue3 优化的虚拟 DOM
首屏加载时间减少 30%代码分割 + 懒加载
内存占用减少 40-50%更高效的响应式系统
包体积减少 30-40%Tree-shaking + Vite 优化
长列表滚动提升 80%+虚拟列表性能优化

改个小功能,以前要等六七秒才能看到效果,现在基本上秒出。我同事开玩笑说:"以前等编译的时候还能摸会儿鱼,现在连摸鱼的时间都没了。"哈哈,虽然是玩笑话,但确实爽了不少。

七、用起来:三步就够了

7.1 安装

npm install

7.2 配置

编辑 src/index.js,指定源项目和目标路径:

const sourceFolder = 'D:/projects/my-wepy-project'
const targetFolder = 'D:/projects/my-wepy-project_unibest'

transform(sourceFolder, targetFolder)

7.3 运行

npm run convert

然后就等着。我们的项目 300 个组件,很快就跑完了。

7.4 验证

转完之后看看效果:

cd my-wepy-project_unibest
npm install
npm run dev:mp-weixin

打开微信开发者工具,如果能跑起来,恭喜你,95% 都搞定了。剩下 5% 需要人工调整的,就是那些特殊的业务逻辑和复杂的组件通信。根据我们的经验,大概需要 2 周左右完成验证和调整。

八、注意事项:别指望100%完美

先打个预防针,这工具虽然能省很多事儿,但也不是什么都能自动搞定。有些地方还是得手动调:

8.1 这些地方可能得手动改改

1. 复杂的组件通信

有些嵌套调用的 $invoke 可能转得不太准:

// 比如这种,可能得手动确认一下引用关系
this.$invoke('modal.popup', 'show', params)

2. 第三方插件

WePY 的专用插件,UniApp 里基本没有。得自己找找有没有类似的:

// 这种就得想办法找替代品
import wepyPlugin from 'wepy-plugin-xxx'

3. 某些特殊写法

有些项目为了绕过 WePY 的限制,会用一些"骚操作"。这种就得看具体情况,可能得重写。

8.2 转完之后该检查啥

根据我们的经验,这些地方最好都看一眼:

  • 先看能不能编译通过,报错的话基本都是类型问题
  • 点点页面,看路由跳转正不正常
  • 看看组件引用有没有问题,路径对不对
  • 试试网络请求、本地存储这些 API
  • 看看样式有没有乱,尤其是用了 Less 函数的地方
  • 点点按钮,测测事件绑定
  • 验证下生命周期,特别是带参数的
  • 试试父子组件通信,看数据传递对不对
  • 如果用了 Pinia,测测状态管理
  • 分包页面都点开看看

8.3 转完之后可以继续优化

能跑起来只是第一步,后面还有不少可以优化的地方:

1. 把类型补得更细致

// 转换工具会生成基础类型,你可以进一步完善
interface UserInfo {
  id: number
  name: string
  avatar: string
  role: 'admin' | 'user' | 'guest'  // 更精确的类型
}

const userInfo = ref<UserInfo | null>(null)

2. 抽取可复用的逻辑

Vue3 有个 Composables 的概念,说白了就是把常用逻辑抽出来复用:

// 比如用户相关的逻辑,好多页面都要用
function useUserInfo() {
  const userInfo = ref<UserInfo | null>(null)
  const isLogin = computed(() => !!userInfo.value)
  
  const login = async (username: string, password: string) => {
    // 登录逻辑
  }
  
  const logout = () => {
    userInfo.value = null
  }
  
  return { userInfo, isLogin, login, logout }
}

3. 性能还能再榨榨

// 数据量特别大的话,用 shallowRef 会快一些
import { shallowRef } from 'vue'
const bigData = shallowRef({ /* 一大堆数据 */ })

// 复杂计算用 computed 缓存起来
const expensiveValue = computed(() => {
  // 各种复杂计算
})

九、迁移之后:不只是技术的升级

做完这个迁移,收获比我想象的多。用罗曼·罗兰的话说:"世上只有一种英雄主义,就是在认清生活真相之后依然热爱生活。"技术迁移也是如此,在认清了老项目的种种问题之后,我们选择用更好的方式重新出发。

9.1 技术债的清零与新生

最直接的变化是技术栈终于跟上时代了。记得迁移完成的那天晚上,我们几个人在办公室加班到很晚,看着 Vite 服务器启动的瞬间,所有人都笑了。三年的技术债,一朝清零,这种感觉就像卸下了背上的大石头。

现在再也不用跟候选人尴尬地解释"WePY 是个啥"了。简历上写着 Vue3 和 TypeScript 的,基本都能快速上手。有个新来的同事跟我说:"这套技术栈学起来很顺,跟我之前的经验都能对上。"这就对了,我们用的是业界主流,而不是自己在小圈子里玩。

技术选型这件事,其实就像《人月神话》里说的:"没有银弹。"但至少,我们现在站在了主流技术的河流里,不用担心被时代抛弃。

9.2 效率的提升与时间的意义

构建速度快了十几倍,这个数字听起来很抽象,但实际用起来,差别大到让人难以置信。

以前用 WePY,改个代码要等五六秒,一天下来光等编译就得好几分钟。现在用 Vite,基本上保存代码的瞬间,页面就更新了。团队里有个老哥感慨:"原来开发可以这么丝滑,这才是程序员该有的体验。"

TypeScript 也帮了大忙。很多低级错误在编译时就被拦下来了,不用等到运行时甚至上线后才发现。有次我写错了一个属性名,IDE 立马就给我标红了,这种即时反馈让人很有安全感。

但效率提升带来的,不仅仅是时间的节省。更重要的是,它让开发变得更纯粹了。不用再因为等待而分心,不用再因为反复调试而烦躁,专注力能更好地投入到业务逻辑和代码设计上。用《黑客与画家》的话说:"好的工具应该是透明的,让你忘记它的存在,只关注你要解决的问题。"

9.3 维护的轻松与代码的尊严

Composition API 让代码的组织方式发生了质的变化。相关的逻辑都在一起了,不用再在文件里上下翻腾。有次改一个功能,以前需要在 data、computed、methods、生命周期四五个地方跳来跳去,现在一个 useTaskList 函数就全搞定了。

重构也变得更安全了。TypeScript 加上现代 IDE,改个变量名,所有引用的地方都自动更新。不像以前用 JavaScript,全局搜索替换,每次都提心吊胆怕漏掉哪里。有次重构一个公共方法,IDE 自动找出了 47 处引用,全部更新,一个都没漏。这种感觉,就像有个可靠的伙伴在帮你兜底。

遇到问题查文档也方便多了。Vue3 的文档写得很详细,社区也活跃,基本上遇到的坑都有人踩过。不像 WePY,搜半天可能就找到几个几年前的帖子,还不一定能用。

代码也有尊严。当你用着被淘汰的技术栈,维护着腐化的代码,每天都在和技术债作斗争,久而久之,连自己都会怀疑工作的意义。但当你用上了现代化的工具,写出了清晰优雅的代码,那种成就感和自豪感是完全不一样的。

9.4 如果有多个项目,就更划算了

这工具开发完,不是用一次就扔了。我们公司还有两个 WePY 项目,都能用这套流程转。

算算账:

手动改一个项目,4 个月,3-4 个人全职,按人月 2.5 万算:

手动成本 = 4 个月 × 3.5 人 × 2.5 万 = 35 万元

用工具转换,投入 4 个人,1 个月时间:

工具开发 2 周 + 快速转换 + 验证 2 周 = 1 个月
成本 = 1 个月 × 4 人 × 2.5 万 = 10 万元

单项目节省:35 - 10 = 25 万元,时间节省 75%

更关键的是,这 1 个月里,真正的转换过程非常快。剩下的时间都在做验证和完善,相当于多出 3 个月可以做其他事情。

如果公司有多个 WePY 项目要迁移,那就更划算了。工具开发一次,后面每个项目只需要快速转换 + 2 周验证。

第二个项目:2 周 × 4 人 × 0.6 万 ≈ 5 万
第三个项目:2 周 × 4 人 × 0.6 万 ≈ 5 万

三个项目总投入:10 + 5 + 5 = 20 万
手动总成本:35 × 3 = 105 万

总节省:85 万元,ROI 达到 5 倍以上

而且时间上,手动改三个项目至少要一年,用工具一个半月就能全部搞定。

十、设计哲学:与复杂度共舞

写代码这么多年,越来越体会到一个道理:好的设计不是一开始就完美,而是能够应对变化。这次工具开发,我们在设计上做了很多取舍和权衡。

10.1 访问者模式:让扩展变得优雅

为什么选访问者模式来遍历 AST?

其实一开始我们写的很简单粗暴,就是一个大大的 switch-case,遇到什么节点就处理什么。写着写着就发现不对了,一个文件 2000 多行,改个东西要翻半天,添加新规则要在好几个地方改代码。

有天晚上加班,突然意识到这样下去不是办法,得重构。想起之前看过的设计模式,访问者模式很适合这个场景。在白板上画了个草图,大概是这个意思:

AST遍历器(Traverser)只负责遍历
各种访问者(Visitor)只负责处理
两者分离,各司其职

这么一改,代码结构立马清晰了。想加新的转换规则?写个新的 visitor 就行,不用动遍历逻辑。每个 visitor 只管自己负责的节点类型,代码看着也舒服多了。其实我在这里走了个弯路,后来才知道基于AST开发时,访问者模式就是标配。

这就是《设计模式》里说的"开放封闭原则":对扩展开放,对修改封闭。

// 伪代码示例
const visitor = {
  // 处理类声明
  ClassDeclaration(path) {
    // 转换为 setup 函数
  },
  
  // 处理类属性
  ClassProperty(path) {
    // data → ref
    // computed → computed
    // methods → 普通函数
  },
  
  // 处理类方法
  ClassMethod(path) {
    // 生命周期函数特殊处理
  }
}

10.2 白名单机制:从教训中学习

第一次运行转换工具,我们信心满满。转换完成后,打开输出目录一看,傻眼了:pages.json 里有 200 多个页面配置。

怎么回事?仔细一看,原来所有的 .wpy 文件都被当成页面了,包括那些公共组件、弹窗、对话框。问题出在哪?我们的逻辑太简单了:凡是 .wpy 文件,就生成路由配置。

吃一堑长一智,加了个白名单机制:

// routeWhitelist.js
const ROUTE_WHITELIST = [
  'custom-modal',      // 弹窗不是页面
  'user-dialog',       // 对话框也不是
  '@/components/**/*'  // components 下面的都不算
]

这样一来,公共组件就不会被误判了。有时候,边界条件的处理比核心逻辑更重要。

10.3 渐进式策略:先求有再求好

一开始我们想做到 100% 完美转换,每种语法都考虑到,每个边界都处理好。结果发现根本不现实,项目里有太多特殊情况,有些甚至是当年为了绕过 WePY 限制写的"黑科技"。

后来转变了思路,分三步走:

第一步:让它能跑

  • 自动化率达到 85-90%
  • 核心功能都能转换
  • 编译能通过

第二步:手动调优

  • 修复那些自动转换不完美的地方
  • 完善 TypeScript 类型
  • 调整业务逻辑

第三步:持续优化

  • 利用 Vue3 的新特性改进代码
  • 提取可复用的 Composables
  • 性能优化

这种"先求有再求好"的策略,让我们能快速看到成果,也让团队保持信心。完美是优秀的敌人,有时候 80 分能交付,比追求 100 分却永远完不成要好得多。

"我们接受了工具的局限性,也接受了需要人工调整的现实。重要的是,工具帮我们做了 95% 的重复劳动,剩下 5% 的人工处理,恰恰是最有技术含量的部分。

关于整个过程的一个简单记录:

  • 最初阶段,简单的语法、API处理 image2025-6-10_14-18-37.png

  • 有点效果了 image2025-6-12_17-44-31.png

  • 一个真实页面的处理 Snipaste_2026-01-23_18-50-23.jpg

十一、还能做什么

11.1 目前能做的事

现在这个版本已经能:

✅ 把 WePY 类组件转成 Vue3 的组合式 API
✅ JS 自动转 TypeScript
✅ Less 转 SCSS
✅ Vant 组件换成 WotUI
✅ 处理分包结构
✅ 优化导入路径
✅ 保留生命周期参数
✅ 转换事件系统

基本上日常用到的都覆盖了。

11.2 以后可能会加的功能

有些想法还没实现,列一下:

🔲 类型推导做得更智能点,现在生成的类型还比较基础
🔲 支持其他框架,比如 Taro、mpvue 转 UniApp
🔲 加个代码质量检查,转完自动找出可能有问题的地方
🔲 提供性能优化建议
🔲 要是能自动生成单元测试就更好了

11.3 关于开源

这工具本身是基于开源项目 wepy-to-uniapp 改造的。站在巨人的肩膀上,我们才能走得更快。

目前这个工具还在公司内部投入使用,我们还有两个 WePY 项目等着用它来迁移。等它完成在公司内部的使命后,我们会整理出一个开源版本,放到 GitHub 上。

为什么不现在就开源?因为现在的代码里有很多公司业务相关的特殊处理和内部配置,需要时间去剥离和通用化。我们希望开源出来的是一个干净、通用、文档完善的版本,而不是一个充斥着内部业务代码的半成品。

开源不是目的,而是一份责任。 我们从开源社区受益,也想回馈给社区一个真正有价值的工具。

如果你对这个工具感兴趣,可以先关注我们的技术博客,或者通过邮件联系我们。等开源版本准备好了,我们会第一时间分享出来。

十二、写在最后:技术的温度

夜深了,保存了最后一行代码,关掉 IDE,看着窗外城市的灯火。回想起这一个月的历程,突然想起了《告别2023》里写过的那句话:

"月亮照着晚归的人。"

我们这些写代码的人,大概就是那些晚归的人吧。在技术的长河里跋涉,有时候会累,会迷茫,会质疑自己在做的事情有没有意义。但当你看到 20 万行代码快速完成转换,当你看到团队不再被技术债困扰,当你看到新人能快速上手现代化的技术栈,你会觉得,这一切都值得。

关于技术的选择

技术更新太快了,框架三五年就换一茬。很多人问:"现在学的技术,过两年会不会又过时了?"可能会,也可能不会。但我想说的是,技术本身不是目的,解决问题才是。

就像林语堂说的:"人生不过如此,且行且珍惜。自己永远是自己的主角,不要总在别人的戏剧里充当着配角。"技术选型也是如此,不要被框架绑架,不要被技术债困住,永远保持对新技术的好奇心和学习力,同时也要有改变现状的勇气。

关于自动化的意义

这次迁移让我更深刻地理解了"自动化"这三个字。不是为了炫技,不是为了追求技术上的极致,而是为了让人从重复劳动中解放出来,去做更有价值的事情。

程序员的本质工作就是两件事:消除重复,创造价值。见到重复的模式就要想办法抽象它、自动化它。这不是偷懒,这是智慧。就像《黑客与画家》里说的:"好的设计师,应该去寻找那些重复的模式,然后消除它们。"

关于团队协作

这次迁移,4 个人,1 个月,完成了原本需要 4-6 个月的工作。这背后是团队的协作与信任。有人负责语法转换,有人搞架构设计,有人做全流程验证,每个人都在自己擅长的领域发光。

想起《人月神话》里的那个著名论断:"向进度落后的项目增加人手,只会使进度更加落后。"但好的团队不是简单的人力叠加,而是 1+1>2 的化学反应。当每个人都在做自己擅长且有价值的事,当目标清晰、分工明确、互相信任,奇迹就会发生。

给正在迁移路上的你

如果你的项目还在用 WePY,或者用着任何已经被淘汰的技术栈,我想说:

不要怕。技术迁移虽然困难,但并非不可完成。我们完成了 760 个文件的转换,95% 的代码自动搞定,你也可以。

不要急。渐进式迁移,先让它跑起来,再慢慢优化。原项目不动,新项目单独验证,就算出了问题也不影响线上。

不要独自扛着。寻求团队的帮助,借助工具的力量,站在前人的肩膀上。我们踩过的坑,希望能帮助你少走弯路。

技术更新太快,与其抱着老技术硬扛,不如早点迁移。反正迟早要做的事,何不趁早?

最后的最后

深夜写完这篇文章,窗外开始飘起了雪。想起年初的冬天,想起这一年的变化,想起那些加班到深夜的日子,想起代码编译通过时的喜悦。

技术在变,框架在变,但那些不变的东西更重要:对代码的热爱,对问题的好奇,对完美的追求,对团队的责任。

愿每一个还在维护老项目的你,都能早日解脱。

愿每一行代码,都奔跑在最好的时代。

愿你在黑暗中前行时,依然能看到月光。

且将新火试新茶,诗酒趁年华。2026里马年,我们继续前行。


附录:想了解更多?

关于工具的使用

目前这个工具还在公司内部使用和完善中。如果你也有 WePY 项目需要迁移,或者对这个工具的实现细节感兴趣,欢迎通过邮件联系我们交流。

等工具完成内部使命、整理好开源版本后,我们会第一时间分享到 GitHub 上,届时会包含:

  • 完整的源代码
  • 详细的使用文档
  • 配置说明和示例
  • 常见问题解答

联系我们

📧 邮箱:gfe@goldentec.com
📝 技术博客:关注我们的掘金账号,获取最新动态
💬 技术交流:欢迎邮件讨论技术方案和实现细节


致谢

感谢一起并肩作战的小伙伴们,将近一个月时间里的通力合作,才有了这个工具。感谢那些在深夜还在调试代码的日子,感谢那些一起讨论技术方案的时刻。

感谢 wepy-to-uniapp 开源项目,为我们提供了基础框架和灵感。站在巨人的肩膀上,我们才能看得更远。

感谢所有在技术社区分享经验的开发者们。没有你们的无私分享,就没有技术的进步。

最后,感谢那个三年前开始这个项目的团队(当时我也在内^_^,但大部分都已离开)。虽然技术栈已经老旧,但你们写下的每一行代码,都是有价值的。我们今天的迁移,不是否定过去,而是在延续你们的工作,让这个项目在新的时代继续发光。


参考链接

官方文档

框架与库

工具与资源

相关技术文章

开源项目

社区与论坛


最后再补充一点

如果你也在维护老项目,也在为技术升级发愁,希望这篇文章能给你一点启发,一点勇气。

技术更新太快,但办法总比困难多。与其焦虑,不如行动。

共勉,愿你我都能在技术的长河里,找到属于自己的路。


"屈原放逐,乃赋离骚;左丘失明,厥有国语;孙子膑脚,兵法修列。"
—— 司马迁《报任安书》

所谓"诗穷而后工",有时候困境反而能激发创造力。技术债固然令人头疼,但正是这些困境,让我们有机会去思考、去创造、去突破。