最近把一个原本运行在微信小程序里的项目迁移出了一套独立 PWA。原因很直接:小程序适合作为轻量入口,但平台审核、类目和能力限制比较明显,希望保留原有版本的同时,再提供一个不依赖微信运行环境的 Web 版本。
这次没有重写后端,而是把整个系统调整成了“双前端 + 单后端”的结构:
1 | 微信小程序 ─┐ |
业务数据仍然集中在服务器,PWA 只是增加了一个新的客户端。这样迁移成本比重新开发一套系统低很多。
小程序前端迁移到 Vue
新的 Web 端使用:
1 | Vue 3 |
第一版没有重新设计 UI,而是优先把原来的页面和功能搬过来,包括时间线、记录、回顾、问答、关注、个人中心,以及一个可拖动的小型桌宠。
小程序 API 需要替换成浏览器对应能力:
1 | wx.request → fetch |
其中业务数据仍然通过 API 获取,localStorage 只保存桌宠位置、界面状态和登录信息,不在浏览器里重新维护一套业务数据库。
移动端还增加了安全区域处理:
1 | env(safe-area-inset-top) |
避免刘海屏和底部手势区域遮挡内容。
把网站变成 PWA
普通 Vue 网站基础上增加了 Web App Manifest 和 Service Worker:
1 | manifest.webmanifest |
配置了应用名称、图标、主题色和:
1 | display: standalone |
这样浏览器可以把网页添加到手机桌面,打开后更接近普通 App。
Service Worker 当前只缓存静态资源,不缓存用户接口:
1 | HTML |
而登录、日志、AI 等 /api 请求全部保持在线访问,避免敏感数据被不必要地缓存。
Vue Router 使用 history 模式后还需要处理一个常见问题:直接刷新 /review、/login 之类的子页面时,静态托管平台可能返回 404。
因此增加 SPA fallback:
1 | /* /index.html 200 |
让所有前端路由重新交给 Vue Router 处理。
GitHub + Cloudflare Pages
Web 版本单独建立 Git 仓库,不和原微信小程序、后端代码混在一起。
部署流程变成:
1 | 本地开发 |
相比手动上传服务器,最大的好处是每次 push 都可以自动部署。
以后也可以使用简单的分支策略:
1 | main |
开发分支可以先生成 Preview 页面,确认没有问题再合并到 main。
部署时遇到的两个问题
第一个问题是 API 地址。
本地开发时习惯直接请求:
1 | /api/... |
部署到静态托管平台以后,这会变成:
1 | PWA域名/api/... |
自然找不到真正的 FastAPI。
后来通过 Vite 环境变量解决:
1 | VITE_API_BASE_URL=https://<API域名> |
然后 API Client 统一拼接 Base URL。
需要注意,VITE_* 是构建时变量,修改托管平台里的环境变量后必须重新 Build,单纯刷新网页不会生效。
第二个问题是 CORS。
PWA 和 FastAPI 已经属于不同 Origin:
1 | PWA域名 |
因此后端需要把 PWA 来源加入 CORS 白名单。
浏览器发送 JSON POST 前还可能先发:
1 | OPTIONS /api/... |
也就是 CORS Preflight。遇到“页面能打开,但登录、验证码全部失败”的情况,Network 面板里的 OPTIONS 请求通常是很好的排查入口。
密钥继续只放服务器
迁移 Web 以后最需要注意的是:浏览器前端没有真正的 Secret。
例如:
1 | 邮件服务 SecretKey |
都不能放进:
1 | VITE_* |
因为 Vite 环境变量最终会被打包到浏览器 JS 中。
正确结构仍然是:
1 | PWA |
PWA 只能知道公开的 API 地址,所有真正的凭据继续保留在服务器环境变量中。
最后的结构
这次迁移完成后,项目从单一的小程序前端变成:
1 | ┌─ 微信小程序 |
整个过程真正改动最大的不是后端,而是运行环境的替换:把微信提供的导航、存储、网络请求、触摸事件逐步换成浏览器标准能力。
PWA 的价值也不在于完全取代原生 App,而是用一套普通 Web 技术获得了接近 App 的安装和使用体验,同时保留 Web 部署简单、跨平台和更新快的特点。
对于已经拥有独立后端的小程序项目,这种迁移方式比重新开发一个完整 App 要轻量得多。
