Skip to content

uni-app 踩坑小记

Published:

写在前面

这里记录一些使用 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

如果是自定义 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 的路径在哪,可以这样:

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

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

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 官方文档指出:

不要使用对象或数组之类的非基本类型值作为 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

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

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

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

<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

解决方案

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

9. uView 两层 tabsSwiper left 计算问题

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

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

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 适配)及安全区设置

15. APP 端锁定屏幕方向

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

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

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

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。

举个例子:

<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 文档 说明,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 官方文档的 全局组件 是这样描述的:

虽然说了文档需在 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/post/uni-app-issues.html/

来源 : https://4ark.me

著作权归作者所有,转载请联系作者获得授权。