命令式组件是指组件的创建、props 的传递、emits 方法的执行都通过一个函数来完成。 常见的命令式组件有 ELMessage、ElMessageBox、ElNotification 等。
应用场景 对于单文件组件,每次用到都需要在父组件中定义所需的 props 和 emits 方法,如果还需要控制子组件的显隐,往往要调用子组件内部控制显隐的方法,这样的话还需要定义一个模板引用 ref。 由此可见,这个子组件的调用逻辑多且分散,如果这个组件需要被多次用到,那么无疑给代码结构和开发人员都带来不好的影响。 而命令式组件仅仅通过一个函数来完成,逻辑高度集中,代码结构清晰易于维护。
构建命令式组件 构建命令式组件的主要思想就是:调用函数时根据已有组件创建一个虚拟节点,将虚拟节点挂载到一个真实 DOM 上并渲染出来。 vue 中可以创建虚拟节点的 API 有两个:createApp
,createVNode/h
。 现在我们来构建一个命令式组件,其功能主要是通过一个弹窗显示一些内容,包括取消和确定两个按钮,点击后分别执行开发者定义的逻辑。
1. 创建单文件组件 首先我们要创建一个组件,完成其内部样式和逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 <template > <div class ="box" > <div > {{ title }}</div > {{ message }} <button @click ="handleAction('cancel')" > {{ cancelButtonText }}</button > <button @click ="handleAction('confirm')" > {{ confirmButtonText }}</button > </div > </template > <script setup lang ="ts" > const props = withDefaults(defineProps<MessageProps>(), { title : "提示" , cancelButtonText : "取消" , confirmButtonText : "确定" }); function handleAction (action: Action ) { props.onAction(action); } </script > <style scoped > .box { width : 200px ; height : 150px ; background-color : red; position : fixed; top : 50% ; left : 50% ; transform : translate (-50% , -50% ); z-index : 999 ; } </style >
这里我们需要传入一些 props,类型可以写入 message.d.ts
文件中:
1 2 3 4 5 6 7 8 9 10 interface MessageOptions { title?: string ; message: string ; confirmButtonText?: string ; cancelButtonText?: string ; } interface MessageProps extends MessageOptions { onAction : (action: Action ) => void ; } type Action = "confirm" | "cancel" ;
onAction
的作用是在用户点击按钮后,判断操作类型并执行相应逻辑。
2. 创建命令函数 基于 createApp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import MessageComponent from "./message.vue" ;import { createApp } from "vue" ;export function CustomMessage (options: MessageOptions ): Promise <void > { return new Promise ((resolve, reject ) => { const messageApp = createApp(MessageComponent, { ...options, onAction : action => { if (action === "confirm" ) resolve(); else reject(); app.unmount(); } }); const container = document .createElement("div" ); document .body.appendChild(container); messageApp.mount(container); }); }
注意:
这里 messageApp
不能直接挂载到 body,因为 body 下的 #app
也是通过 createApp
创建后挂载的,两者会产生冲突。
组件内自定义组件生效,第三方组件库不生效,可通过以下方式解决:1 2 3 import ElementPlus from "element-plus" ;messageApp.use(ElementPlus);
基于 createVNode/h 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import MessageComponent from "./message.vue" ;import { h, render } from "vue" ;export function CustomMessage (options: MessageOptions ): Promise <void > { return new Promise ((resolve, reject ) => { const vnode = h(MessageComponent, { ...options, onAction : action => { if (action === "confirm" ) resolve(); else reject(); render(null , document .body); } }); render(vnode, document .body); }); }
这里需要注意的问题与上面相同,不同的是它无法使用第三方组件库。
3. 使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <template > <button @click ="click" > </button > </template > <script setup lang ="ts" > import { CustomMessage } from "./message" ; function click ( ) { CustomMessage({ title : "提示" , message : "这是一个命令式组件" , confirmButtonText : "确定" , cancelButtonText : "取消" }).then(() => { console .log("点击确定" ); }).catch(() => { console .log("点击取消" ); }); } </script >
4. 拓展 如果想要遵循高内聚原则,即把组件和命令函数都放在一个文件中,我们可以使用 defineComponent
方法来实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 import { defineComponent, h } from "vue" ;const MessageComponent = defineComponent( (props: MessageProps ) => { return () => { return h("div" , { class : "box" }, [h(...), h(...), h(...)]) } }, { props : ["title" , "message" , "cancelButtonText" , "confirmButtonText" ] } )
defineComponent
方法的第一个参数要返回整个组件的虚拟 DOM,用 h()
方法的太过繁琐,我们可以用 tsx 来书写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import { defineComponent } from "vue"; import { styled } from "@styils/vue"; const MessageComponent = defineComponent( (props: MessageProps) => { const Box = styled("div", { width: "200px", height: "150px", backgroundColor: "red", position: "fixed", top: "50%", left: "50%", transform: "translate(-50%, -50%)", zIndex: 999 }); return () => { return ( <Box> <div>{ props.title }</div> { props.message } <button onClick={ () => props.onAction('cancel') }>{ props.cancelButtonText }</button> <button onClick={ () => props.onAction('confirm') }>{ props.confirmButtonText }</button> </Box> ) } }, { props: ["title", "message", "cancelButtonText", "confirmButtonText", "onAction"] } )