跳转到主要内容

小程序

配置、开发、构建和发布 uni-app 多端小程序

小程序端是随主项目发布的独立 uni-app 子模块,本章集中说明从主站接口配置到多端发布、内容渲染和互动功能的完整使用方式。

项目边界

mini/ 是主项目中的 Git 子模块,使用 Vue 3、TypeScript、Vite、wot-design-uni 和 UnoCSS,支持 H5、微信小程序和支付宝小程序。它不直接连接 PostgreSQL,而是通过主站提供的 /api/mini/* 接口读取内容和提交评论,因此主站和小程序可以分别构建、发布和回滚。

克隆主项目时一并拉取子模块:

Bashgit clone --recurse-submodules https://github.com/imqi1-github/imqi1-cms.git

已经克隆主项目但 mini/ 目录为空时,在主项目根目录执行:

Bashgit submodule update --init --recursive

小程序拥有独立的 package.json、tsconfig.json 和 ESLint 配置,依赖不会随主项目的安装自动写入。修改小程序后,需要先在 mini/ 子模块中提交并推送,再回到主项目提交更新后的子模块指针。

环境与安装

小程序开发需要 Nodejs ≥ 22 和 Bun;只有在重新生成图标字体时才需要 Python ≥ 3.12。调试微信或支付宝版本时,还需要安装对应的平台开发者工具。

进入 mini/ 目录安装依赖:

Bashbun install

小程序端使用自己的环境文件,不读取主项目根目录的 .env。复制 mini/.env.example,分别创建 .env.development 和 .env.production,前者用于 dev 命令,后者用于 build、提审和正式发布。

EnvVITE_API_BASE_URL=https://example.com/api/mini
VITE_MINI_API_SECRET=请填写与主站相同的随机密钥

VITE_API_BASE_URL 必须完整包含 /api/mini,生产环境使用 HTTPS;VITE_MINI_API_SECRET 必须与主项目 .env 中的 MINI_API_SECRET 完全一致。开发环境可以同时留空两端密钥,生产环境不能留空,否则主站会拒绝所有小程序接口请求。

主站配置

打开主项目的 site.config.ts,确认 features.miniApi 为 true。这个开关在构建期生效:
false 时 nuxt.config 里的 nitroIgnore 会把 api/mini/** 整个从构建产物里排除(端上请求直接 404),
true 时再通过 routeRules["/api/mini/**"].cors = true 启用跨域。它与 MINI_API_SECRET 是两件事:
前者管构建产物,后者管运行期鉴权。miniQr 只控制主站文章页是否显示小程序码入口,作用与 miniApi 独立,不能互相代替。

主站密钥和微信小程序码配置见 .env 与 site.config.ts。MINI_API_SECRET 只在服务端读取,WECHAT_MINI_APPID 和 WECHAT_MINI_SECRET 则用于主站生成文章小程序码;未配置微信凭据时,小程序码接口返回 404,文章页会隐藏对应入口。文章页的小程序码配置与 mini/src/manifest.json 中的运行 AppID 是两套配置,需要分别填写。

小程序侧的站点展示配置位于 mini/src/site.config.ts。其中 siteName 和 siteUrl 控制站点名称及根相对链接补全,home 控制首页标语和按钮文案,category.pageSize 控制分类分页大小,category.photoCategorySlugs 决定哪些分类使用图片瀑布流,codeFont 控制代码块的字体家族、字体栈与字体文件地址。修改这些值后重新构建小程序才会生效。

代码字体的三处需要保持一致:codeFont.family(用于 uni.loadFontFace 注册名)、codeFont.stack(用于 CSS 渲染栈,App.vue 通过 <style> 块的 v-bind() 把它绑到全局 --code-font-family)、codeFont.url(字体文件下载地址)。ensureCodeFont 按需调用 loadFontFace,失败时静默降级到系统等宽字体。fork 用户改字体只动 site.config.ts 一处即可。

请求签名

生产环境的 /api/mini/* 请求使用 HMAC-SHA256 签名。小程序端会按照请求方法、完整路径、时间戳和随机 nonce 生成签名,并发送 X-Mini-Timestamp、X-Mini-Nonce 和 X-Mini-Sign 三个请求头;路径必须包含查询字符串,例如分类分页参数也必须参与签名。

签名字符串和结果格式如下,PATH 使用服务端收到的完整路径,不包含协议和域名:

TypeScriptconst stringToSign = `${METHOD}\n${PATH}\n${timestamp}\n${nonce}`
const signature = HMAC_SHA256_HEX(MINI_API_SECRET, stringToSign)

服务端允许时间戳与当前时间相差最多 300 秒,同一个 nonce 在有效窗口内只能使用一次。OPTIONS 预检请求不需要签名,其他 GET 和 POST 请求都需要签名;开发环境 NODE_ENV=development 会跳过校验,生产环境没有 MINI_API_SECRET 时直接返回 401。

小程序中的 VITE_MINI_API_SECRET 会在构建时写入客户端包,能够被反编译获取,因此它只是阻挡批量脚本和误用接口的门槛,不是用户身份认证。请使用专用的随机长字符串,不要复用数据库密码、LOGIN_SECRET 或其他管理密钥。

平台配置

在 mini/src/manifest.json 中填写自己的微信 AppID;文件里 mp-weixin.appid 的值是 imqi1 官方小程序当前的 AppID,如果你是 fork 用户或自建部署,必须把它换成自有 AppID 后再上传。微信开发者工具上传产物时 AppID 与小程序后台绑定,未替换会导致提审失败。支付宝小程序的 AppID 在支付宝开发者工具中填写。微信和支付宝开发者工具都需要把主站 API 域名加入 request 合法域名,小程序端不能使用未加入白名单的接口地址。

文章封面、正文图片、评论头像、代码字体和音乐资源会由各平台直接请求。请将实际使用的图片、头像镜像和 CDN 域名加入 downloadFile 合法域名;代码字体默认来自 mini/src/site.config.ts 的 codeFont.url(默认 cdn.imqi1.com),如果你替换了 URL,记得同步把新域名加入白名单,微信真机加载会失败。H5 调试时还需要让 API 服务允许对应的跨域请求。

示例:

Texthttps://cdn.imqi1.com
https://cn.cravatar.com
https://imqi1.com
https://servicewechat.com

开发与构建

在主项目根目录可以使用转发脚本,不需要手动切换到 mini/ 目录。先启动 H5 版本在浏览器中预览:

Bashbun run mini:dev:h5

调试微信小程序:

Bashbun run mini:dev:mp-weixin

命令生成的开发产物位于 mini/dist/dev/mp-weixin,使用微信开发者工具导入该目录。调试支付宝版本:

Bashbun run mini:dev:mp-alipay

正式构建时使用对应的 mini:build:* 命令。微信产物位于 mini/dist/build/mp-weixin,支付宝产物位于 mini/dist/build/mp-alipay,H5 产物位于 mini/dist/build/h5;微信和支付宝产物需要在各自的开发者工具中上传、提交审核和发布,H5 产物可以作为独立静态站点部署。

提交构建前执行小程序类型检查:

Bashbun run mini:type-check

执行独立 ESLint 检查:

Bashbun run mini:lint

小程序端还拥有自己的测试,进入 mini/ 目录后执行 bun run test 运行,bun run type-check:test 校验测试代码的类型;主项目的 bun run test 不会收集小程序的用例。

如果新增了 Remix Icon 字体图标,先安装字体脚本依赖:

Bashpip install -r mini/scripts/requirements.txt

再把图标名加入 mini/scripts/subset-font.py 的 ICONS,运行字体检查或重新生成脚本,避免开发环境能显示而发布包缺少字形。

页面与内容

小程序底部导航包含「首页」「归档」「分类」和「关于」。分类页还提供「足迹」「链接」「留言」和「更新日志」入口;评论关闭时「留言」入口和留言页会一起隐藏。文章详情与分类详情不放在底部导航中,通过文章卡片、归档和分类列表进入。

首页与归档

首页从 /latest-contents 读取最近 6 篇已发布的普通文章,图片分类中的文章不会重复出现在这里。点击文章卡片进入详情,点击「查看全部」进入归档;首页主按钮会把主站网址复制到剪贴板,关于按钮进入「关于」页。

归档页从 /archive 读取全部已发布文章,按创建时间倒序并按年月分组显示。点击时间线中的文章即可进入详情页,归档不受首页的 6 篇数量限制。

分类与文章

分类页从 /categories 显示分类名称、已发布文章数量、最新文章封面和最新文章标题。打开普通分类后,文章按分页标题列表展示;打开 category.photoCategorySlugs 中配置的图片分类后,页面切换为双列封面瀑布流,并在进入文章时带上 photo=1 使用图片版式。分类接口默认每页 12 篇,服务端允许通过 pageSize 调整为 1 至 50 篇。

文章详情支持普通文章和图片文章两种版式。普通版式显示封面、标题、发布时间、分类和正文;多张封面会组成轮播,点击图片可以预览。图片版式使用满屏封面,向下滚动后显示标题、描述、分类和正文。微信小程序码进入文章时可以使用 scene 参数传递文章 ID,普通页面跳转则使用 id 参数。

足迹、链接与留言

「足迹」只显示后台启用的地点,包含地点封面、描述以及关联的已发布文章;图片分类文章会沿用图片详情版式。「链接」把已启用友链和订阅合并,按注册域名去重,友链优先保留,点击卡片会复制网址,因为小程序不能直接打开外部网页。未审核的友链修改不会出现在列表中。

「留言」不是独立的数据表,而是绑定到主站的一篇文章评论区。系统优先使用后台设置中的留言板文章 ID,没有设置时回退到 slug 为 messages 的已发布独立页面;主站 site.config.ts 中的 features.miniComment 关闭后,留言入口和页面都会隐藏。页面内容与文章评论区使用同一套表单和评论树。

更新日志与关于

「更新日志」从 /changelogs 读取全部记录,按年月倒序展示,一条记录可以包含多个「功能」「优化」「修复」「删除」「设计」「新增」「其他」条目。小程序端会自行解析更新内容中的粗体、斜体、行内代码、链接和有序或无序列表;点击链接只复制地址,不在小程序内打开。

「关于」页使用 mini/src/data/about.ts 中的静态数据展示博客信息、联系方式、技术栈和版权信息。网站地址、邮箱等可复制项目点击后会写入剪贴板。

Markdown 与媒体

文章详情接口返回 Markdown 原文,小程序不使用 v-html,而是用原生 view、text、image 和 video 组件解析渲染。基础 Markdown 支持一至六级标题、段落、粗体、斜体、行内代码、链接、引用、分割线、有序列表、无序列表、表格和代码块;在此之上还支持删除线 ~~文字~~、引用式链接([文字][label] 配 [label]: url)、引用式图片 ![alt][label] 与快捷引用 ![label]。代码块支持语言标记、可选文件名和横向滚动,代码字体按需从 CDN 加载。

正文还支持项目已有的容器语法。::: details 标题 用于可折叠内容,::: callout success|warning|error|info 用于提示框,::: card 和 ::: simple-card 用于外链卡片,::: swiper 和 ::: waterfall 用于图片轮播与瀑布流,::: music 用于音乐播放器,::: repo 用于 GitHub 或 Gitee 仓库卡片,::: live-photo 用于实况照片,::: video 用于原生视频播放器。折叠面板支持嵌套,图片和表格超出屏幕时可以横向查看。

音乐容器可以写成 ::: music auto <音乐平台链接>,也可以写成 ::: music <类型> <平台> <编号>。auto 形式只在端上识别网易云(music.163.com)、QQ 音乐(y.qq.com)、酷我(kuwo.cn)、酷狗(kugou.com)四个域名;虾米、百度等其他平台的歌曲链接要走手动 <类型> <平台> <编号> 形式,否则会被解析器丢弃。小程序接口支持 playlist 和 song 类型,并由主站服务端解析真实音频、封面和歌词地址。仓库容器只接受 github.com 或 gitee.com 地址,主站会代为请求仓库信息,避免小程序直接访问第三方 API。

普通图片点击后打开预览,根相对链接会按 mini/src/site.config.ts 中的 siteUrl 补全。外链不会在小程序内跳转,而是复制到剪贴板后由用户在浏览器打开。原始 HTML 不属于小程序 Markdown 支持范围,复杂 HTML 需要改写为上述容器或标准 Markdown。

实况照片的地址末尾保留 #live,或使用 ::: live-photo 容器标记。H5 会把照片内嵌的视频段转换为 Blob 地址;微信和支付宝端会在首次点击时下载整包、提取内嵌 MP4 并写入临时文件,视频播放完自动回到静态图。视频提取失败时会退化为普通图片预览,因此真机测试时仍需把资源域名加入平台白名单。

评论与身份

小程序评论由主站 site.config.ts 的 features.miniComment 独立控制,主站后台「开启评论」只控制主站评论区。关闭该配置后,评论列表接口返回关闭状态,评论提交接口返回 403,评论区和留言入口不会展示。评论区只读取已审核通过的评论,根评论按最新时间显示,回复按阅读顺序嵌套在父评论下;可以发表顶级评论,也可以点击「回复」针对已有评论提交回复。

评论表单至少需要昵称和正文,邮箱、个人链接是否必填跟随主站「评论设置」。成功提交后,小程序会在本地记住昵称、邮箱和链接,下次打开表单时自动带出;小程序没有主站后台账户登录或绑定流程。开启人工审核或百度内容审核时,待审核评论会提示审核后展示,不会立即出现在公开评论列表。评论文本支持主站同款表情占位符 :[分类-名称](例如 :[heo-微笑]、:[猫猫虫-xxx]、:[cat-xxx]),数据源与主站 app/assets/emojis.json 同步复制,识别不出的占位符会按原样保留。

服务端会校验文章必须已发布、正文长度不超过 5,000 个字符、邮箱和链接格式,并按照后台的评论间隔限制同一 IP 的提交频率,默认间隔为 60 秒。评论提交还会经过蜜罐、内容净化和主站审核策略,评论关闭时提交接口返回 403。

接口与缓存

小程序所有请求都通过 VITE_API_BASE_URL 发送到 /api/mini/*,成功响应统一包含 success 和 data。文章数据接口包括 /latest-contents、/archive、/categories、/category/:slug/contents 和 /content/:id;互动和聚合接口包括 /comments、/messages-config、/links、/travels 和 /changelogs;扩展内容接口包括 /music 和 /repo。

公开 GET 接口强制返回 Cache-Control: no-store, max-age=0, must-revalidate,由 server/middleware/mini-auth.ts 在路径匹配后无条件设上。这样主站后台改文章后,小程序下一次请求就拿到最新内容,不会被浏览器或 CDN 在 5 分钟窗口里继续返回旧数据。主站页面的 ISR 缓存和 /api/mini/* 的 HTTP 缓存是两层,invalidateContentCaches 只清前者。代价是每个请求都回源,但 /api/mini/* 流量低且鉴权已校验,可接受。缓存范围和主站内容更新后的清理方式见 缓存管理。

评论写入成功后,主站会失效首页、分类、标签、文章详情和归档等受影响的页面缓存;更新日志、文章和其他管理内容也会按主站的缓存规则处理。小程序接口本身不提供后台管理能力,内容仍然在主站后台维护。

发布流程

先在主站部署并验证 /api/mini/* 接口,再用 .env.production 构建小程序。每次修改 API 域名、签名密钥、siteUrl、图片分类、首页文案或代码字体后,都需要重新构建对应平台产物。

微信和支付宝版本构建完成后,将 mini/dist/build/mp-weixin 或 mini/dist/build/mp-alipay 导入对应开发者工具,确认 AppID、request 合法域名、downloadFile 合法域名和隐私配置,再上传、提交审核和发布。H5 版本直接部署 mini/dist/build/h5,并确保部署地址仍能访问主站 API。

发布微信小程序时,先在 site.config.ts 中把 features.miniFakeData 改为 true,然后构建、上传到服务器并重启主站,再提交小程序。审核模式会把所有 /api/mini/* 返回内容替换为占位数据并关闭评论,避免审核员看到不合规的友链、订阅、旅行或更新日志;审核通过后再改回 false 重新构建。

子模块的提交顺序是:先在 mini/ 目录提交并推送小程序代码,再回到主项目提交子模块指针。主项目只记录子模块提交号,不会把小程序源文件复制到主项目中。

常见问题

小程序接口返回 401 时,先确认主站 MINI_API_SECRET 与小程序 VITE_MINI_API_SECRET 完全一致,再检查请求地址是否包含 /api/mini、设备时间是否准确,以及生产构建是否加载了 .env.production。修改密钥后必须重新构建小程序和重新部署主站。

文章或评论头像无法显示时,通常是图片域名没有加入平台的 downloadFile 合法域名。评论头像来自 Gravatar、Cravatar 或 WeAvatar 等镜像地址;代码字体来自 mini/src/site.config.ts 的 codeFont.url,如果你替换了默认 URL,对应的 CDN 域名也要加白名单。

留言入口消失时,检查主站 site.config.ts 中的 features.miniComment、留言板绑定文章以及 /api/mini/messages-config 返回的 commentEnabled;该配置关闭时这是预期行为。图片分类没有进入瀑布流时,检查文章所属分类 slug 是否写入 mini/src/site.config.ts 的 photoCategorySlugs,并重新构建小程序。

实况照片在真机上只显示静态图时,先确认图片资源可下载、域名已加入白名单,并检查照片文件确实包含内嵌 MP4。视频无法提取时小程序会自动退回静态图片,不会影响文章正文的其他内容。