LoginSignup
42
27

More than 5 years have passed since last update.

create-nuxt-appで構築したNuxt2のプロジェクトにTypeScriptを入れてvuexを使えるところまで持っていく

Last updated at Posted at 2018-10-03

はじめに

Nuxt1を軽く触った程度なのと、tsほぼ未経験の状態でこの記事を記載していますので間違っているところはぜひご指摘頂けますと幸いです🙇

前提

  • nuxt
    • 2.0.0
  • typescript
    • 3.1.1

やってみる

Nuxt2をインストールする

執筆時点ではver2インストールの和訳はされていないのでenページの手順通り行います。
npxを使ったことがなかったので今回はnpxでやってみます。

npx create-nuxt-app sample-project

色々聞かれますが、今回は以下の通りにしてみました。
スクリーンショット 2018-10-02 18.45.25.png

項目 選択肢 補足
Project name 適当
Project description 適当
Use a custom server framework none 素のやつ以外使ったことない😇
Use a custom UI framework vuetify vuesax使ってたけどこの選択肢の中だったらなんとなくこれかな、くらいの気持ち
Choose rendering mode Single Page App SPAしかやったことない😇し、SSRも必要なかった
Use axios module yes
Use eslint no TypeScript使うので
Use prettier no TypeScript使うので
Author name 適当
Choose a package manager yarn

無事インストール完了しました。

スクリーンショット 2018-10-02 18.45.35.png

TypeScriptを入れる

パッケージ導入

まずはtypescriptとtsの関連パッケージを入れます。

yarn add typescript ts-loader tslint tslint-config-prettier --dev

今回はデコレータを使いたかったのでデコレータも入れます。

yarn add nuxt-property-decorator --save

関連ファイル追加

次に、.tsを読み込むための定義ファイルを作成します。

ファイルの内容はnuxt-communityのテンプレをそのまま使いまわします。

modules/typescript.js
module.exports = function() {
  // Add .ts extension for store, middleware and more
  this.nuxt.options.extensions.push("ts")
  // Extend build
  this.extendBuild(config => {
    const tsLoader = {
      loader: "ts-loader",
      options: {
        appendTsSuffixTo: [/\.vue$/]
      }
    }
    // Add TypeScript loader
    config.module.rules.push(
      Object.assign(
        {
          test: /((client|server)\.js)|(\.tsx?)$/
        },
        tsLoader
      )
    )
    // Add TypeScript loader for vue files
    for (let rule of config.module.rules) {
      if (rule.loader === "vue-loader") {
        rule.options.loaders = {
          ts: "tsLoader",
        }
      }
    }
    // Add .ts extension in webpack resolve
    if (config.resolve.extensions.indexOf(".ts") === -1) {
      config.resolve.extensions.push(".ts")
    }
  })
}

typescript.jsをnuxt.config.jsで読み込むようにします。

nuxt.config.js
  modules: [
    '@nuxtjs/axios',
    '~/modules/typescript.js' // この行を追加
  ],

次に、.vueを型定義するためのindex.d.tsを作成します。

ファイルの内容はmodules/typescript.js同様nuxt-communityのテンプレをそのまま使いまわします。

index.d.ts
declare module "*.vue" {
  import Vue from "vue"
  const _default: Vue
  export default _default
}

続いて、tsコンパイルの指定をする為にtsconfig.jsonを作成します。

こちらもnuxt-communityのテンプレをそのまま使いまわします。

tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "es2015"],
    "module": "es2015",
    "moduleResolution": "node",
    "experimentalDecorators": true,
    "noImplicitAny": false,
    "noImplicitThis": false,
    "strictNullChecks": true,
    "removeComments": true,
    "suppressImplicitAnyIndexErrors": true,
    "allowSyntheticDefaultImports": true,
    "allowJs": true,
    "baseUrl": ".",
    "paths": {
      "~/*": ["./*"]
    }
  }
}

tslintも入れたので設定を入れちゃいます。
これはnuxt-communityにないのでひとまず自作のものにしておきます。

tslint.json
{
  "defaultSeverity": "error",
  "extends": [
    "tslint:recommended",
    "tslint-config-prettier"
  ],
  "jsRules": {},
  "rules": {
    "no-namespace": false,
    "no-string-literal": false,
    "max-line-length": {
      "options": {"limit": 120},
      "severity": "warn"
    },
    "interface-name": [true,  "never-prefix"],
    "ordered-imports": false,
    "object-literal-sort-keys": false
  },
  "rulesDirectory": []
}

js→tsに書き換えてみる

これでtypescriptが使えるようになったので、テンプレのlayouts/default.vueファイルにtypescriptを組み込んでみます。

変更前はこちら

layouts/default.vue
// /////////
// before
// /////////
// templateは省略
<script>
  export default {
    data() {
      return {
        clipped: false,
        drawer: true,
        fixed: false,
        items: [
          { icon: 'apps', title: 'Welcome', to: '/' },
          { icon: 'bubble_chart', title: 'Inspire', to: '/inspire' }
        ],
        miniVariant: false,
        right: true,
        rightDrawer: false,
        title: 'Vuetify.js'
      }
    }
  }
</script>

変更後はこちら

layouts/default.vue
// /////////
// after
// /////////
// templateは省略
<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'

@Component
export default class extends Vue {
  clipped = false
  drawer = true
  fixed = false
  items = [
    { icon: 'apps', title: 'Welcome', to: '/' },
    { icon: 'bubble_chart', title: 'Inspire', to: '/inspire' }
  ]
  miniVariant = false
  right = true
  rightDrawer = false
  title = 'Vuetify.js'
}
</script>

余談

ちょっと冗長ですが、現時点では.vueをtsにする場合、

xxx.vue
<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'

@Component
export default class extends Vue {}
</script>

こいつを決め打ちで入れてやる必要があるのかなと思います。

デコレータの書き方はREADMEを見ればわかるかなと思います。
(data()の書き方がわからなくてちょっと焦りました😅)

vuexのテンプレをつくる

TS初心者にはこれが結構きつかったです😅

調べた(ザッピング程度ですが)ところ、こちらの記事がすごく親切だったので参考にさせて頂きました。
※そのままだとtslintで怒られたりうまく動かないところがあったのでところどころ修正しています

やっていることの概要

導入方法の前にやっていることをざっくり説明します。

やっていることは、

  • vuex共通の型定義をする
  • versionというプロパティを作成する(stateだけのモデル)
  • profileというモデルを作成する

こんな感じです。

パッケージの導入

まずはライブラリを入れます。

yarn add vuex-class --save

関連ファイル追加

次に、vuexで最終的に全てのモデルをまるっと入れておくための型を定義します。

store/types.ts
export interface RootState {
  version: string
}

続いて、共通の型定義ファイルを作成します。

store/vuex/types/index.d.ts
import {Action, Mutation, Getter} from 'vuex'

export interface ActionTree<S, R> {
  [key: string]: Action<S, R>
}
export interface MutationTree<S> {
  [key: string]: Mutation<S>
}
export interface GetterTree<S, R> {
  [key: string]: Getter<S, R>
}
export interface Module<S, R> {
  namespaced?: boolean
  state?: S | (() => S)
  getters?: GetterTree<S, R>
  actions?: ActionTree<S, R>
  mutations?: MutationTree<S>
}

次に、ベースとなるindex.tsを作成します。

store/index.ts
import Vue from 'vue'
import Vuex, { StoreOptions } from 'vuex'
import { RootState } from '~/store/types'
import { profile } from './profile/index'

Vue.use(Vuex)

const store: StoreOptions<RootState> = {
  modules: {
    profile
  },
  state: {
    version: '1.0.0' // a simple property
  }
}

const createStore = () => {
  return new Vuex.Store<RootState>(store)
}

export default createStore

最後に、profileモデルの為に必要なファイルを作成します。

store/profile/index.ts
import { Module } from 'vuex'
import { getters } from './getters'
import { actions } from './actions'
import { mutations } from './mutations'
import { ProfileState } from './types'
import { RootState } from '../types'

export const state: ProfileState = {
  user: undefined,
  error: false
}

const namespaced: boolean = true

export const profile: Module<ProfileState, RootState> = {
  namespaced,
  state,
  getters,
  actions,
  mutations
}
store/profile/actions.ts
import { ActionTree } from 'vuex'
// APIは別の話なので今回は固定値としておきます
// import axios from 'axios'
import { ProfileState, User } from './types'
import { RootState } from '../types'

export const actions: ActionTree<ProfileState, RootState> = {
  fetchData({ commit }): any {
    // APIは別の話なので今回は固定値としておきます
    // axios({
    //   url: 'https://....'
    // }).then((response) => {
    //   const payload: User = response && response.data
    //   commit('profileLoaded', payload)
    // }, (error) => {
    //   console.log(error)
    //   commit('profileError')
    // })
    const payload = {
      firstName: 'firstNameのデータ',
      lastName: 'lastNameのデータ',
      email: 'sample@example.com',
      phone: '+819000000000'
    }
    commit('profileLoaded', payload)
  }
}
store/profile/getters.ts
import { GetterTree } from 'vuex'
import { ProfileState } from './types'
import { RootState } from '../types'

export const getters: GetterTree<ProfileState, RootState> = {
  fullName(state): string {
    const { user } = state
    const firstName = (user && user.firstName) || ''
    const lastName = (user && user.lastName) || ''
    return `${firstName} ${lastName}`
  }
}
store/profile/mutations.ts
import { MutationTree } from 'vuex'
import { ProfileState, User } from './types'

export const mutations: MutationTree<ProfileState> = {
  profileLoaded(state, payload: User) {
    state.error = false
    state.user = payload
  },
  profileError(state) {
    state.error = true
    state.user = undefined
  }
}
store/profile/types.ts
export interface User {
  firstName: string
  lastName: string
  email: string
  phone?: string
}

export interface ProfileState {
  user?: User
  error: boolean
}

確認してみる

vuex側のテンプレは以上です。
vuexからデータを引っ張ってくるサンプルページを作成します。

pages/sample.vue
<template>
  <div class="container">
    <p>
      version: {{ version }}
    </p>
    <div v-if="profile.user">
      <p>
        Full name: {{ fullName }}
      </p>
      <p>
        Email: {{ email }}
      </p>
    </div>
    <div v-if="profile.error">
      Oops an error occured
    </div>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import { State, Action, Getter } from 'vuex-class'
import Component from 'vue-class-component'
import { ProfileState, User } from '~/store/profile/types'
const namespace: string = 'profile'
@Component
export default class UserDetail extends Vue {
  @State('version') version: string
  @State('profile') profile: ProfileState
  @Action('fetchData', { namespace }) fetchData: any
  @Getter('fullName', { namespace }) fullName: string

  mounted() {
    // fetching data as soon as the component's been mounted
    this.fetchData()
  }

  // computed variable based on user's email
  get email() {
    const user = this.profile && this.profile.user
    return (user && user.email) || ''
  }
}
</script>

作成したページのメニューを追加します。

layouts/default.vue
// templateは省略
<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'

@Component
export default class extends Vue {
  clipped = false
  drawer = true
  fixed = false
  items = [
    { icon: 'apps', title: 'Welcome', to: '/' },
    { icon: 'bubble_chart', title: 'Inspire', to: '/inspire' },
    { icon: 'account_box', title: 'Sample', to: '/sample' } // ここを追加
  ]
  miniVariant = false
  right = true
  rightDrawer = false
  title = 'Vuetify.js'
}
</script>

長くなりましたがようやく完成です!
確認してみます!

スクリーンショット 2018-10-03 15.19.46.png

無事にデータが表示出来ました!!!

蛇足(scssを使えるようにする)

タイトルと関係ないですね😅
が、scssも結構入れる人多いと思うので記載しておきます。

必要なパッケージを導入します。

yarn add node-sass sass-loader --dev

まずは.vueファイルに記載する場合、

xxx.vue
// template, scriptは省略
<style lang="scss" scoped>
.sample {
  .text {
    color: blue;
  }
}
</style>

これだけです。

次に、共通ファイルを読み込む場合、

assets/sass/mixins.scss
.sample {
  .text {
    background-color: red;
  }
}

ファイルを作成して、

nuxt.config.js
  css: [
    '~/assets/style/app.styl',
    '~/assets/sass/mixins.scss' // ここに追加
  ],

これだけですね。
ちょっとここ定かではないんですが、Nuxt2になってsass-resource-loaderなしで行けるようになったんですかね?
外部のscssファイルを読み込むだけなら大丈夫ですが、その中で変数を使う場合はnuxt-sass-resource-loaderが必要でした。
こちらの記事ですごく丁寧に書かれていますね。

■追記■
デフォでstylus入ってたので無理にscssを使えるようにする必要もなかったですね😅

さいごに

vuexの部分、今回は参考にさせて頂いた記事を真似てactions, getters, mutations, typesをファイル分割しましたが、よほどでかくならない限り個人的には1ファイルでも良いのかなぁと思ったりしました。

サンプル通りに設定してみたつもりだけどうまく動かない😇、みたいなことが度々あったのでもう少し中身を理解しないといけないよなと痛感しました(当たり前

Vue3系もTypeScriptで書き直されるとのことですし、もうTypeScript化の流れから逃げることは出来ないと思うので、私と同じくTypeScriptそろそろさわらなきゃなぁと思っている方の一助になれば幸いです🙇
冒頭に触れましたが、間違っているところはぜひご指摘頂けますと幸いです:bow:

42
27
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
42
27