Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 8920

Vue.js + Express + Sequelize + DockerでCRUD搭載のTodoリストを作ってみる

$
0
0

Vue.js + Express + Sequelize + DockerでCRUD搭載のTodoリストを作ってみる

この記事に書いてあること

  • Expressの実装例
  • Vue.jsの実装例
  • 実行環境として利用するDockerコンテナの作り方
  • Sequelizeの導入方法、簡単な使い方
  • 拙い日本語

しがないエンジニアが人生初アドベントカレンダー参加となりますので、諸々ご容赦頂けますと幸いです。

対象者

細かい説明は割愛していますが、初心者向けに書いてます。

「ゴリゴリにDocker使った環境構築が知りたい!」とか、
「俺はSequelizeなんか使わずにSQLを一つずつ組むね!」とか、
「ここどうなってるかもうちょい細かく説明してほしい」みたいな人にはあまり適してません。

あくまで、
記事をなぞっていくだけで手軽にCRUDがVue.js+Expressが体験できる

っていうのを目的にしています。

各種バージョン

Docker for mac 19.03.5
Node.js 12.13.0
express 4.16.1
Sequelize-cli 5.5.1
vue-cli 4.0.5

DBはsqlite3を利用(なんでもいいけど)

作るもの

CRUD機能を搭載した簡単なTodoリストを作ります。
完成イメージはこんな感じです。

cwljp-8g071.gif

各種説明

Docker

言わずと知れたコンテナマン。コンテナの概念やメリットの説明は割愛。今回環境はこれで構築。

(ベストプラクティスを知りたい)

Vue.js

みんな大好きフロントフレームワーク。特に難しいことはしません。
(vue-cliを使って雛形を作成)

Node.js

みんな大好きサーバーサイドで動くJavaScript。特に難しいことはしません。

Sequelize

Nodeで使えるORM。RailsのActiveRecordみたいな物だと思ってもらえればOK。
特に難しいことはしません

全体的なファイル構成

ファイル構成はこんな感じ。
rootディレクトリ配下をコンテナごとに区切り、後々コンテナにマウントしてあげる。

rootDir/
 ┣ docker-compose.yml
 ┣ vue/
 ┃  ┣ Dockerfile
 ┃  ┗ frontapp/
 ┃      ┗ Vue.jsの雛形ファイル群が入ってくる
 ┗ node/
    ┣ Dockerfile
    ┗ Expressの雛形ファイル群が入ってくる

全体構成

全体構成はこんな感じです。
基本Vue.jsコンテナがリクエストを受けて、axiosでNode.jsコンテナにサーバ通信しています。
SQLiteを使っているので、DBアクセスはNode.jsコンテナ内で処理されるようなイメージ。

vuexpress.png

Node.jsの準備

早速開始、と行きたいところですが、まずは実行環境を構築。
Dockerhubから引っ張ってきてもいいのですが、せっかくなのでDockerfileを書いてあげましょう。
今回はとりあえず各種プログラムが実行できる環境が前提なので、コンテナ内は環境のみ。
ソースファイルはコンテナイメージに含めず、docker-composeを使って後でマウントしてあげることにします。

node/Dockerfile
FROM node:12.13RUN npm install-g express-generator sequelize-cli

Dockerfileの記述が終わったら一旦、Dockerfileからコンテナイメージを作成。
コンテナ起動時にローカルのディレクトリをマウントし、コンテナ内のデータが永続化できるようにします。

docker build node/. -t serverapp:latest
docker run -itd--rm--name serverapp -v$PWD/node:/node serverapp:latest

コンテナの起動が完了したら、コンテナ内にログインする。

docker exec-it serverapp /bin/bash

コンテナログイン後、expressコマンドを実行し、雛形ファイル群を作成。

cd /node
express .
npm install--save sequelize sqlite3 cors nodemon
npm install

docker runを実行した際にローカルのフォルダをマウントしているので、
ローカルのnode/以下にexpressの雛形ファイル群ができているはず。

続いて、DBを作成するためにコンテナに入ったままsequelize initを実行し、
CRUDに必要なtaskモデルを作成する準備をします。

sequelize init

init実行後、一旦コンテナからログアウトし、ローカルでconfig/config.jsonを下記のように修正。

config/config.json
{"development":{"username":"root","password":null,"database":"database_development","host":"127.0.0.1","dialect":"sqlite","storage":"./data/development.sqlite3","operatorsAliases":false},"test":{"username":"root","password":null,"database":"database_test","host":"127.0.0.1","dialect":"sqlite","storage":"./data/test.sqlite3","operatorsAliases":false},"production":{"username":"root","password":null,"database":"database_production","host":"127.0.0.1","dialect":"sqlite","storage":"./data/production.sqlite3","operatorsAliases":false}}

Sequelizeのデータが保存される./dataを作成する。

mkdir data

もう一度コンテナにログインし、taskモデルを作成します。

docker exec-it serverapp /bin/bash
sequelize model:create --name task --underscored--attributes taskname:string
sequelize db:migrate

マイグレーションが無事に成功すればtaskモデルが作成されます。
これでNode.jsの準備は完了。雛形準備に利用したコンテナは停止します。

docker stop serverapp

続いてフロントVue.js側を準備します。

Vue.jsの準備

Node側と同様にDockerfileを記述。

vue/Dockerfile
FROM node:12.13RUN npm install-g @vue/cli

Dockerfileを元にコンテナイメージを作成。起動し、ローカルのフォルダをマウント。

docker build vue/. -t frontapp:latest
docker run -itd--rm--name frontapp -v$PWD/vue:/vue frontapp:latest

Node側同様、Vue.jsの雛形ファイル群を作成するため、一度コンテナ内にログインします。

docker exec-it frontapp /bin/bash

コンテナログイン後、下記コマンドを実行。オプションは defaultで問題ありません。
その後の選択肢としては、yarnnpmが出てくるけど、個人的にはnpmのが使いやすいのでそちらで。

cd /vue
vue create frontapp

以上でVue.js側の準備は終了。Node.js側同様に、一度コンテナは停止します。

docker stop frontapp

docker-compose.ymlの準備

Node.jsとVue.jsそれぞれのコンテナを起動する際、composeファイルがあると起動/終了が楽なので、
docker-compose.ymlを下記のように記入。

docker-compose.yml
version:"3"services:node:build:node/.volumes:-./node:/nodeworking_dir:/nodecommand:["npm","start"]ports:-"3000:3000"vue:build:vue/.volumes:-./vue:/vueworking_dir:/vue/frontappcommand:["npm","run","serve"]ports:-"8080:8080"

プロジェクトのカレントディレクトリで、docker-composeコマンドを実行し、
Node.jsのコンテナとVue.jsのコンテナを起動する。

docker-compose up -d# コンテナ終了は docker-compose down

ブラウザで3000ポートにアクセスしExpressの画面、8080ポートにアクセスしVue.jsの画面が表示されれば、開発用コンテナの構築は完了です。

localhost:3000

スクリーンショット 2019-11-23 14.15.48.png

localhost:8080

スクリーンショット 2019-11-23 14.23.14.png

docker-composeコマンドで初回起動時にコンテナイメージが再ビルドされ、 docker runで起動した時のコンテナイメージとは別の名前でイメージ化されるため、 docker runコマンドで起動した時のコンテナイメージは不要のため削除します。

docker rmi serverapp:latest
docker rmi frontapp:latest

実行環境の作成はこれにて終了。次から各種機能の実装に入っていきます。

Node.js

まずはサーバサイドの処理から実装します。
処理実装に入る前に、ソースコード変更時に自動的にNodeが再起動されるように少しだけ工夫。
app.jsdocker-compose.ymlを修正します。

node/app.js
...varindexRouter=require("./routes/index");varusersRouter=require("./routes/users");varcors=require("cors");varapp=express();app.use(cors());...app.listen(3000,function(){console.log("Node server is started");});module.exports=app
docker-compose.yml
...working_dir:/nodecommand:["./node_modules/.bin/nodemon","app"]ports:...

Node.jsの初期状態だとVueからのリクエストを受けられない(エラーがでてしまう)ので、
インストールしたcorsモジュールを追加して、corsを許可する必要があります。
また、nodemonを利用しファイル変更を検知、ファイルが変更されたら自動でサーバが再起動されるようにします。
設定を反映するため、一度コンテナ再起動を実行。

docker-compose down
docker-compose up -d

コントローラーの新規追加がめんどくさいので最初から用意されているindex.jsを利用します。
実装内容としてはこんな感じ。

node/routes/index.js
varexpress=require("express");varrouter=express.Router();constdb=require("../models/index");// Readrouter.get("/",asyncfunction(req,res,next){try{constresult=awaitdb.task.findAll({});res.send(result);}catch(err){res.status(500).send(err);}});//Createrouter.post("/task",asyncfunction(req,res,next){try{constresult=awaitdb.task.create({taskname:req.body.task});res.send(result);}catch(err){res.status(500).send(err);}});//Updaterouter.put("/task/:id",asyncfunction(req,res,next){try{constresult=awaitdb.task.update({taskname:req.body.task},{where:{id:req.params.id}});res.send(result);}catch(err){res.status(500).send(err);}});//Deleterouter.delete("/task/:id",asyncfunction(req,res,next){try{constresult=awaitdb.task.destroy({where:{id:req.params.id}});res.send({result:result});}catch(err){res.status(500).send(err);}});module.exports=router;

送られてきたパラメータをそのままDBに登録するっていう簡単なCRUD処理の一覧です。
一つずつ説明していきます。

// Readrouter.get("/",asyncfunction(req,res,next){try{constresult=awaitdb.task.findAll({});res.send(result);}catch(err){res.status(500).send(err);}});

getで/にアクセスされた際に、taskテーブルから全てデータを引っ張ってくるように実装。
DBのtaskテーブルから全てデータを引っ張ってくるのに、sequelizeのfindAllというメソッドを使用します。
取得したデータをres.sendを使用して、リクエスト元に戻してあげます。

router.post("/task",asyncfunction(req,res,next){try{constresult=awaitdb.task.create({taskname:req.body.task});res.send(result);}catch(err){res.status(500).send(err);}});

postで/taskにアクセスした際に、リクエストのbody内容をDBのtaskテーブルに登録。
sequelizeのcreateというメソッドを利用します。

router.put("/task/:id",asyncfunction(req,res,next){try{constresult=awaitdb.task.update({taskname:req.body.task},{where:{id:req.params.id}});res.send(result);}catch(err){res.status(500).send(err);}});

putで/task/:idにアクセスした際に、idに紐づくレコードをupdateする処理。
sequelizeのupdateメソッドを利用し、リクエストのbody内容でデータを更新します。

router.delete("/task/:id",asyncfunction(req,res,next){try{constresult=awaitdb.task.destroy({where:{id:req.params.id}});res.send({result:result});}catch(err){res.status(500).send(err);}});

deleteで/task/:idにアクセスした際に、idに紐づくレコードを削除する処理。
sequelizeのdestroyメソッドを利用。

Node.js側CRUDの処理は実装完了です。続いて、Vue.js側の実装。

Vue.js

デフォルトでHelloWorld.vueというコンポーネントが存在しているので、そこに肉付けしていくことにします。
まずは各種部品を設置。

vue/frontapp/src/components/HelloWorld.vue
<template><divclass="hello"><form><inputtype="text"style="display:none"/><inputtype="text"/><inputtype="button"value="add!"/></form><tablealign="center"border="0"><tr><th>task</th><th>update</th><th>delete</th></tr><tr><td><inputtype="text"/></td><td><inputtype="button"value="update"/></td><td><inputtype="button"value="delete"/></td></tr></table></div></template><script>exportdefault{name:"HelloWorld"}</script><!-- Add "scoped" attribute to limit CSS to this component only --><stylescoped>h3{margin:40px00;}ul{list-style-type:none;padding:0;}li{margin:010px;}a{color:#42b983;}.table{height:100%;text-align:center;}</style>

上の基本系を修正します。今の時点だとこんな感じ。
スクリーンショット 2019-11-23 10.12.44.png

まずはGETで/にアクセスした際に、apiアクセスを行う部分を実装する。

vue/frontapp/src/components/HelloWorld.vue
<script>importaxiosfrom"axios";exportdefault{name:"HelloWorld",data:()=>({tasks:[],}),created:asyncfunction(){try{constresult=awaitaxios.get("http://localhost:3000");this.tasks=result.data;}catch(err){alert(JSON.stringify(err));}}}...

createdを定義しておくことで、ページが読み込まれた時に処理を実行することが可能。
createdでもmountedでもどっちでもいいけど、今回の用途にはcreatedの方が適任ですね。
下記記事で詳しく書かれていたので、気になる人は参照してみてください。

Vuejs APIアクセスはcreatedとmountedのどちらで行う?

axiosは非同期で実行されるので、処理を同期的に行うためにawaitで実行。
実行結果としてresult.dataが戻ってくるので、data内のtasksの内容をAPI実行結果に変更する。

続いてtask追加処理を実装。まず入力されたデータにアクセスできるようにする必要があるため、
テキストボックスをv-modelでdataと紐づけます。

vue/frontapp/src/components/HelloWorld.vue
...
    <form><inputtype="text"style="display:none"/><inputv-model="currentTask"type="text"/><inputtype="button"value="add!"/></form>
...
<script>importaxiosfrom"axios";exportdefault{name:"HelloWorld",data:()=>({tasks:[],currentTask:""}),...

こうすることでinputに入力されたデータが、datacurrentTaskに反映され、関数内からデータが参照可能となります。
次に@clickイベントを実装し、ボタンが押された時に関数を呼び出すよう修正。

vue/frontapp/src/components/HelloWorld.vue
...
    <form><inputtype="text"style="display:none"/><inputv-model="currentTask"type="text"/><inputtype="button"value="add!"@click="taskCreate"/></form>
...
<script>...created:asyncfunction(){try{constresult=awaitaxios.get("http://localhost:3000");this.tasks=result.data;}catch(err){alert(JSON.stringify(err));}},methods:{taskCreate:asyncfunction(){alert(this.currentTask);}}};</script>

こうすることで、ボタンが押された時に、taskCreateを呼び出すことが可能です。
仮実装としてボタンを押すと、テキストボックス内に入力したデータの内容をalert表示するように実装してます。
何かしらテキストボックスに入力し、ボタンを押したタイミングでalertが表示されてくればOK。

スクリーンショット 2019-11-23 10.27.08.png

取得したデータをサーバに送信するため、taskCreate関数の続きを実装していく。

vue/frontapp/src/components/HelloWorld.vue
<script>...methods:{taskCreate:asyncfunction(){try{constresult=awaitaxios.post("http://localhost:3000/task",{task:this.currentTask});this.tasks.push(result.data);this.currentTask="";}catch(err){alert(JSON.stringify(err));}}...

node.js側で定義したタスク追加の処理(/task)に繋げる。
サーバ処理終了後に下記部分で動的にtasksにデータを追加。

this.tasks.push(result.data);// data内tasks配列に戻り値(追加したデータ)を追加this.currentTask="";// data内currentTaskとテキストボックスが双方向でバインドされているので、currentTaskを空にすることでテキストボックスが空になる

これで、入力されたデータがサーバにリクエストで送られ、データ保存が可能となります。
このままだとデータを追加しても追加したデータが表示されないので、data内のtasks<tr>v-modelを利用し紐づけます。
HelloWorld.vueのtable部分を下記のように変更。

vue/frontapp/src/components/HelloWorld.vue
<tablealign="center"border="0"><tr><th>task</th><th>update</th><th>delete</th></tr><trv-for="(task, index) in tasks":key="task.id"><td><inputv-model="task.taskname"type="text"/></td><td><inputtype="button"value="update"/></td><td><inputtype="button"value="delete"/></td></tr></table>

v-forを利用することでtasksの数だけ <tr>が生成されます。
こうすることで、ページ読み込み時にサーバからデータが取得され、今まで追加したタスクが出てくるようになり、
ボタンを押した時にもタスクが画面に追加されるようになる。

スクリーンショット 2019-11-23 10.44.46.png

これでタスク追加に関しては実装終了。続いてタスク削除を実装します。
まずは関数を用意。

vue/frontapp/src/components/HelloWorld.vue
<script>...methods:{taskCreate:asyncfunction(){try{constresult=awaitaxios.post("http://localhost:3000/task",{task:this.currentTask});this.tasks.push(result.data);this.currentTask="";}catch(err){alert(JSON.stringify(err));}},taskDelete:asyncfunction(id,index){try{awaitaxios.delete("http://localhost:3000/task/"+id);this.currentTask="";this.tasks.splice(index,1);}catch(err){alert(JSON.stringify(err));}}}

taskDelete関数を追加しました。taskのidと配列のindexを引数に持ってあげます。
呼び出し部分はこんな感じ。

vue/frontapp/src/components/HelloWorld.vue
<trv-for="(task, index) in tasks":key="task.id"><td><inputv-model="task.taskname"type="text"/></td><td><inputtype="button"value="update"/></td><td><inputtype="button"value="delete"@click="taskDelete(task.id, index)"/></td></tr>

こうすることで、リクエスト先のURLが動的に作られます。
タスクのidが2だった場合は、リクエスト先はaxios.delete("http://localhost:3000/task/" + 2);となり、
タスクのidが10だった場合は、リクエスト先はaxios.delete("http://localhost:3000/task/" + 10);となります。

また、配列のindexを引数に持ってあげることによって、spliceメソッドを使って配列を操作することができ、画面のデータを非同期で変更可能です。

this.tasks.splice(index,1);// [{タスク1},{タスク2},{タスク3},{タスク4}]// indexに2が渡された場合は、// [{タスク1},{タスク2},{タスク4}]// このように配列が操作される

最後にタスク修正した際の処理を実装する。まずは関数の用意から。

vue/frontapp/src/components/HelloWorld.vue
<script>...taskUpdate:asyncfunction(id,val){try{awaitaxios.put("http://localhost:3000/task/"+id,{task:val});alert("タスクを修正しました");this.currentTask="";}catch(err){alert(JSON.stringify(err));}}

呼び出し部分はこんな感じ。

vue/frontapp/src/components/HelloWorld.vue
<trv-for="(task, index) in tasks":key="task.id"><td><inputv-model="task.taskname"type="text"/></td><td><inputtype="button"value="update"@click="taskUpdate(task.id, task.taskname)"/></td><td><inputtype="button"value="delete"@click="taskDelete(task.id, index)"/></td></tr>

taskUpdateを呼び出す時に、タスクのID、タスク名を引数で渡してあげることにより、
対象のタスクのみアップデートがかかるようにします。

CRUDの全体が出来上がった最終形のHelloWorld.vueとしてはこんな感じ。

<template><divclass="hello"><form><inputtype="text"style="display:none"/><inputv-model="currentTask"type="text"/><inputtype="button"value="add!"@click="taskCreate"/></form><tablealign="center"border="0"><tr><th>task</th><th>update</th><th>delete</th></tr><trv-for="(task, index) in tasks":key="task.id"><td><inputv-model="task.taskname"type="text"/></td><td><inputtype="button"value="update"@click="taskUpdate(task.id, task.taskname)"/></td><td><inputtype="button"value="delete"@click="taskDelete(task.id, index)"/></td></tr></table></div></template><script>importaxiosfrom"axios";exportdefault{name:"HelloWorld",data:()=>({tasks:[],currentTask:""}),created:asyncfunction(){try{constresult=awaitaxios.get("http://localhost:3000");this.tasks=result.data;}catch(err){alert(JSON.stringify(err));}},methods:{taskCreate:asyncfunction(){try{constresult=awaitaxios.post("http://localhost:3000/task",{task:this.currentTask});this.tasks.push(result.data);this.currentTask="";}catch(err){alert(JSON.stringify(err));}},taskDelete:asyncfunction(id,index){try{awaitaxios.delete("http://localhost:3000/task/"+id);this.currentTask="";this.tasks.splice(index,1);}catch(err){alert(JSON.stringify(err));}},taskUpdate:asyncfunction(id,val){try{awaitaxios.put("http://localhost:3000/task/"+id,{task:val});alert("タスクを修正しました");this.currentTask="";}catch(err){alert(JSON.stringify(err));}}}};</script><!-- Add "scoped" attribute to limit CSS to this component only --><stylescoped>h3{margin:40px00;}ul{list-style-type:none;padding:0;}li{margin:010px;}a{color:#42b983;}.table{height:100%;text-align:center;}</style>

以上で完成です!

作ってみた感想

Vue.jsのバインディングが思っていた以上に使いやすく、非常に簡単にフロントの実装ができました。
規模が大きくなってきたらVuexとかライフサイクルとか考えないといけないこともあるけど、
この程度の規模のアプリケーションであれば、簡単に作ることができるのが良いですね。

あと、ただ実装するだけだと面白く無いので実行環境はDockerにしたのも◯。
汎用的な実行コンテナとして使えるので、チュートリアル的な使い方には非常に適任。
構築も簡単なので非常に使いやすかったかと。

Sequelizeの導入がちょっとだけめんどくさいけど、一度やってしまえばRailsチックにDB操作ができるので、
SQL書かずに手軽にDB操作したい!って人にはおすすめです。

明日は@kazukimatsumotoさんの番です。よろしくおねがいします!


Viewing all articles
Browse latest Browse all 8920

Trending Articles