Vue 3.0.0-betaのお試し環境をVue CLIで作ってみた

2020/04/17 に Vue 3.0.0-beta.1 がリリースされました！
お試し用の環境を作って [Vue Composition API](https://composition-api.vuejs.org/) や [Proxy](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Proxy) によるリアクティブといった待望の機能を試してみましょう。

<blockquote class="twitter-tweet"><p lang="en" dir="ltr">We just released Vue 3.0.0-beta.1! Here&#39;s an overview from <a href="https://twitter.com/youyuxi?ref_src=twsrc%5Etfw">@youyuxi</a> on the status of 3.0 core and official tools &amp; libraries: <a href="https://t.co/7TP5ZMtjK4">https://t.co/7TP5ZMtjK4</a></p>&mdash; Vue.js (@vuejs) <a href="https://twitter.com/vuejs/status/1250890006060818440?ref_src=twsrc%5Etfw">April 16, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>



# 動作環境
- macOS Catalina
- Node.js 12.16.2
    - npm 6.14.4
- Vue CLI v4.3.1
- Chrome 81

# Vue CLI のインストール
```
npm i -g @vue/cli
```

Vue CLI をグローバルインストールしたくない方は、以下の手順の `vue` コマンド部分を `npx @vue/cli` に読み替えていただいても大丈夫です。


# 環境構築
## プロジェクトの作成
`vue-3-beta-trial` というプロジェクト名で環境を構築していきます:

```
vue create vue-3-beta-trial
```

設定は以下のようにしました:

```
Vue CLI v4.3.1
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, TS, Linter
? Use class-style component syntax? No
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? No
? Pick a linter / formatter config: Basic
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? No
```

## プラグインの導入
プロジェクトディレクトリが出来上がったら、[vue-cli-plugin-vue-next](https://github.com/vuejs/vue-cli-plugin-vue-next) を導入します:

```
cd vue-3-beta-trial
vue add vue-next
```

## ファイルの修正
vue-cli-plugin-vue-next を導入した直後に `npm run serve` でビルドしようとしてもエラーが出てしまうので、 `*.d.ts` ファイルと `*.vue` ファイルを修正します:

1. `src/shims-tsx.d.ts` の内容をすべてコメントアウトする
2. `src/App.vue`, `src/components/HelloWorld.vue` の内容を以下のように修正:

```diff:src/App.vue
 <script lang="ts">
-import Vue from 'vue';
+import { defineComponent } from 'vue';
 import HelloWorld from './components/HelloWorld.vue';
```

```diff:src/App.vue
-export default Vue.extend({
+export default defineComponent({
   name: 'App',
   components: {
     HelloWorld
```

以上で `npm run serve` でビルドができるようになりました :tada:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137045/d029e41e-5b28-da24-3875-958c1801794e.png)

# ファイルの差分をみてみる
`vue create` コマンドで作成したプロジェクトは自動で git の初期コミットが済んだ状態のため、 `git status` で `vue add vue-next` コマンド以降に差分が出たファイルを確認できます:

```
$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   .eslintrc.js
        modified:   package-lock.json
        modified:   package.json
        modified:   src/App.vue
        modified:   src/components/HelloWorld.vue
        modified:   src/main.ts
        modified:   src/shims-tsx.d.ts
```

## `package.json` の差分
```diff:package.json
@@ -8,7 +8,7 @@
     "lint": "vue-cli-service lint"
   },
   "dependencies": {
-    "vue": "^2.6.11"
+    "vue": "^3.0.0-beta.1"
   },
   "devDependencies": {
     "@typescript-eslint/eslint-plugin": "^2.26.0",
@@ -16,10 +16,11 @@
     "@vue/cli-plugin-eslint": "~4.3.0",
     "@vue/cli-plugin-typescript": "~4.3.0",
     "@vue/cli-service": "~4.3.0",
+    "@vue/compiler-sfc": "^3.0.0-beta.1",
     "@vue/eslint-config-typescript": "^5.0.2",
     "eslint": "^6.7.2",
-    "eslint-plugin-vue": "^6.2.2",
+    "eslint-plugin-vue": "^7.0.0-alpha.0",
     "typescript": "~3.8.3",
-    "vue-template-compiler": "^2.6.11"
+    "vue-cli-plugin-vue-next": "~0.1.2"
   }
 }
```

## `src/main.ts` の差分
```diff:src/main.ts
@@ -1,8 +1,4 @@
-import Vue from 'vue'
+import { createApp } from 'vue';
 import App from './App.vue'

-Vue.config.productionTip = false
-
-new Vue({
-  render: h => h(App),
-}).$mount('#app')
+createApp(App).mount('#app')
```

[vuejs/rfcs](https://github.com/vuejs/rfcs/) の [0009-global-api-change.md](https://github.com/vuejs/rfcs/blob/49de6decac4dc99840410d082e3bd7c98b580193/active-rfcs/0009-global-api-change.md) に書いてある通り、 Vue 2系 におけるプラグイン導入(`Vue.use()`)によるグローバル汚染問題の解消のためこのような書き方に変更となったようです。

# お試し編
## Vue Composition API を試してみる
https://composition-api.vuejs.org/ にあった Basic example をコピーしてビルドしてみました:

```vue:App.vue
<template>
  <button @click="increment">
    Count is: {{ state.count }}, double is: {{ state.double }}
  </button>
</template>

<script>
import { reactive, computed } from 'vue'

export default {
  setup() {
    const state = reactive({
      count: 0,
      double: computed(() => state.count * 2)
    })

    function increment() {
      state.count++
    }

    return {
      state,
      increment
    }
  }
}
</script>
```

![counter.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137045/c443c4e1-4c3c-9ca0-0455-f935561e5dc0.gif)

シンプルなカウンターが動作しました。
(なお、上記のコードであれば Vue 2.6.11 に [@vue/composition-api](https://github.com/vuejs/composition-api) (Vue 2 向けのプラグイン)を導入して試すことができます)

## Proxy な reactive を試してみる
Vue 3.0 では [Proxy](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Proxy) ベースのリアクティブ実装となる[^1]ため、従来のリアクティブ実装[^2]では実現できなかった配列のインデックスと一緒にアイテムを直接セットする場合についても変更を検知可能になるようです。

[^1]: 現在(2020/04/19)のところ提供されていないIE11互換ビルドでは、従来のリアクティブ実装に近いものが用いられると思われます
[^2]: `Object.defineProperty()` で getter/setter に変換する仕組みなどが用いられています。詳細は公式ドキュメントの[リアクティブの探求](https://jp.vuejs.org/v2/guide/reactivity.html)を参照してください

```vue:App.vue
<template>
  <button @click="increment1">
    counts[0] is: {{ state.counts[0] }}
  </button>
  <button @click="increment2">
    counts[1] is: {{ state.counts[1] }}
  </button>
</template>

<script>
import { reactive } from 'vue'

export default {
  setup() {
    const state = reactive({
      counts: [100, 200],
    })

    function increment1() {
      state.counts[0]++
    }

    function increment2() {
      state.counts[1]++
    }

    return {
      state,
      increment1,
      increment2,
    }
  }
}
</script>
```

![counter2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137045/999f6ff9-6e1f-6077-29f0-6bd7a4f53972.gif)

Vue 2系では変更の検知ができなかった `state.counts[0]++` といった操作でも、画面上に値が反映されました。

また、 `<button>` 要素が `<template>` 直下に2個並んでいることから、ルート要素は必ず1個という掟についてもなくなっていることが分かります。