本文已参与「新人创作礼」活动,一起开启掘金创作之路。
uni-app介绍
是一个使用Vue开发所有前端应用的框架,开发者编写一套代码,可发布多个平台。
多端开发
一套代码编到14个平台
其他跨端框架对比
优秀的小程序开发框架
即使不跨端, uni-app 也是很好的小程序开发框架
比原生的优势
- 精简的代码组织
- 更强大的组件化能力,官方封装了非常多组件
- 应用状态管理(类Vuex/Redux/Mobx等)
- 灵活支持各种
Sass等css预处理器 - 完整的 ES Next 语法支持
- 支持云开发
- 性能优于原生(长列表加载等情形下对比)
- Hbuilder工具支持
- 社区支持
条件编译
uni-app能实现一套代码、多端运行,核心是通过编译器 + 运行时实现的 通过条件编译,将注释里面的代码编译到不同平台。
以 #ifdef 或 #ifndef 加 %PLATFORM% 开头,以 #endif 结尾。
#ifdef H5 || MP-WEIXIN
// Todo
#endif
uni插件及社区
DCloud插件市场
DCloud插件市场 有丰富的插件、组件;支持开发者自己上传提交甚至收费。
插件库
常用插件库推荐
组件库选择原则:vue组件、小程序组件、跨端组件
- uni-ui 官方组件库,支持跨端
- colorUI 非常好看的UI库
- uviewui 功能丰富实用,尤其是内置样式和很多js,vuex,接口封装很完整
uniCloud
类似云开发,需要付费,有免费额度。
uni开发环境准备
Hbuilder一键创建
启动
打开Hbuilder ==》 新建项目(有很多模版可以选择)
启动项目 ==》 如果启动失败检查下微信开发者设置和Hbuilder设置
开发更改是实时预览的~打包发布都可以点击生成
目录结构
┌─components 符合vue组件规范的uni-app组件目录
│ └─comp-a.vue 可复用的a组件
├─pages 业务页面文件存放的目录
│ ├─index
│ │ └─index.vue index页面
├─static 存放应用引用的本地静态资源(如图片、视频等)的目录,注意: 静态资源只能存放于此
├─uni_modules 存放uni_module规范的插件
├─wxcomponents 存放原生小程序组件的目录
├─main.js Vue初始化入口文件
├─App.vue 应用配置,用来配置App全局样式以及监听 应用生命周期
├─manifest.json 配置应用名称、appid、logo、版本等打包信息,详见
└─pages.json 配置页面路由、导航条、选项卡等页面类信息,详见
uni_module
uni-app的插件模块化规范,通常是对一组js sdk、组件、页面、uniCloud云函数、公共模块等的封装,用于嵌入到uni-app项目中使用,也支持直接封装为项目模板,也可以开发自己的插件使用。
uni_module和node_module不太一样,支持云函数,有付费插件的版权保护。可在插件市场下载需要的插件,也可以自己开发通过Hbuilder发布到插件市场。
manifest.json
使用小程序插件:
"mp-weixin": {
"plugins": {
"myPlugin": {
"version": "1.0.0",
"provider": "wxidxxxxxxxxxxxxxxxx",
"export": "index.js"
}
}
}
vue-cli(个人喜好)
启动
# 全局安装vue-cli
npm install -g @vue/cli@4
# 创建uni-app(对应Hbuilder最新的正式版)
vue create -p dcloudio/uni-preset-vue my-project
# 使用vue3版本
npx degit dcloudio/uni-preset-vue#vite my-vue3-project
# 运行和发布
npm run dev:mp-weixin
npm run build:mp-weixin
目录结构
┌─src
│ └─components 符合vue组件规范的uni-app组件目录
│ │ ├─comp-a.vue 可复用的a组件
│ └─pages 可复用的a组件
│ │ ├─index
│ │ │ └─index.vue index页面
│ └─static 本地静态资源
│ └─uni_modules uni插件。
│ └─wxcomponents 存放小程序组件的目录
│ └─main.js Vue初始化入口文件
│ └─App.vue 应用配置,用来配置App全局样式以及监听 应用生命周期
│ └─manifest.json 配置应用名称、appid、logo、版本等打包信息,详见
│ └─pages.json 配置页面路由、导航条、选项卡等页面类信息,详见
└─package.json
两种启动方式的差异
- Hbuilder启动编译器会随着Hbuilder升级升级
- 发布App还是需要Hbuilder操作,小程序等的发布均可
- HBuilder对vue和uni-app的支持更友好:条件编译、图形化操作等
uni与原生小程序
uni中使用原生小程序组件
wxcomponents
支持在 App 和 小程序 中使用小程序自定义组件,从HBuilderX2.4.7起,H5端也可以运行微信小程序组件。如果想用于支付宝百度头条,则需要新建swancomponents等目录。虽然各小程序平台均支持自定义组件,但细节有差异,仍需自己测试。
比较妥善的跨端做法,是在uni-app插件市场寻找类似功能的vue组件,废弃之前的小程序自定义组件。 (uni的处理其实是将该文件夹拷贝到dist目录下)
注意事项
- 小程序组件需要放在项目特殊文件夹
wxcomponents(或 mycomponents、swancomponents)。HBuilderX 建立的工程wxcomponents文件夹在 项目根目录下。vue-cli 建立的工程wxcomponents文件夹在src目录下。可以在 vue.config.js中自定义其他目录 - 当需要在
vue组件中使用小程序组件时,注意在pages.json的globalStyle中配置usingComponents,而不是页面级配置。 - 注意组件使用时需要遵循vue语法,比如:数据和事件绑定的差异,应按照
vue的数据和事件绑定方式
原生小程序转uniapp
miniprogram-to-uniapp
转换小程序做的工作有:
- setData全部转换为data(经测试,部分有遗漏,需要手动检漏)
- template 改为vue
- 生命周期内处理函数移植和对应(不完全匹配需要自行修改)
- ...
原生小程序转uni-app细则
- app.json为pages.json,每个小程序page目录下的index.json添加到pages.json里
- globalData等全局变量或方法更改
- setData的处理
- 自定义组件,需要放到wxcomponents下
- wx api替换为uni api
关于js api中的
wx.,不要全局替换为uni.。因为有的wx的api是微信独有的,替换为uni后,反而在微信下没法用了。替换可以自查下兼容性。 同时uni-app编译器提供了把wx.编译为不同平台的机制,所以直接使用wx.的api完全可以正常在各端运行。新代码api调用还是建议使用uni的,性能更优一些。 - selectComponent 更改为ref,虽然也支持,但是仅能在微信小程序端使用,不通用
<template>
<trtc-room ref="trtcComponent" id="trtc-component" :config="rtcConfig"></trtc-room>
</template>
// 微信小程序中组件通信
this.trtcComponent = this.$mp.page.selectComponent('#trtc-component')
// uni中
this.trtcComponent = this.$refs.trtcComponent;
- 生命周期对应(应用生命周期、页面生命周期、组件生命周期(vue生命周期))
uni开发
需要注意的常用vue语法坑
nextTick
- 在小程序自定义组件,如wxcomponents中使用。wx.nextTick延迟一部分操作到下一个时间片再执行。(类似于 setTimeout) 其他平台无此概念。
- Vue.nextTick 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
slot插槽
<view class="nav-content" >
<slot />
</view>
注意跨端组件兼容性
比如camera组件: 需要注意在各个平台的兼容性; 小程序开发中需要注意api表现是否一致
uni-app非H5端,不支持使用svg标签。但image标签支持svg图片。
状态管理
vuex
专为 Vue.js 应用程序开发的状态管理模式。 作为sdk开发缺点:
- 客户如果也使用了vuex,管理起来比较麻烦,需要多文件管理/命名空间等
- 接入成本高
- 没有那么多复杂的数据需要管理
- 如果客户没有使用vuex,需要install vuex,增加包体
Vue.prototype
Vue.prototype.baseUrl = 'http://uniapp.dcloud.io';
缺点:需要客户引入,增加接入成本 优点:全局调用更改方便
globalData
小程序中有个globalData概念,可以在 App 上声明全局变量。 Vue 之前是没有这类概念的,但 uni-app 引入了globalData概念,并且在包括H5、App等平台都实现了。 globalData绑定到页面,可在页面的onshow声明周期里进行变量重赋值
// App.Vue
<script>
export default {
globalData: {
text: 'text'
},
onLaunch: function() {
// 赋值:
getApp().globalData.text = 'test'
// 取值:
getApp().globalData.text
},
}
</script>
存在的问题:
- 需要挂在App.vue 上,增加客户接入成本
- 命名问题,不能重复,全局调用问题
- globalData不是响应式的,一个文件中对globalData的修改,不会动态的在另一个文件中响应 (如果想实现globalData的"响应",你需要在onShow的生命周期中手动获取值,注意是onShow,比如页面间跳转onLoad等生命周期不一定会执行)
挂载在uni上
实现:同挂在wx上一样,common方法, 挂在uni上,在需要的页面调用 (多次调用记得初始化某些需要的值)
const setCmsConfig = (config) => {
uni.faceidVerifySdkCmsConfig = config ;
}
问题及解决
- 命名不能重复(加专用前缀,防止冲突)
- 多次调用缓存,需初始化
vue2 和 vue3
迁移到 vue3,必须适配的几个点,vue2 项目才能正常运行在 vue3 上。 但如果用了vue3的语法,没办法在vue2中实现。
Vue Demi构建同时兼容Vue2和Vue3的组件库
- 模块导入导出
// 导入
// 之前 - Vue 2, 使用 commonJS
var utils = require("../../../common/util.js");
// 之后 - Vue 3, 只支持 ES6 模块
import utils from "../../../common/util.js";
// 导出
// 之前 - Vue 2, 依赖如使用 commonJS 方式导出
module.exports.X = X;
// 之后 - Vue 3, 只支持 ES6 模块
export default { X };
- 建议避免在同一元素上同时使用 v-if 与 v-for
- 生命周期的变更
-
Vue3 将不支持
slot="xxx"的用法 ,使用v-slot:xxx用法。
常用业务场景封装
uni.request
注意uni.request和wx.request参数返回值差异比较大,不可直接更改。
自定义导航
先在pages.json中取消默认导航
{
"path" : "pages/index/index",
"style" : {
"navigationStyle":"custom"
}
}
uni.getSystemInfo(OBJECT)动态计算状态栏的高度; 内容使用slot的形式插入。 uni.setNavigationBarTitle(OBJECT) 同微信,uni也提供了api修改导航栏设置 wx.getMenuButtonBoundingClientRect() 获取菜单按钮(右上角胶囊按钮)的布局位置信息。坐标信息以屏幕左上角为原点。
导航栏大概代码:
<template>
<view class="fix-full-page">
<view class="navigation-bar" :style="{height:height}">
<view class="navigation-bar-fixed" :style="{height:height}">
<view class="navigation-bar-textbox" :style="{top:marginTop}">
<view class="button-action" hover-class="button-action-active" @click="back_">
<uni-icons type="left" size="20" color="white"></uni-icons>
</view>
<view v-if="config.showHomeBtn" class="button-action" hover-class="button-action-active" @click="home_">
home
</view>
</view>
<!-- title -->
<view class="navigation-bar-title" :style="'margin-top:'+marginTop+';color:'+config.fontcolor">{{cmsConfig.justForJumpVer.title}}</view>
<view class="navigation-bar-settings"></view>
</view>
</view>
<view class="nav-content" :style="{height: contentHeight}">
<slot />
</view>
</view>
</template>
<script>
import uniIcons from "../uni-icons/uni-icons";
export default {
components: {
uniIcons
},
data() {
return {
cmsConfig: uni.faceidVerifySdkCmsConfig
};
},
computed: {
contentHeight(){
return `calc(100% - ${this.height});`
},
height() {
const { platform, statusBarHeight } = uni.getSystemInfoSync();
let height = statusBarHeight + 4; //ios 24px
if (platform.toLowerCase() == "android") {
height += 4; //android 28px
}
// 胶囊高度 32px 下边框6px height 状态栏高度
return height + 38 + "px";
},
marginTop() {
const { platform, statusBarHeight } = uni.getSystemInfoSync();
let height = statusBarHeight + 4;
if (platform.toLowerCase() == "android") {
height += 4;
}
return height + "px";
}
},
methods: {}
};
</script>
--status-bar-heightuni提供了css变量系统状态栏高度来自定义导航栏,方便不用计算。此变量在微信小程序环境为固定 25px ,在 App 里为手机实际状态栏高度。
使用css变量优化:
<view class="status-contents">
<view class="status top-view"></view>
<view class="title" style="height: 88px;">
<text>导航栏</text>
</view>
</view>
.status-contents{
height: calc(var(--status-bar-height) + 88px);
}
.top-view{
width: 100%;
position: fixed;
top:0;
}
.status{
height:var(--status-bar-height);
}
.title{
width: 100%;
position: fixed;
top: var(--status-bar-height);
}
message全局
```javascript
const config = require('../config.js')
var message = {
toast(title, type = 'text') {
uni.showToast({
title,
icon:type
})
},
confirm(title, confirmColor) {
return new Promise((res, rej) => {
uni.showModal({
title,
cancelColor: '#b6b6b6',
confirmColor: confirmColor || config.modalColor,
success: (result) => {
if (result.cancel) {
rej(result)
} else if (result.confirm) {
res(result)
}
}
})
})
},
async message(content, confrimText) {
return new Promise((res) => {
uni.showModal({
title: '提示',
content,
showCancel: false,
confirmColor: config.modalColor,
success: (result) => {
res(result)
}
})
})
}
}
module.exports = message
```
调用示例
```javascript
this.message.toast('你好')
```
globalConfig自定义配置
export default {
normalPath: '' // sdk路径配置
}
trtc音视频组件
uni各平台兼容情况:
安装:
# tim install
npm install tim-wx-sdk
# trtc install
npm install trtc-wx-sdk
<template>
<live-pusher
class="pusher"
:url="pusher.url"
mode="RTC"
:autopush="true"
:enable-camera="true"
:enable-mic="true"
:muted="false"
:enable-agc="false"
:enable-ans="false"
:enable-ear-monitor="false"
:auto-focus="true"
:zoom="true"
:min-bitrate="600"
:max-bitrate="900"
:video-width="360"
:video-height="640"
:beauty="0"
:whiteness="0"
orientation="vertical"
aspect="9:16"
device-position="front"
:remote-mirror="false"
local-mirror=auto
:background-mute="false"
audio-quality="high"
audio-volume-type="voicecall"
:audio-reverb-type="0"
waiting-image="https://mc.qcloudimg.com/static/img/daeed8616ac5df256c0591c22a65c4d3/pause_publish.jpg"
:debug="debug"
@statechange="_pusherStateChangeHandler"
@netstatus="_pusherNetStatusHandler"
@error="_pusherErrorHandler"
@bgmstart="_pusherBGMStartHandler"
@bgmprogress="_pusherBGMProgressHandler"
@bgmcomplete="_pusherBGMCompleteHandler"
/>
</template>
<script>
import TIM from "./libs/tim-wx.js";
import TRTC from "./libs/trtc-wx.js";
export default {
created() {
// 在组件实例刚刚被创建时执行
console.log(TAG_NAME, "created", ENV);
this.TRTC = new TRTC(this);
this.EVENT = this.TRTC.EVENT;
this.InitTRTC();
this.bindTRTCRoomEvent();
},
methods:{
InitTRTC(){
this.userController.on(EVENT.REMOTE_USER_JOIN, event => {
console.log(TAG_NAME, "远端用户进房", event, event.data.userID);
});
// 初始化事件订阅
this.TRTC.on(TRTC_EVENT.LOCAL_JOIN, event => {
console.log("* room LOCAL_JOIN", event);
this._emitter.emit(this.EVENT.LOCAL_JOIN, {
userID: this.pusher.userID
});
});
},
enterRoom(params) {
return new Promise((resolve, reject) => {
// 1. 补齐进房参数,校验必要参数是否齐全
if (params) {
Object.assign(this.pusher, params);
Object.assign(this.config, params);
}
if (!this._checkParam(this.config)) {
reject(new Error("缺少必要参数"));
return;
} // 2. 根据参数拼接 push url,赋值给 live-pusher,
this._getPushUrl(this.config)
.then(pushUrl => {
this.pusher.url = pushUrl;
this.pusher = this.TRTC.enterRoom(this.config);
this.$nextTick(() => {
// 真正进房成功需要通过 1018 事件通知
this.TRTC.getPusherInstance().start(); // 开始推流
this.status.isPush = true;
this._loginIM({ ...this.config, roomID: params.roomID });
setTimeout(() => {
uni.createLivePlayerContext("live-pusher", this).stop();
}, 2000);
resolve();
});
})
.catch(res => {
// 进房失败需要通过 pusher state 事件通知,目前还没有准确的事件通知
console.error(TAG_NAME, "enterRoom error", res);
reject(res);
});
});
},
_initIM(config) {
const tim = TIM.create({
SDKAppID: config.sdkAppID
});
// 0 普通级别,日志量较多,接入时建议使用
// 1 release级别,SDK 输出关键信息,生产环境时建议使用
// 2 告警级别,SDK 只输出告警和错误级别的日志
// 3 错误级别,SDK 只输出错误级别的日志
// 4 无日志级别,SDK 将不打印任何日志
// if (config.debugMode) {
tim.setLogLevel(1);
tim.registerPlugin({ "tim-upload-plugin": TIMUploadPlugin });
// 取消监听
tim.off(TIM.EVENT.SDK_READY, this._onIMReady);
tim.off(TIM.EVENT.MESSAGE_RECEIVED, this._onIMMessageReceived);
tim.off(TIM.EVENT.SDK_NOT_READY, this._onIMNotReady);
tim.off(TIM.EVENT.ERROR, this._onIMError); // 监听事件
tim.on(TIM.EVENT.SDK_READY, this._onIMReady, this);
tim.on(TIM.EVENT.MESSAGE_RECEIVED, this._onIMMessageReceived, this);
tim.on(TIM.EVENT.SDK_NOT_READY, this._onIMNotReady, this);
tim.on(TIM.EVENT.ERROR, this._onIMError, this);
this.tim = tim;
uni.tim = tim;
},
_loginIM(params) {
if (!this.tim) {
return;
}
return this.tim.login({
userID: params.userID,
userSig: params.userSig
});
},
}
}
</script>
遇到的问题
原生组件异步渲染
uni.createCameraContext() 创建并返回 camera 组件的上下文 cameraContext 对象。 cameraContext.startRecord cameraContext.stopRecord
- camera组件用v-if控制了显隐,显示时异步加载未完成就执行startRecord会失败(尤其在切屏再重试时很容易出错)
解决方案;
- 使用定位:position:fixed;left:100%;
- camera相机初始化完成时initdone中执行后续操作
<camera
device-position="front"
flash="off"
@error="ocrCameraError"
@stop="bindstop"
@initdone="handleCameraDone"
/>
- stopRecord无效的问题 startRecord这个api还没有成功返回,如果接着又调stopRecord这个api就会出现没反应。 解决:确保startRecord 返回success了之后再调用停止录像
页面栈10次跳转,超出了将无法打开其他页面
页面跳转方法
-
uni.navigateTo保留当前页面,跳转到应用内的某个页面 打开新页面,新页面入栈 -
uni.redirectTo关闭当前页面,跳转到应用内的某个页面。 当前页面出栈,新页面入栈 -
uni.reLaunch关闭所有页面,打开到应用内的任意页面。 页面全部出栈,只留下新的页面 -
uni.switchTab跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面。 切换tab页面,页面全部出栈,只留下新的 Tab 页面 -
uni.navigateBack关闭当前页面,返回上一页面或多级页面。 返回,页面不断出栈,直到目标返回页注意
navigateTo,redirectTo只能打开非 tabBar 页面。switchTab只能打开tabBar页面。- 不能在
App.vue里面进行页面跳转
getcurrentpages
获取当前页面栈的实例,以数组形式按栈的顺序给出,第一个元素为首页,最后一个元素为当前页面。 当前解决方案: 谨慎使用navigateTo、navigateBack,无需保存页面历史的redirectTo