uni-app 踩坑小记

写在前面

这里记录一些使用 uni-app 和 uView 开发时踩过的坑,希望能对后来者有所帮助。

1. 微信开发者工具关闭 ES6 转 ES5

如果微信开发者工具开启了 ES6 转 ES5 的功能,会导致引入的 uView 组件无法正常使用,会报类似于下面这样的错:

解决方案:把微信开发者工具的 ES6 转 ES5 功能关掉,设置 -> 项目设置 -> 本地设置。

2. ref 不要放在 computed

下面这份代码,在小程序端运行异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<view class="container">
<button ref="button">button</button>
</view>
</template>

<script>
export default {
computed: {
buttonRef() {
return this.$refs.button
}
},

mounted() {
this.buttonRef.someMethods() // 在 mp-weixin 会报:buttonRef 为空
}
}
</script>

解决方案:不要把 refs 的定义放在 computed,每次使用都直接通过 this.$refs.button 获取 。

3. 小程序端的组件样式问题

有可能你在 h5 编写好一个组件的样式,运行好好的,结果在小程序端却不正常了,原因在于小程序端会在组件外额外增加一层视图容器,这可能会导致诸如一些高度、外边距之类的样式失效,解决方案就是修改这个外层的样式,使它与我们这个组件的最外层样式一致:

1
2
3
4
5
6
7
8
9
10
11
12
<style lang="scss">
// #ifdef MP-WEIXIN
// HACk: 微信小程序端组件外面会多一层,需要给它也设置高度
[data-ref='basicLayout'], // 这里的值在微信开发者工具查看
// #endif
.basic-layout,
.page-content {
flex: 1;
display: flex;
flex-direction: column;
}
</style>

注意:由于这个选择器只针对小程序端,所以我们要加一个条件渲染。

4. 小程序端修改的外部组件样式无效的问题

问题描述

如果要在 .vue 中修改外部组件的样式(如 uni-app 和 uView),像下面这样可能会无效:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<view class="container">
<u-search v-model="keyword" placeholder="日照香炉生紫烟"></u-search>
</view>
</template>

<script>
export default {
data() {
return {
keyword: ''
}
}
}
</script>

<style lang="scss">
.container {
.u-search .u-input {
color: red !important;
}
}
</style>

运行结果:


解决方案

这是因为在 uni-app <style>   默认都是 scope 的,所以需要改成下面这样:

1
2
3
4
5
6
7
8
<style lang="scss">
.container {
- .u-search .u-input {
+ /deep/ .u-search .u-input {
color: red !important;
}
}
</style>

这样 H5 也能运行正常了:

小程序端修改 shadow-root 内部样式

但是还有一种情况,就是如果你要修改的组件位于 #shadow-root 下,就有可能无法修改,比如下面这样:

1
2
3
4
5
.nav-bar {
/deep/ .uni-navbar__content {
overflow: unset !important;
}
}

其实根本没有修改到:

解决方案

这种情况,我们必须在 App.vue 下的 <style> 才能进行样式覆盖,但为了方便维护,我们新增了一个 uni-custom-app.scss 专门做这件事,用于解决上面这种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 文件路径:src/styles/uni-custom.scss

/**
* 这个文件用于修改 uni 或者其他 ui 组件库的样式
* 由于微信小程序的限制,无法在组件内部修改其他外部组件的样式,只能在全局设置
* @link https://ask.dcloud.net.cn/question/68411
*
* 注意:如果不是全局修改,为避免影响其它,需要指定在外层页面和组件
*/

// 全局修改
// .u-btn {
// color: red;
// }

// 局部修改
// .page-xxx {
// .container {
// .u-btn {
// color: red;
// }
// }
// }

也就是说,我们只要把上面的样式移动到这个文件即可:

1
2
3
4
5
6
7
8
9
10
11
// 文件路径:src/styles/uni-custom.scss

...

+.page-message {
+ .nav-bar {
+ /deep/ .uni-navbar__content {
+ overflow: unset !important;
+ }
+ }
+}

可以看到是有样式的:

但是你会看到我们写的样式权重似乎不够高,但其实样式已经被应用上去了,所以不用在意这里,至于为什么会这样,可能会微信开发者工具的问题吧?

注意:如果此样式只针对某个页面下的组件,建议在外面包一层该页面的唯一 class 名字,以防影响其它页面。

参考链接:https://ask.dcloud.net.cn/question/68411

如果是自定义 uButton 样式无效,可以看看:[18. 自定义 uButton 样式](

5. 在 HBuilder 运行项目

由于我们是使用 cli 的方式生成的项目,所以基本开发只需要在命令行运行 npm run dev:xxx 即可,但是如果要调试 APP 端,还是使用 HBuilder 要更方便,然而如果在 在 HBuilder 运行 cli 项目 你可能会遇到以下错误:

1
Node Sass could not find a binding for your current environment: OS X 64-bit with Node.js 8.x

说白就是 node-sass 出了问题了,HBuilder 默认使用的 Node 版本是 v8.x,所以我们要修改一下配置,让它使用我们本机的 Node 版本。

在 偏好设置 -> 运行配置 中修改 npm 和 Node 的路径即可:

如果你不知道 Node 的路径在哪,可以这样:

  • Linux/Mac: where node
  • Windows:直接看系统环境变量

参考:https://ask.dcloud.net.cn/question/67852

6. 动态插槽名

前言

要在 uni-app 中使用动态 slot 名字,会比较麻烦,因为:在 MP-WEIXIN、APP-PLUS 都会有坑。

H5 和 小程序端

我们先说比较常用的 H5 和 MP-WEIXIN 好了:

定义:

1
2
3
4
5
6
7
<!-- HACK: uni-app 处理动态 slot 名字不兼容,需要使用不同的语法 -->
<!-- #ifdef H5 -->
<slot :name="`tab:${item.key}`"></slot>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN-->
<slot name="tab:{{item.key}}"></slot>
<!-- #endif -->

使用 slot:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<view>
<!-- HACK: uni-app 处理动态 slot 名字不兼容,需要使用不同的语法 -->
<!-- #ifdef H5 -->
<template v-for="item in list" :slot="`tab:${item.id}`">
<post-list :key="item.id" />
</template>
<!-- #endif -->

<!-- #ifdef MP-WEIXIN-->
<template v-for="item in lits" slot="tab:professional:{{item.id}}">
<post-list :key="item.id" />
</template>
<!-- #endif -->
</view>

参考链接:https://ask.dcloud.net.cn/question/82506

APP 端

如果还要兼容 APP 端(vue 文件),则情况会变得稍微复杂一点,以上两种情况都不适用,先说结论:

  1. 不支持拿 data 的数据用于拼接动态 slot 名字
  2. 在 v-for 中要根据当前项的字段来拼接 slot 名字,则要将 key 指向 item  本身(不推荐
  3. 能拿 v-for 的 index 来拼接 slot 名字(推荐

解决方案

也即,如果是上面的例子,需要改写为如下::

1
2
3
4
5
6
7
8
9
<swiper-item v-for="(item, index) in tabs" :key="item.id" class="swiper-item">
<!-- HACK: uni-app 处理动态 slot 名字不兼容,需要使用不同的语法 -->
<!-- #ifdef H5 || APP-PLUS -->
<slot :name="`tab:${index}`"></slot>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN-->
<slot name="tab:{{index}}"></slot>
<!-- #endif -->
</swiper-item>

使用的时候:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<tab-swiper
ref="tabSwiper"
:tabs="list"
:current.sync="current"
:swiper-current.sync="swiperCurrent"
>
<!-- HACK: uni-app 处理动态 slot 名字不兼容,需要使用不同的语法 -->
<!-- #ifndef H5 || APP-PLUS -->
<template v-for="(item, index) in list" :slot="`tab:${index}`">
<post-list :key="item.id" :stagger="index % 2 !== 0" />
</template>
<!-- #endif -->

<!-- #ifdef MP-WEIXIN-->
<template v-for="(item, index) in list" slot="tab:{{item.id}}">
<post-list :key="item.id" :stagger="index % 2 !== 0" />
</template>
<!-- #endif -->
</tab-swiper>

排查问题

下面开始排查问题,首先我们用以下代码测试用 data 的数据来作为 slot 名字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<view>
testing dynamic slot
<slot :name="key"></slot>
<view :class="key"> test key value </view>
</view>
</template>

<script>
export default {
data() {
return {
key: 'slot-1'
}
}
}
</script>

然后使用命令行或者 HBuilderx 编译 APP 端代码后,我们在 dist/dev/app-plus/app-view.js

搜索 testing dynamic slot ,然后可以看到以下代码:


可以看到,即便同样使用了 key 作为属性,但它们编译后的代码是不一样的,slot 节点直接使用 _vm._key ,而 view 节点变成了 _vm._$(2,'c') ,由此也推断出 uni-app 内部并没有对 slot 的 name 属性做额外处理,其实如果打印 _vm.key 的值, 会发现是空的:

所以结论一:不支持拿 data 的数据用于拼接动态 slot 名字。

但直接拿 data 数据来拼接 slot 名字的情况比较少,更多时候是在 v-for 循环内部,所以我们再拿以下代码做测试:

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
<template>
<view>
testing dynamic slot
<view v-for="item in list" :key="item.id">
{{ item.name }}
<slot :name="`tab:${item.id}`"></slot>
</view>
</view>
</template>

<script>
export default {
data() {
return {
list: [
{
id: 'a',
name: 'item-a'
},
{
id: 'b',
name: 'item-b'
},
{
id: 'c',
name: 'item-c'
}
]
}
}
}
</script>

以上代码在 APP 端依然是不正常的,我们看看编译后的代码:

代码看上去好像挺正常的是吧,但有一点很奇怪:为什么 key 直接指向了 item

果不其然,打印 item 发现这里的 item 并不是 v-for 中的那个 item 对象 ,而是用于指定 key 的值:

所以 item.id 依然是空的,实际上这里的 item === 外部的 item.id

既然如此,我们似乎得到两个解决方案:

  1. 将 v-for 的 key 直接指向 item 本身,使 item.id 能正常访问
  2. 拼接 slot 名字时不使用 item.id  而是使用 item

结论是,方案一可行;方案二不可行。

至于为什么方案二不可行,我认为是 uni-app 的问题,因为打印出来的值是正确的。

但根据 Vue 官方文档指出:

不要使用对象或数组之类的非基本类型值作为 v-for 的 key。请用字符串或数值类型的值。

结论二:在 v-for 中要根据当前项的字段来拼接 slot 名字,则要将 key 指向 item   本身。

最后一个解决方案,那就是通过 index 拼接 slot 名字:

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
<template>
<view>
testing dynamic slot
<view v-for="(item, index) in list" :key="item.id">
{{ item.name }}
<slot :name="`tab:${index}`"></slot>
</view>
</view>
</template>

<script>
export default {
data() {
return {
list: [
{
id: 'a',
name: 'item-a'
},
{
id: 'b',
name: 'item-b'
},
{
id: 'c',
name: 'item-c'
}
]
}
}
}
</script>

以上代码能在 APP 端正常运行。

结论三:拿 v-for 的 index 来拼接 slot 名字。

但毕竟是 HACK,终究原因是 uni-app 目前仍没有在 APP 端支持定义动态 slot ,可见相关的讨论:

所以 uni-app 官方什么时候支持动态 slot 名字呢?

7. 小程序端 v-for 中使用 v-if 问题

问题描述

这个问题需要同时满足以下条件才会触发:

  1. 在 v-for 内部使用了 v-if
  2. v-if 的断言依赖了 methods 或者 闭包 computed(就是需要根据入参动态返回结果
  3. 在 v-for 内部直接使用了小程序端的语法

排查过程

复现代码如下:

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
// tab-swiper.vue
<template>
<view class="list">
<view v-for="(item, index) in list" :key="item.id" class="item">
<!-- HACK: uni-app 处理动态 slot 名字不兼容,需要使用不同的语法 -->
<!-- #ifdef H5 -->
<slot :name="`tab:${item.id}`"></slot>
<!-- #endif -->
<!-- #ifndef H5-->
<!-- @see https://ask.dcloud.net.cn/question/82506 -->
<slot name="tab:{{item.id}}"></slot>
<!-- #endif -->
</view>
</view>
</template>

<script>
export default {
data() {
return {
list: [
{
id: 'recommend',
name: '推荐'
},
{
id: 'public',
name: '公共知识'
},
{
id: 'professional',
name: '专业项知识'
}
]
}
}
}
</script>

index.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<tab-swiper>
<view slot="tab:recommend">recommend</view>
<view slot="tab:public">public</view>
<view slot="tab:professional">professional</view>
</tab-swiper>
</template>

<script>
import TabSwiper from '@/components/tab-swiper'
export default {
components: {
TabSwiper
}
}
</script>

上面代码一切运行正常,slot 也能正常显示:


然而,只要我把代码改成这样:

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
<template>
<view class="list">
<view v-for="(item, index) in list" :key="item.id" class="item">
+ <block v-if="isShow(index)">
+ <view>显示我</view>
+ </block>
<!-- HACK: uni-app 处理动态 slot 名字不兼容,需要使用不同的语法 -->
<!-- #ifdef H5 -->
<slot :name="`tab:${item.id}`"></slot>
<!-- #endif -->
<!-- #ifndef H5-->
<!-- @see https://ask.dcloud.net.cn/question/82506 -->
<slot name="tab:{{item.id}}"></slot>
<!-- #endif -->
</view>
</view>
</template>

<script>
export default {
data() {
return {
list: [
{
id: 'recommend',
name: '推荐',
},
{
id: 'public',
name: '公共知识',
},
{
id: 'professional',
name: '专业项知识',
},
],
}
},
+ methods: {
+ isShow(index) {
+ return index % 2 === 0
+ },
+ },
}
</script>

结果立马变成这样了:

那三个 slot 哪去了?别急,让我们对比一下两者生成的微信小程序代码:

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
<!-- 这是正常的 -->
<view class="list">
<block wx:for="{{list}}" wx:for-item="item" wx:for-index="index" wx:key="id">
<view class="item">
<slot name="tab:{{item.id}}"></slot>
</view>
</block>
</view>

<!-- 这是异常的 -->
<view class="list">
<block
wx:for="{{$root.l0}}"
wx:for-item="item"
wx:for-index="index"
wx:key="id"
>
<view class="item">
<block wx:if="{{item.m0}}">
<block>
<view>显示我</view>
</block>
</block>
<slot name="tab:{{item.id}}"></slot>
</view>
</block>
</view>

下面开始「找不同」,我们发现最大的不同是: wx:for 的值不一样,于是我们看下 $root.l0 的定义:

我们发现,在 $root.l0 下的每一个 item 项都多了一层 $orig 放置原本的内容,所以我们下面这行代码并不能访问到它的 id 值:

1
<slot name="tab:{{item.id}}"></slot>

我们试着把这个值输出来看看:

1
2
+ 当前:{{ item.id }}
<slot name="tab:{{item.id}}"></slot>

你会发现居然能显示出来:

再看看生成的微信小程序代码,终于知道其中的奥妙:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<view class="list">
<block
wx:for="{{$root.l0}}"
wx:for-item="item"
wx:for-index="index"
wx:key="id"
>
<view class="item">
<block wx:if="{{item.m0}}">
<block>
<view>显示我</view>
</block> </block
>{{'当前:'+item.$orig.id+''}}<slot name="tab:{{item.id}}"></slot>
</view>
</block>
</view>

解决方案

原来是 uni-app 自动帮我们添加了 $orig ,但由于 slot 那一行我们直接用了微信小程序的语法,所以并没有帮我们添加 $orig ,那么解决方案就是自己手动加上呗:

1
2
- <slot name="tab:{{item.id}}"></slot>
+ <slot name="tab:{{item.$orig.id}}"></slot>

问题是解决了,那么为什么会这样呢?应该是 uni-app 的转换机制,发现 v-for 和 v-if 存在一定关联,就把它们给缓存了,才会出现这种问题。

核心要点:当微信小程序端异常时,不妨看看生成的代码,说不定有收获哦。

8. 小程序端禁止事件冒泡

问题描述

在小程序端无法动态阻止事件冒泡,也即,在 js 中使用如下无效:

1
2
3
4
handleClick(e) {
e.stopPropagation() // 除了在 h5,其它端均无效
e.preventDefault() // 同上
},

只能使用修饰符:

1
<u-button @click.native.stop="doSomething"></u-button>

注意:需要加 native ,否则可能无法无效。

参考链接:https://github.com/dcloudio/uni-app/issues/1067

关于 uni-app 中的修饰符兼容情况:事件修饰符 —— uni-app

很显然, 使用事件修饰符的缺点是无法动态决定是否阻止事件冒泡,如果有这种需求,提供个 hack 思路:

  • 在外面包一层,利用 css 的 pointer-events 属性决定是否触发内层的 dom 事件

另外还有一个坑,假设有如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<view class="container" @click="show = false">
<u-icon name="plus-circle-fill" @click.native.stop="show = !show" />
<view v-show="show" class="message">这是一条信息</view>
</view>
</template>

<script>
export default {
data() {
return {
show: false
}
}
}
</script>

上述代码想要实现当点击 icon 时切换显示 message,而点击 icon 以外的任意地方则隐藏 message。

在 H5 运行正常,但是在小程序端却会报如下错误:

TypeError: Cannot read property ‘stopPropagation’ of undefined

这是因为我们直接在 @click 的事件绑定中写了代码,而如果改成绑定 methods 则没有该问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<view class="container" @click="show = false">
- <u-icon name="plus-circle-fill" @click.native.stop="show = !show" />
+ <u-icon name="plus-circle-fill" @click.native.stop="toggleShowMessage" />
<view v-show="show" class="message">这是一条信息</view>
</view>
</template>

<script>
export default {
data() {
return {
show: false,
}
},

+ methods: {
+ toggleShowMessage() {
+ this.show = !this.show
+ },
+ },
}
</script>

出现该问题的原因:如果直接在 Vue 的事件绑定中编写单行代码(不是绑定具体的方法),而 uni-app 转换为小程序端代码时会将它放到 .js 文件的方法中,并且由于使用了 .stop 事件修饰符,也在该方法中添加了 $event.stopPropagation() ,然而小程序端打印 $event 根本就是 undefind ,所以导致错误,可以看看生成的小程序代码:

这应该是 uni-app 的问题,关于这个问题更详细的描述可以看我在 uni-app 仓库提的这个 issue

解决方案

总之:尽量不要在事件绑定中使用单行代码,特别是使用了事件修饰符的时候。

9. uView 两层 tabsSwiper left 计算问题

使用 uView 的 tabSwiper 组件时,如果存在两层或多层嵌套,会出现内层的 swiper-item 的 left 值计算错误,这是 uView 的 bug,具体问题描述和临时解决方案可见:https://github.com/YanxinNet/uView/issues/749

10. scss 文件进行条件渲染

.scss   文件中进行 uni-app 的条件渲染,必须为多行注释,如下:

1
2
3
4
5
6
/* #ifdef MP-WEIXIN */
.u-btn {
color: red !important;
}

/* #endif */

另外,如果在 .vue   中的 style   标签,则无论使用单行注释还是多行注释,都是有效的。

11. 方法名不能与生命周期名字冲突

如题,写在 methods   下的方法名字不要与生命周期一致,否则会出现错误,这里的生命周期包括:

12. 调试 uView 组件

如果使用 uView 组件过程中遇到莫名其妙的问题,建议使用断点调试,可帮助你快速定位问题。

举个例子,想看看 u-tabs-swiper    组件的某个方法执行过程,可以到 node_modules/uview-ui/components/u-tabs-swiper/u-tabs-swiper.vue  ,找到你想调试的方法,打个断点:

然后在浏览器刷新,进行某些操作以便触发你想调试的方法,然后就可以进行断点调试了:

遗憾的是 uni-app 的自带组件,虽然也可以在 node-module 目录下找到对应的源码位置,但因为是预编译好的,所以进行修改无效,自然也不能打断点。

大部分 uni-app 的组件都在: node_modules/@dcloudio/uni-h5/src   下。

核心要点:当组件莫名其妙地表现不正常时,别忘了使用断点调试。

13. SwipeAction 组件无法自动隐藏按钮

问题描述

在使用 SwipeAction 组件时,发现在小程序端下的「点击收藏按钮后隐藏按钮」功能不正常,在官方示例程序也可复现该问题:

img

但实际上在 H5 端它是正常的:

img

深入分析源码后,发现点击收藏按钮时, btnClicktouchend 两个方法的执行顺序在 H5 和 小程序端中表现不一致,分别在两个方法中加入 console.log ,运行结果是:

  • H5

img

  • MP-WEIXIN

img

而之所以在小程序端运行异常, 就是因为先执行了 btnClick 方法,而 btnClick 内部将 this.status = false ,导致随后执行 touchend 时进入了错误的条件分支。

解决方案

这问题怎么解决?在不修改组件源代码的时候下,只能使用下面这种方式 HACK:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
methods: {
click(index, index1) {
if (index1 == 1) {
this.list.splice(index, 1)
this.$u.toast(`删除了第${index}个cell`)
} else {
// #ifdef MP-WEIXIN
/**
* HACK: 在 MP-WEIXN 表现异常,为避免被覆盖,需要过一段时间再更新值
* @see https://github.com/YanxinNet/uView/issues/761
*/
setTimeout(() => {
this.list[index].show = false
}, 100)
// #endif
// #ifndef MP-WEIXIN
this.list[index].show = false
// #endif
this.$u.toast(`收藏成功`)
}
},
},

已向 uView 提 issue:https://github.com/YanxinNet/uView/issues/761

14. IOS 底部空白安全区域

子页面在 IOS 会出现一个空白的安全区域,可以通过修改 manifest.json   禁用它:

1
2
3
4
5
6
7
8
9
{
"app-plus": {
"safearea": {
"bottom": {
"offset": "none"
}
}
}
}

参考文档:uni-app 全面屏、刘海屏适配(iphoneX 适配)及安全区设置

15. APP 端锁定屏幕方向

通过 manifest.jsonorientation 选项可以进行横竖屏配置:

1
2
3
4
5
{
"distribute": {
"orientation": ["portrait-primary"]
}
}

但如果在测试基座, 以上选项会失效,所以最保险的方式是在 APP.vue   中进行配置:

1
2
3
4
5
6
7
8
export default {
onLaunch() {
// #ifdef APP-PLUS
// 锁定竖屏
plus.screen.lockOrientation('portrait-primary')
// #endif
}
}

使用该 API 还可以实现仅允许特定页面旋转方向,关于更多可见:HTML+ API Reference

关于屏幕旋转的更多可参考:

16. uButton 自定义样式

根据 uView 文档 说明,如果想要给 button 自定义样式,为了兼容小程序,则要在组件中传递 custom-style   属性,也就意味着需要把 CSS 写在 JS 中,这不是我们想要的,毕竟这样就无法使用 Scss 中的变量了。

为什么要这么麻烦呢?因为在小程序端,原本是一层的元素,被它搞成两层了,所以我们传递过去的 class 属性只应用在外层,而内层才是真正 button。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<view class="test-page">
<u-button class="btn">按钮</u-button>
</view>
</template>

<style lang="scss">
.test-page {
.btn {
color: red;
}
}
</style>

以上代码在 H5 的表现是正常的:


但在小程序的表现却是这样的:

如果想 CSS 层面解决该问题,那就是都将它们都作为选择器即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<view class="test-page">
<u-button class="btn">按钮</u-button>
</view>
</template>

<style lang="scss">
.test-page {
.btn {
+ &,
+ button {
color: red;
+ }
}
}
</style>

虽然看上去很丑,但能 work,且兼容性较好,所以推荐使用该方式。

17. uButton 无法自定义 hover-class

根据 uView 文档 说明,Button 组件有 hover-class 属性,可自定义 hover 时的 class,但实际使用并无效果,查看源代码后发现代码存在问题。

目前 GitHub 上已有人指出该问题,但维护者仍未处理:

在不修改原组件的情况下,只能通过强制覆盖它默认的 hover-class 的样式来解决该问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<view class="test-page">
<u-button class="btn">退出登录</u-button>
</view>
</template>

<style lang="scss">
.test-page {
// HACK: 由于 uButton 的 hover-class 传递无效,暂时只能通过该方式自定义
/deep/ .btn {
&.u-default-hover,
// 兼容小程序
button.u-default-hover {
color: white !important;
background: rgba($color-primary, 0.8) !important;
}
}
}
</style>

注意:

  1. 为了避免影响其它 button,需要限制选择器范围
  2. class 不一定是 u-default-hover ,可以自行测试该 button hover 时实际使用哪个 class
  3. 兼容小程序那里详见:18. uButton 自定义样式

18. uni-app 只能在 main.js 注册全局组件

问题描述

在开发过程中,不可避免需要注册一些全局组件,为了方便管理,我们希望放到一个单独的文件中进行管理,类似下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/plugins/element.js

import BasicLayout from '@/layouts/basic'
import FullLoading from '@/components/full-loading'

export default {
install: (Vue) => {
Vue.component('BasicLayout', BasicLayout)
Vue.component('FullLoading', FullLoading)
}
}

// src/main.js

import element from './plugins/element'

Vue.use(element)

这也是在开发 Vue 项目时常用的方案,但这在微信小程序端会运行异常,根本没有注册到全局组件。

但如果将注册全局组件的工作放到 main.js  ,则是可以正常运行的:

1
2
3
4
5
6
7
8
9
10
import element from './plugins/element'

/**
* FIXME: 因不明原因,在 main.js 外的文件注册全局组件会导致组件在 mp-weixin 端表现不正常
*/
import BasicLayout from '@/layouts/basic'
import FullLoading from '@/components/full-loading'

Vue.component('BasicLayout', BasicLayout)
Vue.component('FullLoading', FullLoading)

该问题也早就有人提出过,但至今仍未看到官方团队有所回应,可见:

而在 uni-app 官方文档的 全局组件 是这样描述的:

虽然说了文档需在 main.js 中进行全局注册,但根据我们以往对 Vue 的认知,这个工作是可以放到外部文件去的,而 uni-app 也没有明确指出一定只能在 main.js 进行注册全局组件,这就造成不少用户困扰。

虽然集中在 main.js 注册全局组件也并不是不能接受,但我们需要知道背后的原因,为什么在外部文件注册就无效?

本文就来一探究竟,了解背后真正的原因。

结论先行

先说结论,说白了就是 uni-app 转换小程序代码时,检测全局组件的方式是通过静态分析 main.js   文件,所以在其他文件注册全局组件的话,uni-app 就无法得知了。

如果想知道更多细节,欢迎继续往下阅读 👇

问题分析

首先,我们来看一下在 main.js   注册全局组件时,生成的微信小程序端 pages.json   的 usingComponents :


很明显这是正常的,那我们再看看如果放在外部文件注册全局组件的话,它是空的:

所以,问题肯定是发生在 uni-app 转换为小程序的过程中,下面就来定位 uni-app 的源码,看看究竟是什么情况。

但问题是,我们并不知道这部分代码在哪个位置,根本不知道从何看起??

别着急,既然我们关心的代码是与 usingComponents   的赋值相关的,那我们可以试试在 node_modules/@dcloudio 搜索关键词 usingComponents =

可以看到结果比较多,我们快速扫一眼,发现几个比较可疑的:

那我们的做法就是进入这几个文件,分别添加一行 console.log  ,以文件名为区分:

 ,如果执行了这个文件,就会打印对应的值:

注意:笔者这时候是把全局组件注册放在 main.js   中

很明显,我们运气不错,这个 main-new.js  应该就是我们想要的代码,下面我们就来看看 components   这个值到底是怎么出来的, components   的定义在 第 84 行


可以看到, components   是根据传入 content   到 traverse()   方法后得到的,我们先来打印一下 content (为避免干扰,可以先把之前的 console.log 去掉),然后我们看到以下:


可以看到这是 main.js   的内容,难道是通过解析这个文件的内容得到全局组件的?

我们还是看一下它给 traverse()   传递的参数,发现是一个通过 babel 解析后的 AST 树:


然后我们再看看 traverse()   内部,在 global-component-traverse.js


那看来的确如此,但这时候我还有一个疑问:上面只是说明 uni-app 是通过静态解析文件内容得到全局组件,那为什么不可以针对其它文件进行解析呢?

我们看到其实 content   是从外部传入的,也就是说是别的文件传入了 main.js   的内容:


于是搜索当前文件的引用,一路顺藤摸瓜,找到起始的入口,它确实是只针对 main.js   进行解析,在 vue-cli-plugin-uni/lib/mp/index.js

到此为止,我们已经知道为什么 uni-app 只能在 main.js   才能注册全局组件,而其它文件无效了。

写在最后

最后还是得心平气和地评价几句,虽然在使用 uni-app 开发过程中踩了不少的坑,心里也曾不止一次有十万个草泥马奔腾而过,但是不可否认这么一个开发跨平台应用的前端框架来说,它做的还是有值得称赞的地方,至少从生态上面就足以秒杀其它同样是 Vue 语法的开发跨平台框架。


作者 : 4Ark
来源 : https://4ark.me
著作权归作者所有,转载请联系作者获得授权。

🥳 加载 Disqus 评论