往期文章:
前言
简单研究了一下 vue3,感觉与 vue2 相比,差别还是蛮大的,新增和去掉了一些API,很多vue2的奇淫巧技无法再使用了,尤其是在改造自定义组件时,差点让我崩溃,这玩意想彻底掌握,还是有很多路要走,本文,将以 Notification 组件为例,描述一下 从 vue2 -> vue3 踩坑之旅
实现效果
准备工作
可以参考 Vue3 - 使用tsx编写组件
采用先整体后细节的方式描述
代码结构
├── components
│ ├── NotifyDemo.vue # notification use example
│ ├── notification # notification custom plugin
│ │ ├── function.tsx # business call the notification. like alert
│ │ ├── index.ts # install plugin out
│ │ └── notification.vue # component
开始
来,跟着我,左手右手一个慢动作~
入口文件注册组件
- vue2
// main.js
import Vue from 'vue'
import App from './App.vue'
import Notification from './components/notification'
// 调用 插件的 install方法
Vue.use(Notification)
new Vue({
render: h => h(App)
}).$mount('#app')
- vue3
// main.ts
import { createApp } from 'vue';
import App from './App.vue';
import Notification from './components/notification'
const app = createApp(App);
// 调用 插件的 install方法
app.use(Notification);
app.mount('#app');
Notification install
- vue2
// notification/index.js
/**
* 通知插件
* 全局注册组件
* 调用方法挂载在全局Vue上,use anywhere,
* 组件内使用:this.$notify({ content: 'test' })
*/
import Notification from './notification.vue'
import notify from './function'
export default (Vue) => {
// 全局注册 component
Vue.component(Notification.name, Notification)
// 将 $notify 方法 挂载在 vue的原型上
Vue.prototype.$notify = notify
}
- vue3
// notification/index.ts
/**
* 通知插件
* 全局注册组件
* vue3 已经去掉 整体导出 Vue,这样无法将方法直接挂载在Vue的原型上了,而是通过config注册
* 使用 组件获取instance.proxy,const { proxy } = getCurrentInstance();
* proxy.$notify({ content: 'test' });
* 不要使用 ctx,生产环境不支持
*/
import { Plugin, App } from 'vue';
import Notification from './notification.vue';
import notify from './function';
// 挂载组件方法
const install = (app: App): App => {
//
app.config.globalProperties.$notify = notify;
app.component(Notification.name, Notification);
return app;
};
export default install as Plugin;
notification.vue
- vue2
<template>
<transition name="fade" @after-leave="afterLeave" @after-enter="afterEnter">
<div class="notification" :style="style" v-show="visible" @mouseenter="clearTimer" @mouseleave="createTimer">
<span class="content">{{ content }}</span>
<a class="btn" @click="handleClose">{{ btn }}</a>
</div>
</transition>
</template>
<script>
export default {
name: 'Notification',
data () {
return {
verticalOffset: 0,
autoClose: 3000,
height: 0,
visible: false,
};
},
props: {
content: {
type: String,
required: true
},
btn: {
type: String,
default: '关闭'
}
},
computed: {
style() {
return {
position: "fixed",
right: "20px",
bottom: `${this.verticalOffset}px`,
};
},
},
mounted() {
this.createTimer();
},
methods: {
handleClose (e) {
e.preventDefault()
this.$emit('close')
},
afterLeave () {
this.$emit('closed')
},
createTimer() {
if (this.autoClose) {
this.timer = setTimeout(() => {
this.visible = false;
}, this.autoClose);
}
},
clearTimer() {
if (this.timer) {
clearTimeout(this.timer);
}
},
afterEnter() {
// debugger // eslint-disable-line
this.height = this.$el.offsetHeight;
}
},
beforeDestory() {
this.clearTimer();
},
}
</script>
<style scoped>
.notification{
display: inline-flex;
background-color: #303030;
color: rgba(255, 255, 255, 1);
align-items: center;
padding: 20px;
min-width: 280px;
box-shadow: 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), 0px 1px 18px 0px rgba(0, 0, 0, 0.12);
flex-wrap: wrap;
transition: all .3s;
}
.content{
padding: 0;
}
.btn{
color: #ff4081;
padding-left: 24px;
margin-left: auto;
cursor: pointer;
}
</style>
- vue3
<template>
<transition name="fade" @after-leave="afterLeave" @after-enter="afterEnter">
<div
v-show="visible"
ref="root"
class="notification"
:style="styleObj"
@mouseenter="clearTimer"
@mouseleave="createTimer"
>
<span class="content">{{ content }}</span>
<a class="btn" @click="handleClose">{{ btn }}</a>
</div>
</transition>
</template>
<script lang="ts">
import {
defineComponent,
reactive,
ref,
toRefs,
computed,
onMounted,
onBeforeUnmount,
} from 'vue';
export default defineComponent({
name: 'Notification',
props: {
content: {
type: String,
required: true,
},
duration: {
type: Number,
default: 3000,
},
btn: {
type: String,
default: '关闭',
},
verticalOffset: {
type: Number,
default: 0,
},
// eslint-disable-next-line vue/require-default-prop
onClosed: Function,
},
setup(props) {
const root = ref(null!);
const state = reactive({
height: 0,
visible: false,
});
const styleObj = computed(() => ({
position: 'fixed',
right: '20px',
bottom: `${props.verticalOffset}px`,
}));
const timer = ref(0);
const handleClose = (e: MouseEvent): void => {
e.preventDefault();
state.visible = false;
};
const afterLeave = () => {
(props as any).onClosed(state.height);
};
const afterEnter = () => {
state.height = (root as any).value.offsetHeight;
};
const createTimer = () => {
if (props.duration) {
timer.value = setTimeout(() => {
state.visible = false;
}, props.duration) as unknown as number;
}
};
const clearTimer = () => {
if (timer.value) {
clearTimeout(timer.value);
timer.value = 0;
}
};
onMounted(() => {
state.visible = true;
createTimer();
});
onBeforeUnmount(() => {
clearTimer();
});
// toRefs 把reactive创建出的数据变更为响应式
return {
...toRefs(state),
root,
styleObj,
handleClose,
afterLeave,
afterEnter,
clearTimer,
createTimer,
};
},
});
</script>
<style scoped>
.notification {
display: inline-flex;
background-color: #303030;
color: rgba(255, 255, 255, 1);
align-items: center;
padding: 20px;
min-width: 280px;
box-shadow: 0px 3px 5px -1px rgba(0, 0, 0, 0.2),
0px 6px 10px 0px rgba(0, 0, 0, 0.14), 0px 1px 18px 0px rgba(0, 0, 0, 0.12);
flex-wrap: wrap;
transition: all 0.3s;
}
.content {
padding: 0;
}
.btn {
color: #ff4081;
padding-left: 24px;
margin-left: auto;
cursor: pointer;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
</style>
function
/**
* 创建通知插件的调用方法
*/
import Vue from 'vue'
import Component from './func-notification'
const NotificationConstructor = Vue.extend(Component)
const instances = []
let seed = 1
const removeInstance = instance => {
if (!instance) return
const len = instances.length
const index = instances.findIndex(inst => instance.id === inst.id)
instances.splice(index, 1)
if (len < 1) return
const removeHeight = instance.vm.height
for (let i = index; i < len - 1; i++) {
instances[i].verticalOffset = parseInt(instances[i].verticalOffset) - removeHeight - 16
}
}
const notify = (options) => {
if (Vue.prototype.$isServer) return
const { autoClose, ...rest } = options
const instance = new NotificationConstructor({
propsData: { ...rest },
data: {
autoClose: autoClose === undefined ? 3000 : autoClose
}
})
const id = `notification_${seed++}`
instance.id = id
instance.vm = instance.$mount()
document.body.appendChild(instance.vm.$el)
instance.visible = true
let verticalOffset = 0
instances.forEach(item => {
verticalOffset += item.$el.offsetHeight + 16
})
verticalOffset += 16
instance.verticalOffset = verticalOffset
instances.push(instance)
instance.vm.$on('closed', () => {
removeInstance(instance)
document.body.removeChild(instance.vm.$el)
instance.vm.$destroy()
})
instance.vm.$on('close', () => {
instance.vm.visible = false
})
return instance.vm
}
export default notify
- vue3
-
- vue3 去掉了 off 等方法,如果想使用,可以使用 mitt 代替,这里换一种方式实现
/**
* 创建通知插件的调用方法
*/
import { createApp, ComponentPublicInstance } from 'vue';
import Notification from './notification.vue';
export type OptionsType = {
content: string;
duration?: number;
btn: string;
};
type InstanceType = {
id: string;
vm: ComponentPublicInstance<any>;
}
const instances: InstanceType[] = [];
let seed = 1;
const removeInstance = (id: string, removeHeight: number): void => {
const index = instances.findIndex(item => item.id === id);
const len = instances.length;
// 删除 instance
instances.splice(index, 1);
if (len < 1) return;
for (let i = index; i < len - 1; i++) {
const inst = instances[i].vm;
inst.bottomOffset = inst.bottomOffset - removeHeight - 16;
}
};
const notify = (options: OptionsType): void => {
const id = `notification_${seed++}`;
const container = document.createElement('div');
document.body.appendChild(container);
let verticalOffset = 16;
instances.forEach(item => {
verticalOffset += item.vm.$el.offsetHeight + 16;
});
const instance = createApp({
data() {
return {
bottomOffset: verticalOffset
}
},
methods: {
closedFunc(height: number):void {
removeInstance(id, height);
document.body.removeChild(container)
instance.unmount();
}
},
render() {
return <Notification { ...options } verticalOffset={this.bottomOffset} onClosed={this.closedFunc} />
}
});
instances.push({
id,
vm: instance.mount(container)
});
};
export default notify;
尾声
代码写的不是很优雅,只是为加深对 vue3 了解,熟悉都谈不上,希望可以帮助到有需要的人。 目前 vue3 体验之旅到这里就结束了,本人在一段时间内也不会太关注 vue3(工作用不到),后续本人会转战到 PhaserJs 的研究,❤️ 期待关注 ❤️
❤️ 加入我们
字节跳动 · 幸福里团队
Nice Leader:高级技术专家、掘金知名专栏作者、Flutter中文网社区创办者、Flutter中文社区开源项目发起人、Github社区知名开发者,是dio、fly、dsBridge等多个知名开源项目作者
期待您的加入,一起用技术改变生活!!!
招聘链接: job.toutiao.com/s/JHjRX8B