はじめに 目標 前提条件 雛形作成 APIモック作成 Vuex導入 コンポーネント作成 アイテム追加機能作成 おわりに はじめに こんにちは。新卒1年目のrksmskです。本記事はVue.jsを学び始めたばかりで実際に手を動かして簡単なアプリケーションを作成してみたい方のためのハンズオン チュートリアル となっております。 是非手を動かしながら本記事をお読みください。なお、本記事はVue3でコードを記述しています。Vue.jsはVue2とVue3で記述方法が大きく異なるため、ご留意ください。 目標 Vue.jsを使ってTodoListの表示と追加が出来る簡単なTodoListアプリケーションが作れるようになること。 前提条件 Node.jsがインストールされていること(Node.jsの説明やインストール方法については以下の記事に詳しく記載されています) 雛形作成 まず、Vueの環境を作成して雛形の画面を表示します。 TodoList作業用 ディレクト リを作成します。 ここからはターミナル上で作業を行います。TodoList作業用 ディレクト リ下でターミナルから npm install -g @vue/cli@next を実行し、Vueをインストールします(インストールが上手くいかない場合にはNode.jsのバージョン等をご確認ください)。 vue create todo-list を実行し、Vueの環境を作成します。その際にVueのバージョンを確認されるので、Vue3を選択します。パッケージマネージャはnpmを選択します。 環境作成が完了しましたら、 cd todo-list を実行してVueの環境に移った後、 npm run serve を実行してローカルのテストサーバを起動します。 立ち上がったサーバにブラウザからアクセスします。基本的には http://localhost:8080/ にサーバが立ち上がります。 下記のような雛形の画面が表示されることを確認します。 雛形画面 これで雛形画面の作成が完了しました。 API モック作成 続いて、TodoListのアイテムを作成します。今回は JSON Serverを使用して API モックを作成し、作成した API モックを通じてアイテムの CRUD (Create、Read、Update、Delete)操作を行います。 ここからはターミナル上で作業を行います。 npm i json-server を実行し、 json -serverをインストールします。 mkdir webapi を実行し *1 、webapi ディレクト リを作成します。 cd ./webapi を実行した後、 touch db.json を実行し *2 、db. json ファイルを作成します。 db. json ファイルに以下を記入します。 { " todos ": [ { " id ": 1 , " text ": " 働く ", " categoryId ": 1 } , { " id ": 2 , " text ": " 寝る ", " categoryId ": 1 } , { " id ": 3 , " text ": " 起きる ", " categoryId ": 2 } ] , " categories ": [ { " id ": 1 , " title ": " やること " } , { " id ": 2 , " title ": " やったこと " } ] } webapi下で npx json-server --watch db.json を実行し、 API モックサーバーを起動します。 立ち上がったサーバにアクセスします。基本的には http://localhost:3000/ にサーバが立ち上がります。 下記のような JSON Serverの起動画面が表示されることを確認します。 JSON Server起動画面 これで API モックの起動が完了しました。 Vuex導入 ここまでで、雛形画面の作成と API モックの起動が完了しました。ですので、ここからは API モックを通じてTodoListを取得し、雛形画面に表示することを行います。その際に、後々状態管理を行う上で便利になるため、Vuexを導入したいと思います。Vuexは共有状態の管理を行うライブラリで、異なる コンポーネント 間で同一のデータを共有する際に重宝します。 ここからはターミナル上で作業を行います。現在の ディレクト リをtodo-listに変更した後、 npm install vuex@next --save を実行し、Vuexライブラリをインストールします。 cd ./src を実行した後、 mkdir store を実行し、todo-list/src ディレクト リ下にstore ディレクト リを作成します。 touch store.js を実行し、store.jsファイルを作成します。本ファイルがVuexで共有状態を管理するコードを記述するファイルとなります。 store.jsを以下のように書き換えます。 import { createStore } from "vuex" ; export default createStore( { state() { return { categoryList: [] , cardList: [] } } , mutations: { setCategoryList(state, categoryList) { state.categoryList = categoryList; } , setCardList(state, cardList) { state.cardList = cardList; } } , actions: { async fetchCategoryList(context) { const categoryList = await fetchItems( "http://localhost:3000/categories" ); context.commit( "setCategoryList" , categoryList); } , async fetchCardList(context) { const cardList = await fetchItems( "http://localhost:3000/todos" ); context.commit( "setCardList" , cardList); } , } } ); actionsに記載されているfetchXXXメソッドでは API から情報を取得し、取得した情報を引数にmutationsのメソッドを呼び出しています。mutationsではstateの変更を行っています。 ここで注意したいこととして、mutationsでは非同期処理を行わないこと、actionsではstateの更新を直接行わないことです。これらの注意点の詳しい説明は以下の記事に詳しく記載されているため、そちらも併せてご覧ください。 Vuexの設定をアプリケーションに反映させるため、main.jsファイルを以下のように書き換えます。 import { createApp } from 'vue' import Store from './store/store.js' ; import App from './App.vue' createApp(App).use(Store).mount( '#app' ) これでVuexでTodoListの状態管理をする仕組みが出来たため、後は API からデータを取得するfetchXXXメソッドを作成し、雛形画面に表示させます。 現在の ディレクト リをtodo-listに変更した後、 mkdir utils を実行し、todo-list下にutils ディレクト リを作成します。 *3 cd ./utils を実行した後、 touch http.js を実行し、todo-list/utils ディレクト リ下にhttp.jsファイルを作成します。 http.jsを以下のように書き換えます。 export const fetchItems = async (url) => { try { // API通信でデータを取得する const response = await fetch(url); // 取得したデータをjson形式で返す return await response.json(); } catch (error) { // API通信でデータを上手く取得できなかった場合、コンソールにエラーを表示 console.error( "データを取得出来ませんでした" ); console.error(error); } } ; store.jsにhttp.jsをインポートするため、store.jsの二行目に以下を追加します。 import { fetchItems } from "../../utils/http" ; App.vueを以下のように書き換えます。 <template> <div> カード一覧: {{ cardList }} <br /> カテゴリ一覧: {{ categoryList }} </div> </template> <script> import { computed, onMounted } from "vue" ; import { useStore } from "vuex" ; export default { name: "App" , components: {} , setup() { // Vuexを使う設定 const store = useStore(); // コンポーネントがマウントされた時にcategoryListとcardListをAPIから取得 onMounted(store.dispatch( "fetchCategoryList" )); onMounted(store.dispatch( "fetchCardList" )); return { cardList: computed(() => store.state.cardList), categoryList: computed(() => store.state.categoryList) } ; } } ; </script> <style></style> 上記の記法は単一ファイル コンポーネント と呼ばれる記述方法で、 templateタグ内に画面に表示するHTMLを記述する。 scriptタグ内にデータの定義や処理を記述する。 styleタグ内に css でスタイルを記述する。 といったようにHTML、 JavaScript 、 CSS の処理をまとめて一つのファイルに記述することが出来ます。これにより、一つ一つの コンポーネント の保守がし易くなっています。 ローカルのテストサーバを再起動し、下記のようなデータが画面に表示されることを確認します。 カード一覧:[ { "id": 1, "text": "働く", "categoryId": 1 }, { "id": 2, "text": "寝る", "categoryId": 1 }, { "id": 3, "text": "起きる", "categoryId": 2 } ] カテゴリ一覧:[ { "id": 1, "title": "やること" }, { "id": 2, "title": "やったこと" } ] これでTodoListの表示が完了しました。 コンポーネント 作成 ここまででTodoListの表示は出来ましたが、現状はデータがそのまま表示されているだけで、非常に見づらいです。 ですので、ここからはTodoListをカード形式で表示するように変更していきたいと思います。その際に、Vueに備わっている一つ一つの部品を コンポーネント として切り出すことで管理し易くする機能を利用します。 今回の コンポーネント は以下のようにボード、リスト、カードで分割を行います。 コンポーネント 分割例 components下のHelloWorld.vueを削除します。 components下にBoard.vue、List.vue、Card.vueを作成します。 App.vueを以下のように書き換えます。 <template> <div> <board></board> </div> </template> <script> import Board from "./components/Board.vue" ; export default { name: "App" , components: { Board } } ; </script> <style></style> 少しだけVue2とVue3の違いを説明すると、Vue2では値はdataプロパティに、関数はmethodsにといったようにプロパティ毎に役割を分けていましたが、Vue3では新しく追加されたComposition API の機能によってsetup関数にこれらの処理をまとめて記述することが出来ます。 そのsetup関数内ではVuexで定義したactions内のメソッドをstore.dispatch("メソッド名")によって呼び出します。その結果、VuexのcardListとcategoryListの状態が変化するため、その状態変化をcomputed関数によって検知し、データの反映を行っています。 Board.vueを以下のように書き換えます。 <template> <div class = "board-style" > <list v- for = "category in categoryList" :key= "category.id" :category= "category" ></list> </div> </template> <script> import { computed, onMounted } from "vue" ; import { useStore } from "vuex" ; import List from "./List.vue" ; export default { components: { List } , setup() { const store = useStore(); onMounted(store.dispatch( "fetchCategoryList" )); return { categoryList: computed(() => store.state.categoryList) } ; } } ; </script> <style scoped> .board-style { display: flex; gap: 10px; } </style> Board.vueではv-forディレクティブによってカテゴリ毎にList コンポーネント を生成しています。その際にList コンポーネント にはカテゴリの情報を渡しています。 List.vueを以下のように書き換えます。 <template> <div class = "list-style" > {{ category.title }} <card v- for = "card in cardList" :key= "card.id" :card= "card" ></card> </div> </template> <script> import { computed, onMounted } from "vue" ; import { useStore } from "vuex" ; import Card from "./Card.vue" ; export default { components: { Card } , props: { category: Object } , setup(props) { const store = useStore(); onMounted(store.dispatch( "fetchCardList" )); const cardList = computed(() => store.state.cardList.filter(card => card.categoryId === props.category.id) ); return { cardList, } ; } } ; </script> <style scoped> .list-style { display: inline-flex; flex-direction: column; text-align: center; background-color: silver; min-width: 200px; min-height: 400px; } </style> List コンポーネント はBoard コンポーネント から渡ってきたカテゴリの情報をもとに、カテゴリIDと一致するカードをArrayオブジェクトの標準ライブラリであるfilterを用いて抽出し、v-forディレクティブによって抽出したカード毎にCard コンポーネント を生成しています。 Card.vueを以下のように書き換えます。 <template> <div class = "card-style" > {{ card.text }} </div> </template> <script> export default { props: { card: Object , } } ; </script> <style scoped> .card-style { display: flex; flex-direction: column; background-color: yellowgreen; margin: 10px; height: 10vh; align-items: center; justify-content: center; border-radius: 10px; } </style> Card コンポーネント はList コンポーネント から渡ってきたカードのテキストを表示しています。 ローカルのテストサーバを再起動し、下記のような完成画面が表示されることを確認します。 完成画面 これで、 コンポーネント 分割が完了し、TodoListが見やすくなりました。 アイテム追加機能作成 現状、アイテムの追加はwebapi下のdb. json をエディタ等で直接書き換えるか、POSTメソッドでHttpRequestを送る必要があり、少々手間がかかります。 そこで、空のカードを用意し、そのカードにテキストを入力して追加ボタンを押したら新しいカードが追加されるようにして利便性を上げたいと思います。 http.jsを以下のように書き換えます。 export const fetchItems = async (url) => { try { // API通信でデータを取得する const response = await fetch(url); // 取得したデータをjson形式で返す return await response.json(); } catch (error) { // API通信でデータを上手く取得できなかった場合、コンソールにエラーを表示 console.error("データを取得出来ませんでした"); console.error(error); } }; +export const insertItems = async (url, data) => { + const response = await fetch(url, { + // json形式でPOSTでデータを送る + method: 'POST', + headers: { + 'Content-Type': 'application/json;charset=utf-8' + }, + body: JSON.stringify(data) + }).catch(() => { + // 上手くいかなかった場合、コンソールにエラーを表示 + console.error(response.json()) + return; + }) +}; store.jsを以下のように書き換えます。 import { createStore } from "vuex"; -import { fetchItems } from "../../utils/http"; +import { fetchItems, insertItems } from "../../utils/http"; export default createStore({ state() { return { categoryList: [], cardList: [] } }, mutations: { setCategoryList(state, categoryList) { state.categoryList = categoryList; }, setCardList(state, cardList) { state.cardList = cardList; }, }, actions: { async fetchCategoryList(context) { const categoryList = await fetchItems("http://localhost:3000/categories"); context.commit("setCategoryList", categoryList); }, async fetchCardList(context) { const cardList = await fetchItems("http://localhost:3000/todos"); context.commit("setCardList", cardList); }, + async addCard(context, data) { + await insertItems("http://localhost:3000/todos", data); + } } }); List.vueを以下のように書き換えます。 <template> <div class="list-style"> {{ category.title }} - <card v-for="card in cardList" :key="card.id" :card="card"></card> + <card + v-for="card in cardList" + :key="card.id" + :card="card" + :isNew="false" + ></card> + <card :card="newCard" :isNew="true"></card> </div> </template> <script> import { computed, onMounted } from "vue"; import { useStore } from "vuex"; import Card from "./Card.vue"; export default { components: { Card }, props: { category: Object }, setup(props) { const store = useStore(); onMounted(store.dispatch("fetchCardList")); const cardList = computed(() => store.state.cardList.filter(card => card.categoryId === props.category.id) ); + const newCard = { + id: -1, + text: "", + categoryId: props.category.id + }; return { cardList, + newCard }; } }; </script> <style scoped> .list-style { display: inline-flex; flex-direction: column; text-align: center; background-color: silver; min-width: 200px; min-height: 400px; } </style> List コンポーネント では、新しく追加するカードの雛形オブジェクトであるnewCardを定義し、Card コンポーネント に渡しています。また、Card コンポーネント には対象のカードが新しく追加するカードなのか否かを識別できるようにboolean型の変数isNewも渡しています。 Card.vueを以下のように書き換えます。 <template> <div class="card-style"> + <input type="text" v-if="isNew" v-model="text" /> + <button @click="addCard" v-if="isNew">追加</button> + <span v-else> {{ card.text }} + </span> </div> </template> <script> +import { ref } from "@vue/reactivity"; +import { useStore } from "vuex"; export default { props: { card: Object, + isNew: Boolean + }, + setup(props) { + const store = useStore(); + let text = ref(""); + const addCard = async () => { + const data = { + text: text.value, + categoryId: props.card.categoryId + }; + await store.dispatch("addCard", data); + await store.dispatch("fetchCardList"); + text.value = ""; + }; + return { + text, + addCard + }; } }; </script> <style scoped> .card-style { display: flex; flex-direction: column; background-color: yellowgreen; margin: 10px; height: 10vh; align-items: center; justify-content: center; border-radius: 10px; } </style> Card コンポーネント のtemplateタグ内では、isNewがTrueの場合に【追加】ボタンが表示されるようにしています。 scriptタグ内では、【追加】ボタンが押された場合にaddCardメソッドを実行する処理を記述しています。addCardメソッドではカードの情報をdataオブジェクトに格納し、Vuexのactionsをdisptach関数によって呼び出しています。 なお、dataオブジェクトのidプロパティについては、 json -serverでは自動で割り振られるようになっているため追加していません。 ローカルのテストサーバを再起動し、下記のように新しいカードの追加が出来ることを確認します。 *4 カード追加前 カード追加後 これで、カードの追加機能が完成しました。 おわりに 今回はVue.jsを使ったTodoList チュートリアル を紹介しました。本記事ではTodoListの表示とアイテムの追加という基本機能だけでしたが、その他にも カードの更新、削除機能 カテゴリ追加機能 カードが API を通じて読み込まれるまでの間のロード画面 カード移動機能( ドラッグ&ドロップ 可能だとより良いと思います) タイトルの色変更機能 デザイン変更(Vuetify等の マテリアルデザイン フレームワーク を利用するだけでもリッチになります) 等々様々な拡張が可能だと思うので、是非拡張してみてください。 エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com *1 : ディレクト リが作成出来ればmkdirコマンドでなくても問題ありません。 *2 : ファイルが作成出来ればtouchコマンドでなくても問題ありません。 *3 : API 通信のような汎用的に使われるメソッドはutils ディレクト リ下に配置することが多いです。 *4 : Board.vueには変更はありません。