Koa2 實作簡易CURD
簡介(koa官網)
koa2 是一個基於 NodeJs 實現的一個新的 web 框架,這裡說的 koa 是指 koa2 而不是第一版的 koa,第一版使用如: function *()、yield、generator+co.js,程式碼整體看起來不夠美觀。
官網上寫著 koa2 是由 express 原班人馬打造,這個框架設計的特點優雅、簡潔、表達力強、自由度高。跟 express 相比是一個更輕量的 框架,它所有功能都通過 Middleware 實現,而 koa2 使用最熱門的 async/await,幫你處理異步。
本篇預計分兩個部分,第一部分介紹 koa 安裝、路由、Middleware 的使用,所有的套件都以 yarn 安裝,文章中所有 yarn 指令都可改為 npm 執行(yarn 跟 npm 指令對照表),第二部分則寫一個簡易 CRUD 的 RESTful API。
一 環境安裝
1. 安裝 Node.js
由於 koa2 可以使用 async/await,所以安裝 Node.js 至少 v7.6.0 以後的版本,若安裝 ES2015 及其他可支持 async 方法,可能會有不支持的問題,需要調整相關程式;本篇不包含 Node.js 基礎教學,各系統平台 Node.js 安裝教學請自行 google。
2. 初始化 package.json
類似於 npm init ,配置package.json
yarn init
3. 安裝 koa2
yarn add koa
官網上給的範例:
const Koa = require('koa');
const app = new Koa();
app.use(async function(ctx) {
ctx.body = 'Hello Koa2';
});
app.listen(3200);
在 CLI 上輸入 node app.js 執行,並在瀏覽器輸入網址 http://localhost:3200/
4. 熱重啟
為了避免每次修改程式都要關閉,再重新執行,建議安裝 nodemon 套件,可以不用一直重複關閉、執行動作。另外每次執行都要輸入 node app.js 這樣不是很直覺的方式,只需修改 package.json 配置,之後即可使用 yarn start 搭配 nodemon 套件。
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon app.js"
},
5. 洋蔥式概念
官網上提到 koa2 核心可以想成一顆洋蔥, koa2 的 Middleware 執行時跟洋蔥類似,但並不是一層一層的方式執行,而是以 next 作為分隔點,先執行本層 next 前的流程,接著跳到下一層 Middleware 執行完成後,再執行本層 next 後的流程。要使用 Middleware 以 use 方法在所需的地方使用,use 這個方法支持異步函數(async),建議使用 async/await,可以解決頻繁 callback 問題。
const Koa = require('koa');
const app = new Koa();
app.use(async(ctx, next) => {
console.log(1);
await next();
console.log(2);
});
app.use(async(ctx, next) => {
console.log(3);
await next();
console.log(4);
});
app.use(async(ctx, next) => {
console.log(5);
await next();
console.log(6);
ctx.body = 'Hello Koa2';
});
app.listen(3200);
// log print 1, 3, 5, 6, 4, 2
6. 建立 Router
在 koa 中路由仍以 Middleware 方式做路由設定,這裡我使用 koa-router。使用時,有固定的位置,可在路由前面加個前綴,網址輸入 http://localhost:3200/api ,才能取得相對應的資訊。
yarn add koa-router
const Koa = require('koa');
const Router = require('koa-router');
const app = new Koa();
const router = new Router({
prefix: '/api'
});
router.get('/', async(ctx) => {
ctx.body = 'Hello Koa2';
});
router.get('/about', async(ctx) => {
ctx.body = 'About Me';
})
app.use(router.routes());
app.listen(3200);
7. GET 及 POST
當需要獲取網址列上帶的參數,只需使用 ctx.query.[params],即可以 GET 方式取得網址列相關參數
const Koa = require('koa');
const Router = require('koa-router');
const app = new Koa();
const router = new Router({
prefix: '/api'
});
router.get('/', async(ctx) => {
ctx.body = 'Hello Koa2';
});
router.get('/about', async(ctx) => {
ctx.body = 'About Me';
});
router.get('/user', async(ctx) => {
let userFirstName = ctx.query.firstName;
let userLastName = ctx.query.lastName
ctx.body = `User Info - ${userLastName} ${userFirstName}`;
});
app.use(router.routes());
app.listen(3200);
若要解析頁面 form 表單所送出的資料,可安裝koa-body 解析 POST 送出的 request 內容。
yarn add koa-body
const Koa = require('koa');
const Router = require('koa-router');
const koaBody = require('koa-body');
const app = new Koa();
const router = new Router({
prefix: '/api'
});
app.use(koaBody());
router.get('/login', async(ctx) => {
ctx.body = `
<form method="POST" action="/api/login">
<label>UserName</label>
<input name="usr" /><br/>
<button type="submit">submit</button>
</form>
`;
});
router.post('/login', async(ctx) => {
console.log(ctx.request.body);
let reqInfo = ctx.request.body.usr;
ctx.body = `<p>Welocome - ${reqInfo}</p>`;
});
app.use(router.routes());
app.listen(3200);
8. 建立 log Middleware
加入一個中間件進行處理-記錄「使用者請求的反應時間」,在request 以及 response 時擷取資訊,透過 async/await 可以輕易做到。
yarn add koa-logger
const Koa = require('koa');
const logger = require('koa-logger');
const Router = require('koa-router');
const app = new Koa();
const router = new Router({
prefix: '/api'
});
app.use(logger());
app.use(async(ctx, next) => {
ctx.body = 'Hello Koa2~~~';
let startTime = new Date();
await next();
let endTime = new Date() - startTime;
console.log(`${ctx.method} ${ctx.url} - ${endTime}ms`);
});
app.listen(3200);
二 RESTful 實作
RESTful 預計五支,註冊、登入、查詢、修改跟刪除,這版沒有使用資料庫,所以宣告兩個變數,一個當作資料的 ID,另一個以 Object 的形式作為暫存資料。註冊跟登入對應 POST,查詢對應 GET,修改對應 PUT,刪除則是 DELETE。這篇一開始就沒有考慮用靜態文件發送請求,可以透過 Postman 來發送,即可取得相對應得回傳值,需要完整code的,可到我的 github 下載。
我自己必沒有用過 express,所以也無法比較兩者的差異,但我覺得 Koa 原生支援 async/await 讓整體程式碼比較直觀,維護上可以不用花太多得時間理解程式,這一點讓我覺得大勝 express,畢竟 express 若要使用 async/await 需要額外安裝套件,不是原生支援,效能上多少會影響。
. 整體架構
├── app.js
├── controllers
│ └── users
│ └── UserController.js
├── node_modules
├── router
│ └── router.js
├── package.json
└──.gitignore
// router.js
const Router = require('koa-router');
const userCtrl = require('../controllers/users/UserController');
const router = new Router({
prefix: '/api'
});
router
.post('/user/reg', userCtrl.addUserData)
.post('/user/login', userCtrl.login)
.get('/user/:id', userCtrl.getUserData)
.put('/user/:id/:method', userCtrl.modifiedUserData)
.delete('/user/:id/:method', userCtrl.deleteUserDelData);
module.exports = router;
// UserController.js
let indexId = 0;
let userDataList = [];
class UserController {
// reg
async addUserData(ctx, next) {
const { name } = ctx.request.body;
const { email } = ctx.request.body;
const { pw } = ctx.request.body;
if (name && email && pw) {
let userName = name;
let userMail = email;
let userPw = pw;
userDataList.push({
userId: ++indexId,
userName: userName,
userMail: userMail,
userPw: userPw,
createTime: new Date(),
});
ctx.status = 201;
ctx.body = {
stat: 'ok',
result: indexId
};
} else {
ctx.status = 400;
}
}
// login check
async login(ctx, next) {
const { email } = ctx.request.body;
const { pw } = ctx.request.body;
if (email && pw) {
let userMail = email;
let userPw = pw;
const newUserData = userDataList.find((item) => (item.userMail === userMail && item.userPw === userPw));
if (newUserData) {
ctx.body = {
stat: "ok",
result: newUserData
};
} else {
ctx.status = 404;
}
} else {
ctx.status = 404;
}
}
// get User Data
async getUserData(ctx, next) {
const userId = parseInt(ctx.params.id);
if (userId) {
const newUserDataList = userDataList.find((item) => item.userId === userId );
if (newUserDataList) {
ctx.body = {
stat: 'ok',
result: newUserDataList
};
} else {
ctx.status = 404;
}
} else {
ctx.status = 404;
}
}
// modified User Data
async modifiedUserData(ctx, next) {
const userId = parseInt(ctx.params.id);
const method = ctx.params.method;
if (userId && method === 'edit') {
const { name } = ctx.request.body;
const { email } = ctx.request.body;
const { interest } = ctx.request.body;
if (name && email && interest) {
let userName = name;
let userMail = email;
let userInterest = interest;
const newUserDataList = userDataList.find((item) => item.userId === userId);
if (newUserDataList) {
newUserDataList.userName = userName;
newUserDataList.userMail = userMail;
newUserDataList.userInterest = userInterest;
newUserDataList.modifiedTime = new Date();
ctx.body = {
stat: 'ok',
result: newUserDataList
};
} else {
ctx.status = 404;
}
} else {
ctx.status = 404;
}
} else {
ctx.status = 404;
}
}
// delete User Data
async deleteUserDelData(ctx, next) {
const userId = parseInt(ctx.params.id);
const method = ctx.params.method;
if (userId && method === 'delete') {
const newUserDataList = userDataList.find((item) => item.userId === userId);
if (newUserDataList) {
userDataList = userDataList.filter((item) => item.userId !== userId );
ctx.status = 204;
ctx.body = {
stat: 'ok',
result: userDataList
};
} else {
ctx.status = 404;
}
} else {
ctx.status = 404;
}
}
}
module.exports = new UserController();