你欠我一个全局组件,咋办?

852 阅读3分钟

众所周知,小程序不是一个单页应用,它的每一个页面都是一个独立的 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>

总结

前面定义的几个目标应该、也许、大概,算是实现了吧。

关注一下

下一篇介绍一下小程序的 只有在需要时才获取用户授权 的简单易用、人性化的实现方案。
这是一个很普通、很合理的可以提高用户体验的需求,但是一般实现起来却并不简单,真正手写实践起来有点令人不愉快。
那么就等着一个好方案来实现吧,求关注一下哈。