前言

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

本文将从环境搭建、核心概念、实战编码到性能优化,系统性地梳理 UniApp 开发中的核心知识点与最佳实践,附带可直接复用的代码示例,帮助开发者快速上手并写出高质量的跨端应用。

一、UniApp 核心优势与适用场景

1.1 核心优势

  • 跨端覆盖广:支持微信 / 支付宝 / 百度 / 字节跳动 / QQ 小程序、H5、App、快应用等 10+ 平台
  • 技术栈友好:基于 Vue.js + 微信小程序 API 设计,前端开发者学习成本极低
  • 性能表现优秀:App 端依托原生渲染,小程序端直接编译为原生代码,性能接近原生开发
  • 生态完善:官方插件市场(DCloud 插件市场)拥有大量现成组件与模板
  • 调试便捷:HBuilderX 提供完善的可视化调试、真机运行与打包发布能力

1.2 适用场景

  • 中小型电商、资讯、工具类应用
  • 企业内部管理系统移动端
  • 快速验证产品原型,多端同步上线
  • 已有 Vue 技术栈的团队快速切入移动端开发

二、开发环境搭建与项目结构

2.1 环境准备

开发 UniApp 推荐使用官方 IDE HBuilderX,内置了完整的编译、运行、打包能力,无需额外配置 Node.js 环境(也支持 CLI 方式)。

  1. 下载并安装 HBuilderX(App 开发版)
  2. 安装对应平台的开发者工具(如微信开发者工具)
  3. 新建项目:文件 → 新建 → 项目 → 选择 uni-app 模板

2.2 标准项目目录结构

plaintext

┌─ common              # 公共资源(工具函数、全局样式)
│  └─ utils.js         # 通用工具方法
├─ components          # 自定义组件
│  └─ my-list.vue      # 列表组件示例
├─ pages               # 页面目录
│  ├─ index            # 首页
│  │  └─ index.vue
│  └─ detail           # 详情页
│     └─ detail.vue
├─ static              # 静态资源(图片、字体等)
├─ store               # Vuex/Pinia 状态管理
│  └─ index.js
├─ uni_modules         # 插件市场安装的插件
├─ App.vue             # 应用入口(全局样式、生命周期)
├─ main.js             # 主入口文件
├─ manifest.json       # 应用配置(各平台权限、打包信息)
├─ pages.json          # 页面路由、导航栏、tabBar 配置
└─ uni.scss            # 全局 SCSS 变量

2.3 核心配置文件说明

pages.json 是 UniApp 中最重要的配置文件,负责路由、窗口样式、底部导航等配置:

json

{
  "pages": [
    {
      "path": "pages/index/index",
      "style": {
        "navigationBarTitleText": "首页",
        "enablePullDownRefresh": true,
        "backgroundTextStyle": "dark"
      }
    },
    {
      "path": "pages/detail/detail",
      "style": {
        "navigationBarTitleText": "详情页"
      }
    }
  ],
  "globalStyle": {
    "navigationBarTextStyle": "black",
    "navigationBarTitleText": "UniApp实战",
    "navigationBarBackgroundColor": "#FFFFFF",
    "backgroundColor": "#F5F5F5"
  },
  "tabBar": {
    "color": "#999999",
    "selectedColor": "#007AFF",
    "backgroundColor": "#FFFFFF",
    "list": [
      {
        "pagePath": "pages/index/index",
        "text": "首页",
        "iconPath": "static/tab/home.png",
        "selectedIconPath": "static/tab/home-active.png"
      },
      {
        "pagePath": "pages/mine/mine",
        "text": "我的",
        "iconPath": "static/tab/mine.png",
        "selectedIconPath": "static/tab/mine-active.png"
      }
    ]
  }
}

三、核心概念与生命周期

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

应用生命周期只在 App.vue 中生效,对应整个应用的启动、前后台切换:

javascript

运行

export default {
  onLaunch() {
    // 应用初始化完成时触发(全局只触发一次)
    console.log('应用启动')
    this.checkUpdate()
  },
  onShow() {
    // 应用从后台进入前台时触发
    console.log('应用显示')
  },
  onHide() {
    // 应用从前台进入后台时触发
    console.log('应用隐藏')
  },
  onError(err) {
    // 应用发生脚本错误或API调用失败时触发
    console.error('应用错误:', err)
  }
}

3.2 页面生命周期

页面生命周期是开发中最常用的部分,UniApp 扩展了 Vue 的生命周期,同时兼容小程序的页面钩子:

表格

生命周期 说明 平台差异
onLoad 页面加载,可获取页面参数 全平台
onShow 页面显示 全平台
onReady 页面初次渲染完成 全平台
onHide 页面隐藏 全平台
onUnload 页面卸载 全平台
onPullDownRefresh 下拉刷新 需在 pages.json 开启
onReachBottom 上拉触底 全平台
onShareAppMessage 分享给好友 微信小程序

页面参数接收示例:

javascript

运行

export default {
  data() {
    return {
      id: '',
      detailData: null
    }
  },
  onLoad(options) {
    // 接收上一页传递的参数
    this.id = options.id
    this.getDetail()
  },
  methods: {
    getDetail() {
      // 根据id请求详情数据
    }
  }
}

3.3 页面路由与传参

UniApp 提供了统一的路由 API,底层自动适配各平台:

javascript

运行

// 1. 保留当前页,跳转新页面(可返回)
uni.navigateTo({
  url: '/pages/detail/detail?id=123&name=test'
})

// 2. 关闭当前页,跳转新页面(不可返回)
uni.redirectTo({
  url: '/pages/login/login'
})

// 3. 跳转到 tabBar 页面
uni.switchTab({
  url: '/pages/index/index'
})

// 4. 关闭所有页面,打开指定页面
uni.reLaunch({
  url: '/pages/index/index'
})

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

四、实战:网络请求封装与列表页开发

4.1 统一网络请求封装

在实际项目中,我们通常会对 uni.request 进行二次封装,统一处理请求头、响应拦截、错误提示、Token 鉴权等逻辑。

common/request.js 中创建封装文件:

javascript

运行

// 基础配置
const BASE_URL = 'https://api.example.com'
const TIMEOUT = 10000

// 请求拦截器
const requestInterceptor = (config) => {
  // 从本地缓存获取Token
  const token = uni.getStorageSync('token')
  if (token) {
    config.header.Authorization = `Bearer ${token}`
  }
  config.header['Content-Type'] = 'application/json'
  return config
}

// 响应拦截器
const responseInterceptor = (response) => {
  const { statusCode, data } = response
  
  // HTTP 状态码判断
  if (statusCode === 200) {
    // 业务状态码判断
    if (data.code === 200) {
      return data.data
    } else if (data.code === 401) {
      // Token过期,跳登录
      uni.removeStorageSync('token')
      uni.redirectTo({ url: '/pages/login/login' })
      return Promise.reject(data.message)
    } else {
      uni.showToast({ title: data.message, icon: 'none' })
      return Promise.reject(data.message)
    }
  } else {
    uni.showToast({ title: '网络请求失败', icon: 'none' })
    return Promise.reject(`HTTP错误: ${statusCode}`)
  }
}

// 核心请求方法
const request = (options) => {
  return new Promise((resolve, reject) => {
    const config = requestInterceptor({
      url: BASE_URL + options.url,
      method: options.method || 'GET',
      data: options.data || {},
      header: options.header || {},
      timeout: TIMEOUT
    })

    uni.request({
      ...config,
      success: (res) => {
        try {
          const result = responseInterceptor(res)
          resolve(result)
        } catch (err) {
          reject(err)
        }
      },
      fail: (err) => {
        uni.showToast({ title: '网络连接异常', icon: 'none' })
        reject(err)
      }
    })
  })
}

// 导出常用方法
export const get = (url, data = {}) => request({ url, method: 'GET', data })
export const post = (url, data = {}) => request({ url, method: 'POST', data })
export const put = (url, data = {}) => request({ url, method: 'PUT', data })
export const del = (url, data = {}) => request({ url, method: 'DELETE', data })

export default request

4.2 接口统一管理

common/api.js 中集中管理所有接口:

javascript

运行

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

// 首页列表
export const getArticleList = (params) => get('/api/article/list', params)

// 文章详情
export const getArticleDetail = (id) => get(`/api/article/${id}`)

// 用户登录
export const login = (data) => post('/api/user/login', data)

4.3 完整列表页实现(下拉刷新 + 上拉加载)

这是业务开发中最常见的场景,包含分页加载、下拉刷新、空状态、加载状态等完整交互:

vue

<template>
  <view class="list-container">
    <!-- 列表内容 -->
    <view class="list-item" v-for="item in list" :key="item.id" @click="goDetail(item.id)">
      <image class="item-cover" :src="item.cover" mode="aspectFill"></image>
      <view class="item-content">
        <text class="item-title">{{ item.title }}</text>
        <text class="item-desc">{{ item.description }}</text>
        <view class="item-footer">
          <text class="item-author">{{ item.author }}</text>
          <text class="item-time">{{ item.createTime }}</text>
        </view>
      </view>
    </view>

    <!-- 加载状态 -->
    <view class="loading-text" v-if="loading">
      <text>加载中...</text>
    </view>
    
    <!-- 没有更多 -->
    <view class="loading-text" v-if="!hasMore && list.length > 0">
      <text>没有更多数据了</text>
    </view>
    
    <!-- 空状态 -->
    <view class="empty-box" v-if="!loading && list.length === 0">
      <text>暂无数据</text>
    </view>
  </view>
</template>

<script>
import { getArticleList } from '@/common/api.js'

export default {
  data() {
    return {
      list: [],
      pageNum: 1,
      pageSize: 10,
      hasMore: true,
      loading: false
    }
  },
  onLoad() {
    this.fetchList()
  },
  // 下拉刷新
  onPullDownRefresh() {
    this.refresh()
  },
  // 上拉加载更多
  onReachBottom() {
    if (this.hasMore && !this.loading) {
      this.pageNum++
      this.fetchList()
    }
  },
  methods: {
    // 获取列表数据
    async fetchList() {
      this.loading = true
      try {
        const res = await getArticleList({
          pageNum: this.pageNum,
          pageSize: this.pageSize
        })
        
        if (this.pageNum === 1) {
          this.list = res.records
        } else {
          this.list = [...this.list, ...res.records]
        }
        
        // 判断是否还有更多
        this.hasMore = this.list.length < res.total
      } catch (err) {
        console.error('获取列表失败:', err)
      } finally {
        this.loading = false
        uni.stopPullDownRefresh()
      }
    },
    
    // 刷新
    refresh() {
      this.pageNum = 1
      this.hasMore = true
      this.fetchList()
    },
    
    // 跳转详情
    goDetail(id) {
      uni.navigateTo({
        url: `/pages/detail/detail?id=${id}`
      })
    }
  }
}
</script>

<style lang="scss" scoped>
.list-container {
  padding: 20rpx;
  box-sizing: border-box;
}

.list-item {
  display: flex;
  padding: 24rpx;
  margin-bottom: 20rpx;
  background: #fff;
  border-radius: 16rpx;
  
  .item-cover {
    width: 200rpx;
    height: 150rpx;
    border-radius: 8rpx;
    flex-shrink: 0;
  }
  
  .item-content {
    flex: 1;
    margin-left: 20rpx;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    
    .item-title {
      font-size: 32rpx;
      font-weight: 500;
      color: #333;
      line-height: 1.4;
      overflow: hidden;
      text-overflow: ellipsis;
      display: -webkit-box;
      -webkit-line-clamp: 2;
      -webkit-box-orient: vertical;
    }
    
    .item-desc {
      font-size: 26rpx;
      color: #666;
      margin-top: 8rpx;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    
    .item-footer {
      display: flex;
      justify-content: space-between;
      margin-top: 12rpx;
      
      text {
        font-size: 24rpx;
        color: #999;
      }
    }
  }
}

.loading-text, .empty-box {
  text-align: center;
  padding: 40rpx 0;
  font-size: 26rpx;
  color: #999;
}
</style>

五、跨端兼容与条件编译

5.1 为什么需要条件编译

虽然 UniApp 致力于抹平平台差异,但各平台仍有独有的 API、组件和样式规则。条件编译可以让同一套代码在不同平台编译出不同的内容。

5.2 三种条件编译写法

1. 模板中条件编译

vue

<template>
  <view>
    <!-- #ifdef MP-WEIXIN -->
    <button open-type="getUserInfo">微信一键登录</button>
    <!-- #endif -->
    
    <!-- #ifdef H5 -->
    <button @click="h5Login">账号密码登录</button>
    <!-- #endif -->
    
    <!-- #ifdef APP-PLUS -->
    <button @click="appLogin">原生登录</button>
    <!-- #endif -->
  </view>
</template>

2. JS 中条件编译

javascript

运行

export default {
  methods: {
    share() {
      // #ifdef MP-WEIXIN
      wx.showShareMenu({ withShareTicket: true })
      // #endif
      
      // #ifdef H5
      navigator.clipboard.writeText(location.href)
      uni.showToast({ title: '链接已复制', icon: 'success' })
      // #endif
    }
  }
}

3. CSS 中条件编译

css

/* 通用样式 */
.container {
  padding: 20rpx;
}

/* #ifdef MP-WEIXIN */
.container {
  padding-top: calc(20rpx + env(safe-area-inset-top));
}
/* #endif */

5.3 常用平台标识

表格

标识 对应平台
MP-WEIXIN 微信小程序
MP-ALIPAY 支付宝小程序
MP-BAIDU 百度小程序
MP-TOUTIAO 字节跳动小程序
H5 H5 网页
APP-PLUS App(iOS/Android)
APP-PLUS-NVUE App nvue 页面

六、组件封装实战:通用弹窗组件

组件化开发是提升代码复用性的关键。下面封装一个高度可定制的通用弹窗组件:

vue

<template>
  <view class="modal-mask" v-if="visible" @click="handleMaskClick">
    <view class="modal-content" @click.stop>
      <!-- 标题 -->
      <view class="modal-title" v-if="title">{{ title }}</view>
      
      <!-- 内容区插槽 -->
      <view class="modal-body">
        <slot></slot>
      </view>
      
      <!-- 底部按钮 -->
      <view class="modal-footer" v-if="showFooter">
        <button class="btn-cancel" @click="handleCancel" v-if="showCancel">
          {{ cancelText }}
        </button>
        <button class="btn-confirm" @click="handleConfirm">
          {{ confirmText }}
        </button>
      </view>
    </view>
  </view>
</template>

<script>
export default {
  name: 'BaseModal',
  props: {
    visible: {
      type: Boolean,
      default: false
    },
    title: {
      type: String,
      default: ''
    },
    showFooter: {
      type: Boolean,
      default: true
    },
    showCancel: {
      type: Boolean,
      default: true
    },
    cancelText: {
      type: String,
      default: '取消'
    },
    confirmText: {
      type: String,
      default: '确定'
    },
    maskClosable: {
      type: Boolean,
      default: true
    }
  },
  methods: {
    handleMaskClick() {
      if (this.maskClosable) {
        this.close()
      }
    },
    handleCancel() {
      this.$emit('cancel')
      this.close()
    },
    handleConfirm() {
      this.$emit('confirm')
    },
    close() {
      this.$emit('update:visible', false)
    }
  }
}
</script>

<style lang="scss" scoped>
.modal-mask {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 999;
}

.modal-content {
  width: 600rpx;
  background: #fff;
  border-radius: 16rpx;
  overflow: hidden;
}

.modal-title {
  padding: 30rpx 40rpx 10rpx;
  font-size: 32rpx;
  font-weight: 500;
  text-align: center;
  color: #333;
}

.modal-body {
  padding: 30rpx 40rpx;
  font-size: 28rpx;
  color: #666;
  line-height: 1.6;
}

.modal-footer {
  display: flex;
  border-top: 1rpx solid #eee;
  
  button {
    flex: 1;
    height: 88rpx;
    line-height: 88rpx;
    font-size: 30rpx;
    border: none;
    background: #fff;
    border-radius: 0;
    
    &::after {
      border: none;
    }
  }
  
  .btn-cancel {
    color: #666;
    border-right: 1rpx solid #eee;
  }
  
  .btn-confirm {
    color: #007AFF;
    font-weight: 500;
  }
}
</style>

使用方式:

vue

<template>
  <view>
    <button @click="showModal = true">打开弹窗</button>
    
    <base-modal 
      :visible.sync="showModal" 
      title="提示"
      @confirm="handleConfirm"
    >
      <text>确定要执行此操作吗?</text>
    </base-modal>
  </view>
</template>

<script>
import BaseModal from '@/components/base-modal.vue'

export default {
  components: { BaseModal },
  data() {
    return { showModal: false }
  },
  methods: {
    handleConfirm() {
      // 确认逻辑
      this.showModal = false
    }
  }
}
</script>

七、性能优化最佳实践

7.1 页面渲染优化

  1. 合理使用 v-ifv-show

    • 频繁切换用 v-show,条件不常变化用 v-if
    • 小程序端 v-if 会直接移除节点,v-show 仅控制显示隐藏
  2. 长列表优化

    • 使用 uni-listrecycle-view 组件实现虚拟滚动
    • 分页加载,避免一次性渲染大量数据
    • 列表项设置唯一 key,提升 diff 效率
  3. 减少 setData 调用(小程序端)

    • 批量更新数据,避免频繁调用 this.setData
    • 仅更新页面上需要展示的数据,剔除冗余字段

7.2 包体积优化

  1. 图片资源处理

    • 小图标使用字体图标(如 iconfont)
    • 大图上传 CDN,使用网络地址
    • 静态图片压缩后再放入项目
  2. 代码分包(小程序端)pages.json 中配置分包,减少主包体积:

json

{
  "subPackages": [
    {
      "root": "pagesA",
      "pages": [
        { "path": "list/list" },
        { "path": "detail/detail" }
      ]
    }
  ],
  "preloadRule": {
    "pages/index/index": {
      "network": "all",
      "packages": ["pagesA"]
    }
  }
}
  1. 按需引入插件
    • 避免引入完整的 UI 组件库,按需引入用到的组件
    • 及时清理无用的页面、组件和静态资源

7.3 启动速度优化

  1. 首页内容精简,首屏只渲染核心内容,非核心模块延迟加载
  2. 减少 App.vue 中的初始化逻辑,耗时操作放到 onReady 之后
  3. 使用本地缓存,数据优先读缓存,后台异步更新
  4. 预加载下一页数据,在列表页点击时就开始请求详情数据

7.4 内存优化

  1. 及时清理定时器与事件监听

javascript

运行

export default {
  data() {
    return { timer: null }
  },
  onReady() {
    this.timer = setInterval(() => {
      // 定时任务
    }, 1000)
  },
  onUnload() {
    // 页面卸载时清除定时器
    if (this.timer) {
      clearInterval(this.timer)
      this.timer = null
    }
  }
}
  1. 避免内存泄漏
    • 全局事件总线 $on 后要在 onUnload$off
    • 大图片列表及时回收,避免内存持续上涨

八、常见踩坑与解决方案

8.1 样式相关

  • 问题:H5 端样式正常,小程序端样式错乱
  • 解决:小程序不支持通配符 * 选择器、不支持部分 CSS 高级选择器;避免使用 !important;单位统一使用 rpx

8.2 事件相关

  • 问题@click 事件在小程序端触发延迟
  • 解决:快速点击场景使用 @tap 事件;避免同时绑定 tapclick

8.3 路由相关

  • 问题navigateTo 跳转无反应
  • 解决:检查目标页面是否在 pages.json 中注册;tabBar 页面必须用 switchTab 跳转;页面栈最多 10 层,超出后使用 redirectTo

8.4 数据更新相关

  • 问题:修改数据后页面不刷新
  • 解决:对象深层属性修改使用 this.$set;数组下标直接赋值不会触发更新,改用 splice 或整体替换

九、打包发布流程

9.1 各平台发布路径

  1. 微信小程序:HBuilderX → 运行 → 运行到小程序模拟器 → 微信开发者工具 → 微信开发者工具中点击上传
  2. H5:HBuilderX → 发行 → 网站 - PC Web 或手机 H5 → 部署到服务器
  3. App:HBuilderX → 发行 → 原生 App - 云打包 → 配置证书 → 生成 apk/ipa

9.2 发布前检查清单

  • 所有页面路径在 pages.json 中正确配置
  • 接口地址切换为生产环境
  • 关闭调试日志与调试工具
  • 图片资源压缩,检查是否有遗漏的本地大图
  • 各平台权限配置完整(定位、相机、存储等)
  • 隐私协议与用户授权流程合规

总结

UniApp 作为成熟的跨端开发方案,在效率和性能之间取得了很好的平衡。掌握本文介绍的网络封装、组件化开发、条件编译、性能优化等核心技能,基本可以应对绝大多数业务场景的开发需求。

跨端开发的核心挑战始终在于平台差异的处理,建议开发者在编码时多从多平台兼容性角度思考,充分利用条件编译处理平台特性,同时遵循性能优化最佳实践,才能打造出体验优秀的跨端应用。

随着 UniApp 的持续迭代,X 版本、Vue3 组合式 API、Vite 构建等新特性也在不断完善,建议开发者持续关注官方动态,及时跟进新技术栈,进一步提升开发效率与应用性能。

Logo

一站式 AI 云服务平台

更多推荐