最近想体验下自定义指令功能,自指令组件看了看文档和 vue2 差异不大,定义定义语法如下:
const myDirective = { // 在绑定元素的实践使用数渲 attribute 前 // 或事件监听器应用前调用created(el, binding, vnode, prevVnode) { // 下面会介绍各个参数的细节 }, // 在元素被插入到 DOM 前调用beforeMount(el, binding, vnode, prevVnode) { },// 在绑定元素的父组件// 及他自己的所有子节点都挂载完成后调用mounted(el, binding, vnode, prevVnode) { },// 绑定元素的父组件更新前调用beforeUpdate(el, binding, vnode, prevVnode) { },// 在绑定元素的父组件// 及他自己的所有子节点都更新后调用updated(el, binding, vnode, prevVnode) { },// 绑定元素的父组件卸载前调用beforeUnmount(el, binding, vnode, prevVnode) { },// 绑定元素的父组件卸载后调用unmounted(el, binding, vnode, prevVnode) { }}
起初,最大的染自痛点是需要手动创建 dom ,然后插入 el 中。令中
const loadingDom = document.createElement('div',自指令组件 { calss: 'loading'})el.append(loadingDom)
这样好难受啊,我不想写原生 dom ,定义定义能不能写个组件渲染到指令里呢?
我想起了我之前看到的实践使用数渲几个 vue 接口,
h函数用法如下:
// 完整参数签名function h( type: string | Component, props?: object | null, children?: Children | Slot | Slots): VNode
例如:
import { h } from 'vue'const vnode = h('div', { class: 'container' }, [ h('h1', 'Hello, Vue 3'), h('p', 'This is a paragraph')])
我们使用h函数创建了一个 VNode,它表示一个包含 div、h1、p 的 DOM 结构。其中,div 的 class 属性为 container
然而,当我使用 props 为组件传递值时,发现是徒劳的。
import Loading from './components/Loading.vue';cconst option = { msg: '一大波僵尸来袭', loading: true}const vnode = h( Loading, { id: 'loading', ...option})
仅仅如下图一样,我以为是给组件的 props,实际上是 dom 的 props。
图片
此时我们的组件只能通过 $attrs 来获取这些 props 了,如下:
<template> <div class="loading"> <div></div> <span v-if="$attrs.msg !== false">{ { $attrs.msg }}</span> </div> </template>
接着我们给组件实现 loading 动画,当然你也可以直接使用组件库的 loading 组件。
我的实现如下:
<style> @keyframes identifier { 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } } .loading { height: 100px; width: 100%; } .loading div { width: 50px; height: 50px; border-radius: 50%; border: 2px solid green; margin: 25px auto; border-top: none; border-left: none; animation: identifier 1s infinite linear; }</style>
接下来我们封装自定义指令。
我们的思路是:
为了可以应对更多场景,我们期望可以配置加载中的提示信息,不配置使用默认值,如果是 false ,那么仅展示 loading 图。所以参数类型如下:
interface Props { loading: boolean, msg?: string | false}v-loading: boolean | Props
由于可能是布尔值,也可能是对象,我们需要初始化配置参数
function formatOption (value: boolean | Props) { const loading = typeof value === 'boolean' ? value : value.loading const option = typeof value !== 'boolean' ? Object.assign(defaultOption, value) : { loading, ...defaultOption } return { loading, option }}
接着再 mounted 阶段获取格式化后的 loading 和 option,如果为 true 则直接渲染组件。
const vLoading: Directive<HTMLElement, boolean | Props> = { mounted(el, binding) { const { loading, option } = formatOption(binding.value) loading && renderLoading(el, option) }}function renderLoading (el: HTMLElement, option: Props) { const vnode = h( Loading, { id: 'loading', ...option} ) el.removeChild(el.children[0]) render(vnode, el)}
如果进入 update 阶段,则根据情况选择渲染 laoding 组件还是 vnode。
const vLoading: Directive<HTMLElement, boolean | Props> = { mounted(el, binding) { const { loading, option } = formatOption(binding.value) loading && renderLoading(el, option) }, updated(el: HTMLElement, binding, vnode) { const { loading, option } = formatOption(binding.value) if (loading) { renderLoading(el, option) } else { renderVNode(el, vnode) } },}function renderLoading (el: HTMLElement, option: Props) { const vnode = h( Loading, { id: 'loading', ...option} ) el.removeChild(el.children[0]) render(vnode, el)}function renderVNode (el: HTMLElement, vnode: VNode) { el.querySelector('#loading')?.remove() render(vnode, el)}
我们验证下功能:
const loading = ref(true)setTimeout(() => loading.value = false, 2000)</script><template> <div style="width: 300px" v-loading=laoding> <h1>加载成功</h1> </div></template>
图片
<template> <div style="width: 300px" v-loading="{ loading, msg: '一大波僵尸来袭' }"> <h1>加载成功</h1> </div></template>
图片
<template> <div style="width: 300px" v-loading="{ loading, msg: false }"> <h1>加载成功</h1> </div></template>
图片
今天的分享就到这了,如果有问题,可以评论区告诉我,我会及时更正。
以下是完整的代码。
<template> <div class="loading"> <div></div> <span v-if="$attrs.msg !== false">{ { $attrs.msg }}</span> </div> </template> <script lang="ts">export default { }</script> <style> @keyframes identifier { 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } } .loading { height: 100px; width: 100%; } .loading div { width: 50px; height: 50px; border-radius: 50%; border: 2px solid green; margin: 25px auto; border-top: none; border-left: none; animation: identifier 1s infinite linear; }</style>
<script setup lang="ts">import { Directive, VNode, h, ref, render } from 'vue';import Loading from './components/Loading.vue';const defaultOption: { msg?: string | false} = { msg: '努力加载中'}interface Props { loading: boolean, msg?: string | false}function formatOption (value: boolean | Props) { const loading = typeof value === 'boolean' ? value : value.loading const option = typeof value !== 'boolean' ? Object.assign(defaultOption, value) : { loading, ...defaultOption } return { loading, option }}function renderLoading (el: HTMLElement, option: Props) { const vnode = h( Loading, { id: 'loading', ...option} ) el.removeChild(el.children[0]) render(vnode, el)}function renderVNode (el: HTMLElement, vnode: VNode) { el.querySelector('#loading')?.remove() render(vnode, el)}const vLoading: Directive<HTMLElement, boolean | Props> = { mounted(el, binding) { const { loading, option } = formatOption(binding.value) loading && renderLoading(el, option) }, updated(el: HTMLElement, binding, vnode) { const { loading, option } = formatOption(binding.value) if (loading) { renderLoading(el, option) } else { renderVNode(el, vnode) } },}const loading = ref(true)setTimeout(() => loading.value = false, 2000)</script><template> <div style="width: 300px" v-loading="{ loading, msg: '一大波僵尸来袭' }"> <h1>加载成功</h1> </div></template>
责任编辑:武晓燕 来源: 萌萌哒草头将军 场景vue3自定义(责任编辑:探索)
彩讯股份(300634.SZ):股东广东达盛累计减持437.99万股
2018年钢铁行业实现利润4704亿元 比上年增长39.3%
中国移动明年重点开展5G终端规模试验 首批5G手机价格预计在8000元以上
大生农业金融(01103.HK)发布公告:年度公司持有人应占亏损11.25亿元
华兰生物(002007.SZ):2020年度净利升25.69% 基本每股收益0.8873元