全局提示组件在前端中算是比较重要的一个组件,他能极大提高用户交互体验。当然,在社区比较有人气的 UI 组件库也必有他身影。
只不过组件库叫法可能不太相同,有叫 message
、toast
、alert
等,但就是这么一玩意。
拿 ant-design-vue
组件库为例
其核心代码
message.info('This is a normal message')
复制代码
他是API
的方式进行调用组件,以平常使用components
注册组件,之后在template
中使用的方式不太相同
想要实现这个,其实并不困难
但在之前我想声明一下:ant-design-vue 的 message 做个type分类,有info、success、error等。但为了读者方便理解,我们统一就用message来进行调用,读者可以根据本文提供的demo源码自行进行调整(贴一下 ant-design-vue 源码)
首先,我们在 components 目录下创建 message 目录,同时创建 Message.vue 和 index.js 文件
然后在 Message.vue 中写
<!-- Message.vue -->
<template>
<div>{{ content }}</div>
</template>
<script>
export default {
name: 'Message',
props: {
content: {
type: String,
default: ''
}
}
}
</script>
复制代码
再者在 index.js 中写
// index.js
import { render, createVNode } from 'vue'
import Message from './Message.vue'
export default function message (content) {
const div = document.createElement('div')
const vm = createVNode(Message, {
content
})
render(vm, div)
document.body.appendChild(div)
}
复制代码
最后在 app.vue 引入使用
<!-- app.vue -->
<template>
</template>
<script>
import message from './components/message'
export default{
setup() {
message('消息组件1')
message('消息组件2')
}
}
</script>
复制代码
效果:
ok,这样就实现了以API
的方式进行调用组件
其核心代码其实只有以下
const div = document.createElement('div')
const vm = createVNode(Message, {
content
})
render(vm, div)
document.body.appendChild(div)
复制代码
createVNode
可以认为就是h
函数,支持直接转成虚拟dom
对象
再去调用 render
,渲染至div
下,再将div
插入body
中
由于我们做的是全局组件,应该不被任何因素干扰,比如 vue-router
但是上面例子我们会发现,我们只要调用一个 message 方法,就要创建一个div插入至body
,这显然不是符合vue数据驱动视图的理念
那,接下里进行改造
先从 Message.vue 文件开始
我们先定义一个消息列表messages
,之后提供添加 add
和删除delete
消息列两方法,再暴露add
方法出去
当然,删除消息是需要根据id进行删除,可不能瞎删
<!-- Message.vue -->
<script>
import { ref, unref } from 'vue'
export default {
name: 'Message',
setup (props, { expose }) {
// 消息列表
const messages = ref([])
let id = 0
// 生成id
const uuid = () => `message_${id++}`
// 添加消息对象
const add = (message) => {
const id = uuid()
const _message = {
...message,
id
}
unref(messages).push(_message)
const { duration = 3 } = message
// 设置定时器
const timer = setTimeout(() => {
clearTimeout(timer)
remove(id)
}, duration * 1000)
}
// 根据 id 删除消息对象
const remove = (id) => {
messages.value = unref(messages).filter(message => message.id !== id)
}
// 暴露出add 和 remove
expose({
add
})
return {
messages
}
}
}
</script>
复制代码
用v-for
把消息列表渲染出来,同时使用transition-group
做一些列表过渡动画
<!-- Message.vue -->
<template>
<transition-group
class="message"
tag="div"
>
<div
class="message-content"
v-for="message in messages"
:key="message.id"
>
{{ message.content }}
</div>
</transition-group>
</template>
复制代码
再编写一点消息列表样式和其弹出动画样式
<!-- Message.vue -->
<style scoped>
.message {
position: fixed;
z-index: 999;
top: 10px;
left: 50%;
transform: translateX(-50%);
}
.message-content {
padding: 8px 16px;
border-radius: 3px;
box-shadow: 0 1px 6px rgba(0, 0, 0, .2);
background: #fff;
margin-bottom: 20px;
}
.v-enter-active,
.-leave-active {
transition: all 200ms ease-in;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
transform: translateY(-30px);
}
</style>
复制代码
再改造一下入口文件
之前我们发现在 app.vue 中调用一次 message方法,就会一次 dom 操作,那我们使用闭包写一个单例模式进行改造
再者我们在 Message.vue 暴露出add
的方法可以直接进行操作消息列表
// index.js
import { render, createVNode } from 'vue'
import Message from './Message.vue'
let vm
// 使用单例模式,不再重新插入body
function getMessageInstance () {
if (vm) return
const div = document.createElement('div')
vm = createVNode(Message)
render(vm, div)
document.body.appendChild(div)
}
export default function message (content = '', duration) {
getMessageInstance()
vm.component.exposed.add({
content,
duration
})
}
复制代码
最后在 app.vue 文件中随便编写一些测试代码
<!-- app.vue -->
<template>
<button @click="onClick">测试</button>
</template>
<script>
import message from './components/message'
export default{
setup() {
message('消息组件1', 4)
message('消息组件2', 2)
let index = 3
const onClick = () => {
message(`消息组件${index++}`)
}
return {
onClick
}
}
}
</script>
复制代码
看看最终效果
好,这就是全局提示组件设计大致思路。
回顾一下会发现,其实Vue的组件开发依旧绕不开 JavaScript,可见 JavaScript 在前端的份量。
感谢阅读