前言
突然想弄个组件库看看。现在vue默认安装都是3.x版本了。那就尝试搭建个vue3组件库。感兴趣的不妨一块试一试。本篇不涉及太多vue新语法,放心尝试。当然对于想了解vue新语法的童鞋,前面有两篇文章vue3之Composition Api和vue3之Reactivity Api或许能帮助你,嘿嘿。开搞。
效果
我搞完了。来上图看看效果。
[图1]
[图2]
图1目录,图2是运行的效果。
实践
组件
下面一块看看具体是怎么实现的。先定义个button组件,就像我们平时写代码一样
<template>
<div>这是一个button</div>
</template>
<script>
export default {
name: 'MButton',
};
</script>
组件写好了,如果你想全局都能用该怎么做呢?有些同学会说调用vue.component传递组件和组件名,没错就是这么实现的。看代码
import Button from './button.vue';
console.log(Button);
Button.install = function (Vue) {
const version = Number(Vue.version.split('.')[0]);
console.log(version, 'version');
if (version !== 3) {
console.warn('This plugin requires Vue 3');
}
console.log('调用');
Vue.component(Button.name, Button);
};
export default Button;
接着在main.js文件引入再调用use就实现了一个组件的全局注册。接下来有几个问题不知道你是否思考过:
1、平时写个组件为什么要写name属性,不写好像也没影响。
2、我们引入的组件是什么类型?
3、既可以单独引入使用也可以一次全部注册使用,是为什么呢?
有了疑问就去寻找答案。
再多问一点,这个install方法是干嘛的?为什么要写这个install方法呢?一脸懵逼,那怎么办,来一块看看。\
原理
下面是use方法的实现。
use(plugin, ...options) {
if (installedPlugins.has(plugin)) {
warn(`Plugin has already been applied to target app.`);
}
else if (plugin && shared.isFunction(plugin.install)) {
installedPlugins.add(plugin);
plugin.install(app, ...options);
}
else if (shared.isFunction(plugin)) {
installedPlugins.add(plugin);
plugin(app, ...options);
}
else {
warn(`A plugin must either be a function or an object with an "install" ` + `function.`);
}
return app;
},
我解释下实现流程:
首先检查了插件池(组件)是否存在,存在就提醒Plugin has already been applied to target app.可以重复注册试试。其次看use方法是否传入了插件,如果传入了插件并且有个install方法,就加入插件池并且执行install方法。如果传入的插件是一个方法,就加入插件池并且执行方法,都不是就提示A plugin must either be a function or an object with an "install" function.可以试试直接调use方法不传递参数或者不按要求传递。以下是伪代码
use(plugin, ...options) {
if (插件池存在plugin) {
提示(`Plugin has already been applied to target app.`);
}
else if (插件 && 插件有install方法) {
把插件加入插件池
执行install方法
}
else if (只传入了方法) {
加入插件池
执行传递的方法
}
else {
啥都不是就提示(`A plugin must either be a function or an object with an "install" ` + `function.`);
}
把实例返回出去,就是为啥能够链式调用
},
看到这我们就明白了,想要直接使用组件必须要给组件挂载一个install方法去注册组件,既然挂载了install方法,那组件类型就是object了。打印如下:
注册组件的时候调用
Vue.component(Button.name, Button)所以name属性还是有用的。
至此我们搞明白了单个组件的注册。那像ant-design-vue、element-plus这种拥有那么多组件不可能一个一个引入吧,事实也确实不是。他们是如何做的呢?
有兴趣可以看看,这里贴一下我的实现:\
import Button from './button/index';
import Icon from './icon/index';
import Message from './message/index';
const components = [Button, Icon];
export function install(Vue) {
components.map((component) => {
Vue.use(component);
});
Vue.config.globalProperties.$mui = {
message: Message,
};
}
export default {
version: '3.0.0-alpha.1',
install,
Button,
Icon
}
好像和单个注册没啥不同,也确实是,调用use方法执行install方法循环注册,就实现了一次注册全局使用。这里我再全局挂载了message方法,这样就可以实现this.$mui.message方法了。当然你也可以挂载全局变量之类的如果有需要的话。至于api不熟悉的建议看官网文档,最好过一遍,帮助加深对vue的理解应用配置。
message实现
把message的实现也贴一下
import Main from './message.vue';
import { createApp, h } from 'vue';
function toConstructor(component) {
console.log(component, 'component');
return function (props, children) {
console.log(children); // 这是一个弹窗展示
// id: "message_1"
// message: "这是一个弹窗展示"
// type: "success"
// verticalOffset: 12
// zIndex: 2000
console.log(props, 'props, children');
const div = document.createElement('div');
const { getContainer } = props;
if (getContainer) {
const root = getContainer();
root.appendChild(div);
} else {
document.body.appendChild(div);
}
let app;
function destroyed() {
app.unmount(div);
}
app = createApp({
unmounted() {
div.parentNode?.removeChild(div);
},
render() {
return h(
component,
{
...props,
destroyed,
ref: 'instance',
},
children
);
},
});
const $ = app.mount(div);
console.log(app);
console.log($);
return {
app,
$,
context: $.$refs.instance,
};
};
}
const MessageConstructor = toConstructor(Main);
const instances = [];
let seed = 1;
const spacing = 12;
const Message = function (options) {
/**
* {message: "这是一个弹窗展示",
* type: "success"}
*
*/
console.log(options, 'options');
const id = 'message_' + seed++;
let verticalOffset = options.offset || 0;
const children = options.message;
instances.forEach((item) => {
verticalOffset += item.$el.offsetHeight + spacing;
});
verticalOffset += spacing;
const { context } = MessageConstructor(
{
...options,
message: options.message,
id,
zIndex: 2000,
verticalOffset,
},
children
);
context.dom = context.$el;
instances.push(context);
return context;
}
export default Message;
看完代码有些同学估计有点懵,这里大致说明下。调用MessageConstructor其实是调的toConstructor的返回函数,传递了props和children参数,在这个函数里面创建了一个div节点并且创建了一个vue实例挂载到这个div节点。可以自己打印看看输出结果。至于createApp都做了什么,总的来说就是这里面实现了一个单例renderer,形成这个单例的过程又是一系列包装最终来到一个createAppAPI方法注入一系列属性方法最后返回app,其中上面我们说的use方法以及component方法等就在这个createAppAPI方法里面,更多细节...不妨深入研究一下。至此,组件库的神秘面纱已被我们揭开,看上去高大上的东西其实你也可以搞一下,行动起来。
总结
本篇以简单的三个组件演示为出发点,但是实际上我们要封装的组件是以业务为出发点,会更加复杂和通用,具体实现看业务需要。希望对有组件库搭建想法却不知从何做起的同学有所启发和帮助。后续代码上传GitHub仓库。
附录
v3.cn.vuejs.org/api/applica…
www.vue-js.com/topic/602e2…
blog.csdn.net/m0_37846579…