初めてのVue Plugin開発 〜 vue-syntagme

この記事は[Vue.js #3 Advent Calendar 2017 – Qiita](https://qiita.com/advent-calendar/2017/vue3) 23日目の記事です。


拙作であるFlux Framework、[syntagme](https://github.com/rymizuki/syntagmejs)のVuePluginを書く機会があり、
それを通じてVueの仕組みの理解を深めたのでそのお話です。

vue-syntagmeはvue-routerの実装を参考にさせていただきました。

## 環境

* Vue 2.5

## 構造

### プラグインの登録 - install

公式のドキュメントでは如何のように説明されています。

> Vue.js プラグインは install メソッドを公開する必要があります。このメソッドは第 1 引数は Vue コンストラクタ、第 2 引数は任意で options が指定されて呼び出されます

https://jp.vuejs.org/v2/guide/plugins.html#%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%E3%81%AE%E8%A8%98%E8%BF%B0

今回、syntagmeではこのinstallを使って2つの機能を提供しました。

* `vm.$state` ... stateオブジェクトへのアクセサ。
* `vm.$action` ... actionCreatorの実行。

### mixinの定義

今回行う対応は3つあります。
1. `$state` というリアクティブなプロパティの定義
2. `$action` というsyntagmeのインスタンスにプロキシするメソッドの定義
3. 継承していれば子でも上記機能を利用できるようにする仕組み

これらは、install関数内でmixinを定義します。


```javascript:vue-syntagme/install.js
export function install (Vue) {
  if (install.installed && _Vue) return
  install.installed = true

  Vue.mixin({
  })
}
```

### 初期化処理

syntagmeの実行を初期化するため、ルートとなるcomponentではsyntagmeをラップしたVueSyntagmeのインスタンスを初期化します。

```javascript:vue-syntagme/install.js
Vue.mixin({
  beforeCreate () {
    this._syntagme = this.$options.syntagme
    this._syntagme.init()
  }
})
```

このとき、 `this` はrootのvmであり、`this.$options.syntagme` はVueSyntagmeのインスタンスを想定しています。

```javascript:App.vue
Vue.use(VueSyntagme)
const syntagme = new VueSyntagme(options)
export { syntagme }
```

こんなイメージで利用します

### `vm.$state`プロパティの定義

`vm.$state` はVueのライフサイクルとは異なるsyntagmeのライフサイクルで更新させます。ただsyntagmeのstateを参照したとしても、そこで起きた更新をVueは検知できません。

そこで初期化時に`Vue.util.defineReactive`を用いてsyntagmeのstateの更新をVueに伝えられるようにします。

`defineReactive`については[Vue.jsでリアクティブな独自データスコープを定義する](https://qiita.com/kazupon/items/de24715e18eedb1a6e8d)の記事を参考にさせていただきました。

```javascript:vue-syntagme/install.js
Vue.mixin({
  beforeCreate () {
    this._syntagme = this.$options.syntagme
    this._syntagme.init()
    Vue.util.defineReactive(this, '_state', this._syntagme._state)
  }
})
```

これでstateを参照できるように... おっと、`_state`直接見るっていうのはお行儀が良くないですね。参照用のプロパティを定義します。

```javascript:vue-syntagme/install.js
export default install (Vue) {
  Vue.mixin({
    beforeCreate () {
      this._syntagme = this.$options.syntagme
      this._syntagme.init()
      Vue.util.defineReactive(this, '_state', this._syntagme._state)
    }
  })

  Object.defineProperty(Vue.prototype, '$state', {
    get () { return this._syntagmeRoot._state }
  })
}
```

### `vm.$action`メソッドの定義

こちらは単純に関数の追加なので、Vueのprototypeに直接機能を追加します。

```javascript:vue-syntagme/install.js
export default install (Vue) {
  Vue.mixin({
    beforeCreate () {
      this._syntagmeRoot = this
      this._syntagme = this.$options.syntagme
      this._syntagme.init()
      Vue.util.defineReactive(this, '_state', this._syntagme._state)
    }
  })

  Object.defineProperty(Vue.prototype, '$state', {
    get () { return this._syntagmeRoot._state }
  })

  Vue.prototype.$action = function (action_type, args) {
    return this._syntagmeRoot._syntagme.action(action_type, args)
  }
}
```

### 機能を継承する

想定では、Rootでsyntagmeのインスタンスを渡し、それを子のContainerComponentで利用します。
なので、Rootのときは初期化を、それ意外の場合は、親からsyntagmeのプロパティを参照します。

```javascript:vue-syntagme/install.js
  Vue.mixin({
    beforeCreate () {
      if (isDef(this.$options.syntagme)) {
        this._syntagmeRoot  = this
        this._syntagme      = this.$options.syntagme
        this._syntagme.init(this)
        Vue.util.defineReactive(this, '_state', this._syntagme._state)
      } else {
        this._syntagmeRoot = (this.$parent && this.$parent._syntagmeRoot)
      }
    },
  })
```

### VueSyntagmeクラス

ここまで度々登場してきた、処理の本体であるところのVueSyntagmeクラスですが、詳細はsyntagmeの実装に依存するので割愛します。

今回プラグインとして公開する`action`, プラグイン内で利用する`init` `_state`, そしてスタティックメソッドとして定義された`install`を持ちます。

```javascript:vue-syntagme/index.js
import install from './install'

export default class VueSyntagme {
  init () { ... }
  action (type, parms) { ... }
  actionCreator(type, ac) { ... }
}

VueSyntagme.install = install
VueSyntagme.version = '__VERSION__'
```

このクラスはインスタンス化され、プラグイン外から呼び出すことも想定しています。

### Vue.use

useします。この辺はおなじみですね。

```javascript:App.vue
import Vue         from 'vue'
import VueSyntagme form 'vue-syntagme'

Vue.use(VueSyntagme)

const syntagme = new VueSyntagme()

syntagme.actionCreator('TAP', () => {
  return { action: 'tap' }
})

export {
  syntagme,
  computed () {
    count () { return this.$state.tapCount }
  },
  methods: {
    tap () { this.$action('TAP') }
  }
}
```

## まとめ

本記事では、Vueのプラグイン機構を用いて、プロパティ、関数などを実際に登録する手順を示しました。

Vueは公式ドキュメントが充実しており、ほとんどの場合公式ドキュメントで問題が解決するのですが、それはそれとして、すでにVueには多くのプラグインが提供され、軽く目を通すだけでも勉強になります。興味がある方はぜひ読んでみるとよいです。

このように、まあ誰が使うかも定かではありませんが、試しにプラグインを書いてみるとより理解が深まりますのでぜひ一度書いてみるとVueの世界をしるのでおすすめです。

※なお、Vueは公式にFluxフレームワークとして[vuexを推奨しています](https://jp.vuejs.org/v2/guide/state-management.html)。僕の場合は事情がある程度特殊なため、自前フレームワークとそれを利用するプラグインを使っています。