qt-uniappx(Android 移动端)
UniAppX(UVue + UTS + Vapor 编译模式)写的音乐应用, 一套代码同时出 Android 与 iOS 包。仓库 https://github.com/barry130/qt-uniappx (CNB 镜像 https://cnb.cool/canace/qt-uniappx),包名 com.qt.app,appid __UNI__7AC1B12。
环境要求
| 依赖 | 要求 |
|---|---|
| HBuilderX | 5.x 或更高(README 推荐);插件声明的引擎下限是 ^4.66 |
| Node.js | 任意 LTS,只用来跑 tools/ 下的静态检查 |
| Android SDK | 通过 HBuilderX 配置;minSdkVersion 26 / targetSdkVersion 34 在 manifest.json |
仓库没有钉住 JDK 与 Android SDK 的具体版本,由 HBuilderX 本地配置决定。
标准基座跑不起来
项目依赖 5 个 uni_modules 插件,其中前四个是 UTS 原生插件(dcloudext.type: "uts"), 标准基座不含它们:
| 插件 | 类型 | 职责 |
|---|---|---|
qt-app-native | uts | 原生能力桥:媒体扫描与删除、公共目录写入、权限查询、设置页跳转、安装 APK |
qt-audio-player | uts | 播放内核:media3 时间线 + 前台服务 + 原生通知栏 + 桌面歌词悬浮窗 |
qt-js-engine | uts | 音源包执行引擎(系统 WebView 的 V8) |
qt-stat | uts | 匿名统计采集与批量上报 |
qt-ui | component | Qt UI 组件库;跟随工程一起编译,不进基座 |
.hbuilderx/launch.json 已把调试方式配成 playground: "custom"。用标准基座调用原生能力会报错, 出现「找不到悬浮窗授权入口」这类假故障。早期独立的 qt-media-store 现已合并进 qt-app-native,看到旧文档说「四个插件」时按上面前四个理解。
常用命令
npm install # 首次克隆后装依赖(唯一运行时依赖是 pinia)
npm run check # 静态检查 .uvue(提交前必跑)
npm run gen:tab-icons # 重新生成 tabBar 图标npm run check 实际是 node tools/check-uvue.mjs:用 @vue/compiler-sfc 解析每个页面 / 组件的 template、script、style 抓结构性问题;校验 <QtIcon name="…"> 引用的图标都在 uni_modules/qt-ui/services/icons.ts 有定义,并报告定义了却没人引用的图标;扫描模板里把 符号当图标的残留字形(♥ ▶ ×)。基线:checked 57 files, 0 with errors; icons defined: 49, referenced: 49。它不替代编译器——UTS 类型检查与 Vapor CSS 编译只有真跑 一次 HBuilderX 构建才知道。报图标错时:新增图标先跑 npm run gen:tab-icons,报「定义了却没 引用」就删掉定义或补上引用。
目录结构
pages/ 页面(34 个,路由表 pages.json;tabBar 只有「发现」「我的」)
services/ 服务层:网络、播放器、桌面歌词、音源包与引擎、升级、公告、统计…
stores/ Pinia 全局状态(player / downloads / dislikes / lyric-offset / source*…)
uni_modules/ qt-ui(唯一 UI 库)+ qt-app-native / qt-audio-player / qt-js-engine / qt-stat
composables/ 页面级函数 utils/ 纯工具(lrc.uts) types/ 类型 static/ 静态资源
styles/ 全局样式 tools/ 校验与生成脚本 docs/ 设计文档
manifest.json / pages.json 应用与路由配置开发流程
- HBuilderX 打开仓库根目录;首次克隆先
npm install。 - 运行 → 运行到手机或模拟器 → 制作自定义调试基座(标准基座缺上面四个 UTS 插件)。
- 改
.uvue的热重载在本项目表现为设备端冷重启,属正常现象。 - 改过
uni_modules/里任何 UTS 代码后必须重新编译插件并重建基座;HBuilderX 按文件指纹 跳过没变化的插件,必要时加--cleanCache true。
关键配置
版本号
manifest.json 是唯一真源:versionName + versionCode(字符串)。运行时由 services/app-version.ts 从 uni.getAppBaseInfo() 读回,代码里不维护第二份常量——发版只改 manifest。同一文件还开着 uni-app-x.vapor: true、vapor-render-target: "bytecode"、 styleIsolationVersion: "2";打包相关是 abiFilters ["arm64-v8a"]、minSdkVersion "26"、 targetSdkVersion "34",nativePlugins 只有 DCloud-UniAppX-FileProvider。
第三方接口与封面大量是 http://,Android 9+ 默认禁止明文流量,所以 app-android/AndroidManifest.xml(与 DCloud 默认清单合并)放行 android:usesCleartextTraffic="true";不放行发行包会报 CLEARTEXT not permitted。
API 基地址
services/config.ts 按环境分离:API_BASE_URL_DEV 可提交,USE_DEV 是开关,实际生效的 API_BASE_URL 由 services/http.ts 取用。生产地址放不提交的 services/config.local.ts:
cp services/config.local.example.ts services/config.local.ts # 填入真实生产地址请求头(三条红线)
services/client-info.ts 统一构造下面四个头,服务端由接口统计(stat_api_hourly 的 ut / app_version)与反馈提交(sys_feedback 的 platform / device / os)两条链路消费:
| 头 | 取值 |
|---|---|
X-App-Ut | app-android / app-ios / app-windows(qt-pc)/ web(管理台),条件编译选平台 |
X-App-Version | manifest.json 的 versionName |
X-Device | uni.getDeviceInfo().deviceModel,截断到 128 |
X-OS | osName + " " + osVersion,截断到 64 |
- 只给 astral 域名的请求加——
services/http.ts先用isAstralUrl()判定,为真才调mergeClientHeaders()(刷新 Token 与主请求两处) - 对象存储预签名 PUT 绝不能带——多一个头就 403
SignatureDoesNotMatch - 空值不写头——取不到设备型号 / 系统版本时直接跳过
绕过 http.ts 直接用 uni.request 的调用点(services/source-update.uts、 uni_modules/qt-stat 上报器)必须自己调 mergeClientHeaders();反过来,第三方直链与音源加速 探测不要带这些头。应用自身升级在 services/upgrade.ts:checkAppUpdate / checkOfficialVersion(校验发行来源)/ isGithub 加速探测 / md5 校验; channel 分 stable|beta,updateType 1 弹窗、2 红点、3 无提示。
UVue 开发约束
文字样式不继承
写在父级 <view> 上的 font-size / color / font-weight 永远到不了子 <text>,文字静默回退 到 css reset(font-size: 16px、color: #000000)。每个文字属性都必须写在 <text> 自身。
| 约束 | 说明 |
|---|---|
| 选择器 | 不要后代 / 子 / 兄弟 / 复杂组合选择器;状态 class 直接绑到目标节点 |
| 宽度 | 不要 max-width: 100%;脚本算出最终宽度,内联绑 width: Npx;组件尺寸用 rpx |
| 合成效果 | linear-gradient、backdrop-filter、CSS 变量在 Vapor 下不可用 |
box-shadow | Android 上会点击穿透 / 命中丢失,层级改用 border + surface 色差 |
aria-* | 内置元素上会被拒绝并丢弃,当前没有无障碍标签 API |
| SCSS 变量 | uni.scss 的 $变量只在 uni.scss 内有效,页面 / 组件 <style> 里写会报 property value $x is not valid for color 并静默丢弃。随主题变化的值走 theme.ts + :class |
| 页面外壳 | 根节点必须是 qt-page-frame,沉浸式媒体页可传 :full-width="true";业务页面不要再写不透明背景色,否则会盖住皮肤背景和壁纸 |
| 宽屏 | 默认断点 720px,但模拟器经高 DPI 缩放后 windowWidth 可能只有 400 左右,所以 isWide 也看横屏;响应式组件统一用 uni_modules/qt-ui/composables/use-qt-layout.ts |
新增一个页面
建目录
pages/<name>/index.uvue,根节点用qt-page-frame:vue<template> <qt-page-frame :max-width="960"><view class="page">…</view></qt-page-frame> </template> <script setup lang="ts"> import QtPageFrame from "@/uni_modules/qt-ui/components/qt-page-frame/qt-page-frame.uvue"; import { useQtLayout } from "@/uni_modules/qt-ui/composables/use-qt-layout"; const layout = useQtLayout(720); </script>在
pages.json的pages数组登记,style给navigationBarTitleText;沉浸式页面用"navigationStyle": "custom",并用qtStatusBarPx()给内容让位。需要新图标就去
services/icons.ts加定义并确保有人引用——只定义不引用会被npm run check判失败。UI 组件一律放uni_modules/qt-ui/components/<name>/<name>.uvue。跑
npm run check,再做一次真机 HBuilderX 构建,紧凑与宽屏两种视口都看一遍。
音源包引擎
Android 端在 qt-js-engine(系统 WebView 的 V8)里执行音源包,契约版本 HOST_API_VERSION = 1, 包侧 bundleInfo() 报回的版本对不上就拒绝装载。宿主通过 prelude 注入 __qtHost.request,包里 所有网络请求都经宿主转发,包侧拿不到 fetch / XMLHttpRequest / WebSocket(已收缴,调用即 抛错)。
- JS→原生的通信是轮询队列:JS 把待办推进队列,原生每 20ms 拉一次。
evaluateJavascript拿 不到 Promise 的值也不回传异常,所以只求值同步版,错误靠信封回传;进出引擎的字符串强制 ASCII,出引擎的中文在 UTS 侧还原 - 包体与状态落在
filesDir/source-bundle/(state.jsonschema 3 +install/<packId>/),不放 uni storage——包体约 2MB,storage 是给小数据的 - 官方包身份靠 ed25519 尾部签名块(
__QT_SIGN__)+ 内置公钥硬校验,与安装渠道无关 - 更新发现走「各包自述
updateUrl探测 + astral manifest」两条路,按包独立节流 4 小时;只提 示不安装:确认后下载 → 校验头 → 替换 → 生效,失败回滚.prev - 播放包里内含 LX 自定义源脚本宿主(实现在
qt-sources,两端共用), 符合该协议的脚本无需改动即可装载,安卓侧与 Windows 侧跑同一份脚本、行为一致
启动时 App.uvue 的 onLaunch 先 applyLegacyInsets()(三星 One UI 首屏竞态)、 refreshQtLayout(720)、恢复各 store,再 prewarmSourceEngine() 装包(失败静默),3 秒后探测更新。
性能教训:UTS 里不要逐字符处理字符串
UTS 把 number 装箱成 kotlin.Number,逐字符循环实测 ~3.3 µs / 字符;String.replace(RegExp, replacer) 更糟——它落到 kotlin.text.Regex.replace,每次命中跨 UTS 调一次闭包(150–370 µs), 405 KB 要 20.5 s。终方案「单引号字面量 + search() 空扫短路」实测 4 ms(原 1238 ms),首屏中位 4915 ms → 1249 ms。
常见问题
改了文件 App 就冷重启——热重载对本项目表现为设备端冷启动,正常。
原生插件改动不生效——必须重新打自定义基座 / 云端打包。
下一步
- 音源包怎么装、怎么签名 → 音源包机制
- 想自己写一个播放包 → 音源包作者指南
- 桌面端怎么构建 → qt-pc(Windows)
- 连不上、装不上、取不到链 → 问题答疑