こんにちは。KOUKIです。
Go言語とJavaScriptでチャットアプリの作成方法を記事にしてきました。
ここでは、ソースコードをまとめて記載しておきます。
<目次>
チャット記事まとめ
ダウンロードするもの
「joewalnes/reconnecting-websocket」のreconnecting-websocket.min.jsを取得してください。
ワークスペース
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
chat-websockets ├── cmd │ └── web │ ├── main.go │ └── routes.go ├── domain │ └── connect.go ├── go.mod ├── html │ └── home.jet ├── internal │ └── handlers │ └── handlers.go └── static ├── reconnecting-websocket.min.js ├── scripts.js └── style.css |
CMD/WEB
main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
package main import ( "chat-websockets/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") _ = 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 |
// routes package main import ( "chat-websockets/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)) fileServer := http.FileServer(http.Dir("./static/")) mux.Get("/static/", http.StripPrefix("/static", fileServer)) return mux } |
domain
connect.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 |
// connect.go package domain import "github.com/gorilla/websocket" // WebSocketsからの返却用データの構造体 type WsJsonResponse struct { Action string `json:"action"` Message string `json:"message"` ConnectedUsers []string `json:"connected_users"` } // WebSocketsコネクション情報を格納 type WebScoketConnection struct { *websocket.Conn } // WebSockets送信データを格納 type WsPayload struct { Action string `json:"action"` Message string `json:"message"` Username string `json:"username"` Conn WebScoketConnection `json:"-"` } |
html
hoge.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 30 31 32 33 34 35 36 37 38 39 40 |
<!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>Cheat App</title> <link rel="stylesheet" href="/static/style.css" /> </head> <body> <div class="chat-container"> <div class="chat-header"> <label for="username">YOUR NAME</label> <input type="text" id="username" class="username" autocomplete="off" placeholder=":) selfnote"> </div> <div class="chat-body"> <ul id="message-list"> </ul> <div class="send-area"> <input type="text" id="message" class="message" autocomplete="off" placeholder="message..."> <button id="submit" class="submit" onclick="sendMessage()"> <i class="far fa-paper-plane"></i> </button> </div> </div> </div> <div class="oneline-user-container"> <ul id="online-users"> </ul> </div> <script src="/static/reconnecting-websocket.min.js"></script> <script src="/static/scripts.js"></script> </body> </html> |
internal/handlers
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 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 |
// handlers.go package handlers import ( "chat-websockets/domain" "fmt" "log" "net/http" "sort" "github.com/CloudyKit/jet/v6" "github.com/gorilla/websocket" ) var ( // ペイロードチャネルを作成 wsChan = make(chan domain.WsPayload) // コネクションマップを作成 // keyはコネクション情報, valueにはユーザー名を入れる clients = make(map[domain.WebScoketConnection]string) ) var views = jet.NewSet( jet.NewOSFileSystemLoader("./html"), jet.InDevelopmentMode(), ) // wsコネクションの基本設定 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サーバーコネクションをWebSocketsプロトコルにアップグレード ws, err := upgradeConnection.Upgrade(w, r, nil) if err != nil { log.Println(err) } log.Println("OK Client Connecting") var response domain.WsJsonResponse response.Message = `<li>Connected to server</li>` // コネクション情報を格納 conn := domain.WebScoketConnection{Conn: ws} // ブラウザが読み込まれた時に一度だけ呼び出されるのでユーザ名なし clients[conn] = "" err = ws.WriteJSON(response) if err != nil { log.Println(err) } go ListenForWs(&conn) } func ListenForWs(conn *domain.WebScoketConnection) { defer func() { // goroutineでパニックが起きた時に起動する if r := recover(); r != nil { log.Println("Error", fmt.Sprintf("%v", r)) } }() var payload domain.WsPayload for { // payloadを読み込む err := conn.ReadJSON(&payload) if err != nil { // payloadがない場合、何もしない } else { // payloadがある場合 payload.Conn = *conn // チャネルにpayloadを渡す wsChan <- payload } } } func ListenToWsChannel() { var response domain.WsJsonResponse for { // メッセージが入るまで、ここでブロック e := <-wsChan switch e.Action { case "username": // ここで、コネクションのユーザー名を格納 clients[e.Conn] = e.Username users := getUserList() response.Action = "list_users" response.ConnectedUsers = users broadcastToAll(response) case "left": fmt.Println("####left") response.Action = "list_users" // clientsからユーザーを削除 delete(clients, e.Conn) users := getUserList() response.ConnectedUsers = users broadcastToAll(response) case "broadcast": response.Action = "broadcast" response.Message = fmt.Sprintf( "<li class='replace'><strong>%s</strong>: %s</li>", e.Username, e.Message) broadcastToAll(response) } } } func getUserList() []string { var clientList []string for _, client := range clients { if client != "" { clientList = append(clientList, client) } } sort.Strings(clientList) return clientList } // 全てのユーザーにメッセージを返す func broadcastToAll(response domain.WsJsonResponse) { // clientsには全ユーザーのコネクション情報が格納されている for client := range clients { err := client.WriteJSON(response) if err != nil { log.Println("websockets err") _ = client.Close() // clients Mapからclientの情報を消す delete(clients, client) } } } 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 } |
static
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 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 |
// scripts.js // WebSocketsオブジェクトの作成 let scoket = null; // メッセージリストを宣言 let messageList = document.getElementById("message-list") // ページから離脱時に発生 window.onbeforeunload = function() { console.log("User Leaving") let jsonData = {} jsonData["action"] = "left" socket.send(JSON.stringify(jsonData)) } // 最初のHTMLの読み込みと解析が完了したとき、スタイルシート、 // 画像、サブフレームの読み込みが完了するのを待たずに発生。 document.addEventListener("DOMContentLoaded", function(){ // WebScoektオブジェクトの作成 // https://developer.mozilla.org/ja/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications socket = new ReconnectingWebSocket( "ws://127.0.0.1:8080/ws", null, { debug: true, reconnectInterval: 3000 // 3s後に再接続 }) // 接続を確立 // https://developer.mozilla.org/ja/docs/Web/API/WebSocket/onopen socket.onopen = () => { console.log("Successfully connected") } // 接続がCLOSEDに変わった時に呼ばれる // https://developer.mozilla.org/ja/docs/Web/API/WebSocket/onclose socket.onclose = () => { console.log("connection closed") } // エラーが発生した時に呼び出される // https://developer.mozilla.org/ja/docs/Web/API/WebSocket/onerror socket.onerror = error => { console.log("there was an error") } // サーバーからメッセージが届いたときに呼び出される // https://developer.mozilla.org/ja/docs/Web/API/WebSocket/onmessage socket.onmessage = msg => { let data = JSON.parse(msg.data) console.log({data}) console.log("Action is", data.action) switch (data.action) { case "list_users": let ul = document.getElementById("online-users") while (ul.firstChild) ul.removeChild(ul.firstChild) if (data.connected_users.length > 0) { data.connected_users.forEach(function(item){ let li = document.createElement("li") li.appendChild(document.createTextNode(item)) ul.appendChild(li) }) } break case "broadcast": let message = data.message let username = document.getElementById("username").value // メッセージが自分のものかチェック // classをmeかotherに書き換え if (message.indexOf(username) > 0) { message = message.replace("replace", "me") } else { message = message.replace("replace", "other") } messageList.innerHTML = messageList.innerHTML + message break } } let userInput = document.getElementById("username") userInput.addEventListener("change", function() { let jsonData = {} // Action Nameをusernameにする jsonData["action"] = "username" jsonData["username"] = this.value; // user名を送信 socket.send(JSON.stringify(jsonData)) }) document.getElementById("message").addEventListener("keydown", function(event) { if (event.code === "Enter") { if (!socket) { console.log("no connection") return false } // HTML要素既存の動きやイベント伝搬をキャンセル event.preventDefault() event.stopPropagation() sendMessage() } }) }) function sendMessage() { console.log("Send Message...") let jsonData = {} jsonData["action"] = "broadcast" jsonData["username"] = document.getElementById("username").value jsonData["message"] = document.getElementById("message").value socket.send(JSON.stringify(jsonData)) document.getElementById("message").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 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 |
/* フォント */ @import url("https://fonts.googleapis.com/css2?family=Indie+Flower&display=swap"); * { /* ボックスサイズを算出 paddingとborderをwidthとheightに含める */ box-sizing: border-box; } body { background-color: #e6f7ec; display: flex; align-items: center; justify-content: center; height: 100vh; overflow: hidden; margin: 0; padding: 0; } .chat-container { width: 40%; height: 98%; } .chat-header { background: #8688f5; box-shadow: 0px 2px 10px rgba(122, 121, 119, 0.966); display: flex; flex-direction: column; text-align: center; justify-content: center; padding: 30px; height: 15%; border-radius: 10px; } .chat-header label { margin-bottom: 10px; color: #fff; font-size: 1.3rem; } .chat-header input { border: none; font-size: 1.3rem; font-family: inherit; padding: 5px; letter-spacing: 1px; text-align: center; } .chat-header input:focus { outline: none; } .chat-body { background-color: ivory; box-shadow: 0px 2px 10px rgba(122, 121, 119, 0.966); height: 75%; border-radius: 10px; } .chat-body ul { font-family: "Indie Flower", cursive; font-size: 1.1rem; list-style: none; padding: 2px; height: 90%; display: flex; flex-direction: column; overflow: scroll; } .chat-body ul li { width: 40%; padding: 15px; border-radius: 240px 15px 100px 15px / 15px 200px 15px 185px; border: none; } .chat-body ul li.me { margin: 10px auto 10px 10px; background-color: orange; color: #fff; } .chat-body ul li.other { /* 右寄せ */ margin: 10px 10px 10px auto; background-color: powderblue; text-align: right; } .send-area { position: relative; } .message { position: absolute; top: 0; left: 10px; width: 80%; height: 30px; margin-bottom: 10px; letter-spacing: 1px; } .message:focus { outline: none; } .submit { position: absolute; top: 0; right: 25px; width: 10%; font-weight: bold; height: 30px; border: none; border-radius: 50%; box-shadow: 1px 1px 10px rgba(0, 0, 0, 0.4); color: #fff; background: rgb(248, 121, 163); } .submit:focus { outline: none; } .oneline-user-container { height: 100vh; width: 200px; } .oneline-user-container ul { font-family: "Indie Flower", cursive; list-style: none; color: #111111; } .oneline-user-container li { margin-bottom: 20px; padding: 10px; border: 30px; background: ivory; border-radius: 10px; } @media (max-width: 1200px) { .chat-header label { margin-bottom: 10px; font-size: 1rem; } .chat-header input { padding: 8px; } } @media (max-width: 560px) { .chat-header label { font-size: 0.9rem; } .chat-header input { padding: 3px; } } |
コメントを残す
コメントを投稿するにはログインしてください。