Warning
本文发布于 2023/08/20,内容可能已过时。
开始
一直很想封装一个 Toast 来着,但想到用函数的形式来调用 Toast('hello')
,才意识到这似乎有一点难搞,于是就去翻看了 element-plus 的实现:el-message
果然还是要用到函数式渲染的 createVNode 来调用组件 SFC,完整可见:Github
Vue Plugin
我们按 “它是怎么被导入并调用” 的思路来理解源码
先从引入 element-plus 的文档开始,它只要 import 并 app.use(ElementPlus)
就能使用了,而 vue app.use 指的是安装一个插件,vue Plugin 是一个有 install()
方法的用在 Vue 全局的工具代码
element-plus 是一个 monorepo,在 /packages 下是项目各个包的根目录,从 import ElementPlus from 'element-plus'
在仓库中找到 /packages/element-plus 这就是它的入口处。在 index.ts 中可以看出它导出了将所有组件和插件设为 vue installer 插件
顺着过去找到 makeInstaller 的定义,它的作用就是将这些组件们打包成一个 element-plus 插件,然后就可以在 vue 入口处使用了 (app.use())
上面是它简化后的样子,其实他还有一个前置步骤是将组件包装成 vue 插件,然后再在这里全部导入 (从 forEach 就可以看出,这就是文档说的全量导入)。这时候我们再点进 /packages/components
里的一个组件 (如 Message),可以看出它是这样写的
重点就是这个 withInstallFunction
,它是将函数组件打包成可调用的关键,但我们先看看远处的组件引用再回过头来看就能更好地李姐了
Component With Install
我们先换一个常规点的组件来看,例如 button,它的使用方式就是简单的 <el-button/>
就好了。在 button 的定义中,简化一下就是 export const ElButton = withInstall(Button) // Button.vue
,这个 withInstall
写法如下
首先定义的类型是因为,其实有 vue app.component() 这样全局注册组件的方式,也就是说其实每个组件 SFC (*.vue 文件) 都是可 install 的,但又不是每个 SFC 都是组件 (defineComponent()),所以需要显示声明它的类型为 Plugin,同时必须要在 SFC 中指定它的名字
所以这个函数做的就是全局注册这些组件,然后只要 export const MyButton = withInstall(Button)
就好,然后再统一在 components.ts 中导入并导出给 installer,最后只需要在 vue 的入口 main.ts 中 app.use(installer)
就完成了组件注册
这么做的原因是为了可以直接在入口处统一导入,而不用一次次地 app.component()
Function With Install
如果想要以函数的方式来召唤组件,就要使用 createVNode 来创建组件。与组件注册不同的是,它是注册到全局方法中,并要为它指定全局上下文 context,以访问一些全局的信息 (如依赖注入或是其他的 app.config.globalProperties 方法)
至此,全局组件的封装大致就是这些了,接下来是如何用函数来调用组件
createVNode
我们先写好 Toast.vue
,并为了方便管理,将 props 抽离出来,以 props 运行时声明的形式来写,再在 SFC 中 import
其中,为了能够给它完整的一生…命周期,用 transition
和 v-show
的形式绑定 hooks,这样就能在 Toast 消失后回收它,而不只是 v-if
。毕竟每一个 Toast 都是一个新的实例,没用了就要销毁,不然可能会内存堆积 (好像是吧)
那么要怎么调用这个生命 hook 呢?因为我们把它写在了 props 中,于是就要在调用方声明它
Toast Offset
Toast 通常是 position: fix;
,为了不让创建出来的组件全堆在一起,我们需要为它指定 top: ${offset}px
。这时候就需要维护一个 Toast 实例的数组,获取当前显示的 Toasts 中最后一个的 offset。并且这一切都得是响应式的,在先出的组件消失之后,后续的 offset 也要随之减少以适应位置
然后在 Toast.vue 中堆计算属性就行 (下面的示例忽略了计时器的处理)
到这里,基本原理基本就是讲完了,但最重要的还是传参的设置与类型安全
Toast Options Params
调用这个 Toast,可能有下面这些方式
首先要将所有的参数都设为可选的 (Partial<T>),对于 type,虽然是 string,但要限定于 'info' | 'error'
等这些类型。这就是为什么要把 props 抽离出来,这也是为了方便管理这些参数
ExtractPropTypes 是为了能够将 props 转为类型,但体验下来还是 sxzz 写的 buildProp 好用很多
于是我们这么定义 toast 函数
在解析参数中,主要就是覆盖上默认参数了
Migrate to Nuxt3
迁移到 Nuxt3 中第一个问题就是 app.use 没了,该怎么转换
好在这个本质上也是一个插件来的,只要定义到 Nuxt3 的插件上就行了。但由于用到了诸如 document 这样 SSR 没有的方法,最好将它注册为纯客户端插件。同时为了避免与自动导入等冲突了,最好还是将它另起目录,而不是一同在 ~/components 中
第二个就是 document not defined 了,这需要特判,export const isClient = process.client
(eslint 或许会提示要用 node:process,但那就更冲突了,client 端是没有 node 的… disable 就好 /* eslint-disable n/prefer-global/process */
)
至此,基本就完成了