うしろのこの本ください

なんでもかきます

Vue 3.0で入る(予定の)Class APIについてのRFCを読んだ

github.com

Evanが先日VueのRFCに投げたやつ。VueのコンポーネントがES6 Class Syntaxで記述できるよう拡張するというもの。

ブラウザ(CDN版)

class App extends Vue {
  // options declared via static properties (stage 3)
  // more details below
  static template = `
    <div @click="increment">
      {{ count }} {{ plusOne }}
    </div>
  `

  // reactive data declared via class fields (stage 3)
  // more details below
  count = 0

  // lifecycle
  created() {
    console.log(this.count)
  }

  // getters are converted to computed properties
  get plusOne() {
    return this.count + 1
  }

  // a method
  increment() {
    this.count++
  }
}

SFC

<template>
  <div @click="increment">
    {{ count }} {{ plusOne }}
    <Foo />
  </div>
</template>

<script>
import Vue from 'vue'
import Foo from './Foo.vue'

export default class App extends Vue {
  static components = {
    Foo
  }

  count = 0

  created() {
    console.log(this.count)
  }

  get plusOne() {
    return this.count + 1
  }

  increment() {
    this.count++
  }
}
</script>

これに伴い今までのオブジェクトベースで記述する際に行っていた new Vueによるマウントは行わず、別に専用のグローバルなAPIを生やしてそっちでやるようになる。ReactDOM的な?

なんでクラスAPIを入れるか、というのはとても丁寧にEvanが書いてくれているので本文を読んで欲しい。

一応要約すると既存のオブジェクトベースの構文では型推論が効き辛い部分があるため、TypeScriptとの相性改善という面が強い。vue-class-componentの利用も考えられるが、vueのcoreとの互換性維持のオーバヘッドがあって、ならcoreに取り込んだれという感じ。

オブジェクトベースな書き方が出来なくなるわけじゃなく、公式のドキュメントも双方確認できるようにするっぽい。描画関数がどんどん希薄な存在になってる気がするけど。例にあるように、基本はVueのサブクラスを作って使う。

data

dataはクラスフィールドとして宣言できるけど、Field declarationsはまだstage 3なのでTSかBabelが必要。

class MyComponent extends Vue {
  count = 0
}

TypeScriptでは型注釈付けられる。

class MyComponent extends Vue {
  count: number = 1

  created() {
    this.count // number
  }
}

それ以外では普通にconstructorthis.count = 0みたいな奴が考えられる。こっちはコンストラクタの中でsuper()が必須になる。

ただこれは非推奨とされていて、何故かというとコンストラクタの中にいるインスタンスをライフサイクルメソッドからthisで指すことが出来ないため。ライフサイクルメソッドから見たthisは実際には本体のインスタンスを指している。

let instance

class MyComponent extends Vue {
  constructor() {
    super()
    instance = this // actual instance
  }

  created() {
    console.log(this === instance) // false, `this` here is the Proxy
  }
}

というわけで、トランスパイルする手段のない環境では以下のようにオブジェクトベースで書いていたものそのままでも良いようだ。

class MyComponent extends Vue {
  data() {
    return {
      count: 0
    }
  }
}

ライフサイクルフック

ライフサイクルフックはクラスメソッドとして記述する。普通。

class MyComponent extends Vue {
  created() {
    console.log('created')
  }
}

props

propsが結構特殊でなんやかんやあってこれだけデコレータ使うんだけど、ちゃんとその理由も順を追ってサンプルコードと一緒に説明されている。Evanのドキュメントはかなり読みやすい。英語も簡単で紛らわしい表現がなくて助かる。

まず、propsを使うだけならば静的プロパティとして書けばおk。

class MyComponent extends Vue {
  // props declarations are fully compatible with v2 options
  static props = {
    msg: String
  }

  created() {
    // available on `this`
    console.log(this.msg)

    // also available on `this.$props`
    console.log(this.$props.msg)
  }
}

全部$props経由でもアクセスできる。ここはv2と変わりない。v3では宣言自体省略できる。

class MyComponent extends Vue {
  created() {
    console.log(this.$props.msg)
  }
}

TypeScriptによる静的な型のチェックのために@propデコレータが提供されるみたいだ。以下は静的解析時のみに動き、実行時ではstatic props = ["count"]と等価になる。

import { prop } from 'vue'

class MyComponent extends Vue {
  @prop count: number

  created() {
    this.count // number
  }
}

またより具体的な検証のためのオプションオブジェクトも渡せるようになる。

import { prop } from 'vue'

class MyComponent extends Vue {
  @prop({
    validator: val => {
      // custom runtime validation logic
    }
  })
  msg: string = 'hello'

  created() {
    this.count // number
  }
}

propsのデフォルト値を設定したい場合TypeScript側の制限で@props count: number = 1みたいには書けない。オプションオブジェクトのプロパティとして渡す方法が提供されている。

class MyComponent extends Vue {
  @prop({ default: 1 }) foo: number
  bar = this.foo + 1
}

TypeScriptではインターフェイスを定義して渡すことが出来る。

interface MyProps {
  msg: string
}

interface MyData {
  count: number
}

class MyComponent extends Vue<MyProps, MyData> {
  count: number = 1

  created() {
    this.$props.msg
    this.$data.count
  }
}

いいっすね~~~

computed

本家と前後したけどcomputedプロパティはクラスのgetterとして記述する。内部ではVueの算出プロパティに変換され、計算結果やキャッシュを返す。

class MyComponent extends Vue {
  count = 0

  get doubleCount() {
    return this.count * 2
  }
}

methods

普通にクラスメソッドとして書く。ライフサイクルフックもこれ扱い。

class MyComponent extends Vue {
  count = 0

  created() {
    this.logCount()
  }

  logCount() {
    console.log(this.count)
  }
}

このthisは自動的にインスタンスにバインドされ、this.hoge.bind(this)をしなくても良くなっている。

その他

クラスベースAPIにないものは静的フィールドとして宣言する必要がある。たとえばtemplate

class MyComponent extends Vue {
  static template = `
    <div>hello</div>
  `
}

静的クラスフィールドはstage 3なのでトランスパイルが必要。だめならテンプレートリテラルObject.assignなどで取り付ける。

class MyComponent extends Vue {}

MyComponent.template = `
  <div>hello</div>
`

または

class MyComponent extends Vue {}

Object.assign(MyComponent, {
  template: `
    <div>hello</div>
  `
})

継承

普通に extends で継承していける。

class A extends Vue {}
class B extends A {}
class C extends B

Vue.extendでこうするのと同じ意味。

const A = Vue.extend({})
const B = A.extend({})
const C = B.extend({})

UIコンポーネントの直接継承は有用ではないとしている。そのロジックだけを継承し、レンダリングに関してカバーできないため。代わりにmixinかslotによるコンポジション集約をする方が良い。まあ継承の継承は変更に弱くなるのでそもそもアレ。GUIは特に変更が多い領域だし、Javaとかのそれより継承とは相性が悪い。基底クラスからの派生以外ではやらない方がいいと思う。

mixins

型推論が飛ばないようにするにはmixin自体をVueのサブクラスとして宣言する必要があるらしい。実際に使う時はサブクラス化したmixinからさらに拡張して使う。

ただし、型推論が必要ないならオブジェクトベースの書き方でも良い。

import Vue, { mixins } from 'vue'

class MixinA extends Vue {
  // class-style mixin
}

const MixinB = {
  // object-style mixin
}

class MyComponent extends mixins(MixinA, MixinB) {
  // ...
}

その他質問とか

本文読んで。1つだけ紹介すると、Reactはhooksでてクラスから離れていってるよ?っていうのに対してEvanは「Reactのコンポーネントモデルの概念はクラスと相性が良くないけど、本質的にクラスが悪いってわけじゃなくてVueではReactのそれよりも適している。hooksみたいなクラス&オブジェクトベースと同等の機能を持つものも提供するかもしれないけど、まだ先のことだね」とのこと。

その他にも色々考えたけど結局こうなったみたいなのが書いてあって読んでて面白かった。nuxt-tsもリリースされたことだしVue3に向けて素振りしておくと良いかもしれない。Vue.extend or vue-class-component で迷うのはあるけど。

個人的な感想

twitterを眺める限り反応よさげ。自分も良いと思ってます。1つ気になったのがVue.extendについて言及がなかった点だけど、既出だったみたい。

t.co

やりとりを見てるとクラスAPIはこれの置き換えな存在っぽい?Vue.extendの型推論を強化するか、クラスAPIへの機械的(又は内部的)な変換が出るか、まだ結論出てない様子。

はい

@rickyruiz

The RFC does not mention anything about Vue.extend explicitly. As a TypeScript user, I've never needed vue-class-component.

Using classes will be the recommended option for TypeScript users? Are there going to be any disadvantages if I keep using Vue.extend instead of classes? Vue.extend will continue to work as part of the existing object-based API support. However, if you are indeed using TypeScript, it's recommended to use classes in v3 as that's where we will be focusing on in the future in terms of >improving type inference.

ようするに、残すけど型のサポート強化されるしクラスで書いた方が良いと思うよってことらしい

おわり