Vue3自定义指令初探以及插件的使用

4,302 阅读6分钟

背景

在我司的项目中,权限管理方面涉及到了Vue自定义指令功能,也使用了一些以自定义指令形式调用的插件。Vue3.0发布后,不再直接暴露一个Vue类而且directive()方法注册自定义指令的hooks也有所改变,意味着2.x版本的Vue.use()方法不再可行,同时在注册组件时各个方法挂载在hooks上的位置也要做调整。

刚好最近有空,就来探究一下组件在Vue3中能否正常运行,如果不能,要怎么做调整。

阅读前提

  • 一定的Vue插件开发知识
  • 对Vue工作原理有所了解

准备工作

1.首先更新一下vue-cli版本

在这里插入图片描述 2.create两个Vue项目,一个2.x版本(hello-vue2),一个3.0版本(hello-vue3)做对照试验

我这里一个是3.0.0-0,一个是2.6.11

在这里插入图片描述 在这里插入图片描述


3.以一款图片查看器插件作为材料进行测试(截止文章发出日期,v-viewer最新版本为1.5.1,还未对Vue3做任何兼容处理),讲道理,这款插件确实好用

开始

首先,在hello-vue2中按照v-viewer文档给出的两种方式引用插件,

<template>
  <div id="app">
    <!-- directive方式引用 -->
    <div class="images" v-viewer>
      <img src="https://picsum.photos/200/200" />
      <img src="https://picsum.photos/300/200" />
      <img src="https://picsum.photos/250/200" />
    </div>
    <!-- component方式引用 -->
    <viewer :images="images">
      <img v-for="src in images" :src="src" :key="src" />
    </viewer>
  </div>
</template>

<script>
import "viewerjs/dist/viewer.css";
import Viewer from "v-viewer";
import Vue from "vue";
Vue.use(Viewer)
export default {
  name: "App",
  data() {
    return {
      images: [
        "https://picsum.photos/200/200",
        "https://picsum.photos/300/200",
        "https://picsum.photos/250/200",
      ],
    };
  },
};
</script>
<style scoped>
.image {
  height: 200px;
  cursor: pointer;
  margin: 5px;
  display: inline-block;
}
</style>

不出所料,两种方式都正常运行,效果如图

在这里插入图片描述 这里之所以没有在components中注册viewer也可以正常使用,是因为插件的install方法中已经全局注册过了

//v-viewer/src/index.js
import {extend} from './utils'
import Component from './component.vue'
import directive from './directive'
import Viewer from 'viewerjs'

export default {
  install (Vue, {name = 'viewer', debug = false, defaultOptions} = {}) {
    Viewer.setDefaults(defaultOptions)

    Vue.component(name, extend(Component, { name }))
    Vue.use(directive, {name, debug})
  },
  setDefaults (defaultOptions) {
    Viewer.setDefaults(defaultOptions)
  }
}



接着,在hello-vue3项目中同样使用两种方式引用,同样的代码,就不再贴一遍了。 果不其然,组件引用的方式正常执行,而自定义指令引用的方式:Uncaught TypeError: Cannot read property 'use' of undefined Vue类是undefined,没有use方法 翻看一下Vue源码,之前的return Vue

//vue 2.6.11
(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
  typeof define === 'function' && define.amd ? define(factory) :
  (global = global || self, global.Vue = factory());
}(this, function () { 'use strict';
  ...
  ...
  return Vue;

}));

变成了

//vue 3.0.0-0
var Vue = (function (exports) {
  'use strict';
  ...
  ...
  exports.BaseTransition = BaseTransition;
  exports.Comment = Comment;
  exports.Fragment = Fragment;
  ...
  ...
  exports.withKeys = withKeys;
  exports.withModifiers = withModifiers;
  exports.withScopeId = withScopeId;

  return exports;

}({}));

按照作者尤大在发布会上表露的思想和文档中的描述,此改动旨在促进开发者们不再通过选项来组织代码,而应该将代码通过自定义选项来组织成为处理特定功能的函数。

The APIs proposed in this RFC provide the users with more flexibility when organizing component code. Instead of being forced to always organize code by options, code can now be organized as functions each dealing with a specific feature. The APIs also make it more straightforward to extract and reuse logic between components, or even outside components.

所以Vue不再支持直接引入一个Vue类,而是将具体api暴露出来供开发者选择使用。2.x版本中const app = new Vue(options)来创建一个实例的方法也变成了:

import { createApp } from 'vue'
import { App } from './App'
const app = createApp(App)

再回到源码中,use的使用者找到了,那么,使用者是否拥有use方法呢?

//vue/dist/vue.global.js
function createAppAPI(render, hydrate) {
  ...
  ...
  use(plugin, ...options) {
      if (installedPlugins.has(plugin)) {
           warn(`Plugin has already been applied to target app.`);
      }
      else if (plugin && isFunction(plugin.install)) {
          installedPlugins.add(plugin);
          plugin.install(app, ...options);
      }Ï
      else if (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;
  },
  ...
  ...
}

还好,use方法仍然保留,那么我们将调用方法修改一下,放在main.js中,同时将组件引用的方式去掉

//main.js
import { createApp } from 'vue'
import App from './App.vue'
import Viewer from 'v-viewer'
import "viewerjs/dist/viewer.css";
const app = createApp(App)
app.use(Viewer)
app.mount('#app')

ok,报错没有了,但是插件并没有生效,点击无反应

在这里插入图片描述 结合注册directive时的生命周期有所改变这一点,猜测可能是注册方法静默失败了。找到插件的安装方法,加上log调试一下:

//v-viewer/dist/v-viewer.js
var install = function install(Vue, _ref) {
  ...
  ...
  console.log('我要进入绑定方法了!')
  Vue.directive('viewer', {
    bind: function bind(el, binding, vnode) {
      console.log('我开始绑定了!')
      log('viewer bind');
      var debouncedCreateViewer = __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_1_throttle_debounce__["a" /* debounce */])(50, createViewer);
      debouncedCreateViewer(el, binding.value);

      createWatcher(el, binding, vnode, debouncedCreateViewer);

      if (!binding.modifiers.static) {
        createObserver(el, binding.value, debouncedCreateViewer, binding.modifiers.rebuild);
      }
    },
    unbind: function unbind(el, binding) {
      log('viewer unbind');

      destroyObserver(el);

      destroyWatcher(el);

      destroyViewer(el);
    }
  });
}

在这里插入图片描述 果然,注册插件的核心方法没有执行,那么我们应该对照文档,将hooks改为对应正确的。先看一下2.x中对钩子的定义:

bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。
componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
unbind:只调用一次,指令与元素解绑时调用。

还有3.0的

//vue3
app.directive('my-directive', {
  // Directive has a set of lifecycle hooks:
  // called before bound element's parent component is mounted
  beforeMount() {},
  // called when bound element's parent component is mounted
  mounted() {},
  // called before the containing component's VNode is updated
  beforeUpdate() {},
  // called after the containing component's VNode and the VNodes of its children // have updated
  updated() {},
  // called before the bound element's parent component is unmounted
  beforeUnmount() {},
  // called when the bound element's parent component is unmounted
  unmounted() {}
})

由于v-viewer只用到了bind()unbind()两个钩子,且钩子中并没有涉及到挂载dom的逻辑,所以这里bind可以选择替换为beforeMount或者mountedunbind可以替换为beforeUnmount或者unmounted

代码修改如下:

// 省略无关部分
Vue.directive('viewer', {
    beforeMount: function bind(el, binding, vnode) {
      console.log('我开始绑定了!')
      log('viewer bind');
      var debouncedCreateViewer = __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_1_throttle_debounce__["a" /* debounce */])(50, createViewer);
      debouncedCreateViewer(el, binding.value);

      createWatcher(el, binding, vnode, debouncedCreateViewer);

      if (!binding.modifiers.static) {
        createObserver(el, binding.value, debouncedCreateViewer, binding.modifiers.rebuild);
      }
    },
    unmounted: function unbind(el, binding) {
      log('viewer unbind');

      destroyObserver(el);

      destroyWatcher(el);

      destroyViewer(el);
    }
  });

结果如图:

在这里插入图片描述 方法进来了,也带来的新的报错:Vue.nextTick is not a function。这想必与开始的Uncaught TypeError: Cannot read property 'use' of undefined是一个原因导致的。找到报错的地方:

//v-viewer/dist/v-viewer.js
var install = function install(Vue, _ref) {
   ...
   ...
   Vue.nextTick(function () {
     if (rebuild || !el['$' + name]) {
       destroyViewer(el);
       el['$' + name] = new __WEBPACK_IMPORTED_MODULE_0_viewerjs___default.a(el, options);
       log('viewer created');
     } else {
       el['$' + name].update();
       log('viewer updated');
     }
   });
 }
  ...
  ...
}

虽然这个地方的Vue与之前的Vue是一样的(都是一个vue实例),但是3.0版本中nextTick方法不再挂载在原型上,而是作为一个api单独暴露。此处如果选择导入nextTick方法会略微有些麻烦,我们借鉴Vue 2.x版本中,对于低版本不支持promise()的浏览器用setTimeout()代替的做法。

//v-viewer/dist/v-viewer.js
var install = function install(Vue, _ref) {
  ...
  ...
  function createViewer(el, options) {
    var rebuild = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;

    setTimeout(() => {
      if (rebuild || !el['$' + name]) {
        destroyViewer(el);
        el['$' + name] = new __WEBPACK_IMPORTED_MODULE_0_viewerjs___default.a(el, options);
        log('viewer created');
      } else {
        el['$' + name].update();
        log('viewer updated');
      }
    }, 0);
  }
  ...
  ...
}

在这里插入图片描述 done,it's worked!

心得

  • Vue 2.x相较于3.0版本,注册自定义指令并没有什么大的变动。调用方式方面,除了文中的调用方式,文档中还有另外两种:
// 第二个参数使用函数,默认在mounted和updated钩子中执行
app.directive('my-directive', () => {
  // do something
})

// 省略第二个参数:getter,返回指令的定义内容
const myDirective = app.directive('my-directive')
  • v-viewer插件之所以只做了少量修改即可兼容vue3(不一定是完美兼容),原因在于插件在注册时以及插件本身的逻辑极少依赖于Vue的底层逻辑
  • 虽然Vue3对老版本代码做了兼容,但是仍有必要使用Composition Api风格对老代码进行重构,使逻辑更为清晰
  • 在阅读Vue文档过程中,越来越觉得Composition Api的写法像极了React

如有错误,欢迎指正!