Vue 如何实现全局插槽?

394 阅读3分钟

先放代码:github.com/cloydlau/vu…

我们知道在 Vue 中插槽是通过模板传递的,那如果所有用到这个组件的地方都需要某个插槽时,可能就不得不进行二次封装了,我们希望插槽能做到像属性 props 一样全局传递。就像这样:

  • Vue 3
app.use(YourComponent, {
  // 全局 Slot
  '#left-footer': () => h('Fragment', undefined, 'Global Slot'),

  // 全局 Scoped Slot
  '#default': ({ option }) => h('Fragment', undefined, `${option.label} (From Global Scoped Slot)`),
})
  • Vue 2
Vue.use(YourComponent, {
  // 全局 Slot
  '#left-footer': () => ({ render: h => h('span', undefined, 'Global Slot') }),

  // 全局 Scoped Slot
  '#default': ({ option }) => ({ render: h => h('span', undefined, `${option.label} (From Global Scoped Slot)`) }),
})

全局传参

在 Vue 中,参数有 props/attrs/listeners/hooks/slots 等多种形式,实现全局传递 props/attrs 是较为常见的,Element / Element Plus 就是案例,全局 listeners/hooks 使用 this.$listeners + 劫持 this.$emits 也可以实现:

  • Vue 3
app.use(YourComponent, {
  // 全局 Prop
  'title': 'Global Title',

  // 全局 Attr
  'data': [
    { key: 1, label: 'Global Option 1' },
    { key: 2, label: 'Global Option 2' },
  ],

  // 全局 Listener
  '@leftCheckChange': function () {
    console.log('Global LeftCheckChange')
  },

  // 全局 Hook
  '@vnodeMounted': function () {
    console.log('Global Mounted')
  },
})
  • Vue 2
Vue.use(YourComponent, {
  // 全局 Prop
  'title': 'Global Title',

  // 全局 Attr
  'data': [
    { key: 1, label: 'Global Option 1' },
    { key: 2, label: 'Global Option 2' },
  ],

  // 全局 Listener
  '@left-check-change': function () {
    console.log('Global LeftCheckChange')
  },

  // 全局 Hook
  '@hook:mounted': function () {
    console.log('Global Mounted')
  },
})

全局配置与局部配置如何进行合并?

原始类型

直接 ?? 一个短路可以简单搞定,不过严谨地来说,只有 undefined 才能视为没有传参,null 应该被理解为传了但为空,可以使用 vue-global-config 提供的 conclude 来处理这个问题。

对象类型

有三种情况,可以是局部配置覆盖全局配置,可以是浅合并,就像现在 Vue 3 的 mixins 合并策略,也可以是深合并,就像 Vue 2 的 mixins 合并策略,而这些 conclude 都支持。

函数类型

可以是局部配置覆盖全局配置,可以是两者都触发,conclude 提供了函数融合器来解决这个问题,高度灵活。


如何在全局监听器/全局钩子中访问 this

组件在绑定全局监听器/全局钩子前逐一给它们 .bind(this)


全局插槽

全局插槽需要通过编程式的方式进行传递,插槽是什么,是一个 HTML 片段,那我们要怎样将一个 HTML 片段以编程方式全局传递给一个组件呢?

答案:使用动态组件 <component :is="xxx">,动态组件不是只支持已经注册了的组件名称吗?非也,其实 is 支持的格式很多。

方式一:渲染函数 h/createVNode

仅限 Vue 3

app.use(YourComponent, {
  '#left-footer': () => h('Fragment', undefined, 'Global Slot'),
})

代码简洁,如果你使用的是 Vue 3,推荐此方式。

方式二:组件定义 + 渲染函数

支持 Vue 2.6/2.7/3

app.use(YourComponent, {
  '#left-footer': () => ({ render: h => h('span', undefined, 'Global Slot') }),
})

如果你使用的是 Vue 2,推荐此方式。

方式三:组件定义 + 模板

支持 Vue 2.6/2.7/3

app.use(YourComponent, {
  '#left-footer': () => ({ template: '...' }),
})

方式四:组件构造器

仅限 Vue 2.6/2.7

Vue.use(YourComponent, {
  '#left-footer': () => Vue.extend(...),
})

由于 Vue.extend 在 Vue 3 中已被废弃,故不推荐此方式。

方式五:导入的 SFC

支持 Vue 2.6/2.7/3

import SomeComponent from './SomeComponent.vue'

app.use(YourComponent, {
  '#left-footer': () => SomeComponent,
})

适用于插槽较为复杂的情况。

方式六:局部或全局注册过的组件名称

支持 Vue 2.6/2.7/3

app.use(YourComponent, {
  '#left-footer': () => 'SomeComponent',
})

适用于插槽是某个已经注册过的组件的情况。


全局作用域插槽

通过给函数传参实现,这也是为什么前面的普通插槽也是函数的形式,只是为了保持一致的格式。

app.use(YourComponent, {
  '#default': ({ option }) => h('Fragment', undefined, `${option.label} (From Global Scoped Slot)`),
})

如何保证局部插槽的优先级高于全局插槽?

这里有一个坑,在 Vue 3 中,通过书写顺序就能解决这个问题,但在 Vue 2 中不行。

Vue 3

在 Vue 3 中,对于同名插槽,书写在后面的优先级更高,所以我们将局部插槽书写在后面即可。

Vue 2

在 Vue 2 中,需要先将全局插槽与局部插槽通过 conclude 进行融合,对于同名插槽,仅留下优先级高的局部插槽,在渲染时判断插槽是来自于全局还是局部,选择不同的标签进行渲染。