uni-app多端跨平台开发从入门到企业级实战 读书笔记
1. 入门篇
1.1 uni-app 简介
1.1.1 什么是 uni-app
uni-app 是 DCloud 公司推出的一款使用 Vue.js 开发所有前端应用的跨平台框架,开发者编写一套代码,可发布到 iOS、Android、H5、以及各种小程序(微信/支付宝/百度/头条/QQ/钉钉/淘宝)等多个平台。uni-app 基于 Vue.js 框架,结合了小程序的开发规范,提供了一套统一的开发体验。
1.1.2 uni-app 的技术架构
uni-app 采用了分层架构设计,从底层到上层依次为:
- 运行时核心:基于不同平台的原生引擎,提供统一的运行时环境
- 编译器:将 Vue.js 代码编译为各平台可执行的代码
- 框架层:提供 Vue.js 语法支持和跨平台 API 封装
- 组件库:提供丰富的 UI 组件和业务组件
- 插件系统:支持扩展平台能力和功能
1.1.3 uni-app 的工作原理
uni-app 的工作原理主要包括以下几个方面:
- 代码编译:开发者编写的 Vue.js 代码通过编译器编译为各平台的原生代码
- 运行时适配:运行时根据不同平台的特性进行适配
- API 封装:封装各平台的原生 API,提供统一的调用接口
- 组件渲染:根据不同平台的渲染机制,将组件渲染为原生 UI
1.1.4 uni-app 的优势
- 开发效率高:一套代码多端运行,减少重复开发,提高开发效率
- 性能优异:底层使用原生渲染,性能接近原生应用,部分场景甚至超过原生
- 生态丰富:集成了大量的 UI 组件和插件,满足各种开发需求
- 开发成本低:使用 Vue.js 语法,学习成本低,开发者可以快速上手
- 跨平台能力强:支持 10+ 个平台,覆盖移动端、Web 和小程序
- 原生能力:可以通过插件机制调用各平台的原生能力
- 热更新:支持 App 端热更新,无需重新提交应用商店
1.1.5 uni-app 的应用场景
- 企业级移动应用:需要跨平台部署,降低开发成本
- 电商应用:需要多端覆盖,提高用户触达率
- 内容类应用:需要快速迭代,多端同步更新
- 工具类应用:需要轻量级部署,快速上线
- 小程序开发:需要同时开发多个平台的小程序
- SaaS 应用:需要为不同客户提供定制化的移动应用
- 内部管理系统:需要快速开发,多端访问
1.1.6 uni-app 与其他跨平台框架的对比
| 框架 | 优势 | 劣势 |
|---|
| uni-app | 支持平台多,性能优异,生态丰富 | 部分平台特性需要条件编译 |
| Flutter | 性能优异,UI 一致性好 | 学习成本高,生态相对较弱 |
| React Native | 生态丰富,社区活跃 | 性能不如原生,平台差异大 |
| Ionic | Web 技术栈,学习成本低 | 性能不如原生,体验差异大 |
| 原生开发 | 性能最佳,体验最好 | 开发成本高,多端维护困难 |
1.2 开发环境搭建
1.2.1 安装 HBuilderX
HBuilderX 是 DCloud 公司推出的一款轻量级、高性能的前端开发工具,专为 uni-app 开发优化,集成了代码编辑器、模拟器、打包工具等功能。
安装步骤:
- 访问 HBuilderX 官网
- 下载对应操作系统的安装包(推荐下载 App 开发版,包含 uni-app 相关插件)
- 解压并安装 HBuilderX
- 首次启动时,HBuilderX 会自动安装必要的插件
HBuilderX 配置优化:
- 编辑器配置:在「工具」→「设置」→「编辑器」中配置字体、缩进、代码提示等
- 插件安装:在「工具」→「插件安装」中安装 uni-ui、uni-stat 等插件
- 快捷键配置:在「工具」→「设置」→「快捷键」中配置常用快捷键
1.2.2 创建 uni-app 项目
方法一:使用 HBuilderX 创建
步骤:
- 打开 HBuilderX
- 点击「文件」→「新建」→「项目」
- 选择「uni-app」项目类型
- 填写项目名称、存储位置
- 选择模板(默认模板、Hello uni-app、空项目、TypeScript 模板等)
- 点击「创建」按钮
方法二:使用命令行工具创建
uni-app 提供了命令行工具 @dcloudio/uni-cli,可以通过 npm 安装并使用:
1 2 3 4 5 6 7 8 9 10 11
| npm install -g @dcloudio/uni-cli
uni init my-project
cd my-project
npm install
|
项目初始化高级选项:
- TypeScript 支持:选择 TypeScript 模板或手动配置 tsconfig.json
- ESLint 配置:在项目根目录创建 .eslintrc.js 文件
- Prettier 配置:在项目根目录创建 .prettierrc 文件
- Git 初始化:使用
git init 初始化 Git 仓库
1.2.3 运行项目
运行到浏览器:
- 点击工具栏的「运行」按钮
- 选择「运行到浏览器」→ 选择浏览器
- 浏览器会自动打开项目页面
运行到小程序模拟器:
- 安装对应小程序的开发工具(微信开发者工具、支付宝开发者工具等)
- 在小程序开发工具中开启「端口号」设置(微信开发者工具:设置 → 安全设置 → 开启服务端口)
- 在 HBuilderX 中点击「运行」→ 选择对应小程序
- HBuilderX 会自动启动小程序开发工具并加载项目
运行到手机:
Android 设备:
- 连接手机到电脑
- 开启 USB 调试模式(设置 → 开发者选项 → USB 调试)
- 在 HBuilderX 中点击「运行」→ 选择「运行到 Android App 基座」
- 手机会自动安装并启动 uni-app 基座应用
iOS 设备:
- 连接 iPhone/iPad 到 Mac 电脑
- 在 HBuilderX 中点击「运行」→ 选择「运行到 iOS App 基座」
- 需要在 Xcode 中配置开发者证书
运行到云端模拟器:
HBuilderX 提供了云端模拟器功能,可以在没有真机的情况下测试应用:
- 在 HBuilderX 中点击「运行」→ 选择「运行到云端模拟器」
- 选择对应的平台和设备类型
- 等待云端模拟器启动并加载项目
1.2.4 开发工具配置
微信开发者工具配置:
- 开启服务端口:设置 → 安全设置 → 开启服务端口
- 关闭 ES6 转 ES5:设置 → 项目设置 → 关闭「ES6 转 ES5」
- 关闭代码压缩:设置 → 项目设置 → 关闭「代码压缩」
HBuilderX 性能优化:
- 关闭不必要的插件
- 调整编辑器缓存大小
- 使用 SSD 存储提高读写速度
多环境配置:
可以在项目中配置多个环境(开发、测试、生产),通过不同的配置文件区分:
1 2 3 4 5 6 7 8 9 10 11 12
| module.exports = { development: { baseURL: 'https://dev-api.example.com' }, test: { baseURL: 'https://test-api.example.com' }, production: { baseURL: 'https://api.example.com' } };
|
1.3 项目结构
1.3.1 基本目录结构
uni-app 项目采用了清晰的目录结构,便于开发者组织代码和资源:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| ├── api/ # API 接口封装 ├── components/ # 组件目录 ├── config/ # 配置文件目录 ├── filters/ # 过滤器目录 ├── mixins/ # 混入目录 ├── pages/ # 页面目录 ├── services/ # 服务层目录 ├── static/ # 静态资源目录 ├── store/ # 状态管理目录 ├── styles/ # 样式文件目录 ├── utils/ # 工具函数目录 ├── unpackage/ # 打包输出目录 ├── App.vue # 应用入口文件 ├── main.js # 应用入口文件 ├── manifest.json # 应用配置文件 ├── pages.json # 页面配置文件 ├── uni.scss # 全局样式文件 ├── package.json # 项目依赖配置 └── tsconfig.json # TypeScript 配置文件
|
1.3.2 目录结构优化
企业级项目目录结构最佳实践:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| ├── api/ # API 接口封装 │ ├── modules/ # 按模块划分的 API │ ├── request.js # 请求封装 │ └── index.js # API 导出 ├── components/ # 组件目录 │ ├── base/ # 基础组件 │ ├── business/ # 业务组件 │ └── common/ # 通用组件 ├── config/ # 配置文件目录 │ ├── env.js # 环境配置 │ ├── api.js # API 配置 │ └── theme.js # 主题配置 ├── filters/ # 过滤器目录 ├── mixins/ # 混入目录 ├── pages/ # 页面目录 │ ├── home/ # 首页相关页面 │ ├── user/ # 用户相关页面 │ └── product/ # 产品相关页面 ├── services/ # 服务层目录 │ ├── auth.js # 认证服务 │ ├── user.js # 用户服务 │ └── product.js # 产品服务 ├── static/ # 静态资源目录 │ ├── images/ # 图片资源 │ ├── fonts/ # 字体资源 │ └── icons/ # 图标资源 ├── store/ # 状态管理目录 │ ├── modules/ # 按模块划分的 store │ └── index.js # store 导出 ├── styles/ # 样式文件目录 │ ├── variables.scss # 样式变量 │ ├── mixins.scss # 样式混入 │ └── common.scss # 通用样式 ├── utils/ # 工具函数目录 │ ├── request.js # 网络请求工具 │ ├── storage.js # 存储工具 │ └── validator.js # 验证工具 ├── App.vue # 应用入口文件 ├── main.js # 应用入口文件 ├── manifest.json # 应用配置文件 ├── pages.json # 页面配置文件 ├── uni.scss # 全局样式文件 ├── package.json # 项目依赖配置 └── tsconfig.json # TypeScript 配置文件
|
1.3.3 配置文件详细说明
1. manifest.json
manifest.json 是 uni-app 的应用配置文件,用于配置应用的基本信息、权限、启动画面等:
- 基础配置:应用名称、版本号、描述、图标等
- 权限配置:相机、定位、录音等权限
- 平台配置:各平台的特有配置
- SDK 配置:地图、支付等 SDK 的配置
示例配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| { "name": "uni-app 示例", "appid": "__UNI__XXXXXX", "versionName": "1.0.0", "versionCode": "100", "description": "uni-app 示例应用", "author": { "name": "开发者", "email": "developer@example.com" }, "permissions": { "scope.userLocation": { "desc": "用于获取用户位置信息" }, "scope.camera": { "desc": "用于拍照和扫码" } }, "mp-weixin": { "appid": "wxXXXXXXXXXXXX", "setting": { "urlCheck": false }, "usingComponents": true }, "h5": { "devServer": { "port": 8080, "proxy": { "/api": { "target": "https://api.example.com", "changeOrigin": true, "pathRewrite": { "^/api": "" } } } } } }
|
2. pages.json
pages.json 是 uni-app 的页面配置文件,用于配置页面路由、导航栏、底部 tab 等:
- pages:配置页面路由和页面样式
- tabBar:配置底部 tab 栏
- globalStyle:配置全局样式
- subPackages:配置分包加载
- preloadRule:配置页面预加载规则
示例配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| { "pages": [ { "path": "pages/index/index", "style": { "navigationBarTitleText": "首页", "enablePullDownRefresh": true } }, { "path": "pages/detail/detail", "style": { "navigationBarTitleText": "详情页", "navigationStyle": "custom" } } ], "tabBar": { "color": "#666666", "selectedColor": "#007AFF", "borderStyle": "black", "backgroundColor": "#FFFFFF", "list": [ { "pagePath": "pages/index/index", "text": "首页", "iconPath": "static/tabbar/home.png", "selectedIconPath": "static/tabbar/home-active.png" }, { "pagePath": "pages/user/user", "text": "我的", "iconPath": "static/tabbar/user.png", "selectedIconPath": "static/tabbar/user-active.png" } ] }, "globalStyle": { "navigationBarTextStyle": "white", "navigationBarTitleText": "uni-app 示例", "navigationBarBackgroundColor": "#007AFF", "backgroundColor": "#F5F5F5" }, "subPackages": [ { "root": "pages/product", "pages": [ { "path": "list/list", "style": { "navigationBarTitleText": "产品列表" } }, { "path": "detail/detail", "style": { "navigationBarTitleText": "产品详情" } } ] } ], "preloadRule": { "pages/index/index": { "network": "all", "packages": ["pages/product"] } } }
|
3. uni.scss
uni.scss 是 uni-app 的全局样式文件,用于定义全局样式变量和混合:
- 颜色变量:主题色、文本色、背景色等
- 尺寸变量:字体大小、间距、边框等
- 混合宏:常用的样式混合
示例配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| $theme-color: #007AFF; $text-color: #333333; $text-color-secondary: #666666; $text-color-light: #999999; $background-color: #F5F5F5; $border-color: #E5E5E5;
$font-size-xs: 24rpx; $font-size-sm: 28rpx; $font-size-base: 32rpx; $font-size-lg: 36rpx; $font-size-xl: 40rpx;
$spacing-xs: 10rpx; $spacing-sm: 20rpx; $spacing-base: 30rpx; $spacing-lg: 40rpx; $spacing-xl: 50rpx;
$border-radius-sm: 4rpx; $border-radius-base: 8rpx; $border-radius-lg: 12rpx; $border-radius-xl: 16rpx;
@mixin flex-center { display: flex; align-items: center; justify-content: center; }
@mixin flex-between { display: flex; align-items: center; justify-content: space-between; }
@mixin text-ellipsis { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@mixin text-ellipsis-2 { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }
|
1.3.4 模块化开发建议
1. 组件模块化
- 基础组件:封装通用的 UI 组件,如按钮、输入框、列表等
- 业务组件:封装特定业务的组件,如商品卡片、订单列表等
- 页面组件:封装页面级的组件,如首页轮播、个人中心头部等
2. API 模块化
- 按业务模块划分:将 API 按业务模块划分,如用户、产品、订单等
- 统一请求封装:封装请求拦截器和响应拦截器
- 错误处理统一:统一处理 API 错误和异常
3. 状态管理模块化
- 按业务模块划分:将状态按业务模块划分,如用户状态、产品状态等
- 模块化命名:使用命名空间避免状态冲突
- 异步操作封装:将异步操作封装在 actions 中
4. 工具函数模块化
- 按功能划分:将工具函数按功能划分,如网络、存储、验证等
- 统一导出:通过 index.js 统一导出工具函数
- 类型定义:为工具函数添加 TypeScript 类型定义
1.3.5 代码组织最佳实践
1. 页面代码组织
每个页面建议按照以下结构组织代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| <template> <!-- 页面模板 --> </template>
<script> // 导入组件、工具函数等 import { mapState, mapActions } from 'vuex'; import MyComponent from '@/components/MyComponent.vue'; import { getUserInfo } from '@/api/user';
export default { name: 'PageName', components: { MyComponent }, data() { return { // 页面数据 }; }, computed: { ...mapState('user', ['userInfo']), // 计算属性 }, watch: { // 监听属性 }, created() { // 页面创建时执行 }, mounted() { // 页面挂载时执行 }, onLoad(options) { // 页面加载时执行 }, onShow() { // 页面显示时执行 }, methods: { ...mapActions('user', ['login']), // 页面方法 } }; </script>
<style scoped> /* 页面样式 */ </style>
|
2. 组件代码组织
每个组件建议按照以下结构组织代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| <template> <!-- 组件模板 --> </template>
<script> export default { name: 'MyComponent', props: { // 组件属性 }, data() { return { // 组件数据 }; }, computed: { // 计算属性 }, watch: { // 监听属性 }, created() { // 组件创建时执行 }, mounted() { // 组件挂载时执行 }, methods: { // 组件方法 emitEvent() { this.$emit('eventName', data); } } }; </script>
<style scoped> /* 组件样式 */ </style>
|
3. API 代码组织
API 接口建议按照以下结构组织代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import request from '@/utils/request';
export const getUserInfo = () => { return request({ url: '/user/info', method: 'GET' }); };
export const login = (data) => { return request({ url: '/user/login', method: 'POST', data }); };
export * from './user'; export * from './product'; export * from './order';
|
通过合理的目录结构和代码组织,可以提高项目的可维护性、可扩展性和可读性,为企业级应用开发奠定良好的基础。
2. 核心概念
2.1 页面生命周期
uni-app 的页面生命周期结合了 Vue 的生命周期和小程序的生命周期特点,形成了一套独特的生命周期体系。理解页面生命周期对于开发高质量的 uni-app 应用至关重要。
2.1.1 页面生命周期函数
| 函数名 | 说明 | 平台差异 | 执行时机 |
|---|
| onLoad | 页面加载时触发,只触发一次 | 所有平台 | 页面创建后,首次显示前 |
| onShow | 页面显示/切入前台时触发 | 所有平台 | 页面加载后或从后台切入前台时 |
| onReady | 页面初次渲染完成时触发,只触发一次 | 所有平台 | 页面显示后,首次渲染完成时 |
| onHide | 页面隐藏/切入后台时触发 | 所有平台 | 页面从前台切入后台时 |
| onUnload | 页面卸载时触发 | 所有平台 | 页面被关闭或跳转时 |
| onPullDownRefresh | 下拉刷新时触发 | 所有平台 | 用户下拉页面时 |
| onReachBottom | 上拉触底时触发 | 所有平台 | 页面滚动到底部时 |
| onShareAppMessage | 用户点击分享时触发 | 微信小程序、QQ小程序 | 用户点击分享按钮时 |
| onShareTimeline | 用户点击分享到朋友圈时触发 | 微信小程序 | 用户点击分享到朋友圈按钮时 |
| onAddToFavorites | 用户点击收藏时触发 | 微信小程序 | 用户点击收藏按钮时 |
| onPageScroll | 页面滚动时触发 | 所有平台 | 页面滚动时 |
| onResize | 窗口尺寸变化时触发 | 微信小程序、H5 | 窗口尺寸变化时 |
| onTabItemTap | 点击 tab 栏时触发 | 所有平台 | 点击 tab 栏时 |
| onNavigationBarButtonTap | 点击导航栏按钮时触发 | 所有平台 | 点击导航栏按钮时 |
| onBackPress | 页面返回时触发 | 所有平台 | 页面返回时 |
2.1.2 生命周期执行顺序
完整的页面生命周期执行顺序:
页面加载阶段:
- beforeCreate(Vue 生命周期)
- created(Vue 生命周期)
- onLoad(uni-app 生命周期)
- onShow(uni-app 生命周期)
- beforeMount(Vue 生命周期)
- mounted(Vue 生命周期)
- onReady(uni-app 生命周期)
页面交互阶段:
- onPageScroll(页面滚动时)
- onPullDownRefresh(下拉刷新时)
- onReachBottom(上拉触底时)
- onShareAppMessage(分享时)
- onTabItemTap(点击 tab 时)
页面隐藏阶段:
页面卸载阶段:
- beforeDestroy(Vue 生命周期)
- destroyed(Vue 生命周期)
- onUnload(uni-app 生命周期)
2.1.3 生命周期平台差异
不同平台的生命周期差异:
| 平台 | 差异点 |
|---|
| H5 | 支持所有 Vue 生命周期,部分 uni-app 生命周期可能有差异 |
| 微信小程序 | 支持所有 uni-app 生命周期,Vue 生命周期正常执行 |
| 支付宝小程序 | 部分生命周期可能有延迟,如 onReady 可能晚于 mounted |
| 百度小程序 | 生命周期执行顺序可能与其他平台略有不同 |
| 头条小程序 | 部分生命周期可能不支持,如 onShareTimeline |
| QQ小程序 | 与微信小程序类似,但部分 API 有差异 |
| App | 支持所有生命周期,性能最优 |
2.1.4 生命周期最佳实践
1. 数据初始化
- onLoad:适合进行数据初始化,如获取页面参数、发起网络请求等
- created:适合进行数据观测、计算属性初始化等
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| onLoad(options) { const { id } = options; this.fetchData(id); },
methods: { fetchData(id) { uni.showLoading({ title: '加载中...' }); uni.request({ url: `/api/detail/${id}`, success: (res) => { this.detail = res.data; }, complete: () => { uni.hideLoading(); } }); } }
|
2. 页面渲染
- onReady:适合进行 DOM 操作,如获取元素尺寸、初始化第三方库等
- mounted:适合进行页面初始化操作,如绑定事件监听器等
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| onReady() { const query = uni.createSelectorQuery().in(this); query.select('.header').boundingClientRect((rect) => { this.headerHeight = rect.height; }).exec(); this.initThirdPartyLib(); },
methods: { initThirdPartyLib() { } }
|
3. 页面隐藏与卸载
- onHide:适合暂停页面活动,如定时器、动画等
- onUnload:适合清理资源,如定时器、事件监听器、WebSocket 连接等
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| data() { return { timer: null }; },
onShow() { this.timer = setInterval(() => { this.updateData(); }, 1000); },
onHide() { if (this.timer) { clearInterval(this.timer); } },
onUnload() { if (this.timer) { clearInterval(this.timer); this.timer = null; } this.cleanupResources(); },
methods: { updateData() { }, cleanupResources() { } }
|
4. 下拉刷新与上拉加载
- onPullDownRefresh:适合刷新页面数据
- onReachBottom:适合加载更多数据
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| onPullDownRefresh() { this.page = 1; this.dataList = []; this.fetchData(); },
onReachBottom() { if (!this.loading && this.hasMore) { this.page++; this.fetchData(); } },
methods: { fetchData() { this.loading = true; uni.request({ url: '/api/list', data: { page: this.page, limit: 10 }, success: (res) => { if (this.page === 1) { this.dataList = res.data.list; } else { this.dataList = [...this.dataList, ...res.data.list]; } this.hasMore = res.data.list.length === 10; }, complete: () => { this.loading = false; uni.stopPullDownRefresh(); } }); } }
|
2.1.5 生命周期与组件生命周期的关系
页面生命周期与组件生命周期的执行顺序:
- 页面的 beforeCreate
- 页面的 created
- 页面的 onLoad
- 页面的 onShow
- 页面的 beforeMount
- 组件的 beforeCreate
- 组件的 created
- 组件的 beforeMount
- 组件的 mounted
- 页面的 mounted
- 页面的 onReady
注意事项:
- 组件的生命周期嵌套在页面的生命周期中执行
- 父组件的生命周期先于子组件执行
- 页面的 onReady 会在所有组件 mounted 后执行
2.1.6 生命周期的性能优化
1. 避免在生命周期中执行耗时操作
- 耗时操作应放在异步函数中执行
- 避免在 onLoad、onShow 等生命周期中执行大量计算
2. 合理使用缓存
- 对于不常变化的数据,可使用本地缓存
- 避免重复发起网络请求
3. 及时清理资源
- 在 onUnload 中清理定时器、事件监听器等
- 避免内存泄漏
4. 优化渲染性能
- 避免在 onPageScroll 中执行频繁的 DOM 操作
- 使用防抖和节流优化滚动事件
通过合理使用页面生命周期,可以提高应用的性能和用户体验,避免常见的性能问题和内存泄漏。
2.2 组件
2.2.1 内置组件
uni-app 提供了丰富的内置组件,如 view、text、button、image 等,这些组件在各个平台都有良好的兼容性。
常用内置组件:
- view:视图容器,类似于 HTML 的 div
- text:文本组件
- button:按钮组件
- image:图片组件
- navigator:页面跳转组件
- scroll-view:滚动视图组件
- swiper:轮播图组件
- picker:选择器组件
- input:输入框组件
2.2.2 自定义组件
创建自定义组件:
- 在 components 目录下创建组件目录
- 创建 .vue 文件,编写组件代码
- 在页面中引入并注册组件
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <!-- components/my-component.vue --> <template> <view class="my-component"> <text>{{ title }}</text> </view> </template>
<script> export default { name: 'my-component', props: { title: { type: String, default: '默认标题' } }, data() { return {}; } }; </script>
<style scoped> .my-component { padding: 20rpx; background-color: #f5f5f5; } </style>
|
使用组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <template> <view> <my-component title="自定义组件" /> </view> </template>
<script> import myComponent from '@/components/my-component.vue';
export default { components: { myComponent } }; </script>
|
2.3 API
uni-app 封装了各平台的原生 API,提供了统一的调用方式。
2.3.1 常用 API
- 网络请求:uni.request()
- 存储:uni.setStorageSync()、uni.getStorageSync()
- 导航:uni.navigateTo()、uni.redirectTo()、uni.switchTab()
- 界面:uni.showToast()、uni.showLoading()、uni.showModal()
- 设备:uni.getSystemInfoSync()
- 媒体:uni.chooseImage()、uni.uploadFile()
2.3.2 API 调用示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| uni.request({ url: 'https://api.example.com/data', method: 'GET', success: (res) => { console.log(res.data); }, fail: (err) => { console.error(err); } });
uni.setStorageSync('userInfo', { name: '张三', age: 20 });
const userInfo = uni.getStorageSync('userInfo');
uni.navigateTo({ url: '/pages/detail/detail?id=1' });
|
2.4 路由与页面跳转
2.4.1 路由配置
在 pages.json 中配置页面路由:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| { "pages": [ { "path": "pages/index/index", "style": { "navigationBarTitleText": "首页" } }, { "path": "pages/detail/detail", "style": { "navigationBarTitleText": "详情页" } } ], "tabBar": { "list": [ { "pagePath": "pages/index/index", "text": "首页", "iconPath": "static/home.png", "selectedIconPath": "static/home-active.png" }, { "pagePath": "pages/mine/mine", "text": "我的", "iconPath": "static/mine.png", "selectedIconPath": "static/mine-active.png" } ] } }
|
2.4.2 页面跳转方式
- uni.navigateTo():保留当前页面,跳转到应用内的某个页面
- uni.redirectTo():关闭当前页面,跳转到应用内的某个页面
- uni.reLaunch():关闭所有页面,打开到应用内的某个页面
- uni.switchTab():跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面
- uni.navigateBack():关闭当前页面,返回上一页面或多级页面
3. 组件与 UI
3.1 内置 UI 组件
3.1.1 uni-ui
uni-ui 是 DCloud 官方提供的一套 UI 组件库,适配多端,风格统一。
安装:
在 HBuilderX 中点击「工具」→「插件安装」→ 搜索「uni-ui」并安装。
使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <template> <view> <uni-badge text="12" type="error"></uni-badge> <uni-button type="primary">按钮</uni-button> </view> </template>
<script> import { uniBadge, uniButton } from '@dcloudio/uni-ui';
export default { components: { uniBadge, uniButton } }; </script>
|
3.1.2 其他 UI 库
- uView UI:基于 uni-app 的 UI 框架
- ColorUI:鲜亮的高饱和色彩,专注视觉的小程序组件库
- ThorUI:轻量级高性能 UI 组件库
3.2 自定义组件开发
3.2.1 组件通信
- props/emit:父组件向子组件传递数据,子组件向父组件传递事件
- $refs:父组件通过 ref 访问子组件实例
- globalData:全局数据
- Vuex:状态管理
3.2.2 组件生命周期
组件的生命周期与页面生命周期类似,但也有一些差异:
- beforeCreate:组件实例创建前
- created:组件实例创建后
- beforeMount:组件挂载前
- mounted:组件挂载后
- beforeUpdate:组件更新前
- updated:组件更新后
- beforeDestroy:组件销毁前
- destroyed:组件销毁后
3.3 组件封装最佳实践
- 单一职责:一个组件只负责一个功能
- 可配置性:通过 props 提供配置项
- 事件传递:通过 emit 传递事件
- 样式隔离:使用 scoped 或 CSS Modules
- 文档完善:提供清晰的使用文档
4. 状态管理
4.1 Vuex
uni-app 内置了 Vuex,可以直接使用。
4.1.1 创建 store
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| import Vue from 'vue'; import Vuex from 'vuex';
Vue.use(Vuex);
export default new Vuex.Store({ state: { userInfo: null, token: '' }, mutations: { setUserInfo(state, userInfo) { state.userInfo = userInfo; }, setToken(state, token) { state.token = token; } }, actions: { login({ commit }, userInfo) { return new Promise((resolve) => { setTimeout(() => { commit('setUserInfo', userInfo); commit('setToken', 'mock-token'); resolve(); }, 1000); }); } }, getters: { isLoggedIn: state => !!state.token } });
|
4.1.2 在 main.js 中引入
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import Vue from 'vue'; import App from './App.vue'; import store from './store';
Vue.config.productionTip = false;
App.mpType = 'app';
const app = new Vue({ ...App, store }); app.$mount();
|
4.1.3 在组件中使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <template> <view> <text v-if="isLoggedIn">{{ userInfo.name }}</text> <button v-else @click="login">登录</button> </view> </template>
<script> import { mapState, mapGetters, mapActions } from 'vuex';
export default { computed: { ...mapState(['userInfo']), ...mapGetters(['isLoggedIn']) }, methods: { ...mapActions(['login']), handleLogin() { this.login({ name: '张三', age: 20 }); } } }; </script>
|
4.2 Pinia
Pinia 是 Vue 3 推荐的状态管理库,也可以在 uni-app 中使用。
4.2.1 安装 Pinia
4.2.2 创建 store
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', { state: () => ({ userInfo: null, token: '' }), getters: { isLoggedIn: (state) => !!state.token }, actions: { login(userInfo) { return new Promise((resolve) => { setTimeout(() => { this.userInfo = userInfo; this.token = 'mock-token'; resolve(); }, 1000); }); } } });
|
4.2.3 在 main.js 中引入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import Vue from 'vue'; import App from './App.vue'; import { createPinia, PiniaVuePlugin } from 'pinia';
Vue.use(PiniaVuePlugin); const pinia = createPinia();
Vue.config.productionTip = false;
App.mpType = 'app';
const app = new Vue({ ...App, pinia }); app.$mount();
|
4.2.4 在组件中使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <template> <view> <text v-if="userStore.isLoggedIn">{{ userStore.userInfo.name }}</text> <button v-else @click="handleLogin">登录</button> </view> </template>
<script> import { useUserStore } from '@/store/user';
export default { data() { return { userStore: useUserStore() }; }, methods: { handleLogin() { this.userStore.login({ name: '张三', age: 20 }); } } }; </script>
|
5. 网络请求
5.1 基础网络请求
5.1.1 使用 uni.request
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| uni.request({ url: 'https://api.example.com/data', method: 'GET', header: { 'Content-Type': 'application/json' }, data: { page: 1, limit: 10 }, success: (res) => { console.log(res.data); }, fail: (err) => { console.error(err); } });
|
5.2 封装请求工具
5.2.1 创建 request.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
|
const baseURL = 'https://api.example.com';
const requestInterceptor = (config) => { const token = uni.getStorageSync('token'); if (token) { config.header.Authorization = `Bearer ${token}`; } return config; };
const responseInterceptor = (response) => { if (response.statusCode === 200) { return response.data; } else { uni.showToast({ title: '请求失败', icon: 'none' }); return Promise.reject(response); } };
const request = (options) => { options.url = baseURL + options.url; options = requestInterceptor(options); return new Promise((resolve, reject) => { uni.request({ ...options, success: (res) => { const result = responseInterceptor(res); resolve(result); }, fail: (err) => { reject(err); } }); }); };
export const get = (url, params) => { return request({ url, method: 'GET', data: params }); };
export const post = (url, data) => { return request({ url, method: 'POST', data }); };
export default request;
|
5.2.2 使用封装的请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import { get, post } from '@/utils/request';
get('/user/list', { page: 1, limit: 10 }) .then((res) => { console.log(res); }) .catch((err) => { console.error(err); });
post('/user/login', { username: 'admin', password: '123456' }) .then((res) => { console.log(res); }) .catch((err) => { console.error(err); });
|
5.3 跨域问题
5.3.1 开发环境跨域
在 HBuilderX 中,可以在 manifest.json 中配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| { "h5": { "devServer": { "proxy": { "/api": { "target": "https://api.example.com", "changeOrigin": true, "pathRewrite": { "^/api": "" } } } } } }
|
5.3.2 生产环境跨域
生产环境需要在服务器端配置 CORS:
1 2 3 4 5 6 7 8 9
| location / { add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS'; add_header Access-Control-Allow-Headers 'Content-Type, Authorization'; if ($request_method = 'OPTIONS') { return 204; } }
|
6. 跨平台适配
6.1 平台差异
uni-app 虽然支持多端运行,但不同平台之间仍然存在一些差异。
6.1.1 平台差异表
| 功能 | H5 | 微信小程序 | 支付宝小程序 | 百度小程序 | 头条小程序 | QQ小程序 |
|---|
| 支付 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 |
| 分享 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 |
| 地图 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 |
| 扫码 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 |
| 推送 | 不支持 | 支持 | 支持 | 支持 | 支持 | 支持 |
6.2 条件编译
使用条件编译可以针对不同平台编写不同的代码。
6.2.1 条件编译语法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| <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 { mounted() { // #ifdef H5 console.log('H5 平台'); // #endif // #ifdef MP-WEIXIN console.log('微信小程序平台'); // #endif } }; </script>
<style> /* #ifdef H5 */ view { font-size: 16px; } /* #endif */
/* #ifdef MP-WEIXIN */ view { font-size: 32rpx; } /* #endif */ </style>
|
6.2.2 平台标识
| 平台 | 标识 |
|---|
| H5 | H5 |
| 微信小程序 | MP-WEIXIN |
| 支付宝小程序 | MP-ALIPAY |
| 百度小程序 | MP-BAIDU |
| 头条小程序 | MP-TOUTIAO |
| QQ小程序 | MP-QQ |
| 钉钉小程序 | MP-DINGTALK |
| 淘宝小程序 | MP-TAOBAO |
| App | APP-PLUS |
| App-nvue | APP-NVUE |
| 快应用 | QUICKAPP-WEBVIEW |
6.3 适配方案
6.3.1 尺寸适配
- 使用 rpx 单位:rpx 是相对单位,1rpx = 屏幕宽度/750
- 避免使用 px 单位:px 是绝对单位,在不同屏幕尺寸下显示效果不同
- 使用 flex 布局:flex 布局可以自适应不同屏幕尺寸
6.3.2 样式适配
- 使用 uni.scss 中的全局变量
- 使用条件编译针对不同平台编写不同的样式
- 使用 CSS 变量实现主题切换
6.3.3 API 适配
- 使用 uni-app 封装的统一 API
- 对于平台特有的 API,使用条件编译
- 封装平台差异,提供统一的调用接口
7. 性能优化
7.1 页面性能优化
7.1.1 渲染性能
- 减少节点数量:避免嵌套层级过深
- 使用虚拟列表:对于长列表,使用虚拟列表减少 DOM 节点
- 避免频繁更新:使用防抖、节流减少频繁更新
- 使用分包加载:减小主包体积,提高加载速度
7.1.2 启动性能
- 减少主包体积:将不常用的页面放入分包
- 预加载:使用 uni.preloadPage() 预加载页面
- 骨架屏:使用骨架屏提升用户体验
- 懒加载:图片、组件懒加载
7.2 网络性能优化
7.2.1 请求优化
- 合并请求:减少 HTTP 请求次数
- 缓存策略:使用本地缓存减少重复请求
- 压缩数据:使用 gzip 压缩数据
- CDN 加速:使用 CDN 加速静态资源
7.2.2 图片优化
- 图片压缩:使用压缩后的图片
- 图片格式:使用 WebP 格式图片
- 图片懒加载:使用 uni.createIntersectionObserver() 实现图片懒加载
- 占位符:使用占位符提升用户体验
7.3 代码优化
7.3.1 代码分割
- 分包加载:将应用分为主包和分包
- 按需加载:使用动态导入按需加载组件
7.3.2 内存优化
- 及时销毁:组件销毁时清理定时器、事件监听器
- 减少全局变量:避免使用过多的全局变量
- 合理使用闭包:避免内存泄漏
8. 企业级应用开发
8.1 项目架构
8.1.1 目录结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| ├── api/ # API 接口 ├── components/ # 组件 ├── pages/ # 页面 ├── static/ # 静态资源 ├── store/ # 状态管理 ├── utils/ # 工具函数 ├── services/ # 服务层 ├── middlewares/ # 中间件 ├── filters/ # 过滤器 ├── mixins/ # 混入 ├── App.vue # 应用入口 ├── main.js # 应用入口 ├── manifest.json # 应用配置 ├── pages.json # 页面配置 └── uni.scss # 全局样式
|
8.1.2 分层架构
- API 层:封装接口调用
- Service 层:封装业务逻辑
- Store 层:状态管理
- Component 层:UI 组件
- Page 层:页面
8.2 开发规范
8.2.1 命名规范
- 文件命名:使用小写字母,单词之间用连字符连接
- 组件命名:使用 PascalCase 命名法
- 变量命名:使用 camelCase 命名法
- 常量命名:使用 UPPER_SNAKE_CASE 命名法
- 函数命名:使用 camelCase 命名法
8.2.2 代码规范
- 缩进:使用 2 个空格
- 分号:使用分号结束语句
- 引号:使用单引号
- 括号:使用大括号包裹代码块
- 注释:使用 JSDoc 注释
8.3 版本控制
8.3.1 Git 规范
分支管理:
- master:主分支
- develop:开发分支
- feature:功能分支
- hotfix:修复分支
提交规范:
- feat:新增功能
- fix:修复 bug
- docs:文档更新
- style:代码风格调整
- refactor:代码重构
- test:测试代码
- chore:构建工具或依赖更新
8.4 部署与发布
8.4.1 构建流程
- 代码检查:使用 ESLint 检查代码
- 单元测试:运行单元测试
- 构建:使用 HBuilderX 或命令行构建
- 部署:部署到服务器或发布到应用商店
8.4.2 发布平台
- H5:部署到 Web 服务器
- 微信小程序:发布到微信小程序平台
- 支付宝小程序:发布到支付宝小程序平台
- 百度小程序:发布到百度小程序平台
- 头条小程序:发布到头条小程序平台
- QQ小程序:发布到 QQ 小程序平台
- App:发布到应用商店
8.5 监控与错误处理
8.5.1 错误监控
- try/catch:捕获同步错误
- Promise.catch:捕获异步错误
- 全局错误处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| Vue.config.errorHandler = (err, vm, info) => { console.error('Vue 错误:', err); console.error('错误信息:', info); };
window.addEventListener('unhandledrejection', (event) => { console.error('未处理的 Promise 错误:', event.reason); });
window.addEventListener('error', (event) => { console.error('全局错误:', event.error); });
|
8.5.2 性能监控
1 2 3 4 5 6 7 8 9
| const startTime = Date.now();
window.addEventListener('load', () => { const loadTime = Date.now() - startTime; console.log('首屏加载时间:', loadTime); });
|
1 2 3 4 5 6 7 8 9 10 11
| uni.onPageShow((res) => { const pageStartTime = Date.now(); uni.onPageReady((res) => { const renderTime = Date.now() - pageStartTime; console.log('页面渲染时间:', renderTime); }); });
|
9. 实战案例
9.1 电商应用
9.1.1 功能模块
- 首页:轮播图、分类导航、推荐商品
- 商品列表:商品展示、筛选、排序
- 商品详情:商品信息、规格选择、加入购物车
- 购物车:商品管理、数量修改、结算
- 订单:订单列表、订单详情、订单状态
- 个人中心:用户信息、收货地址、订单管理
9.1.2 技术实现
- 状态管理:使用 Vuex 管理全局状态
- 网络请求:封装 request 工具
- UI 组件:使用 uni-ui 组件库
- 支付:集成各平台支付
- 分享:集成各平台分享
9.2 企业管理系统
9.2.1 功能模块
- 登录:账号密码登录、验证码登录
- 首页:数据看板、快捷入口
- 用户管理:用户列表、用户详情、添加/编辑用户
- 角色管理:角色列表、权限配置
- 权限管理:菜单权限、按钮权限
- 数据统计:数据图表、报表导出
9.2.2 技术实现
- 权限控制:基于角色的权限控制
- 数据可视化:使用 echarts 实现数据图表
- 表单验证:使用 async-validator 进行表单验证
- 导出功能:使用 xlsx 库实现 Excel 导出
- WebSocket:实时数据推送
10. 总结与展望
10.1 uni-app 的优势
- 开发效率高:一套代码多端运行
- 性能优异:底层使用原生渲染
- 生态丰富:大量的 UI 组件和插件
- 学习成本低:使用 Vue.js 语法
- 跨平台能力强:支持 10+ 个平台
10.2 uni-app 的局限性
- 平台差异:不同平台之间存在差异
- 原生能力:部分原生能力需要使用插件
- 性能优化:需要针对不同平台进行优化
10.3 未来发展
- Vue 3 支持:全面支持 Vue 3
- 更多平台:支持更多的平台
- 更好的性能:进一步提升性能
- 更丰富的生态:完善生态系统
10.4 学习建议
- 掌握基础知识:Vue.js、JavaScript、CSS
- 实践项目:通过实际项目巩固知识
- 关注官方文档:及时了解最新特性
- 社区交流:参与社区讨论,分享经验
- 持续学习:关注前端技术发展趋势
附录
常用 API 参考
| API | 说明 | 平台支持 |
|---|
| uni.request | 网络请求 | 所有平台 |
| uni.setStorageSync | 存储数据 | 所有平台 |
| uni.getStorageSync | 获取存储数据 | 所有平台 |
| uni.navigateTo | 页面跳转 | 所有平台 |
| uni.showToast | 显示提示信息 | 所有平台 |
| uni.showLoading | 显示加载动画 | 所有平台 |
| uni.hideLoading | 隐藏加载动画 | 所有平台 |
| uni.showModal | 显示模态框 | 所有平台 |
| uni.chooseImage | 选择图片 | 所有平台 |
| uni.uploadFile | 上传文件 | 所有平台 |
| uni.downloadFile | 下载文件 | 所有平台 |
| uni.getSystemInfoSync | 获取系统信息 | 所有平台 |
| uni.getLocation | 获取位置信息 | 所有平台 |
| uni.makePhoneCall | 拨打电话 | 所有平台 |
常见问题与解决方案
Q: 跨域问题如何解决?
A: 在开发环境中,在 manifest.json 中配置 devServer.proxy;在生产环境中,在服务器端配置 CORS。
Q: 如何优化首屏加载速度?
A: 减少主包体积、使用分包加载、预加载页面、使用骨架屏、图片懒加载。
Q: 如何处理平台差异?
A: 使用条件编译、封装平台差异、参考官方文档的平台差异说明。
Q: 如何实现图片懒加载?
A: 使用 uni.createIntersectionObserver() 实现图片懒加载。
Q: 如何实现自定义导航栏?
A: 在 pages.json 中设置 navigationStyle: ‘custom’,然后自定义导航栏组件。
Q: 如何实现下拉刷新和上拉加载?
A: 在 pages.json 中配置 enablePullDownRefresh: true,然后在页面中实现 onPullDownRefresh 和 onReachBottom 方法。
Q: 如何实现全局状态管理?
A: 使用 Vuex 或 Pinia 实现全局状态管理。
Q: 如何实现路由拦截?
A: 使用 uni.addInterceptor() 实现路由拦截。
Q: 如何实现支付功能?
A: 使用 uni.requestPayment() 实现支付功能,注意不同平台的参数差异。
Q: 如何实现分享功能?
A: 在页面中实现 onShareAppMessage 方法,注意不同平台的参数差异。
资源推荐
开发工具
结语
uni-app 作为一个跨平台开发框架,为开发者提供了一种高效、便捷的开发方式。通过学习本读书笔记,你应该已经掌握了 uni-app 的核心概念、开发技巧和最佳实践。
在实际开发中,你需要根据具体项目的需求,灵活运用这些知识,同时关注官方文档的更新,及时了解新特性和最佳实践。
最后,希望你在 uni-app 开发的道路上越走越远,开发出更多优秀的跨平台应用!