Koa2 RESTful API Node.js yarn

Koa2 實作簡易CURD

曾建淞 2019/12/18 21:39:53
2610

簡介(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();
曾建淞