基本 CRUD 應用程式
儘管使用 Socket.IO (或純粹的 WebSocket) 進行基本 CRUD 應用程式聽起來有點大材小用,但輕鬆通知所有使用者的能力真的很強大。
在本指南中,我們將建立一個基本 CRUD (代表Create/Read/Update/Delete) 應用程式,它基於令人驚豔的TodoMVC 專案
我們將涵蓋以下主題
開始吧!
安裝
程式碼可以在主要儲存庫的 examples
目錄中找到
git clone https://github.com/socketio/socket.io.git
cd socket.io/examples/basic-crud-application/
你應該會看到兩個目錄
執行前端
此專案是一個使用Angular CLI建立的基本 Angular 應用程式。
執行它的方式
cd angular-client
npm install
npm start
然後如果你在瀏覽器中開啟 http://localhost:4200,你應該會看到
到目前為止,一切都好。
執行伺服器
現在讓我們專注於伺服器
cd ../server
npm install
npm start
現在你可以開啟多個分頁,而待辦事項清單應該會在它們之間神奇地同步
運作方式
伺服器結構
├── 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 使用者。它允許明確指定伺服器與用戶端之間交換的事件,因此您可以獲得自動完成和類型檢查
回到我們的應用程式!然後,我們透過注入應用程式元件來建立我們的處理常式
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
});
如果出現驗證錯誤,我們只會呼叫確認回呼並傳回
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!
}
});
如果有效負載成功符合架構,我們可以產生新的 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);
在客戶端,我們註冊此事件的處理常式
// 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
執行完整的測試套件
就這樣!其他處理常式與第一個相當類似,在此不會詳細說明。
後續步驟
感謝你的閱讀!