在参与的电商项目中,遇到的一些奇葩兼容问题,记录对应的解决方案。
运行小程序 props 传值,对象方法丢失
自定义组件父子组件 prop 传递的变量为对象时,对象内部含有函数属性,该函数属性会直接被删除。
因为为 uniapp 在传递数据的时候使用的是JSON.parse(JSON.stringify(obj1))
这样传递的,无法传递函数。
了解更多,详见
image 组件@load、@error 事件名不能是 onLoad、onError
如果是 onLoad、onError,编译会报错。
小程序无法通过 document 获取 DOM 节点信息
如果有获取元素距离顶部的间距这样一个场景,小程序无法通过 document 来获取相关信息,需要用到uni.createSelectorQuery()
。用法以下几点需要注意:
- 使用 uni.createSelectorQuery() 需要指定的 DOM 节点已渲染完成后。
- 支付宝小程序不支持 in(component),使用无效果
提取公共的方法,兼容实现方案以下几种:
mixin:
// common.mixin.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| getElemRect(selector, all) { return new Promise(resolve => { let query = uni.createSelectorQuery() .in(this); query[all ? 'selectAll' : 'select'](selector) .boundingClientRect(rect => { if (all & Array.isArray(rect) && rect.length) { resolve(rect); } if(!all && rect) { resolve(rect); } }) .exec(); }) }
|
utils:
// utils.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| export function getElemRect(selector, all) { return new Promise((resolve) => { let query = uni .createSelectorQuery() .in(this); query[all ? "selectAll" : "select"](selector) .boundingClientRect((rect) => { if (all & Array.isArray(rect) && rect.length) { resolve(rect); } if (!all && rect) { resolve(rect); } }) .exec(); }); }
|
// component.vue
1 2 3 4 5 6 7 8 9
| <script> import { getElemRect } from "utils.js";
export default { methods: { getElemRect, }, }; </script>
|
页面路由跳转
使用 uni.navigateTo(OBJECT)时,url 可以是相对路径,也可以是绝对路径,注意文件目录层级关系。
swiper 组件@change 从第 2 个开始执行的
不推荐在 change 事件中触发业务场景(如轮播卡片曝光事件),会漏掉第一个。
小程序中子组件 props 值来自父组件 computed 定义的值控制台报 warn 场景
如果父组件 computed 定义的值不具备动态绑定更新特征,而是自变量赋值,子组件 props 引用,小程序控制台可能就会打印警告日志。
具体代码分析
// 父组件
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
| <template> <view class="Demo"> <image class="img" :src="imgUrl" mode="scaleToFill" @load="onLoadImg" @error="onErrorImg" ></image> <Card :margins="margins" /> </view> </template>
<script> import Card from "./Card.vue";
export default { components: { Card }, name: "Demo", data() { return { imgUrl: "", }; }, computed: { margins() { let result = { top: 0, right: 0, bottom: 0, left: 0, }; if (this.imgUrl) { result.bottom = -50; } return result; }, }, methods: { onErrorImg() { this.imgUrl = "https://img.zcool.cn/community/015153563ec9e66ac7259e0fc2d030.jpg@3000w_1l_0o_100sh.jpg"; }, }, }; </script>
<style lang="scss" scoped> .img { display: block; width: 100%; height: 200rpx; } </style>
|
// 子组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <template> <view class="Card"> <view>{{ margins }}</view> </view> </template>
<script> export default { name: "Card", props: { margins: { type: Object, default: () => ({}), }, }, }; </script>
|
微信小程序控制台日志:
1
| [Component] property "margins" of "pagesA/demo/Card" received type-uncompatible value: expected <Object> but got non-object value. Used null instead.
|
以下方案可以解决
子组件 props 类型兼容(不推荐,可能会存在类型漏掉的情况)
1 2 3 4 5 6 7 8 9 10 11
| <script> export default { name: "Card", props: { margins: { type: [Object, null], default: () => ({}), }, }, }; </script>
|
父组件 computed 中不具备动态绑定更新特征的值,换成 data
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
| <template> <view class="Demo"> <image class="img" :src="imgUrl" mode="scaleToFill" @load="onLoadImg" @error="onErrorImg" ></image> <Card :margins="margins" /> </view> </template>
<script> import Card from "./Card.vue";
export default { components: { Card }, name: "Demo", data() { return { imgUrl: "", margins: {}, }; }, methods: { onErrorImg() { this.imgUrl = "https://img.zcool.cn/community/015153563ec9e66ac7259e0fc2d030.jpg@3000w_1l_0o_100sh.jpg"; }, getMargins() { let result = { top: 0, right: 0, bottom: 0, left: 0, }; if (this.imgUrl) { result.bottom = -50; } this.margins = result; }, }, }; </script>
<style lang="scss" scoped> .img { display: block; width: 100%; height: 200rpx; } </style>
|
非页面组件直接用页面生命周期钩子函数无效
非页面组件不具备页面组件的生命周期钩子函数,但是可以同组件通信事件绑定hook:页面生命周期钩子函数名
实现
// 子组件
1 2 3 4 5 6 7 8 9 10
| <script> export default { name: "Card", mounted() { this.$root.$emit("hook:onHide", () => { // 执行业务场景函数 }); }, }; </script>
|
打印 this,控制台 this 日志中,vue2 当前是支持 hook 调用页面生命周期钩子函数的。
点击 icon 图标,让 input 组件聚焦,只需要点击图标时,改变 input 组件框的 focus 属性值。
但是,发现一个问题:这个方法只能触发一次。是因为当点击图标以后,focus 的值已经变成 true 了,所以当我们再次点击图标的时候,就不会出现效果了。
解决办法:获取软键盘的位置,如果软键盘的位置变为 0 ,就让 focus 的值变为 false,成功解决问题。
下面是具体的代码:
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
| <template> <view class="y-input u-flex"> <input class="input" type="text" :focus="focus" @blur="onInputBlur" /> <u-icon name="edit-pen-fill" color="#2979ff" size="60" @click="onEdit" ></u-icon> </view> </template>
<script> export default { name: "y-input", data() { return { focus: false, }; }, methods: { onEdit() { this.focus = true; uni.onKeyboardHeightChange((res) => { if (res.height === 0) this.focus = false; }); }, onInputBlur() { this.focus = false; }, }, }; </script>
<style lang="scss" scoped> .input { height: 60rpx; border: 4rpx solid #e5e5e5; border-radius: 16rpx; } </style>
|
uniapp debugger H5
JavaScript 或 typescript 脚本中单行注入debugger关键词。本地运行,如yarn serve
。
谷歌浏览器访问控制台打印出的域名,打开调试工具,会触发谷歌自带的 debugger 调试功能。
如果遇到不能触发 debugger 调试,需要检查框架忽略列表项,取消或自定义一些规则来触发 debugger 调试。
微信小程序消息订阅点击没有出现弹框
用点击事件 click,tap 生效。touch 不生效。
相关链接:
小程序订阅消息(用户通过弹窗订阅)开发指南
图片渲染出现拉伸到正常过渡,体验不好
image 标签 mode 用了 widthFix 或 heightFix 导致,需要加 css:
1 2 3 4 5 6 7 8
| .img { height: auto; }
.img { width: auto; }
|
heightFix: App 和 H5 平台 HBuilderX 2.9.3+ 支持、微信小程序需要基础库 2.10.3,支付宝小程序不支持。
list 类型的组件封装时遇到 v-for 遍历插槽,微信小程序没生效
uniapp 小程序插槽不支持遍历,因为插槽的具名需要唯一值,且小程序不支持插槽具名是变量值。
微信小程序打包体积过大
小程序分包异步化
相关链接:
微信小程序第三方库的分包异步化实践
自定义 tabbar 微信小程序正常,支付宝原生不隐藏且上边框线去不掉
使用 uview-ui 的 Tabbar 底部导航栏组件。如果不是 tab 页面,不执行 uni.switchTab。需要更改组件代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| function switchTab(index) { if (this.list[index].pagePath === this.pageUrl) return; this.$emit("change", index); if (this.list[index].pagePath) { if (this.list[index].pageType !== "tab") return; uni.switchTab({ url: this.list[index].pagePath, }); } else { this.$emit("input", index); } }
|
支付宝小程序有基础库版本限制条件,可参考官方解决方案:自定义 tabbar。
相关链接:
uniapp 自定义 tabbar 微信小程序正常,支付宝原生不隐藏
瀑布流实现兼容 h5 和小程序
相关链接:
[1] z-paging
[2] uview waterfall
瀑布流加入猜你喜欢卡片
实现思路:
- 猜你喜欢卡片作为被点击 item 的属性值扩展
- 列分配 item 处理时,增加坐标、列数组长度属性值
代码如下:
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 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
| <template> <view class="c-waterfall-shell"> <view :id="selectId(0)" class="column left"> <slot name="left" :leftList="colList[0]" :extra="extra"></slot> </view> <view :id="selectId(1)" class="column right"> <slot name="right" :rightList="colList[1]" :extra="extra"></slot> </view> </view> </template>
<script> import selectorQueryMixin from "@/utils/mixin/selectorQuery.mixin";
const column = 2; // 列数
/** * cWaterfallShell 瀑布流 * @property {Boolean} isOnHide 页面隐藏 * @property {Number} addTime 单位ms。每次向结构插入数据的时间间隔,间隔越长,越能保证两列高度相近,但是对用户体验越不好 * @property {Array} list 待渲染的数据 * @property {Object} extra 额外的props * @event {Function} renderFinished 渲染完成 */ export default { name: "cWaterfallShell", emits: ["renderFinished"], mixins: [selectorQueryMixin], props: { isOnHide: { type: Boolean, default: false, }, addTime: { type: Number, default: 100, }, list: { type: Array, default: () => [], }, extra: { type: Object, default: () => ({}), }, }, data() { return { restNum: 0, // 剩余量 id: this.$u.guid(), colHeight: [], // 列高 colList: [], // 瀑布流 currentListIndex: 0, // 加入下一个瀑布流时当前的list索引 nextColIndex: 0, // 下一个加入瀑布流列的索引 }; }, computed: { colKey() { return (i) => `${i}.${this.waterfallKey}`; }, selectId() { return (column) => { return `${this.id}-${column}`; }; }, }, watch: { list: { handler: function (nVal, oVal) { if (nVal.length > 0 && nVal.length > oVal.length) { this.splitData(); } }, deep: true, }, }, mounted() { this.touchOff(); }, methods: { // 清除数据 clear() { this.colList = new Array(column).fill(1).map(() => []); this.colHeight = new Array(column).fill(0); this.currentListIndex = 0; this.nextColIndex = 0; }, // 触发重新排列 touchOff() { // 初始化 if (this.colHeight.length === 0) { this.clear(); } if (this.list.length > 0) { this.splitData(); } }, // 一个个往列表追加 append() { const item = this.list[this.currentListIndex++]; // 坐标 item.axis = [ this.nextColIndex, this.colList[this.nextColIndex].length, ]; let colSize = this.colList.map((item) => item.length); colSize[this.nextColIndex] += 1; // 列数组长度 item.colSize = colSize; this.colList[this.nextColIndex].push(item); }, // 拼接上原有数据 async splitData() { for (let i = 0; i < column; i++) { const rect = await this.getRect("#" + this.selectId(i)); this.colHeight[i] = rect.height || 0; } if (this.isOnHide) { // console.log('>>> isOnHide', this.colHeight, this.colList, this.list); return; } const minH = Math.min(...this.colHeight); this.nextColIndex = this.colHeight.indexOf(minH); if (this.nextColIndex < 0) { this.nextColIndex = 0; } if (this.list.length - this.restNum === this.currentListIndex) { // 防止list为[]时,重复执行渲染完成事件 if (this.list.length > 0) this.$emit("renderFinished", true); return; } this.append(); this.$nextTick(() => { setTimeout(() => { this.splitData(); }, this.addTime); }); }, // 加入猜你喜欢 spliceList({ currentIndex, axis, guessList, restNum }) { this.restNum = restNum; this.colList[axis[0]][axis[1]].guessList = guessList; this.currentListIndex = currentIndex + 2; this.list[currentIndex + 1].colSize?.forEach((elem, index) => { this.colList[index].splice(elem); }); this.loadNextVirtualPage(); }, // 加载下一个模拟分页数据 loadNextVirtualPage(pageSize = 20) { if (this.restNum > pageSize) { this.restNum -= pageSize; this.$nextTick(() => { this.splitData(); }); } else if (this.restNum > 0) { this.restNum = 0; this.$nextTick(() => { this.splitData(); }); } }, }, }; </script>
<style lang="scss" scoped> .c-waterfall-shell { display: flex; flex-direction: row; align-items: flex-start; justify-content: space-between; width: 100%; padding: 0 24rpx; } .column { display: flex; flex-direction: column; width: 50%; height: auto; } .left { padding-right: 8rpx; } .right { padding-left: 8rpx; } </style>
|
弹出框展示时如何阻止页面可滑动
点击的标签上加入@touchmove.stop.prevent
预防 xss 攻击安全优化,主要涉及 h5
使用外部 cdn,通过 js 引入
1 2 3 4 5 6 7 8 9 10 11 12 13
| export function loadScriptBy(cdn) { return new Promise((resolve, reject) => { const script = document.createElement("script"); const t = Date.now().toString().slice(0, -4); cdn = cdn.indexOf("?") > -1 ? `${cdn}&t=${t}` : `${cdn}?t=${t}`; script.type = "text/javascript"; script.src = cdn; script.onload = resolve; script.onerror = reject; document.head.appendChild(script); }); }
|
app 端内打开 h5 时,隐藏 h5 标题
使用空字符占位符实现
1
| <title><%= '\u200B' %></title>
|
ios 手机上,长按图片出现默认拖拽
uniapp 的版本有关,需要更改 uniapp 封装的组件 image props 值。通过打补丁的方式patch-package
。
1 2 3 4 5 6 7 8 9 10 11
| // node_modules/@dcloudio/uni-h5/src/core/view/components/image/index.vue <script> export { props: { draggable: { type: Boolean, default: false // 默认时true } } } </script>
|
vue3 支持的手机版本最低到多少
vue3 支持的范围是:Android > 4.4(具体因系统 webview 版本而异,原生安卓系统升级过系统 webview 一般 5.0 即可,国产安卓系统未使用 x5 内核时一般需 7.0 以上), ios >= 10
Android < 4.4,配置 X5 内核支持,首次需要联网下载,可以配置下载 X5 内核成功后启动应用。详见
swiper 组件自动轮播时 onChange 事件触发问题
- 首次展示不触发
- h5 中,非可见区域,也会触发 onChange
设置页面背景色
page 相当于 body 节点,设置页面背景颜色,使用 scoped 会导致失效。详见
1 2 3 4 5 6
| <style lang="scss">
page { background-color: #ff4b14; } </style>
|
当然,设置页面高度 100vh,再定义样式背景色,也可以实现,结合实际业务场景选择。
物流信息电话高亮,点击电话拉弹框
需用到 uview-ui u-parse
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
| <template> <u-parse :html="selectPhoneNumber(info)" @linkpress="handlePhone" /> </template>
<script> export { data() { return { currentPhone: '' } } methods: { selectPhoneNumber(str) { const regx = /([\d]{11}|[\d]{3,4}-[\d]{7,8})/g; const result = str.split(regx).map((el) => { let temp = el; if (regx.test(el)) { temp = `<a style="color:#FE4854;text-decoration:none;" href="${el}">${el}</a>`; } return temp; }); return result.join(""); }, handlePhone(e) { e.ignore(); this.currentPhone = e.href; } } } </script>
|
微信小程序投放H5进行的联登操作,需要返回小程序页面怎么实现
需要用navigateBack回退到小程序中。
小程序投放的h5链接是通过web-view实现的。web-view网页中可使用JSSDK 1.3.2提供的接口返回小程序页面
1
| wx.miniProgram.navigateBack()
|
相关链接:
weixin miniprogram web-view