<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          一杯茶的時(shí)間,上手 Koa2 + MySQL 開發(fā)

          共 30803字,需瀏覽 62分鐘

           ·

          2021-03-15 09:38



          ?

          如果您覺得我們寫得還不錯(cuò),記得 「點(diǎn)贊 + 關(guān)注 + 評(píng)論」 三連,鼓勵(lì)我們寫出更好的教程??

          ?

          憑借精巧的“洋蔥模型”和對(duì) Promise 以及 async/await 異步編程的完全支持,Koa 框架自從誕生以來就吸引了無(wú)數(shù) Node 愛好者。然而 Koa 本身只是一個(gè)簡(jiǎn)單的中間件框架,要想實(shí)現(xiàn)一個(gè)足夠復(fù)雜的 Web 應(yīng)用還需要很多周邊生態(tài)支持。這篇教程不僅會(huì)帶你梳理 Koa 的基礎(chǔ)知識(shí),還會(huì)充分地運(yùn)用和講解構(gòu)建 Web 應(yīng)用必須的組件(路由、數(shù)據(jù)庫(kù)、鑒權(quán)等),最終實(shí)現(xiàn)一個(gè)較為完善的用戶系統(tǒng)。

          起步

          Koa 作為 Express 原班人馬打造的新生代 Node.js Web 框架,自從發(fā)布以來就備受矚目。正如 Koa 作者們?cè)?span style="color: rgb(89, 89, 89);">文檔[3]中所指出的:

          ?

          Philosophically, Koa aims to "fix and replace node", whereas Express "augments node".(Express 是 Node 的補(bǔ)強(qiáng),而 Koa 則是為了解決 Node 的問題并取代之。)

          ?

          在這一篇文章中,我們將手把手帶你開發(fā)一個(gè)簡(jiǎn)單的用戶系統(tǒng) REST API,支持用戶的增刪改查以及 JWT 鑒權(quán),從實(shí)戰(zhàn)中感受 Koa2 的精髓,它相比于 Express 做出的突破性的改變。我們將選擇 TypeScript[4] 作為開發(fā)語(yǔ)言,數(shù)據(jù)庫(kù)選用 MySQL,并使用 TypeORM[5] 作為數(shù)據(jù)庫(kù)橋接層。

          ?

          「注意」

          這篇文章不會(huì)涉及 Koa 源碼級(jí)別的原理分析,重心會(huì)放在讓你完全掌握如何去使用 Koa 及周邊生態(tài)去開發(fā) Web 應(yīng)用,并欣賞 Koa 的設(shè)計(jì)之美。此外,「這篇教程比較長(zhǎng)」,如果一杯茶不夠的話可以續(xù)杯~

          ?

          預(yù)備知識(shí)

          本教程假定你已經(jīng)具備了以下知識(shí):

          • JavaScript 語(yǔ)言基礎(chǔ)知識(shí)(包括一些常用的 ES6+ 語(yǔ)法)
          • Node.js 基礎(chǔ)知識(shí),還有 npm 的基本使用,可以參考這篇教程[6]進(jìn)行學(xué)習(xí)
          • TypeScript 基礎(chǔ)知識(shí),只需了解簡(jiǎn)單的類型注解就可以了,可以參考我們的 TypeScript 系列教程[7]
          • *(非必須)*Express 框架基礎(chǔ)知識(shí),對(duì)于體驗(yàn) Koa 之美大有幫助,而且在本文中我們會(huì)大量穿插和 Express 的對(duì)比,可參考這篇教程[8]進(jìn)行學(xué)習(xí)

          所用技術(shù)

          • Node.js:10.x 及以上
          • npm:6.x 及以上
          • Koa:2.x
          • MySQL:推薦穩(wěn)定的 5.7 版本及以上
          • TypeORM:0.2.x

          學(xué)習(xí)目標(biāo)

          學(xué)完這篇教程,你將學(xué)會(huì):

          • 如果編寫 Koa 中間件
          • 通過 @koa/router 實(shí)現(xiàn)路由配置
          • 通過 TypeORM 連接和讀寫 MySQL 數(shù)據(jù)庫(kù)(其他數(shù)據(jù)庫(kù)都類似)
          • 了解 JWT 鑒權(quán)的原理,并動(dòng)手實(shí)現(xiàn)
          • 掌握 Koa 的錯(cuò)誤處理機(jī)制

          準(zhǔn)備初始代碼

          我們已經(jīng)為你準(zhǔn)備好了項(xiàng)目的腳手架,運(yùn)行以下命令克隆我們的初始代碼:

          git clone -b start-point https://github.com/tuture-dev/koa-quickstart.git

          如果你訪問 GitHub 不流暢,可以克隆我們的 Gitee 倉(cāng)庫(kù):

          git clone -b start-point https://gitee.com/tuture/koa-quickstart.git

          然后進(jìn)入項(xiàng)目,安裝依賴:

          cd koa-quickstart && npm install
          ?

          「注意」

          這里我使用了 package-lock.json 確保所有依賴版本一致,如果你用 yarn 安裝依賴出現(xiàn)問題,建議刪除 node_modules ,重新用 npm install 安裝。

          ?

          最簡(jiǎn)單的 Koa 服務(wù)器

          創(chuàng)建 src/server.ts ,編寫第一個(gè) Koa 服務(wù)器,代碼如下:

          // src/server.ts
          import Koa from 'koa';
          import cors from '@koa/cors';
          import bodyParser from 'koa-bodyparser';

          // 初始化 Koa 應(yīng)用實(shí)例
          const app = new Koa();

          // 注冊(cè)中間件
          app.use(cors());
          app.use(bodyParser());

          // 響應(yīng)用戶請(qǐng)求
          app.use((ctx) => {
            ctx.body = 'Hello Koa';
          });

          // 運(yùn)行服務(wù)器
          app.listen(3000);

          整個(gè)流程與一個(gè)基本的 Express 服務(wù)器幾乎完全一致:

          1. 初始化應(yīng)用實(shí)例 app
          2. 注冊(cè)相關(guān)的中間件(跨域 cors 和請(qǐng)求體解析中間件 bodyParser
          3. 添加請(qǐng)求處理函數(shù),響應(yīng)用戶請(qǐng)求
          4. 運(yùn)行服務(wù)器

          定睛一看,第 3 步中的請(qǐng)求處理函數(shù)(Request Handler)好像不太一樣。在 Express 框架中,一個(gè)請(qǐng)求處理函數(shù)一般是這樣的:

          function handler(req, res{
            res.send('Hello Express');
          }

          兩個(gè)參數(shù)分別對(duì)應(yīng)請(qǐng)求對(duì)象(Request)和響應(yīng)對(duì)象(Response),但是在 Koa 中,請(qǐng)求處理函數(shù)卻只有一個(gè)參數(shù) ctx (Context,上下文),然后只需向上下文對(duì)象寫入相關(guān)的屬性即可(例如這里就是寫入到返回?cái)?shù)據(jù) body 中):

          function handler(ctx{
            ctx.body = 'Hello Koa';
          }

          我的天,Koa 這是故意偷工減料的嗎?先不用急,我們馬上在下一節(jié)講解中間件時(shí)就會(huì)了解到 Koa 這樣設(shè)計(jì)的獨(dú)到之處。

          運(yùn)行服務(wù)器

          我們通過 npm start 就能開啟服務(wù)器了??梢酝ㄟ^ Curl (或者 Postman 等)來測(cè)試我們的 API:

          $ curl localhost:3000
          Hello Koa
          ?

          「提示」

          我們的腳手架中配置好了 Nodemon[9],因此接下來無(wú)需關(guān)閉服務(wù)器,修改代碼保存后會(huì)自動(dòng)加載最新的代碼并運(yùn)行。

          ?

          第一個(gè) Koa 中間件

          嚴(yán)格意義上來說,Koa 只是一個(gè)中間件框架,正如它的介紹所說:

          ?

          Expressive middleware for node.js using ES2017 async functions.(通過 ES2017 async 函數(shù)編寫富有表達(dá)力的 Node.js 中間件)

          ?

          下面這個(gè)表格更能說明 Koa 和 Express 的鮮明對(duì)比:

          可以看到,Koa 實(shí)際上對(duì)標(biāo)的是 Connect[10](Express 底層的中間件層),而不包含 Express 所擁有的其他功能,例如路由、模板引擎、發(fā)送文件等。接下來,我們就來學(xué)習(xí) Koa 最重要的知識(shí)點(diǎn):「中間件」。

          大名鼎鼎的“洋蔥模型”

          你也許從來沒有用過 Koa 框架,但很有可能聽說過“洋蔥模型”,而 Koa 正是洋蔥模型的代表框架之一。下面這個(gè)圖你也許很熟悉了:

          不過以個(gè)人觀點(diǎn),這個(gè)圖實(shí)在是太像“洋蔥”了,反而不太好理解。接下來我們將以更清晰直觀的方式來感受 Koa 中間件的設(shè)計(jì)之美。首先我們來看一下 Express 的中間件是什么樣的:

          請(qǐng)求(Request)直接依次貫穿各個(gè)中間件,最后通過請(qǐng)求處理函數(shù)返回響應(yīng)(Response),非常簡(jiǎn)單。然后我們來看看 Koa 的中間件是什么樣的:

          可以看到,Koa 中間件不像 Express 中間件那樣在請(qǐng)求通過了之后就完成了自己的使命;相反,中間件的執(zhí)行清晰地分為「兩個(gè)階段」。我們馬上來看下 Koa 中間件具體是什么樣的。

          Koa 中間件的定義

          Koa 的中間件是這樣一個(gè)函數(shù):

          async function middleware(ctx, next{
            // 第一階段
            await next();
            // 第二階段
          }

          第一個(gè)參數(shù)就是 Koa Context,也就是上圖中貫穿所有中間件和請(qǐng)求處理函數(shù)的綠色箭頭所傳遞的內(nèi)容,里面「封裝了請(qǐng)求體和響應(yīng)體」(實(shí)際上還有其他屬性,但這里暫時(shí)不講),分別可以通過 ctx.requestctx.response 來獲取,以下是一些常用的屬性:

          ctx.url    // 相當(dāng)于 ctx.request.url
          ctx.body   // 相當(dāng)于 ctx.response.body
          ctx.status // 相當(dāng)于 ctx.response.status
          ?

          「提示」

          關(guān)于所有請(qǐng)求和響應(yīng)上面的屬性及其別稱,請(qǐng)參考 Context API 文檔[11]。

          ?

          中間件的第二個(gè)參數(shù)便是 next 函數(shù),這個(gè)熟悉 Express 的同學(xué)一定知道它是干什么的:用來把控制權(quán)轉(zhuǎn)交給下一個(gè)中間件。但是它跟 Express 的 next 函數(shù)本質(zhì)的區(qū)別在于,「Koa 的 next 函數(shù)返回的是一個(gè) Promise」,在這個(gè) Promise 進(jìn)入完成狀態(tài)(Fulfilled)后,就會(huì)去執(zhí)行中間件中第二階段的代碼。

          那么我們不禁要問:這樣把中間件的執(zhí)行拆分為兩個(gè)階段,到底有什么好處嗎?我們來通過一個(gè)非常經(jīng)典的例子來感受一下:日志記錄中間件(包括響應(yīng)時(shí)間的計(jì)算)。

          實(shí)戰(zhàn):日志記錄中間件

          讓我們來實(shí)現(xiàn)一個(gè)簡(jiǎn)單的日志記錄中間件 logger ,用于記錄每次請(qǐng)求的方法、URL、狀態(tài)碼和響應(yīng)時(shí)間。創(chuàng)建 src/logger.ts ,代碼如下:

          // src/logger.ts
          import { Context } from 'koa';

          export function logger({
            return async (ctx: Context, next: () => Promise<void>) => {
              const start = Date.now();
              await next();
              const ms = Date.now() - start;
              console.log(`${ctx.method} ${ctx.url} ${ctx.status} - ${ms}ms`);
            };
          }

          嚴(yán)格意義上講,這里的 logger 是一個(gè)「中間件工廠函數(shù)」(Factory),調(diào)用這個(gè)工廠函數(shù)后返回的結(jié)果才是真正的 Koa 中間件。之所以寫成一個(gè)工廠函數(shù),是因?yàn)槲覀兛梢酝ㄟ^給工廠函數(shù)傳參的方式來更好地控制中間件的行為(當(dāng)然這里的 logger 比較簡(jiǎn)單,就沒有任何參數(shù))。

          在這個(gè)中間件的第一階段,我們通過 Date.now() 先獲取請(qǐng)求進(jìn)入的時(shí)間,然后通過 await next() 讓出執(zhí)行權(quán),等待下游中間件運(yùn)行結(jié)束后,再在第二階段通過計(jì)算 Date.now() 的差值來得出處理請(qǐng)求所用的時(shí)間。

          思考一下,如果用 Express 來實(shí)現(xiàn)這個(gè)功能,中間件應(yīng)該怎么寫,會(huì)有 Koa 這么簡(jiǎn)單優(yōu)雅嗎?

          ?

          「提示」

          這里通過兩個(gè) Date.now() 之間的差值來計(jì)算運(yùn)行時(shí)間其實(shí)是不精確的,為了獲取更準(zhǔn)確的時(shí)間,建議使用 process.hrtime()

          ?

          然后我們?cè)?src/server.ts 中把剛才的 logger 中間件通過 app.use 注冊(cè)進(jìn)去,代碼如下:

          // src/server.ts
          // ...

          import { logger } from './logger';

          // 初始化 Koa 應(yīng)用實(shí)例
          const app = new Koa();

          // 注冊(cè)中間件
          app.use(logger());
          app.use(cors());
          app.use(bodyParser());

          // ...

          這時(shí)候再訪問我們的服務(wù)器(通過 Curl 或者其他請(qǐng)求工具),應(yīng)該可以看到輸出日志:

          關(guān)于 Koa 框架本身的內(nèi)容基本講完了,但是對(duì)于一個(gè)比較完整的 Web 服務(wù)器來說,我們還需要更多的“武器裝備”才能應(yīng)對(duì)日常的業(yè)務(wù)邏輯。在接下來的部分,我們將通過社區(qū)的優(yōu)秀組件來解決兩個(gè)關(guān)鍵問題:路由和數(shù)據(jù)庫(kù),并演示如何結(jié)合 Koa 框架進(jìn)行使用。

          實(shí)現(xiàn)路由配置

          由于 Koa 只是一個(gè)中間件框架,所以路由的實(shí)現(xiàn)需要獨(dú)立的 npm 包。首先安裝 @koa/router 及其 TypeScript 類型定義:

          $ npm install @koa/router
          $ npm install @types/koa__router -D
          ?

          「注意」

          有些教程使用 koa-router ,但由于 koa-router 目前處于幾乎無(wú)人維護(hù)的狀態(tài),所以我們這里使用維護(hù)更積極的 Fork 版本 @koa/router。

          ?

          路由規(guī)劃

          在這篇教程中,我們將實(shí)現(xiàn)以下路由:

          • GET /users :查詢所有的用戶
          • GET /users/:id :查詢單個(gè)用戶
          • PUT /users/:id :更新單個(gè)用戶
          • DELETE /users/:id :刪除單個(gè)用戶
          • POST /users/login :登錄(獲取 JWT Token)
          • POST /users/register :注冊(cè)用戶

          實(shí)現(xiàn) Controller

          src 中創(chuàng)建 controllers 目錄,用于存放控制器有關(guān)的代碼。首先是 AuthController ,創(chuàng)建 src/controllers/auth.ts ,代碼如下:

          // src/controllers/auth.ts
          import { Context } from 'koa';

          export default class AuthController {
            public static async login(ctx: Context) {
              ctx.body = 'Login controller';
            }

            public static async register(ctx: Context) {
              ctx.body = 'Register controller';
            }
          }

          然后創(chuàng)建 src/controllers/user.ts,代碼如下:

          // src/controllers/user.ts
          import { Context } from 'koa';

          export default class UserController {
            public static async listUsers(ctx: Context) {
              ctx.body = 'ListUsers controller';
            }

            public static async showUserDetail(ctx: Context) {
              ctx.body = `ShowUserDetail controller with ID = ${ctx.params.id}`;
            }

            public static async updateUser(ctx: Context) {
              ctx.body = `UpdateUser controller with ID = ${ctx.params.id}`;
            }

            public static async deleteUser(ctx: Context) {
              ctx.body = `DeleteUser controller with ID = ${ctx.params.id}`;
            }
          }

          注意到在后面三個(gè) Controller 中,我們通過 ctx.params 獲取到路由參數(shù) id 。

          實(shí)現(xiàn)路由

          然后我們創(chuàng)建 src/routes.ts,用于把控制器掛載到對(duì)應(yīng)的路由上面:

          // src/routes.ts
          import Router from '@koa/router';

          import AuthController from './controllers/auth';
          import UserController from './controllers/user';

          const router = new Router();

          // auth 相關(guān)的路由
          router.post('/auth/login', AuthController.login);
          router.post('/auth/register', AuthController.register);

          // users 相關(guān)的路由
          router.get('/users', UserController.listUsers);
          router.get('/users/:id', UserController.showUserDetail);
          router.put('/users/:id', UserController.updateUser);
          router.delete('/users/:id', UserController.deleteUser);

          export default router;

          可以看到 @koa/router 的使用方式基本上與 Express Router 保持一致。

          注冊(cè)路由

          最后,我們需要將 router 注冊(cè)為中間件。打開 src/server.ts,修改代碼如下:

          // src/server.ts
          // ...

          import router from './routes';
          import { logger } from './logger';

          // 初始化 Koa 應(yīng)用實(shí)例
          const app = new Koa();

          // 注冊(cè)中間件
          app.use(logger());
          app.use(cors());
          app.use(bodyParser());

          // 響應(yīng)用戶請(qǐng)求
          app.use(router.routes()).use(router.allowedMethods());

          // 運(yùn)行服務(wù)器
          app.listen(3000);

          可以看到,這里我們調(diào)用 router 對(duì)象的 routes 方法獲取到對(duì)應(yīng)的 Koa 中間件,還調(diào)用了 allowedMethods 方法注冊(cè)了 HTTP 方法檢測(cè)的中間件,這樣當(dāng)用戶通過不正確的 HTTP 方法訪問 API 時(shí),就會(huì)自動(dòng)返回 405 Method Not Allowed 狀態(tài)碼。

          我們通過 Curl 來測(cè)試路由(也可以自行使用 Postman):

          $ curl localhost:3000/hello
          Not Found
          $ curl localhost:3000/auth/register
          Method Not Allowed
          $ curl -X POST localhost:3000/auth/register
          Register controller
          $ curl -X POST localhost:3000/auth/login
          Login controller
          $ curl localhost:3000/users
          ListUsers controller
          $ curl localhost:3000/users/123
          ShowUserDetail controller with ID = 123
          $ curl -X PUT localhost:3000/users/123
          UpdateUser controller with ID = 123
          $ curl -X DELETE localhost:3000/users/123
          DeleteUser controller with ID = 123

          同時(shí)可以看到服務(wù)器的輸出日志如下:

          路由已經(jīng)接通,接下來就讓我們來接入真實(shí)的數(shù)據(jù)吧!

          接入 MySQL 數(shù)據(jù)庫(kù)

          從這一步開始,我們將正式接入數(shù)據(jù)庫(kù)。Koa 本身是一個(gè)中間件框架,理論上可以接入任何類型的數(shù)據(jù)庫(kù),這里我們選擇流行的關(guān)系型數(shù)據(jù)庫(kù) MySQL。并且,由于我們使用了 TypeScript 開發(fā),因此這里使用為 TS 量身打造的 ORM[12] 庫(kù) TypeORM。

          數(shù)據(jù)庫(kù)的準(zhǔn)備工作

          首先,請(qǐng)安裝和配置好 MySQL 數(shù)據(jù)庫(kù),可以通過兩種方式:

          • 官網(wǎng)下載安裝包,這里是下載地址[13]
          • 使用 MySQL Docker 鏡像

          在確保 MySQL 實(shí)例運(yùn)行之后,我們打開終端,通過命令行連接數(shù)據(jù)庫(kù):

          $ mysql -u root -p

          輸入預(yù)先設(shè)置好的根帳戶密碼之后,就進(jìn)入了 MySQL 的交互式執(zhí)行客戶端,然后運(yùn)行以下命令:

          --- 創(chuàng)建數(shù)據(jù)庫(kù)
          CREATE DATABASE koa;

          --- 創(chuàng)建用戶并授予權(quán)限
          CREATE USER 'user'@'localhost' IDENTIFIED BY 'pass';
          GRANT ALL PRIVILEGES ON koa.* TO 'user'@'localhost';

          --- 處理 MySQL 8.0 版本的認(rèn)證協(xié)議問題
          ALTER USER 'user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'pass';
          flush privileges;

          TypeORM 的配置和連接

          首先安裝相關(guān)的 npm 包,分別是 MySQL 驅(qū)動(dòng)、TypeORM 及 reflect-metadata(反射 API 庫(kù),用于 TypeORM 推斷模型的元數(shù)據(jù)):

          $ npm install mysql typeorm reflect-metadata

          然后在項(xiàng)目根目錄創(chuàng)建 ormconfig.json ,TypeORM 會(huì)讀取這個(gè)數(shù)據(jù)庫(kù)配置進(jìn)行連接,代碼如下:

          // ormconfig.json
          {
            "type""mysql",
            "host""localhost",
            "port"3306,
            "username""user",
            "password""pass",
            "database""koa",
            "synchronize"true,
            "entities": ["src/entity/*.ts"],
            "cli": {
              "entitiesDir""src/entity"
            }
          }

          這里有一些需要解釋的字段:

          • database 就是我們剛剛創(chuàng)建的 koa 數(shù)據(jù)庫(kù)
          • synchronize 設(shè)為 true 能夠讓我們每次修改模型定義后都能自動(dòng)同步到數(shù)據(jù)庫(kù)*(如果你接觸過其他的 ORM 庫(kù),其實(shí)就是自動(dòng)數(shù)據(jù)遷移)*
          • entities 字段定義了模型文件的路徑,我們馬上就來創(chuàng)建

          接著修改 src/server.ts,在其中連接數(shù)據(jù)庫(kù),代碼如下:

          // src/server.ts
          import Koa from 'koa';
          import cors from '@koa/cors';
          import bodyParser from 'koa-bodyparser';
          import { createConnection } from 'typeorm';
          import 'reflect-metadata';

          import router from './routes';
          import { logger } from './logger';

          createConnection()
            .then(() => {
              // 初始化 Koa 應(yīng)用實(shí)例
              const app = new Koa();

              // 注冊(cè)中間件
              app.use(logger());
              app.use(cors());
              app.use(bodyParser());

              // 響應(yīng)用戶請(qǐng)求
              app.use(router.routes()).use(router.allowedMethods());

              // 運(yùn)行服務(wù)器
              app.listen(3000);
            })
            .catch((err: string) => console.log('TypeORM connection error:', err));

          創(chuàng)建數(shù)據(jù)模型定義

          src 目錄下創(chuàng)建 entity 目錄,用于存放數(shù)據(jù)模型定義文件。在其中創(chuàng)建 user.ts ,代表用戶模型,代碼如下:

          // src/entity/user.ts
          import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

          @Entity()
          export class User {
            @PrimaryGeneratedColumn()
            id: number;

            @Column()
            name: string;

            @Column({ select: false })
            password: string;

            @Column()
            email: string;
          }

          可以看到,用戶模型有四個(gè)字段,其含義很容易理解。而 TypeORM 則是通過裝飾器[14]這種優(yōu)雅的方式來將我們的 User 類映射到數(shù)據(jù)庫(kù)中的表。這里我們使用了三個(gè)裝飾器:

          • Entity 用于裝飾整個(gè)類,使其變成一個(gè)數(shù)據(jù)庫(kù)模型
          • Column 用于裝飾類的某個(gè)屬性,使其對(duì)應(yīng)于數(shù)據(jù)庫(kù)表中的一列,可提供一系列選項(xiàng)參數(shù),例如我們給 password 設(shè)置了 select: false ,使得這個(gè)字段在查詢時(shí)默認(rèn)不被選中
          • PrimaryGeneratedColumn 則是裝飾主列,它的值將自動(dòng)生成
          ?

          「提示」

          關(guān)于 TypeORM 所有的裝飾器定義及其詳細(xì)使用,請(qǐng)參考其裝飾器文檔[15]。

          ?

          在 Controller 中操作數(shù)據(jù)庫(kù)

          然后就可以在 Controller 中進(jìn)行數(shù)據(jù)的增刪改查操作了。首先我們打開 src/controllers/user.ts ,實(shí)現(xiàn)所有 Controller 的邏輯,代碼如下:

          // src/controllers/user.ts
          import { Context } from 'koa';
          import { getManager } from 'typeorm';

          import { User } from '../entity/user';

          export default class UserController {
            public static async listUsers(ctx: Context) {
              const userRepository = getManager().getRepository(User);
              const users = await userRepository.find();

              ctx.status = 200;
              ctx.body = users;
            }

            public static async showUserDetail(ctx: Context) {
              const userRepository = getManager().getRepository(User);
              const user = await userRepository.findOne(+ctx.params.id);

              if (user) {
                ctx.status = 200;
                ctx.body = user;
              } else {
                ctx.status = 404;
              }
            }

            public static async updateUser(ctx: Context) {
              const userRepository = getManager().getRepository(User);
              await userRepository.update(+ctx.params.id, ctx.request.body);
              const updatedUser = await userRepository.findOne(+ctx.params.id);

              if (updatedUser) {
                ctx.status = 200;
                ctx.body = updatedUser;
              } else {
                ctx.status = 404;
              }
            }

            public static async deleteUser(ctx: Context) {
              const userRepository = getManager().getRepository(User);
              await userRepository.delete(+ctx.params.id);

              ctx.status = 204;
            }
          }

          TypeORM 中操作數(shù)據(jù)模型主要是通過 Repository 實(shí)現(xiàn)的,在 Controller 中,可以通過 getManager().getRepository(Model) 來獲取到,之后 Repository 的查詢 API 就與其他的庫(kù)很類似了。

          ?

          「提示」

          關(guān)于 Repository 所有的查詢 API,請(qǐng)參考這里的文檔[16]。

          ?

          細(xì)心的你應(yīng)該還發(fā)現(xiàn)我們通過 ctx.request.body 獲取到了請(qǐng)求體的數(shù)據(jù),這是我們?cè)诘谝徊骄团渲煤玫?bodyParser 中間件在 Context 對(duì)象中添加的。

          然后我們修改 AuthController ,實(shí)現(xiàn)具體的注冊(cè)邏輯。由于密碼不能明文保存在數(shù)據(jù)庫(kù)中,需要使用非對(duì)稱算法進(jìn)行加密,這里我們使用曾經(jīng)獲得過密碼加密大賽冠軍的 Argon2[17] 算法。安裝對(duì)應(yīng)的 npm 包:

          npm install argon2

          然后實(shí)現(xiàn)具體的 register Controller,修改 src/controllers/auth.ts,代碼如下:

          // src/controllers/auth.ts
          import { Context } from 'koa';
          import * as argon2 from 'argon2';
          import { getManager } from 'typeorm';

          import { User } from '../entity/user';

          export default class AuthController {
            // ...

            public static async register(ctx: Context) {
              const userRepository = getManager().getRepository(User);

              const newUser = new User();
              newUser.name = ctx.request.body.name;
              newUser.email = ctx.request.body.email;
              newUser.password = await argon2.hash(ctx.request.body.password);

              // 保存到數(shù)據(jù)庫(kù)
              const user = await userRepository.save(newUser);

              ctx.status = 201;
              ctx.body = user;
            }
          }

          確保服務(wù)器在運(yùn)行之后,我們就可以開始測(cè)試一波了。首先是注冊(cè)用戶(這里我用 Postman 演示,直觀一些):

          你可以繼續(xù)注冊(cè)幾個(gè)用戶,然后繼續(xù)訪問 /users 相關(guān)的路由,應(yīng)該可以成功地獲取、修改和刪除相應(yīng)的數(shù)據(jù)了!

          實(shí)現(xiàn) JWT 鑒權(quán)

          JSON Web Token(JWT)是一種流行的 RESTful API 鑒權(quán)方案。這里我們將手把手帶你學(xué)會(huì)如何在 Koa 框架中使用 JWT 鑒權(quán),但是不會(huì)過多講解其原理(可參考這篇文章[18]進(jìn)行學(xué)習(xí))。

          首先安裝相關(guān)的 npm 包:

          npm install koa-jwt jsonwebtoken
          npm install @types/jsonwebtoken -D

          創(chuàng)建 src/constants.ts ,用于存放 JWT Secret 常量,代碼如下:

          // src/constants.ts
          export const JWT_SECRET = 'secret';

          在實(shí)際開發(fā)中,請(qǐng)?zhí)鎿Q成一個(gè)足夠復(fù)雜的字符串,并且最好通過環(huán)境變量的方式注入。

          重新規(guī)劃路由

          有些路由我們希望只有已登錄的用戶才有權(quán)查看(受保護(hù)的路由),而另一些路由則是所有請(qǐng)求都可以訪問(不受保護(hù)的路由)。在 Koa 的洋蔥模型中,我們可以這樣實(shí)現(xiàn):

          所有請(qǐng)求都可以直接訪問未受保護(hù)的路由,但是受保護(hù)的路由就放在 JWT 中間件的后面(或者從洋蔥模型的角度看是“里面”),這樣對(duì)于沒有攜帶 JWT Token 的請(qǐng)求就直接返回,而不會(huì)繼續(xù)傳遞下去。

          想法明確之后,打開 src/routes.ts 路由文件,修改代碼如下:

          // src/routes.ts
          import Router from '@koa/router';

          import AuthController from './controllers/auth';
          import UserController from './controllers/user';

          const unprotectedRouter = new Router();

          // auth 相關(guān)的路由
          unprotectedRouter.post('/auth/login', AuthController.login);
          unprotectedRouter.post('/auth/register', AuthController.register);

          const protectedRouter = new Router();

          // users 相關(guān)的路由
          protectedRouter.get('/users', UserController.listUsers);
          protectedRouter.get('/users/:id', UserController.showUserDetail);
          protectedRouter.put('/users/:id', UserController.updateUser);
          protectedRouter.delete('/users/:id', UserController.deleteUser);

          export { protectedRouter, unprotectedRouter };

          上面我們分別實(shí)現(xiàn)了 protectedRouterunprotectedRouter ,分別對(duì)應(yīng)于需要 JWT 中間件保護(hù)的路由和不需要保護(hù)的路由。

          注冊(cè) JWT 中間件

          接著便是注冊(cè) JWT 中間件,并分別在其前后注冊(cè)不需要保護(hù)的路由 unprotectedRouter 和需要保護(hù)的路由 protectedRouter。修改服務(wù)器文件 src/server.ts ,代碼如下:

          // src/server.ts
          // ...
          import jwt from 'koa-jwt';
          import 'reflect-metadata';

          import { protectedRouter, unprotectedRouter } from './routes';
          import { logger } from './logger';
          import { JWT_SECRET } from './constants';

          createConnection()
            .then(() => {
              // ...

              // 無(wú)需 JWT Token 即可訪問
              app.use(unprotectedRouter.routes()).use(unprotectedRouter.allowedMethods());

              // 注冊(cè) JWT 中間件
              app.use(jwt({ secret: JWT_SECRET }).unless({ method: 'GET' }));

              // 需要 JWT Token 才可訪問
              app.use(protectedRouter.routes()).use(protectedRouter.allowedMethods());

              // ...
            })
            // ...

          對(duì)應(yīng)剛才“洋蔥模型”的設(shè)計(jì)圖,是不是感覺很直觀?

          ?

          「提示」

          在 JWT 中間件注冊(cè)完畢后,如果用戶請(qǐng)求攜帶了有效的 Token,后面的 protectedRouter 就可以通過 ctx.state.user 獲取到 Token 的內(nèi)容(更精確的說法是 Payload,負(fù)載,一般是用戶的關(guān)鍵信息,例如 ID)了;反之,如果 Token 缺失或無(wú)效,那么 JWT 中間件會(huì)直接自動(dòng)返回 401 錯(cuò)誤。關(guān)于 koa-jwt 的更多使用細(xì)節(jié),請(qǐng)參考其文檔[19]。

          ?

          在 Login 中簽發(fā) JWT Token

          我們需要提供一個(gè) API 端口讓用戶可以獲取到 JWT Token,最合適的當(dāng)然是登錄接口 /auth/login。打開 src/controllers/auth.ts ,在 login 控制器中實(shí)現(xiàn)簽發(fā) JWT Token 的邏輯,代碼如下:

          // src/controllers/auth.ts
          // ...
          import jwt from 'jsonwebtoken';

          // ...
          import { JWT_SECRET } from '../constants';

          export default class AuthController {
            public static async login(ctx: Context) {
              const userRepository = getManager().getRepository(User);

              const user = await userRepository
                .createQueryBuilder()
                .where({ name: ctx.request.body.name })
                .addSelect('User.password')
                .getOne();

              if (!user) {
                ctx.status = 401;
                ctx.body = { message: '用戶名不存在' };
              } else if (await argon2.verify(user.password, ctx.request.body.password)) {
                ctx.status = 200;
                ctx.body = { token: jwt.sign({ id: user.id }, JWT_SECRET) };
              } else {
                ctx.status = 401;
                ctx.body = { message: '密碼錯(cuò)誤' };
              }
            }

            // ...
          }

          login 中,我們首先根據(jù)用戶名(請(qǐng)求體中的 name 字段)查詢對(duì)應(yīng)的用戶,如果該用戶不存在,則直接返回 401;存在的話再通過 argon2.verify 來驗(yàn)證請(qǐng)求體中的明文密碼 password 是否和數(shù)據(jù)庫(kù)中存儲(chǔ)的加密密碼是否一致,如果一致則通過 jwt.sign 簽發(fā) Token,如果不一致則還是返回 401。

          這里的 Token 負(fù)載就是標(biāo)識(shí)用戶 ID 的對(duì)象 { id: user.id } ,這樣后面鑒權(quán)成功后就可以通過 ctx.user.id 來獲取用戶 ID。

          在 User 控制器中添加訪問控制

          Token 的中間件和簽發(fā)都搞定之后,最后一步就是在合適的地方校驗(yàn)用戶的 Token,確認(rèn)其是否有足夠的權(quán)限。最典型的場(chǎng)景便是,在更新或刪除用戶時(shí),我們要「確保是用戶本人在操作」。打開 src/controllers/user.ts ,代碼如下:

          // src/controllers/user.ts
          // ...

          export default class UserController {
            // ...

            public static async updateUser(ctx: Context) {
              const userId = +ctx.params.id;

              if (userId !== +ctx.state.user.id) {
                ctx.status = 403;
                ctx.body = { message: '無(wú)權(quán)進(jìn)行此操作' };
                return;
              }

              const userRepository = getManager().getRepository(User);
              await userRepository.update(userId, ctx.request.body);
              const updatedUser = await userRepository.findOne(userId);

              // ...
            }

            public static async deleteUser(ctx: Context) {
              const userId = +ctx.params.id;

              if (userId !== +ctx.state.user.id) {
                ctx.status = 403;
                ctx.body = { message: '無(wú)權(quán)進(jìn)行此操作' };
                return;
              }

              const userRepository = getManager().getRepository(User);
              await userRepository.delete(userId);

              ctx.status = 204;
            }
          }

          兩個(gè) Controller 的鑒權(quán)邏輯基本相同,我們通過比較 ctx.params.idctx.state.user.id 是否相同,如果不相同則返回 403 Forbidden 錯(cuò)誤,相同則繼續(xù)執(zhí)行相應(yīng)的數(shù)據(jù)庫(kù)操作。

          代碼寫完之后,我們用剛才注冊(cè)的一個(gè)用戶信息去訪問登錄 API:

          成功地獲取到了 JWT Token!然后我們復(fù)制獲取到的 Token,在接下來測(cè)試受保護(hù)的路由時(shí),我們需要添加一個(gè) Authorization 頭部,值為 Bearer <JWT_TOKEN> ,如下圖所示:

          然后就可以測(cè)試受保護(hù)的路由了!這里由于篇幅限制就省略了。

          錯(cuò)誤處理

          最后,我們來簡(jiǎn)單地聊一下 Koa 中的錯(cuò)誤處理。由于 Koa 采用了 async 函數(shù)和 Promise 作為異步編程的方案,所以錯(cuò)誤處理自然也很簡(jiǎn)單了——直接用 JavaScript 自帶的 try-catch 語(yǔ)法就可以輕松搞定。

          實(shí)現(xiàn)自定義錯(cuò)誤(異常)

          首先,讓我們來實(shí)現(xiàn)一些自定義的錯(cuò)誤(或者異常,本文不作區(qū)分)類。創(chuàng)建 src/exceptions.ts ,代碼如下:

          // src/exceptions.ts
          export class BaseException extends Error {
            // 狀態(tài)碼
            status: number;
            // 提示信息
            message: string;
          }

          export class NotFoundException extends BaseException {
            status = 404;

            constructor(msg?: string) {
              super();
              this.message = msg || '無(wú)此內(nèi)容';
            }
          }

          export class UnauthorizedException extends BaseException {
            status = 401;

            constructor(msg?: string) {
              super();
              this.message = msg || '尚未登錄';
            }
          }

          export class ForbiddenException extends BaseException {
            status = 403;

            constructor(msg?: string) {
              super();
              this.message = msg || '權(quán)限不足';
            }
          }

          這里的錯(cuò)誤類型參考了 Nest.js[20] 的設(shè)計(jì)。出于學(xué)習(xí)目的,這里作了簡(jiǎn)化,并且只實(shí)現(xiàn)了我們需要用到的錯(cuò)誤。

          在 Controller 中使用自定義錯(cuò)誤

          接著我們便可以在 Controller 中使用剛才的自定義錯(cuò)誤了。打開 src/controllers/auth.ts,修改代碼如下:

          // src/controllers/auth.ts
          // ...
          import { UnauthorizedException } from '../exceptions';

          export default class AuthController {
            public static async login(ctx: Context) {
              // ...

              if (!user) {
                throw new UnauthorizedException('用戶名不存在');
              } else if (await argon2.verify(user.password, ctx.request.body.password)) {
                ctx.status = 200;
                ctx.body = { token: jwt.sign({ id: user.id }, JWT_SECRET) };
              } else {
                throw new UnauthorizedException('密碼錯(cuò)誤');
              }
            }

            // ...
          }

          可以看到,我們將直接手動(dòng)設(shè)置狀態(tài)碼和響應(yīng)體的代碼改成了簡(jiǎn)單的錯(cuò)誤拋出,代碼清晰了很多。

          ?

          「提示」

          Koa 的 Context 對(duì)象提供了一個(gè)便捷方法 throw ,同樣可以拋出異常,例如 ctx.throw(400, 'Bad request')

          ?

          同樣地,修改 UserController 相關(guān)的邏輯。修改 src/controllers/user.ts,代碼如下:

          // src/controllers/user.ts
          // ...
          import { NotFoundException, ForbiddenException } from '../exceptions';

          export default class UserController {
            // ...

            public static async showUserDetail(ctx: Context) {
              const userRepository = getManager().getRepository(User);
              const user = await userRepository.findOne(+ctx.params.id);

              if (user) {
                ctx.status = 200;
                ctx.body = user;
              } else {
                throw new NotFoundException();
              }
            }

            public static async updateUser(ctx: Context) {
              const userId = +ctx.params.id;

              if (userId !== +ctx.state.user.id) {
                throw new ForbiddenException();
              }

              // ...
            }
           // ...
            public static async deleteUser(ctx: Context) {
              const userId = +ctx.params.id;

              if (userId !== +ctx.state.user.id) {
                throw new ForbiddenException();
              }

              // ...
            }
          }

          添加錯(cuò)誤處理中間件

          最后,我們需要添加錯(cuò)誤處理中間件來捕獲在 Controller 中拋出的錯(cuò)誤。打開 src/server.ts ,實(shí)現(xiàn)錯(cuò)誤處理中間件,代碼如下:

          // src/server.ts
          // ...

          createConnection()
            .then(() => {
              // ...

              // 注冊(cè)中間件
              app.use(logger());
              app.use(cors());
              app.use(bodyParser());

              app.use(async (ctx, next) => {
                try {
                  await next();
                } catch (err) {
                  // 只返回 JSON 格式的響應(yīng)
                  ctx.status = err.status || 500;
                  ctx.body = { message: err.message };
                }
              });

              // ...
            })
            // ...

          可以看到,在這個(gè)錯(cuò)誤處理中間件中,我們把返回的響應(yīng)數(shù)據(jù)轉(zhuǎn)換成 JSON 格式(而不是之前的 Plain Text),這樣看上去更統(tǒng)一一些。

          至此,這篇教程就結(jié)束了。內(nèi)容很多,希望對(duì)你有一定的幫助。我們的用戶系統(tǒng)已經(jīng)能夠處理大部分情形,但是對(duì)于一些邊際情況的處理依然很糟糕(能想到有哪些嗎?)。不過話說回來,相信你已經(jīng)確定 Koa 是一個(gè)很棒的框架了吧?

          ?

          想要學(xué)習(xí)更多精彩的實(shí)戰(zhàn)技術(shù)教程?來圖雀社區(qū)[21]逛逛吧。

          ?

          Reference

          [1]

          mRc: https://github.com/mRcfps

          [2]

          圖雀社區(qū): https://tuture.co?utm_source=juejin_zhuanlan

          [3]

          文檔: https://github.com/koajs/koa/blob/master/docs/koa-vs-express.md

          [4]

          TypeScript: https://www.typescriptlang.org/

          [5]

          TypeORM: https://github.com/typeorm/typeorm

          [6]

          這篇教程: https://juejin.im/post/5df39f94518825122030859c

          [7]

          TypeScript 系列教程: https://juejin.im/post/5ec6377ff265da76f30e45d2

          [8]

          這篇教程: https://juejin.im/post/5df9cf38f265da33b82bf5a2

          [9]

          Nodemon: https://github.com/remy/nodemon

          [10]

          Connect: https://github.com/senchalabs/connect

          [11]

          Context API 文檔: https://github.com/koajs/koa/blob/master/docs/api/context.md

          [12]

          ORM: http://www.ruanyifeng.com/blog/2019/02/orm-tutorial.html

          [13]

          下載地址: https://dev.mysql.com/downloads/mysql/

          [14]

          裝飾器: https://www.tslang.cn/docs/handbook/decorators.html

          [15]

          裝飾器文檔: https://github.com/typeorm/typeorm/blob/master/docs/zh_CN/decorator-reference.md

          [16]

          這里的文檔: https://github.com/typeorm/typeorm/blob/master/docs/zh_CN/repository-api.md

          [17]

          Argon2: https://www.argon2.com/

          [18]

          這篇文章: http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html

          [19]

          文檔: https://github.com/koajs/jwt

          [20]

          Nest.js: https://docs.nestjs.com/exception-filters

          [21]

          圖雀社區(qū): https://tuture.co?utm_source=juejin_zhuanlan


            

          最后



          如果你覺得這篇內(nèi)容對(duì)你挺有啟發(fā),我想邀請(qǐng)你幫我三個(gè)小忙:

          1. 點(diǎn)個(gè)「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點(diǎn)在看,都是耍流氓 -_-)

          2. 歡迎加我微信「qianyu443033099」拉你進(jìn)技術(shù)群,長(zhǎng)期交流學(xué)習(xí)...

          3. 關(guān)注公眾號(hào)「前端下午茶」,持續(xù)為你推送精選好文,也可以加我為好友,隨時(shí)聊騷。

          點(diǎn)個(gè)在看支持我吧,轉(zhuǎn)發(fā)就更好了

          瀏覽 49
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  激情 小说 亚洲 图片: 伦 | 日韩日批| 青青草原在线视频精品 | 99re99在线视频最新 | 国产欧美一区二区三区四区 |