86
80

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Vue.jsAdvent Calendar 2016

Day 3

Vue.jsとTypeScript

Last updated at Posted at 2016-12-02

はじめに

この記事を書いて1年経ちました。
1年間でVue.jsとTypeScriptの環境は大きく変わりました。まずは、その辺を色々と紹介していきます。そのあと、サンプルを作成します。

2017年3月19日に追記

作成したサンプルがエラー吐くとのissueを受けたのを機に、サンプルを結構いじりました。サンプルも必ずチェックしてね。

この1年間で変わったこと

Vue.js 2.0がリリースされた

新しいメジャーリリースに際し、TypeScript関連で一番大きく変わったのは、型定義ファイルの提供方法です。バージョン1まではDefinitelyTypedに型定義ファイルを登録していました。そのため、tsdやtypingsで型定義ファイルをダウンロードする必要がありました。

一方新しいバージョンからは、Vue.jsのリポジトリ内に型定義ファイルがあります。したがって、npmなどを使ってライブラリをダウンロードするだけで使えるようになります。Vue.js本体だけでなく、いくつかの公式プラグインにも提供されています。

現在のところ、型定義ファイルがバンドルされているパッケージは以下の通りです。

TypeScript 2.0がリリースされた

TS 2.0で色々と大きな変更が入りました。詳しくはこちらを参考にしてください(TypeScript 2.0 Beta 変更点 - Qiita)。Vue.js向け型定義ファイルを作成する上で、嬉しかった変更は、function 内で this を明示できるようになったことです。

Vue.jsでコンポーネントを定義するとき、 ComponentOptions の中にプロパティやメソッド、ライフサイクルフックを記述します。

export default {
  data () {
    return {
      key: ''
    }
  },
  mounted () {
    this.$watch(this.key, 'methodName') // 型が合っているかわからない
  },
  methods: {
    methodName () {
    // something
    }
  }
}

TS 1.xではこのmounted内部の this を明示できず、thisany 型になってしまいました。
TS 2.0からは、以下のように記述でき、function 内部の this の型を明示できるようになりました。例えばこんな感じです。

import * as Vue from 'vue'

export interface MyComponent extends Vue {
  key: string
}

export default {
  mounted (this: MyComponent) {
    this.$watch(this.key, 'methodName')
  }
}

型定義ファイルには、interface ComponentOptions<V extends Vue> が定義されています。これを利用することで、ComponentOptions 内部のすべての this を自動的に明示できるようになります。以下のような感じで使います。

import { ComponentOptions } from 'vue'

export interface MyComponent extends Vue {
  key: string
}

export default {
  mounted () {
    this.$watch(this.key, 'methodName')
  },
  beforeDestroy() {
    this.$off('key')
  }
} as ComponentOptions<MyComponent>

また、後述するように、JavaScriptだけではなくHTMLやCSSなどに対しても型定義ファイルを作れるようになったことも嬉しいです。

作るもの

昨年と同様、サンプルアプリケーションを作ろうと思います。
明日12月4日は、『ご注文はうさぎですか?』に登場するキャラクター「香風智乃」の誕生日であります。したがって彼女の誕生日をお祝いするアプリケーションを作ろうと思います。
昨年との変更点は、Vuexを導入したことと、jQueryを使わなくなったことと、データの永続化にDexie.jsを使ったことです。

方法

  • TypeScriptで記述する
  • テンプレートはHTMLファイルに記述する
  • CSSはSCSSで記述する
  • Webpackを使う

デコレータについて

vue-class-component 以外にも色々あります。
2.0に対応しているものをリストアップします。

今回はVue.js公式のvue-class-componentを使います。
また、vue-property-decoratorも合わせて利用します。

TypeScriptのヘルパー関数について

デコレータを使うと、ヘルパー関数が何度も生成されてファイルサイズが増えてしまいます。
TypeScript2.1からは、tslib を使うことで防ぐことができます。
TypeScript 2.1.1 変更点 - Qiitaによると、

  1. tslib をnpmなどでダウンロードする。
  2. tsconfig.json 内の importHelpersnoEmitDecoratorstrue にする。

のようにします。

Viewについて

2.0より前のバージョンでは、template プロパティにHTMLを記述したりテンプレートのidを記述したりしていました。
一方2.0からは仮想DOMが採用されました。したがって仮想DOMを構築する関数の中身を用意することが必要となりました。これは日本語ドキュメントにおいて描画関数と訳されています。

Vue.jsは現在2.1が最新バージョンですが、https://github.com/vuejs/vue/tree/dev/dist をみるといっぱいビルドファイルが並んでいます。注目していただきたいのは、compiler-includedruntime-only の違いです。前者はテンプレートから描画関数を生成するコンパイラが含まれていますが、後者には含まれていません。
したがって、後者を利用するときはいかにして render 及び staticRenderFn といった描画関数を用意するかがポイントになります。

自分で描画関数を書く

デコレータ内部に記述するか、クラス定義の中に記述します。
型チェックが効くのが嬉しいです。

import * as Vue from 'vue'
import Component from 'vue-class-component'

@Component<Vue>({
  render (h) {
    return h('div', this.message)
  }
})
class MyComponent extends Vue {

  message = 'Hello world'

  render (h: Vue.CreateElement) {
    return h('div', this.message)
  }
}

WebpackのloaderでHTMLをコンパイルする

HTMLファイルからVue.jsの描画関数を生成するWebpackのloaderは2つあります。

この2つの違いは、renderstaticRenderFn をどのように提供しているかということです。
ここで、型定義ファイルを書いてみることにします。

vue-template-loader
declare module '*.html' {
  import * as Vue from 'vue'
  import { ComponentOptions } from 'vue'
  
  var template: <V extends Vue>(options: ComponentOptions<V>) => ComponentOptions<V>

  export = template
}


import * as Vue from 'vue'
import Component from 'vue-class-component'
import * as template from 'my-template.html'

// thisを明示するときは、 @Component(template<MyComponent>({}))と書きます。
@Component(template({}))
class MyComponent extends Vue {
}
vue-template-compiler-loader
declare module '*.html' {
  import * as Vue from 'vue'
  import { CreateElement, VNode } from 'vue'
  
  export var render: (this: Vue, createElement: CreateElement) => VNode
  export var staticRenderFns: ((createElement: CreateElement) => VNode)[]
}



import * as Vue from 'vue'
import Component from 'vue-class-component'
import { render, staticRenderFns } from 'my-template.html'

@Component({ render, staticRenderFns })
class MyComponent extends Vue {
}

// もしくは
import * as template from 'my-template.html'

@Component({
  mixins: [template]
})
class MyComponent extends Vue {
}

今回は前者を使いました。

HTMLWebpackPluginと併用すると、エラーを吐いてしまいました。したがってコンポーネントのテンプレートは拡張子を*.template.htmlとしました。

シングルファイルコンポーネントを使う

ts-loaderと組み合わせることで、*.vue内でTypeScriptを使えるようです。

作ったもの

ここにあります。
ダウンロードして、dist/index.htmlをブラウザで開いてみてください。

考察

よくできたと思います。

最後に

最近のVue.jsについて

以前は数あるその他フロントエンドフレームワーク的な存在だったVue.jsは、今やGitHubのスター数でこんな感じになっています。0.10の頃から注目していたので、知名度が大きくなったことはとても嬉しく思っています。

コードネームについて

2.0は攻殻機動隊 (Ghost in the Shell) でした。
次はHUNTER×HUNTERとかどうでしょう。
また1.1はFになるようですが、蒼穹のファフナーか鋼の錬金術師になると信じています。


明日12月4日は@nasumさんです。

86
80
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
86
80

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?