1.Koa
- Koa是一个由 Express 原班人马打造的,尝试在 Web 应用开发中摆脱回调地狱,并增强错误处理的轻量化的 Node.js 框架。
- Koa的目标是成为一个更小、更富表现力、更健壮的 Web 框架。
2.安装
Koa 需要 Node v12 或更高版本以支持 ES2015 和异步函数。 您可以使用您喜欢的版本管理器快速安装一个支持的 Node 版本:
nvm install 12
npm install koa
node my-koa-app.js
3.基本使用
Koa 应用是一个包含中间件函数数组的对象,这些函数在请求时以类似堆栈的方式组合并执行
在 Koa 框架中,ctx
是上下文对象,它封装了原生的 Node.js 请求和响应对象,并且提供了许多方便的方法和属性来处理 HTTP 请求和响应
ctx.req
和 ctx.res
是 Node.js 的原生请求和响应对象。这些对象提供了低级别的请求和响应处理方法
ctx.res.end('ok')
是直接调用 Node.js 原生响应对象的 end
方法来结束响应并发送字符串 ok
import Koa from "koa"
// 创建一个Koa应用实例
const app = new Koa()
app.use(function (ctx) {
// 处理请求,返回'hello'
ctx.res.end("hello")
})
// 监听3000端口,并在控制台打印服务器运行信息
app.listen(3000, () => console.log("server is running at http://localhost:3000"))
4.request&response
在 Koa 中,ctx.request
和 ctx.response
是封装了 Node.js 原生请求和响应对象(ctx.req
和 ctx.res
)的对象。相比于 Node.js 的原生请求和响应对象,ctx.request
和 ctx.response
提供了更多的方法和属性,使得处理 HTTP 请求和响应更加方便和简单。
ctx.request
:ctx.request.query
: 一个包含解析过的查询字符串的对象ctx.request.method
: 请求方法,例如 ‘GET’, ‘POST’ 等ctx.request.url
: 请求的 URLctx.request.header
: 请求头对象ctx.request.body
: 请求体(需要额外的中间件如 koa-bodyparser 来解析请求体)
ctx.response
:ctx.response.body
: 可以设置响应体的内容,它可以是一个字符串、对象或者流ctx.response.status
: 可以设置响应的 HTTP 状态码ctx.response.message
: HTTP 状态消息ctx.response.header
: 响应头对象
1.server
import Koa from "koa"
const app = new Koa()
app.use((ctx) => {
// 打印请求方法
console.log(ctx.request.method)
// 打印请求URL
console.log(ctx.request.url)
// 打印请求路径
console.log(ctx.request.path)
// 打印查询字符串参数
console.log(ctx.request.query)
// 打印请求头
console.log(ctx.request.header)
// 设置响应状态码
ctx.response.status = 200
// 设置响应消息
ctx.response.message = "OK"
ctx.response.set("Content-Type", "text/html;charset=utf-8")
// 设置响应体
ctx.response.body = "hello"
ctx.response.body = "world"
ctx.response.body = "third"
})
app.listen(3000, () => {
console.log("app runing http://localhost:3000")
})
2.server.js
const Koa = require("koa")
const app = new Koa()
app.use((ctx) => {
// 打印请求方法
console.log(ctx.method)
// 打印请求URL
console.log(ctx.url)
// 打印请求路径
console.log(ctx.path)
// 打印查询字符串参数
console.log(ctx.query)
// 打印请求头
console.log(ctx.header)
// 设置响应状态码
ctx.status = 200
// 设置响应消息
ctx.message = "OK"
ctx.set("Content-Type", "text/html;charset=utf-8")
// 设置响应体
ctx.body = "hello"
ctx.body = "world"
ctx.body = "third"
})
app.listen(3001, () => {
console.log("app runing http://localhost:3001")
})
5.中间件级联
5.1 同步中间件
const Koa = require("koa")
const app = new Koa()
const middleware1 = (ctx, next) => {
console.log(1)
next()
console.log(2)
}
const middleware2 = (ctx, next) => {
console.log(3)
next()
console.log(4)
}
const middleware3 = (ctx) => {
console.log(5)
}
app.use(middleware1)
app.use(middleware2)
app.use(middleware3)
app.listen(3000, () => console.log("server is running at http://localhost:3000"))
5.2 异步中间件
const Koa = require("koa")
const app = new Koa()
const middleware1 = async (ctx, next) => {
console.time("cost")
console.log(1)
await next()
console.log(2)
console.timeEnd("cost")
}
const middleware2 = async (ctx, next) => {
console.log(3)
await new Promise((resolve) => {
setTimeout(() => {
console.log("middleware2 sleep 1s")
resolve()
}, 1000)
})
await next()
console.log(4)
}
const middleware3 = async (ctx) => {
console.log(5)
await new Promise((resolve) => {
setTimeout(() => {
console.log("middleware3 sleep 1s")
resolve()
}, 1000)
})
ctx.body = "hello"
console.log(6)
}
app.use(middleware1)
app.use(middleware2)
app.use(middleware3)
app.listen(3000, () => console.log("server is running at http://localhost:3000"))
6. 核心实现
6.1application
import EventEmitter from "events"
import http from "http"
import context from "./context.js"
// 导入自定义的request模块
import request from "./request.js"
// 导入自定义的response模块
import response from "./response.js"
import compose from "./koa-compose.js"
export default class Application extends EventEmitter {
constructor() {
super()
this.context = Object.create(context)
this.request = Object.create(request)
this.response = Object.create(response)
this.middleware = []
}
//存储中间件
use(fn) {
this.middleware.push(fn)
return this
}
// 创建服务
listen(...args) {
const server = http.createServer(this.callback())
server.listen(...args)
}
//请求到来时候的回调函数
callback() {
//组合中间件
const fn = compose(this.middleware)
const handleRequest = (req, res) => {
//创建上下文对象
const ctx = this.createContext(req, res)
return this.handleRequest(ctx, fn)
}
return handleRequest
}
// 请求回调
handleRequest(ctx, fnMiddleware) {
const handleResponse = () => respond(ctx)
const onerror = (err) => ctx.onerror(err)
// 执行中间件 包装为 Promise 执行完成后响应 body
return fnMiddleware(ctx).then(handleResponse).catch(onerror)
}
createContext(req, res) {
//创建上下文对象
const context = Object.create(this.context)
// 创建一个新的request对象,这个对象继承自this.request,并将这个对象赋值给context.request
const request = (context.request = Object.create(this.request))
// 创建一个新的response对象,这个对象继承自this.response,并将这个对象赋值给context.response
const response = (context.response = Object.create(this.response))
context.req = request.req = req
context.res = response.res = res
//返回上下文对象
return context
}
}
// 响应 body
function respond(ctx) {
let { res, body } = ctx
if (Buffer.isBuffer(body)) return res.end(body)
if (typeof body === "string") return res.end(body)
if (body instanceof Stream) return body.pipe(res)
res.end(JSON.stringify(body))
}
export { EventEmitter }
6.2 request
const parse = require("parseurl")
const qs = require("querystring")
module.exports = {
get url() {
return this.req.url
},
get path() {
//把url路径转成对象,pathname是它的路径名 /a/b 参数req: IncomingMessage
return parse(this.req).pathname
},
get method() {
return this.req.method
},
//查询字符串,它的格式是一个字符串 ?a=1&b=2
get querystring() {
return parse(this.req).query
},
//它会调用qs.parse方法把查询字符串从字符串转成对象
get query() {
return qs.parse(this.querystring)
},
get header() {
return this.req.headers
},
get headers() {
return this.header
},
}
6.3response
module.exports = {
//设置状态码 response.status = 200;
set status(code) {
//把状态码code透传给原生的res响应对象
this.res.statusCode = code
},
set message(msg) {
//给响应状态码的原因短语赋值
this.res.statusMessage = msg
},
set body(value) {
//当调用response.body = xxx的时候,会把xxx暂存到response._body上
this._body = value
//一旦调用了res.end方法,则不能再次写入响应了
//this.res.end(value);
},
get body() {
return this._body
},
//set用来设置响应头
set(field, value) {
//调用原生的响应对象的setHeader方法,设置字符和值
this.res.setHeader(field, value)
},
}
6.4 context
const delegate = require("./delegates")
//创建一个空对象并将其导出
const proto = (module.exports = {
onerror(err) {
const { res } = this
this.status = 500
res.end(err.message)
},
})
//使用代理模块将proto对象的一些属性代理到request对象上 proto.url=>proto.request.url
delegate(proto, "request")
.access("method") //将request对象上method属性代理到proto对象上 access能读又能写
.access("query")
.access("url")
.access("path")
.getter("header") //只能读不能写
//使用代理模块将proto对象的一些属性代理到response对象上 proto.body=>proto.response.body
delegate(proto, "response").access("status").access("message").access("body").method("set") //将response对象上的set方法代理到proto对象上
6.5delegates
function Delegator(proto, target) {
if (!(this instanceof Delegator)) {
return new Delegator(proto, target)
}
this.proto = proto
this.target = target
}
Delegator.prototype.getter = function (name) {
const { proto, target } = this
Object.defineProperty(proto, name, {
get() {
return this[target][name]
},
configurable: true,
})
return this
}
Delegator.prototype.setter = function (name) {
const { proto, target } = this
Object.defineProperty(proto, name, {
set(val) {
this[target][name] = val
},
configurable: true,
})
return this
}
Delegator.prototype.access = function (name) {
return this.getter(name).setter(name)
}
Delegator.prototype.method = function (name) {
const { proto, target } = this
proto[name] = function () {
return this[target][name].apply(this[target], arguments)
}
return this
}
module.exports = Delegator
6.6 Koa-compose
/**
* 组合中间件函数,返回一个可以处理上下文的函数
*
* @param {Array} middleware - 中间件函数数组
* @returns {Function} 处理上下文的函数
*/
function compose(middleware) {
// 返回一个函数,该函数接收一个context参数
return function (context) {
let index = -1
// 定义一个dispatch函数,用于控制中间件的执行顺序
function dispatch(i) {
// 如果已经执行过dispatch(i),则返回一个错误
if (i <= index) return Promise.reject(new Error("next() called multiple times"))
// 更新index的值
index = i
// 获取当前中间件函数
let fn = middleware[i]
// 如果中间件函数不存在,则返回一个已解决的Promise
if (!fn) return Promise.resolve()
try {
// 执行中间件函数,并传入context和下一个中间件函数
return Promise.resolve(fn(context, () => dispatch(i + 1)))
} catch (err) {
return Promise.reject(err)
}
}
// 开始执行中间件函数
return dispatch(0)
}
}
module.exports = compose
6.7 cookie 的设置和获取
context.js
const Cookies = require("cookies")
const COOKIES = Symbol("context#cookies")
const proto = (module.exports = {
// ........
get cookies() {
if (!this[COOKIES]) {
this[COOKIES] = new Cookies(this.req, this.res, {
keys: this.app.keys,
secure: this.request.secure,
})
}
return this[COOKIES]
},
set cookies(_cookies) {
this[COOKIES] = _cookies
},
//........
})
cookies.js
function Cookies(req, res) {
this.req = req
this.res = res
}
//用来写cookie,通过响应头的Set-Cookie
//Set-Cookie:name=name_value; path=/; httponly 每个set-cookie只能写入一个cookie
Cookies.prototype.set = function (name, value, attrs) {
//获取响应头中的Set-Cookie的值
const headers = this.res.getHeader("Set-Cookie") || []
//根据name和value创建一个新的cookie
let cookie = new Cookie(name, value, attrs)
//把新的cookie加入到数组中
headers.push(cookie.toHeader())
//写回响应头
this.res.setHeader("Set-Cookie", headers)
}
//用来读cookie,通过请求头中cookie
//请求头 Cookie:name=zhufeng; age=18
Cookies.prototype.get = function (name) {
//name=name_domain
//获取客户端发送过来的cookie
let cookie = this.req.headers.cookie || ""
return getValueFromHeader(name, cookie)
}
function getValueFromHeader(name, cookie) {
//如果客户端根本没有传cookie过来,不用找了,直接返回
if (!cookie) return
//name=name_value; name_domain=; name_path=name_path_value
let regexp = new RegExp("(?:^|;) *" + name + "=([^;]*)") //name_domain=([^;]*)
let match = cookie.match(regexp)
if (match) {
return match[1]
}
}
function Cookie(name, value, attrs) {
this.name = name
this.value = value
//把用户传过来的cookie选项存放到cookie类的实例上等待被使用 path httpOnly
for (let name in attrs) {
this[name] = attrs[name]
}
}
Cookie.prototype.toString = function () {
return this.name + "=" + this.value
}
Cookie.prototype.toHeader = function () {
let header = this.toString()
if (this.path) header += `; path=` + this.path
if (this.maxAge) this.expires = new Date(Date.now() + this.maxAge)
if (this.expires) header += `; expires=` + this.expires.toUTCString()
if (this.domain) header += `; domain=` + this.domain
if (this.httpOnly) header += `; httpOnly`
console.log(header)
return header
}
module.exports = Cookies
注意 : 可以对 cookie 进行签名
源码请看:https://www.npmjs.com/package/cookies?activeTab=code
7. 常见中间件的使用
import { fileURLToPath } from "url"
import path from "path"
import serve from "koa-static"
import Router from "koa-router"
import bodyParser from "koa-bodyparser"
import views from "koa-views"
import Koa from "koa"
import multer from "@koa/multer"
const app = new Koa()
const router = new Router()
// 模拟 __dirname
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
// 配置 multer,用于处理 multipart/form-data(文件上传)
const upload = multer({ dest: "uploads/" })
// 1. 提供静态资源服务
app.use(serve(path.resolve(__dirname, "public")))
app.use(serve(path.resolve(__dirname)))
// 2. 使用 bodyparser 解析请求体
app.use(bodyParser()) // ctx.request.body = 请求体
// 3. 记录访问的时间
app.use(async function (ctx, next) {
console.time("ok")
await next()
console.timeEnd("ok")
})
// 4. 错误捕获
app.use(async function (ctx, next) {
try {
await next()
} catch (err) {
console.error(err)
ctx.status = err.status || 500
ctx.body = { message: "Internal Server Error" }
}
})
// 5. 设置模板引擎
app.use(
views(path.resolve(__dirname, "views"), {
extension: "ejs", // 确保渲染引擎为 ejs
})
)
// 6. 使用路由系统
app.use(router.routes())
app.use(router.allowedMethods()) // 推荐:允许 HTTP 方法控制
// 上传文件的路由,同时传递参数
router.post("/login", upload.array("file", 2), async (ctx) => {
const files = ctx.files // 获取上传的文件
const params = ctx.request.body // 获取表单参数
// console.dir("file=>", file.filename)
ctx.body = {
message: "File uploaded successfully",
fileDetails: files,
formData: params,
}
})
router.get("/user", async (ctx) => {
await ctx.render("user", { name: "zs" })
})
// 启动服务器
app.listen(3000, () => {
console.log("Server started on http://localhost:3000")
})
1. koa-router
- 功能:用于处理路由。允许定义路径和处理请求的方法,如 GET、POST 等。
- 使用场景:创建 API 时进行路由控制。
2. koa-bodyparser
- 功能:解析请求体,用于处理 POST 请求中的 JSON、表单等数据。
- 使用场景:解析
POST
请求的请求体内容,尤其是 JSON 格式的数据。
3. koa-static
- 功能:提供静态文件服务,用于提供诸如 HTML、CSS、JS、图片等文件。
- 使用场景:当你需要为前端应用提供静态资源时,如图片或静态网页。
4. koa-multer
- 功能:用于处理文件上传。
- 使用场景:当应用需要处理用户上传的文件时。
5. koa-session
- 功能:用于会话管理,存储和管理用户会话信息。
- 使用场景:需要对用户登录状态进行管理。
6. koa-jwt
- 功能:用于基于 JWT 的身份验证,解析请求中的 JWT token。
- 使用场景:用于保护路由,需要对用户身份进行认证。
7 koa-view
- 功能:用于在服务器端渲染 HTML 模板,可以动态生成页面
- 使用场景:需要服务端渲染动态内容的场景,比如渲染用户数据、生成 HTML 电子邮件等
- EJS:简单、易用,适合小项目。
- Pug:简洁的缩进式语法,适合中大型项目。
- Nunjucks:功能强大,支持高级特性,适合复杂应用。
- Handlebars:轻量化,适合生成静态页面。
8. 中间件实现
1.koa-router
const methods = ["get", "head", "options", "put", "patch", "post", "delete"]
function Router() {
this.stack = [] //stack里存放路由规则
}
for (const method of methods) {
Router.prototype[method] = function (path, middleware) {
//把请求的方法,请求的路径以及对应的请求处理中间件函数包装成一个对象并放入stack数组
this.stack.push({ path, method, middleware })
}
}
Router.prototype.routes = function () {
//这个才是真正用来处理请求的中间件函数
return async (ctx, next) => {
//在stack数组中找到一个元素,那个元素path和当前的请求的路径相同,它的method和请求的方法名相同
const matchedLayer = this.stack.find((layer) => {
return layer.path === ctx.path && layer.method === ctx.method.toLowerCase()
})
if (!matchedLayer) {
return await next()
}
await matchedLayer.middleware(ctx, next)
}
}
module.exports = Router
2.koa-bodyparser
- 包含解析
application/x-www-form-urlencoded
和application/json
和文件上传multipart/form-data
原理
import querystring from "querystring"
import fs from "fs/promises"
import { v4 } from "uuid"
import path from "path"
const starts = {
"application/x-www-form-urlencoded"(content) {
return querystring.parse(content.toString())
},
"application/json"(content) {
return JSON.parse(content.toString())
},
}
Buffer.prototype.split = function (sep) {
let headerLines = []
sep = Buffer.isBuffer(sep) ? sep : Buffer.from(sep)
let offset = 0
let index = 0
while (-1 !== (index = this.indexOf(sep, offset))) {
headerLines.push(this.slice(offset, index))
offset = index + sep.length
} // str.indexOf(分隔符,查找的位置)
headerLines.push(this.slice(offset)) // 分割后的最后一部分也放到数组里
return headerLines
}
async function formData(content, boundary, uploadDir) {
let headerLines = content.split("--" + boundary)
headerLines = headerLines.slice(1, -1)
const result = {}
await Promise.all(
headerLines.map(async (line) => {
let [head, ...content] = line.split("\r\n\r\n") // type 和 内容之间是两个换行
let contentType = head.toString()
let name = contentType.match(/name="(.+?)"/)[1]
if (contentType.includes("Content-Type")) {
content = Buffer.concat(content).slice(0, -2)
const filename = v4()
await fs.writeFile(path.join(uploadDir, filename), content)
let file = {
originalFilename: contentType.match(/filename="(.+?)"/)[1],
type: contentType.match(/Content-Type: ([^\r\n]+)/)[1],
size: content.length,
filename,
}
result[name] = result[name] || []
result[name].push(file)
} else {
result[name] = Buffer.concat(content).toString().slice(0, -2)
}
})
)
return result
}
function bodyParser({ uploadDir }) {
return async (ctx, next) => {
// 解析请求体,将结果 给 赋值到ctx.request.body
await new Promise((resolve, reject) => {
const arr = []
ctx.req.on("data", function (chunk) {
arr.push(chunk)
})
ctx.req.on("end", function (chunk) {
let type = ctx.get("Content-Type")
// 给上下文中的request自定一个body属性
if (type) {
const content = Buffer.concat(arr)
if (type.includes("multipart/form-data")) {
// 表单内容, 表单的分割符号
formData(content, type.split("=")[1], uploadDir) // 将文件上传到某个目录中
// 信息保存到 ctx.request.body
ctx.request.body = {}
} else {
ctx.request.body = starts[type](content)
}
}
resolve()
})
})
return next()
}
}
export default bodyParser
3.koa-static
import path from "path"
import { stat } from "fs/promises"
import { createReadStream } from "fs"
import mime from "mime"
export default function serve(dirname) {
return async (ctx, next) => {
//先向后执行
await next()
let filePath = path.join(dirname, ctx.path)
try {
const statObj = await stat(filePath)
if (statObj.isFile()) {
ctx.set("Content-Type", mime.getType(filePath) || "text/plain" + ";charset=utf-8")
ctx.body = createReadStream(filePath)
} else {
return next()
}
} catch (e) {
return next()
}
}
}
4. koa-session
function generateKoaSession() {
return Math.random() + "" + Math.random() + "" + Date.now() + "" + Math.random()
}
function session() {
//服务端会有一段内存用来保存每个用户的数据
//用户对就在的会话数据就放在这个对象里
const sessionStorage = {}
return async function (ctx, next) {
//获取用户传递过来的koa.sess
let koasess = ctx.cookies.get("koa.sess")
//如果没有传递,说明一个新的
if (!koasess) {
//生成一个新的koasess
koasess = generateKoaSession()
sessionStorage[koasess] = {}
//通过cookie把这个新的卡号发给或者说种植到客户端
ctx.cookies.set("koa.sess", koasess, { httpOnly: true })
}
ctx.session = sessionStorage[koasess]
await next()
}
}
module.exports = session