跳至主要內容

基本 CRUD 應用程式

儘管使用 Socket.IO (或純粹的 WebSocket) 進行基本 CRUD 應用程式聽起來有點大材小用,但輕鬆通知所有使用者的能力真的很強大。

在本指南中,我們將建立一個基本 CRUD (代表Create/Read/Update/Delete) 應用程式,它基於令人驚豔的TodoMVC 專案

Video of the application in action

我們將涵蓋以下主題

開始吧!

安裝

程式碼可以在主要儲存庫的 examples 目錄中找到

git clone https://github.com/socketio/socket.io.git
cd socket.io/examples/basic-crud-application/

你應該會看到兩個目錄

  • server/:伺服器實作
  • angular-client/:基於Angular的用戶端實作
  • vue-client/:基於Vue的用戶端實作

執行前端

此專案是一個使用Angular CLI建立的基本 Angular 應用程式。

執行它的方式

cd angular-client
npm install
npm start

然後如果你在瀏覽器中開啟 http://localhost:4200,你應該會看到

Screenshot of the application

到目前為止,一切都好。

執行伺服器

現在讓我們專注於伺服器

cd ../server
npm install
npm start

現在你可以開啟多個分頁,而待辦事項清單應該會在它們之間神奇地同步

Video of the application in action

運作方式

伺服器結構

├── lib
│ ├── index.ts
│ ├── app.ts
│ ├── todo-management
│ │ ├── todo.handlers.ts
│ | └── todo.repository.ts
│ └── util.ts
├── package.json
├── test
│ └── todo-management
│ └── todo.tests.ts
└── tsconfig.json

讓我們詳細說明每個檔案的職責

  • index.ts:建立元件並初始化應用程式的伺服器進入點
  • app.ts:應用程式本身,用於建立 Socket.IO 伺服器,並註冊處理常式
  • todo.handlers.ts:處理 Todo 實體操作的處理常式
  • todo.repository.ts:用於從資料庫儲存/擷取 Todo 實體的儲存庫
  • util.ts:專案中使用的一些常見公用方法
  • todo.tests.ts:整合測試

初始化

首先,我們來關注 lib/app.ts 檔案中的 createApplication 方法

const io = new Server<ClientEvents, ServerEvents>(httpServer, serverOptions);

我們使用下列選項建立 Socket.IO 伺服器

{
cors: {
origin: ["http://localhost:4200"]
}
}

因此,在 http://localhost:4200 提供服務的前端應用程式允許連線。

文件

<ClientEvents, ServerEvents> 部分特定於 TypeScript 使用者。它允許明確指定伺服器與用戶端之間交換的事件,因此您可以獲得自動完成和類型檢查

Screenshot of the IDE autocompletion Screenshot of the IDE type checking

回到我們的應用程式!然後,我們透過注入應用程式元件來建立我們的處理常式

const {
createTodo,
readTodo,
updateTodo,
deleteTodo,
listTodo,
} = createTodoHandlers(components);

我們註冊它們

io.on("connection", (socket) => {
socket.on("todo:create", createTodo);
socket.on("todo:read", readTodo);
socket.on("todo:update", updateTodo);
socket.on("todo:delete", deleteTodo);
socket.on("todo:list", listTodo);
});

文件:聆聽事件

注意:事件字尾 (:create:read、...) 取代 REST API 中常見的 HTTP 動詞

  • POST /todos => todo:create
  • GET /todos/:id => todo:read
  • PUT /todos/:id => todo:update
  • ...

事件處理常式

現在,我們來關注 lib/todo-management/todo.handlers.ts 檔案中的 createTodo 處理常式

首先,我們擷取 Socket 實體

createTodo: async function (
payload: Todo,
callback: (res: Response<TodoID>) => void
) {
const socket: Socket<ClientEvents, ServerEvents> = this;
// ...
}

請注意,在此處使用箭頭函式 (createTodo: async () => {}) 無效,因為 this 不會指向 Socket 實體。

然後,我們使用出色的 joi 函式庫驗證酬載

const { error, value } = todoSchema.tailor("create").validate(payload, {
abortEarly: false, // return all errors and not just the first one
stripUnknown: true, // remove unknown attributes from the payload
});

文件:https://joi.dev/api/

如果出現驗證錯誤,我們只會呼叫確認回呼並傳回

if (error) {
return callback({
error: Errors.INVALID_PAYLOAD,
errorDetails: error.details,
});
}

我們在客戶端處理錯誤

// angular-client/src/app/store.ts

this.socket.emit("todo:create", { title, completed: false }, (res) => {
if ("error" in res) {
// handle the error
} else {
// success!
}
});

文件:Acknowledgements

如果有效負載成功符合架構,我們可以產生新的 ID 並保留實體

value.id = uuid();

try {
await todoRepository.save(value);
} catch (e) {
return callback({
error: sanitizeErrorMessage(e),
});
}

如果發生意外錯誤(例如資料庫當機),我們會使用一般錯誤訊息呼叫確認回呼(為了不公開我們應用程式的內部)。

否則,我們只要使用新的 ID 呼叫回呼

callback({
data: value.id,
});

最後(這是神奇的部分),我們通知所有其他使用者已建立

socket.broadcast.emit("todo:created", value);

文件:Broadcasting events

在客戶端,我們註冊此事件的處理常式

// angular-client/src/app/store.ts

this.socket.on("todo:created", (todo) => {
this.todos.push(mapTodo(todo));
});

然後

測試

由於我們是相當合理的開發人員,我們現在會為我們的處理常式新增幾個測試。讓我們開啟 test/todo-management/todo.tests.ts 檔案

應用程式是在 beforeEach 掛鉤中建立的

beforeEach((done) => {
const partialDone = createPartialDone(2, done);

httpServer = createServer();
todoRepository = new InMemoryTodoRepository();

createApplication(httpServer, {
todoRepository,
});

// ...
});

我們建立兩個客戶端,一個用於傳送有效負載,另一個用於接收通知

httpServer.listen(() => {
const port = (httpServer.address() as AddressInfo).port;
socket = io(`http://localhost:${port}`);
socket.on("connect", partialDone);

otherSocket = io(`http://localhost:${port}`);
otherSocket.on("connect", partialDone);
});

重要注意事項:這兩個客戶端會在 afterEach 掛鉤中明確斷開連線,因此不會妨礙程序結束。

文件:https://mocha.dev.org.tw/#hooks

我們的第一次測試(快樂路徑)相當直接

describe("create todo", () => {
it("should create a todo entity", (done) => {
const partialDone = createPartialDone(2, done);

// send the payload
socket.emit(
"todo:create",
{
title: "lorem ipsum",
completed: false,
},
async (res) => {
if ("error" in res) {
return done(new Error("should not happen"));
}
expect(res.data).to.be.a("string");

// check the entity stored in the database
const storedEntity = await todoRepository.findById(res.data);
expect(storedEntity).to.eql({
id: res.data,
title: "lorem ipsum",
completed: false,
});

partialDone();
}
);

// wait for the notification of the creation
otherSocket.on("todo:created", (todo) => {
expect(todo.id).to.be.a("string");
expect(todo.title).to.eql("lorem ipsum");
expect(todo.completed).to.eql(false);
partialDone();
});
});
});

讓我們也使用無效有效負載進行測試

describe("create todo", () => {
it("should fail with an invalid entity", (done) => {
const incompleteTodo = {
completed: "false",
description: true,
};

socket.emit("todo:create", incompleteTodo, (res) => {
if (!("error" in res)) {
return done(new Error("should not happen"));
}
expect(res.error).to.eql("invalid payload");
// check the details of the validation error
expect(res.errorDetails).to.eql([
{
message: '"title" is required',
path: ["title"],
type: "any.required",
},
]);
done();
});

// no notification should be received
otherSocket.on("todo:created", () => {
done(new Error("should not happen"));
});
});
});

你可以使用 npm test 執行完整的測試套件

Screenshot of the test results

就這樣!其他處理常式與第一個相當類似,在此不會詳細說明。

後續步驟

感謝你的閱讀!