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

相关文档: