赞
踩
总结 uniapp 多端项目三个月开发维护的经验,遇到并解决了什么困难,收获了什么。多端指:
部分机型中调用 uniapp
提供的 Api 进行拍照会偶现应用闪退,具体分析在 应用闪退分析与 uniapp 安卓原生插件开发,已经确认是系统回收资源,结束了应用进程导致的(调用系统相机进行拍摄,应用处于后台)。
解决方案:
目前 uniapp 插件市场有很多自定义相机可以使用,或者可以基于 uniapp 官方的 live-pusher 组件和 nvue 结合自定义自己的相机;这里我是写了个仿微信应用内相机的原生插件:
主要使用了三个依赖库来完成拍照、编辑、裁切功能:
感兴趣的可以去 u-android-native-plugin-camera 看看,或者下载 demo 体验。
搞这个还是花了不少时间的,毕竟不是专业的 Android 开发,后续可能会就实现这个功能写一篇文章作为经验分享。
项目是 C 端项目,有 C 到 C 的通讯需求,也接入了各大平台的推送通知功能;系统自带的通知在应用处于前台时是不会弹出提示的,而项目要尽量跟微信对齐,需要能够在应用内弹出通知,也就是这种效果:
这里我对比了微信 Android 端和 iOS 端的应用内通知,区别如下:
这个需求在项目中是使用自定义组件来完成的,这种解决方式有两个很明显的缺点:
我尝试使用原生插件的方式来解决,参考了 NotificationToast 的实现方式,使用 Andorid 中的 Toast
类并加上一些黑科技来模拟系统通知,如下:
因为只是一个 Toast
轻提示, 所以不需要通知权限,可以做到的功能大概如下(PS:不建议使用这种方式代替系统通知,当做应用内通知使用还是可以的):
想做到但没有做到的功能:自定义弹出动画,尝试了网上的一些文章都不能很好的完成效果。
iOS 端,因为没有 iOS 的开发经验,所以不知道有什么方式可以解决,不过也就是添加一个视图,肯定也是能够实现的。
感兴趣的可以去 u-android-native-plugin-notify 看看,或者下载 demo 体验。
这个问题在 入职一个月,总结下 uniapp 多端项目遇到的一些问题与解决方案 中提到过,iOS 侧滑返回是无法阻止的,如果我们在导航守卫中阻止用户离开,而用户使用了侧滑进行了强制返回,此时会出现这种情况:页面卡顿,一段时间后重新回到那个阻止离开的页面,如下图:
用户通过侧滑强制返回时,不能阻止,所以需要知道当前是否是侧滑;经过查找资料和测试,侧滑返回可以靠以下特征来标识:
基于这些特征我写了个判断是否是侧滑返回的工具:
/* * @Author: yuanyxh * @Date: 2023-12-27 12:04:11 * @Last Modified by: yuanyxh * @Last Modified time: 2023-12-27 12:04:11 */ import Vue from "vue"; const ZERO = 0; /** 最小左侧触摸开始位置 */ const MINIMUM_LEFT_DISTANCE = 100; /** 最小移动距离 */ const MINIMUM_MOVE_DISTANCE = 50; /** 最小间隔事件 */ const MINIMUM_TIME_INTERVAL = 100; /** * * @callback OnSideSlipListener * @param {number} sideTime 上次侧滑事件 */ /** * * @description 侦听 ios 侧滑事件, 通过此工具配合 beforeRouteLeave 导航守卫判断是否侧滑返回 */ const sideSlip = (function sideSlipListener(global) { let startX = 0, endX = 0; let sideTime = 0; const vm = Vue.prototype; /** @type {OnSideSlipListener[]} */ const callbackList = []; function start(e) { const point = e.touches?.length ? e.touches[0] : e; startX = point.pageX; } function end(e) { const point = e.changedTouched?.length ? e.changedTouched[0] : e; endX = point.pageX; if ( startX >= ZERO && startX <= MINIMUM_LEFT_DISTANCE && endX < ZERO && vm.windowWidth - Math.abs(endX) > MINIMUM_MOVE_DISTANCE ) { sideTime = Date.now(); callbackList.forEach((callback) => callback(sideTime)); } } function isWithinValidityPeriod(time) { const interval = Date.now() - time; return interval >= ZERO && interval < MINIMUM_TIME_INTERVAL; } if (vm.isIos) { global.addEventListener("touchstart", start); global.addEventListener("touchend", end); global.addEventListener("touchcancel", end); } return { /** * @description 是否左滑 * @readonly */ get isSideSlip() { if (vm.isIos) { return isWithinValidityPeriod(sideTime); } return false; }, /** * @description 判断侧滑时间是否在有效期内 * @param {number} time 侧滑时间 * @returns {boolean} 是否有效 */ isWithinValidityPeriod: isWithinValidityPeriod, /** * * @description 侦听侧滑 * @param {OnSideSlipListener} callback */ on(callback) { if (typeof callback === 'function') { callbackList.push(callback); } }, /** * @description 取消事件侦听 * @param {OnSideSlipListener} callback */ off(callback) { const i = callbackList.indexOf(callback); if (i === -1) return; callbackList.splice(i, 1); }, }; })( (function() { let global = {}; if (typeof window !== "undefined") { return (global = window); } global = { addEventListener() {}, removeEventListener() {}, }; return global; })() ); export default sideSlip;
具体逻辑为:判断是否侧滑返回,touchend 时记录当前时间,在指定时间间隔内触发了诸如 onUnload
、beforeRouteLeave
等钩子时就可以认为是侧滑返回。
不知道从什么版本开始,小米浏览器加了一个让人恼火的功能:左右滑屏手势。通过这个功能可以让左右滑动手势控制网页的前进后退,如下:
看起来很美好,但却与很多项目中的滑动手势冲突,比如 Element 中的滑块功能:
这时候一般就会想到阻止冒泡、阻止默认事件来禁用左右滑动手势了,但恼火的点就在于没有效果,由此可以猜测这个功能的实现没有在浏览器内核中或者没有适配我们熟知的那一套 js 事件体系。
目前还没有研究出解决方案,有知道如何解决的大佬欢迎讨论。
因为内核的限制,网页和基于 webview 实现的框架下使用定时器时,进入后台一段时间后定时器会被挂起,回到前台时才继续执行,导致程序的运行和我们预想的有偏差。
解决方案:
performance.now()
结合来完成,performance.now()
返回的是距 时间源 过去的时间,且不会被系统时间影响。这里给出个人认为的最佳实践:
performance.now()
,App 可以使用 worker 或原生插件App 端软键盘高度的获取还是比较简单的,H5 端就比较难受了,因为不同浏览器对于软键盘弹出时的页面模式是不能确认的,导致我们不能简单的通过 window.onresize
事件来判断软键盘是否弹出,好在目前主流浏览器都支持了 Window.visualViewport
接口,也能兼容到大部分的主流浏览器版本。
我们可以通过 Window.visualViewport
接口的 resize
事件,接收到可视窗口变化的事件,以此计算出软键盘的高度。
import Vue from "vue"; const DELAY_TIME = 300; let callbackList = []; let unimplementedChangeList = []; function emit(target, payload) { if (target.length) { for (let i = 0; i < target.length; i++) { target[i](payload); } } } // 页面原始高度 let windowHeight = 0; function onKeyboardHeightChangeWithH5() { let extraHeight = 0; let hasFocus = false; let originScrollY = 0; let cancheHeight = 0; let keyboardChangeTimer = null; function exec(height) { const keyboardHeight = Math.max(0, windowHeight - height); if (cancheHeight === keyboardHeight) { return; } cancheHeight = keyboardHeight; const { isIos } = Vue.prototype; emit(callbackList, { extra: isIos ? extraHeight : 0, height: keyboardHeight, }); } window.addEventListener( "focus", (e) => { if ( e instanceof FocusEvent && (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || e.target.contenteditable) ) { hasFocus = true; originScrollY = window.scrollY; setTimeout(() => { hasFocus = false; }, 600); } }, { capture: true } ); window.addEventListener( "blur", (e) => { if ( e instanceof FocusEvent && (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || e.target.contenteditable) ) { hasFocus = true; setTimeout(() => { hasFocus = false; }, DELAY_TIME); } }, { capture: true } ); window.addEventListener( "scroll", () => { if (hasFocus) { extraHeight = window.scrollY - originScrollY; } }, { capture: true } ); if (typeof window.visualViewport !== "undefined") { return window.visualViewport.addEventListener("resize", (e) => { if (hasFocus === false) { return emit(unimplementedChangeList); } if (keyboardChangeTimer) { clearTimeout(keyboardChangeTimer); } keyboardChangeTimer = setTimeout(() => { exec(e.target.height); }, DELAY_TIME); }); } window.addEventListener("resize", () => { if (hasFocus === false) { return emit(unimplementedChangeList); } if (keyboardChangeTimer) { clearTimeout(keyboardChangeTimer); } keyboardChangeTimer = setTimeout(() => { exec(window.innerHeight); }, DELAY_TIME); }); } let isInited = false; function initKeyboardHeightChangeListener() { // #ifdef APP-PLUS uni.onKeyboardHeightChange((res) => { emit(callbackList, { extra: 0, height: res.height }); }); // #endif // #ifdef H5 windowHeight = Vue.prototype.windowHeight; onKeyboardHeightChangeWithH5(); // #endif } /** * @callback OnKeyboardHeightChangeCallback * @param {{ extra: number; height: number }} 键盘高度(ios 中还有底部块的高度) */ /** * @typedef Options * @type {Object} * @property {boolean} reset app 端对 uni.onKeyboardHeightChange 重置侦听 */ /** * * @description 侦听键盘高度变化事件, 对多端做兼容处理 * @param {OnKeyboardHeightChangeCallback} callback 键盘高度变化事件回调 * @param {Options} options 额外参数 */ function onKeyboardHeightChange(callback, options = {}) { const { isPc } = Vue.prototype; if (isPc) { return console.error( "PC devices do not need to listen for soft keyboard height" ); } // #ifdef APP-PLUS if (options.reset) { isInited = false; callbackList = []; } // #endif if (typeof callback === "function") { callbackList.push(callback); } if (isInited === false) { isInited = true; initKeyboardHeightChangeListener(); } } /** * * @description 移除事件侦听函数 * @param {OnKeyboardHeightChangeCallback} callback 需要移除的已注册函数 */ function offKeyboardHeightChange(callback) { if (typeof callback === "function") { const index = callbackList.indexOf(callback); if (index >= 0) callbackList.splice(index, 1); } } /** * * @description H5 页面高度变化但未执行键盘高度变化事件时触发 * @param {OnKeyboardHeightChangeCallback} callback 需要添加的注册函数 */ function onUnimplementedChange(callback) { const { isPc } = Vue.prototype; if (isPc) { return console.error( "PC devices do not need to listen for soft keyboard height" ); } if (typeof callback === "function") { unimplementedChangeList.push(callback); } if (isInited === false) { isInited = true; initKeyboardHeightChangeListener(); } } /** * * @description H5 页面高度变化但未执行键盘高度变化事件时触发 * @param {OnKeyboardHeightChangeCallback} callback 需要添加的注册函数 */ function offUnimplementedChange(callback) { if (typeof callback === "function") { const index = unimplementedChangeList.indexOf(callback); if (index >= 0) unimplementedChangeList.splice(index, 1); } } export default { onKeyboardHeightChange, offKeyboardHeightChange, onUnimplementedChange, offUnimplementedChange, };
上述代码对于软键盘高度的获取做了统一的封装,对于 App 端使用 uni.onKeyboardHeightChange
接口;H5 端利用 Window.visualViewport
,同时使用 window.onresize
兜底。
用过 uniapp 的人应该都对全局这个词很敏感,因为很实用但却无法实现(除 H5 外)。接触这个项目后我也感到了局部组件带来的一些问题:
这些问题看起来很小,但对项目的影响巨大,所以我也在找能够实现全局弹窗的办法。
看过插件市场的一些全局弹窗实现,基本思路都是使用一个页面作为全局弹窗的载体,需要弹出时跳转至这个页面即可。
App 端中一个页面就是一个 webview,其实我们可以通过 H5+ Api 的方式创建 webview 来作为全局弹窗的载体,会比页面的形式更好一点,查看 uni-app-picker 组件的源码也可以发现,App 端的 picker
就是依赖于 H5+ webview 接口实现的,且经过实验,创建的 webview 是可以做到全局重用的。
我们还需要考虑这个全局弹窗的可扩展、可重用性:
这个项目是一个比较老的项目了,给我的感觉就是乱,很乱,没有一个统一的规范,你想写什么样的代码都可以,导致经过多个开发人员的手之后,各种风格的代码夹杂其中,给后续的开发维护造成很大的影响。
项目的初期没有规划好,在开发过程中也没有考虑封装统一,比如:
window.onresize
之类的接口完成功能,可能一个组件就添加一个事件,应该封装为模块,内部注册一次事件,提供对外接口,事件触发时再分发出去一开始接手这个项目时就感觉项目问题很大,天天喊着需要重构重构,需要几个月的时间对项目进行大的重构,后来需求要将项目中使用 scroll-view
的列表替换为三方组件,在完成这个需求时就发现了所谓大的重构是不切实际的。
在进行这个需求时出现的比较明显的问题:
scroll-view
的属性、逻辑不兼容,替换需要格外小心,耗时是很漫长的从这个需求中也更能体会到 《重构2》 中作者所践行的 一步一测试 的必要性了,也更深刻的理解了作者表达的:当你有时间且你认为这段代码需要重构时才进行重构,对于那些永远不会碰到的,不要去碰它(让营地比你来时更干净)
一个星期前是试用期结束,告知我试用期没有通过,理由是进度缓慢,我也接受了,这里不是为了吐槽,只是谈谈对于项目进度的看法。
上面也说了,这是一个 C 端的老项目,且存在着比较大的问题,对于我而言,我会让营地比我来时更干净,这是为了后续的项目维护考虑,同时尽量保证项目的逻辑在我的可接受范围内,毕竟连自己都不能接受的程序怎么保证用户的体验呢?
–end
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。