credits: https://hackmd.io/@Heidi-Liu/javascript-websocket
我們使用 Restful API (HTTPS)時,與server建立連線,這種連線是一次性的。
當client滿足需求時(server發送了200 OK的response),這段通訊隨即宣告結束。
WebSocket 是 HTML5 提供的一種網路傳輸協定,是瀏覽器(Client)與伺服器(Server)交換資料的方式之一。
與我們較為熟知的 HTTP 或 HTTPS 協定,同樣位於 OSI 模型的應用層,且基於傳輸層的 TCP 協定。其最大不同在於,WebSocket 協定只需連線一次,就能保持雙向溝通,不需重複發送 Request,因此回應更即時,進而提升 Web 通訊速度。
重點是:Server也能主動發送request!
實際範例:訊息推播、即時聊天室、共同編輯等功能
一般的Websocket 請求網址:
ws://example.com
加上TLS/SSL:
wss://example.com
我們先建立一個websocket server:
# 建立一個專案
npm init -y
# 下載所需要的套件
npm install express
npm install ws
建立一個server.js
的檔案,將下列程式碼貼上去:
// import library
const express = require('express')
const ServerSocket = require('ws').Server // 引用 Server
// 指定一個 port
const PORT = 8000
// 建立 express 物件並用來監聽 8000 port
const app = express();
const server = app.listen(PORT, () =>
console.log(`[Server] Listening on http://localhost:${PORT}`)
);
// 建立實體,透過 ServerSocket 開啟 WebSocket 的服務
const wss = new ServerSocket({ server })
// Connection opened
wss.on('connection', ws => {
console.log('[Client connected]')
// Connection closed
ws.on('close', () => {
console.log('Close connected')
})
})
在package.json
底下做以下修改:
"scripts": {
"server": "node server.js"
},
(如果要加nodemon 可以自己加)
npm install --save-dev nodemon
"scripts": {
"server": "nodemon server.js"
},
在terminal打上 npm run server
就跑起來了!
建好server之後,我們來實作client建立連線:
.
|-- server.js
|-- public
|-- index.html
|-- index.js
In index.html
:
<html>
<head>
</head>
<body>
<!-- Connect or Disconnect WebSocket Server -->
<button id="connect">Connect</button>
<button id="disconnect">Disconnect</button>
<!-- Send Message to Server -->
<div>
Message: <input type="text" id="sendMsg" ><button id="sendBtn">Send</button>
</div>
<!-- Import index.js after UI rendered -->
<script src='./index.js'></script>
</body>
</html>
In index.js
:
var ws
// 監聽 click 事件
document.querySelector('#connect')?.addEventListener('click', (e) => {
console.log('[click connect]')
connect()
})
document.querySelector('#disconnect')?.addEventListener('click', (e) => {
console.log('[click disconnect]')
disconnect()
})
document.querySelector('#sendBtn')?.addEventListener('click', (e) => {
const msg = document.querySelector('#sendMsg')
sendMessage(msg?.value)
})
function connect() {
// Create WebSocket connection
ws = new WebSocket('ws://localhost:8000')
// 在開啟連線時執行
ws.onopen = () => console.log('[open connection]')
}
function disconnect() {
ws.close()
// 在關閉連線時執行
ws.onclose = () => console.log('[close connection]')
}
Serve them on the server:
In server.js
:
app.use(express.static("public"));
Server 端分別能使用 send
發送訊息,以及透過監聽 message
事件接收來自 Client 的訊息:
// Connection opened
wss.on('connection', ws => {
console.log('[Client connected]')
// Listen for messages from client
ws.on('message', data => {
console.log('[Message from client]: ', data)
// Send message to client
ws.send('[Get message from server]')
})
// ...
})
同樣的,Client 端也能使用 send
送出訊息,以及透過 onmessage
接收 Server 端的訊息:
// 監聽 click 事件
document.querySelector('#sendBtn')?.addEventListener('click', (e) => {
const msg = document.querySelector('#sendMsg')
sendMessage(msg?.value)
})
// Listen for messages from Server
function sendMessage(msg) {
// Send messages to Server
ws.send(msg)
// Listen for messages from Server
ws.onmessage = event => console.log('[send message]', event)
}
先前提到 WebSocket 常應用於即時聊天室等功能,也就是實現 Server 同時與多個 Client 連線。那該如何在 ClientA 傳送訊息給 Server 的同時,讓 ClientB 也接收到來自 Server 回傳的訊息呢?
這時就要仰賴「廣播功能」,首先透過 ws
提供的方法 clients
取得目前所有連線中的 Clients 資訊,再使用 forEach 迴圈送出訊息給每個 Client:
// Connection opened
wss.on('connection', ws => {
console.log('[Client connected]')
// Listen for messages from client
ws.on('message', data => {
console.log('[Message from client]: ', data)
// Get clients who connected
let clients = wss.clients
// Use loop for sending messages to each client
clients.forEach(client => {
client.send('[Broadcast][Get message from server]')
})
})
// ...
})
如何區分各個 Client?
ws.id = req.headers['sec-websocket-key'].substring(0, 8)
ws.send(`[Client ${ws.id} is connected!]`)
或是用sequence number 也可以,只要不重複的都是OK的。
最後的程式碼如下:
server.js
const express = require('express')
const ServerSocket = require('ws').Server // 引用 Server
const PORT = 8000
// 建立 express 物件並用來監聽 8000 port
const app = express();
app.use(express.static("public"));
const server = app.listen(PORT, () =>
console.log(`[Server] Listening on http://localhost:${PORT}`)
);
// 建立實體,透過 ServerSocket 開啟 WebSocket 的服務
const wss = new ServerSocket({ server })
// Connection opened
wss.on('connection', (ws, req) => {
ws.id = req.headers['sec-websocket-key'].substring(0, 8)
ws.send(`[Client ${ws.id} is connected!]`)
// Listen for messages from client
ws.on('message', data => {
console.log('[Message from client] data: ', data.toString())
// Get clients who has connected
let clients = wss.clients
// Use loop for sending messages to each client
clients.forEach(client => {
client.send(`${ws.id}: ` + data)
})
})
// Connection closed
ws.on('close', () => {
console.log('[Close connected]')
})
})
public/index.js
var ws
// 監聽 click 事件
document.querySelector('#connect')?.addEventListener('click', (e) => {
connect()
})
document.querySelector('#disconnect')?.addEventListener('click', (e) => {
disconnect()
})
document.querySelector('#sendBtn')?.addEventListener('click', (e) => {
const msg = document.querySelector('#sendMsg')
sendMessage(msg?.value)
})
function connect() {
// Create WebSocket connection
ws = new WebSocket('ws://localhost:8000')
// 在開啟連線時執行
ws.onopen = () => {
console.log('[open connection]')
// Listen for messages from Server
ws.onmessage = event => {
console.log(`[Message from server]:\n %c${event.data}` , 'color: blue')
}
}
}
function sendMessage(msg) {
// Send messages to Server
ws.send(msg)
}
function disconnect() {
ws.close()
// 在關閉連線時執行
ws.onclose = () => console.log('[close connection]')
}
Graphql 是一個 query language,具體來說,使用者會根據自己的需求篩選出適合的資料,而使用的就是query language。
回想axios+database的作法:假設使用者要尋找database的其中一個使用者,會怎麼做呢?
與Restful API的主要差異:
基礎格式:
type Query {
me: User
}
type User {
id: ID
name: String
}
Query 方式:
{
me {
name
}
}
回傳結果:
{
"me": {
"name": "Luke Skywalker"
}
}
我們使用 ApolloGraphQL 來實作。
官網:https://www.apollographql.com/
Create a new project
mkdir graphql-server-example
cd graphql-server-example
npm init --yes && npm pkg set type="module"
npm install @apollo/server graphql
touch index.js
In package.json
:
{
// ...etc.
"type": "module",
"scripts": {
"start": "nodemon index.js"
}
// other dependencies
}
定義 Schema
Every GraphQL server (including Apollo Server) uses a schema to define the structure of data that clients can query.
In index.js
:
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
// A schema is a collection of type definitions (hence "typeDefs")
// that together define the "shape" of queries that are executed against
// your data.
const typeDefs = `#graphql
# Comments in GraphQL strings (such as this one) start with the hash (#) symbol.
# This "Book" type defines the queryable fields for every book in our data source.
type Book {
title: String
author: String
}
# The "Query" type is special: it lists all of the available queries that
# clients can execute, along with the return type for each. In this
# case, the "books" query returns an array of zero or more Books (defined above).
type Query {
books: [Book]
}
`;
創建一個psuedo data:
const books = [
{
title: 'The Awakening',
author: 'Kate Chopin',
},
{
title: 'City of Glass',
author: 'Paul Auster',
},
];
定義 Resolver:
Resolvers tell Apollo Server how to fetch the data associated with a particular type.
const resolvers = {
Query: {
books: () => books,
},
};
最後,創建一個instance來啟動graphql server:
// The ApolloServer constructor requires two parameters: your schema
// definition and your set of resolvers.
const server = new ApolloServer({
typeDefs,
resolvers,
});
// Passing an ApolloServer instance to the `startStandaloneServer` function:
// 1. creates an Express app
// 2. installs your ApolloServer instance as middleware
// 3. prepares your app to handle incoming requests
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
});
console.log(`🚀 Server ready at: ${url}`);
執行 npm start
,就可以看到graphql server啟動了!
打開http://localhost:4000
,就能進到 Apollo sandbox。
如何使用 Apollo sandbox:
https://www.apollographql.com/docs/apollo-server/getting-started/#step-8-execute-your-first-query
CourseMap
新版:https://hackmd.io/zpak6bIvQtaHMJjTVjgAsg
產學部實習地圖
系學會網站 (目前本地跑不動)
大家有甚麼想法,可以打在下面!!
coursemap:
實習地圖:
系學會網站:
(HTTPS 也可以,用SSH需要設定key,請參照 Generate a new SSH key)
cd your-target-directory
# git clone [put the copied url here]
git clone https://github.com/NTUEEInfoDep/NTUEECourseMap.git
cd NTUEECourseMap
# yarn 的速度較 npm 快不少,因此使用 yarn 安裝跟執行
# 使用 yarn
yarn install
# 啟動 coursemap (下列兩種都可以)
yarn run dev
# 或
yarn start
接著,前往 http://localhost:8000/ ,看到畫面就成功囉!
目前還有一些設定沒有完成,之後有弄好再更新
(demo 連結在上方)