React+Node.jsとsocket.ioでチャットアプリの基礎を作成する
新しいエントリを履歴スタックにプッシュします React Router: Declarative Routing for React.js
https://tomiko0404.hatenablog.com/entry/%E2%96%A0
はじめに
今回の記事の目的
ReactとExpressを利用して簡易チャットアプリを作成した。 実施した内容をメモ/紹介する。 (躓いた点は別記事に切り出す。)
仕様
- メッセージ入力エリアと送信ボタンがある。
- 送信ボタンを押すとメッセージ入力エリアに入力した値を全員に送信する。
- これまでに全員が送信したメッセージの一覧が表示されている。
アプリのイメージ。録画の都合上、1画面分しか撮れていませんが2つのブラウザを開いて交互に操作している。
前提
基本的には以下ここまでにやったことを辿って環境構築+サーバ作成を実施している。
利用している技術・モジュールは以下。
- Node.js
- Express
- React(create-react-app)×Typescript
- socket.io
- socket.io-client
ここまでにやったこと
Step1 dockerを利用して画面サーバ用コンテナとWebサーバ用コンテナを起動した。
Step2 Node.jsでWebHTTPサーバを作成して動かした。
Step3 Reactで画面サーバを作成した。
Step4 CORSを勉強して、画面サーバとWebサーバを接続できるようにした。
Step5 socket.ioを利用して、画面サーバとWebサーバで相互通信できるようにした。
チャットアプリ画面の見た目作成
画面の見た目を作成する。
Chakra UIの利用準備
コンポーネントライブラリのchakra-uiを利用する。 コンポーネントライブラリを使わずにHTMLを書いても問題ないが、簡単にそれなりの見た目が作れるため今回利用する。
Chakra UIのインストール
モジュールをインストールする。
$ yarn add @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^4
公式ページ:Getting Started - Chakra UI
Providerの利用を宣言する
- Chakraproviderをインポートする。
- App.tsxから返却する値をすべて
<ChakraProvider>
で囲む。
import React from "react"; import "./App.css"; import { io } from "socket.io-client"; import { ChakraProvider } from "@chakra-ui/react"; function App() { // (過去記事参照)Webサーバとの接続を確立 const socket = io("http://localhost:3000"); //(過去記事参照) サーバに接続できた場合のイベント処理を定義する(過去記事参照) socket.on("connect", () => { console.log(`socket.connectを出力`); console.log(socket.connect()); // サーバに接続できたかどうかを表示 }); return ( <ChakraProvider> <> <h1>画面サーバ</h1> </> </ChakraProvider> ); } export default App;
見た目の作成
見た目を以下のように作成する。
`App.tsx(import文/return文の中身のみ記載)
import { ChakraProvider, Input, Button } from "@chakra-ui/react";
return ( <ChakraProvider> <> <ul> <li>メッセージ1(仮)</li> <li>メッセージ2(仮)</li> <li>メッセージ3(仮)</li> </ul> <Input placeholder="なにか文字を入力してください" /> <Button>送る</Button> </> </ChakraProvider> );
見た目が完成した。
クライアント側の処理を作成する
App.tsxへの記載内容
クライアントとサーバの相互通信を設定した際に記載したコードをベースに書き足す。
App.tsx
は最終的に以下のようになる。
import React, { useState, useEffect } from "react"; import "./App.css"; import { io } from "socket.io-client"; import { ChakraProvider } from "@chakra-ui/react"; import { Input, Button } from "@chakra-ui/react"; // socketを接続する。引数にはWebサーバ側が待ち受けるドメインを指定する。 const socket = io("http://localhost:3000"); socket.on("connect", () => { console.log(`socket.connectの中身:`); console.log(socket.connect()); }); function App() { // メッセージリスト(全員が送ったメッセージの一覧)をStateとして定義 const [messageList, setMessageList] = useState<string[]>([]); // インプットエリアに入力するメッセージをStateとして定義 const [message, setMessage] = useState(""); // サーバから"chat"イベントが送信されたときの処理 // messageListに、Webサーバから受け取ったメッセージを追加する。 useEffect(() => { socket.on("chat", (msg) => { setMessageList((messageList) => [...messageList, msg]); }); }, []); // インプットエリアの文字が変更されたときの処理。 // messageの値を都度変更する。 const onChangeMessage = (e: React.ChangeEvent<HTMLInputElement>) => { setMessage(e.target.value); }; // 「送る」ボタンを押したときの処理。サーバにmessageを送信する。 const onClickSend = ( e: React.MouseEvent<HTMLButtonElement, MouseEvent> ) => { e.preventDefault(); socket.emit("chat message", message); setMessage(""); }; return ( <ChakraProvider> <> <ul> {messageList.map((data, index) => { return <li key={index}>{data}</li>; })} </ul> <Input placeholder="なにか文字を入力してください" value={message} onChange={onChangeMessage} /> <Button onClick={onClickSend}>送る</Button> </> </ChakraProvider> ); } export default App;
解説
socket接続部分
Webサーバとの接続を確立する、const socket = io("http://localhost:3000");
以下の文については、画面がレンダリングされるたびに実行される必要はなく、最初の1回のみ実行されればよい。
そのため、App関数の外に出しておいた。
インプットエリアに入力した文字をサーバに送る機能
送るボタンの設定
<Button>
コンポーネントのonClickイベントにonClickSend
関数を設定する。
<Button onClick={onClickSend}>送る</Button>
送るボタンをクリックしたときに起動する関数
onClickSend
関数の処理内容は以下のようになっている。
// 「送る」ボタンを押したときの処理。サーバにmessageを送信する。 const onClickSend = ( e: React.MouseEvent<HTMLButtonElement, MouseEvent> ) => { e.preventDefault(); socket.emit("chat message", message); setMessage(""); };
引数
引数になっている(e)
は、一般的なJavaScriptと同じでbuttonコンポーネントから受け取るイベントである。TypeScriptではe
の型も指定する必要があるため、以下の記事を参考に設定している。
any型で諦めない React.EventCallback - Qiita
e.preventDefault()
次にe.preventDefault();
は、「クリックなどのイベントに対し、ブラウザが勝手に規定している処理を実行させない」という指定である。
こちらの記事がわかりやすかった。
JavaScriptのpreventDefault()って難しくない?preventDefault()を使うための前提知識 - Qiita
socket.emit
一番重要なのがsocket.emit
である。
socket.emit([イベント名], 引数);
の構文でサーバに対してイベントを発行できる。
公式ページ:Client API | Socket.IO
socket.emit("chat message", message);
今回は、chat message というイベント名で、message
に入っている値をWebサーバ側に送信する。
messageを管理するstate
ここで、message
はインプットエリアに表示する値であり、「送る」ボタンを押したときにWebサーバ側に送る値である。この値は画面がレンダリングされるたびに初期化されては困るので、state管理する。
// インプットエリアに入力するメッセージをStateとして定義 const [message, setMessage] = useState("");
初期値は空文字""
としている。
useState
のインポートも必要なので注意する。
import React, { useState} from "react";
インプットエリアの表示制御
インプットエリアの値が変更されたらその値を新たにmessage
ステートに格納し、更にインプットエリアには常にmessage
の値を表示する(一般的な入力フォームの書き方)。
<Input placeholder="なにか文字を入力してください" value={message} onChange={onChangeMessage} />
// インプットエリアの文字が変更されたときの処理。 // messageの値を都度変更する。 const onChangeMessage = (e: React.ChangeEvent<HTMLInputElement>) => { setMessage(e.target.value); };
メッセージの一覧を表示する機能
メッセージリストの管理
メッセージリストもStateとして管理する。
// メッセージリスト(全員が送ったメッセージの一覧)をStateとして定義 const [messageList, setMessageList] = useState<string[]>([]);
メッセージの一覧なので、初期値は空配列[]
となる。型定義は<string[]>
と書くことでstring型の配列を指定する。
サーバから「誰かがメッセージを送信した」イベントを受け取ったときの処理
// サーバから"chat"イベントが送信されたときの処理 // messageListに、Webサーバから受け取ったメッセージを追加する。 useEffect(() => { socket.on("chat", (msg) => { setMessageList((messageList) => [...messageList, msg]); }); }, []);
socket.on
重要なのはsocket.on
関数である。
socket.on(イベント名, 関数)
の構文で、Webサーバから指定のイベント名のイベント受け取ったら、関数で定義した処理を実行することができる。今回は、誰かがメッセージを送信したら全員がWebサーバからchat
イベントを受け取る。
公式ページ: Client API | Socket.IO
イベントを受け取ったときの処理
setMessageList((messageList) => [...messageList, msg]);
useState
で定義されたsetState
関数(ここではsetMessageList
)には、値または関数を設定することができる。
これまでのmessageList
の値(イベントを受け取る前までのメッセージ一覧)に対し、Webサーバから受け取った新規の発言をmessageListの一番後ろに追加する。
useEffectについて
「イベントを受け取ったときに実行する関数」をレンダリングのたびに定義していると、レンダリング量が増えすぎてしまった。結果、画面表示が非常に低速になってしまった。
そこで、useEffectを利用することで、レンダリングの数を抑えることにした。
実際、setState関数に設定する値の中に、そのstateを含む場合(今回の場合、setMessageListに設定する値に、messageListを利用する)は処理が難しくなるようで、かなり苦労した部分なので、別記事にて解説する。
useStateの使い方で苦しんだ別記事:
【React/ useState】setState関数の書き方が下手で大量レンダリングが起こった話 - エンジニアを目指す日常ブログ
メッセージリストの表示
こちらはメッセージリストから、'map'関数を利用して1個ずつ要素を取り出し<li>
要素に入れているだけ。key
はインデックス(配列の何個目の要素かを表す)にしてしまったが、実際は「名前+送信時間」などにすべきと考えている。
<ul> {messageList.map((data, index) => { return <li key={index}>{data}</li>; })} </ul>
サーバ側の処理の作成
クライアントとサーバの相互通信を設定した際に記載したコードをベースに書き足す。
あるブラウザからメッセージを受け取ったら、全員に送信する機能
追記するのは以下の部分だけである。
socket.on("chat message", (msg) => { io.emit("chat", msg); });
クライアント側のsocket.emit("chat message", message);
で指定されたchat message
イベントを受けて、クライアント全体にchat
イベントを送信する。
公式ドキュメント: Server API | Socket.IO
index.jsの記載内容
最終的にサーバ側のindex.js
の記載は以下のようになる。
const express = require("express"); const portNumber = 3000; // ポート番号 const app = express(); // Expressを利用したサーバ const server = require("http").createServer(app); // Expressを用いないserverも必要 app.get("/", (req, res) => { res.status(200).send("OK!"); }); // サーバーオブジェクトsocketioを作成する const { Server } = require("socket.io"); const io = new Server(server, { cors: { // Server作成時の引数にCORSオプションを追加する origin: "*", methods: ["GET", "POST"], }, }); io.on("connection", (socket) => { // ブラウザから接続されたときの処理 console.log("a user connected"); // ブラウザが切断したときの処理 socket.on("disconnect", () => { console.log("user disconnected"); }); socket.on("chat message", (msg) => { io.emit("chat", msg); }); }); // serverをPORT3000で待ち受ける。app.listenだとNG。 server.listen(portNumber); console.log(`Web server is on. PortNumber is ${portNumber}.`);
アプリ動作確認
ブラウザの接続
2つのブラウザを画面サーバに接続すると、Webサーバ側とsocket接続が2件確認できた。
メッセージの送信
左側のブラウザにメッセージを入れて送信すると、2つのブラウザにメッセージが表示された。
おわりに
socket.ioを利用してクライアント(ブラウザ)とWebサーバの接続を維持して、イベントを送信/受信できるようになった。
関連記事
useStateの書き方で苦しんだ部分
つづき
入室機能とユーザ名設定機能を実装する
チャットアプリの見た目を修正する
画面サーバ(React)とWebサーバ(Node.js)をSocket.ioで接続する方法メモ
はじめに
今回の記事の目的
画面サーバ(React)とWebサーバ(Node.js)を別々に構築して、クライアントとWebサーバ間をSocket.ioで接続する。
チャットアプリを作る事前準備になる。
参考記事
- 公式ドキュメントのクライアントサイドの設定方法。Reactでのやり方は載っていない。
Client Initialization | Socket.IO
- Reactを利用している例。 socket.io で初めてチャット機能を作ってみる
前提
環境は以下の記事の通り、Webサーバ用コンテナと画面サーバ用コンテナを用意し、各サーバを立てている状況。
おそらくこんなイメージ。(間違っていたら修正します)
Webサーバ側の設定
socket.ioのインストール
socket.ioをインストールする。
$ yarn add socket.io
index.jsの変更
Webサーバ側のindex.js
は以下の記載とする。
index.js
// 基本設定 const express = require("express"); const portNumber = 3000; const app = express(); // Expressを利用したサーバ作成 const server = require("http").createServer(app); // Expressを用いないserverも必要なので作成 // 別オリジンからのアクセスを許可する(CORSモジュール利用) const cors = require("cors"); app.use(cors()); // HTTPアクセス時の応答を定義 app.get("/", (req, res) => { res.status(200).send("OK!"); }); // サーバーオブジェクトsocketioを作成する const { Server } = require("socket.io"); const io = new Server(server, { cors: { // corsモジュールでは上手くCORSできないため、Server作成時の引数にオプションを追加する origin: "*", methods: ["GET", "POST"], }, }); // ブラウザから接続されたときの処理を定義する io.on("connection", (socket) => { // ブラウザから接続されたときの処理 console.log("a user connected"); socket.on("disconnect", () => { // ブラウザが切断したときの処理 console.log("user disconnected"); }); }); // serverをPORT3000で待ち受ける。 // ※app.listenだとNG。 server.listen(portNumber); console.log(`Web server is on. PortNumber is ${portNumber}.`);
解説
基本設定
以下の部分は、基本設定になるため過去の記事を参照。Expressサーバのapp
と、createServerで作ったserver
の2つがあることに注意。listen
する際はcreateServerで作ったserver
を利用している。
// 基本設定 const express = require("express"); const portNumber = 3000; const app = express(); // Expressを利用したサーバ作成 const server = require("http").createServer(app); // Expressを用いないserverも必要なので作成 // 別オリジンからのアクセスを許可する(CORSモジュール利用) const cors = require("cors"); app.use(cors()); // HTTPアクセス時の応答を定義 app.get("/", (req, res) => { res.status(200).send("OK!"); }); // serverをPORT3000で待ち受ける。 // ※app.listenだとNG。 server.listen(portNumber); console.log(`Web server is on. PortNumber is ${portNumber}.`);
過去の記事
■WEBサーバと画面サーバを作成して相互に通信させる - エンジニアを目指す日常ブログ
■
Node.js + Express でWEB-APIサーバを作成する - エンジニアを目指す日常ブログ
クライアントとの接続
Socket.ioサーバの作成は以下の記載で実施する。
const { Server } = require("socket.io"); const io = new Server(server)
公式ドキュメント socket.io
CORSについて
CORSについては、cors
モジュールを利用すれば問題ないかと思ったが、これでは接続できなかった。
そのため以下のドキュメントに従って設定を行なった。 socket.io
// const io = new Server(server)を書き換え const io = new Server(server, { cors: { // corsモジュールでは上手くCORSできないため、Server作成時の引数にオプションを追加する origin: "*", methods: ["GET", "POST"], }, });
ブラウザからの接続時の挙動
以下の記載をすることで、ブラウザから接続された際にログを出力することができる。
io.on("connection", (socket) => { console.log("a user connected"); });
また、接続しているときに切断された場合は再度ログを出力するためには、以下の記載に変更する。接続されたときの処理の中に、切断されたときの処理が記載されていることに注意する。
io.on("connection", (socket) => { // ブラウザから接続されたときの処理 console.log("a user connected"); socket.on("disconnect", () => { // ブラウザが切断したときの処理 console.log("user disconnected"); }); });
画面サーバ側の設定
socket.ioとsocket.io-clientをインストール
画面サーバにモジュールをインストールする。
$ yarn add socket.io-client
もしかしたらsocket.io
は不要かも。
$ yarn add socket.io
以下のページを参照。
Client Initialization | Socket.IO
ちなみに、socket.io-client
をインストールしない方法としてindex.html
に以下記載を追加したり色々試してみたが、うまくいかなかった。
<script src="/socket.io/socket.io.js"></script>
App.tsxの変更
App.tsx
に以下記載を追加する。
App.tsx
import React from "react"; import "./App.css"; // socket.io-clientをインポートする import { io } from "socket.io-client"; function App() { // Webサーバとの接続を確立 // ※Webサーバと別ドメインの場合には、io()に引数が必要 const socket = io("http://localhost:3000"); // サーバに接続できた場合のイベント処理を定義する socket.on("connect", () => { console.log(`socket.connectを出力`); console.log(socket.connect()); // サーバに接続できたかどうかを表示 }); return ( <div className="App"> <h1>画面サーバからの返却</h1> </div> ); } export default App;
解説
io()
以下の記載でWebサーバに接続している。
const socket = io("http://localhost:3000");
ドメインの異なるWebサーバに接続する際は引数にURLを入れる必要がある。
socket.on
socket.on("connect", () => { });
socket.on()
は、イベント名と処理を引数に取る関数。イベント名は自分で決めることができる。このイベント名をキーにしてサーバと情報をやり取りする。
一方で、"connect"
は事前に定義された特別なイベントであり、サーバに接続したことを示す。今回のように書くとサーバ接続時に実行したい処理を記載できる。
socket.io
socket.connect()
socket.connect()
を利用すると、サーバへの接続時の情報を取得できる。
socket.io
接続できたかどうかの二値だけであれば、socket.connected()
が使える。
接続結果
Webサーバと画面サーバを起動して、画面サーバlocalhost:8000
にアクセスすると、以下のようにコンソールに接続情報が表示された。
また、画面サーバ側もコンソールログが表示されていることが確認できた。
$ node index.js Web server is on. PortNumber is 3000. a user connected a user connected
※1回ブラウザにアクセスすると2回接続しているように見えるので要調査。
おわりに
画面サーバ(React)とWebサーバ(Node.js)を別々に構築して、クライアントとWebサーバ間をSocket.ioで接続することができた。
関連記事
つづき:チャットアプリの基礎を作成する
WEBサーバと画面サーバを作成して相互に通信させる
はじめに
今回の記事の目的
自分で画面サーバとWebサーバを作成し、通信させる。 具体的には、画面サーバがWebサーバから値を取得して、画面表示する。
また、このときに必要になるCORSについて理解する。
前提
開発環境
- ローカル環境はLinuxを利用する。
Windows上にLinux環境を構築する方法
Linux仮想環境:Vagrantの立ち上げ方メモ - エンジニアを目指す日常ブログ
docker環境
必須ではないが、今回は画面サーバ用のdockerコンテナとWebサーバ用のdockerコンテナを2つ用意した。
将来的にAWS ECSに載せたいのでこの構成にした。
dockerを立ち上げる方法メモ
tomiko0404.hatenablog.com
Webサーバの作成
WebサーバはNode.jsで作成した。
Webサーバの作成方法メモ。コンテナ上で実施しているので、「Node.js自体のインストール」は不要。 tomiko0404.hatenablog.com
画面サーバの作成
画面サーバはReact×Typescriptで構築した。
■Reactプロジェクトを作成する方法
tomiko0404.hatenablog.com
■Reactアプリを編集する方法
tomiko0404.hatenablog.com
全体構成
ローカルフォルダの構成は以下となっている。
├── back ※backendコンテナにマウントしている │ ├── index.js │ ├── node_modules │ ├── package-lock.json │ ├── package.json │ └── yarn.lock ├── docker │ ├── Dockerfile │ └── docker-compose.yml └── front ※frontendコンテナにマウントしている ├── README.md ├── node_modules ├── package.json ├── public ├── src ├── tsconfig.json └── yarn.lock
Webサーバの内容
Webサーバは、実行するとポート3000番でリクエストを待ち受け、GETメソッドを受け取ると、ステータスコード200、ボディ部に"Hello World from backend!!!"を返却するアプリにする。
index.js
の記載内容は以下のようにする(後ほど、CORSのために追記が必要)。
index.js
// expressをrequireする const express = require("express"); // ポート番号を変数に設定 const portNumber = 3000; // appオブジェクトを作成する const app = express(); // getメソッドで、ルートパスにアクセスしてきたときの処理を記載する app.get("/", (req, res) => { res.status(200).send("Hello World from backend!!!"); }); // リクエストを待ち受ける app.listen(portNumber); console.log(`PortNumber is ${portNumber}`);
node index.js
を実行した後、ブラウザにlocalhost:3000
を入力してアクセスすると、値が返ってきているのがわかる。
画面サーバの内容
画面サーバは、WebサーバにGETメソッドでHTTPアクセスして、応答された値をコンソールに表示した上で、画面にも表示するアプリとする。
axiosのインストール
HTTPで値を取得するために便利なaxiosモジュールをインストールする。
node@b0af091276ac:/app$ yarn add axios
App.tsxの作成
前提記事の通りにReact×Typescriptで作成した上で、今回、App.tsx
を以下のように記載する。
App.tsx
import React, { useState } from "react"; import axios from "axios"; import "./App.css"; function App() { const [msg, setMsg] = useState("なし"); // msgステートを作成し、初期値は"なし" axios .get("http://localhost:3000") // localhost:3000にアクセス .then((res) => { console.log(res.data); // 取得した値をコンソールに表示 setMsg(res.data); // msgステートに取得した値を格納 }) .catch((err) => console.log(err.responce)); return ( <div className="App"> <h1>{msg}</h1> // h1の中身にmsgステートの値を表示 </div> ); } export default App;
get
の中身はlocalhost:3000
ではなくhttp://localhost:3000
となることに注意する。(補完してくれるツールが多いため忘れてしまった)
Reactがわからない場合はこちらへ: Reactに入門した人のためのもっとReactが楽しくなるステップアップコース完全版 | Udemy
実際に画面サーバからWebサーバにアクセスすると、同一オリジンポリシーにより、はじかれる
画面サーバからWebサーバにアクセスする
Webサーバ側のコンテナでnode index.js
を実行した状態で、画面サーバ側でyarn start
を実行する。
Webサーバから値をとることができていないことがわかる。
エラーの内容は、「CORSポリシーにブロックされており、応答のヘッダに'Access-Control-Allow-Origin'が無いです」というもの。
Access to XMLHttpRequest at 'http://localhost:3000/' from origin 'http://localhost:8000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
CORS(Cross-Origin Resource Sharing) について
少し勉強が必要。私の理解で一言にすると以下(正確ではない)。
■CORSとはそもそも何かの参考
なんとなく CORS がわかる...はもう終わりにする。 - Qiita
■具体的なCORSの解説
こちらによれば「単純な要求」と「単純ではない要求」で対処が変わるとのこと。(今回は単純な要求)
CORS とは? - JavaScript の基本 - JavaScript 入門
ヘッダを確認
Postmanを利用してローカルからWebサーバにアクセスした際のレスポンスのヘッダを確認すると、Access-Control-Allow-Origin
応答は無さそう。
異なるオリジンからのアクセスを許可する
すべてのAPIへのアクセスを許可する方法
CORS専用のモジュールがあるとのことなので、単純に使うのがよさそう。
corsモジュールインストール
Webサーバのdocker上で以下を実行する。
$ yarn add cors
index.jsの追記
以下の記載をindex.js
に追記する。
index.js
const express = require("express"); // CORSをインポートする const cors = require("cors"); const portNumber = 3000; const app = express(); // 全てのオリジンからのアクセスを許可する app.use(cors()); app.get("/", (req, res) => { res.status(200).send("Hello World from backend!!!"); }); app.listen(portNumber); console.log(`PortNumber is ${portNumber}`);
結果確認
ここで画面サーバ(localhost:8000
)にアクセスしてみると、Webサーバから値を取得できたことがわかる。
Postmanでアクセスしてみると、ヘッダに
Access-Control-Allow-Origin: "*"
が追加されているのがわかる。
一部のAPIへのアクセスを許可する方法
個別のAPIへのアクセスを許可するには、get
メソッドの引数に設定する。
app.get("/", cors(), (req, res) => { res.status(200).send("Hello World from backend!!!"); });
参考資料:
express.jsのcors対応 - Qiita
特定のオリジンからのアクセスのみ許可する場合
App.tsx
を以下のように書き換える。
App.tsx
const express = require("express"); const cors = require("cors"); const portNumber = 3000; const app = express(); // 一部のオリジンからのアクセスを許可する const corsOptions = { origin: ["http://localhost:8000", "http://example.com"], }; app.use(cors(corsOptions)); app.get("/", (req, res) => { res.status(200).send("Hello World from backend!!!"); }); app.listen(portNumber); console.log(`PortNumber is ${portNumber}`);
参考資料:
ExpressとTypeScript APIにCORSサポートを追加する方法
おわりに
今回Webサーバと画面サーバを通信させることができた。また、CORSへの理解が深まった。
docker-composeを利用してdockerコンテナを作成する方法メモ(複数コンテナ対応)
はじめに
本記事の目的
dockerコンテナを立ち上げる。 また、今回はdocker-composeというツールも利用する。
基礎知識として、docker-composeツールを使わずにDockerを立ち上げる方法は以下の記事に記載している。
いかの記事ではAWSのEC2上で実施しているが、もちろんEC2を使わずローカルで実施も可能。
EC2サーバ(Linux)上でdockerコンテナを起動する方法メモ(nginxベース) - エンジニアを目指す日常ブログ
前提
- Linux環境とする。
今回はWindows上にVagrantで仮想環境を用意している。 tomiko0404.hatenablog.com
- はじめに
- dockerインストール
- Dockerfileの作成
- docker-compose.ymlファイルの作成
- イメージをビルドし、コンテナを作成する
- 【補足】コンテナ上でアプリを実行してみる
- おわりに
dockerインストール
以下サイトを参考に実施した。
Vagrantの上にDockerを載せて開発環境を作成する(おそらくこちらを見ながらやったはず)
Ubuntuにdockerをインストールする - Qiita
docker-composeもインストールが必要。
Docker Compose のインストール | Docker ドキュメント
Dockerfileの作成
Dockerfileとは、dockerイメージファイルを作るときに元ネタとなる設定値を記載するファイル。 dockerイメージはdockerコンテナの設計図のようなもので、基本的にdockerは、
- Dockerfileに従ってdockerイメージをビルド
- dockerイメージに従ってコンテナを作成
という流れでコンテナを作成する必要がある。
今回はDockerfileを以下のように記述した。
FROM node:14.15.4-slim RUN apt-get update && apt-get install -y locales \ && locale-gen en_US.UTF-8 \ && localedef -i en_US -f UTF-8 en_US.UTF-8 \ && echo "export LC_ALL=en_US.UTF-8" >> ~/.bashrc \ && ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime RUN apt-get install -y \ wget \ curl \ && apt-get clean \ && rm -rf /var/lib/apt/lists/*
一番上の
FROM node:14.15.4-slim
は、dockerhubという、コンテナイメージの倉庫のようなところから既存のイメージを持ってくるための記載。
今回はNode.jsを利用してアプリを作りたいので、Node.jsのイメージを持ってくる。
■docker hubの node ページ。 hub.docker.com ■nodeのReadmeページ。Dockerfileの書き方例なども載っている。 github.com
docker-compose.ymlファイルの作成
docker-composeは、一言でいえば「Docker構築を楽にするためのツール」である。
docker-composeツールを使うと、コマンド docker-compose up
一つで「Dockerfileに従ってdockerイメージをビルド」⇒「dockerイメージに従ってコンテナを作成」を実行してくれる。
その際の設定値を規定するのがdocker-compose.yml
である。
dockerの各種コマンド(イメージをビルドするコマンドや、コンテナ起動するコマンド)の引数をdocker-compose.yml
で事前に定義しておくイメージ。
今回作成したdocker-compose.ymlファイル
今回は、「画面サーバ」と「Webサーバ」を用意したいので、2つのコンテナを立ち上げたい。 記載内容は以下となる。
docker-compose.yml
# このファイルはversion3の書き方仕様で書いてますよという宣言。おまじない扱いで良い。 version: '3' # networkを作成する。複数のコンテナどうしでネットワークを構成したいときに # 作成する。基本はbridgeを利用する。「react_net」は自分で設定したネットワーク名。 networks: react_net: driver: bridge # 作りたいサービスを規定する。 # 今回は「frontend」と「backend」の2つのサービスを作成。 services: frontend: # サービス名 build: ./ # Dockerfileを利用してイメージをbuildする。Dockerfileの場所を記載する。 image: react_front # buildしたイメージの名前を決める。 container_name: frontend # 起動するコンテナの名前を決める。 tty: true # 調査中 volumes: - ../front:/app # ローカルの ../front フォルダを、コンテナ上の/appフォルダにマウントする working_dir: "/app" # コンテナが立ち上がったときに最初にいるフォルダの場所を決める ports: - 8000:8000 # ローカルの8000ポートとコンテナの8000ポートをポートフォワーディング networks: - react_net # 上で定義したreact_netネットワークに接続する backend: build: ./ image: node_back container_name: backend # コンテナ名は別名を設定する tty: true volumes: - ../back:/app working_dir: "/app" ports: - 3000:3000 # ポートフォワーディングもかぶらないよう設定 networks: - react_net
コンテナ1つで良い場合
コンテナ1つで良い場合は、以下の記載とする。
version: '3' networks: react_net: driver: bridge services: frontend: build: ./ image: react_front container_name: frontend tty: true volumes: - ../:/app working_dir: "/app" ports: - 8000:8000 networks: - react_net
docker-compose.ymlの記載内容について
各種設定の中身は以下記事が参考になる。 【Docker】初心者のための Docker Compose まとめ - AI can fly !!
参考:Dockerfileを利用しない方法
上記参考記事のimage
オプションの解説を見ると以下の記載がある。
前述の build が指定されている場合は、 build で指定された Dockerfile から Docker イメージが構築され、そのイメージ名が image で指定した名前になります。 build を指定しない (Dockerfile からイメージを構築しない) 場合、ここで指定された Docker イメージからコンテナを作成しますが、指定した Docker イメージがローカルに存在しない場合は、 Docker Hub から Pull (取得) した上でコンテナが作成されます。
今回はbuild
コマンドで自作のDockerfileを指定しているが、単純に
image: node:14.15.4-slim
と記載しても問題ない。この場合Dockerfileが不要になる。
イメージをビルドし、コンテナを作成する
(事前確認)dockerイメージの存在確認
今あるdockerイメージを確認する。
dockerイメージを確認するコマンド:docker images
$ sudo docker images
過去に作ったものが残っていた。node
は公式イメージ。
REPOSITORY TAG IMAGE ID CREATED SIZE react-practice-6 latest d4a0b79d2c36 3 weeks ago 473MB node 14.15.4-slim 2f75d89d8162 8 months ago 167MB
イメージ作成とコンテナ起動
以下のコマンドを実行する。
$ sudo docker-compose up -d
イメージが作られたことの確認
docker-compose.ymlで指定した名前のdockerイメージが作成された。
$ sudo docker images REPOSITORY TAG IMAGE ID CREATED SIZE node_back latest e6e824760f03 About a minute ago 216MB react_front latest e6e824760f03 About a minute ago 216MB react-practice-6 latest d4a0b79d2c36 3 weeks ago 473MB node 14.15.4-slim 2f75d89d8162 8 months ago 167MB
コンテナが起動したことの確認
- 起動中のコンテナを確認するコマンド:
docker ps
- 起動中に限らずコンテナを確認するコマンド:
docker ps -a
frontendコンテナとbackendコンテナが起動された。
$ sudo docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES fce6328cdc9f react_front "docker-entrypoint.s…" 3 minutes ago Up 3 minutes 0.0.0.0:8000->8000/tcp, :::8000->8000/tcp frontend 3eb02504a2a0 node_back "docker-entrypoint.s…" 3 minutes ago Up 3 minutes 0.0.0.0:3000->3000/tcp, :::3000->3000/tcp backend
コンテナの中に入って操作する
コンテナの操作は基本的に'docker exec'で実施することができる。 毎回書くのは大変なので以下コマンドでターミナルを立ち上げることができる。
$ sudo docker exec -u node -it frontend /bin/bash -l
docker exec
の構文は以下の通りである。
$ docker exec [オプション] [コンテナ名] [コマンド] [引数...]
-u node
はユーザ名を指定している。今回は公式のnode
イメージのデフォルトユーザ名node
を指定。
-it
は対話型を指定するオプション。
frontend
はコンテナ名。
/bin/bash -l
が実行したいコマンドとなる。
コンテナから抜ける
コンテナから抜けたいときはexit
コマンドで抜けられる。
コンテナを停止する
コンテナを停止したいときは
$docker-compose down
【補足】コンテナ上でアプリを実行してみる
Node.jsで簡単なHTTPサーバが動くか確認。
コンテナと共有しているフォルダにapp.js
を作成する。
ローカルのフォルダ構成
ローカルのフォルダ構成は以下のようになっている。
. ├── back ※backendのdockerにマウント │ └── app.js ├── docker │ ├── Dockerfile │ └── docker-compose.yml └── front ※frontendのdockerにマウント(今は空)
app.jsの作成
app.js
const portNumber = 8000; const http = require("http"); http.createServer((req, res) => { res.writeHead(200, { "Content-Type": "text/plain" }); res.end("Hello World"); }).listen(portNumber); console.log(`PortNumber is ${portNumber}`);
app.jsを実行
コンテナに入った状態で以下を実行する。
node@fce6328cdc9f:/app$ node app.js
ブラウザからlocalhost:8000
にアクセスすると応答が返ってくる。
今回は、Node.jsのみで実行できるアプリのため、app.js
を作成するだけで実行できる。追加モジュールが必要な場合は、npm install
などで追加する必要がある。
おわりに
docker-composeを利用してコンテナを立ち上げる方法をメモした。
以前にも同じような記事を書いたが、この理解度で複数のコンテナを立ち上げようとしたら行き詰ってしまったので、docker-composeについて勉強しました。 結果、理解が深まったので整理がてら書き直したのが本記事です。
以前の記事
vagrant上にdockerコンテナを立ち上げる方法メモ(docker-compose利用) - エンジニアを目指す日常ブログ
関連記事
つづき:WebAPIサーバの開発
上記のapp.js
の続きで、Node.jsでWebAPIサーバを作成した。
つづき:フロントエンドの開発
dockerコンテナ上にReactアプリ作成した。
参考資料
dockerコマンドが豊富な記事。
Linux仮想環境:Vagrantの立ち上げ方メモ
はじめに
今回の記事の目的
Windows上にLinuxの仮想環境を構築する方法として有名なVagrantの立ち上げ方メモ。
Vagrantと言っているが、仮想環境の実態はVirtualBoxで、VagrantはVirtualBoxを操作するためのソフトらしい。
元々以下の記事に記載していたが、わかりにくいので切り出した。 tomiko0404.hatenablog.com
前提
今回は以下の前提とする。やり方を忘れてしまったので、別途復習したら記事にする予定。
- VirtualBoxがインストールされていること。
- Vagrantがインストールされていること。
- Vagrantを用いたLinux環境構築ができていること。
参考になりそうな資料
kitsune.blog
vagrantを立ち上げる
Powershell(Window標準)を管理者権限で立ち上げる
Vagrantfileのあるフォルダに移動する
PS C:\WINDOWS\system32> cd C:\Users\User\vagrant\ubuntu64_18
vagrantの起動とssh接続
Vagrantの起動。
> vagrant up
> vagrant ssh
Vagrantをシャットダウンする際は、
> vagrant halt
とする。
Vagrantfileの中身
Vagrantfileには以下の記載がある。
Vagrant.configure("2") do |config| config.vm.box = "ubuntu/bionic64" config.vm.network "forwarded_port", guest: 8000, host: 8000 config.vm.synced_folder "./workspace", "/home/vagrant/workspace" config.vm.provider :virtualbox do |vb| vb.customize ["setextradata", :id, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/home/vagrant/workspace","1"] end config.vm.provider "virtualbox" do |vb| vb.memory = "1024" end end
workspaceに移動する
Vagrantfileの中で
config.vm.synced_folder "./workspace", "/home/vagrant/workspace"
と記載しているため、ローカルの./workspace
フォルダ(Vagrantfileがある場所からの相対パス)と、Vagrant上の/home/vagrant/workspace
は同期されている。
扱いやすいように、このフォルダ上で作業をするのが良い。
$ cd workspace
でworkspaceフォルダに移動できる。このフォルダはWindows上からも操作可能。
おわりに
開発作業するたびにVagrantを起動する必要があるため、やり方をメモした。
手書きで書いた文字を画像に入れ込む方法
はじめに
今回の記事の目的
今回はプログラミングの勉強から少し離れる。 手書きで書いた文字の黒い部分を抽出し、データ化する方法を考えたのでメモ。
活用方法
手書きで味のある文字を写真の上などに乗せられたらかっこいいなと思い実施。 お祝いのプレゼントの添え物として、フォトブック(すぐアル|パレットプラザなど)を作り、そのうち1ページは写真の上に手書きの手紙が乗っているとオシャレではないかと思います。
両親の還暦祝いがあったので検討した。
前提
画像編集ソフトのGIMPがインストールされていること。
手書きの文字の準備
手書きの文字を用意。
- なるべく白い紙に
- なるべく黒いペンで書く。
書けたら写真を撮ってPCに転送する。
手書き文字から文字のみ抽出
写真をGIMPに取り込む
GIMPのウィンドウに画像をドラッグ&ドロップする。(上のバーから「ファイル」⇒「開く/インポート」でも良い)
トーンカーブの補正
右クリック⇒「色」⇒トーンカーブを選択。
背景部分を真っ白に寄せる
まずはトーンカーブの右上の点を、真左にスライドしていく。 すると、背景にしたい領域である、「元の画像で白に近いけど真っ白ではなかった部分」が、真っ白に近づいていく。
これくらいまでスライドすると、ノートの罫線が消えて文字と背景を白黒の2色に分けることができた。
もっとスライドすると画像の下のほうの陰になっている部分も白に近づけることができるが、ここはトリミングでざっくり消せそうなので、放置とする。
文字部分を真っ黒に寄せる
次にトーンカーブの左下の点を、真右にスライドしていく。 すると、文字領域である、「元の画像で黒に近いけど真っ黒ではなかった部分」が、真っ黒に近づいていく。
これで、文字の部分がだいぶはっきりした。変更後のカーブを見ると、変更後の明るさが明るさ最大(真っ白)と明るさ最小(真っ黒)の二値に近づいていることがわかる。
モードを白黒二値に変更する
画像を拡大してよく見ると、白と黒の中間の色が少しずつ残ってしまっているので、これらを完全に白or黒に寄せる。
画像⇒「モード」⇒「インデックス」を選択。
「Use black and white (1bit) palette」を選んで「変換」を押下。
これで拡大しても、白黒の二値画像となった。
背景(白部分)を削除する
アルファチャンネルの追加
これまで作業していた文字のレイヤーにアルファチャンネルを追加する。
レイヤーウインドウから、レイヤーを右クリック⇒「アルファチャンネルを追加」を押下する。
色を透明度に
上のバーから「色」⇒「色を透明度に」を選択する。これで白部分が透明となる。
モードをRGBに戻す
今後の編集を考えて、モードを「インデックス」から「RGB」に戻す。
完成
手書き文字だけ切り抜かれた画像が完成した。
下の影は矩形選択⇒消去でOK。
【応用編】写真の上に画像を乗せる
背景写真に文字を乗せる
写真に文字を乗せてみた。(注:写真は自分で撮影した画像です)
このままでも十分読めるが、写真によっては暗かったりして文字が読みにくい場合があるので、調整することも可能。
このように画像の明るい部分をより明るくしてもよい。
また、暗い部分を白っぽくし、コントラストを弱くすることで背景っぽい画像にしてもよい。こちらは人物が入った写真の時に有用な感覚が(私には)ある。
文字の周りに光彩を追加する
背景が暗い画像の時は文字のほうを工夫しても良い。 (注:この写真も自分で撮影した画像です)
光彩を追加してみる。
選択範囲の作成
レイヤーウインドウで、文字のレイヤーを選択し右クリック⇒「不透明部分を選択領域に」を押下する。
画像を右クリック⇒「選択」⇒「選択範囲の拡大」をクリックする。
拡大範囲は適宜調整だが、今回は4pxとした。
塗りつぶし
この状態で「新しいレイヤーを追加」し「光彩」レイヤーとする。
その後、新しいレイヤーを選択した状態で黄色で塗りつぶした。
ぼかし
画像を右クリック⇒「フィルター」⇒「ぼかし」⇒「ガウスぼかし」⇒2.5pxと選択していく。
レイヤー順変更
最後に文字レイヤーの下に光彩レイヤーを配置すると、光彩のようになる。
【参考】トーンカーブとは
画像の明るさを調整するツール。
- 横軸が「今の明るさ」
- 縦軸が「変更後の明るさ」
を示している。 また、うっすら見える面グラフは、選択中の画像のうち、「今の明るさ」の分布を示している。
横軸で、ある明るさを持つ領域を示す点を決めて、その点を上下に動かすと、「その領域を今より明るくしたいのか暗くしたいのか」を調整できる。
おわりに
手書きの文字を抽出するために、色を「白」「黒」の二値に分けてからの切り抜きを実施した。
今後活用していきたい。
【Node.js+Express】ミドルウェアとapp.useについてメモ
はじめに
今回の記事の目的
Expressで利用される「ミドルウェア」と、ミドルウェアを呼び出すときに使われるapp.use
について調べたことをメモ。
前提
- Node.jsとnpm(Node.jsのパッケージマネージャ)がインストールされていること。
- Node.js + Expessを利用してHTTPサーバを立ち上げていること。
前回の記事 tomiko0404.hatenablog.com
ミドルウェアとは
Expressの公式ページには以下の記載がある。
Express は、それ自体では最小限の機能を備えたルーティングとミドルウェアの Web フレームワークです。Express アプリケーションは基本的に一連のミドルウェア関数呼び出しです。
そもそもExpressはミドルウェアの呼び出しという機能がメインなんです、という話のようである。
ミドルウェアは、以下のように関数で定義される。
( req, res, next) => { 処理 next(); }
app.send()
の引数にする関数と同様で、reqはhttp.IncomingMessageクラスのインスタンスであり、resはhttp.ServerResponseクラスのインスタンスである。
next
は次のミドルウェアを呼び出す関数である。
また、エラー処理を行なうミドルウェアは、以下のように引数が4つとなる。
// エラー処理を行うミドルウェア ( err, req, res, next) => { 処理 }
ミドルウェアを利用するapp.use
ミドルウェアの種類
ミドルウェアには以下の種類があるらしい。
アプリケーション・レベルのミドルウェアは、app.use()
などのappオブジェクトで利用できる。
const express = require('express'); const app = express(); app.use( (req, res, next) => { 処理 next() })
ルーター・レベルのミドルウェアは、routerオブジェクトで利用できる。(routerオブジェクトについては未勉強のため今回は省略する。)
const express = require('express'); const router = express.Router(); router.use((req,res,nest) => { 処理 next() })
app.use()について
app.use()
は、リクエストの種類にかかわらず実行される関数を定義できる。
公式の記載は以下の通り。
指定されたミドルウェア関数を指定されたパスにマウントします。ミドルウェア関数は、要求されたパスのベースがパスと一致したときに実行されます。 Express 4.x - API リファレンス
全てのリクエストに対し処理する場合の記載は以下。
app.use( (req, res, next) => { 処理 }
パスを指定する場合の記載は以下。
app.use("[パス]", (req, res, next) => { 処理 }
app.use()を使ってみる
パスの指定なし
以下のようにapp.js
を記載し実行してみる。
app.js
const express = require("express"); const portNumber = 8000; const app = express(); app.use((req, res, next) => { console.log("パス指定無し"); next(); }); app.get("/",(req,res)=>{ res.status(200).send("Hello!") }) app.listen(portNumber); console.log(`PortNumber is ${portNumber}`);
実行し、GETメソッドでlocalhost:8000
にリクエストすると以下のようコンソールに表示された。
$ node app.js PortNumber is 8000 パス指定無し
ちなみに、next();
を記載し忘れると、コンソールログは表示されるが、レスポンスはエラーとなった。
パスを指定する
app.use( "/sample" , メソッド)
を利用し、localhost:8000/sample
やlocalhost:8000/sample/abc
など、sample
以下のURLにアクセスした場合の処理を記載する。
app.js
const express = require("express"); const portNumber = 8000; const app = express(); // どこにアクセスしても実行 app.use((req, res, next) => { console.log("パス指定無し"); next(); }); // /sampleの配下にアクセスしたら実行 app.use("/sample", (req, res, next) => { console.log("/sampleへのアクセス"); next(); }); // ルートフォルダにGETしたら実行 app.get("/", (req, res) => { res.status(200).send("Hello!"); }); // /sampleフォルダにGETしたら実行 app.get("/sample", (req, res) => { res.status(200).send("Hello! /sample"); }); // /sample/abcフォルダにGETしたら実行 app.get("/sample/abc", (req, res) => { res.status(200).send("Hello! /sample/abc"); }); app.listen(portNumber); console.log(`PortNumber is ${portNumber}`);
この状態でlocalhost:8000/sample/abc
にアクセスすると、以下のようにコンソールに表示された。
$ node app.js PortNumber is 8000 パス指定無し /sampleへのアクセス
パス指定なしのapp.useと、"/sample"
を指定したapp.useの処理がどちらも実行されている。また、順序はソースコードで呼び出した順になっている。
ミドルウェアを外部呼出し場合
以下の記事で関数を外部から呼び出したように、ミドルウェア関数も呼び出すことができる。
おわりに
今回はアプリケーション・レベルのミドルウェアを作成、利用することができた。