Nuxt(vue) + TypeScriptをはじめるときに知っておきたかった10のこと
概要
1 年半前の自分にむけて Nuxt + TypeScript で開発する時に伝えたかった 10 のこと
(Nuxt + TypeScript の開発を 1 年半やった振り返りと反省)
社内 LT 用に作ったスライドはコチラ
注意事項
記載したコードはvue-property-decoratorを使用した記法になっています
その 1
props と $emit だと props で完結させた方がシンプルでいいぞ
props で関数を渡せるので関数渡したほうがシンプルでいいよねってお話(補完も効くようになるし)
atomic design でコンポーネント分けてたりすると、親 → 子 → 孫 → 孫孫みたいにコンポーネントのリレーが長くなった際に、props で完結させておくと props で渡した挙動だけしかしないので親で定義した部分を見るだけでいい。
それに対し、emit の場合、emit で定義された内容が入る余地があるので、子コンポーネントの emit で定義された内容も確認しないといけない。
emit を使った実装
子コンポーネント
<template>
  <p @click="onClick">{{ text }}</p>
</template>
<script lang="ts">
import { Component, Emit, Prop, Vue } from "nuxt-property-decorator";
@Component({
  name: "LinkText",
})
export default class LinkText extends Vue {
  @Prop() private readonly text!: string;
  @Emit() private onClick(): void {}
}
</script>
親コンポーネント
<template>
  <link-text text="なんかurl" @on-click="clickUrl()" />
</template>
<script lang="ts">
import { Component, Prop, Vue } from "nuxt-property-decorator";
import LinkText from "@/components/atoms/button/LinkText/index.vue";
@Component({
  name: "ParentComponent",
  components: {
    LinkText,
  },
})
export default class ParentComponent extends Vue {
  private clickUrl(): void {
    // なんかクリックされたときの処理
  }
}
</script>
props だけで完結させた場合
子コンポーネント
<template>
  <p @click="onClick">{{ text }}</p>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "nuxt-property-decorator";
@Component({
  name: "LinkText",
})
export default class LinkText extends Vue {
  @Prop() private readonly text!: string;
  @Prop() private onClick(): () => void;
}
</script>
親コンポーネント
<template>
  <link-text text="なんかurl" :on-click="clickUrl" />
</template>
<script lang="ts">
import { Component, Prop, Vue } from "nuxt-property-decorator";
import LinkText from "@/components/atoms/button/LinkText/index.vue";
@Component({
  name: "ParentComponent",
  components: {
    LinkText,
  },
})
export default class ParentComponent extends Vue {
  private clickUrl(): void {
    // なんかクリックされたときの処理
  }
}
</script>
その 2
Storybook は導入すべし
- atoms や molecules などのコンポーネントの動作確認が楽になった
- こまめにデザイナさんレビューを入れられるようになった
- コンポーネント作成するだけであれば他の開発環境に依存しなくなった
その 3
atomic design のルールはしっかり決めとくべき
特に atoms と molecules の関係をしっかり決めておく
page
template 一つ以外持たない
data, method が全部この page に集約されてる
template
主に UI のワイヤーフレームをここで定義する
organisms
form や card や navigation などワイヤーフレームを構成してる要素たち
molecules
atom を組み合わせてつくる
atom にないものは atom として作る
atoms
コンポーネントの最小単位
その 4
props の型の置き場所に気を付けろ!
コンポーネントの props の型はコンポーネントと同じ階層に types.ts を用意し、
そこで定義することで、model からコンポーネントを切り離せる
その 5
似た様な method や data を持つコンポーネントには基底クラスが使えるぞ
import { Component, Vue } from "nuxt-property-decorator";
import { LoadingDialogProps } from "@/components/organisms/dialog/LoadingDialog/types";
@Component({
  name: "BasePage",
})
export class BasePage extends Vue {
  protected errorDialog: ErrorDialogProps = {
    title: "エラー",
    content: "データ処理に問題がありました。",
    isShowDialog: false,
  };
  protected handleCloseErrorDialog(): void {
    this.errorDialog.isShowDialog = false;
  }
  protected handleOpenErrorDialog(): void {
    this.errorDialog.isShowDialog = true;
  }
}
<template>
  <hoge-template :error-dialog="errorDialog" />
</template>
<script lang="ts">
import { Component } from "nuxt-property-decorator";
import { BasePage } from "@/components/pageModules/base";
import HogeTemplate from "@/components/template/hoge/Template/index.vue";
@Component({
  name: "hogePage",
  components: {
    HogeTemplate,
  },
})
export default class HogePage extends BasePage {
  // このコンポーネント独自の実装
}
</script>
その 6
ESLint は有能だよ
あんなに手で頑張ってた import 順は簡単に設定できて format もできるんやぞ
"eslint-plugin-import": "2.22.1",
"eslint-plugin-simple-import-sort": "5.0.3",
module.exports = {
  rules: {
    "simple-import-sort/sort": "error",
    "sort-imports": "off",
    "import/first": "error",
    "import/newline-after-import": "error",
    "import/no-duplicates": "error",
  },
};
その 7
TypeScript の interface と type alias の違いを知れ
より厳密にするなら(props の型とか)type 使った方がよかった
api のレスポンス等、意図せぬタイミングで型が変わる場合は interface 使う?
その 8
ハンドラの命名規則は揃えるとかっこいい
template 以下のコンポーネントの場合は on〇〇(onClick や onChange)
page の場合は handle〇〇(handleSubmit や handleUpload)
atom や molecule はより完結に(onClick)
template には複数のコンポーネントのon〇〇が来る場合があるのでより詳細に(onClickCloseDialog, onClickSubmitForm とか)
その 9
API と component の依存関係を断ち切れっ
atoms から開発し、atoms は他の何にも依存しないように実装する
molecules 以降は自身より小さいコンポーネントのみに依存する様にする
また API のレスポンスをそのまま子コンポーネントの props にしない(API に依存させない)
その 10
props はひとまとめに
template の props は多くなりがちになるので
organisms 以下のコンポーネントは props を一つにまとめる
悪い例
どの props がどの子のコンポーネントに使われてるのかわかりづらい
<template>
  <line-channel-create-template
    :content-titles="['チャネル登録']"
    :current-stepper-num="currentStepperNum"
    :error-dialog="errorDialog"
    :how-to-component-name="howToComponentName"
    :is-created-line-login-channel="isCreatedLineLoginChannel"
    :is-created-messaging-api-channel="isCreatedMessagingApiChannel"
    :is-duplicated-messaging-api-channel="isDuplicatedMessagingApiChannel"
    :is-linked-channels="isLinkedChannels"
    :is-progress="isProgress"
    :is-show-how-to="isShowHowTo"
    :is-show-introduction-dialog="isShowIntroductionDialog"
    :line-channel-id="lineChannelId"
    :messaging-api-name="lineChannelName"
    :prevent-dialog="preventDialog"
    :total-steps="totalSteps"
    @click-close-error-dialog-btn="errorDialog.isShowDialog = false"
    @click-close-introduction-dialog-btn="isShowIntroductionDialog = false"
    @click-close-how-to-btn="isShowHowTo = false"
    @click-created-line-login-channel-btn="showLineLoginChannelForm"
    @click-created-messaging-api-channel-btn="showMessagingApiChannelForm"
    @click-how-to-btn="showHowTo"
    @click-linked-channels-btn="linkedChannels"
    @click-pasted-btn="completedCurrentStep"
    @click-prevent-dialog-action-btn="stayPage"
    @click-prevent-dialog-cancel-btn="leavePage"
    @close-prevent-dialog="preventDialog.showDialog = false"
    @click-show-introduction-btn="isShowIntroductionDialog = true"
    @submit-line-login-channel-form="registerLineLoginChannel"
    @submit-messaging-api-channel-form="registerMessagingApiChannel"
  />
</template>
いい例
なんということでしょう・・
あれほど散らかっていた props がこんなに・・
どのコンポーネントで使われる props なのか一目瞭然
<template>
  <enquete-create-template
    :error-dialog-props="errorDialogProps"
    :enquete-form-props="enqueteForm"
    :loading-dialog-props="loadingDialog"
    :draft-completed-dialog-props="draftCompletedDialog"
  />
</template>



Discussion
とても参考になります。
いくつか質問してもいいですか?
(this as any).~~~の記述が多くなったりしないですか?(asyncDataの返り値がthis解釈してくれないのとか)ケースにもよるとは思うんですが(メンバの習熟度とか、vueの方がhtml cssっぽくてマークアップしてた人が入りやすいとか)
reactでts書くのとvueでts書くのを比較した時に、reactで書いてた方が簡単に全体を型チェックなりエディタで保管してくれるのでtsを入れるのであれば次はreactを選定したいなと個人的に思ってます
(vueのv3をまだ追えてなくて、v3になるとtypescritp対応が進んでたりcompositionAPIだったりで、また違うのかもしれません)
妥協してますね・・・
しかもテストコードについてはどこまで書くのがいいのか決めかねているところです
現状はコンポーネントのテストはstoryshots使ってimage snap shotテストするのと、ユニットテストはコンポーネント外のメソッド等(utilとか)に書くのがいいのかなとか思ってたりします
(this as any)の記述は基本的にないですね!asyncDataをあまり使ってないのでもしかしたら解釈間違ってるかもですが
こんな感じでdata名をオブジェクトのプロパティ名と同じにしたらそのdataに値を入れてくれる認識でした
あんまり今のところ変な挙動はしてないですね・・
デコレータの書き方が好きなのでデコレータ入れたいんですが、そもそも
stage 2 proposalなので仕様がどんどん変わっていった際に追えるかどうかと言われると厳しい気がするのでVue.extendの方を採用するかもですvueのv3に期待して・・
丁寧に回答ありがとうございます。
(xxx as any)みたいな書き方が増えることが多かったので。。。