Nuxt.js チュートリアル(todo app)

つくる物
https://silly-wilson-2f221d.netlify.app/

対象者
html、css、javascriptは少しわかる
vueの基本を調べてみて、何か簡単なアプリ作ってみたい人

学べること
Nuxtの基礎
Vuexの基礎
element-uiの基礎
Netlifyへのデプロイ方法

yarnを使いますので、npmでされる方は適宜コマンドを置き換えてください

セットアップ

yarn create nuxt-app nuxt-todo

nuxt-todoの箇所が作成されるディレクトリ名になります
お好きな名前を入れていただいて大丈夫です
上記のコマンドを打った後に、いくつか質問されます

今回は下画像のようにしました
Project name、Project description、Author nameは任意の物を入力していただいて大丈夫です
Choose the package managerはyarnでもnpmでもどちらでも構いません
他の項目は画像のものに合わせてください

スクリーンショット 2020-05-16 14.24.21

アプリを起動する

cd nuxt-todo
​yarn dev

アプリがたちがったらブラウザでlocalhost:3000にアクセスすると、下画像のようなデフォルト画面が表示されるはずです

スクリーンショット 2020-05-16 14.29.08

NuxtでScssを使えるようにする
参考: https://ja.nuxtjs.org/api/configuration-css/#__layout

Nuxtはデフォルトでscssで使えません。
scssを使える方が便利なので、scssを使えるようにします

ターミナル(コマンドプロンプト)を開いてください
プロジェクト直下で下コマンドを叩いてください

yarn add -D node-sass sass-loader

これでscssが使えるようになります

Netlifyのためのconfig修正

後々必要になるので、プロジェクト直下にあるnuxt.config.jsにgenerate: { fallback: true }を追加してください

スクリーンショット 2020-05-16 17.03.52

Nuxtのディレクトリ構造
参考 https://ja.nuxtjs.org/guide/directory-structure

今回はpages、store、layoutsディレクトリのみに手を加えるので、これらの解説をします

見た目(html、cssなどでいじるところ)はpages、layoutsです
layoutsがレイアウトというだけあって見た目の骨組みで、このlayoutsの中でpagesが表示されると思っていただければ大丈夫です(詳しくは公式を参照ください)

layoutsから修正します
layoutsディレクトリ直下のdefault.vueを以下のように書き換えてください

<template>
 <div>
   <el-container>
     <el-header height="60px">
       TODO APP
     </el-header>
     <nuxt />
   </el-container>
 </div>
</template>

<style>
html {
 font-family: 'Source Sans Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI',
   Roboto, 'Helvetica Neue', Arial, sans-serif;
 font-size: 1.2rem;
 word-spacing: 1px;
 -ms-text-size-adjust: 100%;
 -webkit-text-size-adjust: 100%;
 -moz-osx-font-smoothing: grayscale;
 -webkit-font-smoothing: antialiased;
 box-sizing: border-box;
}

*,
*:before,
*:after {
 box-sizing: border-box;
 margin: 0;
}

.el-header {
 background-color: #2cb696 ;
 display: flex;
 align-items: center;
 color: white;
}
</style>

layouts/default.vueの解説

vueの復習
vueファイルは3つの部分から構成されます
templateタグ -> htmlを書く場所
script -> JavaScriptやVueでロジックを書く場所
style -> css、scssを書く場所

templateタグにel-から始まるタグがありますが、これはelement-uiのコンポーネントを示しています
ここではel-containerとel-headerを使っていますが、これらはページのレイアウトを調整してくれます (詳しくは公式を参照)
構造としては下画像のようになります

スクリーンショット 2020-05-16 14.51.49

el-containerの中にある<nuxt />タグの箇所に、ページ(上画像の青枠2つの部分)が自動入ります。この部分をpagesディレクトリ直下のファイルに記述します

書き換えると下画像のように表示されるはずです

スクリーンショット 2020-05-16 15.01.12

lintエラーに対応する

実装中に画面にいきなり赤字でErrorが表示されることがあるかもしれません
これはeslintというコーディング規約に則っていないために発生します

スクリーンショット 2020-05-16 15.02.20

この時は一つ一つコード修正でもいいのですが、ターミナル上で下コマンドを打てば自動で修正されるので便利です

yarn lint --fix

pages/index.vue
下のように記述してください

<template>
 <el-container>
   <el-aside width="400px">
     <el-form
       ref="form"
       :rules="rules"
       :model="form"
       label-width="120px"
       @submit.native.prevent="handleAdd"
     >
       <el-form-item prop="input">
         <el-input
           v-model.trim="form.input"
           placeholder="todoを入力"
           clearable
         />
       </el-form-item>
     </el-form>
   </el-aside>
   <el-main>
     <el-row>
       <el-col :span="8">
         <ul class="list">
           <li v-for="(todo, index) in todoList" :key="index" class="todo">
             <span class="title" :class="todo.isChecked && 'checked'">{{
               todo.title
             }}</span>
             <div class="icons">
               <i class="el-icon-check" @click="handleCheck(index)"></i>
               <i class="el-icon-delete" @click="handleDelete(index)"></i>
             </div>
           </li>
         </ul>
       </el-col>
     </el-row>
   </el-main>
 </el-container>
</template>

<script>
export default {
 data() {
   return {
     form: {
       input: ''
     },
     todoList: [],
     rules: {
       input: [
         { required: true, message: '何か入力してください', trigger: 'change' }
       ]
     }
   }
 },
 methods: {
   handleAdd() {
     this.todoList.push({
       title: this.form.input,
       isChecked: false
     })
     this.$refs.form.resetFields()
   },
   handleCheck(index) {
     this.todoList = this.todoList.map((todo, i) => {
       return {
         ...todo,
         isChecked: index === i ? !todo.isChecked : todo.isChecked
       }
     })
   },
   handleDelete(index) {
     this.todoList.splice(index, 1)
   }
 }
}
</script>

<style scoped lang="scss">
.el-aside {
 padding: 20px 10px 20px 20px;
}
.list {
 padding: 0;
}
.todo {
 height: 60px;
 list-style: none;
 display: flex;
 justify-content: space-between;
 .title {
   &.checked {
     text-decoration: line-through;
   }
 }
}
.icons i {
 padding-right: 10px;
 transition: all 0.5s ease-in;
 &:hover {
   transform: scale(1.5);
   color: #2cb696 ;
 }
}
</style>

長くまりましたが、解説をします

styleタグにscopedとlang="scss"と付いています
lang="scss"が付いたstyleタグ内ではscssが記述できるようになります
scopedは、css、scssが記述しているvueファイルのみで使用可能となります、なので他のコンポーネントに予期せぬ影響が及ばなくなるメリットがあります

templateタグは大きく2つの部分になります
el-asideとel-mainで、それぞれelement-uiのコンポーネントです
el-aside側ではinputフォームを、el-mainでは作成したtodoを表示します
el-formもelement-uiのコンポーネントで、formタグのラッパーだと思っていただければと思います(参考 https://element.eleme.io/#/en-US/component/form)
基本的な使い方は、el-formの中にel-form-itemその中にel-inputを入れて使います
el-formの嬉しいところは、バリデーションがすごく簡単にできることです

el-formのバリデーションの使い方

el-formのrulesにどういうバリデーションをしたいかのルールを渡します
scriptタグ内のdataプロパティにrulesとありますが、これが今回のルールです(下画像の赤枠のこと)

スクリーンショット 2020-05-16 15.28.53

required: trueでinputは必須項目にする
messageはエラー発生時に表示する文言
trigger: 'change'で入力値が変わるたびにバリデーションを発火させます

el-formに:rules="rules"と書くことでルールを渡すのは完了です
さらにel-form-inputのpropにinputと渡してください
ここで注意なのが、el-form-inputのprop(input)、rulesのキー(input)と、
el-inputのv-modelに渡すキー(form.inputのinput)はinput(同じ名前)で統一させる必要があります

スクリーンショット 2020-05-16 15.35.06

一度文字を入力し、全て消すと下画像のようにエラーメッセージが表示されます

スクリーンショット 2020-05-16 15.32.55

el-inputタグにv-model.trim="form.input"とありますが、これはscriptタグのdata内のform.inputを指します
これで何か文字を入力すれば、入力した文字列が、form.inputに入ります
v-model.trimのtrimは入力値の前後の空白を自動で排除してくれます

todoの作成

先のel-formで、@submit.native.prevent="handleAdd"とありますが、ここでtodoの作成を行います。
@submit.native.preventとすることで、inputタグに何か入力しenterキーを押すだけでhandleAddが発火します


handleAddはscriptタグ内のmethodsに書きます

  handleAdd() {
   this.todoList.push({
     title: this.form.input,
     isChecked: false
   })
   this.$refs.form.resetFields()
 }

this.todoListはscriptタグのdata内のtodoListを指します
todoListはarrayなので、pushメソッドは使って配列にtodoを追加します
titleはtodoのタイトル、isCheckedはtodoが完了したかを表す予定で初期値はfalseで設定します

this.$refs.form.resetFields()

$refsを使うことで、templateタグ内でrefを記述しているタグを取得することができます

スクリーンショット 2020-05-16 15.49.21

今回はel-formにいref="form"で設定しているのでthis.$refs.formでこのel-formを取得できます
el-formに対してresetFields()を発火させることで入力欄を空白に戻すことができます

文字を入力すると右側にxが表示されますが、これを押下すると入力欄がクリアされます

スクリーンショット 2020-05-16 15.51.34

これはel-inputにclearableを記述することでできます

スクリーンショット 2020-05-16 15.52.56

el-mainの部分
ここでは作成したtodoを表示します

スクリーンショット 2020-05-16 15.56.15

v-forでtodoList内のtodoの数の分だけループを回してliタグを生成します
さらに:keyにindexを指定します、このkeyのおかげでVueがどのliタグのことかわかるようになります

liタグに:class="todo.isChecked && 'checked'"とありますが、todoのisCheckedがtrueならcheckedクラスをつけるという意味です

iiタグが2つありますが、これはtodoを完了するアイコンと、削除するアイコンです
それぞれに@clickをつけてアイコンをクリックしたら@click="ここ"が発火するようにします

<i class="el-icon-check" @click="handleCheck(index)"></i>

checkアイコンではhandleCheckをindexを引数に発火させます

// indexはtodoListの何番目のtodoがクリックされたかのindex
handleCheck(index) {
 // iはループのインデックス
 this.todoList = this.todoList.map((todo, i) => {
   return {
     ...todo,
     // iとindexが一致(クリックされたtodo)の場合、isCheckedの値を反転させる
     isChecked: index === i ? !todo.isChecked : todo.isChecked
   }
 })
},

deleteアイコンではhandleDeleteをindexを引数に発火させます​​

// indexはtodoListの何番目のtodoがクリックされたかのindex
handleDelete(index) {
 // クリックされたtodoをtodoListから削除する
 this.todoList.splice(index, 1)
}

これで作成されたtodoが表示されるはずです

スクリーンショット 2020-05-16 16.05.44

Vuexを導入する

todoListをpages/index.vueで管理してますが、これをvuexに移行します
大規模なアプリ開発ではほとんどの場合、todoListなどのデータをvuexに状態管理します
各ページからデータをアクセスするときに、vuexで一元管理していれば、管理しやすいからです

storeディレクトリにtodo.jsを作成し、下のように記述してください

export const state = () => ({
 todoList: []
})

export const getters = {
 todoList({ todoList }) {
   return todoList
 }
}

export const actions = {
 handleAdd({ commit }, title) {
   commit('add', { title, isChecked: false })
 },
 handleCheck({ commit }, index) {
   commit('check', index)
 },
 handleDelete({ commit }, index) {
   commit('remove', index)
 }
}

export const mutations = {
 add(state, todo) {
   state.todoList.push(todo)
 },
 check(state, index) {
   state.todoList = state.todoList.map((todo, i) => {
     return {
       ...todo,
       isChecked: index === i ? !todo.isChecked : todo.isChecked
     }
   })
 },
 remove(state, index) {
   state.todoList.splice(index, 1)
 }
}

vuexは4つの部分からなります
データを保存するstate
データを取得するためのgetters
api通信をしたり、vueファイルから呼び出すためのハンドラーとなるactions
stateを変化させりmutationsです

stateにtodoListを持たせ、gettersにもtodoListを与えます
actionsにはpages/index.vueで作成したmethodsと同い名前で用意し、commitを発火させます
mutationsではstate.todoListに変化を加えます

これでvuexの設定が完了なので、pages/index.vueとのつなぎこみを行います

pages/index.vueのscriptタグ内を下のようにしてください

<script>
 import { mapActions, mapGetters } from 'vuex'
 export default {
   data() {
     return {
       form: {
         input: ''
       },
       rules: {
         input: [
           { required: true, message: '何か入力してください', trigger: 'change' }
         ]
       }
     }
   },
   computed: {
     ...mapGetters({
       todoList: 'todo/todoList'
     })
   },
   methods: {
     ...mapActions({
       add: 'todo/handleAdd',
       check: 'todo/handleCheck',
       remove: 'todo/handleRemove'
     }),
     handleAdd() {
       if (this.form.input) {
         this.add(this.form.input)
         this.$refs.form.resetFields()
       }
     },
     handleCheck(index) {
       this.check(index)
     },
     handleDelete(index) {
       this.remove(index)
     }
   }
 }
</script>

import { mapActions, mapGetters } from 'vuex'
でmapActions, mapGettersを使えるようにします
mapActionsはvuexのactionsをvueコンポーネント内のmethodsとして使えるようにし、mapGettersはvuexのgettersをvueコンポーネント内のcomputedとして使えるようにします

dataプロパティからtodoListを消している
vuex内にtodoListを移行したためです
下のようにcomputedに書くことで、this.todoListでtodo.jsのtodoListにアクセスすることができます

computed: {
 ...mapGetters({
   todoList: 'todo/todoList'
 })
},

methodsについてはmapActionsを設定します
this.addでtodo.jsのhandleAddにアクセスすることができます
this.checkでtodo.jsのhandleCheckにアクセスすることができます
this.removeでtodo.jsのhandleRemoveにアクセスすることができます
そしてそれぞれをhandle~メソッド内で呼び出します

methods: {
 ...mapActions({
   add: 'todo/handleAdd',
   check: 'todo/handleCheck',
   remove: 'todo/handleRemove'
 }),
 handleAdd() {
   if (this.form.input) {
     this.add(this.form.input)
     this.$refs.form.resetFields()
   }
 },
 handleCheck(index) {
   this.check(index)
 },
 handleDelete(index) {
   this.remove(index)
 }
}

以上でvuexへの移行は完了です

Netlifyへデプロイ

Netlifyとは静的サイトやSPAを簡単にホスティングすることのできるサービスです。Sign upから会員登録と、githubも必要になりますので、githubアカウントをお持ちでない方はこちらも登録よろしくお願い位します

下準備

githubに作成したものをpushします

Repository nameとDescriptionを入力し、Create repositoryを入力してください

スクリーンショット 2020-05-16 16.41.49

ターミナル上で以下のコマンドでコミットしてください

git add .
git commit -m 'initial coommit'

create repositoryを押下後に遷移するページにて赤枠の箇所のコマンドをターミナルで叩いてください

スクリーンショット 2020-05-16 16.42.11

これで下準備完了です

Nelifyの設定

下画像のNew site from Gitを押下してください

スクリーンショット 2020-05-16 16.38.56

下画像の赤枠のGitHubを押下

スクリーンショット 2020-05-16 16.57.16

自分が作成したGitHubのリポジトリを選択する

スクリーンショット 2020-05-16 16.59.09

Build commandに npm run build
Publish directoryに dist
をそれぞれ入力し、Deploy siteを押下

スクリーンショット 2020-05-16 16.51.34

しばらくするとデプロイが完了し、urlができるので、そこをクリックすれば作成したアプリを確認できる

スクリーンショット 2020-05-16 16.53.46

以上で、チュートリアル終わりです!
こういうチュートリアル作成に慣れていないので拙い点がありますが、
わかりにくいところ、もっと解説してほしい点があればコメントください
追記修正いたします!
あと、何かしらリアクションいただけるとうれしいです
フォロー、スキ、サポートお待ちしております
では、お疲れ様です!

この記事が気に入ったらサポートをしてみませんか?