众所周知,小程序不是一个单页应用,它的每一个页面都是一个独立的 WebView,又因为没有提供可以跨越多页面的渲染能力,所以没有办法实现全局的可以跨越多页面的组件。
但是群众们有智慧啊,那我们就麻烦点,每个页面都手动引入一次组件好了,反正不管咋样,总算能实现了,算是填了一个坑。
这其中具体的实现方案多种多样,各有千秋,无论是渲染体验,还是 coding 调用体验,都千差万别,难以详述。
这篇来描述一种还不错的实现方案,以 uni-app 为例(原生,及其他框架类同),来一步步实现全局组件的效果。
在开始之前,先重新简单梳理下小程序的页面栈。
- 每个页面都是一个独立的 WebView
- 底部的 Tab 页面是固定的,一旦打开就只会创建一次并且不会销毁,当切换回该页面时会重新位于页面栈的顶部
- 其他非 Tab 页面当打开时会新建一次,并置于页面栈顶部;当打开新页面时,新页面置顶,旧页面被压在了下面;当返回上一页时,当前页面销毁,上一页重新置于页面栈顶部
正是由于页面栈的存在,以及每个页面的独立性、隔离性,导致实现可以跨越多页面的全局组件复杂重重。
由上可知,理论上来讲,没有办法可以真正实现覆盖在所有页面之上的全局组件。
不过我们可以模拟来实现,模拟的恰到好处,就是个中差别了,可以模拟一个体验不错的全局组件出来。
首先,我们定几个目标:
- 要少写代码
- 要简单易用
- 支持多个组件
一. 封装一个组件容器
封装一个组件容器,将多个组件全部引入这个容器,这样一来,我们就可以在 Page 上只引入一次这个组件容器,就相当于引入了全部的全局组件。
为了降低实现和使用的复杂性,这个组件容器就仅仅只是一个组件容器而已。
另外,我们也额外在这里封装下简单的组件注册函数。
<!-- leaf.vue -->
<template>
<view>
<image-edit />
<notification />
<alert />
</view>
</template>
<script>
// 全局 API
uni.$leaf = {
$register(vm, name) { // 注册组件
if (!uni.$leaf[name]) {
// 根据 name 注册组件 API
uni.$leaf[name] = (data) => {
// 为了“简单易用”,所以返回 Promise
return Promise((resolve, reject) => {
const pages = getCurrentPages()
let vm = pages[pages.length - 1]
// #ifdef MP-WEIXIN
vm = vm.$vm
// #endif
uni.$emit('leaf/' + name, {
$route: vm.$route, // 用于判断区分当前的页面栈
data,
resolve,
reject,
})
})
}
}
// 封装事件,以简化自定义组件
const handler = (e) => {
if (e.$route !== vm.$route || e.$stop) {
return
}
// 只有栈顶的页面才能获得事件,
// 这样就能保证,无论 API 调用是由哪个页面或页面内的子组件发起,
// 都能立刻显示在用户面前
e.$stop = true
// 调用组件上的事件侦听
vm.leafHandler(e)
}
uni.$on('leaf/' + name, handler)
vm.$on('hook:beforeDestroy', () => {
uni.$off('leaf/' + name, handler)
})
},
}
import ImageEdit from './image-edit'
import notification from './notification'
import alert from './alert'
export default {
components: {
ImageEdit,
notification,
alert,
},
}
</script>
我们可以直接在 Page 中引入这个组件容器。
<!-- index.vue -->
<template>
<view>
<leaf />
</view>
</template>
<script>
import leaf from '@/components/leaf'
export default {
components: {
leaf,
},
}
</script>
二. 定义组件
定义组件时,需要注册一下,并提供一个事件侦听函数来处理 API 的调用。
我们努力保证简单性。
<!-- alert.vue -->
<template>
<uni-popup ref="popup">
<view>{{content}}</view>
<button @click="confirm">确定</button>
<button @click="cancel">取消</button>
</uni-popup>
</template>
<script>
export default {
data() {
return {
content: '',
}
},
created() {
// 注册组件
uni.$leaf.$register(this, 'alert')
},
methods: {
leafHandler(e) {
const { data } = e
this.content = data.content
this.$e = e // 缓存事件
this.$refs.popup.open()
},
confirm() {
this.$refs.popup.close()
// 啊哈,我们可以使用 Promise 来管理组件的状态,可以为所欲为,
// 这个 API 的实现方案很人性化吧。
this.$e.resolve()
},
cancel() {
this.$refs.popup.close()
this.$e.reject()
},
},
}
</script>
三. 使用
现在我们可以很简单、很方便、很人性化的来使用全局组件了。
前面之所以用 Promise 来封装,就是为了这里能人性化的使用,简单易用才令人愉快。
<!-- index.vue -->
<template>
<view>
<button @click="testAlert">测试下弹框</button>
<leaf />
</view>
</template>
<script>
import leaf from '@/components/leaf'
export default {
components: {
leaf,
},
methods: {
async testAlert() {
try {
// 用最少的代码,简单测试下效果
await uni.$leaf.alert({ content: '你觉得这个方案还可吗?' })
} catch (error) {
uni.showToast({ title: '有啥好建议么?' })
}
},
},
}
</script>
总结
前面定义的几个目标应该、也许、大概,算是实现了吧。
关注一下
下一篇介绍一下小程序的 只有在需要时才获取用户授权 的简单易用、人性化的实现方案。
这是一个很普通、很合理的可以提高用户体验的需求,但是一般实现起来却并不简单,真正手写实践起来有点令人不愉快。
那么就等着一个好方案来实现吧,求关注一下哈。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!