🕌

Nuxt(vue) + TypeScriptをはじめるときに知っておきたかった10のこと

2020/11/23に公開3

概要

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 は導入すべし

その 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 を持つコンポーネントには基底クラスが使えるぞ

base.ts
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;
  }
}
hoge.vue
<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 使う?
https://qiita.com/tkrkt/items/d01b96363e58a7df830e#比較まとめ

その 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>
GitHubで編集を提案

Discussion

サトウのごはんサトウのごはん

とても参考になります。
いくつか質問してもいいですか?

  1. reactで書いとけばよかったーって気持ちになります?(vue×tsは相性悪いイメージがあるので)
  2. テストコードで.vueを呼び出すとき、ほぼ型の機能が得られないのは妥協してますか?(全部Vueの同じ型になる)
  3. (this as any).~~~の記述が多くなったりしないですか?(asyncDataの返り値がthis解釈してくれないのとか)
  4. vscodeのVueturが変な挙動して.vueのts周りがおかしくなったりしないですか?(ts とだけメッセージ出たり)
  5. デコレータを使用した書き方、とVue.extendを使用する書き方、今ならどちらを選択しますか?
nus3nus3

reactで書いとけばよかったーって気持ちになります?(vue×tsは相性悪いイメージがあるので)

ケースにもよるとは思うんですが(メンバの習熟度とか、vueの方がhtml cssっぽくてマークアップしてた人が入りやすいとか)
reactでts書くのとvueでts書くのを比較した時に、reactで書いてた方が簡単に全体を型チェックなりエディタで保管してくれるのでtsを入れるのであれば次はreactを選定したいなと個人的に思ってます
(vueのv3をまだ追えてなくて、v3になるとtypescritp対応が進んでたりcompositionAPIだったりで、また違うのかもしれません)

テストコードで.vueを呼び出すとき、ほぼ型の機能が得られないのは妥協してますか?(全部Vueの同じ型になる)

妥協してますね・・・
しかもテストコードについてはどこまで書くのがいいのか決めかねているところです
現状はコンポーネントのテストはstoryshots使ってimage snap shotテストするのと、ユニットテストはコンポーネント外のメソッド等(utilとか)に書くのがいいのかなとか思ってたりします

(this as any).~~~の記述が多くなったりしないですか?(asyncDataの返り値がthis解釈してくれないのとか)

(this as any)の記述は基本的にないですね!
asyncDataをあまり使ってないのでもしかしたら解釈間違ってるかもですが

private item: Item | undiend = undefined
// 返り値の型はちゃんと定義した方がいいですがいったん適当
private async asyncData({ app }: Context): Promise<{item: Item | undefined}> {
  // $axiosのラッパー的なやつでget()読んだら Item | undiendを返してくれると思ってもらえれば
  const item = await app.$httpClient.get()
  // エラーハンドリングとか省いてます
  return { item }
}

こんな感じでdata名をオブジェクトのプロパティ名と同じにしたらそのdataに値を入れてくれる認識でした

vscodeのVueturが変な挙動して.vueのts周りがおかしくなったりしないですか?(ts とだけメッセージ出たり)

あんまり今のところ変な挙動はしてないですね・・

デコレータを使用した書き方、とVue.extendを使用する書き方、今ならどちらを選択しますか?

デコレータの書き方が好きなのでデコレータ入れたいんですが、そもそもstage 2 proposalなので仕様がどんどん変わっていった際に追えるかどうかと言われると厳しい気がするのでVue.extendの方を採用するかもです
vueのv3に期待して・・

Decorators are a stage 2 proposal for JavaScript and are available as an experimental feature of TypeScript.

https://www.typescriptlang.org/docs/handbook/decorators.html

サトウのごはんサトウのごはん

丁寧に回答ありがとうございます。

  1. やっぱりそうですよね。結構vueとなじめないとこが多くて悩んでいます。。
  2. .vueの型付けどうにかしてほしいですよね
  3. なるほどー、丁寧にサンプルまでありがとうございます。あとはmixinを使ったときとか、テストから呼び出したときとか、(xxx as any)みたいな書き方が増えることが多かったので。。。
  4. これはすみません、vscode-insider版の不具合でした。
  5. vue.extendの方を採用していますがこっちはこっちで型補完が弱くてこまっていますね。迷いどころですよね。