Skip to content

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-project

2. 项目结构

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 提供了跨平台开发的能力,通过一套代码可以同时开发多个平台的应用。通过合理使用框架特性和遵循最佳实践,可以构建出高性能、可维护的跨平台应用。

参考资源