uni-app开发总结(持续更新...)

912 阅读7分钟

uni-app开发总结文档

项目结构

access(权限)

api(请求后台接口)

components(项目中公用组件)

filters(过滤器)

store(vuex)

static(项目中引用的静态文件如图片等)

common

App.vue

config.js

main.js

manifest.json

package.json

pages.json(页面配置)

	pages(页面)
                        |
                        |-  control 自控
                        |		|-index (tab)
                        |		|
                        |		|_detail 空调详情
                        |
                        |_  device 设备
                                    |-list 设备列表
                                    |
                                    |_detail 设备详情

		

特殊需求功能实现

一、富文本展示

项目:app Android端

使用uniapp官方提供的组件【rich-text】,官方文档地址:uniapp.dcloud.io/component/r…

官方示例:

<!-- 本示例未包含完整css,获取外链css请参考上文,在hello uni-app项目中查看 -->
<template>
    <view class="content">
        <page-head :title="title"></page-head>
        <view class="uni-padding-wrap">
            <view class="uni-title uni-common-mt">
                数组类型
                <text>\nnodes属性为Array</text>
            </view>
            <view class="uni-common-mt" style="background:#FFF; padding:20rpx;">
                <rich-text :nodes="nodes"></rich-text>
            </view>
            <view class="uni-title uni-common-mt">
                字符串类型
                <text>\nnodes属性为String</text>
            </view>
            <view class="uni-common-mt" style="background:#FFF; padding:20rpx;">
                <rich-text :nodes="strings"></rich-text>
            </view>
        </view>
    </view>
</template>

本项目中使用了节点(content)是字符串类型,因为本项目中的富文本中有图片,直接将content用组件渲染到页面上时,图片的宽高会不适应手机端的屏幕导致图片展示不全(如下图),因此本项目对html片段字符串content加工了一下

<!-- typeof content === string 是一段html片段-->
<rich-text :nodes="content"></rich-text>
//获取公告内容
getContent(id) {
    this.$api.businessList({
        id
    }).then(res => {
        if (res && res.code === 0 && res.data) {
            this.info = res.data[0]
            if (this.info.content) {//this.info.content是从后台获取到的需要渲染到页面的内容节点
                const regex = new RegExp('<img', 'gi');
                this.content = this.info.content.replace(regex, '<img style = "max-width: 100%;"')
            }
        }
    }).catch(err => console.log(err))
},

参考文档:ask.dcloud.net.cn/article/357… ask.dcloud.net.cn/article/364…

二、原生标题栏按钮配置

项目:app Android端

官方文档:uniapp.dcloud.io/collocation…

image-20211118181728563.png

//pages.json中配置
{
    “pages”:[
        ...,
        {
			"path": "pages/meeting/roomList",
			"style": {
				"app-plus": {
					"titleNView": {
						"buttons": [ //原生标题栏按钮配置,
							{
								"fontSize": "17px",
								"color": "#3695FF",
								"text": "预约" //原生标题栏增加按钮,点击事件可通过页面的 onNavigationBarButtonTap 函数进行监听
							}
						],
						"titleText": "会议室"
					}
				}
			}
		},
    ]
}
//相应页面中监听原生导航栏按钮点击事件  pages/meeting/roomList页面

//监听原生导航栏 右键【预约】
onNavigationBarButtonTap(e) {
    console.log(e);
    if (e.text === '预约') {
        if(!this.$useAccess('10')){
            uni.showToast({
                title: '对不起,您没有该权限!',
                icon: 'none',
            })
            return
        }
        uni.navigateTo({
            url: './addMeeting'
        })
    }
},
onShow(){
    ...
},
data(){
    return{
        ...
    }
}

三、实现自定义导航栏

项目:app Android端

官方文档:uniapp.dcloud.io/collocation…

本项目中 首页(pages/home/index) 因为导航栏右侧需要自定义图标按钮,所以没有用原生导航栏,而是使用了uniapp插件市场里uni官方出品的插件

自定义导航栏【uni-nav-bar】 地址:ext.dcloud.net.cn/plugin?id=5…

//需要pages.json中将首页的原生导航栏禁掉
{
    "pages":[
        {
			"path": "pages/home/index",
			"style": {
				"enablePullDownRefresh": true,//页面是否可以下拉刷新
				"app-plus": {
					"titleNView": false//禁掉原生导航栏
				},
				"navigationBarTitleText": "首页"
			}
		},
    ]
}
<!--对应的页面中 使用uni-nav-bar-->
<!-- 顶部导航条-->
<uni-nav-bar :fixed="true" :statusBar='true'>
    <view class="tab-bar">首页</view>
    <view slot="right" @click="goMessage" style="position: relative;">
        <view class="flex align-items-c">
            <image src="../../static/ic_warn.png" style="width: 46rpx;height: 46rpx;"></image>
            <view v-if="hasNewMessage" class="nav-bar-num"></view>
        </view>
    </view>
</uni-nav-bar>

四、统计图绘制

1)项目:app Android端

本项目中使用 uni插件市场中的【ucharts echarts】秋云 ucharts echarts 高性能跨全端图表组件,插件地址:ext.dcloud.net.cn/plugin?id=2…

此插件兼容app、小程序、h5。ucharts有在线配置地址,可以配置好后把配置代码移植到项目中,比较方便。但是相较于echarts的可配置项要少,算是一个简易版的统计图绘制库

image-20211119092621251.png

2)项目:微信小程序

【微信小程序】中的统计图绘制,使用的是uni插件市场的【echarts-for-wx】,插件地址:ext.dcloud.net.cn/plugin?id=1…

这个插件是基于 echarts官方提供的【echarts-for-wx】二次封装,专门给uniapp开发的 微信小程序 使用,配置项和echart相差无几(除了部分echart官方的echarts-for-wx不支持的功能)

image-20211119093428105.png

3)未实践,官网可参考demo

从HBuilderX 2.6起,App端新增了renderjs,这是一种运行在视图层的js,vue页面通过renderjs可以操作浏览器对象,进而可以让基于浏览器的库直接在uni-app的App端运行,诸如echart、threejs,详见:renderjs

image-20220303172922227.png

五、即时通讯 WebSocket

项目:app Android端

本项目使用:

本项目中使用了第三方插件【socket.io ui-app版】 ^1.x版本,适用于uni-app的socket.io封装,可用于uni-app、微信小程序。

插件地址:ext.dcloud.net.cn/plugin?id=1…

npm地址:www.npmjs.com/package/@hy…

安装方式:使用npm安装相应版本!!!

实现socket连接探索过程:

尝试一:使用uni官方提供的连接WebSocket方式。 官方文档地址:uniapp.dcloud.io/api/request…

结果失败。使用了一个测试的ws协议地址可以连接成功,但是连接本项目服务端的socket不成功,失败原因分析——本项目服务端的socket版本太低了,导致连接失败。

尝试二:使用uni插件市场第三方插件【GoEasy IM聊天和即时通讯】 。插件地址:ext.dcloud.net.cn/plugin?id=5…

没有尝试,放弃理由:这个是收费的,不同服务类目每月/年 要收取服务费。但是看这个插件介绍很牛叉的样子,提供的功能很多(GoEasy IM聊天即时通讯、客服,支持私聊、群聊、会话列表和历史消息,发送图片/视频/语音/表情,支持通知栏厂商推送)以后如果有这种需求和足够的项目预算可以尝试,能不能用有没有坑我也不知道。

尝试三:使用socket.io

结果失败。在浏览器上可以正常连接、接收消息,但是本项目是app,在真机上运行的时候直接报错(报错原因推断:socket.io封装的一些东西在app中不适用)

尝试四:使用uni插件市场的第三方插件【socket.io ui-app版】。

初次的时候直接npm 安装没有指定版本,安装的是最新版,连接失败,原因是本项目服务端的socket版本太低了。

第二次尝试时,问了一下后端开发人员服务端使用的socket服务端版本——2.x ,于是使用npm 安装【uni-socket1.x 版本,最后成功。

插件地址:ext.dcloud.net.cn/plugin?id=1…

npm地址:www.npmjs.com/package/@hy…

npm i @hyoga/uni-socket.io @1.x --save

image-20211119101246225.png

// 封装socket 连接监听、关闭 方法
// common/utils/socketTool.js
import io from '@hyoga/uni-socket.io'
import config from '../../config.js'
const {
    socket_url//socket连接地址
} = config
​
// 初始化socket连接
export function socketInit(vm) {
    const socket = io.connect(socket_url, {
        query: {},
        transports: ['websocket'],
        timeout: 5000,
    })
​
    socket.on('connect', () => {
        // ws连接已建立,此时可以进行socket.io的事件监听或者数据发送操作
        console.log('ws 已连接', socket_url);
        getApp().globalData.socket = socket
        // 监听告警
        socket.on('device_alarm_data', (data) => {
            if (!vm.$store.state.realAlarmList.length && !data.length) return
            console.log('device_alarm_data==', data, typeof data)
            vm.$store.dispatch('updateRealAlarmList', data)
        })
        // 监听待审批会议
        socket.on('new_meeting_examine', (data) => {
            console.log('new_meeting_examine==', data)
            vm.$store.dispatch('changeHasNewNewApprMeet', true)
        })
        // 监听待审批工单
        socket.on('new_order_examine', (data) => {
            console.log('new_order_examine==', data)
            vm.$store.dispatch('changeHasNewApprOrder', true)
        })
        // 监听工单审批结果
        socket.on('order_apply_result', (data) => {
            const obj = JSON.parse(data)
            if (obj.contactUserId == uni.getStorageSync('userId')) {
                console.log('order_apply_result 需要通知')
                this.getMessageList(1)
            }
        })
        // 监听会议审批结果
        socket.on('meeting_apply_result', (data) => {
            const obj = JSON.parse(data)
            if (obj.userId == uni.getStorageSync('userId')) {
                console.log('meeting_apply_result 需要通知')
                this.getMessageList(2)
            }
        })
        // 监听待接工单
        socket.on('new_order', (data) => {
            console.log('new_order==', data)
            vm.$store.dispatch('changeHasNewReceOrder', true)
        })
    });
​
    socket.on('connect_error', (msg) => {
        console.log('ws error', msg);
    });
}
​
// 关闭socket连接
export function socketClose() {
    let socket = getApp().globalData.socket
    if (socket) {
        socket.close()
        socket = undefined
    }
}
//页面中使用
//App.vue
import {socketInit,socketClose} from '@/common/utils/socketTool.js'
​
...
onShow: function() {
    console.log('App Show', this)
    if (uni.getStorageSync('userId')) {
       ...
        // 连接socket.io
        socketInit(this, getApp().globalData.socket)
    }
},
onHide: function() {
    console.log('App Hide')
    socketClose() //关闭socket
},
...

六、权限

// 在pages文件夹的同路径下新建文件夹access,access下新建index.js文件
// 权限查找
/**
 * 判断是否有这个权限
 * @param {String|Array[String]}  需要查询的权限
 * @return {Boolean} 返回判断结果
 */
export default function(authorize) {
	let result = false
	if (authorize !== undefined) {
		let authorizeList = Array.isArray(authorize) ? authorize : [authorize]
		const AUTH_LIST = JSON.parse(uni.getStorageSync('pageAuth'))
		// console.log('AUTH_LIST==',AUTH_LIST) AUTH_LIST是该登录账户下有的权限id数组 ['1','4','30']
		for (let v in authorizeList) {
			if (AUTH_LIST.indexOf(authorizeList[v]) < 0) {
				return result = false
			} else {
				result = true
			}
		}
	} else {
		throw new Error('$useAccess参数不能为空!')
	}
	return result
}

// access/authList.js
// 没啥用,就是为了记录app中功能对应的权限
export default [
	'2',//事务处理
	'3',//已接工单
	'4',//待接工单
	'5',//工单审批
	'6',//会议审批
	'7',//自控
	'8',//照明控制
	'9',//空调控制
	'10',//会议预约
	'11',//事件上报
	'12',//告警消息通知
]
// main.js 中 将useAccess挂到Vue原型下
import useAccess from '@/access/index.js'

...
Vue.prototype.$useAccess = useAccess //权限
...
// 项目页面中使用案例 pages/home/index.vue
<!-- 会议审批 -->
<view class="card5" style="background-image: url('../../static/img_meeting@2x.png');"
    @click="goMeetApproveList" v-if="$useAccess('6')">
        <view class="text" style="margin-right:18rpx">
            会议审批
    </view>
    <view class="card5_num" v-if="hasUnApprMeet || $store.state.hasNewApprMeet"></view>
</view>

...

methods:{
    // 查询是否有未接工单
    getUnReceOrder() {
        if (!this.$useAccess('4')) return //未接工单对应的权限id是'4',这里判断该用户是否有此权限
        this.$api.orderMissedList({
            pageNum: 1,
            pageSize: 1,
        }).then(res => {
            if (res && res.code === 0 && res.data && res.data.length) {
                this.hasUnReceOrder = true
            } else {
                this.hasUnReceOrder = false
            }
        }).catch(err => {
            console.log(err)
            this.hasUnReceOrder = false
        })
    },
    ...
}

登录时调用登录接口,将登录成功后接口返回的数据中的pageAuth(该账户有的权限)放入storage中。

调用获取用户详情成功后接口返回的数据中的pageAuth(该账户有的权限)放入storage中

pageAuth:['1','4','10']

七、版本检测更新

1)小程序
// App.vue文件
<script>
export default {
    onLaunch: function() {
        console.log('App Launch')
        if (uni.canIUse('getUpdateManager')) {
            const updateManager = uni.getUpdateManager()
            //检测版本更新
            updateManager.onCheckForUpdate(function(res) {
                // 请求完新版本信息的回调
                if (res.hasUpdate) {
                    //监听小程序有版本更新事件
                    updateManager.onUpdateReady(function() {
                        uni.showModal({
                            title: '更新提示',
                            content: '新版本已经准备好,是否重启应用?',
                            success(res) {
                                if (res.confirm) {
                                    // 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
                                    updateManager.applyUpdate();
                                }
                            }
                        })
                    })

                    updateManager.onUpdateFailed(function() {
                        // 新版本下载失败
                        uni.showModal({
                                title: '已经有新版本咯~',
                                content: '请您删除当前小程序,到微信 “发现-小程序” 页,重新搜索打开呦~',
                        })
                    })
                }
            })
        }
    },
    
    onShow: function() {
        console.log('App Show')
    },
    onHide: function() {
        console.log('App Hide')
    }
}
</script>

<style  lang="scss">
	/*每个页面公共css */
	@import '@/common/scss/common.scss';
</style>

2)app

uniapp 支持原生APP整包升级ask.dcloud.net.cn/article/349… 资源在线升级/热更新ask.dcloud.net.cn/article/356…

可参考博文:www.cnblogs.com/goloving/p/…

举例:整包更新(应用项目:app)

// App.vue
/*
在onLaunch中,请求后台接口拿到最新版本号,与 plus.runtime.version(当前运行版本号——即当前运行的apk包在打包时manifest.json中配置的versionName) 对比,若不一样则调用【plus.runtime.openURL(最新apk的下载地址)】下载最新包。
注意:一定要写条件编译 #ifdef APP-PLUS 仅在 App 平台执行此升级逻辑。
*/
// #ifdef APP-PLUS
API.getVersion()
    .then(res => {
    if (res && res.code === 0) {
        const {
            data
        } = res

        if (data.version && data.version != plus.runtime.version && data.downloadPath) {
            uni.showModal({ //提醒用户更新  
                title: "发现新版本",
                content: "确认下载更新",
                success: (result) => {
                    if (result.confirm) {
                        plus.runtime.openURL(data.downloadPath);//下载最新包
                    }
                }
            })
        }
    }
})
    .catch(err => console.log(err))
// #endif

自开发项目基础框架CLI(基于Vue2)

git地址:gitee.com/zf-zfzn/zf-…

image-20220224154650928.png

该CLI 集成/引入了:Vuex、filters过滤器;

对request 请求进行了二次封装:【common】—【http】模块,包括了请求、响应拦截器,可完成统一请求错误处理、请求头token设置 等;

App.vue中加入了【检测版本更新】功能;

在此基础上可继续进行业务代码的开发,可拓展性高;

开发中遇到的坑

1、app真机调试(运行到手机)

1)调试准备

连接手机数据线 =>手机打开 【开发者选项】=> 允许【USB调试模式】=>允许【USB安装】、允许【USB调试】=> 关闭【停用adb授权超时功能】=>【默认USB配置】选择【MIDI】

2)真机运行失败,失败原因:手机与HBuilder连接失败,请拔掉手机重新插入或重开USB调试模式,并重新运
行真机调试

①拔掉数据线重开USB调试模式

② 步骤①还没有解决的话,可参考ask.dcloud.net.cn/question/13…

image-20220309163904269.png

3)真机运行失败,失败原因:手机上没有信任本计算机的授权,请在手机上信任该授权

① 撤销USB调试授权,拔掉连接的数据线,

重开USB调试,连接数据线,手机会弹出授权窗口,授权后即可正确调试。

image-20220309164345204.png

②若步骤①做完后,手机没有弹出授权窗口,可以参考:zhuanlan.zhihu.com/p/427117697…

手机开发者选项中,关闭【停用adb授权超时功能】;

计算机中删除【adbkey】【adbkey.pub】后;

重新连接usb,手机弹出了授权窗口,授权后,就可以调试了。

image-20220309165734658.png

image-20220309170033957.png