uni-app、UTS 插件与原生壳工程:创建流程、目录结构与引用链路
uni-app 项目里同时存在三种工程:跑业务代码的 uni-app 前端工程、写原生能力的 UTS 插件(uni_modules)、以及承接离线打包的 原生壳工程。三者之间靠目录约定和编译产物对接,不靠人工搬运。
下面按创建 → 结构 → 配置 → 引用链路的顺序整理一遍。
1. 三个工程的职责边界
┌──────────────────────────────┐
│ uni-app 前端工程(Node/Vite)│ Vue3 + TS 业务代码、pages.json、manifest.json
│ src/ index.html vite.config│
└───────────┬──────────────────┘
│ uni build -p app-android / app-ios
▼
┌──────────────────────────────┐
│ dist/build/app(离线资源包) │ app-service.js、pages/、static/、uni_modules/
└───────────┬──────────────────┘
│ 复制资源 + 同步 UTS 编译产物
▼
┌──────────────────────────────┐ ┌──────────────────────────┐
│ Android 壳(Gradle 多模块) │ │ iOS 壳(CocoaPods) │
│ app/ + 每个 UTS 插件一个模块│ │ Podfile + UTSPlugins/ │
└──────────────────────────────┘ └──────────────────────────┘
职责划分:
| 工程 | 负责 | 不负责 |
|---|---|---|
| uni-app 前端工程 | 页面、路由、状态、请求、uni_modules 源码 |
原生编译、签名、上架产物 |
| UTS 插件 | 用 UTS/Kotlin/Swift 实现原生 API 与组件 | 应用清单、启动页、权限合并 |
| 原生壳工程 | 应用清单、签名、SDK/AAR/Framework、打包出 APK/AAB/IPA | 业务页面 |
前端工程和壳工程之间只有两份契约:离线资源目录结构(assets/apps/<APPID>/www 或 Pandora/apps/<APPID>/www)和 UTS 插件编译产物(Kotlin 源码 / Swift 源码 + config.json)。
2. 创建 uni-app 工程
2.1 CLI 创建
Vue3 + Vite 模板(需要 Node 18+ / 20+):
# JavaScript
npx degit dcloudio/uni-preset-vue#vite my-vue3-project
# TypeScript
npx degit dcloudio/uni-preset-vue#vite-ts my-vue3-project
# alpha 版模板
npx degit dcloudio/uni-preset-vue#vite-alpha my-vue3-project
Vue2 模板(需要先 npm i -g @vue/cli):
vue create -p dcloudio/uni-preset-vue my-project
vue create -p dcloudio/uni-preset-vue#alpha my-alpha-project
模板仓库在 GitHub,网络不通时可直接下载 uni-preset-vue 的 vite.zip / vite-ts.zip 归档。
模板项目名即 package.json 的 name,默认是 uni-preset-vue,建议改名以免多工程混用 pnpm store 时产生误解。
2.2 编译器版本管理
CLI 工程的编译器装在项目 node_modules 下,不会跟随 HBuilderX 升级,需要显式更新:
npx @dcloudio/uvm@latest # 最新正式版
npx @dcloudio/uvm@latest alpha # 最新 alpha
npx @dcloudio/uvm@latest 3.2.12.20211029 # 指定版本
uvm 只更新 uni-app 编译器主依赖(@dcloudio/*),不会同步新增的 scripts,需要对照新模板手动补。
@dcloudio/* 的版本号形如 3.0.0-5020620260917001,后缀是 HBuilderX 编译器的构建号。多端依赖(uni-app-plus、uni-mp-weixin、uni-h5、uni-app-harmony…)必须保持同一版本,否则会出现运行时 __uniConfig 结构不匹配。
2.3 CLI 工程与 HBuilderX 可视化工程的差别
| 维度 | CLI 工程 | HBuilderX 可视化工程 |
|---|---|---|
| 代码位置 | src/ |
项目根目录 |
| 编译器位置 | 项目 node_modules |
HBuilderX 安装目录 plugin/ |
| 产物目录 | dist/dev、dist/build |
unpackage |
| d.ts 提示 | 模板自带 shims-uni.d.ts + @dcloudio/types |
HBuilderX 内置提示库 |
| 运行 App / uniCloud | 仍需 HBuilderX | HBuilderX |
| 其他 IDE 支持 | VSCode / WebStorm 直接可用 | 需先 npm init + npm i @dcloudio/types -D |
CLI 工程也可以整个拖进 HBuilderX;此时编译走项目内编译器。若只拖 src 目录进去,则走 HBuilderX 内置编译器。
2.4 目录与关键文件
my-project/
├── index.html H5 模板,注入 viewport 与 /src/main.ts
├── package.json scripts + @dcloudio/* 依赖
├── vite.config.js Vite/Uni 编译配置(可改 .ts)
├── tsconfig.json TS 编译选项与路径别名
├── shims-uni.d.ts uni-app 生命周期钩子的类型增强
├── uni.scss? 全局 SCSS 变量(HBuilderX 模板在根,CLI 模板在 src)
├── src/
│ ├── main.ts createSSRApp 入口,注册 Pinia/i18n
│ ├── App.vue onLaunch / onShow / onHide 等应用级生命周期
│ ├── pages.json 路由、导航栏、tabBar、分包、easycom
│ ├── manifest.json appid、版本号、权限、SDK 配置、各端专属配置
│ ├── uni.scss 内置样式变量
│ ├── static/ 原样复制到产物(图片、字体、tab 图标)
│ ├── pages/ 业务页面
│ └── uni_modules/ UTS 插件与 uni_modules 包
├── hybrid/ App 端本地 html(web-view 资源,需构建插件发射)
└── dist/ 编译产物(gitignore)
文件职责细节:
| 文件 | 作用 | 常见坑 |
|---|---|---|
main.ts |
导出 createApp(),返回 { app } |
必须用 createSSRApp,不能 createApp |
App.vue |
应用级生命周期与全局样式 | 页面级逻辑不要写在这里 |
pages.json |
路由与窗口样式 | 只声明页面会打进产物;分包私有模块不能跨包 import |
manifest.json |
appid、versionName/versionCode、权限、app-plus.distribute |
appid 必须与壳工程 dcloud_control.xml 一致 |
static/ |
原样拷贝 | 未引用文件也会进包,注意体积 |
uni_modules/ |
插件源码 | 不含 package.json 的目录不会被识别为插件 |
main.ts 的最小形态:
import { createSSRApp, type App as VueApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
export function createApp(): { app: VueApp } {
const app = createSSRApp(App);
app.use(createPinia());
return { app };
}
2.5 条件编译
uni-app 用注释做条件编译,编译期剔除代码:
// #ifdef APP-PLUS
import { somethingNative } from '@/services/native';
// #endif
// #ifndef MP-WEIXIN
const onlyNotWeixin = true;
// #endif
pages.json、manifest.json、uni.scss、static/ 都支持同样的条件编译写法。平台标识:APP-PLUS、APP-ANDROID、APP-IOS、H5、MP-WEIXIN、MP-ALIPAY……
2.6 运行与产物
npm run dev:mp-weixin # 开发:产物在 dist/dev/mp-weixin
npm run dev:h5 # 开发:H5 走内存服务
npm run build:mp-weixin # 生产:dist/build/mp-weixin
npm run build:app-plus # 生产:App 资源包
新版 CLI 也接受 app-android / app-ios / app-harmony 作为 -p 取值,配合 --mode 指定环境:
uni build -p app-android --mode debug
uni build -p app-ios --mode release
dev 与 build 的差别:dev 带 sourcemap、NODE_ENV=development;build 压缩、NODE_ENV=production。App 端 dev 只能在 HBuilderX 里跑真机调试;纯 CLI 只能 build 出资源包再交给壳工程。
3. HBuilderX CLI
3.1 位置与基础命令
CLI 是独立于可视化界面的可执行文件:
- Windows:安装目录根下
cli.exe - macOS:
/Applications/HBuilderX.app/Contents/MacOS/cli(Alpha 版为HBuilderX-Alpha.app) - Linux:安装目录根下
cli(2025 年初推出,官方只在 Ubuntu 验证)
cli ver # 版本
cli open # 启动 HBuilderX(多数命令要求先启动)
cli app quit # 关闭
cli --help / cli help # 帮助
cli user login # 登录插件作者账号
把 CLI 加进 PATH 后可在任意目录调用;CI 场景用 Linux CLI,配合 cli user login 完成授权。
3.2 发行与项目命令
# 生成本地打包 App 资源(离线包)
cli publish --platform APP --type appResource --project my-project
cli publish app-android --type appResource --project my-project # 4.67-alpha+
cli publish app-ios --type appResource --project my-project
# 云打包
cli pack --help
appResource 就是离线打包需要的 www 资源,导出位置随项目类型不同(HBuilderX 工程在 unpackage/resource/,CLI 工程在 dist/)。
3.3 uni_modules 插件命令
这是最容易忘的一组命令,覆盖插件的下载、创建、发布全流程。
cli uni_modules --help
cli uni_modules --publish help # 单命令详细用法
| 命令 | 用途 | 登录 | 最低版本 |
|---|---|---|---|
--create |
新建 uni_modules 插件骨架 | 否 | 5.31+ |
--publish |
发布插件到市场 | 是 | 5.31+ |
--updateInfo |
改已发布插件的基本信息(不产生新版本) | 是 | 5.31+ |
--install |
安装插件 uni_modules.dependencies 声明的依赖 |
是 | 5.31+ |
--validate |
发布前校验 | 是 | 5.31+ |
--download |
从市场下载插件到项目 | 是 | 5.0+ |
--upgrade |
更新项目中的插件 | 是 | 5.0+ |
--list |
列出项目已安装插件 | 否 | 5.0+ |
下载插件(源码授权版用 --extType source,普通加密版用 encrypt):
cli uni_modules --download yourname-socket \
--project /abs/path/my-project \
--extType source
# 指定版本、覆盖已有文件、输出 JSON 供 CI 解析
cli uni_modules --download yourname-socket \
--project /abs/path/my-project \
--version 1.1.0 --force --json
其余常用形态:
# 新建 UTS API 插件骨架(会生成 package.json / readme.md / changelog.md / utssdk/)
cli uni_modules --create yourname-demo --template uts --project /abs/path/my-project
# 只看会创建哪些文件
cli uni_modules --create yourname-demo --template uts --project /abs/path/my-project --dryRun
# 列出项目内插件
cli uni_modules --list --project /abs/path/my-project --json
# 更新与依赖安装
cli uni_modules --upgrade yourname-demo --project /abs/path/my-project
cli uni_modules --install yourname-demo --project /abs/path/my-project --force
--create 的模板类型:component-vue、component-mp、sdk-js、uts、component-uts、uts-vue-component(仅 uni-app x)、uniapp-template-page、unicloud-*、uasm(仅 uni-app x)。
要点:
- 除
--list外都要--project,且一次只能执行一个命令。 - 插件 ID 格式是「作者ID-名称」,只允许英文、数字、下划线、连字符;普通账号不能用
uni、dcloud、uts前缀。 --install只处理 uni_modules 依赖,不执行npm install。- 闭环的发布流程:
--create→ 开发 →--validate→--publish;changelog.md需含当前版本内容,或用--changeLog传入。
3.4 编译 UTS
HBuilderX 5.21+ 提供 CLI 编译,便于在不开 IDE 的情况下验证 UTS 语法:
cli compile app-android --project my-project --uni_module yourname-socket
cli compile app-android --project my-project --file src/uni_modules/yourname-socket/utssdk/app-android/index.uts
cli compile app-ios --project my-project --uni_module yourname-socket
--file 与 --uni_module 互斥;--uni_module 可以传模块名简写,CLI 会自动拼成项目源码根下的 uni_modules/<名>。
3.5 Linux 下编译 UTS 的环境依赖
只有当 UTS 插件依赖第三方 Gradle 时,才需要在 Linux 上准备 Android 工具链:JDK 17、Gradle、Android SDK(cmdline-tools + platform-tools + platforms + build-tools)。
HBuilderX 不读 shell 环境变量,必须写入自身配置:
cli config set --key uts-development-android.gradleHome --value /opt/gradle/gradle-8.7/bin/gradle
cli config set --key uts-development-android.sdkDir --value /home/user/android-sdk
等价于直接编辑 ~/.local/share/HBuilder\ X/user/settings.json。
4. UTS 插件
4.1 UTS 与编译目标
UTS(uni type script)语法接近 TypeScript,编译到各平台原生语言:
| 目标平台 | 编译结果 |
|---|---|
| Web / 小程序 | JavaScript |
| Android | Kotlin |
| iOS | Swift(HBuilderX 3.6.7+) |
| HarmonyOS | ArkTS(HBuilderX 4.22+) |
所以 UTS 调原生代码不是跨语言 FFI,而是同一编译单元内的函数调用,没有序列化开销。这也是原生混编能直接 import 同目录 Kotlin/Swift 文件的原因。
UTS 插件分两类:API 插件(在 script 里调用)和组件插件(在 template 里当组件用)。二者都是 uni_modules 包。
4.2 uni_modules 规范
uni_modules 是 uni-app 的包管理方案:既能装 npm 库,也能装 Android 仓储、iOS CocoaPods、鸿蒙 ohpm,还能装付费加密插件。目录结构直接照抄 uni-app 项目结构:
uni_modules/
└── yourname-demo/ 插件 ID 目录
├── package.json 必需:清单
├── readme.md 插件文档
├── changelog.md 更新日志
├── components/ easycom 组件
├── pages/ 插件页面(配合 pages_init.json 注册)
├── static/ 静态资源
├── hybrid/ 本地 html
├── utssdk/ UTS 插件主体
├── uniCloud/ 云函数、公共模块
└── license.md
package.json 关键字段:
{
"id": "yourname-demo",
"displayName": "示例插件",
"version": "1.0.0",
"description": "……",
"keywords": ["demo"],
"engines": {
"HBuilderX": "^4.25",
"uni-app": "^4.25",
"uni-app-x": "^4.25"
},
"dcloudext": {
"type": "uts",
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "网络权限"
}
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"client": {
"uni-app": {
"app": {
"android": { "minVersion": "21" },
"ios": { "minVersion": "15" }
}
}
}
}
}
}
package.json 不支持 pages.json、App.vue、main.ts、manifest.json、uni.scss 这类「使用者必须改」的文件——需要用户改的内容只能写进 readme.md。插件目录不支持嵌套 uni_modules。
HBuilderX 4.71 起平台兼容性拆成 uni-app / uni-app-x 两个维度,取值 √ 支持、x 不支持、- 未确定。
4.3 utssdk 目录结构
yourname-demo/
├── package.json
└── utssdk/
├── interface.uts 必需:对外暴露的 API 类型声明
├── unierror.uts 可选:错误码与错误类
├── index.uts 可选:跨平台入口
├── app-android/
│ ├── index.uts 平台入口实现
│ ├── config.json 原生层配置
│ ├── AndroidManifest.xml 可选:清单片段
│ ├── assets/ res/ libs/ 可选:原生资源与三方库
│ └── socket/*.kt 原生混编 Kotlin
├── app-ios/
│ ├── index.uts
│ ├── config.json
│ ├── Info.plist / UTS.entitlements 可选
│ ├── Frameworks/ Libs/ Resources/ 可选
│ └── socket/*.swift
├── app-harmony/
├── web/ mp-weixin/ mp-alipay/ ... 可选
入口解析顺序:先找平台目录的 index.uts,找不到再退回 utssdk/index.uts。所以只做 Android 插件时可以完全没有根 index.uts。
interface.uts 与 index.uts 是声明与实现的关系:类型写在 interface.uts 里,HBuilderX 才能给使用者补全。
前端引用只支持到插件根目录:
// 正确
import { createTcpSocket } from '@/uni_modules/yourname-demo';
// 错误:不能直接 import 插件内部文件
import { createTcpSocket } from '@/uni_modules/yourname-demo/utssdk/app-android/index.uts';
加密插件的内部文件不能单独导出。
4.4 Android config.json
{
"abis": ["armeabi-v7a", "arm64-v8a"],
"minSdkVersion": 21,
"dependencies": [
"androidx.core:core-ktx:1.6.0",
{ "id": "com.xxx.richtext:richtext", "source": "implementation 'com.xxx.richtext:richtext:3.0.7'" }
],
"project": {
"plugins": ["com.huawei.agconnect"],
"dependencies": ["com.huawei.agconnect:agcp:1.6.0.300"]
},
"components": [
{ "name": "zl-text", "class": "uts.sdk.modules.zlText.ZlTextComponent" }
],
"hooksClass": "uts.sdk.modules.zlText.ZlTextHook"
}
| 字段 | 作用 | 离线壳里落点 |
|---|---|---|
abis |
支持的 CPU 架构 | 库模块与主模块的 ndk.abiFilters |
minSdkVersion |
最低 Android 版本 | 库模块 minSdk |
dependencies |
仓储依赖 | 中文档字符串拼 implementation;JSON 形式取其 source |
project.plugins |
Gradle 插件 | 库模块 plugins |
project.dependencies |
插件 classpath | 根工程 buildscript.dependencies |
components |
UTS 组件注册 | uni-app:主模块 dcloud_uniplugins.json;uni-app x:buildConfigField UTSRegisterComponents |
hooksClass |
应用生命周期钩子 | buildConfigField UTSHooksClassArray |
libs/、assets/、res/、AndroidManifest.xml 都要原样搬到对应库模块。若 AndroidManifest.xml 带 package 属性,导入库模块时要把它的值改成 namespace。
4.5 iOS config.json
{
"deploymentTarget": "15",
"frameworks": ["StoreKit"],
"dependencies-pods": [
{ "name": "CocoaAsyncSocket", "version": "7.6.5" }
],
"plists": {},
"hooksClass": "",
"providers": [],
"components": []
}
| 字段 | 落点 |
|---|---|
frameworks |
插件工程/主工程的系统库链接 |
deploymentTarget |
插件 pod 的 s.platform |
dependencies-pods |
插件 Podfile 或 podspec 的 s.dependency |
plists |
主工程 Info.plist 的键值 |
hooksClass / providers / components |
插件 uts-config.json 的 hooksClasses / providers / components |
4.6 原生混编
HBuilderX 4.25+ 支持在 app-android / app-ios 下直接放 Kotlin / Swift / Java / ArkTS / JS 文件,由 UTS 直接调用。
// app-android/MemoryInfoNative.kt
package uts.sdk.modules.uniMemoryInfo // 建议与 UTS 默认包名一致
import io.dcloud.uts.UTSAndroid
import io.dcloud.uts.console
object MemoryInfoNative {
fun getMemInfoKotlin(): Array<Number> { /* ... */ }
}
// app-android/index.uts
import { MemoryInfoNative } from 'uts.sdk.modules.uniMemoryInfo';
默认包名规则:
uni_modules/<插件ID> → uts.sdk.modules.<插件ID 转小驼峰>
utssdk/<插件ID> → uts.sdk.<插件ID 转小驼峰>
包名与默认值不一致时,按普通 Kotlin 包路径 import 即可。
原生侧也能反调 UTS 内置对象,只要 import io.dcloud.uts.*:
| UTS 类型 | 编译后 Kotlin 类型 |
|---|---|
Array |
io.dcloud.uts.UTSArray |
Map / Set |
io.dcloud.uts.Map / io.dcloud.uts.Set |
UTSJSONObject |
io.dcloud.uts.UTSJSONObject |
JSON |
io.dcloud.uts.JSON |
Date / Math |
io.dcloud.uts.Date / io.dcloud.uts.Math |
Promise |
io.dcloud.uts.UTSPromise |
RegExp |
io.dcloud.uts.UTSRegExp |
Error |
io.dcloud.uts.UTSError |
console |
io.dcloud.uts.console |
限制与差异:
- Kotlin / Swift 混编代码可直接真机运行,Java 需要自定义基座。
- UTS 允许在需要的地方直接用平台类型,例如
Int、MutableList<String>、KotlinArray<string>(import KotlinArray from 'kotlin.Array')。 - 线程环境:uni-app 端 UTS 代码默认跑在
WeexJSBridgeThread;操作 UI 或做耗时任务时显式切线程:
UTSAndroid.getDispatcher('io').async(function (_) {
// 耗时任务
}, null)
UTSAndroid.getDispatcher('main').async(function (_) {
// 操作界面
}, null)
5. Android 壳工程
5.1 离线 SDK 目录
HBuilder-Integrate-AS/ 集成 uni-app 的最简示例
HBuilder-Hello/ 离线打包演示应用
UniPlugin-Hello-AS/ 原生插件开发示例
SDK/ 库文件(Libs / Bundles / assets / inc)
Feature-Android.xls 各扩展 Feature 与配置对照
两种起手方式:
- 方式一,新建工程:Android Studio 建空工程,最低 API 21+。早期文档强调「离线 SDK 不支持 Kotlin」,现在壳工程用 Kotlin DSL(
build.gradle.kts)和 Kotlin 已可用。 - 方式二,导入官方示例:直接导入
HBuilder-Integrate-AS,跑通simpleDemo后改成自己的包名与清单。
版本约束:HBuilderX 4.81 起为适配 16KB page size,需要 compileSdk 36、buildToolsVersion 36.0.0、Gradle 8.14.3、AGP 8.12.0。
5.2 多模块布局
离线壳建议按「主模块 + 每个 UTS 插件一个 library module」组织,因为每个 UTS 插件在 Android 侧就对应一个 lib module:
android/
├── settings.gradle.kts include(":app") + 每个插件模块
├── build.gradle.kts 插件 classpath、clean 任务
├── gradle/libs.versions.toml 版本目录(AGP、androidx、firebase…)
├── gradle.properties android.useAndroidX / enableJetifier
├── keystore/ 签名文件
├── app/
│ ├── build.gradle.kts 应用配置、签名、依赖
│ ├── libs/*.aar 离线 SDK 与扩展 AAR
│ └── src/main/
│ ├── AndroidManifest.xml
│ ├── assets/ data/ 与 apps/<APPID>/www
│ ├── res/ icon、splash、strings、styles
│ └── java/ 自定义 Application、服务覆盖实现
└── <plugin-module>/ 每个 UTS 插件一个模块:build.gradle.kts + src/main/java
settings.gradle.kts 里要显式 include 每个插件模块,并在主模块 dependencies 里 implementation(project(":plugin-module"))。
5.3 基础库与依赖
把 SDK 的 AAR 拷进 app/libs:
| AAR | 用途 |
|---|---|
lib.5plus.base-release.aar |
5+ 运行时基础库 |
uniapp-v8-release.aar |
V8 JS 引擎 |
android-gif-drawable-*.aar |
图片动图支持 |
oaid_sdk_*.aar |
设备标识 |
breakpad-build-release.aar |
崩溃捕获(HBuilderX 3.5.0+) |
install-apk-release.aar |
应用内安装(HBuilderX 3.8.7+,上架 Google Play 不能带) |
utsplugin-release.aar |
UTS 运行时(含 UTS 插件时必备,HBuilderX 4.18+) |
主模块依赖(dependencies):
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar", "*.jar"))))
implementation(libs.appcompat)
implementation(libs.localbroadcastmanager)
implementation(libs.androidx.core)
implementation(libs.androidx.fragment)
implementation(libs.androidx.recyclerview)
implementation(libs.fresco) // 3.4.0+,16KB page size 合规
implementation(libs.fresco.animated.gif)
implementation(libs.glide)
implementation(libs.fastjson)
implementation(libs.androidx.webkit)
// UTS 运行时依赖(utsplugin-release.aar 需要)
implementation("com.squareup.okhttp3:okhttp:3.12.12")
implementation("androidx.core:core-ktx:1.6.0")
implementation("org.jetbrains.kotlin:kotlin-stdlib:2.2.0")
implementation("org.jetbrains.kotlin:kotlin-reflect:2.2.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
implementation("com.github.getActivity:XXPermissions:18.63") // UTSAndroid.requestSystemPermission
XXPermissions 只发布在 JitPack,如果仓库里用阿里云镜像优先,需要额外补 maven { url = uri("https://jitpack.io") }。
插件库模块的依赖用 compileOnly 指向主模块的 libs,避免重复打包:
dependencies {
compileOnly(fileTree(mapOf("dir" to "../app/libs", "include" to listOf("*.aar", "*.jar"))))
compileOnly(libs.fastjson)
compileOnly("org.jetbrains.kotlin:kotlin-reflect:2.2.0")
compileOnly("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
}
另外两个必须的构建配置:
android {
androidResources {
additionalParameters += "--auto-add-overlay"
ignoreAssetsPattern = "!.svn:!.git:.*:!CVS:!thumbs.db:!picasa.ini:!*.scc:*~"
}
packaging {
jniLibs { useLegacyPackaging = true } // targetSdk 34+ 时需要
}
}
5.4 AndroidManifest 关键节点
<application
android:name="com.example.app.MyApplication"
android:icon="@drawable/icon"
android:label="@string/app_name"
tools:replace="android:name">
<meta-data android:name="dcloud_appkey" android:value="申请的 AppKey" />
</application>
- 启动 Activity 必须是
io.dcloud.PandoraEntry(带 LAUNCHER intent-filter),并删掉 Android Studio 默认生成的MainActivity。 io.dcloud.PandoraEntryActivity的configChanges要包含smallestScreenSize|screenLayout|screenSize(折叠屏)与uiMode(暗黑模式)。android:name指向自定义 Application 时必须继承io.dcloud.application.DCloudApplication,否则 SDK 逻辑不启动。- Appkey 从 HBuilderX 3.1.10 起必需。
- 应用清单里的权限要与
manifest.json的app-plus.distribute.android.permissions对齐;需要移除离线 AAR 传递进来的权限时用tools:node="remove"。
5.5 assets 资源
app/src/main/assets/
├── data/
│ ├── dcloud_control.xml 声明 appid,必须与目录名、manifest.json 一致
│ ├── dcloud_properties.xml Feature / service 注册表
│ └── dcloud_error.html 错误页
└── apps/
└── __UNI__XXXXXXX/
└── www/ 离线资源包(app-service.js、pages/、static/、uni_modules/)
dcloud_control.xml:
<hbuilder>
<apps>
<app appid="__UNI__XXXXXXX" appver="" />
</apps>
</hbuilder>
自定义基座调试时在根节点加 debug="true" 和 syncDebug="true",正式包去掉。
dcloud_properties.xml 里 features 决定启用哪些内置 Feature(Barcode、Camera、Payment、OAuth…),services 决定 Boot 实现;UTS 组件的注册不在这里,而在 assets/dcloud_uniplugins.json:
{
"nativePlugins": [
{
"plugins": [
{ "type": "component", "name": "zl-text", "class": "uts.sdk.modules.zlText.ZlTextComponent" }
]
}
]
}
6. iOS 壳工程
6.1 目录与 Pod
ios/
├── Podfile 主工程依赖:本地 pod 'uniapp' + 各 UTS 插件 pod
├── uniapp.podspec uni-app 离线 SDK 的本地 podspec(Core + 各 subspec)
├── App/ 应用源码:AppDelegate、ViewController、Info.plist、entitlements、Pandora/apps
├── SDK/ 离线 SDK:Libs、Bundles、inc、ExtApiSrc
├── UTSPlugins/ UTS 插件源码(每个插件一个子目录)
└── scripts/ Pod 辅助脚本(生成 pod、写 plist/entitlements)
Podfile 用本地 podspec + subspec 声明需要的模块:
platform :ios, '15.0'
use_frameworks! :linkage => :static
project 'App.xcodeproj'
uniapp_subspecs = [
'Core', 'Barcode', 'Audio', 'CameraGallery', 'File', 'Orientation',
'XMLHttpRequest', 'Zip', 'Canvas', 'Sqlite', 'Log',
'Oauth', 'Oauth-Google', 'Oauth-Apple', 'Push-FCM'
]
target 'App' do
pod 'uniapp', :path => '.', :subspecs => uniapp_subspecs
uts_plugins.each do |plugin|
pod plugin[:pod_name], :path => File.join('UTSPlugins', '.generated', plugin[:pod_name])
end
end
post_install do |installer|
# 把权限文案、feature.plist 命令、插件 plist/entitlements 合并进主工程
end
subspec 的取舍直接决定二进制里带了哪些原生能力,裁剪与 dcloud_properties.xml 的 Feature 列表要保持一致。
6.2 UTSPlugins 与 podspec
iOS 侧同样是每个 UTS 插件一个产物,但产物是 pod:UTSPlugins/<插件名>/app-ios/ 下放导出的源码与 config.json,Podfile 扫描该目录并为每个插件生成一个本地 pod。
UTSPlugins/
└── yourname-demo/
└── app-ios/
├── config.json
├── Info.plist 需要合并到主工程 Info.plist
├── UTS.entitlements 需要合并到主工程 entitlements
├── src/ 编译生成的 Swift 源码(含桥接文件)
├── Frameworks/ Libs/ 三方依赖
└── Resources/ 资源(合并进主 Bundle)
生成的 podspec 大致做四件事:
s.source_files = ['src/**/*.{h,m,mm,swift,c,cc,cpp}',
'DCloudUTSConfigXXX.{h,m}', 'UTSCPPXXX.{h,mm}']
s.resources = ['Resources/**/*', 'DCloudUTSConfigXXX-config.json', 'PrivacyInfo.xcprivacy']
s.dependency 'uniapp/Core'
DCloudUTSConfig / UTSCPP 这类支撑文件在每个插件 pod 里都要有一份,且类名和 config.json 文件名要按插件加后缀区分,否则多个 pod 之间符号冲突。插件 pod 的产物按官方文档是编译成 .framework/.xcframework 后 Embed & Sign;使用本地 pod 方案时由 CocoaPods 处理嵌入。
6.3 配置合并
iOS 侧不像 Android 有 dcloud_properties.xml 这种集中注册表,配置散落在三处,靠脚本在 pod install 的 post_install 阶段合并:
| 来源 | 目标 | 内容 |
|---|---|---|
| subspec 定义 | 主工程 Info.plist |
权限文案(相机、相册、麦克风、定位…) |
| subspec 定义 | PandoraApi.bundle/feature.plist |
Feature 映射,如 Oauth-Apple → PGAppleOauth |
插件 config.json 的 plists |
主工程 Info.plist |
插件需要的键值(如客户端 ID) |
插件 UTS.entitlements |
主工程 entitlements | 能力(如关联域名、无线局域网信息) |
插件 config.json 的 provider/components/hooksClass |
插件 uts-config.json |
UTS 运行时注册 |
config.json 里若出现 {$PLACEHOLDER} 形式的占位符,需要在构建时用外部配置替换,缺失时脚本应报错而不是静默保留。
7. 引用链路
7.1 安装插件
三种来源:
| 方式 | 命令/操作 | 结果 |
|---|---|---|
| 插件市场(CLI) | cli uni_modules --download <id> --project <abs> --extType source |
下载到 uni_modules/<id>,同时下载其依赖 |
| 插件市场(IDE) | 详情页「使用 HBuilderX 导入插件」 | 同上,并初始化依赖 |
| 自有源码 | 直接把插件目录放进 uni_modules/ |
与市场下载结构一致 |
--extType 只有两种:source 源码授权版、encrypt 普通授权版。加密付费插件不能走原生离线 SDK 打包,需要源码授权版。
7.2 前端如何引用
// src/features/xxx.ts
import { createTcpSocket } from '@/uni_modules/yourname-demo';
CLI 工程需要在 vite.config.js 与 tsconfig.json 里同时声明别名,否则会出现「TS 能解析、Vite 解析不到」或反之:
// vite.config.js
resolve: {
alias: [
{ find: '@/uni_modules', replacement: fileURLToPath(new URL('./src/uni_modules', import.meta.url)) },
{ find: '@', replacement: fileURLToPath(new URL('./src', import.meta.url)) },
],
}
{
"compilerOptions": {
"paths": {
"@/uni_modules/*": ["./src/uni_modules/*"],
"@/*": ["./src/*"]
}
}
}
单元测试环境(jsdom)没有原生运行时,通常用别名把插件指向 mock:
// vitest.config.ts
resolve: {
alias: {
'@/uni_modules/yourname-demo': fileURLToPath(new URL('./src/test/mocks/yourname-demo.ts', import.meta.url)),
},
}
组件类插件走 easycom,无需 import,规则来自插件 components/ 目录的命名。
调用点用条件编译包住,避免 H5/小程序端解析到原生插件:
// #ifdef APP-PLUS
import { createTcpSocket } from '@/uni_modules/yourname-demo';
// #endif
7.3 编译期:UTS 源码变成原生源码
uni build 期间,UTS 文件被编译成目标平台语言:
- 各平台入口
utssdk/app-<platform>/index.uts - 同目录的原生混编文件(
.kt/.swift)原样参与编译 - 中间产物落在
dist/build/.tsc/app-<platform>/(TS 转换)与dist/build/.uvue/app-<platform>/(uvue 转换),可用来排错
最终产物分两处,职责不同:
dist/build/app/uni_modules/<插件ID>/
└── utssdk/app-android/
├── config.json 插件原生层配置(原样带出)
└── src/ 编译生成的 Kotlin,含 UTS 桥接类与原生混编文件
├── index.kt ← index.uts 编译结果
└── socket/*.kt ← 混编 Kotlin 原样复制
dist/build/hx/app/app-android/uts/uni_modules/<插件ID>/
├── manifest.json 参与编译的文件 → md5 清单,含 compilerVersion
└── dependencies.json 插件声明的仓储 / Pod 依赖
即:app/uni_modules 是给原生工程用的源码,hx/.../uts 是增量编译用的清单。
两处的更新方式不同,这点在排查「改了插件没生效」时很关键:
| 位置 | 更新行为 | 后果 |
|---|---|---|
dist/build/app/uni_modules/<插件ID>/ |
每次 uni build 重建,只保留本次目标平台 |
上一次 app-ios 的产物会被替换掉,跨平台需分别构建 |
dist/build/hx/app/app-<平台>/uts/ |
按平台各留一份,只刷新本次构建的平台 | 另一平台的 manifest.json 仍是旧构建时间戳,不要拿它判断本次是否编译过 |
dist/build/.tsc、dist/build/.uvue |
按平台各留一份中间转换结果 | 排错用,不参与 Gradle / CocoaPods |
manifest.json 片段:
{
"version": "1",
"env": { "vapor": false, "compilerVersion": "5.26" },
"files": {
"utssdk/app-android/index.uts": { "md5": "……" },
"utssdk/interface.uts": { "md5": "……" },
"utssdk/app-android/config.json": { "md5": "……" },
"utssdk/app-android/socket/TcpClient.kt":{ "md5": "……" }
}
}
清单里同时列出 .uts 与混编 .kt——这就是增量编译判断依据:任一文件 md5 变化都要重编该插件。
产物里同时能看到前端调用侧的桥接元数据。前端 bundle 中每个 UTS 方法都有这样一段描述:
// app-service.js 片段(已压缩)
{
moduleName, moduleType: '', main: true,
package, // 插件包名,如 uts.sdk.modules.xxx
class: ..., name: 'createUdpSocketByJs',
params: [{ name: 'options', type: 'UTSSDKModulesXxxCreateUdpSocketOptionsJSONObject' }],
return: { type: 'interface', options: 'UdpSocketTaskOptions' }
}
也就是说:JS 侧调用 import 进来的方法时,实际是通过 package + class + 方法名去查原生注册表。这解释了为什么包名必须和插件 ID 一致、以及为什么改名插件 ID 会导致运行时报「方法不存在」。
7.4 原生壳如何接收这些产物
Android(离线打包,HBuilderX 4.18+):
uni build -p app-android
├─ 离线资源 → app/src/main/assets/apps/<APPID>/www/
└─ UTS 编译产物 → <插件ID>/src/main/java/ (作为 library module 源码参与 Gradle 构建)
关键点:原生工程不会从 assets 里编译 Kotlin。编译生成的 Kotlin 必须作为 Android library module 的源码参与 Gradle 构建;同时把 config.json 的 abis / minSdkVersion / dependencies / project 同步到对应模块的 build.gradle,components / hooksClass 同步到主模块的注册项。
iOS:
uni build -p app-ios
├─ 离线资源 → Pandora/apps/<APPID>/www/
└─ uni_modules/<插件ID>/utssdk/app-ios/ → UTSPlugins/<插件ID>/app-ios/
→ pod install 生成并编译插件 pod
7.5 全链路时序
① 前端工程 ② 插件源码 ③ 原生壳
────────────────────────────────────────────────────────────────────────
src/uni_modules/<id> ──┐
│ uni build -p app-android / app-ios
▼
dist/build/app/
├── app-service.js (含 UTS 桥接元数据)
├── manifest.json
└── uni_modules/<id>/
├── utssdk/app-android/src/*.kt
└── utssdk/app-ios/src/*.swift
① 复制资源 ──────────────────────────────────────▶ assets/apps/<APPID>/www
或 Pandora/apps/<APPID>/www
② 同步插件源码 ─────────────────────────────────▶ <id>/src/main/java (Android)
或 UTSPlugins/<id>/app-ios (iOS)
▼
gradlew assembleDebug / pod install + Xcode Archive
▼
APK / AAB / IPA
8. Node / TS / Web 工程侧
UTS 插件之外,工程的主体仍是一个标准 Vite + TS 项目,配置文件各有明确职责。
8.1 package.json
{
"name": "my-project",
"scripts": {
"dev:mp-weixin": "uni -p mp-weixin",
"dev:h5": "uni -p h5",
"build:mp-weixin": "uni build -p mp-weixin",
"build:app-android": "uni build -p app-android --mode release",
"typecheck": "vue-tsc --noEmit -p tsconfig.json",
"lint": "eslint src",
"test": "vitest",
"check": "npm run lint && npm run typecheck"
},
"dependencies": {
"@dcloudio/uni-app": "3.0.0-xxxxxxxxx",
"@dcloudio/uni-app-plus": "3.0.0-xxxxxxxxx",
"@dcloudio/uni-h5": "3.0.0-xxxxxxxxx",
"@dcloudio/uni-mp-weixin": "3.0.0-xxxxxxxxx",
"vue": "^3.5.0",
"pinia": "^2.1.7"
},
"devDependencies": {
"@dcloudio/types": "^3.4.32",
"@dcloudio/uni-cli-shared": "3.0.0-xxxxxxxxx",
"@dcloudio/uni-uts-v1": "3.0.0-xxxxxxxxx",
"@dcloudio/uts-linux-x64-gnu": "3.0.0-xxxxxxxxx",
"@dcloudio/vite-plugin-uni": "3.0.0-xxxxxxxxx",
"@dcloudio/uni-stacktracey": "3.0.0-xxxxxxxxx",
"@dcloudio/uni-automator": "3.0.0-xxxxxxxxx",
"typescript": "^5.4.5",
"vite": "5.2.8",
"vue-tsc": "^2.2.8",
"sass": "^1.85.1",
"vitest": "^1.6.1"
}
}
要点:
uni命令由@dcloudio/vite-plugin-uni的bin/uni.js提供,因此必须作为直接依赖安装。@dcloudio/uni-uts-v1与平台化的@dcloudio/uts-<os>-<arch>(如uts-linux-x64-gnu)是 UTS 编译器,缺了会导致 UTS 插件编译失败。- 只有用不到的端才不装对应
@dcloudio/uni-mp-*包;装了就会去解析pages.json的平台字段。 - 每个平台都可以单独一个 script;CI 里直接跑
uni build -p <平台> --mode <环境>。 - 脚本里用
execSync('uni build …')时,uni只在node_modules/.bin里存在。直接node scripts/xxx.mjs调用会报uni: 未找到命令,必须从 npm/pnpm script 进入,或在execSync的env里补上node_modules/.bin到PATH。
8.2 vite.config.js
import { defineConfig } from 'vite';
import uni from '@dcloudio/vite-plugin-uni';
import { fileURLToPath, URL } from 'node:url';
export default defineConfig(({ command, mode }) => ({
plugins: [uni()],
resolve: {
alias: [
{ find: '@/uni_modules', replacement: fileURLToPath(new URL('./src/uni_modules', import.meta.url)) },
{ find: '@', replacement: fileURLToPath(new URL('./src', import.meta.url)) },
],
},
define: {
__APP_VERSION__: JSON.stringify('1.0.0'),
},
css: {
preprocessorOptions: {
scss: { quietDeps: true, silenceDeprecations: ['import', 'legacy-js-api'] },
},
},
server: {
host: '0.0.0.0',
port: 5173,
strictPort: true,
proxy: { '/_api': { target: 'https://api.example.com', changeOrigin: true } },
},
}));
职责分层:
| 配置 | 用途 |
|---|---|
plugins: [uni()] |
uni-app 编译器(页面扫描、条件编译、多端转换) |
resolve.alias |
路径别名,必须与 tsconfig.json 的 paths 一致 |
define |
注入编译期常量(版本号、构建平台、开关) |
server.proxy |
开发期跨域代理(H5 专用,App/小程序不走 Vite server) |
css.preprocessorOptions |
SCSS 兼容性开关,压掉第三方库的弃用告警 |
| 自定义 plugin | 发射 hybrid/ 资源、清理空 chunk、控制 manualChunks |
自定义 plugin 里可以用 process.env.UNI_PLATFORM 判断目标平台(app-android、mp-weixin、h5…),apply: 'build' 限制只在构建期生效。
8.3 tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"skipLibCheck": true,
"lib": ["ESNext", "DOM"],
"types": ["@dcloudio/types", "vitest/globals"],
"paths": {
"@/uni_modules/*": ["./src/uni_modules/*"],
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "shims-uni.d.ts"],
"exclude": ["node_modules", "dist"]
}
types里的@dcloudio/types提供uni.*API 的类型;skipLibCheck可以避开第三方 d.ts 的噪声。paths与 Vite alias 必须成对维护。- 生命周期钩子(
onLaunch、onShow、onPullDownRefresh)的类型增强放在shims-uni.d.ts:
/// <reference types='@dcloudio/types' />
import 'vue';
declare module '@vue/runtime-core' {
type Hooks = App.AppInstance & Page.PageInstance;
interface ComponentCustomOptions extends Hooks {}
}
export {};
8.4 质量工具
| 文件 | 作用 | 注意 |
|---|---|---|
eslint.config.js |
Flat config,需把 uni、App、Page、UniNamespace 声明为全局 |
Vue SFC 用 vue-eslint-parser + @typescript-eslint/parser |
.prettierrc.json |
格式化规则 | 与 ESLint 通过 eslint-config-prettier 解耦 |
.editorconfig |
编辑器层面的换行/缩进 | 团队统一用 |
vitest.config.ts |
单测 | 用独立 config,避免把 uni() 插件带进测试构建 |
.npmrc |
pnpm 相关(如 shamefully-hoist=true) |
某些老库不支持严格 node_modules 结构 |
pnpm-workspace.yaml |
allowBuilds、patchedDependencies |
需要补丁的依赖在这里声明 |
project.config.json |
微信开发者工具工程配置 | App 打包无关 |
8.5 多环境配置
uni-app CLI 传统上靠 .env.development / .env.production + import.meta.env。当需要区分的不只是环境、还要区分平台(App / 小程序不同网关)时,纯 .env 不够用,常见做法是引入一份类似 project.properties 的集中配置,由构建脚本与 vite.config.js 一起读取,再通过 define 注入:
app.release.appid=__UNI__XXXXXXX
app.release.android.packageName=com.example.app
vite.api.baseUrl.release=https://api.example.com
vite.api.baseUrl.debug=https://api-test.example.com
vite.log.network.release=false
android.native.projectDir=/abs/path/native-android
android.native.assetsPath=/abs/path/native-android/app/src/main/assets
好处是一次构建同时驱动前端产物与原生壳(版本号、包名、资源目录),不需要在两个仓库里各改一遍版本。
9. 对照检查清单
排错时按这个顺序看,基本能定位到是哪一段断了:
| 现象 | 先查 |
|---|---|
| 报「方法不存在」/ 插件调用无响应 | 插件 ID 与 <APPID>、Kotlin 包名 uts.sdk.modules.<camelCaseId> 是否一致 |
前端 import 解析失败 |
vite.config.js alias 与 tsconfig.json paths 是否都写了 @/uni_modules |
| TS 有提示但运行报错 | 是否 import 到了插件内部文件(只允许 import 到插件根) |
uni build 后壳工程里没有插件 Kotlin |
config.json 是否缺失、插件是否在 src/uni_modules 下 |
Android 编译过、运行崩溃 NoClassDefFoundError |
utsplugin-release.aar 与 Kotlin/coroutines/XXPermissions 等运行时依赖是否齐 |
| 权限在 APK 里被合并进来删不掉 | tools:node="remove",以及 AAR 传递进来的 uses-permission |
| 启动白屏 / 直接报错 | assets/data/dcloud_control.xml 的 appid、assets/apps/<APPID>/www 目录名、manifest.json 的 appid 三者是否一致 |
| Appkey 校验失败 | AndroidManifest.xml 的 dcloud_appkey 是否与当前 appid 匹配 |
iOS 链接报 Undefined symbols |
离线 SDK 与插件用的 DCloudUTSFoundation / DCloudUTSExtAPI 版本是否同源 |
| iOS 权限文案缺失被拒 | config.json 的 plists、Info.plist、脚本合并是否都执行到了 |
| 版本号不一致 | manifest.json 的 versionName/versionCode 与壳工程 build.gradle / Info.plist 是否同源 |
10. 命令速查
# —— 创建前端工程 ——
npx degit dcloudio/uni-preset-vue#vite-ts my-project
pnpm install
npx @dcloudio/uvm@latest alpha
# —— 插件管理(HBuilderX CLI)——
cli uni_modules --create yourname-demo --template uts --project /abs/my-project
cli uni_modules --download yourname-socket --project /abs/my-project --extType source
cli uni_modules --list --project /abs/my-project --json
cli uni_modules --upgrade yourname-socket --project /abs/my-project --force
cli uni_modules --validate yourname-demo --project /abs/my-project
cli uni_modules --publish yourname-demo --project /abs/my-project --changeLog "修复重连"
# —— 单独编译 UTS 验证语法 ——
cli compile app-android --project my-project --uni_module yourname-socket
cli compile app-ios --project my-project --uni_module yourname-socket
# —— 生成离线资源 ——
cli publish app-android --type appResource --project my-project
# —— 前端构建 ——
uni build -p app-android --mode release
uni build -p app-ios --mode release
# —— 原生 ——
cd native-android && ./gradlew --no-daemon :app:assembleRelease
cd native-ios && pod install && open App.xcworkspace
相关文档:
- uni-app CLI 创建与运行:https://uniapp.dcloud.net.cn/quickstart-cli.html
- uni-app 工程结构:https://uniapp.dcloud.net.cn/tutorial/project.html
- uni_modules 规范:https://uniapp.dcloud.net.cn/plugin/uni_modules.html
- UTS 插件:https://uniapp.dcloud.net.cn/plugin/uts-plugin.html
- UTS 原生混编:https://uniapp.dcloud.net.cn/plugin/uts-plugin-hybrid.html
- uts for Android:https://uniapp.dcloud.net.cn/plugin/uts-for-android.html
- HBuilderX CLI:https://hx.dcloud.net.cn/cli/README
- CLI uni_modules 命令:https://hx.dcloud.net.cn/cli/uniModules
- CLI 编译 UTS:https://hx.dcloud.net.cn/cli/compile-uts
- Android 离线打包:https://nativesupport.dcloud.net.cn/AppDocs/usesdk/android.html



