packages/src/app/components/client-only.mjs

export default defineComponent({
  name: 'ClientOnly',
  // eslint-disable-next-line vue/require-prop-types
  props: ['fallback', 'placeholder', 'placeholderTag', 'fallbackTag'],
  setup (_, { slots }) {
    const mounted = ref(false)
    onMounted(() => { mounted.value = true })
    return (props) => {
      if (mounted.value) { return slots.default?.() }
      const slot = slots.fallback || slots.placeholder
      if (slot) { return slot() }
      const fallbackStr = props.fallback || props.placeholder || ''
      const fallbackTag = props.fallbackTag || props.placeholderTag || 'span'
      return createElementBlock(fallbackTag, null, fallbackStr)
    }
  }
})

CSR 的生命周期与 SSR 的生命周期是有差异的,我们可以利用这些差异来达到我们的目的,也就是利用了 onMounted 钩子只会在客户端执行这一特性。

我们创建了一个标记变量 mounted,初始值为 false,并且仅在客户端渲染时将其设置为 true,这意味着,在服务端渲染的时候,<ClientOnly> 组件的插槽内容不会被渲染。而在客户端渲染的时候,只有等 mounted 钩子触发之后才会渲染 <ClientOnly> 组件的插槽内容。这样就实现了被 <ClientOnly> 组件包裹的内容仅会在客户端被渲染。
另外,<ClientOnly> 组件并不会导致客户端激活失败,hyr,因为在客户端激活的时候,mounted钩子还没有触发,所以服务端与客户端渲染内容一致,即什么都不渲染。等激活完成,且 mounted钩子触发执行之后,才会在客户端将 <ClientOnly> 组件的插槽内容渲染出来。

–《Vue.js 设计与实现》