Nuxt.js PWA(Progressive Web Apps) のベースアプリをTypeScript対応する

Nuxt.js を使うことで簡単に PWA なアプリを作ることができます。そのアプリを作る際に TypeScript を使うとコードの品質を高めることができ、また複数人やチームでの開発生産性を向上させることができます。今回は Nuxt.js で作ったアプリに TypeScript を導入する方法を紹介します。

前回Nuxt.js で PWA(Progressive Web Apps) のベースアプリを作るで、Nuxt.jsを使ったPWAのベースアプリを作りました。そのまま JavaScript で開発を進めることができますが、TypeScript を導入することで変数や関数に型定義が行えるようになり開発生産性や保守性を向上できます。今回のベースアプリはゼロから作っているので最初から入れてしまうのがよいでしょう。また TypeScript の関連モジュールを導入したとしても、必ずしも TypeScript の利用が必須ではなく、JavaScript で書いていって、TypeScript にできるところから適用していく段階的な利用も可能です。そのような観点からも入れてしまうのがよいでしょう。

シリーズの記事

環境
本記事の開発環境は以下となります。

  • Windows 10 64bit + WSL Ubuntu 18.04.1 LTS
  • Visual Studio Code
  • Node.js 12.1.0
  • Yarn 1.15.2
  • Nuxt.js 2.6.3

TypeScript とは

TypeScriptは JavaScript に型とクラスを導入する AltJS のプログラミング言語で、JavaScript のスーパーセットです。クラスは ES6 で導入されたので TypeScript 固有ではなくなりましたが、やはり「型」の導入が TypeScript のメリットといえるでしょう。

型の例で、たとえば JavaScript で以下のようなコードがあったとします。
単純な足し算を想定したコードですが、型がないので引数に任意の値を投入できます。その結果、戻り値が想定外になることもあります。

1
2
3
4
5
6
7
8
const add = (a, b) => {
return a + b;
}

console.log(add(1, 2)); // 3
console.log(add(1, "2")); // 12
console.log(add(1, [ 2 ])); // 12
console.log(add(1, { value: "2" })); // 1[object Object]

TypeScript では以下のようになります。
引数と戻り値に:numberで、数値型を明示します。それにより数値でない引数を渡している部分はコンパイルエラーとなり実行コード(JavaScript)が生成できません。

1
2
3
4
5
6
7
8
const add = (a: number, b: number): number => {
return a + b;
}

console.log(add(1, 2));
console.log(add(1, "2")); // error TS2345: Argument of type '"2"' is not assignable to parameter of type 'number'.
console.log(add(1, [ 2 ])); // error TS2345: Argument of type 'number[]' is not assignable to parameter of type 'number'.
console.log(add(1, { value: "2" })); // error TS2345: Argument of type '{ value: string; }' is not assignable to parameter of type 'number'.

基本的には型定義が入っただけで書いていることに変わりはありません。またコンパイラや Lint の設定で、制約を強くしたり、甘くしたりできます。最初から使う場合はある程度強くできるでしょうし、途中から導入したり慣れていない場合などは徐々に強くしていくこともでき、状況に合わせて使えます。

このぐらいの関数でしたら型がなくてもとくに問題ないかもしれませんが、もっと大きなものや他の人が書いたコードなどになってくると話が異なります。関数の引数が何を期待しているのか、戻り値は何が返ってくるのか、型情報があるだけでだいぶ変わってきます。

そんな TypeScript ですが、最近はとくに注目されており Meetup イベントが東西で開催されます!抽選枠で、まだ応募可能です。
TypeScript Meetup #1 にエントリーしましたが、案内メールが来て一瞬で定数枠の 60 を超え、本記事執筆時点で 500 人を超えてます。スゴイ勢い!

Riotz.works は、TypeScript で開発するチームなのですが、実はずっと Java で開発をしてきたチームでした。あるトラブルがあって TypeScript に舵を切り、以来 TypeScript で開発するチームとなったのですが、その際の発表資料がこちらになります。もしよかったらご参照ください。(いつかの Meetup の機会でお話しできるといいな)

Nuxt.js のプロジェクトに TypeScript を導入

前回作成Nuxt.js で PWA(Progressive Web Apps) のベースアプリに TypeScript を導入します。

TypeScript サポートのモジュールを追加

まずは TypeScript サポートのモジュールを追加します。プロジェクトのディレクトリで下記コマンドを実行します。

1
$ yarn add -D @nuxt/typescript ts-node

※ npm の場合、npm i -D @nuxt/typescript ts-node

TypeScript の設定ファイルを生成

TypeScript の設定ファイルtsconfig.jsonを作成します。
※ このfeat(ts): auto generate tsconfig.json by kevinmarrec · Pull Request #4776 · nuxt/nuxt.jsプルリクがリリースされるとtouchしなくても済みそうですが、2019年5月現在、まだ事前に作成が必要です。

1
$ touch tsconfig.json

Nuxt.js 開発サーバーを起動し、tsconfig.json の定義を生成します。起動情報にTypeScript support is enabledが出ていることがわかります。

1
2
3
4
5
6
7
8
9
10
$ yarn dev
╭──────────────────────────────────────────╮
│ │
│ Nuxt.js v2.6.3 │
│ Running in development mode (spa) │
│ TypeScript support is enabled │
│ │
│ Listening on: http://localhost:3000/ │
│ │
╰──────────────────────────────────────────╯

tsconfig.jsonの内容が更新されます。JSON ファイルをモジュールとして扱える定義resolveJsonModuleを追加します。これは追加したほうが便利なのと、後述のnuxt.config.tspackage.jsonを扱えるようにするためです。

1
2
3
4
5
6
{
"compilerOptions": {
// ...(省略)
"resolveJsonModule": true,
}
}

この定義は追加するプルリクと削除するプルリクが流れているようで、日付的には消すほうが最後なので自分で追加が必要そうです。
“Remove resolveJsonModule as , after little thoughts, we should not force it to users -#4842“ のだそうで、フレームワークのポリシーや考え方ですね。(なぜなのかは知りたかった)

Nuxt.js の設定ファイルを TypeScript 化

続いて設定を TypeScript で書くためnuxt.config.jsファイルをnuxt.config.tsにリネームします。
ファイルの内容、先頭と最後を以下のように変更します。とくに'./package.json'.jsonをつけ忘れないように注意します。このpackage.jsonを読み込むのに先ほどのresolveJsonModuleが必要でした。

変更前

1
2
3
4
5
import pkg from './package'

export default {
// ...(省略)
}

変更後

1
2
3
4
5
6
7
8
import NuxtConfiguration from '@nuxt/config'
import pkg from './package.json';

const config: NuxtConfiguration = {
// ...(省略)
}

export default config

これにより設定ファイル内でCtrl + Spaceなどの入力補完が使えるようになります。

ページやコンポーネントを TypeScript 化

プロジェクトが TypeScript 対応したので、ページやコンポーネントを TypeScript 化します。

今回はスクリプトベースで実装します。基本的な構文は下記です。

  • <script>タグにlang="ts"を追加して TypeScript として認識させます
  • 全体に型定義を効かせるためにVue.extendを追加します
  • </script>タグ前の括弧閉じに ‘)’ を追加するのを忘れないよう注意します
    1
    2
    3
    4
    5
    6
    <script lang="ts">
    import Vue from 'vue'

    export default Vue.extend({
    })
    </script>

ベースアプリの/pages/index.vueは以下のようになります。

1
2
3
4
5
6
7
8
9
10
<script lang="ts">
import Vue from 'vue'
import Logo from '~/components/Logo.vue'

export default Vue.extend({
components: {
Logo
}
})
</script>

また、Visual Studio Code の Vue 拡張Veturを使っている場合/components/Logo.vueに以下の空実装コードを追加します。
(nuxtコマンドはエラーではないので、Vetur を使わない場合は追加しなくても大丈夫です)

1
2
3
4
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({})
</script>

Note
公式ドキュメントTypeScript サポート - Nuxt.jsでは、今回のようなスクリプトベースではなくクラスベースの “vue-property-decoratorを利用することを強くお薦め -公式サイト“ しています。
しかしながらクラス構文にすると、実装方法が Vue.js/Nuxt.js とまったく異なってしまいます。今回は、これまでの実装経験やサイト、書籍を活かすためにスクリプトベースとしました。まったくゼロから勉強するとのことでしたらクラス構文を使うのも手かもしれません。

カウンターを設置して TypeScript の実装を確認

TypeScript を導入したので実装を確認します。
トップページにボタンを設置し、クリックした数をカウントする簡単なアクションを追加します。

絵文字ボタンとカウンターの導入

/pages/index.vueの [Documentation] と [GitHub] の下、<div class="links"></div>ブロックの直後に以下を追加します。

1
2
3
4
5
6
7
<div class="actions">
<a @click="addTada" class="button--action">🎉 {{ tada }}</a>
<a @click="addSparkles" class="button--action">✨ {{ sparkles }}</a>
<a @click="addThumbsup" class="button--action">👍 {{ thumbsup }}</a>
<a @click="addHeart" class="button--action">🧡 {{ heart }}</a>
<a @click="clear" class="button--grey">Clear</a>
</div>

カウンター表示用のデータプロパティを追加します。(dataのみ抜粋)

1
2
3
4
5
6
7
8
9
10
11
export default Vue.extend({
// ...(省略)
data() {
return {
tada: 0,
sparkles: 0,
thumbsup: 0,
heart: 0
}
},
})

@clickで呼び出されるメソッドを追加します。(methodsのみ抜粋)
各絵文字ボタンから呼び出されるメソッドは対応する変数をインクリメント、[Clear] は全変数を0に初期化します。各メソッドは TypeScript の型定義を導入し、戻り値:voidと明示しています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export default Vue.extend({
// ...(省略)
methods: {
addTada: function(): void {
this.tada++
},
addSparkles: function(): void {
this.sparkles++
},
addThumbsup: function(): void {
this.thumbsup++
},
addHeart: function(): void {
this.heart++
},
clear: function(): void {
this.tada = 0
this.sparkles = 0
this.thumbsup = 0
this.heart = 0
}
}
})

以上で、絵文字ボタンを押すと各ボタン横のカウンターが増える動作となります。
カウンターぐらいだと型のメリットが感じられませんが、たとえばclearの初期化で"0"のような文字列を入れるとコンパイルエラーとなります。しっかり TypeScript の型チェックが効いています。

カウンターのトータル回数、ロゴを回すアニメーションの導入

カウンターの値を使ったアクションを作りたいので、各カウンターのトータルを計算し、その値の回数ロゴを回してみます。

/pages/index.vueに、すべての絵文字カウンターの総計を持つ算術プロパティを追加します。(computedのみ抜粋)
counter()メソッドを作り、戻り値はnumber型、すべての絵文字カウンターの合計です。

1
2
3
4
5
6
7
8
export default Vue.extend({
// ...(省略)
computed: {
counter(): number {
return this.tada + this.sparkles + this.thumbsup + this.heart
}
},
})

ロゴを回転させるために、ロゴのコンポーネントへカウンターの値を渡します。
HTML テンプレートの<logo>タグにv-bind:counter="counter"属性を追加しcounterの値をロゴのコンポーネントで使えるようにします。

1
<logo v-bind:counter="counter" />

/components/Logo.vueで、カウンターの値を受け取りロゴを回転する CSS を追加します。
ロゴ全体を回転させるため、現在の HTML テンプレートの実装部分を<div :style="rotation">でラップします。
回転の CSS はstyle属性直接に、rotationプロパティ経由で設定します。

1
2
3
4
5
6
7
8
9
10
<template>
<div :style="rotation">
<div class="VueToNuxtLogo">
<div class="Triangle Triangle--two" />
<div class="Triangle Triangle--one" />
<div class="Triangle Triangle--three" />
<div class="Triangle Triangle--four" />
</div>
</div>
</template>

後はcounterの値を使って CSS を作るだけですが、CSS アニメーションを再実行できるようにするため、一手間かけています。(スクリプト全文)

  • 親コンポーネント/pages/index.vueで絵文字ボタンを押されると、props: counterに新しい値がセットされます。それをwatchで監視していて、変化があるとwatch: counterを呼び出します。
  • watch: counterは、dataプロパティにアニメーションの CSS をオブジェクトとして設定します。いったんアニメーション無し{ animation: 'none' }をセットし、10ミリ秒後に{ animation:rotate 1s linear 0s ${ value } forwards}をセットします。(アニメーション再実行のトリック)
  • 最後にcomputed: rotation経由で作られた CSS オブジェクトを HTML テンプレートへ渡します。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    export default Vue.extend({
    props: {
    counter: Number
    },
    data() {
    return {
    animation: {}
    }
    },
    computed: {
    rotation: function(): object {
    return this.animation
    }
    },
    watch: {
    counter: function(value: number): void {
    this.animation = { animation: 'none' }
    setTimeout(() => {
    this.animation = { animation: `rotate 1s linear 0s ${ value } forwards` }
    }, 10)
    }
    }
    })

だいぶ TypeScript の型が登場しました。
computed: rotationでは、戻り値がobjectで CSS の文字列ではないことがわかります。
watch: counterでは、引数がnumber型です。(フレームワークからの呼出し&テンプレートリテラルなので型明示のメリットはあまりないですが)

なお、CSS のアニメーション定義は下記です。

1
2
3
4
5
6
7
8
@keyframes rotate {
0% {
transform: rotateY(0);
}
100% {
transform: rotateY(360deg);
}
}

このくらいの規模だと型定義によるメリットはまだまだありませんが、アニメーションのためのトリックとは言え色々と取り回ししているので、他人の書いたこんなコードが急に出てきたと思うと型情報ぐらいあると嬉しいのではないでしょうか。

動作確認!

yarn devで開発サーバーを起動し、ブラウザでhttp://localhost:3000/へアクセスします。

※ 開発サーバーは常に立ち上げておいて大丈夫です。実際の開発に当たってはホットリロードを活用し、コードを保存するとブラウザが自動でリロードしてくれます。

ソースコード

今回作成した部分までのソースを GitHub へアップしました。
https://github.com/riotz-works/samples-pwa-base-app/tree/0.0.2

GitHub Pages にホスティングもしました。
(公開サイトは1つのため記事公開に合わせて変わり、本記事の内容とは異なります)
https://riotz.works/samples-pwa-base-app/


ベースアプリを TypeScript 化できました。
今回のカウンターボタンぐらいだとコーディング量が増えるだけでメリットが感じられませんが、他の人が書いた共通ライブラリを使うときなどに型があることで助かることが増えてきます。

型を導入できたところで Lint も入れたいところですが、こちらの話はまた重いので別途書きます。
もし使たい場合は、公式ドキュメントESLint を使った Linting - TypeScript サポート - Nuxt.jsをご参照ください。