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

公開当初はVue CLIを利用してVue3の環境を構築していましたがVue3から公式のVueプロジェクト作成ツールcreate-vueが登場したのでcreate-vueを利用した方法での手順を追加しています。Vue CLIの場合はOptions APIを利用し、create-vueの場合はComposition APIを利用しています。

Vue CLIの場合の後に記述されているコードはVue CLIを利用して作成したプロジェクトを利用しoptions APIを利用しています。Vue3でoptions APIを利用してい場合はVue CLIの場合を参考にしてください。
fukidashi

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

プロジェクトの作成

プロジェクトを作成する際プロジェクト名には任意の名前をつけることができます。ここではvue-plugin-testという名前をつけて実行しています。

create-vueを利用した場合

create-vueコマンドを実行するとプロジェクトの中で利用したい機能を追加でインストールすることが可能です。本文書のプラグインの作成ではプラグインの動作確認に焦点を当てているためどの追加機能も利用しないので”No”を選択しています。


% npm init vue@latest
Need to install the following packages:
  create-vue@latest
Ok to proceed? (y) y

Vue.js - The Progressive JavaScript Framework

✔ Project name: … vue-plugin-test
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit Testing? … No / Yes
✔ Add Cypress for both Unit and End-to-End testing? … No / Yes
✔ Add ESLint for code quality? … No / Yes

Scaffolding project in /Users/mac/Desktop/vue/vue-plugin-test...

Done. Now run:

  cd vue-plugin-test
  npm install
  npm run dev

Vue CLIを利用した場合

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');

createApp関数でインスタンス化したappインスタンスのuseメソッドの引数に作成したプラグインを指定することでプラグインを利用できることを理解することができました。

パラメータの確認

プラグインは以下のドキュメントの説明通り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 = (app) => {
//   console.log(app);
//   console.log('Hello World from Function');
// };

export default FirstPlugin;

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

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

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


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ファイルです。


<script setup></script>
<template>
  <div>Hello SecondPlugin</div>
</template>

【Vue CLIの場合】


<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';

import './assets/main.css';

const app = createApp(App);

app.use(SecondPlugin);

app.mount('#app');

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


<script setup></script>

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

<style>
//略
</style>

main.jsファイルでimportしているmain.cssを開いて#appに適用されているCSSをコメントします。


//略
@media (min-width: 1024px) {
  body {
    display: flex;
    place-items: center;
  }

  /* #app {
    display: grid;
    grid-template-columns: 1fr 1fr;
    padding: 0 2rem;
  } */
}
//略

【Vue CLIの場合】


<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には名前をつけていません。


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

【Vue CLIの場合】


<template>
  <div class="dropdown">
    <button><slot></slot></button>
    <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クラスを設定しています。


<script setup></script>

<template>
  <img alt="Vue logo" src="./assets/logo.svg" />
  <div>
    <drop-down-menu>
      ドロップダウンメニュー
      <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>

<style scoped>
.dropdown-item {
  display: block;
  padding: 0.25rem 1.5rem;
  font-weight: 400;
  color: #212529;
  text-decoration: none;
}

//略
</style>

【Vue CLIの場合】


<template>
  <img alt="Vue logo" src="./assets/logo.png" />
  <div>
    <drop-down-menu>
      ドロップダウンメニュー
      <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によるボタンとメニューの設定

表示・非表示の制御

ドロップダウンメニューの開閉を制御するためにref関数を利用してリアクティブな変数showを定義します。デフォルト値はfalseに設定しています。


<script setup>
import { ref } from 'vue';
const show = ref(false);
</script>

<template>
  <img alt="Vue logo" src="./assets/logo.svg" />
  <div>
    <drop-down-menu v-model="show">
      ドロップダウンメニュー
      <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>

【Vue CLIの場合】

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


<template>
  <img alt="Vue logo" src="./assets/logo.png" />
  <div>
    <drop-down-menu v-model="show">
      ドロップダウンメニュー
      <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://vuejs.org/guide/components/events.html#usage-with-v-modelを参考に設定することができます。

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


<script setup>
const props = defineProps({
  modelValue: Boolean,
});

</script>
<template>
  <div class="dropdown" >
    <button><slot></slot></button>
    <div v-show="modelValue">
      <slot name="dropdown"></slot>
    </div>
  </div>
</template>

【Vue CLIの場合】


<template>
  <div class="dropdown">
    <button><slot></slot></button>
    <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と切り替わります。


<script setup>
const props = defineProps({
  modelValue: Boolean,
});
const emit = defineEmits(['update:modelValue']);

const toggleMenu = () => {
  emit('update:modelValue', !props.modelValue);
};
</script>
<template>
  <div class="dropdown">
    <button @click="toggleMenu"><slot></slot></button>
    <div v-show="modelValue">
      <slot name="dropdown"></slot>
    </div>
  </div>
</template>
<style>
.dropdown {
  display: inline-block;
}
</style>

【Vue CLIの場合】


<template>
  <div class="dropdown">
    <button @click="toggleMenu"><slot></slot></button>
    <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>

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

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

ドロップダウンメニューの外側をクリックした時にメニューを閉じる機能を追加します。

この機能は利用するしないを親コンポーネント側で指定できるようにref関数でリアクティブな変数closeOnClickOutsideを追加し、propsで渡します。


<script setup>
import { ref } from 'vue';
const show = ref(false);
const closeOnClickOutside = ref(true);
</script>

<template>
  <img alt="Vue logo" src="./assets/logo.svg" />
  <div>
    <drop-down-menu v-model="show" :closeOnClickOutside="closeOnClickOutside">
      ドロップダウンメニュー
      <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>

【Vue CLIの場合】

この機能は利用するしないを親コンポーネント側で指定できるようにデータプロパティcloseOnClickOutsideを追加し、propsで渡します。


<template>
  <img alt="Vue logo" src="./assets/logo.png" />
  <div>
    <drop-down-menu v-model="show" :closeOnClickOutside="closeOnClickOutside">
      ドロップダウンメニュー
      <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の設定を行います。


const props = defineProps({
  modelValue: Boolean,
  closeOnClickOutside: {
    type: Boolean,
    default: true,
  },
});

【Vue CLIの場合】


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

DropDownMenuではイベントリスナーを設定することでクリックイベントを検知してメニューの外側でクリックされてかどうか判断します。

イベントリスナーをライフサイクルフックのonMountedで追加しますが、イベントリスナーを追加する場合は忘れずに削除処理を追加する必要があります。削除処理はライフサイクルフックのonUnmountedで行います。propsで渡されるcloseOnClickOutsideがtureの場合のみイベントリスナーの追加・削除処理を実行します。onMountedとonUnmountedを利用するためにはimportが必要です。


<script setup>
import { onMounted, onUnmounted, ref } from 'vue';

//略

onMounted(() => {
  if (props.closeOnClickOutside) {
    document.addEventListener('click', clickOutside);
  }
});

onUnmounted(() => {
  if (props.closeOnClickOutside) {
    document.removeEventListener('click', clickOutside);
  }
});

【Vue CLIの場合】

イベントリスナーをライフサイクルフックの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の要素に含まれているかチェックを行い、含まれていないのみメニューを閉じる処理を行なっています。DropDownMenuの要素はref関数を利用して取得しています。


const target = ref(null);

const emit = defineEmits(['update:modelValue']);

const toggleMenu = () => {
  emit('update:modelValue', !props.modelValue);
};

const clickOutside = (event) => {
  if (!target.value.contains(event.target)) {
    if (props.modelValue && props.closeOnClickOutside) {
      emit('update:modelValue', false);
    }
  }
};

【Vue CLIの場合】

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にした場合は外側をクリックしてもメニューが閉じないことも確認してください。

スクリプトのロードプラグインの作成(Vue CLIの場合)

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のプラグインの設定方法、使い方がわからなかった人も本文書の手順を確認してプラグインの理解を深まったのではないでしょうか。ぜひオリジナルのプラグインを作成にチャレンジしてみてください。