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

7 min read読了の目安(約7000字 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>

この記事に贈られたバッジ