uni-app 开发指南
简介
uni-app 是一个使用 Vue.js 开发所有前端应用的框架,开发者编写一套代码,可发布到 iOS、Android、Web(响应式)、以及各种小程序(微信/支付宝/百度/头条/QQ/钉钉/淘宝)、快应用等多个平台。本文将介绍 uni-app 的核心概念、开发流程和最佳实践。
项目创建
1. 环境准备
bash
# 安装 Vue CLI
npm install -g @vue/cli
# 安装 uni-app CLI
npm install -g @dcloudio/vue-cli-plugin-uni
# 创建项目
vue create -p dcloudio/uni-preset-vue my-project2. 项目结构
bash
my-project/
├── src/
│ ├── pages/ # 页面文件夹
│ ├── static/ # 静态资源
│ ├── components/ # 组件文件夹
│ ├── store/ # Vuex 状态管理
│ ├── utils/ # 工具函数
│ ├── App.vue # 应用配置
│ ├── main.js # 入口文件
│ ├── manifest.json # 配置文件
│ └── pages.json # 页面配置
├── package.json
└── README.md页面开发
1. 页面配置
json
// pages.json
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"enablePullDownRefresh": true
}
},
{
"path": "pages/user/user",
"style": {
"navigationBarTitleText": "用户中心"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
},
"tabBar": {
"color": "#7A7E83",
"selectedColor": "#3cc51f",
"borderStyle": "black",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/index/index",
"iconPath": "static/tabbar/home.png",
"selectedIconPath": "static/tabbar/home-active.png",
"text": "首页"
},
{
"pagePath": "pages/user/user",
"iconPath": "static/tabbar/user.png",
"selectedIconPath": "static/tabbar/user-active.png",
"text": "我的"
}
]
}
}2. 页面开发
vue
<!-- pages/index/index.vue -->
<template>
<view class="container">
<view class="header">
<text class="title">{{ title }}</text>
</view>
<view class="content">
<view class="card" v-for="(item, index) in list" :key="index">
<image :src="item.image" mode="aspectFill"></image>
<text class="card-title">{{ item.title }}</text>
</view>
</view>
<view class="footer">
<button @tap="loadMore">加载更多</button>
</view>
</view>
</template>
<script>
export default {
data() {
return {
title: '首页',
list: [],
page: 1
}
},
onLoad() {
this.loadData()
},
methods: {
async loadData() {
try {
const res = await this.$api.getList({
page: this.page,
size: 10
})
this.list = [...this.list, ...res.data]
} catch (error) {
uni.showToast({
title: '加载失败',
icon: 'none'
})
}
},
loadMore() {
this.page++
this.loadData()
}
}
}
</script>
<style lang="scss">
.container {
padding: 20rpx;
.header {
margin-bottom: 30rpx;
.title {
font-size: 36rpx;
font-weight: bold;
}
}
.content {
.card {
margin-bottom: 20rpx;
background: #fff;
border-radius: 12rpx;
overflow: hidden;
image {
width: 100%;
height: 300rpx;
}
.card-title {
padding: 20rpx;
font-size: 28rpx;
}
}
}
.footer {
margin-top: 30rpx;
text-align: center;
button {
width: 200rpx;
height: 80rpx;
line-height: 80rpx;
font-size: 28rpx;
}
}
}
</style>组件开发
1. 自定义组件
vue
<!-- components/custom-card.vue -->
<template>
<view class="custom-card" @tap="handleTap">
<image :src="image" mode="aspectFill"></image>
<view class="content">
<text class="title">{{ title }}</text>
<text class="desc">{{ description }}</text>
</view>
<view class="footer">
<slot name="footer"></slot>
</view>
</view>
</template>
<script>
export default {
name: 'CustomCard',
props: {
image: {
type: String,
required: true
},
title: {
type: String,
required: true
},
description: {
type: String,
default: ''
}
},
methods: {
handleTap() {
this.$emit('tap')
}
}
}
</script>
<style lang="scss">
.custom-card {
background: #fff;
border-radius: 12rpx;
overflow: hidden;
margin-bottom: 20rpx;
image {
width: 100%;
height: 300rpx;
}
.content {
padding: 20rpx;
.title {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 10rpx;
}
.desc {
font-size: 28rpx;
color: #666;
}
}
.footer {
padding: 20rpx;
border-top: 1rpx solid #eee;
}
}
</style>2. 组件使用
vue
<!-- pages/index/index.vue -->
<template>
<view class="container">
<custom-card
v-for="(item, index) in list"
:key="index"
:image="item.image"
:title="item.title"
:description="item.description"
@tap="handleCardTap(item)"
>
<template #footer>
<view class="card-footer">
<text class="price">¥{{ item.price }}</text>
<button size="mini" @tap.stop="handleBuy(item)">购买</button>
</view>
</template>
</custom-card>
</view>
</template>
<script>
import CustomCard from '@/components/custom-card.vue'
export default {
components: {
CustomCard
},
data() {
return {
list: []
}
},
methods: {
handleCardTap(item) {
uni.navigateTo({
url: `/pages/detail/detail?id=${item.id}`
})
},
handleBuy(item) {
// 处理购买逻辑
}
}
}
</script>状态管理
1. Vuex 配置
javascript
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
user: null,
cart: []
},
mutations: {
SET_USER(state, user) {
state.user = user
},
ADD_TO_CART(state, product) {
state.cart.push(product)
}
},
actions: {
async login({ commit }, credentials) {
try {
const res = await uni.request({
url: '/api/login',
method: 'POST',
data: credentials
})
commit('SET_USER', res.data)
return res.data
} catch (error) {
throw error
}
}
},
getters: {
isLoggedIn: state => !!state.user,
cartCount: state => state.cart.length
}
})2. 使用 Vuex
vue
<!-- pages/user/user.vue -->
<template>
<view class="container">
<view v-if="isLoggedIn">
<text>欢迎, {{ user.name }}</text>
<button @tap="logout">退出登录</button>
</view>
<view v-else>
<button @tap="login">登录</button>
</view>
</view>
</template>
<script>
import { mapState, mapGetters, mapActions } from 'vuex'
export default {
computed: {
...mapState(['user']),
...mapGetters(['isLoggedIn'])
},
methods: {
...mapActions(['login']),
async handleLogin() {
try {
await this.login({
username: 'test',
password: '123456'
})
uni.showToast({
title: '登录成功'
})
} catch (error) {
uni.showToast({
title: '登录失败',
icon: 'none'
})
}
},
logout() {
this.$store.commit('SET_USER', null)
}
}
}
</script>网络请求
1. 请求封装
javascript
// utils/request.js
const baseURL = 'https://api.example.com'
export const request = (options) => {
return new Promise((resolve, reject) => {
uni.request({
url: baseURL + options.url,
method: options.method || 'GET',
data: options.data,
header: {
'Content-Type': 'application/json',
...options.header
},
success: (res) => {
if (res.statusCode === 200) {
resolve(res.data)
} else {
reject(res)
}
},
fail: (err) => {
reject(err)
}
})
})
}
// api/index.js
import { request } from '@/utils/request'
export const api = {
// 获取列表
getList(params) {
return request({
url: '/list',
method: 'GET',
data: params
})
},
// 获取详情
getDetail(id) {
return request({
url: `/detail/${id}`,
method: 'GET'
})
},
// 提交表单
submitForm(data) {
return request({
url: '/submit',
method: 'POST',
data
})
}
}2. 使用请求
vue
<!-- pages/detail/detail.vue -->
<template>
<view class="container">
<view v-if="loading">加载中...</view>
<view v-else-if="error">{{ error }}</view>
<view v-else>
<image :src="detail.image" mode="aspectFill"></image>
<text class="title">{{ detail.title }}</text>
<text class="price">¥{{ detail.price }}</text>
<text class="desc">{{ detail.description }}</text>
</view>
</view>
</template>
<script>
import { api } from '@/api'
export default {
data() {
return {
loading: true,
error: null,
detail: null
}
},
onLoad(options) {
this.loadDetail(options.id)
},
methods: {
async loadDetail(id) {
try {
this.loading = true
const res = await api.getDetail(id)
this.detail = res.data
} catch (error) {
this.error = '加载失败'
} finally {
this.loading = false
}
}
}
}
</script>条件编译
vue
<template>
<view>
<!-- #ifdef H5 -->
<view>H5 平台特有内容</view>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<view>微信小程序特有内容</view>
<!-- #endif -->
<!-- #ifdef APP-PLUS -->
<view>App 平台特有内容</view>
<!-- #endif -->
</view>
</template>
<script>
export default {
data() {
return {
// #ifdef H5
platform: 'H5'
// #endif
// #ifdef MP-WEIXIN
platform: '微信小程序'
// #endif
// #ifdef APP-PLUS
platform: 'App'
// #endif
}
},
methods: {
handlePlatform() {
// #ifdef H5
console.log('H5 平台')
// #endif
// #ifdef MP-WEIXIN
console.log('微信小程序平台')
// #endif
// #ifdef APP-PLUS
console.log('App 平台')
// #endif
}
}
}
</script>
<style>
/* #ifdef H5 */
.h5-style {
color: red;
}
/* #endif */
/* #ifdef MP-WEIXIN */
.wx-style {
color: green;
}
/* #endif */
/* #ifdef APP-PLUS */
.app-style {
color: blue;
}
/* #endif */
</style>性能优化
1. 图片优化
vue
<template>
<view>
<!-- 使用 webp 格式 -->
<image src="/static/image.webp" mode="aspectFill"></image>
<!-- 使用懒加载 -->
<image lazy-load src="/static/image.jpg" mode="aspectFill"></image>
<!-- 使用图片预加载 -->
<image v-for="(item, index) in images"
:key="index"
:src="item"
mode="aspectFill"
@load="onImageLoad(index)">
</image>
</view>
</template>
<script>
export default {
data() {
return {
images: [
'/static/image1.jpg',
'/static/image2.jpg',
'/static/image3.jpg'
],
loadedImages: 0
}
},
methods: {
onImageLoad(index) {
this.loadedImages++
if (this.loadedImages === this.images.length) {
console.log('所有图片加载完成')
}
}
}
}
</script>2. 列表优化
vue
<template>
<view>
<!-- 使用虚拟列表 -->
<recycle-list
:list="list"
:height="800"
:item-height="100"
>
<template v-slot="{ item }">
<view class="list-item">
<text>{{ item.title }}</text>
</view>
</template>
</recycle-list>
<!-- 使用分页加载 -->
<view class="list">
<view v-for="(item, index) in displayList"
:key="index"
class="list-item">
<text>{{ item.title }}</text>
</view>
</view>
<view v-if="hasMore" class="load-more" @tap="loadMore">
加载更多
</view>
</view>
</template>
<script>
export default {
data() {
return {
list: [],
page: 1,
pageSize: 10,
hasMore: true
}
},
computed: {
displayList() {
return this.list.slice(0, this.page * this.pageSize)
}
},
methods: {
async loadMore() {
if (!this.hasMore) return
try {
const res = await this.$api.getList({
page: this.page,
size: this.pageSize
})
this.list = [...this.list, ...res.data]
this.page++
this.hasMore = res.data.length === this.pageSize
} catch (error) {
uni.showToast({
title: '加载失败',
icon: 'none'
})
}
}
}
}
</script>最佳实践
1. 项目结构
bash
src/
├── api/ # API 接口
├── components/ # 公共组件
├── pages/ # 页面
├── static/ # 静态资源
├── store/ # Vuex 状态管理
├── utils/ # 工具函数
├── App.vue # 应用配置
├── main.js # 入口文件
├── manifest.json # 配置文件
└── pages.json # 页面配置2. 代码规范
javascript
// 使用 ESLint 配置
// .eslintrc.js
module.exports = {
root: true,
env: {
node: true
},
extends: [
'plugin:vue/essential',
'@vue/standard'
],
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
}
}
// 使用 Prettier 配置
// .prettierrc
{
"semi": false,
"singleQuote": true,
"printWidth": 80,
"trailingComma": "none"
}工具链
1. 开发工具
json
{
"devDependencies": {
"@dcloudio/vue-cli-plugin-uni": "^2.0.0",
"@vue/cli-plugin-babel": "^4.5.0",
"@vue/cli-plugin-eslint": "^4.5.0",
"@vue/cli-service": "^4.5.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"sass": "^1.26.5",
"sass-loader": "^8.0.2"
}
}2. 发布工具
json
{
"scripts": {
"dev:app": "uni -p app",
"dev:custom": "uni -p",
"dev:h5": "uni",
"dev:h5:ssr": "uni --ssr",
"dev:mp-alipay": "uni -p mp-alipay",
"dev:mp-baidu": "uni -p mp-baidu",
"dev:mp-kuaishou": "uni -p mp-kuaishou",
"dev:mp-lark": "uni -p mp-lark",
"dev:mp-qq": "uni -p mp-qq",
"dev:mp-toutiao": "uni -p mp-toutiao",
"dev:mp-weixin": "uni -p mp-weixin",
"dev:quickapp-webview": "uni -p quickapp-webview",
"dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
"dev:quickapp-webview-union": "uni -p quickapp-webview-union",
"build:app": "uni build -p app",
"build:custom": "uni build -p",
"build:h5": "uni build",
"build:h5:ssr": "uni build --ssr",
"build:mp-alipay": "uni build -p mp-alipay",
"build:mp-baidu": "uni build -p mp-baidu",
"build:mp-kuaishou": "uni build -p mp-kuaishou",
"build:mp-lark": "uni build -p mp-lark",
"build:mp-qq": "uni build -p mp-qq",
"build:mp-toutiao": "uni build -p mp-toutiao",
"build:mp-weixin": "uni build -p mp-weixin",
"build:quickapp-webview": "uni build -p quickapp-webview",
"build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
"build:quickapp-webview-union": "uni build -p quickapp-webview-union"
}
}总结
uni-app 提供了跨平台开发的能力,通过一套代码可以同时开发多个平台的应用。通过合理使用框架特性和遵循最佳实践,可以构建出高性能、可维护的跨平台应用。