Vue+ts插件编写

1. 为什么要写插件

​ 一开始我也是以为Vue插件离我的日常开发很遥远的,但直到有一天公司用的组件库换了,换成了MD风格的Vuetify。

​ 这个组件库优点就是好看、且各种插槽props可以充分满足自定义需求;缺点也很明显:在用惯了Element的人看来,这个组件库不但缺少了很多全局函数,如$message,而且自定义的插槽、Prop太多了,需要一段时间熟悉。其中尤其是缺少了函数式组件让我很难受,虽然可以通过顶层写个message组件来解决,但用起来还是太麻烦了。

​ 于是再三思索下我决定自己用渲染函数封装个函数式组件。

2. 组件编写

​ Vue的组件封装还是很友好的,render能够直接渲染写好的Vue模板并把Prop传进去,就像下面这样:

Vue.extend({
    
    
  data() {
    
    
    return {
    
    
      ...
    };
  },
  render(h): VNode {
    
    
    const props = {
    
    
      ...
    };
    return h(Component, {
    
     props });
  },
});

​ 看上去和单文件组件也没太大区别嘛,其实Element也是这么实现的函数式组件的。

1. Vuetify封装问题

​ 尽管使用的Vuetify,但Vue模板中不能使用Vuetify组件。原因在于Vuetify对组件库中的所有组件的封装方法会生成一个递归调用自身的实例,详情可以在console中打印vuetify组件dom查看,Vuetify组件会带有一个$vuetify属性,其中递归放置了它自身,初步猜测是为了防止被二次封装。

​ 因此Vue模板中需要使用原生实现。具体实现不在此赘述,可以前往npm下载本项目查看。

2. 同时使用typescript的问题

​ 由于ts中无法直接在Vue实例上挂载额外的全局方法,需要编写声明文件实现,在声明文件中,需要添加以下代码将$message方法声明到Vue实例上:

declare module "vue/types/vue" {
    
    
  interface Vue {
    
    
    $message(options: MessageOptions): void;
  }
}

​ 同时也必需引入此模块,否则仍会报错未找到模块。引入此模块后会提示模块未使用,忽略即可。

3. 插件注册

​ Vue中使用install函数定义插件,Vue.use会自动注入install方法的内容,install方法中第一个参数为注入的Vue实例,第二个参数为可选项。

​ 具体实现如下:

const MESSAGE: PluginObject<any> = {
    
    
  duration: 2000, // 显示时间常量
  animateTime: 600, // 出现/隐藏动画时间常量
  instances: [], // message组件实例数组(避免同时过多message导致重叠、出现位置错误问题)
  install(Vue, globalOptions: GlobalOptions = {
    
    }) {
    
    
    function msg(options: MessageOptions) {
    
    
      // 初始化可选参数
      const currOptions: GlobalOptions = {
    
    };
      currOptions.verticalOffset =
        globalOptions.verticalOffset === 0 || globalOptions.verticalOffset
          ? globalOptions.verticalOffset
          : 40;
      currOptions.flat = globalOptions.flat || false;
      currOptions.position = globalOptions.position || "right";
      currOptions.width = globalOptions.width || "30%";

      // 渲染组件
      const VueMessage = Vue.extend({
    
    
        data() {
    
    
          return {
    
    
            show: false,
          };
        },
        render(h): VNode {
    
    
          const props = {
    
    
            type: options.type,
            message: options.message,
            show: this.show,
            flat: currOptions.flat,
            position: currOptions.position,
            width: currOptions.width,
          };
          return h(Message, {
    
     props });
        },
      });

      // 创建实例并绑定
      const newMessage = new VueMessage();
      let vm: typeof newMessage | null = newMessage.$mount();
      const el = vm.$el;
      document.body.appendChild(el);

      // 重新定位实例垂直位置,避免重叠
      let verticalOffset = currOptions.verticalOffset;
      const offsetTopArr: number[] = [];
      if (MESSAGE.instances.length) {
    
    
        const itemHeight = MESSAGE.instances[0].$el.offsetHeight + 8;
        MESSAGE.instances.forEach((item: any) => {
    
    
          offsetTopArr.push(item.$el.offsetTop);
        });
        offsetTopArr.sort((prev, next) => prev - next);
        if (offsetTopArr[0] !== verticalOffset) {
    
    
          offsetTopArr.unshift(verticalOffset - itemHeight);
        }

        verticalOffset =
          offsetTopArr.reduce((prev, next) => {
    
    
            if (next - prev > itemHeight) {
    
    
              return prev;
            } else {
    
    
              return next;
            }
          }) + itemHeight;
      }
      const child: any = vm.$children[0];
      child.verticalOffset = verticalOffset;

      // 创建实例成功,显示实例
      vm.show = true;
      MESSAGE.instances.push(vm);
      // 实例生命周期结束,清除绑定实例(发现清除实例后Vue会默认在原本实例的dom位置上增加一行注		 释<!-- -->,若要完全清除可能要全原生实现组件渲染和自定义生命周期)
      const t1 = setTimeout(() => {
    
    
        clearTimeout(t1);
        vm!.show = false;
        const t2 = setTimeout(() => {
    
    
          clearTimeout(t2);
          document.body.removeChild(el);
          newMessage.$destroy();
          vm = null;
          options.callback && options.callback();
          MESSAGE.instances.shift();
        }, MESSAGE.animateTime);
      }, MESSAGE.duration);
    }

    // 绑定全局方法
    Vue.prototype.$message = msg;
  },
};

​ 至此整个插件就编写完成了,限于篇幅,一些interface、模板实现没有介绍,感兴趣的可以到npm下载ca-vuetify-message组件查看源码。

​ 由于本人也是第一次编写Vue插件,其中有错误内容还望互相交流斧正。

猜你喜欢

转载自blog.csdn.net/qq_41575208/article/details/119333198