上記のプロジェクトでComposition API とTypeScriptを組み込んでみたので、その時の考えをまとめて記事にしました。
前置き
Composition API とは
Composition API RFC | Vue Composition API
Composition APIについては上記の公式ホームページの概要を読むのが良いと思います。
メリット的には以下みたいなものがあげられます
- 型推論がしやすい形式でVueが書ける
- Vueのコードを分割しやすくなる
Vue v3では標準で使用できます。Vue v2でも以下のライブラリを使用すれば実装できます
Vue.jsのTSXサポート
以下のライブラリを使用することでvueをtsxで書くことができます。
vueでComposition API + TSXで書かれたデモが、こちらになります。
ファイルの拡張子をtsxにするか、vueファイルで <script lang="tsx">
にするかで利用可能になります。
NuxtでTypeScriptを採用した時の実装パターン
上記の事を踏まえた上で、現状Nuxtでの実装パターン考えると、こんな感じ
- パターン①. Composition API + TSXファイル + CSS Modules
- パターン②. Composition API + Vueファイル +
<script lang="tsx">
- パターン③. Composition API + Vueファイル +
<script lang="ts">
各パターンについては以下でデモを作成したので、これを元に解説
では、各パターンについて解説していきます。
パターン①. Composition API + TSXファイル + CSS Modules
コード的に、こんな感じになります。
import { defineComponent, SetupContext } from '@vue/composition-api' import firebase from 'firebase' import Login from '~/components/templates/login/index.tsx' export type LoginType = { fbGoogleLogin: () => Promise<void> } export default defineComponent({ layout: 'simple', setup(_, context: SetupContext) { const fbGoogleLogin = async () => { try { const googleProvider = new firebase.auth.GoogleAuthProvider() await context.root.$fireAuth.signInWithPopup(googleProvider) context.root.$fireAuth.onAuthStateChanged(() => { context.root.$router.push('/') }) } catch (e) { console.log(e) } } return () => ( <div> <Login fbGoogleLogin={fbGoogleLogin} /> </div> ) }, })
setupのreturnに、そのままTSX形式で記載します。
TSXにする場合は、Vueの<template>
や<style>
は使用できません。
なので、TSXにする場合のstyleの宣言にはCSS Modulesを使用します。 書き方は以下の通り
■ tsx-demo/components/templates/login/index.tsx
import { defineComponent } from '@vue/composition-api' import style from './index.module.scss?module' import { LoginType } from '~/pages/login.tsx' export default defineComponent({ props: { fbGoogleLogin: { type: Function, required: true }, }, setup(props: LoginType) { return () => ( <div class={style.root}> <div class={style.icon}> <v-img src="/logo.png" alt="logo" width="50px" height="50px" contain /> </div> <v-sheet tile class="mx-auto" height="300" width="100%" max-width="480" elevation="2" > <div class={style['form-container']}> <div class={style.title}>新規会員登録</div> <v-divider class="divider" /> <div class={style['button-container']}> <v-btn x-large class="text-transform-none" block outlined onClick={props.fbGoogleLogin} > <v-icon left color="secondary"> mdi-google-plus </v-icon> Googleで登録する </v-btn> </div> <div class={style.guide}> <div class={style.link}> <v-btn x-small class="text-transform-none" text color="primary" href="https://amazing-hawking-a280c3.netlify.com/general/account/" target="_blank" > 会員登録するとできること <v-icon small left> mdi-chevron-right </v-icon> </v-btn> </div> </div> </div> </v-sheet> <div class={style.links}> <v-btn x-small class="text-transform-none" text href="https://peperomia.app/tos" target="_blank" > 利用規約 </v-btn> | <v-btn x-small class="text-transform-none" text href="https://peperomia.app/policy" target="_blank" > プライバシーポリシー </v-btn> </div> </div> ) }, })
■ tsx-demo/components/templates/login/index.module.scss
@import '~/assets/variables.scss'; .root { display: flex; justify-content: center; align-items: center; flex-direction: column; height: 100%; } .icon { display: flex; justify-content: center; align-items: center; padding: 2rem; } .form-container { display: flex; justify-content: center; flex-direction: column; .title { text-align: center; padding-top: 15px; } .divider { margin: 15px; } .button-container { display: flex; justify-content: center; flex-direction: column; padding: 25px 60px; width: 100%; } .guide { position: relative; height: 100px; .link { position: absolute; left: 35%; bottom: 0; } } } .links { display: flex; justify-content: center; align-items: center; height: 50px; font-size: 12px; }
内容的には以下の部分でscssファイルをimportして
import style from './index.module.scss?module'
importしたstyleをclassに設定して使用できます。
<div class={style.title}>新規会員登録</div>
また、TSXは一部Vueのテンプレート構文とは異なる構文があります。
例えばvueファイルでは、クリックイベントのハンドラーを以下のように書きますが、
<v-btn @click="onClick">登録する</v-btn>
TSXでは@click
はonClick
に変わります
<v-btn onClick={props.onClick}>登録する</v-btn>
この辺の微妙な違いはドキュメントに記載してあります。
GitHub - wonderful-panda/vue-tsx-support: TSX (JSX for TypeScript) support library for Vue
パターン②. Composition API + Vueファイル + <script lang="tsx">
コードはほぼパターン①と変わりませんが、vueファイル内にtsxを宣言しているためVueの<style>
を使用できます。
コードは以下の通りです。
■ vue-script-tsx-demo/components/templates/login/index.vue
<script lang="tsx"> import { defineComponent } from '@vue/composition-api' import { LoginType } from '~/pages/login.vue' export default defineComponent({ props: { fbGoogleLogin: { type: Function, required: true }, }, setup(props: LoginType) { return () => ( <div class="root"> <div class="icon"> <v-img src="/logo.png" alt="logo" width="50px" height="50px" contain /> </div> <v-sheet tile class="mx-auto" height="300" width="100%" max-width="480" elevation="2" > <div class="form-container"> <div class="title">新規会員登録</div> <v-divider class="divider" /> <div class="button-container"> <v-btn x-large class="text-transform-none" block outlined onClick={props.fbGoogleLogin} > <v-icon left color="secondary"> mdi-google-plus </v-icon> Googleで登録する </v-btn> </div> <div class="guide"> <div class="link"> <v-btn x-small class="text-transform-none" text color="primary" href="https://amazing-hawking-a280c3.netlify.com/general/account/" target="_blank" > 会員登録するとできること <v-icon small left> mdi-chevron-right </v-icon> </v-btn> </div> </div> </div> </v-sheet> <div class="links"> <v-btn x-small class="text-transform-none" text href="https://peperomia.app/tos" target="_blank" > 利用規約 </v-btn> | <v-btn x-small class="text-transform-none" text href="https://peperomia.app/policy" target="_blank" > プライバシーポリシー </v-btn> </div> </div> ) }, }) </script> <style lang="scss" scoped> @import '~/assets/variables.scss'; .root { display: flex; justify-content: center; align-items: center; flex-direction: column; height: 100%; } .icon { display: flex; justify-content: center; align-items: center; padding: 2rem; } .form-container { display: flex; justify-content: center; flex-direction: column; .title { text-align: center; padding-top: 15px; } .divider { margin: 15px; } .button-container { display: flex; justify-content: center; flex-direction: column; padding: 25px 60px; width: 100%; } .guide { position: relative; height: 100px; .link { position: absolute; left: 35%; bottom: 0; } } } .links { display: flex; justify-content: center; align-items: center; height: 50px; font-size: 12px; } </style>
<script lang="tsx">
を記載することでtsx形式で書くことができます。
vueファイルで実装しているのでパターン①と違いファイルを1つにまとめられます。
また、tsxで書くので、<template>
はありません。
パターン③. Composition API + Vueファイル + <script lang="ts">
3パターンの中で最も標準的なVueのコードに近いと思います。 コードは以下の通りです。
■ vue-script-ts-demo/components/templates/login/index.vue
<template> <div class="root"> <div class="icon"> <v-img src="/logo.png" alt="logo" width="50px" height="50px" contain /> </div> <v-sheet tile class="mx-auto" height="300" width="100%" max-width="480" elevation="2" > <div class="form-container"> <div class="title">新規会員登録</div> <v-divider class="divider" /> <div class="button-container"> <v-btn x-large class="text-transform-none" block outlined @click.prevent="fbGoogleLogin" > <v-icon left color="secondary">mdi-google-plus</v-icon> Googleで登録する </v-btn> </div> <div class="guide"> <div class="link"> <v-btn x-small class="text-transform-none" text color="primary" href="https://amazing-hawking-a280c3.netlify.com/general/account/" target="_blank" > 会員登録するとできること <v-icon small left>mdi-chevron-right</v-icon> </v-btn> </div> </div> </div> </v-sheet> <div class="links"> <v-btn x-small class="text-transform-none" text href="https://peperomia.app/tos" target="_blank" > 利用規約 </v-btn> | <v-btn x-small class="text-transform-none" text href="https://peperomia.app/policy" target="_blank" > プライバシーポリシー </v-btn> </div> </div> </template> <style lang="scss" scoped> @import '~/assets/variables.scss'; .root { display: flex; justify-content: center; align-items: center; flex-direction: column; height: 100%; } .icon { display: flex; justify-content: center; align-items: center; padding: 2rem; } .form-container { display: flex; justify-content: center; flex-direction: column; .title { text-align: center; padding-top: 15px; } .divider { margin: 15px; } .button-container { display: flex; justify-content: center; flex-direction: column; padding: 25px 60px; width: 100%; } .guide { position: relative; height: 100px; .link { position: absolute; left: 35%; bottom: 0; } } } .links { display: flex; justify-content: center; align-items: center; height: 50px; font-size: 12px; } </style> <script lang="ts"> import { defineComponent } from '@vue/composition-api' import { LoginType } from '~/pages/login.vue' export default defineComponent({ props: { fbGoogleLogin: { type: Function, required: true }, }, setup(props: LoginType) { return { fbGoogleLogin: props.fbGoogleLogin, } }, }) </script>
<script lang="ts">
を記載することでTypeScriptでscript部分を書くことができます。
scriptがTypeScriptになっただけなので、Vueの<template>
や<style>
を使用できます。
各パターンの評価
■ TypeScriptの恩恵が受けられる
パターン① ≧ パターン② > パターン③
パターン①はTSXファイルなので、TSX部分でも型推論ができて、VSCodeでも補完が効きます。
それに比べるとパターン③はVueの<template>
には型は適用されないので、型チェックやVSCodeの補完は効きません。
(※追記)
パターン③はVueの
<template>
には型は適用されないので、型チェックやVSCodeの補完は効きません。
↑の通り実はveturのコマンドラインでチェックする方法があるみたいなので、ここは後で別途記事を書こうと思います
■ 書きやすさ( + Vueっぽさ)
パターン③ > パターン② > パターン①
やはり素のVueと同様の書き方ができるパターン③が書きやすいと思います。 パターン①とパターン②を比べるとcssをわけなくてよいので、パターン②の方がVueっぽく書けます。
最終的に採用したパターン
最終的にペペロミアのWeb版ではパターン③の Composition API + Vueファイル + <script lang="ts">
を採用することにしました。
TSXを採用しなかった1番の理由は、Vueのテンプレート構文とTSXで書き方が変わってしまうところです。
Vueのテンプレート構文とTSXで書き方が変わる例
例①: クリックイベントはonClickを使用する
これは上記でも記載していますが、以下の違いがあります。
■ Vueのテンプレート構文
<v-btn @click="onClick">登録する</v-btn>
■ TSX
<v-btn onClick={props.onClick}>登録する</v-btn>
例②: v-slotはscopedSlotsを使用する
■ Vueのテンプレート構文
<v-calendar :now="dayjs().format('YYYY-MM-DD')" :value="dayjs(calendarDate).format('YYYY-MM-DD')" > <template v-slot:day="{ date }"> <div> {{ date }} </div> </template> </v-calendar>
■ TSX
<v-calendar now={dayjs().format('YYYY-MM-DD')} value={dayjs(props.calendarDate).format('YYYY-MM-DD')} scopedSlots={{ day: ({ date }: { date: string }) => { return (<div> {{ date }} </div>) }, }} />
例③: v-if、v-for、v-modelが使用できない
v-ifは、JSのif文や三項演算子を使用する。 v-forは、mapで書く。
https://github.com/vuejs/babel-plugin-transform-vue-jsx/issues/38
v-modelは使用できないのでonChange等を使用して値を受け渡す形式にする
と、こんな感じに違いがあります。
TSXを採用しなかった理由
上記のVueのテンプレート構文とTSXの差異とTSXで得られる型推論のメリットを比較して、Vueのテンプレート構文が使えるメリットの方が勝っていると感じためTSXの採用は見送りました。
実際に実装してみた感想
- TypeScriptに関しては、やはりVueより、ReactやAngularの方が相性が良さそう。
- SSRを必須でTypeScriptもガンガン使いたいというプロジェクトならNuxt + TSXの組み合わせは候補になるかも(逆にそれ以外の状況では、個人的に採用は無さそう)
- また、TSXを採用するとVuetifyのdemoページのコピペで動かなくなるとかも、わりかし辛いなと思う
demoのレポジトリを整理したらqiitaにも同じような内容で投稿しようかと思います