前言

在跨端开发技术百花齐放的今天,UniApp 凭借「一套代码,多端运行」的核心优势,已成为中小企业快速落地多端产品的首选方案。从微信小程序、支付宝小程序到 H5、App、抖音小程序,UniApp 能够覆盖几乎所有主流应用平台,大幅降低开发与维护成本。

本文将从环境搭建、核心语法、进阶技巧到企业级封装,系统梳理 UniApp 开发的完整知识体系,配合可直接运行的代码示例与真实项目踩坑总结,帮助读者从入门快速进阶到独立负责项目开发。无论你是前端新手准备入门跨端开发,还是有 Vue 基础想快速上手 UniApp,本文都能为你提供体系化的参考。


一、UniApp 基础认知与环境搭建

1.1 什么是 UniApp

UniApp 是由 DCloud 推出的基于 Vue.js 的跨端开发框架,开发者编写一套代码,可发布到 iOS、Android、H5、以及各种小程序(微信 / 支付宝 / 百度 / 头条 / 飞书 / QQ / 快手 / 钉钉 / 淘宝)等多个平台。

核心优势:

  • 跨端能力强:一套代码同时支持 10+ 平台
  • 学习成本低:完全兼容 Vue 语法,Vue 开发者可无缝上手
  • 生态丰富:官方插件市场有大量现成组件与模板
  • 性能优异:底层通过原生渲染,接近原生应用体验
  • 工具链完善:配套 HBuilderX 编辑器,开箱即用

1.2 开发环境搭建

UniApp 主要有两种开发方式:HBuilderX 可视化开发(推荐新手)和 Vue-CLI 命令行开发(适合有工程化需求的团队)。

方式一:HBuilderX 搭建(官方推荐)
  1. 前往 DCloud 官网 下载 HBuilderX 正式版
  2. 安装后打开,依次点击「文件 → 新建 → 项目」
  3. 选择「uni-app」模板,填写项目名称与存储路径,选择 Vue 版本(推荐 Vue3)
  4. 点击创建,自动生成项目基础结构
方式二:Vue-CLI 脚手架搭建

适合习惯 VS Code 开发、需要自定义构建配置的开发者:

bash

运行

# 全局安装 vue-cli(已安装可跳过)
npm install -g @vue/cli

# 创建 uni-app 项目(Vue3 版本)
npx degit dcloudio/uni-preset-vue#vite-ts my-uniapp-project

# 进入项目目录
cd my-uniapp-project

# 安装依赖
npm install

# 运行到微信小程序
npm run dev:mp-weixin

# 运行到 H5
npm run dev:h5

1.3 项目目录结构详解

一个标准的 UniApp 项目目录如下,每个文件都有明确的职责:

plaintext

├── common              # 公共资源:工具函数、全局样式
├── components          # 自定义组件目录
├── pages               # 页面目录,所有业务页面都放这里
│   └── index           # 首页目录
│       └── index.vue   # 首页文件
├── static              # 静态资源:图片、字体等(注意:此目录不参与编译)
├── uni_modules         # uni_modules 插件目录(插件市场下载的插件放这里)
├── App.vue             # 应用配置文件:全局样式、应用生命周期
├── main.js             # 入口文件:初始化 Vue 实例
├── manifest.json       # 应用配置文件:各平台专属配置
├── pages.json          # 页面路由配置:页面路径、导航栏、tabBar 等
└── uni.scss            # 全局 SCSS 变量文件

💡 注意:static 目录下的文件不会被编译,图片、字体等静态资源必须放在这里;common 目录下的 js、css 文件会被编译处理。


二、核心基础:页面、生命周期与路由

2.1 页面创建与路由配置

在 UniApp 中,每新增一个页面都需要在 pages.json 中注册,否则无法访问。

步骤 1:在 pages 目录下新建页面pages 文件夹新建 detail/detail.vue

vue

<template>
  <view class="detail-container">
    <text>这是详情页</text>
  </view>
</template>

<script setup>
// Vue3 组合式 API 写法
</script>

<style scoped>
.detail-container {
  padding: 30rpx;
}
</style>

步骤 2:在 pages.json 中注册页面

json

{
  "pages": [
    {
      "path": "pages/index/index",
      "style": {
        "navigationBarTitleText": "首页"
      }
    },
    {
      "path": "pages/detail/detail",
      "style": {
        "navigationBarTitleText": "详情页",
        "navigationBarBackgroundColor": "#409EFF",
        "navigationBarTextStyle": "white"
      }
    }
  ],
  "globalStyle": {
    "navigationBarTextStyle": "black",
    "navigationBarTitleText": "UniApp Demo",
    "navigationBarBackgroundColor": "#FFFFFF",
    "backgroundColor": "#F5F5F5"
  }
}

pages 数组的第一项默认为应用首页,style 字段可以单独配置每个页面的导航栏样式、下拉刷新等属性。

2.2 应用生命周期与页面生命周期

UniApp 的生命周期分为应用生命周期(App.vue 中)和页面生命周期(页面组件中)两类。

应用生命周期(仅在 App.vue 中生效)

表格

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

代码示例:App.vue

vue

<script>
export default {
  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-size: 28rpx;
}
</style>
页面生命周期(常用)

表格

生命周期 触发时机
onLoad 页面加载时触发,可获取页面参数
onShow 页面显示时触发,每次切回页面都会执行
onReady 页面初次渲染完成时触发
onHide 页面隐藏时触发
onUnload 页面卸载时触发
onPullDownRefresh 下拉刷新时触发
onReachBottom 页面滚动到底部时触发

代码示例:页面生命周期完整演示

vue

<template>
  <view class="page">
    <text>页面生命周期演示</text>
  </view>
</template>

<script>
export default {
  data() {
    return {}
  },
  onLoad(options) {
    console.log('页面加载,参数:', options)
    // 页面初始化请求数据通常放在这里
  },
  onShow() {
    console.log('页面显示')
    // 需要每次进入都刷新的数据放这里
  },
  onReady() {
    console.log('页面渲染完成')
    // 获取 dom 节点、canvas 绘制等放这里
  },
  onHide() {
    console.log('页面隐藏')
  },
  onUnload() {
    console.log('页面卸载')
    // 清除定时器、取消事件监听等
  },
  onPullDownRefresh() {
    console.log('触发下拉刷新')
    // 刷新数据后调用 uni.stopPullDownRefresh() 结束刷新
  },
  onReachBottom() {
    console.log('滚动到底部')
    // 分页加载下一页数据
  }
}
</script>

2.3 页面跳转与参数传递

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

表格

API 说明 特点
uni.navigateTo 保留当前页面,跳转到新页面 可返回,页面栈最多 10 层
uni.redirectTo 关闭当前页面,跳转到新页面 不可返回
uni.switchTab 跳转到 tabBar 页面 只能用于 tabBar 页面跳转
uni.navigateBack 返回上一页 / 多级页面 delta 参数控制返回层数
uni.reLaunch 关闭所有页面,打开指定页面 常用于登录后重置页面栈

跳转与传参示例:

javascript

运行

// 首页跳转详情页,携带 id 参数
uni.navigateTo({
  url: '/pages/detail/detail?id=1001&name=测试商品'
})

// 详情页接收参数
onLoad(options) {
  console.log(options.id)   // 1001
  console.log(options.name) // 测试商品
}

⚠️ 注意:传递参数如果包含特殊字符(如空格、&、中文),需要使用 encodeURIComponent 编码,接收时解码。


三、进阶核心:条件编译与跨端适配

3.1 条件编译:跨端差异的核心解决方案

条件编译是 UniApp 最核心的特性之一,通过特殊的注释语法,让同一份代码在不同平台编译出不同的内容。

语法格式

plaintext

// #ifdef 平台标识
代码块(仅该平台生效)
// #endif

// #ifndef 平台标识
代码块(除了该平台都生效)
// #endif
常用平台标识

表格

标识 对应平台
APP-PLUS App 端
H5 H5 端
MP-WEIXIN 微信小程序
MP-ALIPAY 支付宝小程序
MP-BAIDU 百度小程序
MP-TOUTIAO 字节跳动小程序
实战场景示例

1. 模板中使用条件编译

vue

<template>
  <view>
    <!-- 仅微信小程序显示 -->
    <!-- #ifdef MP-WEIXIN -->
    <button open-type="getUserInfo">微信一键登录</button>
    <!-- #endif -->
    
    <!-- 仅 H5 显示 -->
    <!-- #ifdef H5 -->
    <button @click="h5Login">账号密码登录</button>
    <!-- #endif -->
    
    <!-- 除了 App 端都显示 -->
    <!-- #ifndef APP-PLUS -->
    <text>非 App 环境提示</text>
    <!-- #endif -->
  </view>
</template>

2. JS 中使用条件编译

javascript

运行

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

3. CSS 中使用条件编译

css

/* 微信小程序下的特殊样式 */
/* #ifdef MP-WEIXIN */
.container {
  padding-top: 20rpx;
}
/* #endif */

3.2 跨端样式适配

rpx 响应式单位

UniApp 推荐使用 rpx 作为尺寸单位,屏幕宽度固定为 750rpx,会根据屏幕宽度自动换算,完美适配不同尺寸设备。

css

/* 设计稿 375px 宽度,1px = 2rpx */
.box {
  width: 300rpx;  /* 对应 150px */
  height: 200rpx; /* 对应 100px */
  margin: 20rpx;
}
样式注意事项
  • 小程序端不支持 * 通配符选择器
  • 小程序端不支持部分 CSS 高级选择器(如相邻兄弟选择器部分场景有兼容问题)
  • 背景图片建议使用网络图片或 base64,本地图片在部分小程序端有限制
  • 使用 scoped 隔离组件样式,避免全局污染

四、企业级封装:网络请求与状态管理

4.1 网络请求统一封装

真实项目中,绝对不能直接在页面里写 uni.request,必须统一封装,便于管理接口、处理错误、添加 loading 和 token 鉴权。

新建 common/request.js:

javascript

运行

// 基础配置
const BASE_URL = 'https://api.example.com'
const TIME_OUT = 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 Promise.resolve(data.data)
    } else if (data.code === 401) {
      // token 过期,跳转登录
      uni.showToast({ title: '登录已过期', icon: 'none' })
      uni.reLaunch({ url: '/pages/login/login' })
      return Promise.reject(data)
    } else {
      uni.showToast({ title: data.msg || '请求失败', icon: 'none' })
      return Promise.reject(data)
    }
  } else {
    uni.showToast({ title: `网络错误 ${statusCode}`, icon: 'none' })
    return Promise.reject(response)
  }
}

// 错误处理
const errorHandler = (error) => {
  uni.hideLoading()
  uni.showToast({ title: '网络连接失败', icon: 'none' })
  return Promise.reject(error)
}

// 封装请求方法
const request = (options) => {
  // 显示 loading(可配置是否显示)
  if (options.loading !== false) {
    uni.showLoading({ title: '加载中', mask: true })
  }
  
  return new Promise((resolve, reject) => {
    let config = {
      url: BASE_URL + options.url,
      method: options.method || 'GET',
      data: options.data || {},
      header: options.header || {},
      timeout: TIME_OUT,
      success: (res) => {
        uni.hideLoading()
        resolve(responseInterceptor(res))
      },
      fail: (err) => {
        uni.hideLoading()
        reject(errorHandler(err))
      }
    }
    
    // 执行请求拦截
    config = requestInterceptor(config)
    uni.request(config)
  })
}

// 封装常用方法
export const http = {
  get: (url, data, options = {}) => request({ url, method: 'GET', data, ...options }),
  post: (url, data, options = {}) => request({ url, method: 'POST', data, ...options }),
  put: (url, data, options = {}) => request({ url, method: 'PUT', data, ...options }),
  delete: (url, data, options = {}) => request({ url, method: 'DELETE', data, ...options })
}

export default http

使用示例:新建 api/goods.js 管理商品接口

javascript

运行

import http from '@/common/request.js'

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

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

页面中调用:

javascript

运行

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

export default {
  data() {
    return {
      list: [],
      page: 1
    }
  },
  onLoad() {
    this.loadList()
  },
  methods: {
    async loadList() {
      try {
        const res = await getGoodsList({ page: this.page, pageSize: 10 })
        this.list = res.records
      } catch (err) {
        console.error('加载失败', err)
      }
    }
  }
}

4.2 Pinia 状态管理(Vue3 推荐)

Vue3 版本的 UniApp 官方推荐使用 Pinia 进行全局状态管理,比 Vuex 更简洁轻量。

安装 Pinia:

bash

运行

npm install pinia

main.js 中注册:

javascript

运行

import { createSSRApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

export function createApp() {
  const app = createSSRApp(App)
  const pinia = createPinia()
  app.use(pinia)
  return { app }
}

创建用户状态模块:stores/user.js

javascript

运行

import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    token: uni.getStorageSync('token') || '',
    userInfo: uni.getStorageSync('userInfo') || {}
  }),
  
  getters: {
    isLogin: (state) => !!state.token
  },
  
  actions: {
    // 登录
    login(token, userInfo) {
      this.token = token
      this.userInfo = userInfo
      uni.setStorageSync('token', token)
      uni.setStorageSync('userInfo', userInfo)
    },
    
    // 退出登录
    logout() {
      this.token = ''
      this.userInfo = {}
      uni.removeStorageSync('token')
      uni.removeStorageSync('userInfo')
      uni.reLaunch({ url: '/pages/login/login' })
    }
  }
})

页面中使用:

vue

<script setup>
import { useUserStore } from '@/stores/user.js'
import { storeToRefs } from 'pinia'

const userStore = useUserStore()
// 解构保持响应式
const { isLogin, userInfo } = storeToRefs(userStore)

const handleLogin = () => {
  userStore.login('token_123', { nickname: '测试用户' })
}
</script>

五、实战案例:待办事项 TodoList

下面通过一个完整的 TodoList 案例,串联前面所学的知识点,包含列表展示、新增、删除、本地存储功能。

vue

<template>
  <view class="todo-container">
    <!-- 顶部输入区 -->
    <view class="input-bar">
      <input 
        class="todo-input" 
        v-model="inputValue" 
        placeholder="输入待办事项..."
        @confirm="addTodo"
      />
      <button class="add-btn" @click="addTodo">添加</button>
    </view>
    
    <!-- 统计信息 -->
    <view class="stats">
      <text>共 {{ total }} 项,已完成 {{ doneCount }} 项</text>
    </view>
    
    <!-- 待办列表 -->
    <view class="todo-list">
      <view 
        class="todo-item" 
        v-for="(item, index) in list" 
        :key="index"
        @click="toggleTodo(index)"
      >
        <view class="checkbox" :class="{ checked: item.done }">
          <text v-if="item.done">✓</text>
        </view>
        <text class="todo-text" :class="{ done: item.done }">{{ item.text }}</text>
        <button class="delete-btn" size="mini" @click.stop="deleteTodo(index)">删除</button>
      </view>
      
      <view class="empty" v-if="list.length === 0">
        <text>暂无待办事项</text>
      </view>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      inputValue: '',
      list: []
    }
  },
  
  computed: {
    total() {
      return this.list.length
    },
    doneCount() {
      return this.list.filter(item => item.done).length
    }
  },
  
  onLoad() {
    // 页面加载时从本地存储读取数据
    const saved = uni.getStorageSync('todo_list')
    if (saved) {
      this.list = JSON.parse(saved)
    }
  },
  
  methods: {
    // 添加待办
    addTodo() {
      if (!this.inputValue.trim()) {
        uni.showToast({ title: '请输入内容', icon: 'none' })
        return
      }
      
      this.list.unshift({
        text: this.inputValue.trim(),
        done: false,
        createTime: Date.now()
      })
      
      this.inputValue = ''
      this.saveToStorage()
      uni.showToast({ title: '添加成功', icon: 'success' })
    },
    
    // 切换完成状态
    toggleTodo(index) {
      this.list[index].done = !this.list[index].done
      this.saveToStorage()
    },
    
    // 删除待办
    deleteTodo(index) {
      uni.showModal({
        title: '提示',
        content: '确定要删除这条待办吗?',
        success: (res) => {
          if (res.confirm) {
            this.list.splice(index, 1)
            this.saveToStorage()
          }
        }
      })
    },
    
    // 保存到本地存储
    saveToStorage() {
      uni.setStorageSync('todo_list', JSON.stringify(this.list))
    }
  }
}
</script>

<style scoped>
.todo-container {
  padding: 30rpx;
  min-height: 100vh;
  box-sizing: border-box;
}

.input-bar {
  display: flex;
  gap: 20rpx;
  margin-bottom: 30rpx;
}

.todo-input {
  flex: 1;
  height: 80rpx;
  padding: 0 20rpx;
  background: #fff;
  border-radius: 10rpx;
  border: 1px solid #e4e7ed;
}

.add-btn {
  width: 160rpx;
  height: 80rpx;
  line-height: 80rpx;
  background: #409eff;
  color: #fff;
  border-radius: 10rpx;
  font-size: 28rpx;
}

.stats {
  margin-bottom: 20rpx;
  color: #909399;
  font-size: 26rpx;
}

.todo-item {
  display: flex;
  align-items: center;
  gap: 20rpx;
  padding: 24rpx 20rpx;
  background: #fff;
  border-radius: 10rpx;
  margin-bottom: 16rpx;
}

.checkbox {
  width: 40rpx;
  height: 40rpx;
  border: 2px solid #dcdfe6;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 24rpx;
  color: #fff;
  flex-shrink: 0;
}

.checkbox.checked {
  background: #67c23a;
  border-color: #67c23a;
}

.todo-text {
  flex: 1;
  font-size: 28rpx;
  color: #303133;
}

.todo-text.done {
  text-decoration: line-through;
  color: #c0c4cc;
}

.delete-btn {
  font-size: 24rpx;
  color: #f56c6c;
}

.empty {
  text-align: center;
  padding: 100rpx 0;
  color: #909399;
}
</style>

六、性能优化实战技巧

6.1 页面性能优化

  1. 分包加载:将不常用的页面放入分包,减少主包体积,提升启动速度

json

{
  "subPackages": [
    {
      "root": "pages/sub",
      "pages": [
        { "path": "setting/setting" }
      ]
    }
  ]
}
  1. 图片懒加载:长列表图片使用 lazy-load 属性,进入可视区再加载

html

预览

<image src="xxx.jpg" lazy-load mode="aspectFill"></image>
  1. 减少 setData 调用:小程序端频繁 setData 会导致卡顿,尽量批量更新数据
  2. 合理使用 onReachBottom:分页加载时加锁,防止滚动到底部重复触发请求

6.2 包体积优化

  • 静态资源尽量使用 CDN,不要放本地
  • 大图片压缩后再使用,推荐 WebP 格式
  • 移除未使用的组件和插件,定期清理无用代码
  • 小程序端开启代码压缩,在 manifest.json 中配置
  • 合理使用条件编译,移除其他平台的冗余代码

七、高频踩坑与解决方案

坑 1:本地图片在小程序端不显示

原因:小程序对本地背景图有限制,不支持 css 中的本地背景图 解决:将图片转为 base64,或上传到 CDN 使用网络地址

坑 2:H5 端跨域问题

原因:浏览器同源策略限制,直接请求后端接口会跨域 解决:在 manifest.json 中配置代理

json

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

坑 3:页面跳转传参丢失

原因:参数中包含特殊字符(&、=、中文)导致解析错误 解决:传递时编码,接收时解码

javascript

运行

// 传递
const data = encodeURIComponent(JSON.stringify(obj))
uni.navigateTo({ url: `/pages/detail/detail?data=${data}` })

// 接收
onLoad(options) {
  const obj = JSON.parse(decodeURIComponent(options.data))
}

坑 4:scroll-view 下拉刷新冲突

原因:页面同时开启了原生下拉刷新和 scroll-view,会出现手势冲突 解决:使用 scroll-view 时关闭页面的 enablePullDownRefresh,使用 scroll-view 自身的下拉刷新

坑 5:App 端软键盘遮挡输入框

解决:在 manifest.json 的 App 配置中开启 adjustResize

json

"app-plus": {
  "softinput": {
    "mode": "adjustResize"
  }
}

八、总结与学习建议

UniApp 的核心价值在于「一次开发,多端运行」,但这并不意味着可以完全忽略平台差异。优秀的 UniApp 开发者,既要熟练运用条件编译处理跨端差异,也要懂得针对不同平台做性能优化和体验适配。

学习路线建议:

  1. 先掌握 Vue 基础,再入手 UniApp 会事半功倍
  2. 从简单页面入手,熟悉生命周期、路由、组件三大基础
  3. 深入学习条件编译,理解跨端原理
  4. 尝试封装通用组件和工具函数,培养工程化思维
  5. 研究官方插件市场,学习优秀开源项目的代码
  6. 真实项目中多踩坑、多总结,积累跨端兼容经验

UniApp 生态还在持续演进,建议多关注官方更新日志和社区动态。跨端开发的本质是在「开发效率」和「原生体验」之间寻找平衡,而 UniApp 无疑是目前性价比最高的方案之一。

Logo

一站式 AI 云服务平台

更多推荐