前言

在移动互联网多元化的今天,一套代码同时运行在微信小程序、支付宝小程序、H5、App(iOS/Android)等多个平台,已经成为很多团队的刚需。UniApp 作为 DCloud 推出的跨端开发框架,基于 Vue.js 技术栈,凭借 "一次编写,多端运行" 的特性,已经成为国内跨端开发的主流方案之一。

本文将从 UniApp 的核心原理出发,带你系统掌握环境搭建、页面开发、组件封装、网络请求、状态管理、跨端兼容、性能优化等全链路知识,并结合完整的实战代码,帮助你快速构建高质量的跨端应用。文章内容兼顾入门与进阶,适合有一定 Vue 基础的前端开发者阅读。

一、UniApp 核心认知与技术架构

1.1 什么是 UniApp

UniApp 是一个使用 Vue.js 开发所有前端应用的框架,开发者编写一套代码,可发布到 iOS、Android、Web(响应式)、以及各种小程序(微信 / 支付宝 / 百度 / 头条 / 飞书 / QQ / 快手 / 钉钉 / 淘宝)、快应用等多个平台。

核心优势:

  • 跨端能力强:支持 10+ 平台,一套代码多端运行
  • 学习成本低:基于 Vue.js 语法,前端开发者上手快
  • 生态丰富:插件市场组件丰富,社区活跃
  • 性能优异:原生渲染,接近原生应用体验
  • 开发效率高:热重载、可视化调试工具完善

1.2 技术架构原理

UniApp 的底层采用了 "编译器 + 运行时" 的双引擎架构:

  1. 编译器:将 Vue 代码编译为各端可识别的代码

    • H5 端:编译为标准 HTML/CSS/JS
    • 小程序端:编译为对应小程序的 WXML/WXSS/JS 结构
    • App 端:编译为原生渲染视图 + JS 逻辑层
  2. 运行时:提供统一的 API 适配层,抹平各端差异

    • 封装了各平台的原生能力为统一 API
    • 处理生命周期、事件机制、组件差异
    • 提供页面路由、数据绑定等基础能力

1.3 适用场景与选型建议

推荐使用的场景:

  • 企业级业务系统的移动端适配
  • 内容展示、电商、工具类应用
  • 需要快速上线多端产品的创业项目
  • 团队技术栈以 Vue 为主

谨慎选择的场景:

  • 重度游戏、高性能图形渲染应用
  • 大量复杂原生交互的应用
  • 对包体大小有极致要求的纯原生场景

二、环境搭建与项目初始化

2.1 开发工具准备

开发 UniApp 官方推荐使用 HBuilderX,这是 DCloud 专门为 UniApp 打造的 IDE,内置了编译、运行、调试、打包等全套能力。

也可以使用 VS Code + 插件的方式开发,但 HBuilderX 在跨端编译和真机调试方面体验更优。

必备工具清单:

  • HBuilderX 最新版(App 开发版)
  • 微信开发者工具(小程序调试)
  • Chrome 浏览器(H5 调试)
  • Android Studio / Xcode(原生 App 调试)

2.2 创建第一个项目

打开 HBuilderX → 文件 → 新建 → 项目 → 选择 uni-app → 输入项目名称 → 选择模板。

推荐新手从 默认模板 开始,项目目录结构如下:

plaintext

┌─ common          // 公共资源
├─ components      // 自定义组件
├─ pages           // 页面目录
│  └─ index        // 首页
│     └─ index.vue
├─ static          // 静态资源(图片、字体等)
├─ App.vue         // 应用配置,全局样式、生命周期
├─ main.js         // 入口文件
├─ manifest.json   // 应用配置文件(各端配置)
├─ pages.json      // 页面路由、导航栏配置
└─ uni.scss        // 全局样式变量

2.3 运行到不同平台

在 HBuilderX 中点击 "运行" 菜单,可以选择运行到不同平台:

  • 运行到浏览器:快速开发调试,H5 模式
  • 运行到小程序模拟器:需要对应小程序开发者工具
  • 运行到手机或模拟器:App 端真机调试

以微信小程序为例,需要先在微信开发者工具中开启 "服务端口",然后 HBuilderX 会自动唤起开发者工具并编译运行。

三、页面开发核心语法

3.1 页面结构与生命周期

UniApp 的页面遵循 Vue 单文件组件规范,由 template、script、style 三部分组成。

完整页面示例:

vue

<template>
  <view class="container">
    <view class="title">{{ title }}</view>
    <view class="list">
      <view 
        v-for="(item, index) in list" 
        :key="index" 
        class="list-item"
        @click="handleItemClick(item)"
      >
        <text>{{ item.name }}</text>
      </view>
    </view>
    <button @click="loadMore" type="primary">加载更多</button>
  </view>
</template>

<script>
export default {
  data() {
    return {
      title: '商品列表',
      list: [],
      page: 1
    }
  },
  
  // 页面生命周期 - 页面加载
  onLoad(options) {
    console.log('页面参数:', options)
    this.getList()
  },
  
  // 页面生命周期 - 页面显示
  onShow() {
    console.log('页面显示')
  },
  
  // 页面生命周期 - 下拉刷新
  onPullDownRefresh() {
    this.page = 1
    this.getList().then(() => {
      uni.stopPullDownRefresh()
    })
  },
  
  // 页面生命周期 - 触底加载
  onReachBottom() {
    this.page++
    this.loadMore()
  },
  
  methods: {
    async getList() {
      // 模拟接口请求
      const res = await this.$request('/api/goods/list', { page: this.page })
      this.list = res.data
    },
    
    loadMore() {
      this.page++
      this.getList()
    },
    
    handleItemClick(item) {
      uni.navigateTo({
        url: `/pages/goods/detail?id=${item.id}`
      })
    }
  }
}
</script>

<style lang="scss" scoped>
.container {
  padding: 20rpx;
  
  .title {
    font-size: 32rpx;
    font-weight: bold;
    margin-bottom: 20rpx;
  }
  
  .list-item {
    padding: 24rpx;
    border-bottom: 1rpx solid #eee;
  }
}
</style>

3.2 重要生命周期说明

UniApp 有两套生命周期体系:应用生命周期页面生命周期组件生命周期

应用生命周期(App.vue):

  • onLaunch:应用初始化完成时触发(全局只触发一次)
  • onShow:应用从后台进入前台显示
  • onHide:应用从前台进入后台
  • onError:应用发生脚本错误或 API 调用失败

页面生命周期(重点):

表格

生命周期 触发时机 常用场景
onLoad 页面加载,参数可获取路由参数 数据初始化、接口请求
onShow 页面显示 刷新数据、状态重置
onReady 页面初次渲染完成 获取元素尺寸、DOM 操作
onHide 页面隐藏 暂停定时器、音频
onUnload 页面卸载 销毁定时器、解绑事件
onPullDownRefresh 下拉刷新 列表刷新
onReachBottom 滚动到底部 分页加载
onShareAppMessage 点击右上角分享 自定义分享内容

3.3 路由与页面跳转

UniApp 提供了统一的路由 API,对应不同的跳转场景:

javascript

运行

// 1. 保留当前页面,跳转到应用内的某个页面(可返回)
uni.navigateTo({
  url: '/pages/detail/index?id=123'
})

// 2. 关闭当前页面,跳转到应用内的某个页面
uni.redirectTo({
  url: '/pages/home/index'
})

// 3. 跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面
uni.switchTab({
  url: '/pages/mine/index'
})

// 4. 关闭所有页面,打开到应用内的某个页面
uni.reLaunch({
  url: '/pages/login/index'
})

// 5. 返回上一页或多级页面
uni.navigateBack({
  delta: 1  // 返回层数
})

// 接收页面参数(onLoad 中)
onLoad(options) {
  console.log(options.id) // '123'
}

四、组件化开发与封装

4.1 内置组件使用

UniApp 提供了丰富的内置基础组件,如 view、text、image、button、input、swiper、scroll-view 等,用法与 HTML 标签类似,但需要注意:

  • 不能使用 div、span 等 HTML 标签,必须使用 uni-app 组件
  • 图片必须使用 image 组件,有自己的裁剪模式
  • 所有组件默认都是块级元素

常用组件示例:

vue

<template>
  <view>
    <!-- 轮播图 -->
    <swiper class="banner" indicator-dots autoplay circular>
      <swiper-item v-for="item in banners" :key="item.id">
        <image :src="item.url" mode="aspectFill" />
      </swiper-item>
    </swiper>

    <!-- 列表项 -->
    <view class="card" v-for="item in list" :key="item.id">
      <image :src="item.cover" mode="aspectFill" class="cover" />
      <view class="info">
        <text class="name">{{ item.name }}</text>
        <text class="price">¥{{ item.price }}</text>
      </view>
    </view>
  </view>
</template>

4.2 自定义组件封装

组件化是提高代码复用性的核心。下面封装一个通用的 空状态组件 作为示例。

components/empty-state/empty-state.vue:

vue

<template>
  <view class="empty-state">
    <image :src="icon" mode="aspectFit" class="empty-icon" />
    <text class="empty-text">{{ text }}</text>
    <button 
      v-if="showBtn" 
      class="empty-btn" 
      type="primary" 
      @click="handleRetry"
    >
      {{ btnText }}
    </button>
  </view>
</template>

<script>
export default {
  name: 'EmptyState',
  props: {
    // 空状态图标
    icon: {
      type: String,
      default: '/static/empty.png'
    },
    // 提示文字
    text: {
      type: String,
      default: '暂无数据'
    },
    // 是否显示按钮
    showBtn: {
      type: Boolean,
      default: false
    },
    // 按钮文字
    btnText: {
      type: String,
      default: '重新加载'
    }
  },
  methods: {
    handleRetry() {
      this.$emit('retry')
    }
  }
}
</script>

<style lang="scss" scoped>
.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 100rpx 0;
  
  .empty-icon {
    width: 200rpx;
    height: 200rpx;
    margin-bottom: 30rpx;
    opacity: 0.6;
  }
  
  .empty-text {
    font-size: 28rpx;
    color: #999;
    margin-bottom: 40rpx;
  }
  
  .empty-btn {
    width: 240rpx;
    height: 72rpx;
    line-height: 72rpx;
    font-size: 28rpx;
  }
}
</style>

页面中使用:

vue

<template>
  <view>
    <empty-state 
      v-if="list.length === 0 && !loading" 
      text="暂无商品数据"
      show-btn
      btn-text="刷新试试"
      @retry="fetchData"
    />
    <view v-else class="list">
      <!-- 列表内容 -->
    </view>
  </view>
</template>

<script>
import EmptyState from '@/components/empty-state/empty-state.vue'

export default {
  components: { EmptyState },
  // ...
}
</script>

4.3 组件通信方式

  1. 父传子:props 传值
  2. 子传父:$emit 触发事件
  3. 兄弟组件:事件总线(EventBus)或 Vuex/Pinia
  4. 父调用子方法:ref 引用

javascript

运行

// 父组件通过 ref 调用子组件方法
this.$refs.myComponent.someMethod()

五、网络请求封装与实战

5.1 统一请求封装

实际项目中,不能直接使用 uni.request,需要封装统一的请求拦截、响应拦截、错误处理。

common/request.js:

javascript

运行

const BASE_URL = 'https://api.example.com'

// 请求队列,处理 loading
let requestCount = 0

const showLoading = () => {
  if (requestCount === 0) {
    uni.showLoading({ title: '加载中', mask: true })
  }
  requestCount++
}

const hideLoading = () => {
  requestCount--
  if (requestCount <= 0) {
    uni.hideLoading()
    requestCount = 0
  }
}

const request = (options) => {
  const { url, method = 'GET', data = {}, header = {}, showLoading: needLoading = true } = options
  
  needLoading && showLoading()
  
  // 获取 token
  const token = uni.getStorageSync('token') || ''
  
  return new Promise((resolve, reject) => {
    uni.request({
      url: BASE_URL + url,
      method,
      data,
      header: {
        'Content-Type': 'application/json',
        'Authorization': token ? `Bearer ${token}` : '',
        ...header
      },
      success: (res) => {
        const { statusCode, data } = res
        
        // HTTP 状态码判断
        if (statusCode >= 200 && statusCode < 300) {
          // 业务状态码判断
          if (data.code === 200) {
            resolve(data.data)
          } else if (data.code === 401) {
            // token 过期,跳登录
            uni.showToast({ title: '登录已过期', icon: 'none' })
            uni.reLaunch({ url: '/pages/login/index' })
            reject(data)
          } else {
            uni.showToast({ title: data.msg || '请求失败', icon: 'none' })
            reject(data)
          }
        } else {
          uni.showToast({ title: `网络错误 ${statusCode}`, icon: 'none' })
          reject(res)
        }
      },
      fail: (err) => {
        uni.showToast({ title: '网络连接失败', icon: 'none' })
        reject(err)
      },
      complete: () => {
        needLoading && hideLoading()
      }
    })
  })
}

// 快捷方法
export const get = (url, data, options = {}) => {
  return request({ url, method: 'GET', data, ...options })
}

export const post = (url, data, options = {}) => {
  return request({ url, method: 'POST', data, ...options })
}

export const put = (url, data, options = {}) => {
  return request({ url, method: 'PUT', data, ...options })
}

export const del = (url, data, options = {}) => {
  return request({ url, method: 'DELETE', data, ...options })
}

export default request

5.2 API 模块化管理

按业务模块拆分 API,便于维护。

api/goods.js:

javascript

运行

import { get, post } from '@/common/request.js'

// 获取商品列表
export const getGoodsList = (params) => {
  return get('/api/goods/list', params)
}

// 获取商品详情
export const getGoodsDetail = (id) => {
  return get(`/api/goods/detail/${id}`)
}

// 创建订单
export const createOrder = (data) => {
  return post('/api/order/create', data)
}

页面中使用:

javascript

运行

import { getGoodsList } from '@/api/goods.js'

export default {
  data() {
    return {
      list: [],
      loading: false
    }
  },
  onLoad() {
    this.fetchList()
  },
  methods: {
    async fetchList() {
      this.loading = true
      try {
        const data = await getGoodsList({ page: 1, pageSize: 10 })
        this.list = data.records
      } catch (e) {
        console.error('获取列表失败', e)
      } finally {
        this.loading = false
      }
    }
  }
}

5.3 全局挂载

在 main.js 中挂载到 Vue 原型,方便全局调用:

javascript

运行

import Vue from 'vue'
import App from './App'
import request, { get, post } from '@/common/request.js'

Vue.prototype.$request = request
Vue.prototype.$get = get
Vue.prototype.$post = post

Vue.config.productionTip = false
App.mpType = 'app'

const app = new Vue({
  ...App
})
app.$mount()

六、状态管理:Vuex 实战配置

对于中大型项目,状态管理必不可少。UniApp 官方推荐 Vuex。

6.1 Store 配置

store/index.js:

javascript

运行

import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user'
import cart from './modules/cart'

Vue.use(Vuex)

const store = new Vuex.Store({
  modules: {
    user,
    cart
  },
  // 全局状态
  state: {
    appName: 'UniApp Demo'
  },
  getters: {
    fullAppName: (state) => `【${state.appName}】`
  },
  mutations: {},
  actions: {}
})

export default store

store/modules/user.js:

javascript

运行

export default {
  namespaced: true,
  state: {
    userInfo: uni.getStorageSync('userInfo') || null,
    token: uni.getStorageSync('token') || ''
  },
  getters: {
    isLogin: (state) => !!state.token
  },
  mutations: {
    SET_USER_INFO(state, userInfo) {
      state.userInfo = userInfo
      uni.setStorageSync('userInfo', userInfo)
    },
    SET_TOKEN(state, token) {
      state.token = token
      uni.setStorageSync('token', token)
    },
    CLEAR_USER(state) {
      state.userInfo = null
      state.token = ''
      uni.removeStorageSync('userInfo')
      uni.removeStorageSync('token')
    }
  },
  actions: {
    login({ commit }, loginData) {
      // 模拟登录请求
      return new Promise((resolve) => {
        setTimeout(() => {
          commit('SET_TOKEN', 'mock_token_123')
          commit('SET_USER_INFO', {
            id: 1,
            nickname: '测试用户',
            avatar: '/static/avatar.png'
          })
          resolve()
        }, 500)
      })
    },
    logout({ commit }) {
      commit('CLEAR_USER')
    }
  }
}

6.2 页面中使用

javascript

运行

import { mapState, mapGetters, mapActions } from 'vuex'

export default {
  computed: {
    ...mapState('user', ['userInfo']),
    ...mapGetters('user', ['isLogin'])
  },
  methods: {
    ...mapActions('user', ['login', 'logout']),
    
    async handleLogin() {
      await this.login({ username: 'test', password: '123456' })
      uni.showToast({ title: '登录成功' })
    }
  }
}

七、跨端兼容与条件编译

7.1 为什么需要条件编译

虽然 UniApp 努力抹平各端差异,但不同平台仍有各自的特性 API 和能力限制。这时就需要条件编译,让特定代码只在指定平台生效。

7.2 条件编译语法

模板中使用:

vue

<template>
  <view>
    <!-- #ifdef MP-WEIXIN -->
    <view>仅微信小程序显示</view>
    <!-- #endif -->
    
    <!-- #ifdef H5 -->
    <view>仅H5端显示</view>
    <!-- #endif -->
    
    <!-- #ifndef APP-PLUS -->
    <view>除了App端都显示</view>
    <!-- #endif -->
  </view>
</template>

JS 中使用:

javascript

运行

export default {
  methods: {
    share() {
      // #ifdef MP-WEIXIN
      wx.showShareMenu()
      // #endif
      
      // #ifdef H5
      navigator.clipboard.writeText('分享链接')
      // #endif
    }
  }
}

CSS 中使用:

css

/* #ifdef MP-WEIXIN */
.box {
  padding-top: 88rpx; /* 适配微信小程序导航栏 */
}
/* #endif */

7.3 常用平台标识

表格

标识 平台
MP-WEIXIN 微信小程序
MP-ALIPAY 支付宝小程序
H5 H5
APP-PLUS App(iOS/Android)
APP-PLUS-NVUE App nvue 页面
MP 所有小程序

7.4 跨端兼容最佳实践

  1. 优先使用 UniApp 官方 API,不直接调用平台原生 API
  2. 差异部分抽离为公共方法,通过条件编译内部处理
  3. 样式使用 rpx 单位,自动适配不同屏幕
  4. 避免操作 DOM,小程序和 App 端没有 DOM 环境
  5. 不使用浏览器特有对象,如 window、document 等

八、性能优化实战技巧

8.1 页面渲染优化

1. 合理使用 data 数据

  • 只把需要渲染的数据放到 data 中
  • 大数据量列表避免一次性渲染,使用分页

2. 列表性能优化

vue

<scroll-view scroll-y class="list-box">
  <view 
    v-for="(item, index) in list" 
    :key="item.id"
    class="list-item"
  >
    <!-- 列表项内容 -->
  </view>
</scroll-view>

优化要点:

  • 必须设置 :key,且使用唯一 ID 而非 index
  • 长列表使用 uni-list 组件或虚拟列表
  • 避免在 v-for 中使用复杂计算

3. 减少 setData 调用次数 小程序端数据更新通过 setData 实现,频繁调用会卡顿。建议合并数据更新:

javascript

运行

// 不好的写法:多次 setData
this.title = '新标题'
this.list = newList
this.loading = false

// 好的写法:一次更新
this.$set(this, {
  title: '新标题',
  list: newList,
  loading: false
})

8.2 包体积优化

  1. 图片资源压缩,大图建议放 CDN,不打包进项目
  2. 按需引入组件,删除未使用的组件和页面
  3. 分包加载(小程序端)
  4. 移除 console 日志,生产环境关闭调试
  5. 静态资源使用 CDN,减少主包体积

8.3 启动速度优化

  1. 首页精简,减少首屏渲染复杂度
  2. 非首屏数据延迟加载
  3. 分包预下载
  4. 避免在 App.vue 的 onLaunch 中执行大量同步操作

8.4 内存优化

  1. 页面卸载时清理定时器和事件监听
  2. 大图列表使用懒加载
  3. 及时销毁不用的对象,避免内存泄漏

javascript

运行

onUnload() {
  clearInterval(this.timer)
  this.timer = null
}

九、企业级项目目录结构推荐

plaintext

┌─ api                // 接口层,按模块拆分
│  ├─ user.js
│  ├─ goods.js
│  └─ order.js
├─ common             // 公共工具
│  ├─ request.js      // 请求封装
│  ├─ utils.js        // 工具函数
│  └─ validate.js     // 表单校验
├─ components         // 公共组件
│  ├─ empty-state     // 空状态
│  ├─ load-more       // 加载更多
│  └─ nav-bar         // 自定义导航栏
├─ pages              // 主包页面
│  ├─ index           // 首页
│  ├─ category        // 分类
│  └─ mine            // 我的
├─ pagesA             // 分包A
│  └─ goods           // 商品模块
├─ pagesB             // 分包B
│  └─ order           // 订单模块
├─ static             // 静态资源
│  ├─ images
│  └─ tabbar
├─ store              // Vuex 状态管理
│  ├─ index.js
│  └─ modules
├─ styles             // 全局样式
│  ├─ common.scss
│  └─ variables.scss
├─ App.vue
├─ main.js
├─ manifest.json
├─ pages.json
└─ uni.scss

十、常见踩坑与解决方案

10.1 样式相关

问题:rpx 在不同端表现不一致

  • 解决方案:以设计稿 750px 为基准,rpx 会自动换算;注意 border 用 px 单位

问题:小程序端样式不生效

  • 检查是否加了 scoped,组件内样式无法影响子组件内部
  • 小程序不支持部分 CSS 选择器,如通配符 *、属性选择器等

10.2 数据更新相关

问题:数据修改了但视图不更新

  • 对象新增属性使用 this.$set(obj, 'key', value)
  • 数组修改索引使用 this.$set(arr, index, value) 或 splice

10.3 路由相关

问题:navigateTo 跳转没反应

  • 检查页面是否在 pages.json 中注册
  • tabBar 页面必须用 switchTab 跳转
  • 页面栈最多 10 层,超过后无法 navigateTo

10.4 图片相关

问题:图片不显示

  • 检查路径是否正确,static 目录下用绝对路径 /static/xxx.png
  • 网络图片必须是 https(小程序和正式环境)
  • image 组件必须设置宽高,否则不显示

十一、打包发布流程

11.1 H5 端发布

HBuilderX → 发行 → 网站 - H5 手机版 → 配置域名和路径 → 发行。

生成的 unpackage/dist/build/h5 目录部署到 Nginx 或其他 Web 服务器即可。

11.2 微信小程序发布

  1. HBuilderX → 发行 → 小程序 - 微信
  2. 编译完成后在微信开发者工具中打开
  3. 点击 "上传",填写版本号和备注
  4. 登录微信公众平台 → 版本管理 → 提交审核 → 发布

11.3 App 端打包

  1. 配置 manifest.json 中的 App 权限、图标、启动图
  2. HBuilderX → 发行 → 原生 App - 云打包
  3. 选择 Android/iOS,填写证书信息
  4. 等待云端打包完成,下载安装包

总结

UniApp 作为国内成熟的跨端开发方案,在效率和性能之间取得了很好的平衡。掌握 UniApp,意味着你可以用一套代码覆盖绝大多数移动端场景,极大提升开发效率。

本文从核心原理到实战代码,从基础语法到性能优化,系统地讲解了 UniApp 开发的完整知识体系。但框架只是工具,真正的能力在于对业务的理解和工程化实践。建议大家在实际项目中多思考、多总结,逐步形成自己的开发规范和最佳实践。

如果你刚接触 UniApp,可以从一个简单的列表页开始,逐步尝试组件封装、接口对接、状态管理,最终完成一个完整的项目。跨端开发的路上,我们一起成长。

Logo

一站式 AI 云服务平台

更多推荐