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リストを作ります。
完成イメージはこんな感じです。
各種説明
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コンテナ内で処理されるようなイメージ。
Node.jsの準備
早速開始、と行きたいところですが、まずは実行環境を構築。
Dockerhubから引っ張ってきてもいいのですが、せっかくなのでDockerfileを書いてあげましょう。
今回はとりあえず各種プログラムが実行できる環境が前提なので、コンテナ内は環境のみ。
ソースファイルはコンテナイメージに含めず、docker-composeを使って後でマウントしてあげることにします。
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
を下記のように修正。
{"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を記述。
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
で問題ありません。
その後の選択肢としては、yarn
とnpm
が出てくるけど、個人的にはnpmのが使いやすいのでそちらで。
cd /vue
vue create frontapp
以上でVue.js側の準備は終了。Node.js側同様に、一度コンテナは停止します。
docker stop frontapp
docker-compose.ymlの準備
Node.jsとVue.jsそれぞれのコンテナを起動する際、composeファイルがあると起動/終了が楽なので、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
localhost:8080
docker-composeコマンドで初回起動時にコンテナイメージが再ビルドされ、 docker run
で起動した時のコンテナイメージとは別の名前でイメージ化されるため、 docker run
コマンドで起動した時のコンテナイメージは不要のため削除します。
docker rmi serverapp:latest
docker rmi frontapp:latest
実行環境の作成はこれにて終了。次から各種機能の実装に入っていきます。
Node.js
まずはサーバサイドの処理から実装します。
処理実装に入る前に、ソースコード変更時に自動的にNodeが再起動されるように少しだけ工夫。app.js
とdocker-compose.yml
を修正します。
...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
...working_dir:/nodecommand:["./node_modules/.bin/nodemon","app"]ports:...
Node.jsの初期状態だとVueからのリクエストを受けられない(エラーがでてしまう)ので、
インストールしたcorsモジュールを追加して、corsを許可する必要があります。
また、nodemonを利用しファイル変更を検知、ファイルが変更されたら自動でサーバが再起動されるようにします。
設定を反映するため、一度コンテナ再起動を実行。
docker-compose down
docker-compose up -d
コントローラーの新規追加がめんどくさいので最初から用意されている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というコンポーネントが存在しているので、そこに肉付けしていくことにします。
まずは各種部品を設置。
<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>
まずはGETで/
にアクセスした際に、apiアクセスを行う部分を実装する。
<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
と紐づけます。
...
<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
に入力されたデータが、data
内currentTask
に反映され、関数内からデータが参照可能となります。
次に@click
イベントを実装し、ボタンが押された時に関数を呼び出すよう修正。
...
<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。
取得したデータをサーバに送信するため、taskCreate
関数の続きを実装していく。
<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
部分を下記のように変更。
<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>
が生成されます。
こうすることで、ページ読み込み時にサーバからデータが取得され、今まで追加したタスクが出てくるようになり、
ボタンを押した時にもタスクが画面に追加されるようになる。
これでタスク追加に関しては実装終了。続いてタスク削除を実装します。
まずは関数を用意。
<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を引数に持ってあげます。
呼び出し部分はこんな感じ。
<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}]// このように配列が操作される
最後にタスク修正した際の処理を実装する。まずは関数の用意から。
<script>...taskUpdate:asyncfunction(id,val){try{awaitaxios.put("http://localhost:3000/task/"+id,{task:val});alert("タスクを修正しました");this.currentTask="";}catch(err){alert(JSON.stringify(err));}}
呼び出し部分はこんな感じ。
<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さんの番です。よろしくおねがいします!