深夜的办公室,显示器的光映在键盘上。打开那个运行了三年的小程序项目,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 2 | Vue 3 | 性能提升 2 倍,代码更简洁 |
| JavaScript | TypeScript | 类型安全,减少 bug |
| Webpack | Vite | 构建速度快 10-15 倍 |
| Less | SCSS | 生态更好,功能更强 |
| Vant Weapp | WotUI | 更现代化的组件库 |
这样的升级,如果手动做,工作量有多大?让我们来算一笔账:
| 项目规模 | 文件数量 | 代码行数 | 手动迁移时间 | 人力投入 |
|---|---|---|---|---|
| 小型项目 | 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.data 或 obj.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
- 打开网站
- 左边输入代码
- 右边自动生成 AST 树形结构
- 可以选择不同的 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 类组件,对吧?但它有几个问题:
- 逻辑分散:同一个功能的代码分散在
data、computed、methods、onLoad四个地方 - this 满天飞:到处都是
this.xxx,容易出错 - 手动刷新:
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
}
}
})
这段代码看起来很长,但逻辑很清晰:
- 找到 WePY 类组件
- 把各个部分(data、computed、methods、生命周期)拆出来
- 转换成 Vue3 Composition API 的写法
- 生成新的 AST 节点
- 替换掉原来的类声明
这就是"编译器"的工作方式:理解→转换→生成。
其中最关键的是 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>
主要变化:
wx:if→v-ifwx:for→v-forbindtap→@tap{{variable}}→{{ variable }}(去掉外层花括号)van-button→wot-button(组件库替换)- 添加了
<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 多个页面用到了这个参数。再看看其他生命周期方法,onShareAppMessage、onPullDownRefresh 这些带参数的方法也都有同样的问题,加起来可能有上百处。
问题定位清楚了:是转换逻辑在处理生命周期方法时,把参数给丢了。既然是 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 里完全没有对应的东西。怎么办?
脑子里过了几种方案:
- 用 Pinia 的状态管理?太重了,而且语义不对。
- 用 provide/inject?层级深了不好用。
- 自己实现一个全局事件总线?简单,但得考虑内存泄漏。
权衡了一下,选了第三个方案,实现一个轻量的 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 个 .wpy | 300 个 .vue | 97% | 类组件转组合式 API |
| JS 文件 → TS 文件 | 149 个 .js | 149 个 .ts | 94% | 自动添加类型标注 |
| WXSS → SCSS | 73 个 .wxss | 73 个 .scss | 99% | 样式语法转换 |
| WXML → Vue Template | 71 个 .wxml | 整合到 .vue | 96% | 模板语法转换 |
| Less → SCSS | 11 个 .less | 11 个 .scss | 99% | 变量语法替换 |
| WXS → TS 工具函数 | 29 个 .wxs | 29 个 .ts | 85% | 模板脚本转换 |
| JSON 配置整合 | 66 个配置 | pages.json + manifest.json | 92% | 配置合并 |
| 组件库替换 (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处理
-
有点效果了
-
一个真实页面的处理
十一、还能做什么
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 开源项目,为我们提供了基础框架和灵感。站在巨人的肩膀上,我们才能看得更远。
感谢所有在技术社区分享经验的开发者们。没有你们的无私分享,就没有技术的进步。
最后,感谢那个三年前开始这个项目的团队(当时我也在内^_^,但大部分都已离开)。虽然技术栈已经老旧,但你们写下的每一行代码,都是有价值的。我们今天的迁移,不是否定过去,而是在延续你们的工作,让这个项目在新的时代继续发光。
参考链接
官方文档
- WePY 官方文档 - WePY 框架官方文档
- UniApp 官方文档 - UniApp 跨端开发框架
- Vue 3 官方文档 - Vue 3 中文文档
- Vite 官方文档 - 下一代前端构建工具
- TypeScript 官方文档 - TypeScript 语言官网
框架与库
- Unibest 框架 - UniApp + Vue3 + TS + Vite5 最佳实践模板
- WotUI 组件库 - 基于 Vue3 的 UniApp 组件库
- Pinia 状态管理 - Vue 3 官方推荐的状态管理库
- UnoCSS - 即时按需原子化 CSS 引擎
工具与资源
- Babel 官方文档 - JavaScript 编译器
- AST Explorer - 在线 AST 可视化工具
- Prettier 官方文档 - 代码格式化工具
- 微信小程序官方文档 - 微信小程序开发指南
相关技术文章
- Vue 3 迁移指南 - 从 Vue 2 迁移到 Vue 3
- Composition API 最佳实践 - Vue 3 组合式 API FAQ
- 访问者模式详解 - 设计模式之访问者模式
- AST 抽象语法树 - 维基百科 AST 介绍
开源项目
- wepy-to-uniapp - 本项目的基础框架
- 微信小程序示例 - 微信官方小程序示例
社区与论坛
- UniApp 社区 - UniApp 官方论坛
- Vue.js 中文社区 - Vue 官方中文社区
- 掘金前端社区 - 技术文章与讨论
最后再补充一点
如果你也在维护老项目,也在为技术升级发愁,希望这篇文章能给你一点启发,一点勇气。
技术更新太快,但办法总比困难多。与其焦虑,不如行动。
共勉,愿你我都能在技术的长河里,找到属于自己的路。
"屈原放逐,乃赋离骚;左丘失明,厥有国语;孙子膑脚,兵法修列。"
—— 司马迁《报任安书》所谓"诗穷而后工",有时候困境反而能激发创造力。技术债固然令人头疼,但正是这些困境,让我们有机会去思考、去创造、去突破。