こんにちは、 KOUKIです。
本記事では、掲示板アプリ開発のプロセスをハンズオン形式で記事にしています!
本記事で実装したソースコードを乗せておきます。
<目次>
前回
まとめページ
ワークスペース
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
$ tree . ├── Makefile ├── cmd │ └── web │ ├── main.go │ └── routes.go ├── domain │ └── connect.go ├── go.mod ├── html │ └── home.jet ├── internal │ └── handlers │ └── handlers.go └── static ├── reconnecting-websocket.min.js ※1 ├── scripts.js └── style.css |
※1 ・・・ joewalnes/reconnecting-websocketからダウンロードする
ソースコード
Makefile
1 2 3 4 |
.PHONY: run run: go run cmd/web/*.go |
main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// main.go package main import ( "bulletin-board/internal/handlers" "log" "net/http" ) func main() { mux := routes() log.Println("Starting channel listener") go handlers.ListenToWsChannel() log.Println("Starting web server on port 8080") log.Fatal(http.ListenAndServe(":8080", mux)) } |
routes.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// routes.go package main import ( "bulletin-board/internal/handlers" "net/http" "github.com/bmizerany/pat" ) func routes() http.Handler { mux := pat.New() mux.Get("/", http.HandlerFunc(handlers.Home)) mux.Get("/ws", http.HandlerFunc(handlers.WsEndpoint)) // staticファイルの配信設定 fileServer := http.FileServer(http.Dir("./static/")) mux.Get("/static/", http.StripPrefix("/static", fileServer)) return mux } |
connect.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// connect.go package domain import "github.com/gorilla/websocket" type WsJsonResponse struct { Action string `json:"action"` Post string `json:"post"` } // WebSocketコネクション情報格納 type WebSocketConnection struct { *websocket.Conn } // WebSockets受信データ type WsPayload struct { Action string `json:"action"` Post string `json:"post"` Username string `json:"username"` Conn WebSocketConnection `json:"-"` } |
home.jet
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<!-- home.jet --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css" integrity="sha512-1PKOgIY59xJ8Co8+NE6FZ+LOAZKjy+KY8iq0G4B3CyeY6wYHN3yt9PW0XpSriVlkMXe40PTKnXrLnZ9+fkDaog==" crossorigin="anonymous" /> <title>Bulletin Board App</title> <link rel="stylesheet" href="/static/style.css" /> </head> <body> <div id="post-container" class="post-container"> </div> <div class="form-area"> <input type="text" class="username" id="username" autocomplete="off" placeholder="Your Name"> <textarea name="post" id="post" cols="30" rows="10" autocomplete="off"></textarea> <button id="submit" class="submit" onclick="sendPost()">POST</button> </div> <script src="/static/reconnecting-websocket.min.js"></script> <script src="/static/scripts.js"></script> </body> </html> |
handlers.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
// handlers.go package handlers import ( "bulletin-board/domain" "fmt" "log" "net/http" "github.com/CloudyKit/jet/v6" "github.com/gorilla/websocket" ) var ( // payloadのチャネルを作成 wsChan = make(chan domain.WsPayload) // key: コネクション情報 value: ユーザー名のマップ clients = make(map[domain.WebSocketConnection]string) ) // Jetをセットアップ var views = jet.NewSet( // htmlフォルダを読み込む jet.NewOSFileSystemLoader("./html"), jet.InDevelopmentMode(), ) // WebSocketsの基本設定 // https://pkg.go.dev/github.com/gorilla/websocket#hdr-Overview var upgradeConnection = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, CheckOrigin: func(r *http.Request) bool { return true }, } // WebSocketsのエンドポイント func WsEndpoint(w http.ResponseWriter, r *http.Request) { // HTTP requestから*Connを取得 wsConn, err := upgradeConnection.Upgrade(w, r, nil) if err != nil { log.Println(err) } log.Println("WS Connecting....") // コネクション情報を格納 conn := domain.WebSocketConnection{Conn: wsConn} // ページを開いたときは、ユーザー名を持っていないので、空 clients[conn] = "" // goroutineでListenForWsを起動 go ListenForWs(&conn) } func ListenForWs(wsConn *domain.WebSocketConnection) { defer func() { // パニックが起きた時に任意の処理ができる if r := recover(); r != nil { log.Println("Error", fmt.Sprintf("%v", r)) } }() var payload domain.WsPayload for { err := wsConn.ReadJSON(&payload) if err != nil { // no action } else { payload.Conn = *wsConn wsChan <- payload } } } func broadcastToAllUser(response domain.WsJsonResponse) { // clientsには接続済みの全てのユーザー情報が格納されている for client := range clients { // 投稿をクライアントに返す err := client.WriteJSON(response) if err != nil { log.Println("WebSockets Error") // WebSocketsコネクションを破棄 _ = client.Close() // mapから削除 delete(clients, client) } } } func ListenToWsChannel() { var response domain.WsJsonResponse for { // wsChanにデータが入るまでブロックするので、無限ループでOK payload := <-wsChan switch payload.Action { case "username": clients[payload.Conn] = payload.Username case "left": // clients変数からコネクションを削除 delete(clients, payload.Conn) case "broadcast": response.Action = "broadcast" response.Post = fmt.Sprintf(` <div class="post"> <p>%s</p> <small class="name">%s</small> </div>`, payload.Post, payload.Username) broadcastToAllUser(response) } } } func Home(w http.ResponseWriter, r *http.Request) { err := renderPage(w, "home.jet", nil) if err != nil { log.Println(err) } } func renderPage(w http.ResponseWriter, tmpl string, data jet.VarMap) error { view, err := views.GetTemplate(tmpl) if err != nil { log.Println(err) return err } err = view.Execute(w, data, nil) if err != nil { log.Println(err) return err } return nil } |
scripts.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
// scripts.js let socket = null; // ページから離脱時に発生 window.onbeforeunload = function() { console.log("User Leaving") let jsonData = {} jsonData["action"] = "left" socket.send(JSON.stringify(jsonData)) } document.addEventListener('DOMContentLoaded', function(){ // WebSocketsオブジェクトの作成 socket = new ReconnectingWebSocket( "ws://127.0.0.1:8080/ws", null, { debug: true, reconnectInterval: 3000 // 3s後に再接続 }) // 接続を確立時 socket.onopen = () => { console.log("Successfully connectted!") } // メッセージ受信時 socket.onmessage = msg => { // JSONデータをJavaScriptオブジェクトへパース let postData = JSON.parse(msg.data) let postContainer = document.getElementById("post-container") switch (postData.action) { // actionごとに追加していく case "broadcast": let post = postData.post postContainer.innerHTML = postContainer.innerHTML + post break } } let userInput = document.getElementById("username") userInput.addEventListener("change", function() { let jsonData = {} jsonData["action"] = "username" jsonData["username"] = this.value socket.send(JSON.stringify(jsonData)) }) document.getElementById("post").addEventListener("keydown", function(event){ if (event.code === "Enter") { if (!socket) { console.log("no connection") return } // イベントの伝搬やデフォルトの挙動をキャンセル event.preventDefault() event.stopPropagation() sendPost() } }) }) function sendPost() { console.log("Send Post...") let jsonData = {} jsonData["action"] = "broadcast" jsonData["username"] = document.getElementById("username").value jsonData["post"] = document.getElementById("post").value // サーバーへメッセージを送信 socket.send(JSON.stringify(jsonData)) // 入力項目を空にする document.getElementById("post").value = "" } |
style.css
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
/* style.css */ @import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap"); * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: "Roboto", sans-serif; background: url("https://images.unsplash.com/photo-1508768787810-6adc1f613514?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=e27f6661df21ed17ab5355b28af8df4e&auto=format&fit=crop&w=1350&q=80") no-repeat center center/cover; height: 100vh; overflow: hidden; position: relative; display: flex; align-items: center; justify-content: space-evenly; } body::before { content: ""; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.4); } .post-container { height: 100vh; width: 80vw; max-width: 560px; background-color: rgba(41, 39, 39, 0.6); overflow-y: scroll; z-index: 100; } .post { display: flex; flex-direction: column; justify-content: center; margin: 10px; background-color: rgba(41, 39, 39, 1); padding: 10px; } .post:nth-child(even) { align-items: flex-end; } .post p { color: #fff; } .post small { color: rgb(248, 197, 31); } .form-area { display: flex; flex-direction: column; z-index: 2; } .form-area input { display: inline-block; margin-bottom: 10px; font-size: 1.5rem; letter-spacing: 1.5px; text-align: center; border-radius: 5px; border: 0; box-shadow: 1px 2px 4px 2px rgba(247, 7, 7, 0.4); padding: 5px; width: 25vw; max-width: 500px; } .form-area input:focus { outline: none; } .form-area textarea { display: inline-block; margin-bottom: 10px; border-radius: 5px; border: 0; padding: 10px; letter-spacing: 1.3px; box-shadow: 1px 2px 4px 2px rgba(247, 7, 7, 0.4); } .form-area textarea:focus { outline: none; } .submit { border-radius: 5px; border: 0; font-size: 1rem; box-shadow: 1px 2px 4px 2px rgba(247, 7, 7, 0.4); padding: 10px; cursor: pointer; letter-spacing: 1.5px; background-color: rgba(41, 39, 39, 1); color: #fff; } .submit:focus { outline: none; } .submit:active { transform: scale(0.98); } |
コメントを残す
コメントを投稿するにはログインしてください。