Vue2にComposition APIを追加してみた

# はじめに

Composition APIのVue2への導入方法と基本的な機能についての実装方法を試した。
基本的に公式のガイドおよびQiitaの既存記事の内容であるが、Vueユーザにとって当たり前（？）の部分が省略されておりVue初心者の自分にとって手探りな部分があったので、やや冗長ながら極力省略せずに実装方法を紹介する。

### 開発環境

- Vue CLI: 4.1.1
- Visual Studio Code: 1.40.2
- Visual Studio Code拡張機能
  - Vetur: 0.22.6
  - Vue 2 Snippets: 0.1.11
  - Vue Peek: 1.0.2
  - ESLint 1.9.1

# プロジェクト作成
## ベース作成
### テンプレート作成

Vue CLIを使用してテンプレートを作成する。TypeScriptを導入するがクラススタイルにはしなくてよい。（どちらにせよ後でまるごと書き換える）

```
? Check the features needed for your project: TS, Linter, Unit
? Use class-style component syntax? No
```

### TypeScriptアップデート（Optional）

Vue CLIで作成した場合、TypeScriptのバージョンは3.5.3になる（2019/12/10時点）ので3.7.3にアップデートした。（詳細は補足で）

### Lintルール変更（Optional）

基本的には推奨設定に従うが、個人的な趣味でLintルールを一部変更した。（詳細は補足で）
以下で紹介するコードは変更後ルールに従っているのであしからず。

## Composition API追加

本題であるComposition APIを利用するためにライブラリを追加する。

### 依存関係を追加

```
npm install @vue/composition-api
```

### ライブラリの利用を宣言

APIの使用に先立ってライブラリ利用の宣言を追加する。

```typescript:main.ts
import Vue from 'vue';
import VueCompositionApi from '@vue/composition-api'; // ★追加
import App from './App.vue';

Vue.use(VueCompositionApi);	// ★追加

Vue.config.productionTip = false;

new Vue({
  render: h => h(App),
}).$mount('#app');

```

# Composition APIでの実装
## ローカルな変数・関数

まずはComposition APIによるコンポーネント作成の基本形を示す。

```vue:App.vue
<template>
  <div id="app"><!-- ★内容変更 -->
    <div>{{msg}}</div>
    <button @click="changeMessage">hello</button>
  </div>
</template>

<script lang="ts">
import { createComponent, ref } from '@vue/composition-api'; // ★'vue'に代えてimport

export default createComponent({ // ★export defaultの内容全面書き換え
  setup: () => {
    const msg = ref('Hello!');

    const changeMessage = () => {
      msg.value = 'What\'s up?';
    };

    return {
      msg,
      changeMessage,
    };
  },
});
</script>
<!-- styleは省略 -->
```

- `createComponent`関数の戻り値をデフォルトエクスポートする
  - `Vue`は直接は使用しないのでimport宣言ごと除去する
- `createComponent`関数の引数オブジェクト内の`setup`プロパティの内容を関数とし、Viewに使用する変数や関数をまとめて戻り値とする。
  - プリミティブな値は`ref`関数により`Ref`型でラップする（上記コードの`msg`）

## 自作コンポーネントの利用/props/computed

次にVueらしくコンポーネントを親子関係にする。また、propsによる値の受け渡しとcomputedによる自動再計算も合わせて実装する。

### 子コンポーネント

`props`と`computed`を持つ子コンポーネントを作成する。

```vue:HelloWorld.vue
<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <h1>{{ upperMsg }}</h1>
  </div>
</template>

<script lang="ts">
import { createComponent, computed } from '@vue/composition-api';

interface Props {
  msg: string;
}

export default createComponent({
  props: {
    msg: {
      type: String,
      default: 'Hello, world.',
    },
  },
  setup: (props: Props) => {
    const upperMsg = computed(() => props.msg.toUpperCase());

    return {
      upperMsg,
    };
  },
});
</script>
<!-- styleは省略 -->
```

- props
  - 定義: `createComponent`関数の引数オブジェクト内の`props`プロパティにPropsとして利用する内容を記述
  - Viewでの利用: 上の定義をしておけばそのまま使える
  - 加工して利用: `setup`で記述する処理に使用する場合は`setup`に引数を追加する。なお、型は別途interfaceかtypeで定義しておく
- computed
  - `createComponent`関数の引数オブジェクト内の`setup`内で`computed`関数の引数に計算を実行する関数定義を記述する

### 親コンポーネント

App.vueを上記コンポーネントを利用するように修正する。

```vue:App.vue
<template>
    ...
    <HelloWorld :msg="msg"/><!-- ★追加 -->
    ...
</template>

<script lang="ts">
import { createComponent, ref } from '@vue/composition-api';
import HelloWorld from './components/HelloWorld.vue'; // ★追加

export default createComponent({
  components: { // ★componentsプロパティ追加
    HelloWorld,
  },
  setup: () => {
    ...
  },
});
</script>
<!-- styleは省略 -->
```

- 子コンポーネントをimportする
- `createComponent`関数の引数オブジェクト内の`components`プロパティに使用するコンポーネントを列挙する
- template内で子コンポーネントを使用する（従来通り）

## setup内容の外部化/ライフサイクルフック

最後にComposition APIらしく、従来コンポーネントファイル内で実装する必要のあった内容の外部化を試す。ライフサイクルフックへの登録も合わせて試す。

### 外部setupファイル

公式サイトの例（ https://vue-composition-api-rfc.netlify.com/#logic-extraction-and-reuse ）に従ってマウスの位置を取得するモジュールを作成（Lintエラー解消のため一部差異あり）

```typescript:mouse.ts
import { ref, onMounted, onUnmounted } from '@vue/composition-api';

export function useMousePosition () {
  const x = ref(0);
  const y = ref(0);

  const update = (e: MouseEvent) => {
    x.value = e.pageX;
    y.value = e.pageY;
  };

  onMounted(() => {
    window.addEventListener('mousemove', update);
  });

  onUnmounted(() => {
    window.removeEventListener('mousemove', update);
  });

  return { x, y };
}

```

- exportする関数内でsetupで本来実行する処理を記述し、リアクティブなオブジェクトを戻り値とする
- ライフサイクルフックに登録する内容は`onMounted`などの対応する関数の引数として記述する

## 外部ファイルのインポート

HelloWorld.vueにmouse.tsをインポートする

```vue:HelloWorld.vue
<template>
    ...
    <div>({{x}}, {{y}})</div><!-- ★追加 -->
    ...
</template>

<script lang="ts">
import { createComponent, computed } from '@vue/composition-api';
import { useMousePosition } from '@/util/mouse'; // ★追加
...
export default createComponent({
  ...
  setup: (props: Props) => {
    const upperMsg = computed(() => props.msg.toUpperCase());

    const { x, y } = useMousePosition(); // ★追加

    return {
      upperMsg,
      x, // ★追加
      y, // ★追加
    };
  },
});
</script>
<!-- styleは省略 -->
```

- mouse.tsをインポートする
- `setup`関数内で外部化した関数（上の例での`userMousePosition`）を実行する
- 戻り値があり、それをviewで利用する場合は自身の`setup`関数の戻り値に追加する
- ローカルで設定したリアクティブオブジェクトと同様にテンプレート内で使用する

## 成果物

最終的なプログラムを実行すると以下のようになる。

![composition-api-demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266076/d8d770c5-06ef-f427-8a70-547abcf19313.gif)


# 参考

- [Composition API RFC | Vue Composition API](https://vue-composition-api-rfc.netlify.com)
- [先取りVue 3.x !!  Composition API を試してみる  - Qiita](https://qiita.com/ryo2132/items/f055679e9974dbc3f977)
- [VeturにTypeScript3.7のOptional Chainingを適用してみる - Qiita](https://qiita.com/ibara1454/items/10d6091858db106e717b)

# 補足
## TypeScriptアップデート

- ライブラリアップデート

```
npm install typescript@3.7.3 --save-dev
npm install @typescript-eslint/parser --save-dev
```

- Visual Studio Codeの設定
  - tsファイル表示に右下出現するTypeScriptのバージョンをクリックして「ワークスペースのバージョンを使用」に変更する
  - ワークスペース配下のsettings.jsonに以下の記述が追加される

```json:.vscode/settings.json
"typescript.tsdk": "node_modules\\typescript\\lib"
```

- Visual Studio Codeの拡張機能の設定
  - Veturをインストールしている前提（Vueを書くならほぼ必須）
  - **アプリケーション**のsettings.jsonに以下の記述を追加する

```json:User/settings.json
"vetur.useWorkspaceDependencies": true
```

## Lintルール変更

`vue create`時に`ESLint + Standard config `を選択したが、個人的な趣味で`comma-dangle`と`semi`については独自ルールに変更。最終的に以下のようにした。

```js
extends: [
    'plugin:vue/essential',
    '@vue/standard',
    '@vue/typescript',
  ],
  rules: {
    'comma-dangle': ['error', {
      'arrays': 'always-multiline',
      'objects': 'always-multiline',
      'imports': 'never',
      'exports': 'never',
      'functions': 'never',
    }],
    'semi': ['error', 'always'],
    'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
  },
```

ルール変更後、以下のコマンドで既存コードを一括修正。

```
npm run lint
```