上記のプロジェクトで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にも同じような内容で投稿しようかと思います