uni-app + Vue 3 微信小程序开发入门指南

阅读时间
349 分钟
字数统计
30,037
阅读次数
1
发布时间
2026-07-27
适用场景:使用 uni-app、Vue 3、TypeScript 开发微信小程序

适用场景:使用 uni-app、Vue 3、TypeScript 开发微信小程序 推荐技术栈:Vue 3 + Vite + TypeScript + Pinia + SCSS 文档版本:2026 年 7 月


一、uni-app 是什么

uni-app 是一个使用 Vue 语法开发多端应用的框架。同一套 Vue 代码可以编译为:

  • 微信小程序
  • 支付宝小程序
  • 抖音小程序
  • H5
  • Android、iOS App
  • 鸿蒙应用等

在微信小程序平台,uni-app 编译器会把 .vue 文件转换成微信小程序可以运行的 WXML、WXSS、JavaScript、JSON 文件。

虽然开发语法以 Vue 3 为主,但小程序并不是浏览器环境,因此需要注意:

  1. 页面标签主要使用 view、text、image、button、input,而不是 div、span、img
  2. 页面跳转不使用 Vue Router。
  3. 网络请求使用 uni.request,而不是直接依赖浏览器的 fetch
  4. 本地缓存使用 uni.setStorageSync 等 API。
  5. 不应直接操作 window、document、localStorage
  6. 除了 Vue 生命周期,还有应用生命周期和页面生命周期。

uni-app 在 H5 端可以支持大部分 Vue 语法,但编译到小程序和 App 时,会受到目标平台能力限制。


二、推荐的项目方案

对于一个正式微信小程序项目,建议采用:

uni-app Vue 3 Composition API <script setup> TypeScript Vite Pinia SCSS 微信开发者工具 VS Code 或 HBuilderX

Vue 3 项目推荐使用 Composition API 和 <script setup>。Vue 官方文档的大部分 Composition API 示例也采用这种写法。

为什么推荐 TypeScript

TypeScript 在以下场景非常有价值:

  • 接口返回值类型定义
  • 页面参数类型定义
  • 组件 Props 类型定义
  • Pinia 状态类型定义
  • 避免接口字段拼写错误
  • 提升 VS Code 自动提示体验

对于有 React、NestJS 开发经验的开发者,直接使用 TypeScript 会更顺手。


三、开发环境安装

3.1 安装 Node.js

推荐使用 Node.js 20 LTS,并通过 nvm 管理 Node 版本。

查看版本:

node -v npm -v

uni-app 的 Vue 3/Vite CLI 工程要求使用较新的 Node.js 版本,官方文档列出了 Node 18+、20+ 环境。


3.2 安装微信开发者工具

微信开发者工具主要用于:

  • 小程序预览
  • 真机调试
  • 查看编译后的 WXML
  • 查看网络请求
  • 查看 Storage
  • 上传体验版
  • 上传正式代码

uni-app 负责开发和编译,微信开发者工具负责运行、调试和上传。


3.3 HBuilderX 和 CLI 如何选择

uni-app 支持两种创建方式:

方式一:HBuilderX

适合:

  • 第一次接触 uni-app
  • 希望开箱即用
  • 不想处理脚手架依赖
  • 需要开发 App

操作路径:

文件 → 新建 → 项目 → uni-app → 选择 Vue 3 → 创建

HBuilderX 可以直接运行到微信开发者工具。

方式二:CLI

适合:

  • 熟悉 npm、Vite
  • 使用 VS Code
  • 需要 Git、ESLint、自动化构建
  • 需要自定义工程结构
  • 与现有前端团队协作

本文主要采用 CLI 方式。


四、创建 Vue 3 + TypeScript 项目

执行:

npx degit dcloudio/uni-preset-vue#vite-ts uni-weixin-demo

进入项目:

cd uni-weixin-demo

安装依赖:

npm install

运行微信小程序:

npm run dev:mp-weixin

正式构建:

npm run build:mp-weixin

官方 Vue 3 CLI 模板使用 Vite;TypeScript 模板分支为 vite-ts

开发模式的微信小程序代码通常输出到:

dist/dev/mp-weixin

正式构建代码通常输出到:

dist/build/mp-weixin

使用微信开发者工具导入对应目录即可。开发模式通常保留 Source Map,正式构建则会压缩代码。


五、微信开发者工具配置

启动:

npm run dev:mp-weixin

然后打开微信开发者工具:

导入项目 → 选择 dist/dev/mp-weixin → 填写 AppID → 完成导入

开发初期没有正式 AppID 时,可以选择测试号,但登录、支付、订阅消息等能力可能受限。

正式项目需要在 manifest.json 中配置微信小程序 AppID:

{ "mp-weixin": { "appid": "wx1234567890abcdef", "setting": { "urlCheck": true } } }

也可以在 HBuilderX 的 manifest.json 可视化界面中填写。


六、项目目录结构

一个常见的 uni-app 项目结构如下:

src ├── api │ ├── request.ts │ └── user.ts ├── components │ ├── AppHeader.vue │ └── ProductCard.vue ├── composables │ └── usePagination.ts ├── pages │ ├── index │ │ └── index.vue │ ├── login │ │ └── login.vue │ └── detail │ └── detail.vue ├── static │ ├── images │ └── icons ├── stores │ └── user.ts ├── styles │ ├── variables.scss │ └── common.scss ├── types │ ├── api.ts │ └── user.ts ├── utils │ ├── auth.ts │ └── format.ts ├── App.vue ├── main.ts ├── manifest.json ├── pages.json └── uni.scss

6.1 重要文件

App.vue

整个应用的根组件,主要放:

  • 应用生命周期
  • 全局样式
  • 应用初始化逻辑

main.ts

负责创建 Vue 应用、注册 Pinia 和其他插件。

pages.json

用于注册页面、配置导航栏、配置 tabBar、设置全局页面样式。

uni-app 的路由由 pages.json 统一管理,它承担了一部分微信小程序 app.json 的职责。

manifest.json

负责配置:

  • 小程序 AppID
  • 应用名称
  • 平台权限
  • 微信小程序相关配置
  • App 打包配置

static

用于存放图片、字体、视频等静态资源。static 目录通常会被整体复制到构建产物中,因此不要存放未使用的大文件。


七、页面注册与 pages.json

新建页面:

src/pages/index/index.vue src/pages/detail/detail.vue

然后在 pages.json 中注册:

{ "pages": [ { "path": "pages/index/index", "style": { "navigationBarTitleText": "首页" } }, { "path": "pages/detail/detail", "style": { "navigationBarTitleText": "商品详情" } } ], "globalStyle": { "navigationBarTextStyle": "black", "navigationBarTitleText": "示例小程序", "navigationBarBackgroundColor": "#ffffff", "backgroundColor": "#f5f5f5" } }

注意:

  • 页面路径不写 .vue
  • 页面必须在 pages.json 中注册
  • 第一个页面默认是启动首页
  • tabBar 页面也必须注册在 pages

八、第一个完整页面

创建:

src/pages/index/index.vue

代码如下:

<script setup lang="ts"> import { computed, ref } from 'vue' import { onLoad, onShow } from '@dcloudio/uni-app' interface Product { id: number name: string price: number } const keyword = ref('') const productList = ref<Product[]>([ { id: 1, name: '机械键盘', price: 399 }, { id: 2, name: '无线鼠标', price: 199 }, { id: 3, name: '显示器', price: 1699 } ]) const filteredProducts = computed(() => { const value = keyword.value.trim() if (!value) { return productList.value } return productList.value.filter(item => { return item.name.includes(value) }) }) function openDetail(id: number) { uni.navigateTo({ url: `/pages/detail/detail?id=${id}` }) } onLoad(() => { console.log('页面首次加载') }) onShow(() => { console.log('页面进入前台') }) </script> <template> <view class="page"> <view class="header"> <text class="title">商品列表</text> </view> <input v-model="keyword" class="search-input" placeholder="请输入商品名称" /> <view v-for="item in filteredProducts" :key="item.id" class="product-card" @tap="openDetail(item.id)" > <text class="product-name"> {{ item.name }} </text> <text class="product-price"> ¥{{ item.price }} </text> </view> <view v-if="filteredProducts.length === 0" class="empty" > 暂无数据 </view> </view> </template> <style scoped lang="scss"> .page { min-height: 100vh; padding: 32rpx; background: #f5f6f8; box-sizing: border-box; } .header { margin-bottom: 24rpx; } .title { font-size: 40rpx; font-weight: 600; } .search-input { height: 80rpx; padding: 0 24rpx; margin-bottom: 24rpx; background: #ffffff; border-radius: 16rpx; } .product-card { display: flex; align-items: center; justify-content: space-between; padding: 32rpx; margin-bottom: 20rpx; background: #ffffff; border-radius: 20rpx; } .product-name { font-size: 30rpx; } .product-price { font-size: 32rpx; font-weight: 600; color: #e5484d; } .empty { padding: 80rpx 0; text-align: center; color: #999999; } </style>

九、uni-app 常用基础组件

uni-app 推荐使用小程序风格组件,而不是 HTML 标签。官方内置了 view、text、image、input、button、scroll-view、swiper 等基础组件。

9.1 view

类似 Web 中的 div

<view class="container"> 内容 </view>

9.2 text

用于文本内容:

<text class="title"> 标题 </text>

9.3 image

<image class="avatar" src="/static/images/avatar.png" mode="aspectFill" />

常用 mode

aspectFill 保持比例并裁剪 aspectFit 保持比例完整显示 widthFix 宽度固定,高度自动计算

9.4 button

<button type="primary" @tap="handleSubmit"> 提交 </button>

9.5 input

<input v-model="username" placeholder="请输入用户名" />

9.6 scroll-view

<scroll-view scroll-y class="scroll-container" > <view v-for="item in list" :key="item.id" > {{ item.name }} </view> </scroll-view>

十、Vue 3 最常用语法

10.1 <script setup>

这是 Vue 3 项目中最常用的组件写法:

<script setup lang="ts"> const title = '首页' function handleClick() { console.log('click') } </script> <template> <view @tap="handleClick"> {{ title }} </view> </template>

<script setup> 中声明的变量、函数和导入的组件,都可以直接在模板中使用,不需要手动 return


10.2 ref

ref 用于声明响应式数据。

import { ref } from 'vue' const count = ref(0) const username = ref('') const loading = ref(false)

在 JavaScript 或 TypeScript 中修改时,需要使用 .value

count.value += 1 loading.value = true

在模板中会自动解包,不需要 .value

<text>{{ count }}</text>
<button @tap="count++"> 加一 </button>

实际项目建议

一般可以把 ref 作为默认选择:

const list = ref<Product[]>([]) const form = ref({ username: '', password: '' })

10.3 reactive

reactive 适合声明一个结构固定的对象:

import { reactive } from 'vue' const form = reactive({ username: '', password: '', remember: false })

修改时不需要 .value

form.username = 'admin' form.remember = true

模板:

<input v-model="form.username" /> <input v-model="form.password" password />

注意不要直接解构:

const { username } = form

这样可能失去响应式联系。需要解构时使用:

import { toRefs } from 'vue' const { username, password } = toRefs(form)

10.4 computed

computed 用于根据已有状态计算新数据。

import { computed, ref } from 'vue' const price = ref(100) const count = ref(2) const totalPrice = computed(() => { return price.value * count.value })

模板:

<text>合计:¥{{ totalPrice }}</text>

适用场景:

  • 总金额
  • 筛选后的列表
  • 按钮是否禁用
  • 登录状态
  • 格式化后的显示文字

10.5 watch

监听某个响应式数据变化:

import { ref, watch } from 'vue' const keyword = ref('') watch(keyword, (newValue, oldValue) => { console.log('新值:', newValue) console.log('旧值:', oldValue) })

监听对象中的字段:

watch( () => form.username, username => { console.log(username) } )

监听多个值:

watch( [keyword, currentPage], ([newKeyword, newPage]) => { console.log(newKeyword, newPage) } )

监听复杂对象:

watch( form, value => { console.log(value) }, { deep: true } )

不要滥用深度监听。能监听具体字段时,优先监听具体字段。


10.6 watchEffect

watchEffect 会自动收集函数中使用的响应式依赖:

import { ref, watchEffect } from 'vue' const keyword = ref('') const currentPage = ref(1) watchEffect(() => { console.log(keyword.value) console.log(currentPage.value) })

适合:

  • 简单副作用
  • 自动同步状态
  • 依赖较多且不想手动声明

接口搜索、防抖请求等场景通常使用 watch 更容易控制。


10.7 v-bind

动态绑定属性:

<image :src="avatarUrl" /> <button :disabled="loading"> 提交 </button>

完整写法:

<image v-bind:src="avatarUrl" />

常用简写:

<image :src="avatarUrl" />

动态 Class:

<view :class="{ active: selected, disabled: loading }" > 内容 </view>

数组写法:

<view :class="['card', selected ? 'active' : '']"> 内容 </view>

10.8 v-on

事件绑定:

<button @tap="handleClick"> 点击 </button>

完整写法:

<button v-on:tap="handleClick"> 点击 </button>

传递参数:

<view @tap="openDetail(item.id)"> 查看详情 </view>

阻止冒泡:

<view @tap="openCard"> <button @tap.stop="deleteItem"> 删除 </button> </view>

uni-app 中通常可以使用 @click,但在微信小程序原生点击或自定义组件事件存在歧义时,使用 @tap 更稳妥。Vue 3 已移除 .native 修饰符。


10.9 v-model

双向绑定:

const keyword = ref('')
<input v-model="keyword" />

相当于把输入值同步给变量。

表单示例:

<input v-model="form.username" placeholder="用户名" /> <input v-model="form.password" password placeholder="密码" /> <switch v-model="form.remember" />

10.10 v-if

条件渲染:

<view v-if="loading"> 加载中 </view> <view v-else-if="errorMessage"> {{ errorMessage }} </view> <view v-else> 页面内容 </view>

v-iffalse 时,对应节点不会被渲染。


10.11 v-show

<view v-show="visible"> 内容 </view>

v-show 一般通过切换样式控制显示和隐藏。

选择原则:

很少变化:v-if 频繁切换:v-show

10.12 v-for

列表渲染:

<view v-for="item in productList" :key="item.id" > {{ item.name }} </view>

带下标:

<view v-for="(item, index) in productList" :key="item.id" > {{ index + 1 }}. {{ item.name }} </view>

尽量使用稳定、唯一的业务 ID 作为 key,避免直接使用数组下标。


十一、组件开发

创建组件:

src/components/ProductCard.vue
<script setup lang="ts"> interface Product { id: number name: string price: number } defineProps<{ product: Product }>() const emit = defineEmits<{ select: [id: number] }>() function handleClick(id: number) { emit('select', id) } </script> <template> <view class="product-card" @tap="handleClick(product.id)" > <text>{{ product.name }}</text> <text>¥{{ product.price }}</text> </view> </template>

父页面使用:

<script setup lang="ts"> import ProductCard from '@/components/ProductCard.vue' function handleSelect(id: number) { console.log('选中商品:', id) } </script> <template> <ProductCard v-for="item in productList" :key="item.id" :product="item" @select="handleSelect" /> </template>

11.1 defineProps

用于接收父组件数据:

const props = defineProps<{ title: string count?: number }>()

使用:

console.log(props.title)

模板中可以直接使用:

<text>{{ title }}</text>

11.2 withDefaults

为 Props 设置默认值:

interface Props { title?: string showMore?: boolean } const props = withDefaults( defineProps<Props>(), { title: '', showMore: false } )

11.3 defineEmits

子组件向父组件发送事件:

const emit = defineEmits<{ confirm: [value: string] cancel: [] }>() emit('confirm', '提交内容') emit('cancel')

父组件:

<CustomDialog @confirm="handleConfirm" @cancel="handleCancel" />

11.4 插槽 Slot

子组件:

<template> <view class="card"> <view class="card-header"> <slot name="header" /> </view> <view class="card-body"> <slot /> </view> </view> </template>

父组件:

<CustomCard> <template #header> <text>卡片标题</text> </template> <text>卡片正文</text> </CustomCard>

11.5 defineExpose

父组件需要调用子组件方法时使用:

<script setup lang="ts"> function reset() { console.log('重置组件') } defineExpose({ reset }) </script>

父组件:

<script setup lang="ts"> import { ref } from 'vue' import SearchForm from '@/components/SearchForm.vue' const searchFormRef = ref<InstanceType<typeof SearchForm> | null>(null) function handleReset() { searchFormRef.value?.reset() } </script> <template> <SearchForm ref="searchFormRef" /> </template>

不过大多数场景优先使用 Props 和 Emits,不要过度依赖组件实例调用。


十二、Vue 生命周期与 uni-app 页面生命周期

这是 uni-app 和普通 Vue Web 项目区别较大的地方。

12.1 Vue 组件生命周期

import { onMounted, onUpdated, onUnmounted } from 'vue' onMounted(() => { console.log('组件挂载完成') }) onUpdated(() => { console.log('组件更新完成') }) onUnmounted(() => { console.log('组件销毁') })

Vue 生命周期主要关注组件自身。


12.2 uni-app 页面生命周期

import { onLoad, onShow, onReady, onHide, onUnload } from '@dcloudio/uni-app'

onLoad

页面首次加载时执行,可以接收路由参数:

onLoad(options => { console.log(options.id) })

onShow

页面每次显示时都会执行:

onShow(() => { refreshData() })

例如:

首页进入详情页 → 返回首页 → 首页再次执行 onShow

onReady

页面首次渲染完成:

onReady(() => { console.log('首次渲染完成') })

onHide

页面进入后台或被其他页面覆盖:

onHide(() => { console.log('页面隐藏') })

onUnload

页面被关闭:

onUnload(() => { clearInterval(timer) })

官方页面生命周期中,onLoad 在页面加载时触发,onShow 在页面每次显示时触发,onReady 在页面首次渲染完成时触发。


12.3 常用页面生命周期

import { onLoad, onShow, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app' onLoad(options => { console.log('页面参数', options) loadData() }) onShow(() => { console.log('页面显示') }) onPullDownRefresh(async () => { try { await loadData() } finally { uni.stopPullDownRefresh() } }) onReachBottom(() => { loadMore() })

适用场景:

onLoad 初始化数据 onShow 返回页面后刷新 onPullDownRefresh 下拉刷新 onReachBottom 上拉加载更多 onUnload 清理定时器和监听

十三、页面跳转与传参

uni-app 页面路由不是 Vue Router,而是由 pages.json 和路由 API 统一管理。

13.1 普通页面跳转

uni.navigateTo({ url: '/pages/detail/detail?id=100' })

详情页接收参数:

import { onLoad } from '@dcloudio/uni-app' onLoad(options => { const id = Number(options?.id) console.log(id) })

13.2 返回上一页

uni.navigateBack({ delta: 1 })

13.3 关闭当前页面并跳转

uni.redirectTo({ url: '/pages/login/login' })

13.4 跳转到 tabBar 页面

uni.switchTab({ url: '/pages/index/index' })

tabBar 页面必须使用 switchTab,不能使用 navigateTo。目标页面也必须已在 pages.json 中注册。


13.5 关闭所有页面并重新打开

uni.reLaunch({ url: '/pages/index/index' })

常用于:

  • 登录成功后进入首页
  • 退出登录后进入登录页
  • 重置整个页面栈

十四、网络请求

uni-app 使用:

uni.request()

小程序平台发送网络请求前,需要在微信小程序后台配置合法请求域名。

14.1 基础请求

uni.request({ url: 'https://api.example.com/products', method: 'GET', success(response) { console.log(response.data) }, fail(error) { console.error(error) } })

正式项目不要在页面里到处直接调用 uni.request,建议统一封装。


14.2 请求封装

创建:

src/api/request.ts
const BASE_URL = 'https://api.example.com' export interface ApiResponse<T> { code: number message: string data: T } export interface RequestOptions extends Omit<UniApp.RequestOptions, 'url' | 'success' | 'fail'> { url: string showLoading?: boolean } export function request<T>( options: RequestOptions ): Promise<T> { const { url, header, showLoading = false, ...restOptions } = options const token = uni.getStorageSync('token') if (showLoading) { uni.showLoading({ title: '加载中', mask: true }) } return new Promise<T>((resolve, reject) => { uni.request({ ...restOptions, url: `${BASE_URL}${url}`, header: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), ...header }, success(response) { const statusCode = response.statusCode if (statusCode < 200 || statusCode >= 300) { reject( new Error(`HTTP 请求失败:${statusCode}`) ) return } const result = response.data as ApiResponse<T> if (result.code === 0) { resolve(result.data) return } if (result.code === 401) { uni.removeStorageSync('token') uni.reLaunch({ url: '/pages/login/login' }) } uni.showToast({ title: result.message || '请求失败', icon: 'none' }) reject(new Error(result.message)) }, fail(error) { uni.showToast({ title: '网络请求失败', icon: 'none' }) reject(error) }, complete() { if (showLoading) { uni.hideLoading() } } }) }) }

使用:

import { request } from '@/api/request' interface Product { id: number name: string price: number } function getProductList() { return request<Product[]>({ url: '/products', method: 'GET', showLoading: true }) }

页面:

const productList = ref<Product[]>([]) async function loadData() { try { productList.value = await getProductList() } catch (error) { console.error(error) } }

十五、本地缓存

15.1 保存数据

uni.setStorageSync('token', 'abc123')

15.2 获取数据

const token = uni.getStorageSync('token')

15.3 删除数据

uni.removeStorageSync('token')

15.4 清空缓存

uni.clearStorageSync()

建议:

token、用户基础信息 可以缓存 密码、敏感隐私信息 不要缓存 超大的接口列表 谨慎缓存 需要实时更新的数据 设置过期时间

可封装带过期时间的缓存:

interface StorageValue<T> { value: T expireAt?: number } export function setStorage<T>( key: string, value: T, duration?: number ) { const data: StorageValue<T> = { value, expireAt: duration ? Date.now() + duration : undefined } uni.setStorageSync(key, data) } export function getStorage<T>( key: string ): T | null { const data = uni.getStorageSync(key) as StorageValue<T> | undefined if (!data) { return null } if ( data.expireAt && Date.now() > data.expireAt ) { uni.removeStorageSync(key) return null } return data.value }

十六、Pinia 状态管理

Pinia 用于跨页面和跨组件共享状态,例如:

  • 当前登录用户
  • Token
  • 购物车
  • 应用配置
  • 消息未读数
  • 全局筛选条件

uni-app 的 Vue 3 工程支持 Pinia;HBuilderX 已内置 Pinia,CLI 项目可以通过 npm 安装。

安装:

npm install pinia

16.1 注册 Pinia

修改 main.ts

import App from './App.vue' import { createSSRApp } from 'vue' import { createPinia } from 'pinia' export function createApp() { const app = createSSRApp(App) const pinia = createPinia() app.use(pinia) return { app } }

16.2 创建用户 Store

创建:

src/stores/user.ts
import { computed, ref } from 'vue' import { defineStore } from 'pinia' interface UserInfo { id: number nickname: string avatar: string } export const useUserStore = defineStore( 'user', () => { const token = ref( uni.getStorageSync('token') || '' ) const userInfo = ref<UserInfo | null>(null) const isLoggedIn = computed(() => { return Boolean(token.value) }) function setToken(value: string) { token.value = value uni.setStorageSync('token', value) } function setUserInfo(value: UserInfo) { userInfo.value = value } function logout() { token.value = '' userInfo.value = null uni.removeStorageSync('token') uni.reLaunch({ url: '/pages/login/login' }) } return { token, userInfo, isLoggedIn, setToken, setUserInfo, logout } } )

页面使用:

import { useUserStore } from '@/stores/user' const userStore = useUserStore() console.log(userStore.isLoggedIn) userStore.setToken('new-token')

解构 Store 时使用:

import { storeToRefs } from 'pinia' const userStore = useUserStore() const { token, userInfo, isLoggedIn } = storeToRefs(userStore)

不要直接这样解构响应式状态:

const { token } = userStore

因为可能失去响应式关联。


十七、样式开发

uni-app 支持 px、rpx 等 CSS 单位,也支持 Sass、SCSS、Less 等预处理器。rpx 会根据屏幕宽度进行适配,uni-app 规定屏幕基准宽度为 750rpx

17.1 推荐使用 rpx

.card { width: 686rpx; padding: 32rpx; border-radius: 20rpx; }

设计稿宽度为 750px 时:

设计稿 100px ≈ 代码 100rpx

设计稿宽度为 375px 时:

设计稿 100px ≈ 代码 200rpx

17.2 使用 class 选择器

推荐:

<view class="product-card"> 内容 </view>
.product-card { padding: 24rpx; }

尽量不要依赖:

div { } p { }

Vue 3 小程序平台的样式应优先使用 class 选择器,尤其是自定义组件。


17.3 全局样式

App.vue

<style lang="scss"> page { min-height: 100%; background: #f5f5f5; color: #222222; font-size: 28rpx; } button::after { border: none; } </style>

页面背景一般设置在 page 选择器上。

不要给该段样式添加 scoped,否则全局 page 样式可能无法生效。


十八、条件编译

当某段代码只在微信小程序运行时,可以使用条件编译。

18.1 模板条件编译

<!-- #ifdef MP-WEIXIN --> <button open-type="contact"> 联系微信客服 </button> <!-- #endif -->

18.2 JavaScript 条件编译

// #ifdef MP-WEIXIN console.log('当前是微信小程序') // #endif // #ifdef H5 console.log('当前是 H5') // #endif

18.3 样式条件编译

/* #ifdef MP-WEIXIN */ .page { padding-bottom: 40rpx; } /* #endif */

微信小程序的平台标识是:

MP-WEIXIN

样式中的条件编译必须使用 /* */ 注释形式。

不要把整个项目写成大量条件编译。推荐先封装统一接口:

export function login() { // #ifdef MP-WEIXIN return loginByWeixin() // #endif // #ifdef H5 return loginByWeb() // #endif }

页面只调用:

await login()

十九、封装可复用 Composition API

Vue 3 中可以把可复用的状态逻辑封装为 Composable,作用类似 React 的自定义 Hook。Vue 官方将 Composable 定义为使用 Composition API 封装和复用有状态逻辑的函数。

创建:

src/composables/usePagination.ts
import { ref } from 'vue' interface PaginationOptions<T> { request: ( page: number, pageSize: number ) => Promise<T[]> pageSize?: number } export function usePagination<T>( options: PaginationOptions<T> ) { const list = ref<T[]>([]) const page = ref(1) const loading = ref(false) const finished = ref(false) const pageSize = options.pageSize ?? 20 async function loadMore() { if (loading.value || finished.value) { return } loading.value = true try { const result = await options.request( page.value, pageSize ) list.value.push(...result) if (result.length < pageSize) { finished.value = true } else { page.value += 1 } } finally { loading.value = false } } async function refresh() { page.value = 1 finished.value = false list.value = [] await loadMore() } return { list, page, loading, finished, loadMore, refresh } }

页面使用:

const { list, loading, finished, loadMore, refresh } = usePagination<Product>({ request: getProductList }) onLoad(() => { loadMore() }) onReachBottom(() => { loadMore() }) onPullDownRefresh(async () => { try { await refresh() } finally { uni.stopPullDownRefresh() } })

二十、Vue 3 与 React 常用概念对照

Vue 3React主要用途
refuseState响应式状态
reactive多字段 state对象状态
computeduseMemo派生数据
watchuseEffect监听指定状态
watchEffect自动依赖版 useEffect自动收集依赖
onMounted空依赖 useEffect组件挂载
onUnmounteduseEffect cleanup组件卸载
ComposableCustom Hook复用状态逻辑
PropsProps父传子
Emits回调 Props子传父
PiniaZustand/Redux全局状态

这只是帮助建立直觉,两套框架的具体运行机制并不完全相同。


二十一、推荐的页面代码结构

建议页面内部按照以下顺序组织:

<script setup lang="ts"> // 1. 第三方依赖 import { computed, ref } from 'vue' import { onLoad, onShow } from '@dcloudio/uni-app' // 2. 项目内部依赖 import { getProductList } from '@/api/product' import { useUserStore } from '@/stores/user' // 3. 类型 interface Product { id: number name: string } // 4. Store const userStore = useUserStore() // 5. 状态 const loading = ref(false) const productList = ref<Product[]>([]) // 6. 计算属性 const hasData = computed(() => { return productList.value.length > 0 }) // 7. 普通方法 async function loadData() { loading.value = true try { productList.value = await getProductList() } finally { loading.value = false } } // 8. 事件方法 function handleProductClick(id: number) { uni.navigateTo({ url: `/pages/detail/detail?id=${id}` }) } // 9. 生命周期 onLoad(() => { loadData() }) onShow(() => { console.log('页面显示') }) </script>

这种顺序在页面变复杂后更容易维护。


二十二、常见错误

22.1 页面没有注册

错误:

navigateTo:fail page not found

检查:

pages.json 是否注册 路径是否拼写正确 路径是否带了多余的 .vue

22.2 tabBar 页面使用 navigateTo

错误写法:

uni.navigateTo({ url: '/pages/home/home' })

正确写法:

uni.switchTab({ url: '/pages/home/home' })

22.3 在 TypeScript 中忘记 .value

错误:

const count = ref(0) count++

正确:

count.value++

模板中不需要:

<text>{{ count }}</text>

22.4 直接使用浏览器 API

不推荐:

localStorage.setItem('token', token) window.location.href = '/' document.querySelector('.card')

推荐:

uni.setStorageSync('token', token) uni.reLaunch({ url: '/pages/index/index' })

需要获取节点信息时,使用 uni-app 提供的节点查询 API,而不是直接访问 DOM。


22.5 忘记配置请求域名

开发者工具中可能关闭了域名校验,所以本地请求正常;真机或正式版可能请求失败。

正式发布前必须检查微信公众平台中的服务器域名配置。


22.6 把 onMounted 当作每次进入页面

onMounted 通常只关注组件首次挂载。

从详情页返回列表页,需要重新刷新时,使用:

onShow(() => { refreshData() })

而不是只依赖:

onMounted(() => { refreshData() })

22.7 页面销毁后定时器仍在执行

let timer: ReturnType<typeof setInterval> | null = null onLoad(() => { timer = setInterval(() => { console.log('轮询') }, 5000) }) onUnload(() => { if (timer) { clearInterval(timer) timer = null } })

网络请求、定时器和事件监听等副作用,应在页面关闭时主动处理。uni-app 页面关闭后,未完成请求和定时器不会自动替开发者完成业务清理。


二十三、初学阶段最应该掌握的 Vue 3 语法

第一优先级:

<script setup> ref computed v-if v-for v-model v-bind v-on defineProps defineEmits onLoad onShow uni.navigateTo uni.request

第二优先级:

reactive watch watchEffect onMounted onUnmounted Slot Pinia Composable onPullDownRefresh onReachBottom

第三优先级:

provide / inject defineExpose nextTick 自定义指令 高级组件封装 平台条件编译 分包 性能优化

初期不需要一上来学习:

render 函数 JSX 复杂自定义指令 底层编译原理 过度抽象的组件体系

二十四、推荐学习路线

第一阶段:完成静态页面

目标:

掌握 view、text、image 掌握 rpx 掌握 flex 布局 掌握 pages.json 掌握页面跳转

练习项目:

商品列表 商品详情 个人中心

第二阶段:接入接口

目标:

封装 uni.request 实现 loading 实现错误提示 实现下拉刷新 实现分页加载 实现登录 Token

第三阶段:组件化

目标:

Props Emits Slot 通用弹窗 列表卡片 空状态组件 分页 Composable

第四阶段:全局状态

目标:

Pinia 用户信息 登录状态 购物车 权限判断 缓存同步

第五阶段:微信能力

目标:

微信登录 获取手机号 订阅消息 分享 扫码 地图定位 支付 客服

这些能力通常需要真实微信小程序 AppID、后台配置和服务端接口配合。


二十五、项目开发规范建议

命名规范

页面目录:

pages/product-detail/product-detail.vue

组件:

ProductCard.vue UserAvatar.vue AppEmpty.vue

Composable:

usePagination.ts useAuth.ts useUpload.ts

Store:

user.ts cart.ts app.ts

接口模块:

api/user.ts api/product.ts api/order.ts

方法命名

用户操作:

handleSubmit() handleLogin() handleProductClick()

数据请求:

loadData() loadMore() refreshData() getProductDetail()

状态修改:

setToken() setUserInfo() resetForm()

二十六、最终推荐的技术架构

页面层 pages 业务组件 components Composable 复用逻辑 Pinia 全局状态 API 模块 统一 request 封装 NestJS 后端接口

页面不应直接处理大量底层请求逻辑。

推荐:

// 页面 const list = await getProductList(params)

而不是在每个页面重复:

uni.request({ url: '...', header: { Authorization: '...' }, success() { // 重复判断状态码 // 重复判断登录过期 // 重复显示错误 } })

对于正式项目,应该逐步形成:

页面负责交互 组件负责展示 Composable 负责复用逻辑 Pinia 负责全局状态 API 层负责业务接口 request 负责网络基础能力

二十七、总结

uni-app + Vue 3 开发微信小程序,本质上需要同时掌握三套知识:

Vue 3 的响应式与组件开发 uni-app 的页面、路由和跨端 API 微信小程序的平台规则和发布流程

初学时最重要的不是一次学完所有 API,而是先掌握下面这条主线:

创建页面 → 在 pages.json 注册 → 使用 Vue 3 管理状态 → 使用 uni.navigateTo 跳转 → 使用 uni.request 调接口 → 使用 Pinia 管理登录状态 → 在微信开发者工具中调试

掌握这条主线后,已经能够开发大多数列表、详情、表单、登录和个人中心类微信小程序。

文章标签
admin
版权声明

本文采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!

分享到: