UniApp 深度使用实战
一套 Vue 语法,多端发布:从条件编译、路由分包到登录支付、性能优化与工程化落地的完整指南。
目录
- [导读:UniApp 架构与选型](#导读:UniApp 架构与选型)
- [工程初始化:Vue3 + TypeScript + Vite](#工程初始化:Vue3 + TypeScript + Vite)
- 条件编译:多端差异的「开关」
- [页面路由与 pages.json 深度配置](#页面路由与 pages.json 深度配置)
- 网络层封装与鉴权
- [本地存储与 Pinia 持久化](#本地存储与 Pinia 持久化)
- [组件体系:内置、easycom 与 uni_modules](#组件体系:内置、easycom 与 uni_modules)
- 样式适配:rpx、安全区与暗黑模式
- 分包、预加载与性能优化
- 登录、支付与平台能力
- [H5 / 小程序 / App 差异速查](#H5 / 小程序 / App 差异速查)
- [调试、构建与发布 checklist](#调试、构建与发布 checklist)
- 常见踩坑与最佳实践
一、导读:UniApp 架构与选型
1.1 UniApp 是什么
UniApp 是基于 Vue 的跨端框架,通过编译将同一套源码输出到:
| 端 | 运行时 | 典型场景 |
|---|---|---|
| 微信小程序 | 微信 JS 运行时 | 私域、扫码、订阅消息 |
| H5 | 浏览器 | 分享链接、内嵌 WebView |
| App | uni-app 原生渲染 / WebView 混合 | 相机、推送、离线包 |
| 其他小程序 | 各平台适配 | 支付宝、抖音等 |
text
┌──────────────────────────────────────┐
│ 业务代码(.vue + TS + Pinia) │
├──────────────────────────────────────┤
│ uni.* API(跨端统一接口) │
├──────────────────────────────────────┤
│ 条件编译 #ifdef MP-WEIXIN / APP-PLUS │
├──────────────────────────────────────┤
│ 编译器 → 各端原生/小程序产物 │
└──────────────────────────────────────┘
1.2 与 Taro、原生小程序的对比
| 维度 | UniApp | Taro | 原生小程序 |
|---|---|---|---|
| 语法 | Vue 3 | React / Vue 可选 | WXML + 自有逻辑 |
| 学习成本 | Vue 开发者低 | React 栈更顺 | 平台绑定深 |
| 生态 | DCloud 插件市场 | 京东系 | 微信文档最全 |
| 适用 | 多端统一、中小团队快发 | React 技术栈团队 | 单端极致体验 |
前置阅读 :
23-Vue 高阶技巧实战.md、29-Pinia深度使用实战.md中的 Composition API、Pinia 可直接迁移到 UniApp。
1.3 学习目标
- 会用 条件编译 处理平台差异,而不是到处
if (isWeixin) - 掌握 pages.json 路由、分包、tabBar、权限声明
- 封装 uni.request + 登录态 + 错误提示
- 理解 rpx / 安全区 / 分包 对体验与审核的影响
二、工程初始化:Vue3 + TypeScript + Vite
2.1 创建项目
bash
# 官方推荐:HBuilderX 可视化 或 CLI
npx degit dcloudio/uni-preset-vue#vite-ts my-uniapp
cd my-uniapp
npm install
2.2 推荐目录结构
text
src/
├── pages/ # 主包页面
│ ├── index/
│ │ └── index.vue
│ └── user/
│ └── profile.vue
├── pages-sub/ # 分包页面(示例)
│ └── order/
│ └── detail.vue
├── components/ # 全局组件
├── composables/ # 组合式逻辑
├── stores/ # Pinia
├── api/ # 接口
├── utils/
│ ├── request.ts
│ └── platform.ts
├── static/
├── App.vue
├── main.ts
├── pages.json
├── manifest.json
└── uni.scss # 全局 SCSS 变量
2.3 main.ts 接入 Pinia
typescript
// main.ts
import { createSSRApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
export function createApp() {
const app = createSSRApp(App);
const pinia = createPinia();
app.use(pinia);
return { app };
}
UniApp 要求导出
createApp函数(非直接createApp(App).mount),由框架在各端挂载。
2.4 manifest.json 要点
json
{
"name": "my-app",
"appid": "__UNI__XXXX",
"vueVersion": "3",
"mp-weixin": {
"appid": "wxXXXXXXXX",
"setting": { "urlCheck": true },
"usingComponents": true
},
"h5": {
"router": { "mode": "history", "base": "/h5/" },
"devServer": { "port": 5173 }
}
}
三、条件编译:多端差异的「开关」
3.1 语法形式
vue
<!-- 模板 -->
<!-- #ifdef MP-WEIXIN -->
<button open-type="getPhoneNumber" @getphonenumber="onPhone">微信手机号</button>
<!-- #endif -->
<!-- #ifdef H5 -->
<a href="/download">下载 App</a>
<!-- #endif -->
<!-- #ifndef APP-PLUS -->
<view>非 App 端显示</view>
<!-- #endif -->
typescript
// script
// #ifdef MP-WEIXIN
import { loginByWeixin } from '@/api/auth';
// #endif
// #ifdef APP-PLUS
import { loginByApple } from '@/api/auth';
// #endif
export function doLogin() {
// #ifdef H5
uni.navigateTo({ url: '/pages/login/h5-login' });
// #endif
// #ifdef MP-WEIXIN
loginByWeixin();
// #endif
}
scss
/* style */
/* #ifdef APP-PLUS */
.page {
padding-top: var(--status-bar-height);
}
/* #endif */
3.2 常用平台标识
| 标识 | 含义 |
|---|---|
MP-WEIXIN |
微信小程序 |
MP-ALIPAY |
支付宝小程序 |
H5 |
浏览器 |
APP-PLUS |
App(Android/iOS) |
APP-ANDROID / APP-IOS |
细分 App 平台 |
3.3 与运行时判断的配合
typescript
// utils/platform.ts
export const isMpWeixin = () => {
// #ifdef MP-WEIXIN
return true;
// #endif
return false;
};
export const getPlatform = (): string => {
// @ts-ignore
return process.env.UNI_PLATFORM ?? 'unknown';
};
原则 :差异大的用 条件编译 (编译期剔除死代码);轻量判断用 uni.getSystemInfoSync()。
四、页面路由与 pages.json 深度配置
4.1 页面注册
json
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"enablePullDownRefresh": true
}
},
{
"path": "pages/user/profile",
"style": {
"navigationStyle": "custom"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarBackgroundColor": "#ffffff",
"backgroundColor": "#f5f5f5"
},
"tabBar": {
"color": "#999",
"selectedColor": "#07c160",
"list": [
{ "pagePath": "pages/index/index", "text": "首页", "iconPath": "static/tab/home.png", "selectedIconPath": "static/tab/home-active.png" },
{ "pagePath": "pages/user/profile", "text": "我的", "iconPath": "static/tab/user.png", "selectedIconPath": "static/tab/user-active.png" }
]
}
}
注意 :
pages数组 第一项为首页 ;tabBar 页面必须用switchTab跳转。
4.2 路由 API 与传参
typescript
// 保留当前页,压栈
uni.navigateTo({
url: '/pages-sub/order/detail?id=1001&type=paid'
});
// 关闭当前页
uni.redirectTo({ url: '/pages/login/index' });
// 关闭所有页到 tab
uni.switchTab({ url: '/pages/index/index' });
// 接收参数 --- onLoad 仅 options 页面可用
// detail.vue
import { onLoad } from '@dcloudio/uni-app';
onLoad((options) => {
const id = options?.id;
});
复杂对象传参 :用 Pinia / 全局事件 / encodeURIComponent(JSON.stringify(obj)),避免 URL 过长。
4.3 路由守卫(登录拦截)
typescript
// utils/routerGuard.ts
const whiteList = ['/pages/login/index', '/pages/index/index'];
export function setupRouterGuard() {
const methods = ['navigateTo', 'redirectTo', 'reLaunch', 'switchTab'] as const;
methods.forEach((method) => {
uni.addInterceptor(method, {
invoke(args) {
const url = args.url.split('?')[0];
const token = uni.getStorageSync('token');
if (!token && !whiteList.includes(url)) {
uni.navigateTo({ url: '/pages/login/index' });
return false;
}
return args;
}
});
});
}
// App.vue onLaunch
import { setupRouterGuard } from '@/utils/routerGuard';
setupRouterGuard();
五、网络层封装与鉴权
5.1 统一 request 封装
typescript
// utils/request.ts
const BASE_URL = import.meta.env.VITE_API_BASE;
type RequestOptions = UniApp.RequestOptions & {
hideLoading?: boolean;
hideErrorToast?: boolean;
};
export function request<T = unknown>(options: RequestOptions): Promise<T> {
const token = uni.getStorageSync('token') as string;
if (!options.hideLoading) {
uni.showLoading({ title: '加载中', mask: true });
}
return new Promise((resolve, reject) => {
uni.request({
...options,
url: options.url.startsWith('http') ? options.url : BASE_URL + options.url,
header: {
'Content-Type': 'application/json',
Authorization: token ? `Bearer ${token}` : '',
...options.header
},
success: (res) => {
const data = res.data as { code: number; data: T; message?: string };
if (res.statusCode === 401) {
uni.removeStorageSync('token');
uni.navigateTo({ url: '/pages/login/index' });
reject(new Error('未登录'));
return;
}
if (data.code === 0) {
resolve(data.data);
} else {
if (!options.hideErrorToast) {
uni.showToast({ title: data.message ?? '请求失败', icon: 'none' });
}
reject(new Error(data.message));
}
},
fail: (err) => {
if (!options.hideErrorToast) {
uni.showToast({ title: '网络异常', icon: 'none' });
}
reject(err);
},
complete: () => {
if (!options.hideLoading) uni.hideLoading();
}
});
});
}
// api/order.ts
export const getOrderDetail = (id: string) =>
request<OrderDetail>({ url: `/orders/${id}`, method: 'GET' });
5.2 上传文件
typescript
export function uploadFile(filePath: string): Promise<string> {
const token = uni.getStorageSync('token');
return new Promise((resolve, reject) => {
uni.uploadFile({
url: BASE_URL + '/upload',
filePath,
name: 'file',
header: { Authorization: `Bearer ${token}` },
success: (res) => {
const body = JSON.parse(res.data);
if (body.code === 0) resolve(body.data.url);
else reject(new Error(body.message));
},
fail: reject
});
});
}
5.3 小程序合法域名
微信小程序要求 request/upload 域名在后台配置 request 合法域名 ,且必须 HTTPS 。开发阶段可在开发者工具勾选「不校验合法域名」,上线前必须配置。
六、本地存储与 Pinia 持久化
6.1 uni.storage API
typescript
// 同步(简单场景)
uni.setStorageSync('token', token);
const token = uni.getStorageSync('token');
// 异步(推荐大数据)
uni.setStorage({
key: 'userProfile',
data: profile,
fail: () => uni.showToast({ title: '存储失败', icon: 'none' })
});
小程序单 key 上限约 1MB ,总容量约 10MB;勿存超大列表,用分页 + 服务端。
6.2 Pinia + 持久化(衔接 29 篇)
typescript
// stores/user.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
const TOKEN_KEY = 'token';
export const useUserStore = defineStore('user', () => {
const token = ref(uni.getStorageSync(TOKEN_KEY) || '');
const userInfo = ref<UserInfo | null>(null);
const isLoggedIn = computed(() => !!token.value);
function setToken(t: string) {
token.value = t;
uni.setStorageSync(TOKEN_KEY, t);
}
function logout() {
token.value = '';
userInfo.value = null;
uni.removeStorageSync(TOKEN_KEY);
}
return { token, userInfo, isLoggedIn, setToken, logout };
});
七、组件体系:内置、easycom 与 uni_modules
7.1 easycom 自动引入
json
// pages.json
{
"easycom": {
"autoscan": true,
"custom": {
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue",
"^App(.*)": "@/components/App$1/App$1.vue"
}
}
}
组件放在约定目录后,页面可直接 <AppCard />,无需 import。
7.2 跨端组件注意点
vue
<template>
<!-- 小程序不支持部分 HTML 标签,用 view/text/image -->
<view class="card">
<image :src="cover" mode="aspectFill" lazy-load />
<text class="title">{{ title }}</text>
</view>
</template>
| Web 习惯 | UniApp 推荐 |
|---|---|
div / span |
view / text |
img |
image(必须写宽高或 flex) |
a 跳转 |
navigator 或 @click + uni.navigateTo |
7.3 uni_modules 插件
bash
# 从插件市场导入后
# uni_modules/uni-icons/components/uni-icons/uni-icons.vue
<uni-icons type="contact" size="30" />
选型时看:支持 Vue3 、最近更新 、是否含 nvue/App 条件编译。
八、样式适配:rpx、安全区与暗黑模式
8.1 rpx 与固定单位
scss
// 750 设计稿:1px 设计稿 ≈ 1rpx
.container {
width: 750rpx;
padding: 24rpx;
font-size: 28rpx;
}
// 边框 1px 真机锐利 --- 用 1px 或 half-pixel 方案
.border {
border-bottom: 1px solid #eee;
}
8.2 安全区(刘海、底部横条)
vue
<template>
<view class="page safe-area-bottom">
<view class="footer">提交订单</view>
</view>
</template>
<style lang="scss">
.safe-area-bottom {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
</style>
typescript
// 自定义导航栏时获取状态栏高度
const { statusBarHeight, safeAreaInsets } = uni.getSystemInfoSync();
8.3 暗黑模式
json
// pages.json globalStyle
"darkmode": true,
"themeLocation": "theme.json"
json
// theme.json
{
"light": { "navBgColor": "#ffffff", "pageBg": "#f5f5f5" },
"dark": { "navBgColor": "#1a1a1a", "pageBg": "#000000" }
}
九、分包、预加载与性能优化
9.1 分包配置
json
{
"pages": [
{ "path": "pages/index/index" }
],
"subPackages": [
{
"root": "pages-sub/order",
"pages": [
{ "path": "list", "style": { "navigationBarTitleText": "订单列表" } },
{ "path": "detail", "style": { "navigationBarTitleText": "订单详情" } }
]
}
],
"preloadRule": {
"pages/index/index": {
"network": "all",
"packages": ["pages-sub/order"]
}
}
}
微信主包体积限制约 2MB (总包 20MB+ 视政策调整),静态资源大文件放 CDN,分包放低频业务。
9.2 列表性能
vue
<template>
<!-- 长列表优先 z-paging / mescroll-uni 等成熟组件 -->
<scroll-view scroll-y class="list" @scrolltolower="loadMore">
<view v-for="item in list" :key="item.id" class="row">
{{ item.title }}
</view>
</scroll-view>
</template>
- 图片:
lazy-load、webp、合适mode - 避免在
scroll-view内嵌套过多复杂组件 - 减少
watch深度监听大数组
9.3 setData 优化(小程序)
- 合并多次数据更新为一次赋值
- 只传列表变更片段,避免整个
list复制
十、登录、支付与平台能力
10.1 微信小程序登录流程
typescript
// composables/useWxLogin.ts
export async function wxLogin(): Promise<string> {
const { code } = await new Promise<UniApp.LoginRes>((resolve, reject) => {
uni.login({ provider: 'weixin', success: resolve, fail: reject });
});
// 将 code 发给后端,换取 openid + 自建 token
const { token } = await request<{ token: string }>({
url: '/auth/wx-login',
method: 'POST',
data: { code }
});
return token;
}
vue
<!-- 手机号快捷登录(需企业认证小程序) -->
<button open-type="getPhoneNumber" @getphonenumber="onGetPhone">
授权手机号
</button>
10.2 微信支付(小程序)
typescript
export async function wxPay(orderId: string) {
const payParams = await request<WechatPayParams>({
url: '/pay/prepay',
method: 'POST',
data: { orderId }
});
await new Promise<void>((resolve, reject) => {
uni.requestPayment({
provider: 'wxpay',
...payParams,
success: () => resolve(),
fail: reject
});
});
}
后端需按微信文档生成 timeStamp、nonceStr、package、signType、paySign。
10.3 App 端能力示例
typescript
// #ifdef APP-PLUS
// 扫码
uni.scanCode({
success: (res) => console.log(res.result)
});
// 选择图片并上传
uni.chooseImage({
count: 1,
success: async (res) => {
const url = await uploadFile(res.tempFilePaths[0]);
}
});
// #endif
十一、H5 / 小程序 / App 差异速查
| 能力 | 微信小程序 | H5 | App |
|---|---|---|---|
| 路由 | pages.json + 页面栈 |
vue-router 或默认 hash | 同小程序栈 |
| 登录 | uni.login + code |
账号密码 / OAuth | 一键登录、原生 SDK |
| 分享 | onShareAppMessage |
浏览器分享受限 | 系统分享 SDK |
| 支付 | requestPayment |
微信 JSAPI / 支付宝 WAP | 微信/支付宝 SDK |
| DOM | 无 document | 完整 DOM | WebView |
| 存储 | 本地 storage | localStorage | plus.storage |
11.1 H5 微信内 JSSDK
typescript
// #ifdef H5
import wx from 'weixin-js-sdk';
export async function initWxJsSdk() {
const config = await request<WxConfig>({ url: '/wx/js-config', data: { url: location.href.split('#')[0] } });
wx.config({ ...config, jsApiList: ['chooseWXPay', 'updateAppMessageShareData'] });
}
// #endif
11.2 小程序隐私协议(2023+)
在 manifest.json → 微信小程序配置 用户隐私保护指引 ,调用相册、定位等 API 前需 uni.requirePrivacyAuthorize 或按钮 open-type="agreePrivacyAuthorization"。
十二、调试、构建与发布 checklist
12.1 调试手段
| 端 | 工具 |
|---|---|
| 微信小程序 | 微信开发者工具 + 真机预览 |
| H5 | Chrome DevTools |
| App | HBuilderX 真机运行 / 自定义基座 |
typescript
// 开发环境日志
if (import.meta.env.DEV) {
console.log('[api]', url, data);
}
12.2 发行命令
bash
# 微信小程序
npm run build:mp-weixin
# 产物:dist/build/mp-weixin → 导入微信开发者工具上传
# H5
npm run build:h5
# App
# HBuilderX 云打包 或 离线打包
12.3 上线前 checklist
- 关闭「不校验合法域名」
- 配置业务域名、webview 域名(如有内嵌 H5)
- 检查隐私协议与权限声明(
manifest→permission) - 主包体积、分包是否合理
- 真机测试:iOS / Android 各一款;弱网环境
- 生产环境 API 地址、
import.meta.env是否正确 - 移除
console.log、测试账号入口
十三、常见踩坑与最佳实践
13.1 踩坑速查
| 现象 | 原因 | 解决 |
|---|---|---|
navigateTo 无效 |
目标是 tabBar 页 | 改用 switchTab |
| 样式在 H5 正常、小程序错乱 | 用了不支持的选择器 / 标签 | 改 view,避免 * 深层选择器 |
| 图片不显示 | 域名未加入 download 合法域名 | 微信公众平台配置 |
| 登录后返回仍要登录 | 未 setStorageSync 或拦截器未读 token |
统一 request 封装 |
| iOS 滚动穿透 | 弹层未阻止触摸 | catchtouchmove 或组件库方案 |
| 热更新后白屏 | 缓存旧分包 | 升版本号、清缓存 |
13.2 最佳实践
- 条件编译 处理大差异,composables 复用业务逻辑
- 网络、登录、存储 各一层封装,页面只调 API
- Pinia 管用户态,与
29篇模式一致 - 分包 + 预加载 控制首屏,静态资源上 CDN
- 平台能力 按端拆分文件,避免单文件
#ifdef堆满 - 与 PC 后台 (Vue3 + Vite)共享
types、api类型定义(monorepo 可选)
13.3 与系列文章衔接
| 文章 | 衔接点 |
|---|---|
| 23 Vue 高阶 | Composition API、组件设计 |
| 29 Pinia | 用户态、持久化 |
| 05 网络层 | 请求/缓存概念迁移到 uni.request |
| 10 安全 | Token 存储、HTTPS |
| 11 性能 | 分包、图片、首屏 |
| 18~21 排错 | 小程序 Network、合法域名、白屏 |
十四、总结
UniApp 的核心不是「学一堆 API」,而是建立 一套代码 → 条件编译 → 各端产物 的工程思维:路由与分包服务审核与性能,封装层服务可维护性,平台能力按端隔离。
注意:
- 大差异用条件编译,小差异用 composable + 运行时判断
- 请求、登录、存储必须统一封装,页面不写裸
uni.request - 先过真机与审核规则,再追求 H5 开发时的爽感