Help us understand the problem. What is going on with this article?

Vue.jsのTodoサンプルをコンポーネント化してTypeScriptで書く

More than 3 years have passed since last update.

Vue.jsを使う機会が出てきそうなので、最近Angular2から浮気して勉強中。
でも、Angular2でTypeScriptで書くことに慣れてしまったから、Vue.jsもTypeScriptで書きたい。
そこで、TypeScriptでVue.js(特にコンポーネント)を書く方法についていろいろ調べてみました。
Vue.jsのバージョンとしては1.0.28を使っています。
最終的にはVue.jsのサイトに載ってるTodoのサンプルをコンポーネント化して、TypeScriptで書くところまでやってみます。

Vue.jsとTypeScript

相性が悪い

いろいろ調べていくと、Vue.jsとTypeScriptは相性が悪いそうです。
というか、Angular2のTypeScript親和性がすごすぎる感じはしてます。
まあ、Angular2の場合、もともとの設計思想がTypeScriptで書けるようにというのもあるんでしょうが。
閑話休題。Vue.jsでコンポーネントを作る場合はVue.extendに私引数オブジェクトのプロパティとしてデータやメソッドを定義していきます。
それらをVue.jsが内部で処理することでコンポーネント化していますが、このような場合コンパイラがうまく型チェックや補完を行うことがができません。
そんな事情もあり、型定義ファイルを入れてTypeScriptである程度書くことはできても、いまいちTypeScriptの恩恵を得ることができないのが実情のようです。

ライブラリを使う

そんな相性の悪さを解消してTypeScriptで記述可能にしてくれるライブラリは実はいろいろ出ています。

  • vue-class-component
  • vue-typescript
  • vueit

おそらく一番有名なんではないかと思うのがvue-class-componentです。
Babel(ES7)やTypeScriptのデコレータでVue.jsのコンポーネントを記述できるライブラリです。
最初はこれを使おうとしたんですが、私の環境ではCannnot find module 'vue'エラーが出てしまいました。型定義をうまく読み込めてない感じがします。
一応、エラーは出てもコンパイルは通ってjsは生成されるみたいなんですけどね。
公式のexampleの中とか見るとjsで書いてるので、Babel(ES7)向けってことなのかもしれません。

vueitは今回参考にさせていただいているこの記事の投稿者の方がvue-class-componentとvue-typescriptの不満なところを解消するために作ったライブラリとのこと。
ただ、構文を見る限りだとなんとなくvue-class-componentのほうが直観的にわかりやすそうだったので、今回はこちらは使ってないです。

vue-typed

で、結局どうしたかというと、npmの公開パッケージを見て行ってよさそうなものを見つけました。
vue-typedです。
説明を読むと、TypeScript用のvue-class-componentという感じのようです。
上述のCannnot find module 'vue'エラーが解消したので、これを採用しました。
次の項目からは実際にvue-typedを使いながらTodoコンポーネントを作成していきます。

 TypeScriptでTodoコンポーネント

準備

まずは事前準備としてnpmやTypeScript、Webpack、typings、gulpなどの環境を構築しておきましょう。
その上で次のコマンドを実行し、Vue.js本体とvue-typedを入れます。
また、typingsでVueの型定義ファイルも入れておきます。

npm i vue --save
npm i vue-typed --save-dev
typings install -G dt~vue

サンプルを動かす

今回対象とするサンプルをまずは動かしましょう。
ここのコードを使います。
部分的にしか乗ってないので、scriptの読み込みコードなどを追加して動かせる状態にします。

index.html
<!DOCTYPE html>
<html>
    <head>
      <meta charset="utf-8">
      <title>VueJSTest</title>
    </head>
    <body>
      <div id="app">
        <input v-model="newTodo" v-on:keyup.enter="addTodo">
        <ul>
          <li v-for="todo in todos">
            <span>{{ todo.text }}</span>
            <button v-on:click="removeTodo($index)">X</button>
         </li>
       </ul>
      </div>
    </body>
    <script src="./node_modules/vue/dist/vue.js"></script>
    <script src="./bundle.js"></script>
</html>
bundle.js
new Vue({
  el: '#app',
  data: {
    newTodo: '',
    todos: [
      { text: 'Add some todos' }
    ]
  },
  methods: {
    addTodo: function () {
      var text = this.newTodo.trim()
      if (text) {
        this.todos.push({ text: text })
        this.newTodo = ''
      }
    },
    removeTodo: function (index) {
      this.todos.splice(index, 1)
    }
  }
})

Todoアプリが動けばOK。
image
挙動を確かめたい方はリンク先の下の方に実際のコンポーネントがあるので確かめてみましょう。

コンポーネント化する

TypeScriptで書く前にjavascriptのままでコンポーネント化します。

index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>VueJSTest</title>
</head>
  <body>
    <div id="app">
      <todo>
      </todo>
    </div>
  </body>
  <script src="./node_modules/vue/dist/vue.js"></script>
  <script src="./bundle.js"></script>
</html>
index.js
var TodoComponent = Vue.extend({
  template:`
  <input v-model="newTodo" v-on:keyup.enter="addTodo">
  <ul>
      <li v-for="todo in todos">
          <span>{{ todo.text }}</span>
          <button v-on:click="removeTodo($index)">X</button>
      </li>
  </ul>`,
  data: function(){
    return {
      newTodo: '',
      todos: [
        { text: 'Add some todos' }
      ]
    };
  },
  methods: {
    addTodo: function () {
      var text = this.newTodo.trim();
      if (text) {
        this.todos.push({ text: text });
        this.newTodo = '';
      }
    },
    removeTodo: function (index) {
      this.todos.splice(index, 1);
    }
  }
});
// コンポーネント登録
Vue.component("todo", TodoComponent);
// root インスタンスを作成
new Vue({
  el: "#app"
});

TodoComponentというコンポーネントを作ってそれをtodoタグに登録します。
html側に書いたtodoタグの場所にTodoComponentが表示されます。
ただし、Vueのルートが#appであるため、id=appのdivの外にtodoタグを書いても意味がないです。

TypeScriptで書く

いよいよ、TypeScriptでTodoComponentを書きます。
HTMLのほうはコンポーネント化後のHTMLをそのまま使うので、変更の必要はないです。

index.ts
///<reference path="./typings/index.d.ts"/>
import { Component, Data } from "vue-typed";

@Component({
  template: `
  <input v-model="newTodo" v-on:keyup.enter="addTodo">
  <ul>
      <li v-for="todo in todos">
          <span>{{ todo.text }}</span>
          <button v-on:click="removeTodo($index)">X</button>
      </li>
  </ul>`
})
class TodoComponent {
  @Data()
  newTodo:string =  "";
  @Data()
  todos: any[] = [
    { text: "Add some todos" }
  ];
  addTodo() {
    let text = this.newTodo.trim();
    if (text) {
      this.todos.push({ text: text });
      this.newTodo = "";
    }
  }
  removeTodo (index) {
    this.todos.splice(index, 1);
  }
};
// コンポーネント登録
Vue.component("todo", TodoComponent);
// root インスタンスを作成
new Vue({
  el: "#app"
});

まず、///<reference path="./typings/index.d.ts"/>でtypingsの型定義ファイルを読み込みます。
コンポーネントの作成自体はvue-typedだけで事足りますが、コンポーネントの登録やVueのrootインスタンス作成の時にVueへのアクセスが必要であり、そのままでは型エラーが発生するので読み込んでいます。
逆にいえば、型定義ファイルはコンポーネント登録とrootインスタンス登録を行うエントリポイントとなるtsファイルだけで読み込みを行うだけでいいということです。

その後、vue-typedのComponentデコレータを使って記述します。
templateはデコレータの中に書き、datamethodsに記述していたものはそれぞれクラスのフィールド、メソッドとして定義し直します。
ただし、data(フィールド)は@Data()を使って書く必要があります。

ここまで書いて、webpackなどでbundle.jsにコンパイルすればTodoが動作します。

他のデコレータ

ちらっと見た限りですが、vue-typedには6つデコレータがあるようです。

  • Component
  • Data
  • Getter
  • Action
  • Props
  • Watch

今回、ComponentとDataは使いました。PropsとWatchはVue.jsのprops、watchオプションを記述するためのデコレータのようです。
GetterとActionは調査中。

参考

TypescriptでVue componentを書くためのライブラリを作った

frost_star
まだまだ半人前プログラマー。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした