4/26 大一部課

  • Websocket
  • graphql
  • 期末進度:分配工作

WebSocket

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!

實際範例:訊息推播、即時聊天室、共同編輯等功能

Start!!

一般的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

Graphql 是一個 query language,具體來說,使用者會根據自己的需求篩選出適合的資料,而使用的就是query language。

回想axios+database的作法:假設使用者要尋找database的其中一個使用者,會怎麼做呢?

與Restful API的主要差異:

  • Restful API 使用 HTTP協定,不失一般性假設query有多種可能,那麼最簡單的實作方式就是將整個資料一起回傳,再從前端篩選出各自需要的資料。
  • Graphql 同樣使用大眾的傳輸協定,不過透過query language,只須回傳使用者指定的資料,如此可以減少data flow,進而提升效能。

基礎格式:

type Query { me: User } type User { id: ID name: String }

Query 方式:

{ me { name } }

回傳結果:

{ "me": { "name": "Luke Skywalker" } }

Let's create a simple GraphQL server!

我們使用 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

Discussion for future works

CourseMap
新版:https://hackmd.io/zpak6bIvQtaHMJjTVjgAsg
產學部實習地圖
系學會網站 (目前本地跑不動)

大家有甚麼想法,可以打在下面!!

coursemap:

實習地圖:

系學會網站:

使用 coursemap

  1. Coursemap 的 repo clone 下來

(HTTPS 也可以,用SSH需要設定key,請參照 Generate a new SSH key)
image

cd your-target-directory

# git clone [put the copied url here]
git clone https://github.com/NTUEEInfoDep/NTUEECourseMap.git
  1. 進入coursemap 資料夾裡,安裝專案需要的套件
cd NTUEECourseMap

# yarn 的速度較 npm 快不少,因此使用 yarn 安裝跟執行
# 使用 yarn
yarn install

# 啟動 coursemap (下列兩種都可以)
yarn run dev
# 或
yarn start

接著,前往 http://localhost:8000/ ,看到畫面就成功囉!

使用系學會網站

目前還有一些設定沒有完成,之後有弄好再更新
(demo 連結在上方)