前言 Koa 是一个由 Express 的原始开发者创建的 Node.js 后端框架,旨在提供更小、更灵活的基础后端框架。
koa - npm (npmjs.com)
安装准备 安装nodemon
安装koa
在package.json文件中使用es6规范且将调试改为使用nodemon
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 { "name" : "kora" , "version" : "1.0.0" , "main" : "index.js" , "type" : "module" , "scripts" : { "dev" : "nodemon index.js" , "test" : "echo \"Error: no test specified\" && exit 1" } , "keywords" : [ ] , "author" : "" , "license" : "ISC" , "description" : "" , "dependencies" : { "koa" : "^2.15.3" } }
创建路由 首先需要创建一个koa app 实例,使用ctx上下文对象来获取request,使用ctx.body来返回具体相应的内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import Koa from "koa" ;const hostname = "127.0.0.1" ;const port = "8008" const app = new Koa ();app.use (async (ctx)=>{ ctx.body = "hello world" }) app.listen (port,hostname,()=> { console .log (`Server running at http://${hostname} :${port} /` ); })
koa app实例还可以对其他非法路由进行捕获处理
1 2 3 4 5 6 app.use (async (ctx) => { if (!ctx.body ) { ctx.status = 404 ; ctx.body = "404 Not Found" ; } });
ctx中具有request和response两个对象的性质,和nodejs中的使用方法相同,可以使用ctx.request
和ctx.response
获取特定的对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 router.get ("/index" , async (ctx) => { if (ctx.request .method == "GET" ) { let id = ctx.query .id ; let name = ctx.query .name ; ctx.status = 200 ; ctx.method = "GET" ; ctx.type = "Application/json" ; ctx.path = "/index" ; ctx.body = { id : id, name : name, }; } });
在网页中便可以看到显示格式为Json(JavaScript Object Notation)
1 2 3 4 { "id" : "1" , "name" : "koa" }
洋葱模型 app.use()
方法用于注册中间件,一个use()
就是一个中间件,可以使用next()
进行中间件的阻断。
当程序遇到next()
的时候,会暂停当前中间件的执行,将控制权传递给下一个中间件。控制权的来回切换符合洋葱模型,看如下的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import Koa from "koa" ;const hostname = "127.0.0.1" ; const port = 8008 ; 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) => { ctx.body = "Hello World" ; }); app.listen (port, hostname, () => { console .log (`Server running at http://${hostname} :${port} /` ); });
打印得到的结果为:
如果出现两次打印结果,这是因为浏览器默认会申请访问网站的ico(图标),可以在调试工具中禁用
安装与配置路由 安装@koa/router
koa-router 兼容 koa1 和 koa2 的历史版本 @koa/router 专为 koa2 设计的新版本
package.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 { "name" : "kora" , "version" : "1.0.0" , "main" : "index.js" , "type" : "module" , "scripts" : { "dev" : "nodemon index.js" , "test" : "echo \"Error: no test specified\" && exit 1" } , "keywords" : [ ] , "author" : "" , "license" : "ISC" , "description" : "" , "dependencies" : { "@koa/router" : "^12.0.1" , "koa" : "^2.15.3" } }
创建Router实例并将其运用到Koa app实例中,下面是一个简单的例子,访问默认的网址会显示Hello world
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import Koa from "koa" ;import Router from "@koa/router" ;const hostname = "127.0.0.1" ; const port = 8008 ; const app = new Koa ();const router = new Router ();router.get ("/" , async (ctx) => { ctx.body = "Hello World!" ; }); app.use (router.routes ()); app.listen (port, hostname, () => { console .log (`Server running at http://${hostname} :${port} /` ); });
对于带有参数的访问,可以使用query和params对象进行获取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 router.get ("/index" , async (ctx) => { let id = ctx.query .id ; let name = ctx.query .name ; ctx.status = 200 ; ctx.body = `id: ${id} , name: ${name} ` ; }); router.get ("/user/id/:id/name/:name" , async (ctx) => { let id = ctx.params .id ; let name = ctx.params .name ; ctx.status = 200 ; ctx.body = `id: ${id} , name: ${name} ` ; }); app.use (router.routes ());
在创建路由的时候,可以使用prefix参数进行路由的分组
1 2 3 4 5 6 7 8 9 10 11 12 13 const router2 = new Router ({prefix : "/api" });router2.get ("/get" , async (ctx) => { ctx.body = "get" ; }); router2.get ("/post" , async (ctx) => { ctx.body = "post" ; }); app.use (router2.routers ())
还可以使用Router对象的redirect方法进行网页重定向
1 2 3 4 router.redirect ("/redirect" , "https://www.bilibili.com/" ); app.use (router.routers ())
post请求 安装@koa/post
package.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 { "name" : "kora" , "version" : "1.0.0" , "main" : "index.js" , "type" : "module" , "scripts" : { "dev" : "nodemon index.js" , "test" : "echo \"Error: no test specified\" && exit 1" } , "keywords" : [ ] , "author" : "" , "license" : "ISC" , "description" : "" , "dependencies" : { "@koa/bodyparser" : "^5.1.1" , "@koa/router" : "^12.0.1" , "koa" : "^2.15.3" } }
让中间件使用bodyparser,要获取请求体中的参数,使用ctx.request.body.属性名
来获取参数数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import koaBody from "@koa/bodyparser" ;const app = new Koa ();const router = new Router ();app.use (koaBody ()); router.post ("/postUrl" , async (ctx) => { let id = ctx.request .body .id ; let name = ctx.request .body .name ; ctx.status = 200 ; ctx.body = `id: ${id} , name: ${name} ` ; }); router.post ("/postJSon" , async (ctx) => { let id = ctx.request .body .id ; let name = ctx.request .body .name ; ctx.status = 200 ; ctx.body = `id:${id} , name:${name} ` ; }); app.use (async (ctx) => { if (!ctx.body ) { ctx.status = 404 ; ctx.body = "404 Not Found" ; } }); app.listen (port, hostname, () => { console .log (`Server running at http://${hostname} :${port} /` ); });
使用apiPost工具进行模拟请求
完整代码备份如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 import Koa from "koa" ;import Router from "@koa/router" ;import koaBody from "@koa/bodyparser" ;const hostname = "127.0.0.1" ; const port = 8008 ; const app = new Koa ();const router = new Router ();app.use (koaBody ()); router.post ("/postUrl" , async (ctx) => { let id = ctx.request .body .id ; let name = ctx.request .body .name ; ctx.status = 200 ; ctx.body = `id: ${id} , name: ${name} ` ; }); router.post ("/postJSon" , async (ctx) => { let id = ctx.request .body .id ; let name = ctx.request .body .name ; ctx.status = 200 ; ctx.body = `id:${id} , name:${name} ` ; }); router.get ("/" , async (ctx) => { ctx.body = "Hello World!" ; }); router.get ("/index" , async (ctx) => { if (ctx.request .method == "GET" ) { let id = ctx.query .id ; let name = ctx.query .name ; ctx.status = 200 ; ctx.method = "GET" ; ctx.type = "Application/json" ; ctx.path = "/index" ; ctx.body = { id : id, name : name, }; } }); router.get ("/user/id/:id/name/:name" , async (ctx) => { let id = ctx.params .id ; let name = ctx.params .name ; ctx.status = 200 ; ctx.body = `id: ${id} , name: ${name} ` ; }); const router2 = new Router ({ prefix : "/api" });router2.get ("/get" , async (ctx) => { ctx.body = "get" ; ctx.response .status = 200 ; }); router2.get ("/post" , async (ctx) => { ctx.body = "post" ; }); router.redirect ("/redirect" , "https://www.bilibili.com/" ); app.use (router.routes ()); app.use (router2.routes ()); app.use (async (ctx) => { if (!ctx.body ) { ctx.status = 404 ; ctx.body = "404 Not Found" ; } }); app.listen (port, hostname, () => { console .log (`Server running at http://${hostname} :${port} /` ); });
注意,koaBody
中有一个关键的parsedMethods
参数,如果不额外设置,默认为['POST', 'PUT', 'PATCH']
,使用范围以外的请求方法会无法获取body(如DELETE请求),其他参数详见@koa/bodyparser - npm (npmjs.com)
错误捕获 使用try catch
语句进行错误的捕获
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import Koa from "koa" ;import Router from "@koa/router" ;const hostname = "127.0.0.1" ; const port = 8008 ; const app = new Koa ();const router = new Router ();router.get ("/" , async (ctx) => { throw new Error ("error" ); }); app.use (async (ctx, next) => { try { await next (); } catch (err) { ctx.status = 500 ; ctx.body = "网络错误" ; } }); app.use (router.routes ()); app.listen (port, hostname, () => { console .log (`Server running at http://${hostname} :${port} /` ); });
允许跨域请求 安装
package.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 { "name" : "kora" , "version" : "1.0.0" , "main" : "index.js" , "type" : "module" , "scripts" : { "dev" : "nodemon index.js" , "test" : "echo \"Error: no test specified\" && exit 1" }, "keywords" : [], "author" : "" , "license" : "ISC" , "description" : "" , "dependencies" : { "@koa/bodyparser" : "^5.1.1" , "@koa/cors" : "^5.0.0" , "@koa/router" : "^12.0.1" , "koa" : "^2.15.3" } }
如果出现跨域请求会出现报错:
Access to fetch at ‘http://127.0.0.1:8008/ ‘ from origin ‘null’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. If an opaque response serves your needs, set the request’s mode to ‘no-cors’ to fetch the resource with CORS disabled.
什么情况下算跨域请求呢?
在web开发中,”域” 主要指的是网络请求的来源(origin),由协议(如:http或https)、域名和端口三部分组成,任何一部分的不同都算跨域
想要允许跨域请求,让app对象使用Cors
即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import Koa from "koa" ;import Router from "@koa/router" ;import Cors from "@koa/cors" ;const hostname = "127.0.0.1" ; const port = 8008 ; const app = new Koa ();const router = new Router ();app.use (Cors ()); router.get ("/" , async (ctx) => { ctx.status = 200 ; ctx.body = "Hello World" ; }); app.use (router.routes ()); app.listen (port, hostname, () => { console .log (`Server running at http://${hostname} :${port} /` ); });
上传文件 安装
首先创建一个磁盘存储引擎,并规定上传的路径(服务端的文件夹)和文件名规则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import Multer from "@koa/multer" ;import path from "path" const storage = Multer .diskStorage ({ destination : (request, file, callbackFunc ) => { callbackFunc (null , "./upload" ); }, filename : (request, file, callbackFunc ) => { callbackFunc (null , Date .now () + path.extname (file.originalname )); }, });
接着定一个multer对象,可以限定文件的上传类型和文件大小
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const multer = Multer ({ storage, limits : { fileSize : 2 * 1024 * 1024 , }, fileFilter : (request, file, callbackFunc ) => { let allowedTypes = ["image/jpeg" , "image/jpg" , "image/png" ]; if (allowedTypes.includes (file.mimetype )) { callbackFunc (null , true ); } else { callbackFunc (new Error ("不支持的文件类型" ), false ); } }, });
创建后,在对应路由中使用multer对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import Koa from "koa" ;import Router from "@koa/router" ;import Cors from "@koa/cors" ;const app = Koa ();app.use (Cors ()); const router = Router ();router.post ("/upload" , multer.single ("file" ), async (ctx) => { const file = ctx.request .file ; if (file) { ctx.body = { status : "success" , message : "文件上传成功" , data : file, }; }else { ctx.body = { status : "error" , message : "文件上传失败" , }; } }); app.use (router.routers ()) app.listen (port, host, () => { console .log (`Server is running on http://${host} :${port} ` ); });
选择txt进行上传,打印不支持文件类型
选择大于2MB文件进行上传,返回文件过大(file too large)
以下为成功的例子
上传的文件都会收集到服务端的upload
文件夹中
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 import Koa from "koa" ;import Router from "@koa/router" ;import Cors from "@koa/cors" ;import BodyParser from "@koa/bodyparser" ;import Multer from "@koa/multer" ;import path from "path" ;const host = "127.0.0.1" ;const port = 3000 ;const app = new Koa ();const router = new Router ();app.use (Cors ()); app.use (BodyParser ()); const storage = Multer .diskStorage ({ destination : (request, file, callbackFunc ) => { callbackFunc (null , "./upload" ); }, filename : (request, file, callbackFunc ) => { callbackFunc (null , Date .now () + path.extname (file.originalname )); }, }); const multer = Multer ({ storage, limits : { fileSize : 2 * 1024 * 1024 , }, fileFilter : (request, file, callbackFunc ) => { let allowedTypes = ["image/jpeg" , "image/jpg" , "image/png" ]; if (allowedTypes.includes (file.mimetype )) { callbackFunc (null , true ); } else { callbackFunc (new Error ("不支持的文件类型" ), false ); } }, }); router.post ("/upload" , multer.single ("file" ), async (ctx) => { const file = ctx.request .file ; if (file) { ctx.body = { status : "success" , message : "文件上传成功" , data : file, }; }else { ctx.body = { status : "error" , message : "文件上传失败" , }; } }); app.use (async (ctx, next) => { try { await next (); } catch (err) { ctx.status = 500 ; ctx.body = "err: " + err.message ; } }); app.use (router.routes ()); app.listen (port, host, () => { console .log (`Server is running on http://${host} :${port} ` ); });
cookie cookie通常用来存储用户当前的状态信息
在koa中通过ctx.cookies.set()
来设置cookie信息,常用maxAge
和httpOnly
参数来分别设置cookie的保存时间和是否允许脚本获取cookie
如果设置的不是英文,而是其他字符,常见的是通过uri来进行加密和解密
后续可以通过ctx.cookies.get
方法获取相应的cookie信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import Koa from "koa" ;import Router from "@koa/router" ;const hostname = "127.0.0.1" ;const port = 8008 ;const app = new Koa ();const router = new Router (); router.get ("/" , async (ctx) => { ctx.cookies .set ("name" , encodeURIComponent ("百度" )); ctx.cookies .set ("web" , "baidu.com" , { maxAge : 30 * 1000 , httpOnly : false , }); let name = ctx.cookies .get ("name" ); console .log ("name:" , decodeURIComponent (name)); ctx.body = "欢迎访问百度" ; }); app.use (router.routes ()); app.listen (port, hostname, () => { console .log (`服务器已启动: http://${hostname} :${port} ` ); });
可以在调试工具中看到cookie信息
如果要删除cookie,可以通过设置空白值的方法进行清除
1 2 3 4 5 6 router.get ("/" , async (ctx) => { ctx.cookies .set ("name" ,"" ,{maxAge : 0 }) })
Session session用于服务端存储状态信息,通过 Session ID 在 Cookie 中的存在,来识别和跟踪用户,同时在服务器端安全地存储用户的状态和数据。
和cookie不同的是,除了设置属性值和过期时间,session需要单独Session ID(存储在cookie中的唯一标识)和密钥(防止数据被篡改)
安装
需要注意的是,此方法会将Session存储到客户端的cookie中 编码部分采用的是base64编码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import Koa from "koa" ;import Router from "@koa/router" ;import Session from "koa-session" ;const hostname = "127.0.0.1" ;const port = 8008 ;const app = new Koa ();const router = new Router ();app.keys = ["session.koa" ]; const CONFIG = { key : "koa" , maxAge : 24 * 60 * 60 * 1000 , signed : true , secure : false , }; app.use (Session (CONFIG , app)); router.get ("/" , async (ctx) => { ctx.session .name = "百度" ; ctx.session .url = "baidu.com" ; if (!ctx.session .user ) { ctx.session .user = 1 ; } else { ctx.session .user += 1 ; } let name = ctx.session .name ; console .log ("name:" , name); ctx.body = "用户:" + ctx.session .user ; }); app.use (router.routes ()); app.listen (port, hostname, () => { console .log (`服务器已启动: http://${hostname} :${port} ` ); });
在浏览器的调试工具中可以查看
通过以下方法可以删除session
1 2 3 4 5 6 router.get ("/" , async (ctx) => { ctx.session = null delete ctx.session .name });
jwt JWT(JSON Web Token)是一种用于安全传输信息的开放标准(RFC 7519),通常用于身份验证和信息交换。
JWT 由三部分组成:
头部(Header):通常包含令牌的类型(JWT)和所使用的签名算法(如 HMAC SHA256)。
载荷(Payload):包含声明(claims),即要传输的数据,可以是用户信息、权限等。
签名(Signature):通过头部和载荷生成的签名,用于验证令牌的完整性和来源。
格式为:header.payload.signature
安装依赖
需要定义一个生成token的函数,需要传入一个key作为加密的密匙。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import JWT from 'jsonwebtoken' let generateToken = (key ) => { let id = 1 ; let now = Math .floor (Date .now () / 1000 ); let expire = 24 * 60 * 60 ; let payload = { sub : id, iss : "baidu.com" , iat : now, nbf : now, exp : now + expire, aud : ["" ], data : { name : "gcnanmu" , gender : "man" , }, }; let token = JWT .sign (payload, key, { algorithm : "HS256" }); return token; };
解密可以使用如下代码:
1 2 3 4 let parseToken = (token, key ) => { let payload = JWT .verify (token, key); return JSON .stringify (payload); };
如果想要解析其中的信息,可以使用:https://jwt.io/ 这个网站
其中得到的结果为:
1 2 生成token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEsImlzcyI6ImJhaWR1LmNvbSIsImlhdCI6MTcyMzEyOTc5NiwibmJmIjoxNzIzMTI5Nzk2LCJleHAiOjE3MjMyMTYxOTYsImF1ZCI6WyIiXSwiZGF0YSI6eyJuYW1lIjoiZ2NuYW5tdSIsImdlbmRlciI6Im1hbiJ9fQ.9 XLXbBDitorJG22p7aZ64OArcTI0EM2gJgtsHVQOEUE 解析token: {"sub" :1 ,"iss" :"baidu.com" ,"iat" :1723129796 ,"nbf" :1723129796 ,"exp" :1723216196 ,"aud" :["" ],"data" :{"name" :"gcnanmu" ,"gender" :"man" }}
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 import Koa from "koa" ;import Router from "@koa/router" ;import JWT from "jsonwebtoken" ;const hostname = "127.0.0.1" ;const port = 8008 ;const app = new Koa ();const router = new Router ();router.get ("/" , (ctx ) => { const key = "koa2" ; const token = generateToken (key); ctx.status = 200 ; let result = "生成token: " + token + "\n" + "解析token: " + parseToken (token, key); ctx.body = result; }); let generateToken = (key ) => { let id = 1 ; let now = Math .floor (Date .now () / 1000 ); let expire = 24 * 60 * 60 ; let payload = { sub : id, iss : "baidu.com" , iat : now, nbf : now, exp : now + expire, aud : ["" ], data : { name : "gcnanmu" , gender : "man" , }, }; let token = JWT .sign (payload, key, { algorithm : "HS256" }); return token; }; let parseToken = (token, key ) => { let payload = JWT .verify (token, key); return JSON .stringify (payload); }; app.use (router.routes ()); app.listen (port, hostname, () => { console .log (`服务器已启动: http://${hostname} :${port} ` ); });