本文書にたどり着いた人はVueのプラグインを知りたいので公式のドキュメント見たけど理解できなかったまたはVue.use()メソッドとプラグインとの関係を知りたいという人ではないでしょうか。本文書ではドキュメントを参考にプラグインの作成の基礎の説明を行い、基礎を理解した後により実践的なドロップダウンメニューとスクリプトのロードに関するプラグインの作成を行います。読み終えたころにはオリジナルのプラグインの作成方法とプラグインの追加方法が理解できているはずです。

動作確認はVue3を利用しています。

プラグインの作成は下記のGoogle Mapでも行なっているのでプラグインに関する他の例を確認したい場合は参考にしてみてください。

一番簡単なプラグインを作成してみよう

早速最も簡単なプラグインを作成してみましょう。Vue CLIを利用してプロジェクトの作成を行います。presetではVue 3 Previewを選択しています。


 % vue create vue-plugin-test
//略
? Please pick a preset: 
  Default ([Vue 2] babel, eslint) 
❯ Default (Vue 3 Preview) ([Vue 3] babel, eslint) 
  Manually select features 

プラグインの作成

プロジェクトの作成後、srcフォルダにpluginsフォルダを作成しその下にFirstPlugin.jsファイルを作成します。

プラグインの作成はドキュメントに記述されている通りに行います。

Whenever this plugin is added to an application, the install method will be called if it is an object. If it is a function, the function itself will be called. In both cases, it will receive two parameters – the app object resulting from Vue’s createApp, and the options passed in by the user.

プラグインが追加された時、オブジェクトであればinstallメソッドが実行され、関数であればその関数が実行されると記述されているので2つの方法でプラグインを作成してみましょう。

オブジェクトの場合は下記のように記述することができます。installメソッドを含んでいます。


const FirstPlugin = {
  install() {
    console.log('Hello Plugin from Object');
  },
};
export default FirstPlugin;

関数の場合は下記のように記述することができます。こちらはinstallメソッドはいれません。関数の名前はどんな名前にしても実行されます。


const FirstPlugin = () => {
  console.log('Hello World from Function');
};

export default FirstPlugin;

プラグインの追加と実行方法

プラグインの使い方についてもドキュメントの記述されている通り実行してみましょう。

After a Vue app has been initialized with createApp(), you can add a plugin to your application by calling the use() method.

Vue appがcreateApp()で初期化された後use()メソッドでアプリケーションにプラグインを追加することができると記述されています。

main.jsファイルを開いて記述通りに設定を行います。追加するプラグインをimportしてapp.use()メソッドの指定します。


import { createApp } from 'vue';
import App from './App.vue';
import FirstPlugin from './plugins/FirstPlugin';

const app = createApp(App);

app.use(FirstPlugin);

app.mount('#app');

設定が完了したらnpm run serveコマンドで開発サーバを起動してブラウザでアクセスしてデベロッパーツールでコンソールを確認してください。関数を利用して実行したので”Hello World from Function”が表示されます。オブジェクトを利用した場合は”Hello World from Object”が表示されます。

プラグインで実行したメッセージ表示
プラグインで実行したメッセージ表示

プラグインを作成したことがない人にとってはプラグインの作成と追加はこんなに簡単なのかと思ったのではないでしょうか。

main.jsではFirstPluginをimportしていますが、importしなくても直接main.jsにFirstPluginのオブジェクトまたは関数を記述しても動作します。


import { createApp } from 'vue';
import App from './App.vue';

const app = createApp(App);

const FirstPlugin = {
  install() {
    console.log('Hello Plugin from Object');
  },
};

app.use(FirstPlugin);

app.mount('#app');

パラメータの確認

プラグインは以下のドキュメントの説明通り2つのパラメータを受け取ることができるのでこちらも動作確認してみます。

it will receive two parameters – the app object resulting from Vue’s createApp, and the options passed in by the user.

1つはcreateAppを実行した後に作成されるオブジェクト、1つのはユーザによって渡されるオプションと記述されています。

まずは一つ目の引数に入るappオブジェクトの確認です。オブジェクトでも関数でも結果は同じなので好きな方で確認してください。


//オブジェクトの場合
const FirstPlugin = {
  install(app) {
    console.log(app);
    console.log('Hello Plugin from Object');
  },
};

//関数の場合
// const FirstPlugin = () => {
//   console.log(app);
//   console.log('Hello World from Function');
// };

export default FirstPlugin;

Vueの情報が確認できます。component, directive, mixin, useなどの関数を確認することができます。これらの関数はプラグインの中で利用することができます。本文書では後ほどcomponentを利用します。

パラメータのappを確認
パラメータのappを確認

もしVueのバージョンが知りたい場合はapp.versionでアクセスすることができます。バージョン情報を利用することで稼働するバージョンによって異なる処理を行うといったようなことが可能になります。


const FirstPlugin = {
  install(app) {
    console.log(app.version);
    console.log('Hello Plugin from Object');
  },
};

実行するとコンソールには”3.1.1”が表示されました。実行した時に利用したVueのバージョンによって値は異なります。

次に2つ目のパラメータoptionsを確認しましょう。引数にoptionsを追加します。


//オブジェクトの場合
const FirstPlugin = {
  install(app, options) {
    console.log(options);
    console.log(app);
    console.log('Hello Plugin from Object');
  },
};

//関数の場合
// const FirstPlugin = (app, options) => {
//   console.log(options);
//   console.log(app);
//   console.log('Hello World from Function');
// };

export default FirstPlugin;

optionsが設定されていないのでコンソール上には”undefined”と表示されます。

次にapp.use()メソッドのプラグイン名の後に文字列を追加します。


app.use(FirstPlugin, 'オプション');

再度コンソールを確認すると渡された文字オプションが表示されていることが確認できます。文字列以外のオブジェクト等も渡すことができます。

optionsで渡された内容を表示
optionsで渡された内容を表示

最も簡単なプラグインを作成を通してプラグインを作成するための基礎を理解することができました。

プラグイン作成の実践編

プラグインの基礎を理解することができたので実践的なコードで記述されたプラグインにはどのようなものがあるか知りたいという人もいるかと思います。そのような場合は”vue プラグイン おすすめ”等で検索してみてください。さまざまなプラグインが公開されているのでどれか一つを選択してGitHubのページを確認すると設定方法ではVue.useメソッドを使っていることソースコードの中でinstallメソッドを利用していることが確認できるかと思います。運良くシンプルなプラグインであればコードを読み進めることでプラグインの理解がさらに深まるでしょう。

本文書ではオープンソースのプラグインとして公開されているDropDownMenuを参考をよりシンプルにしたドロップダウンメニューのプラグインを作成します。実践的なプラグインの作成方法を理解することができれば自作のプラグイン作成の敷居がぐっと下がるはずです。

Vue.componentメソッドの利用方法

先ほどはプラグインを作成したといってもコンソールにメッセージを表示させるだけのものでした。ここではVue.component()メソッドをinstallメソッド内で利用することでVue全体で利用できるコンポーネントを作成します。

pluginsフォルダに中にSecondPlugin.jsファイルを作成します。app.componentでコンポーネントを追加することができます。もっとapp.componentを知りたいという場合はVue.jsのドキュメンを確認してください。


import DropDownMenu from './DropDownMenu';

const SecondPlugin = {
  install(app) {
    app.component('DropDownMenu', DropDownMenu);
  },
};

export default SecondPlugin;

componentの第一引数はコンポーネント名で第二引数にはvueファイルを指定しています。コンポーネント名はvueで利用する際にタグ名として利用します。指定したvueファイルであるDropDwonMenu.vueファイルをpluginフォルダに作成してください。中身は通常のvueファイルです。


<template>
  <div>Hello SecondPlugin</div>
</template>
<script>
export default {};
</script>

main.jsではVue.use()メソッドで作成したSecondPluginを追加します。


import { createApp } from 'vue';
import App from './App.vue';
import SecondPlugin from './plugins/SecondPlugin';

const app = createApp(App);

app.use(SecondPlugin);

app.mount('#app');

これでコンポーネントを追加できるプラグインの設定は完了です。App.vueファイルの中で追加したコンポーネントを利用してみましょう。


<template>
  <img alt="Vue logo" src="./assets/logo.png" />
  <drop-down-menu />
</template>

<script>
export default {
  name: 'App',
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

ブラウザで確認するとDropDownMenu.vueファイルのtemplateタグの中で記述したSecondPluginが表示されます。Pluginを利用してコンポーネントを追加する方法も確認できました。

Plugin内で追加したコンポーネント表示
Plugin内で追加したコンポーネント表示

ドロップダウンメニュープラグインの作成

ここからはDropDownMenu.vueファイルでプラグインの中身を更新しながらApp.vueからslotまたはpropsで渡す値を設定していきます。

Slotの設定

汎用的にするためにボタンやメニューはプラグインの内部での設定ではなくslotを使って外側(親コンポーネント)から設定を行います。DropDownMenu.vueファイルでは2つのslotを設定します。2つのSlotを利用しているので片方のSlotにはdropdownという名前をつけています。デフォルトのslotには名前をつけていません。


<template>
  <div class="dropdown">
    <slot></slot>
    <div>
      <slot name="dropdown"></slot>
    </div>
  </div>
</template>
<script>
export default {};
</script>
<style>
.dropdown {
  display: inline-block;
}
</style>

DropDownMenuコンポーネントを利用するApp.vueからSlotの中身を設定します。デフォルトのSlotにはボタンの要素を渡しています。dropdownのSlotには3つのメニューを入れています。またメニューにはdropdown-itemクラスを設定しています。


<template>
  <img alt="Vue logo" src="./assets/logo.png" />
  <div>
    <drop-down-menu>
      <button>ドロップダウンメニュー</button>
      <template v-slot:dropdown>
        <a class="dropdown-item" href="#">Vue.js</a>
        <a class="dropdown-item" href="#">React</a>
        <a class="dropdown-item" href="#">Svelte</a>
      </template>
    </drop-down-menu>
  </div>
</template>

<script>
export default {
  name: 'App',
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
.dropdown-item {
  display: block;
  padding: 0.25rem 1.5rem;
  font-weight: 400;
  color: #212529;
  text-decoration: none;
}
</style>

ブラウザにはボタンと3つのメニューが表示されます。この時点でコンポーネントを利用したプラグインを作成することに成功しました。あとはDropDownMenuコンポーネントに機能を追加していくだけです。

Slotによるボタンとメニューの設定
Slotによるボタンとメニューの設定

表示・非表示の制御

ドロップダウンメニューの開閉を制御するデータプロパティshowをApp.vueファイルで設定します。


<template>
  <img alt="Vue logo" src="./assets/logo.png" />
  <div>
    <drop-down-menu v-model="show">
      <button>ドロップダウンメニュー</button>
      <template v-slot:dropdown>
        <a class="dropdown-item" href="#">Vue.js</a>
        <a class="dropdown-item" href="#">React</a>
        <a class="dropdown-item" href="#">Svelte</a>
      </template>
    </drop-down-menu>
  </div>
</template>

<script>
export default {
  name: 'App',
  data() {
    return {
      show: false,
    };
  },
};
</script>

v-modelを設定しているのでDropDownMenuコンポーネント側ではpropsのmodalValueとして値を受け取ることができます。コンポーネントにv-modelを設定した場合のpropsのmodalValueについてはhttps://v3.vuejs.org/guide/component-basics.html#using-v-model-on-componentsで確認することができます。

受け取ったmodalVauleをv-showで利用します。App.vueで設定したデフォルト値はfalseになります。


<template>
  <div class="dropdown">
    <slot></slot>
    <div v-show="modelValue">
      <slot name="dropdown"></slot>
    </div>
  </div>
</template>
<script>
export default {
  props: {
    modelValue: {
      type: Boolean,
    },
  },
};
</script>

v-showがfalseなのでブラウザで確認するとメニューが非表示になります。

メニューが非表示
メニューが非表示

表示と非表示を切り替えれるようにクリックイベントを設定します。クリックイベントで設定したtoggleMenuメソッドではemitにupdate:modalValueイベントを設定することでthis.modalValueがfalseの場合はtrue, trueの場合はfalseを親コンポーネントに伝えます。親コンポーネントであるApp.vueファイル側ではemitのイベントを受け取ることでshowの値がボタンをクリックするとtrue,falseと切り替わります。


<template>
  <div class="dropdown" @click="toggleMenu">
    <slot></slot>
    <div v-show="modelValue">
      <slot name="dropdown"></slot>
    </div>
  </div>
</template>
<script>
export default {
  props: {
    modelValue: {
      type: Boolean,
    },
  },
  emits: ['update:modelValue'],
  methods: {
    toggleMenu() {
      this.$emit('update:modelValue', !this.modelValue);
    },
  },
};
</script>
<style>
.dropdown {
  display: inline-block;
}
</style>

ブラウザで確認するとボタンを押すとことで表示・非表示を繰り返すドロップダウンメニューを作成することができます。

メニューの外側でのクリックの制御

ドロップダウンメニューの外側をクリックした時にメニューを閉じる機能を追加します。この機能は利用するしないを親コンポーネント側で指定できるようにデータプロパティcloseOnClickOutsideを追加し、propsで渡します。


<template>
  <img alt="Vue logo" src="./assets/logo.png" />
  <div>
    <drop-down-menu v-model="show" :closeOnClickOutside="closeOnClickOutside">
      <button>ドロップダウンメニュー</button>
      <template v-slot:dropdown>
        <a class="dropdown-item" href="#">Vue.js</a>
        <a class="dropdown-item" href="#">React</a>
        <a class="dropdown-item" href="#">Svelte</a>
      </template>
    </drop-down-menu>
  </div>
</template>

<script>
export default {
  name: 'App',
  data() {
    return {
      show: false,
      closeOnClickOutside: true,
    };
  },
};
</script>

渡されたcloseOnClickOutsideを受け取れるようにpropsの設定を行います。


props: {
  modelValue: {
    type: Boolean,
  },
  closeOnClickOutside: {
    type: Boolean,
    default: true,
  },
},

DropDownMenuではイベントリスナーを設定することでクリックイベントを検知してメニューの外側でクリックされてかどうか判断します。イベントリスナーをライフサイクルフックのmountedで追加しますが、イベントリスナーを追加する場合は忘れずに削除処理を追加する必要があります。削除処理はライフサイクルフックのdestroyedで行います。propsで渡されるcloseOnClickOutsideがtureの場合のみイベントリスナーの追加・削除処理を実行します。


mounted() {
  if (this.closeOnClickOutside) {
    document.addEventListener('click', this.clickOutside);
  }
},
destoryed() {
  if (this.closeOnClickOutside) {
    document.removeEventListener('click', this.clickOutside);
  }
},

再度にclickOutsideメソッドを追加します。$eventではクリックした要素を取得することができるのでクリックした要素がDropDownMenuの要素(this.$el)に含まれているかチェックを行い、含まれていないのみメニューを閉じる処理を行なっています。


methods: {
  toggleMenu() {
    this.$emit('update:modelValue', !this.modelValue);
  },
  clickOutside($event) {
    if (!this.$el.contains($event.target)) {
      if (this.modelValue && this.closeOnClickOutside) {
        this.$emit('update:modelValue', false);
      }
    }
  },
},

これで設定は完了です。App.vueでcloseOnClickOutsideの値をtrueにした場合にメニューを開いた後、ドロップダウンメニューの外側の要素をクリックすることメニューが非表示になることを確認してください。またfalseにした場合は外側をクリックしてもメニューが閉じないことも確認してください。

スクリプトのロードプラグインの作成

Google Mapなどサードパーティのライブラリを利用する場合にheadタグへのscriptタグの挿入を行いたい場合があります。そのような場合vue-plugin-load-scriptといったライブラリを利用しているかもしれません。vue-plugin-load-scriptも中身は非常にシンプルなので参考にプラグインを作成してみましょう。ドロップダウンメニューではコンポーネントを利用したプラグインの追加方法を確認しましたここでは関数を利用したプラグインの追加方法を確認しています。

プラグイン内の関数を実行

pluginsフォルダの下にLoadScript.jsファイルを作成します。プラグインを追加後にアプリケーションのコンポーネントでthis.$loadScriptを実行するとLoadScript内に記述したloadScript関数が実行できるようにコードを記述します。


const LoadScript = {
  install(app) {
    const loadScript = () => {
      console.log('load script plugin');
    };
    app.config.globalProperties.$loadScript = loadScript;
  },
};

export default LoadScript;

main.jsファイルでLoadScriptプラグインの追加を行います。


import { createApp } from 'vue';
import App from './App.vue';
import LoadScript from './plugins/LoadScript';

const app = createApp(App);
app.use(LoadScript);
app.mount('#app');

App.vueファイルでプラグインのloadScipt関数を実行します。


<template>
  <h1>Load Script Plugin</h1>
</template>

<script>>
export default {
  name: 'App',
  mounted() {
    this.$loadScript();
  },
};
</script>

ブラウザで確認するとでベロパーツールのコンソールに”load script plugin”が表示されます。プラグインを利用して関数の実行方法を理解することができました。

関数に引数を渡す

loadScript関数にコンポーネントから引数を渡します。引数でURLを受け取ります。


const loadScript = (src) => {
  console.log(src);
};

自分が利用したいライブラリのURLを設定してください。ここではApp.vueからGoogle Mapで利用するURLを渡しています。Google Mapは登録が必要でYOUR_MAP_KEYには各自がGoogle Cloud Platformで取得したキーを設定する必要があります。


this.$loadScript(
  'https://maps.googleapis.com/maps/api/js?key=YOUR_MAP_KEY'
);

loadScript関数の中ではPromiseとEventListenerを利用します。scriptの要素にEventListenerを設定し、error、about,、loadイベントを監視しています。ライブラリのロードが完了するとPromiseのresolveが返されます。失敗した場合にはrejectが戻されます。document.head.appendChildでheadタグへのscriptタグの追加を行なっています。


const LoadScript = {
  install(app) {
    const loadScript = (src) => {
      return new Promise((resolve, reject) => {
        let script = document.createElement('script');
        script.src = src;
        script.async = true;
        script.addEventListener('error', reject);
        script.addEventListener('abort', reject);
        script.addEventListener('load', () => {
          resolve(script);
        });
        document.head.appendChild(script);
      });
    };
    app.config.globalProperties.$loadScript = loadScript;
  },
};

export default LoadScript;

App.vueファイル側でthis.$loadScriptを実行するとPromiseが戻されるのでthen, catchを利用して処理をわけます。thenの中ではライブラリが完了しているのでGoogle Mapを描写する処理を実行しています。ライブラリのロードに失敗した場合にはcatchの中身が実行されコンソールに”ロードが失敗しました”と表示されます。成功した時とthenの中身と失敗した時のcatchの処理は各自が設定することになります。


<template>
  <h1>Load Script Plugin</h1>
  <div> ref="map" style="height: 500px; width: 800px"></div>
</template>

<script>>
export default {
  name: 'App',
  mounted() {
    this.$loadScript(
      'https://maps.googleapis.com/maps/api/js?key=YOUR_MAP_KEY'
    )
      .then(() => {
        new window.google.maps.Map(this.$refs.map, {
          center: { lat: -34.397, lng: 150.644 },
          zoom: 8,
        });
      })
      .catch(() => {
        console.log('ロードに失敗しました。');
      });
  },
};
</script>

ここまでの設定でloadScriptは動作し指定したURLのscriptタグをheadタグに追加することができます。

しかし一度this.$loadScriptでライブラリがロードされた後に他のコンポーネントでも同じURLでthis.$loadScriptを実行するとすでに登録されているにもかかわらず再度登録が行われます。複数回同じsrcを持つscriptがロードされないようにLoadScriptプラグインで制御を行う必要があります。

scriptタグの複数登録の制御

複数の登録が行われているかどうかはscriptタグをquerySelectorでアクセスすることでチェックします。


let script = document.querySelector('script[src="' + src + '"]');

分岐を行いscriptが登録されていない時のみ登録の処理を行います。shouldAppendという変数も追加しscriptタグが登録されていない場合のみ分岐の処理の中で値をtrueにします。一度trueにすると分岐の中のscriptの追加の処理のための準備が行われなくなります。これで複数のscriptタグの登録はなくなります。


let shouldAppend = false;
let script = document.querySelector('script[src="' + src + '"]');
if (!script) {
  script = document.createElement('script');
  script.src = src;
  script.async = true;
  shouldAppend = true;
} 

//略
if (shouldAppend) document.head.appendChild(script);

scriptが登録されていた場合にはresolveを戻す設定を行います。


if (!script) {
  script = document.createElement('script');
  script.src = src;
  script.async = true;
  shouldAppend = true;
} else {
  resolve(script);
}

この場合、複数ほぼ同時に登録処理がおこなわれた場合に最初のloadScriptがscriptタグを行いロードが完了していない状態で次のloadScriptが実行されるとロードが完了していない状態でresolveが戻されることになります。resolveが戻されたコンポーネントはロードが完了していると思いライブラリにアクセスを行おうとしますが実はまだロードが完了していないのでエラーになります。

この問題を防ぐためにはロードが完了したことをわかる情報を追加する必要があります。ロードが完了したらscriptタグにdata-loadedという属性を追加し完了したことがわかるように設定を行います。このdata-loadedがscriptタグについていない場合はscriptタグは追加されたがロードは完了していないという状態をあらわすことができます。


script.addEventListener('error', reject);
script.addEventListener('abort', reject);
script.addEventListener('load', () => {
  script.setAttribute('data-loaded', true);
  resolve(script);
});

data-loadedをチェックする分岐をもう一つ追加します。scriptタグにdata-loaded属性が付いている時はロードが完了しているのでresolveを戻します。もし登録されていない場合はイベントによりloadが完了するか監視を行います。


if (!script) {
  script = document.createElement('script');
  script.src = src;
  script.async = true;
  shouldAppend = true;
} else if (script.hasAttribute('data-loaded')) {
  resolve(script);
}

これで同時に複数のscriptの登録を実施しても一つのみscriptタグが登録されロードが完了した後にのみ処理が実行委できるようになります。


const LoadScript = {
  install(app) {
    const loadScript = (src) =< {
      return new Promise((resolve, reject) =< {
        let shouldAppend = false;
        let script = document.querySelector('script[src="' + src + '"]');
        if (!script) {
          script = document.createElement('script');
          script.src = src;
          script.async = true;
          shouldAppend = true;
        } else if (script.hasAttribute('data-loaded')) {
          resolve(script);
        }

        console.log('shodAppend', shouldAppend);

        script.addEventListener('error', reject);
        script.addEventListener('abort', reject);
        script.addEventListener('load', () =< {
          script.setAttribute('data-loaded', true);
          resolve(script);
        });

        if (shouldAppend) document.head.appendChild(script);
      });
    };
    app.config.globalProperties.$loadScript = loadScript;
  },
};

export default LoadScript;

まとめ

本文書を読み進めた後であればプラグインって実はこんなものなのだったのかと驚いている人もいるかもしれません。実際に公開されスターがついているプラグインでも思ったよりもシンプルなものもあるのでもしプラグインのコードが読めるようなら実際にコード読み進めてみてください。コーディングスキルの向上につながるはずです。

本文書を読み始めた時にVue.jsのプラグインの設定方法、使い方がわからなかった人も本文書の手順を確認してプラグインの理解を深まったのではないでしょうか。ぜひオリジナルのプラグインを作成にチャレンジしてみてください。