6

uni-app 踩坑小记

 2 years ago
source link: https://4ark.me/post/uni-app-issues/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

# 写在前面

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

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

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

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

# 2. ref 不要放在 computed

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

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

<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),像下面这样可能会无效:

<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 的,所以需要改成下面这样:

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

这样 H5 也能运行正常了:

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

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

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

其实根本没有修改到:

# 解决方案

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

// 文件路径: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;
//     }
//   }
// }

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

// 文件路径:src/styles/uni-custom.scss

...

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

可以看到是有样式的:

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

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

参考链接:https://ask.dcloud.net.cn/question/68411 (opens new window)

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

# 5. 在 HBuilder 运行项目

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

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 (opens new window)

# 6. 动态插槽名

# 前言

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

# H5 和 小程序端

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

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

使用 slot:

<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 (opens new window)

# APP 端

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

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

# 解决方案

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

<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>

使用的时候:

<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 名字:

<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 循环内部,所以我们再拿以下代码做测试:

<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 官方文档 (opens new window)指出:

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

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

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

<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 内部直接使用了小程序端的语法

# 排查过程

复现代码如下:

// 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

<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 也能正常显示:

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

<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 哪去了?别急,让我们对比一下两者生成的微信小程序代码:

<!-- 这是正常的 -->
<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 值:

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

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

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

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

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

<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 ,那么解决方案就是自己手动加上呗:

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

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

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

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

# 问题描述

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

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

只能使用修饰符:

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

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

参考链接:https://github.com/dcloudio/uni-app/issues/1067 (opens new window)

关于 uni-app 中的修饰符兼容情况:事件修饰符 —— uni-app (opens new window)

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

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

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

<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 则没有该问题:

<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 (opens new window)

# 解决方案

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

# 9. uView 两层 tabsSwiper left 计算问题

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

# 10. scss 文件进行条件渲染

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

/* #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,运行结果是:

  • img

  • MP-WEIXIN

img

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

# 解决方案

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

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 禁用它:

{
  "app-plus": {
    "safearea": {
      "bottom": {
        "offset": "none"
      }
    }
  }
}

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

# 15. APP 端锁定屏幕方向

通过 manifest.jsonorientation (opens new window) 选项可以进行横竖屏配置:

{
  "distribute": {
    "orientation": ["portrait-primary"]
  }
}

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

export default {
  onLaunch() {
    // #ifdef APP-PLUS
    // 锁定竖屏
    plus.screen.lockOrientation('portrait-primary')
    // #endif
  },
}

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

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

# 16. uButton 自定义样式

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

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

举个例子:

<template>
  <view class="test-page">
    <u-button class="btn">按钮</u-button>
  </view>
</template>

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

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

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

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

<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 文档 (opens new window) 说明,Button 组件有 hover-class 属性,可自定义 hover 时的 class,但实际使用并无效果,查看源代码后发现代码存在问题。

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

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

<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 注册全局组件

# 问题描述

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

// 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 ,则是可以正常运行的:

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 官方文档的 全局组件 (opens new window) 是这样描述的:

虽然说了文档需在 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 (opens new window) 应该就是我们想要的代码,下面我们就来看看 components 这个值到底是怎么出来的, components 的定义在 第 84 行 (opens new window)

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

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

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

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

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

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

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

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

# 写在最后

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK