前言

在小程序、H5、App 多端并行的业务场景下,前端团队常陷入「一套需求、多端开发、重复维护」的效率困境。由 DCloud 推出的 uni-app 框架,基于 Vue.js 技术栈构建,支持一套代码同时发布到 iOS/Android App、H5、微信 / 支付宝 / 抖音等十余种小程序平台,凭借极低的学习成本、成熟的生态体系与贴近原生的性能表现,成为目前国内跨端开发领域的主流选型。

本文将从环境搭建、核心配置、业务实战、跨端兼容、性能优化到工程化踩坑,系统梳理 uni-app 全链路开发要点。全文代码均来自企业级项目沉淀,兼顾入门友好度与进阶深度,帮助你从零搭建可落地的高质量跨端应用。

一、uni-app 基础认知与环境搭建

1.1 什么是 uni-app

uni-app 是一个基于 Vue.js 的全端开发框架,开发者编写一套代码,可编译发布到 iOS、Android、Web(响应式)、微信 / 支付宝 / 百度 / 字节 / QQ / 快手等各类小程序,以及快应用等多个平台。

核心优势:

  • 多端覆盖,成本可控:一套源码适配 10+ 平台,开发与维护成本降低 70% 以上
  • 技术栈统一,学习门槛低:完全兼容 Vue 2 / Vue 3 语法,前端开发者可无缝上手
  • 原生能力兼容,生态丰富:深度适配各端原生 API,配套 uni-ui 组件库、插件市场与 uniCloud 云开发能力
  • 开发调试高效:配套 HBuilderX 编辑器,支持热更新、真机调试与云打包一站式流程

1.2 开发环境搭建

开发 uni-app 推荐使用官方编辑器 HBuilderX,工程化项目也支持 VS Code + 命令行的开发方式。

方式一:HBuilderX 可视化创建(新手推荐)
  1. 前往 DCloud 官网 下载 HBuilderX 正式版
  2. 打开编辑器 → 文件 → 新建 → 项目 → 选择 uni-app
  3. 填写项目名称与存储路径,选择默认模板即可一键创建
方式二:Vite 命令行创建(Vue3 推荐,工程化首选)

目前官方已全面主推 Vite + Vue3 方案,编译速度与开发体验更优:

bash

运行

# 使用 npx 直接创建(无需全局安装)
npx degit dcloudio/uni-preset-vue#vite my-uniapp-project

# 进入项目安装依赖
cd my-uniapp-project
npm install

# 运行对应平台
npm run dev:mp-weixin  # 编译为微信小程序
npm run dev:h5         # 编译为 H5
npm run dev:app        # 编译为 App 端

注:Vue2 版本可继续使用 Vue CLI 方案,新项目优先选择 Vue3 + Vite 以获得更好的性能与生态支持。

二、项目目录结构与核心概念

2.1 标准目录结构

一个规范的 uni-app 项目目录如下,理解各文件职责是高效开发的基础:

plaintext

┌─ common               # 公共资源、通用工具函数
│  └─ request.js        # 网络请求封装
├─ components           # 自定义公共组件
│  └─ my-card
│     └─ my-card.vue
├─ pages                # 业务页面目录,所有页面必须在此注册
│  └─ index
│     └─ index.vue
├─ static               # 静态资源(图片、字体等,编译时直接拷贝)
├─ uni_modules          # uni-app 官方插件模块,自动按需引入
├─ App.vue              # 应用根组件,全局配置、全局样式、应用生命周期
├─ main.js              # 项目入口文件,挂载 Vue 实例与全局插件
├─ manifest.json        # 应用配置文件,各端打包参数、权限、AppID 配置
├─ pages.json           # 页面路由、导航栏、tabBar、分包等全局配置
└─ uni.scss             # 全局 SCSS 变量,自动注入到所有页面

目录规范注意事项:

  1. static 目录仅存放静态资源,编译时不会进行压缩处理,大体积图片建议上传 CDN
  2. 业务页面必须全部注册在 pages 目录下,否则无法被路由系统识别
  3. 符合规范的 uni_modules 插件无需手动注册,框架会自动按需引入

2.2 三大核心配置文件

1. pages.json —— 页面路由与全局外观

pages.json 是 uni-app 中最核心的配置文件,负责页面注册、导航栏样式、底部 tabBar、分包规则等全局外观与路由配置。

json

{
  "pages": [
    {
      "path": "pages/index/index",
      "style": {
        "navigationBarTitleText": "首页",
        "navigationBarBackgroundColor": "#2979ff",
        "navigationBarTextStyle": "white",
        "enablePullDownRefresh": true
      }
    },
    {
      "path": "pages/detail/detail",
      "style": {
        "navigationBarTitleText": "详情页"
      }
    }
  ],
  "globalStyle": {
    "navigationBarTextStyle": "black",
    "navigationBarTitleText": "uni-app 演示",
    "navigationBarBackgroundColor": "#ffffff",
    "backgroundColor": "#f8f8f8"
  },
  "tabBar": {
    "color": "#7A7E83",
    "selectedColor": "#2979ff",
    "borderStyle": "black",
    "backgroundColor": "#ffffff",
    "list": [
      {
        "pagePath": "pages/index/index",
        "iconPath": "static/tab/home.png",
        "selectedIconPath": "static/tab/home-active.png",
        "text": "首页"
      },
      {
        "pagePath": "pages/mine/mine",
        "iconPath": "static/tab/user.png",
        "selectedIconPath": "static/tab/user-active.png",
        "text": "我的"
      }
    ]
  }
}

进阶配置:针对小程序端体积限制,可配合 subPackages 分包与 preloadRule 分包预下载优化首屏速度,具体配置会在性能优化章节详细说明。

2. manifest.json —— 应用与各端专属配置

用于配置应用名称、版本号、AppID、权限声明、各平台专属编译参数。例如微信小程序的 AppID、App 端的权限申请、H5 端的代理配置等,均在此文件中管理。

3. App.vue —— 全局入口组件

App.vue 是 uni-app 的根组件,所有页面都在其下进行切换,常用于定义全局样式、监听应用级生命周期。

vue

<script>
export default {
  // 应用生命周期——仅在 App.vue 中生效
  onLaunch: function() {
    console.log('应用初始化完成,全局仅执行一次')
    // 可在此做登录态校验、全局数据初始化、版本更新检测
  },
  onShow: function() {
    console.log('应用从后台切入前台')
  },
  onHide: function() {
    console.log('应用从前台切至后台')
  },
  onError: function(err) {
    console.error('应用全局异常捕获:', err)
  }
}
</script>

<style>
/* 全局样式,作用于所有页面 */
page {
  background-color: #f5f5f5;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.container {
  padding: 20rpx;
  box-sizing: border-box;
}
</style>

注意:onLaunchonShowonHide 属于应用生命周期,仅在根组件 App.vue 中生效;页面级生命周期(如 onLoadonPullDownRefresh)仅在页面组件中生效,不可在 App.vue 中使用。

三、核心开发实战:常用 API 与组件封装

3.1 页面生命周期与数据绑定

uni-app 页面同时兼容 Vue 原生生命周期与 uni-app 专属页面生命周期,业务开发中最常用的为 onLoad(页面加载)、onShow(页面显示)、onReady(页面初次渲染完成)、onReachBottom(页面触底)等。

vue

<template>
  <view class="container">
    <view class="title">{{ title }}</view>
    <view class="list">
      <view 
        v-for="(item, index) in list" 
        :key="item.id" 
        class="list-item"
        @click="goDetail(item.id)"
      >
        {{ item.name }}
      </view>
    </view>
    <button @click="loadMore" :disabled="loading" class="load-btn">
      {{ loading ? '加载中...' : '加载更多' }}
    </button>
  </view>
</template>

<script>
export default {
  data() {
    return {
      title: '商品列表',
      list: [],
      page: 1,
      loading: false
    }
  },
  onLoad(options) {
    // 页面加载时触发,可获取上一页传递的路由参数
    console.log('页面入参:', options)
    this.getList()
  },
  onPullDownRefresh() {
    // 下拉刷新回调,需在 pages.json 中开启 enablePullDownRefresh
    this.page = 1
    this.getList().finally(() => {
      uni.stopPullDownRefresh()
    })
  },
  onReachBottom() {
    // 页面滚动触底回调,用于分页加载
    if (!this.loading) {
      this.page++
      this.loadMore()
    }
  },
  methods: {
    async getList() {
      this.loading = true
      // 模拟接口请求
      const res = await this.$request.get('/api/goods/list', { page: this.page })
      if (res.code === 0) {
        this.list = this.page === 1 ? res.data : [...this.list, ...res.data]
      }
      this.loading = false
    },
    goDetail(id) {
      // 页面跳转,保留当前页,可返回
      uni.navigateTo({
        url: `/pages/detail/detail?id=${id}`
      })
    },
    loadMore() {
      this.getList()
    }
  }
}
</script>

<style scoped>
.title {
  font-size: 32rpx;
  font-weight: bold;
  margin-bottom: 20rpx;
  color: #333;
}
.list-item {
  padding: 24rpx;
  background: #fff;
  border-radius: 12rpx;
  margin-bottom: 16rpx;
  transition: opacity 0.2s;
}
.list-item:active {
  opacity: 0.7;
}
.load-btn {
  margin-top: 20rpx;
  width: 100%;
}
</style>

新手避坑:Vue 的 created 生命周期执行早于 uni-app 的 onLoad,若需获取页面跳转参数,必须在 onLoad 中接收,不可在 created 中获取。

3.2 网络请求统一封装

原生 uni.request 缺少统一的异常处理、鉴权注入与交互反馈,重复代码多。企业级项目中通常会对其进行二次封装,实现接口请求的标准化管理。

common/request.js 中创建封装方法:

javascript

运行

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

const request = (url, data = {}, method = 'GET') => {
  return new Promise((resolve, reject) => {
    // 请求前展示加载态
    uni.showLoading({ title: '加载中', mask: true })
    
    uni.request({
      url: BASE_URL + url,
      method,
      data,
      timeout: TIME_OUT,
      header: {
        'Content-Type': 'application/json',
        'Authorization': uni.getStorageSync('token') || ''
      },
      success: (res) => {
        uni.hideLoading()
        const { statusCode, data } = res
        
        // 统一 HTTP 状态码处理
        if (statusCode === 200) {
          // 统一业务状态码处理
          if (data.code === 0) {
            resolve(data)
          } else if (data.code === 401) {
            // Token 失效,清空登录态并跳转登录页
            uni.showToast({ title: '登录已过期,请重新登录', icon: 'none' })
            uni.removeStorageSync('token')
            uni.redirectTo({ url: '/pages/login/login' })
            reject(data)
          } else {
            uni.showToast({ title: data.msg || '请求失败', icon: 'none' })
            reject(data)
          }
        } else {
          uni.showToast({ title: `网络错误 ${statusCode}`, icon: 'none' })
          reject(res)
        }
      },
      fail: (err) => {
        uni.hideLoading()
        uni.showToast({ title: '网络连接失败,请检查网络', icon: 'none' })
        reject(err)
      }
    })
  })
}

// 导出 RESTful 风格方法
export default {
  get: (url, data) => request(url, data, 'GET'),
  post: (url, data) => request(url, data, 'POST'),
  put: (url, data) => request(url, data, 'PUT'),
  delete: (url, data) => request(url, data, 'DELETE')
}

main.js 中挂载到 Vue 原型,全局可用:

javascript

运行

import request from './common/request.js'
Vue.prototype.$request = request

页面中直接调用即可:

javascript

运行

// GET 请求
const res = await this.$request.get('/api/user/info', { id: 1001 })

// POST 请求
const res = await this.$request.post('/api/login', { username: 'admin', password: '123456' })

进阶优化:可通过请求计数器实现多请求合并 loading,避免频繁触发弹窗闪烁;也可增加请求取消、重复请求拦截等能力,适配更复杂的业务场景。

3.3 自定义组件封装

以通用卡片组件为例,展示 uni-app 组件的封装规范与父子通信方式。

创建 components/my-card/my-card.vue

vue

<template>
  <view class="my-card" @click="handleClick">
    <view class="card-header">
      <text class="card-title">{{ title }}</text>
      <text v-if="extra" class="card-extra">{{ extra }}</text>
    </view>
    <view class="card-content">
      <!-- 默认插槽 -->
      <slot></slot>
    </view>
    <view v-if="showFooter" class="card-footer">
      <!-- 具名插槽 -->
      <slot name="footer"></slot>
    </view>
  </view>
</template>

<script>
export default {
  name: 'MyCard',
  props: {
    title: {
      type: String,
      default: ''
    },
    extra: {
      type: String,
      default: ''
    },
    showFooter: {
      type: Boolean,
      default: false
    }
  },
  methods: {
    handleClick() {
      // 向父组件触发事件,支持传参
      this.$emit('click')
    }
  }
}
</script>

<style scoped>
.my-card {
  background: #fff;
  border-radius: 16rpx;
  padding: 24rpx;
  margin: 20rpx;
  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
}
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20rpx;
  padding-bottom: 16rpx;
  border-bottom: 1rpx solid #f0f0f0;
}
.card-title {
  font-size: 30rpx;
  font-weight: 600;
  color: #333;
}
.card-extra {
  font-size: 26rpx;
  color: #999;
}
.card-content {
  font-size: 28rpx;
  color: #666;
  line-height: 1.6;
}
.card-footer {
  margin-top: 20rpx;
  padding-top: 16rpx;
  border-top: 1rpx solid #f0f0f0;
}
</style>

页面中使用组件:

vue

<template>
  <view class="container">
    <!-- 基础用法 -->
    <my-card title="用户信息" extra="编辑" @click="editUser">
      <view>姓名:张三</view>
      <view>手机号:138****8888</view>
    </my-card>
    
    <!-- 带底部具名插槽 -->
    <my-card title="订单详情" :show-footer="true">
      <view>商品名称:uni-app 开发实战</view>
      <view>订单金额:¥99.00</view>
      <template v-slot:footer>
        <button size="mini" type="primary" @click.stop="payOrder">立即支付</button>
      </template>
    </my-card>
  </view>
</template>

<script>
import MyCard from '@/components/my-card/my-card.vue'
export default {
  components: { MyCard },
  methods: {
    editUser() {
      uni.showToast({ title: '点击了编辑', icon: 'none' })
    },
    payOrder() {
      uni.showToast({ title: '支付成功', icon: 'success' })
    }
  }
}
</script>

高效技巧:如果组件存放路径符合 components/组件名/组件名.vue 的规范,uni-app 会通过 easycom 机制自动注册组件,无需在页面中手动 import 和 components 声明,直接在模板中使用即可,大幅简化开发流程。

注意:小程序端不支持 $refs 直接操作 DOM,组件间通信优先使用 props + $emit,全局状态可使用 Vuex/Pinia。

四、跨端兼容核心:条件编译

跨端开发最大的痛点在于各平台 API 与组件能力的差异。uni-app 提供了条件编译机制,可在编译阶段根据平台剔除无关代码,是实现跨端兼容的核心手段。

4.1 条件编译语法

#ifdef(if defined,仅在某平台生效)和 #endif 包裹代码块,指定对应平台:

表格

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

支持逻辑运算:

  • #ifndef MP-WEIXIN:除微信小程序外,其他端均生效
  • #ifdef H5 || MP-WEIXIN:H5 或微信小程序端生效

4.2 三种核心使用场景

1. 模板中使用

vue

<template>
  <view>
    <!-- 仅 H5 端显示分享按钮 -->
    <!-- #ifdef H5 -->
    <button @click="shareH5">分享到朋友圈</button>
    <!-- #endif -->
    
    <!-- 仅微信小程序显示原生分享按钮 -->
    <!-- #ifdef MP-WEIXIN -->
    <button open-type="share">微信分享</button>
    <!-- #endif -->
  </view>
</template>
2. 脚本中使用

javascript

运行

methods: {
  initShare() {
    // 通用逻辑,全端执行
    const shareConfig = { title: 'uni-app 实战指南', path: '/pages/index/index' }
    
    // 微信小程序专属 API
    // #ifdef MP-WEIXIN
    wx.updateShareMenu({ withShareTicket: true })
    // #endif
    
    // App 端专属原生 API
    // #ifdef APP-PLUS
    plus.navigator.setStatusBarStyle('dark')
    // #endif
  }
}
3. 样式中使用

css

/* 通用样式,全端生效 */
.box {
  padding: 20rpx;
}

/* 仅 H5 端生效,添加 hover 交互 */
/* #ifdef H5 */
.box {
  cursor: pointer;
  transition: all 0.3s ease;
}
.box:hover {
  transform: translateY(-2rpx);
  box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.1);
}
/* #endif */

4.3 配置文件与整目录级条件编译

除了页面内代码,条件编译也可用于配置文件,控制特定平台的页面注册:

json

{
  "pages": [
    {
      "path": "pages/index/index",
      "style": {}
    }
    // #ifdef MP-WEIXIN
    ,{
      "path": "pages/wx-subscribe/wx-subscribe",
      "style": {
        "navigationBarTitleText": "订阅消息授权"
      }
    }
    // #endif
  ]
}

五、uni-app 性能优化方案

跨端应用的性能瓶颈多集中在小程序端,以下优化方案均来自项目实战,可显著提升页面流畅度与加载速度。

5.1 页面渲染优化

  1. 合理使用 v-ifv-show

    • 频繁切换的场景用 v-show,减少节点销毁与重建开销
    • 条件不常变化的场景用 v-if,降低初始渲染压力
  2. 控制响应式数据量级

    • 仅将需要驱动视图更新的数据放入 data,静态常量可定义在组件外部
    • 大体积数组可使用 Object.freeze() 冻结,取消响应式监听,减少内存占用
    • 小程序端底层依赖 setData 渲染,建议批量更新数据,避免频繁零散修改
  3. 长列表专项优化

    • 常规列表使用 scroll-view + 分页加载,避免一次性渲染成百上千节点
    • 超大数据量(上千条)场景,使用官方 <recycle-list> 组件实现虚拟列表渲染
    • 保证 v-forkey 唯一且稳定,避免列表重排时的性能损耗
  4. 计算属性缓存 对于复杂计算的派生数据,使用 computed 计算属性缓存结果,避免重复计算。

5.2 包体积优化

  1. 启用分包加载与预下载 小程序端有严格的体积限制(主包 2MB,总包 20MB),分包是最有效的体积优化手段:

    json

    {
      "pages": [
        {"path": "pages/index/index"},
        {"path": "pages/mine/mine"}
      ],
      "subPackages": [
        {
          "root": "pages-goods",
          "name": "goods",
          "pages": [
            {"path": "list/list"},
            {"path": "detail/detail"}
          ]
        }
      ],
      "preloadRule": {
        "pages/index/index": {
          "network": "wifi",
          "packages": ["goods"]
        }
      }
    }
    

    配合 preloadRule 可实现:进入首页后,在 WiFi 环境自动预下载商品分包,用户跳转时无感加载。

  2. 静态资源瘦身

    • 图片优先使用 WebP 格式,单张图片建议控制在 40KB 以内
    • 大体积图片、音视频资源上传 CDN,使用网络地址,不占用包体积
    • 图标优先使用 iconfont 字体图标,替代多套切图
  3. 代码与组件按需引入

    • uni_modules 插件按需安装,避免未使用的组件被打包
    • 第三方工具库按需引入,减少冗余代码

5.3 网络性能优化

  • 接口请求合并,减少并发请求数量,避免页面加载时请求阻塞
  • 合理使用本地缓存(uni.setStorageSync),不常变化的数据优先读缓存、后台异步更新
  • 图片开启懒加载:<image lazy-load="true" :src="item.img" />,降低首屏带宽压力
  • 接口数据按需返回,剔除冗余字段,减小数据包体积

六、高频踩坑与解决方案

6.1 样式单位适配问题

uni-app 推荐使用 rpx 作为尺寸单位,它会根据屏幕宽度自适应换算:

  • 以 750px 宽度的设计稿为基准时,1rpx = 0.5px = 1 物理像素
  • 字体大小也可使用 rpx,但需注意大屏设备下文字过大的问题,重要文案可搭配 px 使用
  • 小程序端不支持 CSS 百分比高度计算,固定高度请使用 rpx 或 flex 布局

6.2 H5 端接口跨域问题

开发 H5 时,本地请求后端接口会出现跨域,需在 manifest.json 中配置开发代理:

json

{
  "h5": {
    "devServer": {
      "proxy": {
        "/api": {
          "target": "https://api.example.com",
          "changeOrigin": true,
          "pathRewrite": {
            "^/api": ""
          }
        }
      }
    }
  }
}

6.3 小程序端背景图限制

微信小程序不支持本地图片作为 background-image,解决方案:

  1. 使用网络图片地址
  2. 将小图片转为 base64 编码写入样式
  3. 使用 <image> 标签绝对定位,模拟背景图效果

6.4 页面跳转传参长度限制

小程序端 URL 参数长度存在上限,传递大量数据时不建议拼接在 URL 中:

  • 简单数据传递 id,详情页重新请求接口获取完整数据
  • 复杂数据可存入全局状态(Vuex/Pinia)或本地缓存,目标页再读取

6.5 生命周期执行顺序问题

Vue 生命周期与 uni-app 页面生命周期并行执行,执行顺序为:createdonLoadonShowmountedonReady

  • 页面路由参数只能在 onLoad 中获取,created 阶段尚未注入
  • H5 端 DOM 操作需在 onReadymounted 中执行

6.6 小程序端 DOM/BOM 限制

小程序端无浏览器环境,禁止直接使用 windowdocumentlocalStorage 等 Web API,对应能力需使用 uni 官方 API 替代:

  • 本地存储:uni.setStorageSync / uni.getStorageSync
  • 系统信息:uni.getSystemInfoSync
  • 定时器:可正常使用 setTimeout / setInterval

七、打包发布全流程

7.1 微信小程序发布

  1. HBuilderX 顶部菜单 → 运行 → 运行到小程序模拟器 → 微信开发者工具
  2. 微信开发者工具中调试无误后,点击「上传」,填写版本号与项目备注
  3. 登录微信公众平台 → 版本管理 → 提交审核 → 审核通过后点击发布上线

7.2 H5 发布

bash

运行

# 执行生产环境打包命令
npm run build:h5

打包产物位于 dist/build/h5 目录,将该目录部署到 Nginx、静态服务器或对象存储服务即可。

部署提示:若部署在网站子目录下,需在 manifest.json 的 h5 配置中修改 router.base 路径,避免资源引用 404。

7.3 App 打包

  1. HBuilderX 顶部菜单 → 发行 → 原生 App - 云打包
  2. 选择 Android /iOS 平台,填写证书信息与打包配置
  3. 提交云打包,完成后下载 apk/ipa 文件,即可上架对应应用市场

八、总结与进阶学习路径

uni-app 的核心价值,在于用一套熟悉的 Vue 技术栈,解决了多端业务的开发效率问题。对于中小型项目、快速验证的业务需求、企业内部工具类应用,uni-app 都是投入产出比极高的技术选型。

想要从「能用」到「用好」uni-app,建议沿着以下路径持续深耕:

  1. 夯实基础:先吃透 Vue 核心语法与 uni-app 生命周期、API 体系,这是跨端开发的根基
  2. 理解差异:深入了解各端(尤其是小程序)的运行机制与限制,跨端兼容的本质是补齐平台差异
  3. 工程化落地:逐步引入分包、组件库、请求封装、状态管理、自动化构建等工程化能力
  4. 原生扩展:遇到框架能力边界时,学习原生插件开发、nvue 原生渲染等进阶能力,突破性能瓶颈

目前 uni-app 已全面迭代至 Vue3 + Vite 架构,新一代 uni-app x 也在持续演进,跨端性能与开发体验还在不断提升。掌握好这套技术栈,无论是个人独立开发还是企业级项目落地,都能成为你前端能力体系中的重要加分项。

Logo

一站式 AI 云服务平台

更多推荐