用 Cursor 把数独小程序的 game.js 拆到 UI.js,最容易出现的结局不是「拆成功」,而是一片引用报错:函数找不到、this 指向丢了、页面 bindtap 指向的函数不在 game.js 里了。撤回和提示这两个功能刚写完就卡在这一步,最后回滚版本收场——这不是 AI 不会写代码,而是模型只负责「搬函数」,不负责「追引用」。想让它把引用关系一并理清楚,前提是对话通道别中途掉线:先去 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 创建一把 Key,再把 Cursor 的模型通道指到 TaoToken,配通之后再谈拆文件,顺序反了就是白折腾。
这篇文章不谈「AI 到底能不能写小程序」,只谈一件很具体的事:当 game.js 里的 UI 渲染部分要拆到 UI.js,Cursor 把函数挪过去之后引用对不上,怎么一步步查、怎么一步步挪回来。我会把通道配置放在前面,因为很多人的第一反应是反复重试、换提示词、甚至让 Cursor「全部重写一遍」,结果越改越乱——其实换一条稳定通道,再让它先输出引用关系,问题会小很多。
1. 数独小程序拆 UI.js 之后,报错到底红在哪几行
1.1 三类最常见的小程序报错,先认脸
微信小程序的报错信息其实很有规律,拆文件之后基本跑不出这三类:
第一类是xxx is not defined或Cannot read property 'xxx' of undefined。典型场景是原来game.js里有个renderBoard(),Page({...})内部直接调用;拆到UI.js后,game.js里还在调renderBoard(),但这个函数已经不在当前作用域了。
第二类是模块导入报错,比如module "utils/UI.js" is not defined或者路径写错。小程序对相对路径和大小写都敏感,UI.js和ui.js在两个平台上可能表现不一样。
第三类是运行时才炸的:this.setData is not a function。原来函数挂在Page上下文里,this天然指向页面实例;挪到UI.js后this变成了模块对象,setData自然就没了。
1.2 Cursor 只搬函数体,不会自动追引用
这一点必须说透。你在 Cursor 里选中game.js的一段 UI 代码,说「把这段拆到 UI.js」,模型看到的上下文通常只有选中的那几十行。它会把函数体搬过去,但不会主动去搜:这个函数被谁调用、页面 wxss 里有没有引用、onLoad里注册的回调是不是还指向旧位置。
所以最后的结果往往是「搬了 70%」。game.js里留下半截调用,UI.js里躺着一堆没人引用的函数,编译能过但点一下就报错。回滚版本不是失败,是你终于意识到:这个活儿需要按引用清单来做,而不是按函数块来做。
2. 让 Cursor 继续改代码之前,先把模型通道换成 TaoToken
2.1 打开官网创建一把专属 API Key
原文里作者是直接打开 Cursor 就开始对话改代码的,仿写到这里要先把通道这一步补上:打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 注册账号,进控制台创建一把 API Key。Key 只显示有限次数,建议建好就存进密码管理器,别散落在聊天记录里。
这里要说清楚 TaoToken 在整件事里的角色:它给 Cursor 提供的是 Key 和 Base URL,让这个很吃 Token 的编程工具能稳定调模型。它不负责修你的函数引用,引用问题还是得靠对话一步步排。把工具定位摆正,后面不容易期待错。
2.2 Cursor 自定义 OpenAI 兼容模型:字段就这么填
Cursor 的设置面板里能挂自定义的 OpenAI 兼容模型。打开设置,找到 Models 区域,展开 OpenAI 相关的配置项,按下面的方式填:
| 配置项 | 填什么 |
|---|---|
| API Key | YOUR_API_KEY,也就是你在 TaoToken 控制台创建的那把 |
| Override OpenAI Base URL | https://taotoken.net/api |
| 模型 ID | 以 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 模型广场当时的列表为准 |
Base URL 这一栏最容易填错:末尾不要加/v1,也不要往上面拼任何查询参数。填成https://taotoken.net/api/v1大概率会返回 404,填成带 UTM 的地址则完全是两码事——落地页是给人点的,接口地址才是给工具用的。
2.3 模型 ID 别自己猜,去模型广场抄
很多人在这里翻车:习惯性地写一个听起来很厉害的模型名,结果请求直接报「model not found」。正确的做法是回到 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 的模型广场,把当前可用的模型 ID 原样复制,粘进 Cursor 的模型列表。
填完之后,在 Cursor 里发一句最简单的对话测试,比如「用一句话说明这段代码在做什么」。能正常回,说明通道通了;报 401 就是 Key 复制时带了空格,报 404 基本就是 Base URL 多写了路径。这两个错排掉,再进入改代码的环节。
3. 先让 Cursor 输出 game.js 与 UI.js 的引用关系清单
3.1 只读不改:第一轮对话只要一张表
通道通了的第一个动作,不是让它继续拆,而是让它只读。提示词可以这样写:
先不要修改任何文件。请通读 game.js 和将要新建的 UI.js 的目标范围,列出:1)每个准备迁移的函数名;2)它在 game.js 里被哪些地方调用;3)它内部是否用到 this.setData 或 this.data;4)迁移到 UI.js 后,这些调用点需要怎么改。用表格输出。
关键在「先不要修改任何文件」这半句。模型在没有写权限压力的情况下,更容易老老实实做检索,而不是急着给你一份看起来完整、其实引用全断的重构。
3.2 自己再核一遍 wxml、json 和 bindtap
模型给的清单不能全信,尤其是小程序里这些「不在 js 里的引用」:
index.wxml里的bindtap="onCellTap",指向的函数必须在页面对象上,不能只存在于 UI.js;index.json里若配置了自定义组件,组件路径别写错;wx.开头的 API 调用(比如wx.showToast)属于全局对象,搬到哪里都能用,这类不用管;getApp()取全局数据的地方,拆完要确认取到的还是同一个实例。
把这几项和 Cursor 给的表格对照一遍,你会发现真正需要小心处理的函数,往往只占迁移总量的三成。剩下七成是纯计算、纯格式化,挪走几乎不会出事。
4. 一次只搬一组:game.js 到 UI.js 的迁移顺序
4.1 先搬不碰 this 的纯函数
第一步挑最安全的:生成棋盘数组、计算某个九宫格、把数字转成展示文案这一类。它们不依赖页面实例,不调setData,挪进 UI.js 之后用module.exports或export暴露出来,在 game.js 顶部 require 进来就能用。
这一批搬完立刻编译一次。小程序开发者工具的编译很快,红了就当场看是哪一行。别攒着五六个文件一起改,那样出错你根本定位不到是哪个函数。
4.2 再搬 setData 渲染函数,把 this 显式传进去
第二批是渲染类函数,它们原本依赖页面上下文。推荐的处理方式不是硬绑this,而是把页面实例当参数传:
// UI.js function renderBoard(page, board) { page.setData({ cells: formatCells(board) }) } module.exports = { renderBoard }// game.js const UI = require('./UI.js') Page({ refresh() { UI.renderBoard(this, this.data.board) } })这样写的好处是职责清晰:UI.js 只管怎么画,game.js 只管什么时候画。this不再跨文件漂移,setData is not a function这类报错基本就消失了。
4.3 撤回和提示这两个功能的引用要单独盯
撤回和提示是这篇里刚加完的功能,也是最容易在拆分中受伤的部分,因为它们同时牵扯数据变更和界面刷新。迁移时注意两点:一是历史栈这种状态数据继续留在 game.js,UI.js 只负责展示;二是wx.showToast、按钮置灰这类交互反馈,要么统一收在 UI.js,要么统一留在 game.js,别一半一半。
判断标准很简单:如果某个函数里既有this.data.history.push(...),又有this.setData(...),那它就不该整体搬走,而是拆成「改数据」和「刷界面」两段。
5. 拆分报错对照:红哪一行就查哪一类
5.1 路径、大小写、require 与 export 混用
小程序的 require 路径是相对当前文件的。game.js和UI.js同目录写require('./UI.js'),如果 UI.js 放在utils/下就得写require('./utils/UI.js')。文件名大小写也要一致,本地能跑不代表真机没问题。
另一个高频坑是导出方式混用:一边用module.exports = {...},另一边用export default {...},然后require拿到的对象结构和你以为的不一样,报错提示往往还很含糊。整个项目统一一种写法,最省事。
5.2 循环依赖和「搬了一半」的中间状态
game.js require UI.js,UI.js 又 require game.js 里的某个常量——这就形成了循环依赖,小程序里表现为某个模块在初始化时是空对象。解决办法是把共用常量抽到独立的constants.js,两边都从那里拿。
还有一种报错不是代码错,而是中间状态错:函数已经复制到 UI.js,但 game.js 里的原函数还没删,导致两份实现并存、行为不一致。每次迁移都以「复制 → 改调用 → 删除原函数 → 编译」为一个完整循环,别停在中间。
6. 引用理顺之后,回控制台对一下这次调用
6.1 用同一把 Key 做一次端到端确认
代码跑通、撤回和提示都还在,说明这次拆分真的落地了。这时候可以拿刚才那把 Key,在 TaoToken 模型对话 里发一条消息,确认通道仍然是通的——毕竟排障过程中你很可能反复改过 Cursor 的模型设置。
如果准备长期用 AI 写小程序,Token 消耗会比想象中快(一次重构对话动辄几万 Token),可以顺手看看 Coding Plan 的套餐档位是否合适。需要再开一把 Key 给别的工具用,就去 控制台 API Keys 创建;想对照着把通道配到其他执行工具上,Claude Code 接入文档 里的环境变量写法可以直接参考,注意填进工具的 Base URL 始终是https://taotoken.net/api,末尾不带/v1。
回看整件事,拆分失败的核心从来不是模型不够聪明,而是我们把「搬函数」当成了「重构」。先配通通道,再让它只读列引用,最后按依赖顺序一组一组挪、每挪一组编译一次——这套流程比任何一次「帮我全部重写」都靠谱。数独这种体量的小程序尚且如此,再大一点的项目,引用清单只会更重要。