vue实现js调用式组件
vue实现js调用式组件
前言
本文主要讲解vue2
和vue3
如何创建js调用式组件
。其中vue3
包含了两种实现方案
vue2 创建 js 调用式组件
关键实现函数是Vue.extend
,通过Vue.extend
可以创建一个子类组件出来
我们以实现一个loading-bar
组件为例,步骤如下:
- 新建一个
.vue
文件,在里面编写loading-bar
组件,实现你想要的的UI
效果
<template>
<div class="loading-bar">
<div
:style="{ transform: `translateX(-${100 - totalProgress}%)` }"
class="loading-bar-progress"
:class="{ 'is-error': isError }"
>
<div class="loading-bar-peg"></div>
</div>
<div class="loading-bar-spinner" v-if="showSpinner">
<div
:style="{ 'animation-timing-function': easing }"
class="loading-bar-icon"
:class="{ 'is-icon-error': isError }"
></div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
// 加载器(转圈圈的那个东西),运动形式
easing: "linear",
// 是否为错误类型
isError: false,
// 显示加载器
showSpinner: true,
// 加载的总进度
totalProgress: 0,
// 每次前进的百分比
percentNum: 0,
// 加载速度
speed: 5,
};
},
};
</script>
- 通过
Vue.extend
继承loading-bar
组件
import LoadingBar from "./loading-bar.vue";
const LoadingBarConstructor = Vue.extend(LoadingBar);
LoadingBarConstructor
是一个构造函数(类),我们可以添加一些额外的方法
// 设置全局配置信息
LoadingBarConstructor.prototype.config = function config(options) {
//todo
};
// 初始化加载进度条
LoadingBarConstructor.prototype.init = function init() {
//todo
};
// 显示加载进度条
LoadingBarConstructor.prototype.start = function start() {
//todo
};
// 关闭加载进度条
LoadingBarConstructor.prototype.end = function end() {
//todo
};
// 显示错误进度条
LoadingBarConstructor.prototype.error = function error() {
//todo
};
- 初始化 vue 实例
const instance = new LoadingBarConstructor();
instance.totalProgress = 10;
instance
实例就是一个vue
实例,你可以通过这个实例访问或者修改组件中的easing
,isError
,totalProgress
等响应式数据
- 生成并挂载 DOM
const vm = instance.$mount();
document.body.appendChild(vm.$el);
instance.$mount()
是生成DOM
的,可通过vm.$el
访问DOM
最终还需要把生成出来的DOM
挂载到页面上
- 销毁
document.body.removeChild(vm.$el);
instance.$destroy();
instance = null;
销毁的时候需要先移除页面上的DOM
元素,然后在进行实例的销毁
- 最终效果
对于生成并挂载 DOM
,销毁
这些流程,我们可以进行一些封装,对外屏蔽一些实现的细节,最终代码如下:
const LoadingBarConstructor = Vue.extend(LoadingBar);
LoadingBarConstructor.prototype.destroyTimer = function destroyTimer() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
};
LoadingBarConstructor.prototype.destroyRemoveTimer =
function destroyRemoveTimer() {
if (this.removeTimer) {
clearTimeout(this.removeTimer);
this.removeTimer = null;
}
};
// 设置全局配置信息
LoadingBarConstructor.prototype.config = function config(options) {
Object.keys(options).forEach((key) => {
if (key === "isError" || key === "totalProgress") {
return;
}
this[key] = options[key];
});
};
// 初始化加载进度条
LoadingBarConstructor.prototype.init = function init() {
this.destroyTimer();
this.totalProgress = 0;
this.isError = false;
this.vm = this.$mount();
document.body.appendChild(this.vm.$el);
return this;
};
// 显示加载进度条
LoadingBarConstructor.prototype.start = function start() {
this.init();
this.timer = setInterval(() => {
// 小于90的时候才进行前进
if (this.totalProgress < 90) {
this.totalProgress += (this.percentNum || Math.random()) * this.speed;
}
}, 100);
};
// 关闭加载进度条
LoadingBarConstructor.prototype.end = function end() {
if (!timer) {
this.init();
}
// 先把总进度设置为100,让他走完
this.totalProgress = 100;
this.destroyRemoveTimer();
this.removeTimer = setTimeout(() => {
this.destroyTimer();
document.body.removeChild(this.vm.$el);
}, 200);
};
// 显示错误进度条
LoadingBarConstructor.prototype.error = function error() {
this.end();
this.totalProgress = 100;
this.isError = true;
};
- 使用方式
const instance = new LoadingBarConstructor();
instance.start();
setTimeout(() => {
instance.end();
}, 3000);
vue3 创建 js 调用式组件
在vue3
中,已经废弃了vue.extend
全局 api。我们可以通过createApp
或者createVNode
+render
的方式去实现
createApp
使用过 vue3
的同学应该知道,createApp
是用来创建一个app
实例的
我们以实现一个loading-bar
组件为例,步骤如下:
- 新建一个
.vue
文件,编写loading-bar
组件,实现想要的UI
效果
<template>
<div class="loading-bar">
<div
:style="{ transform: `translateX(-${100 - totalProgress}%)` }"
class="loading-bar-progress"
:class="{ 'is-error': isError }"
>
<div class="loading-bar-peg"></div>
</div>
<div class="loading-bar-spinner" v-if="showSpinner">
<div
:style="{ 'animation-timing-function': easing }"
class="loading-bar-icon"
:class="{ 'is-icon-error': isError }"
></div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
export default defineComponent({
setup() {
// 加载器(转圈圈的那个东西),运动形式
const easing = ref("linear");
// 是否为错误类型
const isError = ref(false);
// 显示加载器
const showSpinner = ref(true);
// 加载的总进度
const totalProgress = ref(0);
// 每次前进的百分比
const percentNum = ref(0);
// 加载速度
const speed = ref(5);
return {
easing,
isError,
showSpinner,
totalProgress,
percentNum,
speed,
};
},
});
</script>
- 创建
app
实例
import LoadingBar from "./index.vue";
const app = createApp(LoadingBar);
- 创建
vue
实例
const rootContainer = document.createElement("div");
const vm = app.mount(rootContainer);
vm.isError = true;
vm
实例就是vue
实例,可以通过这个实例访问或者修改组件中的easing
,isError
,totalProgress
等响应式数据
注意:app.mount 中的第一个参数要求必须传入一个根元素,但是这个根元素不能是 document.body
- 挂载 DOM
document.body.appendChild(rootContainer);
// 或者
// document.body.appendChild(vm.$el);
在挂在 DOM 的时候,你可以选择把根元素rootContainer
挂在上去,或者把组件的 DOM 元素挂在上去。区别只是在于rootContainer
在组件的 DOM 元素外层多了一个div
元素
- 销毁
app.unmount();
rootContainer.remove();
销毁的时候,直接调用app
实例上面的unmount
函数进行销毁即可,同时需要把 DOM 进行移除
- 最终效果
根据上面的步骤,我们进行一次封装,对外屏蔽一些实现的细节,最终代码如下:
import { isPlainObject } from "@packages/utils";
import { App, createApp } from "vue";
import LoadingBar from "./index.vue";
interface RootData {
easing?: string;
isError?: boolean;
showSpinner?: boolean;
totalProgress?: number;
percentNum?: number;
speed?: number;
}
class LoadingBarConstructor {
private app: App | null = null;
private vm: any | null = null;
private timer: number | null = null;
private removeTimer: number | null = null;
private rootContainer: HTMLElement | null = null;
private options: RootData = {};
private init(options: RootData = {}) {
this.destroy();
this.app = createApp(LoadingBar);
this.rootContainer = document.createElement("div");
// 这个是为了获取组件实例,方便后面对组件变量动态操作
this.vm = this.app.mount(this.rootContainer);
const config: any = {
...this.options,
...options,
};
for (const key in config) {
if (Object.prototype.hasOwnProperty.call(config, key)) {
this.vm[key] = config[key];
}
}
document.body.appendChild(this.rootContainer);
return this;
}
private destroy() {
this.app?.unmount();
this.rootContainer?.remove();
this.app = null;
this.vm = null;
this.rootContainer = null;
}
// 设置全局配置信息
config(options: RootData) {
if (isPlainObject(options)) {
this.options = options;
}
return this;
}
private destroyTimer() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
}
private destroyRemoveTimer() {
if (this.removeTimer) {
clearTimeout(this.removeTimer);
this.removeTimer = null;
}
}
// 显示加载进度条
start(options: RootData) {
this.init(options);
this.timer = window.setInterval(() => {
// 小于90的时候才进行前进
if (this.vm.totalProgress < 90) {
this.vm.totalProgress +=
(this.vm.percentNum || Math.random()) * this.vm.speed;
}
}, 100);
return this;
}
// 关闭加载进度条
end() {
if (!this.timer) {
this.init();
}
// 先把总进度设置为100,让他走完
this.vm.totalProgress = 100;
this.destroyRemoveTimer();
this.removeTimer = window.setTimeout(() => {
this.destroyTimer();
this.destroy();
}, 200);
return this;
}
// 显示错误进度条
error() {
this.end();
this.vm.totalProgress = 100;
this.vm.isError = true;
return this;
}
}
- 使用方式
const instance = new LoadingBarConstructor();
instance.start();
setTimeout(() => {
instance.end();
}, 3000);
createVNode + render
createVNode
和render
在官方文档中提及的比较少。通过阅读element-plus
源码可以发现element
是通过createVNode
和render
来实现 js 调用式组件。
createVNode(component,props)
:第一个参数为组件,第二个参数为组件的props
render(vm,rootContainer)
:第一个参数为VNode
,第二个参数为组件的根元素
我们以实现一个loading
组件为例,步骤如下:
- 创建组件
因为我们需要访问或者修改组件的响应式数据,所以我们不能通过.vue
文件来创建组件,createVNode
是无法获取得到组件的实例
import { defineComponent, reactive, Transition } from "vue";
const data = reactive({
// 加载文案
text: "",
// 是否全屏
fullscreen: true,
// 控制是否显示
visible: false,
// 背景
background: "",
// loading颜色
loadingColor: "",
// 文本颜色
textColor: "",
});
const destroySelf = () => {
// todo
};
const loadingComponent = defineComponent({
setup() {
return { data, destroySelf };
},
render() {
return (
<Transition name="fade" onAfterLeave={destroySelf}>
{data.visible ? (
<div
class={["loading-mask", { "is-fullscreen": data.fullscreen }]}
style={{ backgroundColor: data.background || "" }}
>
<div class="loading-content">
<span
class="loading-icon"
style={{
"border-top-color": data.loadingColor || "",
"border-right-color": data.loadingColor || "",
}}
></span>
{data.text ? (
<span
style={{ color: data.textColor || "" }}
class="loading-text"
>
{data.text}
</span>
) : null}
</div>
</div>
) : null}
</Transition>
);
},
});
- 生成并挂载 DOM
const div = document.createElement("div");
const vm = createVNode(loadingComponent);
render(vm, div);
document.body.appendChild(vm.el as HTMLElement);
// 或者
// document.body.appendChild(div);
在render
函数中,第二个参数不能是document.body
要注意的是vm
是VNode
实例,并不是组件的实例
- 销毁
document.body.removeChild(vm.el as HTMLElement);
render(null, div);
- 最终效果
根据上面的步骤,我们进行一次封装,对外屏蔽一些实现的细节,最终代码如下:
import { pickObject } from "@packages/utils";
import {
createVNode,
defineComponent,
reactive,
render,
Transition,
} from "vue";
import { Options } from "./types";
const createComponent = () => {
const data = reactive({
// 加载文案
text: "",
// 是否全屏
fullscreen: true,
// 控制是否显示
visible: false,
// 背景
background: "",
// loading颜色
loadingColor: "",
// 文本颜色
textColor: "",
});
const div = document.createElement("div");
const destroySelf = () => {
document.body.removeChild(vm.el as HTMLElement);
render(null, div);
};
const loadingComponent = defineComponent({
setup() {
return { data, destroySelf };
},
render() {
return (
<Transition name="fade" onAfterLeave={destroySelf}>
{data.visible ? (
<div
class={["loading-mask", { "is-fullscreen": data.fullscreen }]}
style={{ backgroundColor: data.background || "" }}
>
<div class="loading-content">
<span
class="loading-icon"
style={{
"border-top-color": data.loadingColor || "",
"border-right-color": data.loadingColor || "",
}}
></span>
{data.text ? (
<span
style={{ color: data.textColor || "" }}
class="loading-text"
>
{data.text}
</span>
) : null}
</div>
</div>
) : null}
</Transition>
);
},
});
const vm = createVNode(loadingComponent);
render(vm, div);
const open = (options?: Options) => {
if (!options) {
return;
}
const object = pickObject(options, [
"text",
"fullscreen",
"background",
"loadingColor",
"textColor",
"fullscreen",
]);
// 修改响应式数据
Object.keys(object).forEach((key) => {
(data as any)[key] = object[key as keyof Options];
});
// 已经是显示状态就不需要走下面了
if (data.visible) {
return;
}
// 添加上去
document.body.appendChild(vm.el as HTMLElement);
// 显示
data.visible = true;
};
const close = () => {
if (!data.visible) {
return;
}
data.visible = false;
};
return {
open,
close,
};
};
- 使用
const instance = createComponent();
instance.open();
setTimeout(() => {
instance.close();
}, 3000);
两种方式对比
如果只是实现一个js调用的组件,推荐使用createVNode
+render
的方式。因为createApp
会生成一个app
实例,会初始化一些无关要紧的东西,比如app.use
,app.config.globalProperties
等属性和函数,这些东西根本就用不上,每次都会进行一次初始化,会造成不必要的性能浪费
更多推荐
所有评论(0)