uniapp跨端开发踩坑

在参与的电商项目中,遇到的一些奇葩兼容问题,记录对应的解决方案。

运行小程序 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()
// #ifndef MP-ALIPAY
.in(this);
// #endif
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()
// #ifndef MP-ALIPAY
.in(this);
// #endif
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 组件聚焦

点击 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 调试。

image-20240703224606640

微信小程序消息订阅点击没有出现弹框

用点击事件 click,tap 生效。touch 不生效。

相关链接:

小程序订阅消息(用户通过弹窗订阅)开发指南

图片渲染出现拉伸到正常过渡,体验不好

image 标签 mode 用了 widthFix 或 heightFix 导致,需要加 css:

1
2
3
4
5
6
7
8
/* 如果是 widthFix */
.img {
height: auto;
}
/* 如果是 heightFix */
.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
// 切换tab
function switchTab(index) {
// 防止同一个页面tab重复触发点击
if (this.list[index].pagePath === this.pageUrl) return;
// 发出事件和修改v-model绑定的值
this.$emit("change", index);
// 如果有配置pagePath属性,使用uni.switchTab进行跳转
if (this.list[index].pagePath) {
// 如果不是tab页面,不执行uni.switchTab
if (this.list[index].pageType !== "tab") return;
uni.switchTab({
url: this.list[index].pagePath,
});
} else {
// 如果配置了papgePath属性,将不会双向绑定v-model传入的value值
// 因为这个模式下,不再需要v-model绑定的value值了,而是通过getCurrentPages()适配
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
// 加载 script 标签脚本资源
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">
/* good */
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