使用 Vue Suspense 对异步组件和异步 setup 一起做了封装

3,064 阅读2分钟

Suspense 基础使用

参考文章: vueschool.io/articles/vu…

官网暂未放出 Suspense 文档,因为属于 feature,API 还未稳定。使用时还会弹出提示。

is an experimental feature and its API will likely change.

已知 Suspense 可以对异步的 setup 组件,提供 fallback 的 slot

又已知 defineAsyncComponent 提供异步组件懒加载功能。功能非常相近。并且文档中有提到 defineAsyncComponent 可与 Suspense 一起使用。不过部分 options 会无效。文档

Suspense 一起使用 异步组件在默认情况下是可挂起的。这意味着如果它在父链中有一个 <Suspense>,它将被视为该 <Suspense> 的异步依赖。 在这种情况下,加载状态将由 <Suspense> 控制,组件自身的加载、错误、延迟和超时选项将被忽略。

所以要使用的话,要抽象出公共一个方法,简化调用。

实现要点

  • import 函数的动态路径引入
  • 实现 delaytimeouterrorloading
  • 高阶组件的 propseventslotsref 的透传
  • retry

部分主要实现代码

function createInnerComp(
  comp,
  {
    vnode: { ref, props, children }
  }
) {
  const compVnode = h(comp, props, children)
  // ref 透传
  compVnode.ref = ref
  return compVnode
}

function asyncLoader (componentPath, options = {}) {
  return {
    name: 'asyncLoaderWrapper',
    emits: ['resolve', 'fallback', 'pending'],
    inheritAttrs: false,
    /* 骗过上帝
    https://github.com/vuejs/core/blob/main/packages/runtime-core/src/rendererTemplateRef.ts#L43
    setRef 时,通过 __asyncLoader 字段判断来跳过该组件 ref 的设置。再通过 render 函数把 ref 字段赋值给我们想要透传的组件的 vnode 即可完成 forWardRef
    */
    __asyncLoader: Promise.resolve(),
    setup(props, { emit }) {
      const { retry, error, setComponentLoadStatus } = useComponentStatus()
      const { 
        errorComponent, 
        loadingComponent,
        delay,
        ...defineAsyncOptions 
      } = { ...asyncLoaderDefaultOptions, ...pluginOptions, ...options, }

      const instance = getCurrentInstance()

      const optionsComponent = normalizeSuspenseDefaultSFC(componentPath, {
        ...defineAsyncOptions,
        onComponentLoadStatus: setComponentLoadStatus
      })

      return () => {
        if (error.value) {
          return h(errorComponent, { error: error.value, retry })
        }

        const defaultChildVnode = createInnerComp(optionsComponent, instance)

        const fallbackVnode = h({
          props: {
            loadingDelay: Number
          },
          setup(props) {
            // patch 的时候 container 是 null. 
            // component effect 的时候, prevTree = instance.subTree 居然是 comment 节点
            // hostParentNode(prevTree.el) 寻找父节作为 patch 的 container 为 null. 导致插入节点失败
            // 原因: fallback 的 rerender 后. Suspense 没有更新 subTree.
            // vue3 slot 只能加载 templte 上. 除非只有 defualt 插槽
            const { delayed } = useDelay(props.loadingDelay)
            return () => !delayed.value ? h(loadingComponent) : null
          }
        }, {
          loadingDelay: delay
        })

        return h(Suspense, {
          onFallback(...args) {
            emit('fallback', ...args)
          },
          onResolve(...args) {
            emit('resolve', ...args)
          },
          onPending(...args) {
            emit('pending', ...args)
          },
        }, {
          default: wrapTemplate(defaultChildVnode),
          // fallback 变动好像会导致 default 重新渲染, delay 只能放 fallback 里执行
          fallback: wrapTemplate(fallbackVnode)
        })
      }
    }
  }
}

演示

完整代码

demo

代码

能力有限。如果哪里写的不好,欢迎指点一下

TODO

支持 typescipt,增加类型定义