浅尝封装命令式组件
在项目开发中,我们经常需要封装通用性的功能,比如弹窗组件。通用性功能的特点在于我们需要着重考虑使用的便捷度,而不是开发的便捷度。尽管开发过程可能会很复杂,但由于我们只需要进行一次开发,却可以在项目中无数次使用。
让我们来思考一个简单的算术问题。假设开发成本是100,非常高昂,但是使用成本只有1。由于开发只需要一次,而使用却需要无数次,那么在使用100次之后的总成本就是200。然而,如果我们倒过来考虑,开发成本只有1,但是使用成本却是100。这样使用100次之后,总成本就变成了10001。因此,我们应该更加关注使用的便捷度,而不是开发的便捷度。
组件化方式是将弹窗功能封装为一个独立的组件,然后在需要使用的地方引入该组件。这种方式需要在组件之间进行导入和数据传递。您提供的示例代码展示了在组件A中导入并使用组件B的过程。这种方式可以实现组件的复用,但在使用时需要进行额外的导入和配置。
那么在Vue中如何实现弹窗组件呢?如果使用组件化的方式实现,开发过程可能会相对简单,但使用起来却可能比较复杂。通常情况下,使用组件化方式,我们会将弹窗功能封装为一个独立的组件,然后在需要使用的地方引入该组件。
以下是一个示例,演示了在组件A中导入组件B的过程:
- 在A组件导入B组件:
- B组件:
可以看到为了使用B组件,我们在A组件中先是导入B组件,然后导入Vue中的ref函数定义msg
、showMsg
这2个响应式对象,然后再给<MessageBox />
这个组件使用v-if
判断showMsg
、:bind
来绑定传递给B组件的数据;
另一种方式是采用命令式
封装的方式,类似于React的开发方式。正好最近我看到了一个使用React编写的项目,里面的代码非常简洁,而且都是使用函数式组件编写的。虽然我对React不太了解,但是我想能否参考React的开发方式,在Vue中简单实现一个命令式封装的组件。于是我写了下面这篇文章来讨论这个想法。
首先,根据React项目内使用组件的方法,我们可以直接调用一个外部引入的方法来使用组件。例如:
import showMsg from "@/utils/showMsg"
const clickHandle = () => {
// 关闭弹窗
showMsg("传递给B组件的数据", close => {
console.log("点击了确定")
// TODO: Something
close() // 关闭弹窗
})
}
在上述代码中,我们通过调用showMsg
方法来显示弹窗。这个方法接收两个参数,一个是传递给B组件的数据,另一个是一个回调函数 close
,用于关闭弹窗。如下图:
接下来,我们可以实现一个简单的弹窗组件 showMsg
,它可以接收传递给弹窗的数据,并提供确定按钮的点击事件回调。
import { createApp } from 'vue'
import MessageBox from '@/components/MessageBox.vue'
/**
*
* @param {String} msg - 外部传入的数据
* @param {Function} clickHandler - 点击按钮时的回调函数
*/
const showMsg = (msg, clickHandler) => {
// 由于在main.js文件中,#App已经被使用了,这里选择再创建一个div
const div = document.createElement('div')
document.body.appendChild(div)
const app = createApp(MessageBox)
app.mount(div)
}
export default showMsg
在这个示例中,我们定义了一个 showMsg
组件,它接收一个 msg
属性作为弹窗内容。点击确定按钮时,通过 $emit
方法触发了一个 close
事件,通知父组件关闭弹窗。
现在,我们可以在项目中引入并使用这个弹窗组件了。
首先,我们需要在项目中创建一个 utils
文件夹,并在其中创建一个 showMsg.js
文件,用于封装弹窗的显示逻辑。
接下来的问题是,要怎么传递组件的属性和事件,因为原来MessageBox
这个组件内有msg属性和通过emit通知外部组件更新的自定义方法。
查阅读Vue文档后得知,createApp()方法原来有第二个可选参数可以配置。这么一来,在外部调用showMsg
这个方法,并在方法内传递了数据,就可以通过配置对象的获取外部调用时传入的msg
,然后渲染到使用createApp()
生成的div
元素上,于是乎showMsg.js文件内的代码便成了如下:
const showMsg = (msg, clickHandler) => {
...
const app = createApp(MessageBox, {
msg
})
app.mount(div)
}
查看效果:
OK,接下来就是处理供外部使用的自定义emit事件,在Vue 3中,使用defineEmits来定义组件的自定义事件时,要使用camelCase(驼峰命名)来定义,就可以在createApp
这个方法的配置对象中,定义供外部调用的emit方法。
如下面的测试例子:
const showMsg = (msg, clickHandler) => {
...
const app = createApp(MessageBox, {
msg,
onClick() {
console.log('触发点击事件')
}
})
console.log(app.onClick)
app.mount(div)
}
到浏览器查看效果:
既然以上的逻辑行得通,那么接下就需要获取在调用
showMsg
这个方法时,传入的close
这个参数,然后判断有没这个这个值,有值才将对话框关闭:
const showMsg = (msg, clickHandler) => {
...
onClick() {
clickHandler & clickHandler(() => {
app.unmount()
div.remove()
})
}
}
为了验证,可以在调用showMsg方法的地方,打印close这个参数:
这样一来,在将来要使用这个弹窗组件的时候,只需要使用命令式的编程方式就可以调用这个弹窗组件。
然而,仅仅到这一步是不够的。在代码的顶部,我们通过import
语句导入了MessageBox.vue
组件。这导致弹窗功能分散在了两个文件中,不符合高内聚原则。本来这是同一个功能,却分成了两个文件,这显得有些冗余。因此,最好将它们都写在同一个文件中,现在我们需要想办法将组件内的代码放到这个 JavaScript 文件中。
在普通的 JavaScript 文件中,无法直接解析单文件组件内的代码,有时候组件写久了,换成普通代码就不会写了。
寻根溯本,其实组件的本质就是对象,既然是对象,那我们就可以在.js
文件通过对象的方式,定义MessageBox
这个组件:
const MessageBox = {
props: {
msg: {
type: String,
required: true
}
}
}
然后我们来讨论模板部分。回顾一下 Vue 的原理,模板是通过 render
函数进行渲染的。一旦我们了解了这个原理,就可以在 MessageBox
对象内调用 render
方法,并返回模板。考虑到使用虚拟节点的写法可能有些复杂,我建议在这里使用 JSX 语法进行编写。
接下来,我们可以愉快地进行组件视图 (CV) 的编写,并结合 JSX 语法修改 render
函数内的模板
const MessageBox = {
props: {
msg: {
type: String,
required: true
}
},
render() {
return (
<div class="modal">
<div class="box">
<div class="text">{msg}</div>
<ElButton type="primary" click={emit('click')}>按钮</ElButton>
</div>
</div>
)
}
}
当我保存上述代码后,控制台和浏览器同事抛出了一个错误:
通过与 ChatGPT 激烈讨论后,了解到在纯 JavaScript 文件(.js 后缀)中,不能直接编写 JSX
代码,因为 JavaScript 本身不支持 JSX 语法。JSX 是一种特殊的语法扩展,需要通过构建工具(例如 Babel)将其转换为普通的 JavaScript 代码。为了解决这个问题,你需要使用 @vitejs/plugin-vue
和 @vitejs/plugin-vue-jsx
这两个插件。
具体步骤:
- 安装
npm install --save-dev @vitejs/plugin-vue-jsx @vitejs/plugin-vue
2.配置vite.config.js
文件:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
export default defineConfig({
plugins: [
vue(),
vueJsx()
]
})
- 然后将showMsg.js的后缀名改为
jsx
接下来,在调用 render
函数时,我们可以从 ctx
参数中获取 msg
和 emit
这两个值。可以在 render
函数内打印一下 ctx
这个参数的值。如下所示:
以上代码通过
ctx
可以获取到 props
和 msg
这两个值。在模板中,我们可以使用 $props.msg
来访问 msg
的值。并且使用 $emit
来触发 onClick
事件。
render(ctx) {
const { $props, $emit } = ctx
return (
<div class="modal">
<div class="box">
<div class="text">{$props.msg}</div>
<ElButton type="primary" click={$emit('onClick')}>按钮</ElButton>
</div>
</div>
)
}
回到浏览器来看下效果:
现在需要在 JavaScript 文件中编写样式。这里可以使用 @styils/vue
库来实现类似于 Styled Components 的功能。具体操作步骤如下:
- 安装
stylus
和stylus-loader
:
npm install stylus stylus-loader --save-dev
- 在
showMsg.jsx
文件中引入styled
:
import { styled } from '@styils/vue'
- 将
MessageBox.vue
文件中的样式复制到showMsg.jsx
文件中,并对样式中的-
使用 camelCase 的形式进行修改。然后使用复制的 HTML 代码替换原始的模板部分。具体代码如下:
const DivModal = styled('div', {
position: 'fixed',
width: '100%',
height: '100%',
left: 0,
top: 0,
Zindex: 99,
background: '#00000050',
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
})
const DivBox =styled('div',{
minWidth: '30%',
background: '#fff',
padding: '10px 0',
color: '#333',
borderRadius: '10px',
boxShadow: '0 0 3px #00000080',
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
})
const DivText = styled('div', {
marginBottom: '1em'
})
const MessageBox = {
props: {
msg: {
type: String,
required: true
}
},
render(ctx) {
const { $props, $emit } = ctx
return (
<DivModal>
<DivBox>
<DivText>{$props.msg}</DivText>
<ElButton type="primary" onClick={$emit('click')}>按钮</ElButton>
</DivBox>
</DivModal>
)
}
}
最后,回到浏览器进行测试:
可以看到结构和样式都生成了。
在本文中,我们探讨了在Vue.js中实现弹窗组件的两种常见方式:组件化方式和命令式封装方式。这两种方式都可以实现弹窗功能的封装和复用,但在使用方式和实现细节上有所区别。
首先,组件化方式是将弹窗功能封装为一个独立的组件。我们可以创建一个Modal
组件,在需要使用弹窗的地方引入该组件,并通过v-if
来控制弹窗的显示与隐藏。这种方式能够实现组件的复用,并且能够通过事件来处理弹窗的关闭后逻辑。
其次,命令式封装方式是通过调用一个外部引入的函数来显示弹窗。我们可以封装一个showMsg
函数,接收弹窗的内容和确定按钮回调函数作为参数,在函数内部实现弹窗的显示和逻辑处理。这种方式比较简洁,但相对来说较为复杂,不太适用于需要复用弹窗组件的情况。
无论选择哪种方式,都可以根据项目需求和个人偏好来决定。如果需要更高的复用性和灵活性,建议使用组件化方式;如果希望使用更简洁的方式来调用弹窗,可以选择命令式封装方式。
需要注意的是,以上示例仅为简单示范,实际项目中可能需要根据具体需求进行更加复杂的弹窗组件设计和实现。同时,还可以使用Vue.js的插件机制或第三方库来简化弹窗组件的开发和使用过程。
转载自:https://juejin.cn/post/7251515125048213541