uni-app + Vue 3 微信小程序开发入门指南
适用场景:使用 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 为主,但小程序并不是浏览器环境,因此需要注意:
- 页面标签主要使用
view、text、image、button、input,而不是div、span、img。 - 页面跳转不使用 Vue Router。
- 网络请求使用
uni.request,而不是直接依赖浏览器的fetch。 - 本地缓存使用
uni.setStorageSync等 API。 - 不应直接操作
window、document、localStorage。 - 除了 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-if 为 false 时,对应节点不会被渲染。
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 3 | React | 主要用途 |
|---|---|---|
ref | useState | 响应式状态 |
reactive | 多字段 state | 对象状态 |
computed | useMemo | 派生数据 |
watch | useEffect | 监听指定状态 |
watchEffect | 自动依赖版 useEffect | 自动收集依赖 |
onMounted | 空依赖 useEffect | 组件挂载 |
onUnmounted | useEffect cleanup | 组件卸载 |
| Composable | Custom Hook | 复用状态逻辑 |
| Props | Props | 父传子 |
| Emits | 回调 Props | 子传父 |
| Pinia | Zustand/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 管理登录状态 → 在微信开发者工具中调试
掌握这条主线后,已经能够开发大多数列表、详情、表单、登录和个人中心类微信小程序。
本文采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!
相关文章
在 Linux 系统中如何使用 yum 安装 nginx?
在Linux系统中,使用yum来安装Nginx的步骤如下:具体步骤更新软件包仓库:确保你的软件包仓库是最新的。运行以下命令来更新:<sp
把服务器迁移到阿里云了
之前贪便宜花了几百块买了华为云的ecs服务器,服务运行了一年多懒得换,但是最近华为云要求域名必须在华为云备案才可以解析,否则域名解析会被做阻断处理,于是索性把服务迁移到阿里云,毕竟阿里云的服务比华为云强的不是一点。linux用的不是很熟,尤其是装一些必备的服务,这次做个笔记...
俄罗斯方块生成算法
俄罗斯方块是一款经典的拼图游戏,其核心算法包含方块生成、方块移动、旋转、碰撞检测等功能。我们这里重点介绍方块生成的算法,并使用JavaScript实现它。1.方块生成逻辑俄罗斯方块中的方块称为「Tetrominoes」,一共有7种不同的形状,每种形状由4个方块组成。它们通常...
