VUE3组件库

498 阅读5分钟

前言

突然想弄个组件库看看。现在vue默认安装都是3.x版本了。那就尝试搭建个vue3组件库。感兴趣的不妨一块试一试。本篇不涉及太多vue新语法,放心尝试。当然对于想了解vue新语法的童鞋,前面有两篇文章vue3之Composition Apivue3之Reactivity Api或许能帮助你,嘿嘿。开搞。

效果

我搞完了。来上图看看效果。
image.png
[图1] image.png
[图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了。打印如下:
image.png 注册组件的时候调用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…