Timeless
花开成景,花落成诗
1.5.0 之后躺了一年半,这次一口气把 TypeScript、自动文档、二十万行数据都处理了,顺手补了 21 个接口,也跟 4 个老接口说了再见
开头先说两件事:
一是感谢。这次重构能啃下来,得多谢 GPT 大人——类型声明、迁移里那些重复劳动、生成器的主体,基本都是它陪着写的,我主要负责定规则、看结果。以前一个人磨一两个月的事,现在一个周末能推掉大半,时代确实变了~
二是道歉。1.6.0 砍掉了哔哩哔哩、QQ 音乐、番剧、碧蓝航线舰船这四个接口,还在用它们的朋友,对不住啦~ 数据源撑不住、维护成本又高,硬留着只会让文档和实现一起烂掉,与其半死不活地挂着,不如干脆说再见。如果确实还有使用场景,欢迎提个 issue 说说,说不定哪天换个数据源它又回来了。
说回正题。没想到这个项目还能再折腾一次。上一个版本 1.5.0 还是去年三月发的,之后基本就躺平了,偶尔想起来加两个接口,文档也懒得同步,慢慢就变成了"能跑就行"的状态~这次趁着有点动力,把一直想做的三件事一起做了:全量换 TypeScript、接口文档改成从代码生成、把仓库里那堆几百兆的二进制搬出去。顺带补了 21 个接口,也砍掉了 4 个。
这三个都不致命,但每天都在磨人,攒到最后干脆一起解决。
项目用的是 ThinkJS,一个有点年头的框架,写法是 export = class extends Base,没有装饰器,路由靠目录约定。所以不能直接把 .js 改名成 .ts 就跑。做法是按"最小可运行"推进:先补 tsconfig.json 和第三方声明,再按 config → utils → model → logic → controller 的顺序一个个文件迁,每迁完一批就跑一遍 npm run typecheck。整个过程塞在一个 commit 里,97 个文件,+8566 / −478。
踩过的坑大概这几个:
lunar-javascript、ip2region 都没有类型声明,手写了两份 .d.ts 丢进 src/types/。老库的 API 面很宽,只声明用到的部分,够用就行。this.mongo()、this.get() 都不在类型里,给 think.Controller 打了份补丁声明,再把取参数包成 getApiQuery<T>(),控制器里就有明确类型了。strict 之后,modelInstance 这种运行时注入的属性必须 declare,不然 tsc 会认为它和父类冲突。开发体验上收益最大的是 tsc-watch:保存即编译,编译成功才重启服务,编译失败继续跑旧代码。比改出个语法错误整个进程就挂掉舒服太多了~
这块是这次最有意思的。
以前是在 Apifox 里手写接口,写完还得再维护一遍。现在 scripts/generate-openapi.cjs 直接用 TypeScript Compiler API 解析源码,规则很朴素:
*Action 就是一个接口,路径按 ThinkJS 约定,router.ts 里写死的别名也能识别;@summary、@description 就是接口标题和说明;src/types/*.ts 里属性的 JSDoc,必填、范围、枚举来自同名 Logic 的 scope;this.success(data) 的实参类型决定响应结构;@internal 的(定时任务、资源更新)不进文档。所以源码里写:
构建的时候就变成 OpenAPI 里的一个 path,带 summary、description 和两个可选参数。以后加接口只要写 Controller、写类型、写校验,文档自己就出来了,不用再打开任何文档工具。
三个刻意的设计:
顺带把 Swagger UI 也搬进了仓库,www/swagger/ 下就是 swagger-ui-dist 的 JS 和 CSS,首页直接用本地文件渲染,不依赖任何 CDN。
这里澄清一下:Apifox 没有弃用,我还是天天在用的,调试和分享接口它最顺手。变的只是文档的源头,从"手工维护的 Apifox 项目"变成了代码。生成的 /swagger/openapi.json 还能直接导回 Apifox,两边不会再漂移~
1.6.0 的 diff 是 +22,031 / −206,117,删掉的那些不是代码,是数据。
数据库 BSON 备份、ip2region 的 xdb、Live2D 模型、宠物图片,全部挪到 GitHub Releases,用 assets-YYYY.MM.DD 单独打标签,跟应用版本解耦。源码仓库只留代码、配置、小 JSON、Swagger 和资源清单。
配套写了两个脚本:
npm run assets:pack:复制现成的数据库备份,打包三类静态资源,生成 manifest.json 和 SHA256SUMS.txt,再自己校验一遍解压后的每个文件;npm run assets:install:校验 SHA256、拒绝路径越界/符号链接/清单外文件,通过了才装进 www/。支持 --only 只装一个、--overwrite 覆盖(覆盖前把旧目录备份到 deploy/resource-backups/)。数据库始终不自动还原,导入导出还是交给部署的人用 mongorestore 手动来。工具只负责把文件搬到位,不碰线上数据,这条线我觉得得划清楚。
加了 21 个,比较有意思的几个:
/time/calendar):一次返回整月的公历、农历、节日、节气、宜忌、工作日标记。麻烦的不是算,是"哪一年有调休数据",库没收录当年安排的时候 isWorkday 得返回 null,不能瞎猜 true 或 false。/time/bazi):四柱、藏干、十神、纳音都是确定性的历法计算。旺衰、喜用神、运势这类主观内容一律不做,文档里也写清楚"民俗娱乐参考"。/time/history):公元前用负数、没有公元 0 年、2 月 29 日要能查,这几个边界讨论的时间比写代码还长。/dictionary/word|riddle|xiehouyu|couplet):汉字、谜语、歇后语、对联,统一分页,保留数据源里的多条释义和原始标点。砍掉的也有四个:
/bilibili):上游数据不稳定,经常整段拉不到。/music/tencent):依赖第三方音乐接口,维护成本太高。/bangumi):番剧数据源停更了,留着也是死数据。/azurlane/ship):舰船数据体量大、更新又勤,整体下线。原因各不相同,本质一样:公共服务的成本是持续的,上游一变、数据一停,接口就从资产变成负债。Spine 模型留着了,但资源改成从上游项目自己下,版权和数据都不该由这个仓库来背。
重构这事,如果目标只是"把 JS 改成 TS",我大概改到一半就没劲了。这次是"TS + 文档生成 + 数据分离"一起做,每一步都能立刻看到回报:类型让重构有安全感,文档生成让类型有了用处,资源分离让仓库终于能正常 clone。成就感大概就是这么一点点攒出来的~
还有个感受是,删功能比加功能更需要说明,应该提前发公告逐条写了原因和替代方案,并在文档标注将废弃以及废弃时间。毕竟这是公开给大伙用的,下次一定注意。(还有下次?)
项目地址:https://github.com/Spacesless/api,文档:https://api.timelessq.com
Power by ThinkJS + Nuxt.js,已经稳稳地存活了 8年202天 

Copyright © 2018 - 2026 Timeless. All rights reserved.桂ICP备18003656号-1桂公网安备45092102000147号
评论