写在前面
这里记录一些使用 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 的路径在哪,可以这样:
- Linux/Mac:
where node
- Windows:直接看系统环境变量
参考: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 文件),则情况会变得稍微复杂一点,以上两种情况都不适用,先说结论:
- 不支持拿 data 的数据用于拼接动态 slot 名字
- 在 v-for 中要根据当前项的字段来拼接 slot 名字,则要将 key 指向
item
本身(不推荐 - 能拿 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
。
既然如此,我们似乎得到两个解决方案:
- 将 v-for 的 key 直接指向 item 本身,使
item.id
能正常访问 - 拼接 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,可见相关的讨论:
- https://ask.dcloud.net.cn/question/95109
- 如何评价 uni-app? - 蘑菇王的回答 - 知乎 https://www.zhihu.com/question/309490398/answer/1181409781
所以 uni-app 官方什么时候支持动态 slot 名字呢?
7. 小程序端 v-for 中使用 v-if 问题
问题描述
这个问题需要同时满足以下条件才会触发:
- 在 v-for 内部使用了 v-if
- v-if 的断言依赖了 methods 或者 闭包 computed(就是需要根据入参动态返回结果
- 在 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 思路:
- 在外面包一层,利用 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。
解决方案
总之:尽量不要在事件绑定中使用单行代码,特别是使用了事件修饰符的时候。
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 组件时,发现在小程序端下的「点击收藏按钮后隐藏按钮」功能不正常,在官方示例程序也可复现该问题:
但实际上在 H5 端它是正常的:
深入分析源码后,发现点击收藏按钮时, btnClick
和 touchend
两个方法的执行顺序在 H5 和 小程序端中表现不一致,分别在两个方法中加入 console.log
,运行结果是:
- H5
- MP-WEIXIN
而之所以在小程序端运行异常,就是因为先执行了 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.json
的 orientation 选项可以进行横竖屏配置:
{
"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>
注意:
- 为了避免影响其它 button,需要限制选择器范围
- class 不一定是
u-default-hover
,可以自行测试该 button hover 时实际使用哪个 class - 兼容小程序那里详见: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 语法的开发跨平台框架。