uniapp 深度使用

UniApp 深度使用实战

一套 Vue 语法,多端发布:从条件编译、路由分包到登录支付、性能优化与工程化落地的完整指南。


目录

  1. [导读:UniApp 架构与选型](#导读:UniApp 架构与选型)
  2. [工程初始化:Vue3 + TypeScript + Vite](#工程初始化:Vue3 + TypeScript + Vite)
  3. 条件编译:多端差异的「开关」
  4. [页面路由与 pages.json 深度配置](#页面路由与 pages.json 深度配置)
  5. 网络层封装与鉴权
  6. [本地存储与 Pinia 持久化](#本地存储与 Pinia 持久化)
  7. [组件体系:内置、easycom 与 uni_modules](#组件体系:内置、easycom 与 uni_modules)
  8. 样式适配:rpx、安全区与暗黑模式
  9. 分包、预加载与性能优化
  10. 登录、支付与平台能力
  11. [H5 / 小程序 / App 差异速查](#H5 / 小程序 / App 差异速查)
  12. [调试、构建与发布 checklist](#调试、构建与发布 checklist)
  13. 常见踩坑与最佳实践

一、导读: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 高阶技巧实战.md29-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-loadwebp、合适 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
    });
  });
}

后端需按微信文档生成 timeStampnonceStrpackagesignTypepaySign

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)
  • 检查隐私协议与权限声明(manifestpermission
  • 主包体积、分包是否合理
  • 真机测试: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)共享 typesapi 类型定义(monorepo 可选)

13.3 与系列文章衔接

文章 衔接点
23 Vue 高阶 Composition API、组件设计
29 Pinia 用户态、持久化
05 网络层 请求/缓存概念迁移到 uni.request
10 安全 Token 存储、HTTPS
11 性能 分包、图片、首屏
18~21 排错 小程序 Network、合法域名、白屏

十四、总结

UniApp 的核心不是「学一堆 API」,而是建立 一套代码 → 条件编译 → 各端产物 的工程思维:路由与分包服务审核与性能,封装层服务可维护性,平台能力按端隔离。

注意

  1. 大差异用条件编译,小差异用 composable + 运行时判断
  2. 请求、登录、存储必须统一封装,页面不写裸 uni.request
  3. 先过真机与审核规则,再追求 H5 开发时的爽感
相关推荐
路光.2 小时前
uniapp小程序/App使用webview打通麦克风权限实现录音功能
小程序·uni-app·app
xiaoyan20152 小时前
全新首发uniapp+deepseek-v4三端通用智能ai助手
uni-app·ai编程·deepseek
anyup3 小时前
【最全鸿蒙】uni-app 转鸿蒙:从打包失败到商店上架成功全过程
前端·uni-app·harmonyos
2501_915106323 小时前
深入解析HTTPS抓包原理、中间人攻击及反抓包技术攻防
数据库·网络协议·ios·小程序·https·uni-app·iphone
游戏开发爱好者84 小时前
React Grab工具详解:AI助力Vue3、Svelte和Solid前端元素调试
android·ios·小程序·https·uni-app·iphone·webview
sN2vuQ08W4 小时前
uni-app 实现视频聊天、屏幕分享,支持Android、HarmonyOS、iOS
android·uni-app·音视频
遗憾随她而去.14 小时前
uniapp App平台 真机运行
uni-app
愚者Pro16 小时前
Flutter Widget组件学习(专为 Uniapp 转 Flutter 定制)
vue.js·学习·flutter·uni-app
粉末的沉淀2 天前
uniapp:带参数回到上一页
uni-app