摘要:本文给出一个可落地的"Vue 中间件(SDK)+ 适配层 + UI 抽象"方案,让同一套业务逻辑同时支持 UniApp 与 Taro(Vue)。我们以契约驱动的方式统一数据模型、API、权限审计与 AI 网关,并提供架构图、接口设计、代码示例、测试与发布治理清单,帮助团队快速上线、可控演进、低成本迁移。
关键字:Vue中间件、跨端适配、UniApp、Taro、契约驱动
目标与原则
- **统一目标:**一套业务逻辑(SDK),两套前端实现(UniApp、Taro),治理与审计不分家。
- **契约优先:**数据模型、API、权限、审计事件、AI 调用统一定义与版本化。
- **最薄表层:**把差异放在适配器与 UI 层,业务与治理集中在中间件。
- **可测可管:**统一测试、观测、发布与合规策略,确保可灰度、可回滚、可审计。
- **渐进演进:**先拆出中间件,再并行两栈;复杂产品线逐步转向 Taro。
总览架构与职责清表
Domain Contracts 契约 Vue SDK 中间件 平台适配层 UniApp Adapter Taro(Vue) Adapter UI 抽象接口层 UniApp 组件实现 Taro 组件实现 观测与审计 日志/错误/性能/AI审计 后端审计总线与治理平台
| 层级 | 职责 | 产出 |
|---|---|---|
| 契约层 | 数据模型、API、权限、审计事件、AI 网关协议 | versioned JSON/YAML + 文档 |
| Vue SDK | 认证、数据流、缓存、错误处理、审计埋点 | 框架无关的 Vue 插件/composables |
| 平台适配 | 网络/路由/设备能力统一封装 | Uni/Taro adapter + ports 接口 |
| UI 抽象 | 表单/列表/弹窗/上传等统一组件接口 | 同名 API 的两套实现 |
| 观测与审计 | 错误、性能、行为事件、AI 调用审计 | 上报客户端 + 后端看板/告警 |
契约与模型:把差异挡在边界之外
-
数据契约:
-
Label: 统一请求/响应模型、分页、排序、错误码。
-
示例:
json{ "Task": { "id": "string", "title": "string", "state": "Pending|Approved|Rejected", "assigneeId": "string", "updatedAt": "datetime" }, "Page": { "page": 1, "pageSize": 20, "total": 100 } }
-
-
API 契约:
- Label: REST/GraphQL 合约与版本号(如 /v1)。
- Label: 认证头一致(Authorization: Bearer )。
-
权限合约:
- Label: 统一角色/资源/动作模型(RBAC/ABAC)。
- Label: 中间件内置
can(action, resource)校验。
-
审计事件:
- Label: 统一事件名与载荷(login, view_task, approve_task)。
- Label: 事件含 traceId、userId、tenantId、timestamp。
-
AI 网关协议:
- Label: 统一
POST /ai/invoke,包含模型、上下文、配额与审计信息。 - Label: 前端仅传递合约,不嵌套 Prompt 细节。
- Label: 统一
中间件设计:端口接口 + 组合式 API
端口接口(ports)
ts
// ports/http.ts
export interface HttpAdapter {
get<T = any>(url: string, options?: { headers?: Record<string,string>; query?: Record<string, any> }): Promise<T>
post<T = any>(url: string, body?: any, options?: { headers?: Record<string,string> }): Promise<T>
}
// ports/router.ts
export interface RouterAdapter {
push(path: string, params?: Record<string, any>): Promise<void>
replace(path: string, params?: Record<string, any>): Promise<void>
back(): Promise<void>
}
// ports/device.ts
export interface DeviceAdapter {
scanQR(): Promise<{ text: string }>
getLocation(): Promise<{ lat: number; lng: number }>
pickImage(): Promise<{ uri: string }>
}
// ports/telemetry.ts
export interface Telemetry {
error(e: Error, ctx?: Record<string, any>): void
perf(metric: string, value: number, ctx?: Record<string, any>): void
event(name: string, payload?: Record<string, any>): void
}
Vue SDK(业务与治理)
ts
// sdk/core.ts
import { ref, computed } from 'vue'
import type { HttpAdapter, RouterAdapter, DeviceAdapter, Telemetry } from './ports'
export function createMobileSDK(deps: {
http: HttpAdapter
router: RouterAdapter
device: DeviceAdapter
log: Telemetry
}) {
const token = ref<string>('')
const user = ref<any>(null)
async function login(payload: { account: string; password: string }) {
const res = await deps.http.post('/v1/auth/login', payload)
token.value = res.token
user.value = res.user
deps.log.event('login', { userId: res.user.id })
return res
}
async function fetchTasks(page = 1) {
const res = await deps.http.get('/v1/workflow/tasks', {
headers: { Authorization: `Bearer ${token.value}` },
query: { page, pageSize: 20 },
})
deps.log.perf('fetch_tasks_ms', res.duration ?? 0)
return res.items
}
function can(action: string, resource: string) {
// 简化示例:真实逻辑应读取权限策略
const roles = user.value?.roles ?? []
return roles.includes('admin') || (action === 'view' && resource === 'task')
}
const isAuthed = computed(() => !!token.value)
return { login, fetchTasks, can, isAuthed, user }
}
平台适配层:Uni 与 Taro 的差异归一
UniApp 适配器
ts
// adapters/uni/http.ts
export const uniHttp = {
get(url, options) {
return new Promise((resolve, reject) => {
uni.request({
url: buildUrl(url, options?.query),
method: 'GET',
header: options?.headers,
success: (res) => resolve(res.data),
fail: reject,
})
})
},
post(url, body, options) {
return new Promise((resolve, reject) => {
uni.request({
url: buildUrl(url),
method: 'POST',
data: body,
header: options?.headers,
success: (res) => resolve(res.data),
fail: reject,
})
})
},
}
ts
// adapters/uni/router.ts
export const uniRouter = {
async push(path, params) { uni.navigateTo({ url: toUrl(path, params) }) },
async replace(path, params) { uni.redirectTo({ url: toUrl(path, params) }) },
async back() { uni.navigateBack() },
}
ts
// adapters/uni/device.ts
export const uniDevice = {
async scanQR() {
return new Promise((resolve, reject) => {
uni.scanCode({ success: (res) => resolve({ text: res.result }), fail: reject })
})
},
async getLocation() {
return new Promise((resolve, reject) => {
uni.getLocation({ success: (res) => resolve({ lat: res.latitude, lng: res.longitude }), fail: reject })
})
},
async pickImage() {
return new Promise((resolve, reject) => {
uni.chooseImage({ count: 1, success: (res) => resolve({ uri: res.tempFilePaths[0] }), fail: reject })
})
},
}
Taro(Vue) 适配器
ts
// adapters/taro/http.ts
import Taro from '@tarojs/taro'
export const taroHttp = {
async get(url, options) {
const res = await Taro.request({ url: buildUrl(url, options?.query), method: 'GET', header: options?.headers })
return res.data
},
async post(url, body, options) {
const res = await Taro.request({ url: buildUrl(url), method: 'POST', data: body, header: options?.headers })
return res.data
},
}
ts
// adapters/taro/router.ts
import Taro from '@tarojs/taro'
export const taroRouter = {
async push(path, params) { Taro.navigateTo({ url: toUrl(path, params) }) },
async replace(path, params) { Taro.redirectTo({ url: toUrl(path, params) }) },
async back() { Taro.navigateBack() },
}
ts
// adapters/taro/device.ts
import Taro from '@tarojs/taro'
export const taroDevice = {
async scanQR() {
// 具体平台差异需用插件或原生模块
const res = await Taro.scanCode()
return { text: (res as any).result || '' }
},
async getLocation() {
const res = await Taro.getLocation()
return { lat: res.latitude, lng: res.longitude }
},
async pickImage() {
const res = await Taro.chooseImage({ count: 1 })
return { uri: res.tempFilePaths?.[0] || '' }
},
}
UI 抽象接口层:同名组件,不同实现
- 统一接口原则:
- Label: 表单(Form)、列表(List)、弹窗(Modal)、上传(Uploader)对外暴露同样的 props 与事件。
- Label: 实现层分别绑定到
uView/uni-ui(Uni)与NutUI/Taro-UI(Taro)。
ts
// ui/contracts.ts
export interface ListProps<T> {
data: T[]
loading?: boolean
onRefresh?: () => Promise<void>
onItemClick?: (item: T) => void
}
vue
<!-- ui/uni/List.vue -->
<template>
<u-list :loading="loading">
<u-list-item v-for="item in data" :key="item.id" @click="onItemClick?.(item)">
<slot name="item" :item="item">{{ item.title }}</slot>
</u-list-item>
</u-list>
</template>
<script setup lang="ts">
defineProps<ListProps<any>>()
</script>
vue
<!-- ui/taro/List.vue -->
<template>
<nut-list :loading="loading">
<nut-list-item v-for="item in data" :key="item.id" @click="onItemClick?.(item)">
<slot name="item" :item="item">{{ item.title }}</slot>
</nut-list-item>
</nut-list>
</template>
<script setup lang="ts">
defineProps<ListProps<any>>()
</script>
观测与审计:一次埋点,两端可见
ts
// telemetry/basic.ts
export const basicTelemetry = {
error(e, ctx) { console.error('[err]', e, ctx) },
perf(metric, value, ctx) { console.log('[perf]', metric, value, ctx) },
event(name, payload) {
// 统一上报至后端审计总线
fetch('/audit/ingest', { method: 'POST', body: JSON.stringify({ name, payload, ts: Date.now() }) })
},
}
- 统一指标:
- Label: 错误(message, stack, userId)。
- Label: 性能(冷启动、接口耗时、渲染耗时)。
- Label: 行为事件(页面浏览、任务审批、AI 调用)。
AI 网关与端侧能力:薄前端、厚网关
- 统一网关:
- Label:
POST /ai/invoke,字段包含modelId, input, context, auditMeta。 - Label: SDK 暴露
ai.invoke(),仅传入合约,不关心模型细节。
- Label:
ts
// sdk/ai.ts
export function createAIClient(http: HttpAdapter, log: Telemetry) {
async function invoke(req: { modelId: string; input: string; context?: any }) {
const res = await http.post('/v1/ai/invoke', req)
log.event('ai_invoke', { modelId: req.modelId, tokens: res?.usage?.tokens })
return res.output
}
return { invoke }
}
- 端侧增强:
- Label: OCR/ASR/NLP 用插件桥接(Uni/Taro 各用其生态)。
- Label: 重度模型(离线/本地)优先 Taro+RN;Uni 通过原生插件桥接。
项目结构建议与示例
txt
mobile-sdk/
├─ contracts/ # 数据/API/权限/审计合约
├─ sdk/ # Vue 插件与业务逻辑(无框架依赖)
│ ├─ core.ts
│ ├─ ai.ts
│ └─ auth.ts
├─ ports/ # 端口接口定义
├─ adapters/ # 平台适配器(uni/ taro)
│ ├─ uni/
│ └─ taro/
├─ ui/ # UI 抽象接口与两套实现
│ ├─ contracts.ts
│ ├─ uni/
│ └─ taro/
├─ telemetry/
├─ tests/ # 单元/契约测试
└─ examples/
├─ uniapp-demo/
└─ taro-vue-demo/
测试、发布与合规清单
-
契约与单元测试:
- Label: API/模型/权限/审计契约的 schema 测试(版本锁定)。
- Label: SDK 业务函数的单测与适配器的端到端测试(mock uni/taro API)。
-
CI/CD 与发布:
- Label: 两栈分别打包与渠道灰度(小程序/APP/H5)。
- Label: 快速回滚与依赖安全扫描(SCA),保留 2 个稳定版本。
-
审计与观测:
- Label: 统一看板(错误率、接口耗时、崩溃率、AI 调用成功率)。
- Label: 告警阈值与值班制度(生产事故 10 分钟内回滚)。
-
合规与隐私:
- Label: Token/敏感数据最小化与加密存储。
- Label: 地区化策略开关(地图/支付/推送差异适配)。
演进路线与落地节奏
-
阶段 1(0--6 周):
- Label: 梳理契约 + 搭建 Vue SDK + Uni 适配器 + Uni UI 实现。
- Label: 单端上线(UniApp),打通登录/任务/审批/审计。
-
阶段 2(6--12 周):
- Label: 补齐 Taro 适配器 + Taro UI 实现,双端共存验证。
- Label: 引入统一 AI 网关,落地表单智能与检索。
-
阶段 3(12--24 周):
- Label: 细分产品线:入口型继续 Uni;复杂交互迁 Taro。
- Label: 完善治理:监控看板、灰度/回滚、依赖安全、模型版本管理。
操作要点与踩坑提醒
-
接口抽象边界:
- Label: 不要把框架 API 暴露到业务层;适配器承担所有"平台差异"。
- Label: UI 接口必须稳定,否则双端改动会指数级增长。
-
治理与性能:
- Label: 观测从第一天开始做;别等事故发生才补埋点。
- Label: 列表/表单为主时优先稳定与易用;动画与高交互模块再做性能专项。
-
AI 与合规:
- Label: 前端只传合约,Prompt 与策略在网关;所有调用纳入审计事件。
- Label: 明确数据边界与缓存策略,避免敏感数据在端侧滥存。
结语
中间件不是"再封一层"的复杂化,而是用契约把不确定挡在边界之外,用统一治理放大确定性。让 UniApp 带你跑得快、跑得稳;让 Taro 带你跑得久、跑得远。你要做的,是把这两种能力用"一个 Vue 中间件"连接起来,给团队一条清晰的、可演进的移动端路线。