Koa 学习笔记
本期内容主要为
koa
基础与源码学习,后续会开一期koa
项目实战。本文基础部分目录结构按照阮一峰老师的koa
教程(有作修动)。望本文能对您有所帮助!☀️
-
前置基础
-
node
基础 -
HTML,JavaScript (ES6)
和客户端服务器工作原理等 - 了解或学习过
Express
或Egg
框架
-
-
本文参考(特别鸣谢❤️)
1. koa 概念
- 官方是这么说的,
koa
是由Express
原班人马打造的,致力于成为一个更小、更富有表现力、更健壮的Web
框架。使用koa
编写web
应用,通过组合不同的generator
,可以免除重复繁琐的回调函数嵌套,并极大地提升错误处理的效率。koa
不在内核方法中绑定任何中间件,它仅仅提供了一个轻量优雅的函数库,使得编写Web
应用变得得心应手。
2. 安装
$ npm i koa
# 需要 node v7.6.0 或更高版本
3. koa 基础入门
3.1 架设 HTTP 服务
const Koa = require('koa');
// Koa K 要大写 因为导入包是 koa 是类
// 创建一个 Koa 的实例 app 对象作为 Koa 应用
const app = new Koa();
// 监听端口 3000
app.listen(3000);
// node xxx.js 或者 nodemon xxx.js (nom install -g nodemon)
- 访问
http://127.0.0.1:3000
后发现页面显示 “Not Found”,表示没有发现任何内容。
3.2 Context 对象
Koa
提供一个Context
对象,称为上下文,其中包括HTTP
请求和响应。通过加工这个对象,就可以控制返回给用户的内容。Koa Context
将node
中原生的request
和response
对象封装到单个对象ctx
,为编写Web
应用程序和API
提供许多有用的方法。- 注意:每个请求都将创建一个
context
,并在中间件中作为参数引用(后续说明)。
const Koa = require('koa');
const app = new Koa();
// ctx 是 content(上下文) 的缩写 上(request)下(response)沟通的环境
// koa 中把他们两都封装进了 ctx 对象
const main = ctx => {
// ctx.body = 'Hello World'
ctx.response.body = 'Hello World'
}
// main 方法用于设置 ctx.response.body
app.use(main);
// app.use 方法用于加载 main 函数
app.listen(3000);
// 然后页面显示 Hello World
- 可以推测
ctx.response
代表HTTP Response
,那么ctx.request
代表HTTP Request
。
3.3 HTTP Response 的类型
-
Koa
默认的返回类型是text/plain
-
如果想返回其他类型的内容,可以先用
ctx.request.accepts
判断一下,客户端希望接受什么数据(根据HTTP Request
的Accept
字段),然后使用ctx.response.type
指定返回类型。const main = ctx => { if (ctx.request.accepts('xml')) { ctx.response.type = 'xml'; ctx.response.body = '<data>Hello World</data>'; } else if (ctx.request.accepts('json')) { ctx.response.type = 'json'; ctx.response.body = { data: 'Hello World' }; } else if (ctx.request.accepts('html')) { ctx.response.type = 'html'; ctx.response.body = '<p>Hello World</p>'; } else { ctx.response.type = 'text'; ctx.response.body = 'Hello World'; } };
3.4 网页模板
- 实际开发中,返回给用户的网页往往都写成模板文件。我们可以让
Koa
先读取模板文件,然后将这个模板返回给用户。
/*
03-template.html
...
<h1>Hello World</h1>
...
*/
// 系统模块 fs
const fs = require('fs')
const main = ctx => {
// 指定上下文返回类型
ctx.response.type = 'html'
// 设置响应体内容
// ctx.body
ctx.response.body = fs.createReadStream('./03-template.html')
}
const Koa = require('koa')
const app = new Koa();
app.use(main)
app.listen(3000)
- 访问
http://127.0.0.1:3000
后发现页面显示Hello World
3.5 路由
3.5.1 原生路由
- 网站一般都有多个页面。通过
ctx.request.path
或ctx.path
可以获取用户请求的路径,由此实现简单的路由。
const Koa = require('koa')
const app = new Koa()
const main = (ctx) => {
// 访问路由 url 不为 '/' 页面显示链接,点击后就跳到首页显示(Hello World)。
if (ctx.request.path != '/') {
ctx.response.type = 'html'
ctx.response.body = '<a href="/">Index Page</a>'
} else {
// ctx.request.path = '/'
ctx.response.body = 'Hello World';
}
}
app.use(main)
app.listen(3000)
- 访问
http://127.0.0.1:3000
页面显示Hello World
,访问http://127.0.0.1:3000/about
页面显示Index Page
超链接,点击后跳转至根路由显示Hello World
。
3.5.2 koa-route 模块
- 了解过
Express
朋友知道,在Express
中直接引入Express
就可以配置路由,Koa
中的路由和Express
有所不同,在Koa
中我们需要安装对应的koa-router
路由模块来实现。 - 使用封装的
koa-route
模块。
// Module Not Found ==> npm install koa-route
const Koa = require('koa')
// 导入路由模块
const route = require('koa-route')
const app = new Koa()
const main = ctx => {
ctx.response.body = 'Hello World';
}
const about = ctx => {
ctx.response.type = 'html';
ctx.response.body = '<a href="/">Index Page</a>';
}
// 根路径 '/' 的处理函数是 main
app.use(route.get('/', main))
// '/about' 路径的处理函数是 about
app.use(route.get('/about', about))
// 结果跟 3.5.1 相同
app.listen(3000)
3.6 静态资源
- 如果网站提供静态资源(图片、字体、样式表、脚本…),为它们一个个写路由就很麻烦,也没必要。
koa-static
模块封装了这部分的请求。
// module not found ==> npm install koa-static
// 06-static.js
const Koa = require('koa')
const app = new Koa()
const path = require('path')
const serve = require('koa-static')
const main = serve(path.join(__dirname))
app.use(main)
app.listen(3000)
- 访问
localhost:3000/06-static.js
,在浏览器里就可以看到这个脚本的内容。
3.7 重定向
- 有些场合,服务器需要重定向(
redirect
)访问请求。 - 例如:用户登陆以后,将他重定向到登陆前的页面。使用
ctx.response.redirect()
方法可以发出一个 302 跳转,将用户导向另一个路由。
const Koa = require('koa')
const app = new Koa()
const route = require('koa-route')
const redirect = ctx => {
ctx.response.redirect('/')
}
const main = ctx => {
ctx.response.body = 'Hello World'
}
app.use(route.get('/', main))
// 访问 '/redirect' 重定向至 '/'
app.use(route.get('/redirect', redirect))
app.listen(3000)
- 访问
http://127.0.0.1:3000/redirect
,浏览器会将用户导向根路由。
4. 中间件
- 在上述学习示例中,我们已经不知不觉使用中间件!
4.1 什么是 Koa 中间件
Koa
的中间件(Middleware
) 是一个函数,是在请求和响应中间的处理程序,它可以通过ctx
对象访问请求对象和响应对象。比如,在处理请求中,可以在响应之前,可以在请求和响应之间做一些操作,并且可以将这个处理结果传递给下一个函数继续处理。
4.2 Logger 功能
Logger
作为Koa
的最大特色,也是最重要的一个设计,就是中间件。为了理解中间件,我们先看一下Logger
打印日志功能的实现。
const Koa = require('koa')
const app = new Koa()
const main = ctx => {
console.log(`ctx.request.method = ${ctx.request.method}`)
console.log(`ctx.request.url = ${ctx.request.url}`)
ctx.response.body = 'Hello World'
}
app.use(main)
app.listen(3000)
// ctx.request.method = GET
// ctx.request.url = /
4.2 中间件的基本使用
- 在上一个例子里面的
Logger
功能,可以拆分成一个独立函数
const Koa = require('koa')
const app = new Koa()
const logger = (ctx, next) => {
console.log(`ctx.request.method = ${ctx.request.method}`)
console.log(`ctx.request.url = ${ctx.request.url}`)
// 如果不调用 next() 无法把把执行权转交给下一个中间件 main 导致页面显示 Not Found
next()
}
const main = ctx => {
ctx.response.body = 'Hello World'
}
app.use(logger)
app.use(main)
app.listen(3000)
// 结果跟 4.1 一样
- 中间件处在
HTTP Request
和HTTP Response
中间,用来实现某种中间功能。app.use()
用来加载中间件。 - 基本上,
Koa
所有的功能都是通过中间件实现的,之前示例里面的main
也是中间件。 - 每个中间件默认接受两个参数,第一个参数是
Context
对象,第二个参数是next
函数。只要调用next
函数,就可以把执行权转交给下一个中间件。
4.3 中间件栈
4.3.1 洋葱模型
-
在上图中,我们可以发现洋葱切面的每一层都表示一个独立的中间件,用于实现不同的功能。每次请求都会从最外层开始一层层地经过每层的中间件,当进入到最里层的中间件之后,就会从最里层的中间件开始逐层返回。
-
数据结构
- 多个中间件会形成一个栈结构(
middle stack
),以先进后出(first-in-last-out
)的顺序执行。
- 多个中间件会形成一个栈结构(
-
流程
① 最外层的中间件首先执行 |
---|
② 调用 next 函数,把执行权转交给下一个中间件 |
③ … |
④ 最内层的中间件最后执行 |
⑤ 执行结束后,把执行圈交回上一层的中间件 |
⑥ … |
⑦ 最外层的中间件收回执行权之后,执行 next 函数后面的代码 |
const Koa = require('koa')
const app = new Koa()
const one = (ctx, next) => {
console.log('>> one')
next()
console.log('<< one')
}
const two = (ctx, next) => {
console.log('>> two')
next()
console.log('<< two')
}
const three = (ctx, next) => {
console.log('>> three')
next()
console.log('<< three')
}
app.use(one)
app.use(two)
app.use(three)
app.listen(3000)
// >> one
// >> two
// >> three
// << three
// << two
// << one
-
注意:如果中间件内部没有调用
next
函数,那么执行权就不会传递下去。 -
如果将
two
函数里面next()
这一行注释掉再执行。
// 结果:
// >> one
// >> two
// << two
// << one
// 也就是说中间件内部没有调用 next 函数,two 作为中间件不会把执行权传递给 three
4.4 异步中间件
- 迄今为止,所有例子的中间件都是同步的,不包含异步操作。如果有异步操作(比如读取数据库,读取文件),中间件就必须写成
async
函数。
// npm i fs.promised
const fs = require('fs.promised');
const Koa = require('koa');
const app = new Koa();
const main = async function (ctx, next) {
ctx.response.type = 'html';
ctx.response.body = await fs.readFile('03-template.html', 'utf8');
};
app.use(main);
app.listen(3000);
// 访问 localhost:3000 页面显示模板文件的内容 ( Hello World )
// fs.readFile 是一个异步操作,必须写成 await fs.readFile(),然后中间件必须写成 async 函数。
4.5 中间件的合成
koa-compose
模块可以将多个中间件合成为一个。
const compose = require('koa-compose');
const Koa = require('koa');
const app = new Koa();
const logger = (ctx, next) => {
console.log(`ctx.request.method = ${ctx.request.method}`);
console.log(`ctx.request.url = ${ctx.request.url}`);
next() // 注释掉 main 函数不会加载,界面显示 Not Found
}
const main = ctx => {
ctx.response.body = 'Hello World';
}
const middlewares = compose([logger, main])
app.use(middlewares);
app.listen(3000);
5. 错误处理
5.1 500 错误
- 如果代码运行过程中发生错误,我们需要把错误信息返回给用户。
HTTP
协定约定这时要返回500状态码。Koa
提供了ctx.throw()
方法,用来抛出错误。ctx.throw(500)
就是抛出 500 错误。
const Koa = require('koa')
const app = new Koa()
const main = ctx => {
ctx.throw(500)
}
app.use(main)
app.listen(3000)
// 访问 localhost:3000 页面显示 Internal Server Error
5.2 404 错误
- 如果将
ctx.response.status
设置成 404,就相当于ctx.throw(404)
,返回 404 错误。
const Koa = require('koa')
const app = new Koa()
const main = ctx => {
ctx.response.status = 404
ctx.response.body = '404 Not Found'
};
app.use(main)
app.listen(3000)
// 访问 http://127.0.0.1:3000,可以看到一个 404 页面 "404 Not Found"。
5.3 处理错误的中间件
- 为了方便处理错误,最好使用
try...catch
将其捕获。但是,为每个中间件都写try...catch
过于麻烦,我们可以让最外层的中间件,负责所有中间件的错误处理。(3.3 中间件栈)
const Koa = require('koa')
const app = new Koa()
const handler = async (ctx, next) => {
try {
await next()
} catch (err) {
ctx.response.status = err.statusCode || err.status || 500;
ctx.response.body = {
message: err.message
};
}
};
const main = ctx => {
ctx.throw(500)
};
app.use(handler)
app.use(main)
app.listen(3000)
// 访问 http://127.0.0.1:3000 ,你会看到一个500页面
// 显示报错提示 {"message":"Internal Server Error"}
5.4 error 事件监听与触发
- 运行过程中一旦出错,
Koa
会触发一个error
事件。监听这个事件,也可以处理错误。
const Koa = require('koa')
const app = new Koa()
const main = ctx => {
ctx.throw(500)
}
app.on('error', err => {
console.error(err)
// InternalServerError: Internal Server Error
})
app.use(main)
app.listen(3000)
// 访问 http://127.0.0.1:3000,页面显示 Internal Server Error
- 如果错误被
try...catch
捕获,就不会触发error
事件。此时必须调用ctx.app.emit()
手动触发error
事件,才能让监听函数生效。
const Koa = require('koa')
const app = new Koa()
const handler = async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.response.status = err.statusCode || err.status || 500
ctx.response.type = 'html'
ctx.response.body = '<p>Something wrong</p>'
ctx.app.emit('error', err, ctx) // 注释掉就无法监听 error 事件
}
}
const main = ctx => {
ctx.throw(500)
}
app.on('error', function(err) {
console.log(err.message) // Internal Server Error
})
app.use(handler)
app.use(main)
app.listen(3000)
6. Web App 功能
6.1 Cookies
ctx.cookies
用来读写Cookie
。
const Koa = require('koa');
const app = new Koa()
const main = function (ctx) {
// ctx.cookies.get(views) 返回 string 类型
const n = Number(ctx.cookies.get('view') || 0) + 1
ctx.cookies.set('view', n)
ctx.response.body = n + ' views'
}
app.use(main)
app.listen(3000)
// 访问 http://127.0.0.1:3000,可以看到 1 views。刷新一次页面,就变成了 2 views。再刷新,每次都会计数增加 1。
6.2 表单
Web
应用离不开处理表单。本质上,表单就是POST
方法发送到服务器的键值对。koa-body
模块可以用来从POST
请求的数据体里面提取键值对(key-value
)。
// npm install koa-body
const Koa = require('koa')
const { koaBody } = require('koa-body')
const app = new Koa()
const main = async function (ctx) {
ctx.body = ctx.request.body
console.log(ctx.body);
// { name: jack }
}
app.use(koaBody()) // 在所有中间件之前注册
app.use(main)
app.listen(3000)
- 打开另一个命令行窗口,需要使用
curl
命令。 - curl 教程
# linux 下运行
curl -X POST –data "name=Jack" http://localhost:3000
# Window 下运行
curl -Uri '127.0.0.1:3000' -Body 'name=Jack' -Method 'POST'
# Postman ① 选择 POST Body ② 设置 x-www-form-urlencoded ③ key 设置 name ④ value 设置 jack
6.3 文件上传
koa-body
模块还可以用来处理文件上传。koa-body
更多用法
6.4 koa 更多模块使用
7. koa 源码目录解析
powershell
中使用命令
npm install koa # 安装 koa
node_modules
目录下找到koa
目录
-
koa/lib
application.js
(作为koa
的入口文件,封装了context
,request
,response
,以及最核心的中间件处理流程)context.js
(处理应用上下文)request.js
(扩展处理请求)response.js
(扩展处理响应)
-
示例回顾
// koa_learn.js
const Koa = require('./koa')
// 实例化 Koa 对象(app)创建一个应用
const app = new Koa() // application.js 中实现
// main 中间件函数
const main = async (ctx, next) => {
ctx.body = '你好'
await next()
}
/*
代码测试使用
const main = (req, res) => {
res.end('ok')
}
*/
// 注册中间件函数并使用
app.use(main)
// 监听 3000 端口
app.listen(3000, () => {
console.log('server start at http://localhost:3000')
})
// nodemon koa_learn.js
- 上述示例中,至少我们需要实现以下目标
new Koa()
app.use()
app.listen()
ctx
对象- 中间件(洋葱模型)
- 异常处理
- …
8. 手写 koa(简易版)
8.1 基本结构搭建
mkdir koa
npm init -y # 生成 package.json
mkdir lib # 进入 lib 目录(cd lib)
touch xxx.js # (lib 目录下四个文件)
package.json
中设置入口文件
{
// todo...
"main": "./lib/application.js"
// todo...
}
8.2 application.js 基本结构搭建
- 声明一个类
Application
use
方法:中间件注册函数listen
方法:封装http.createServer
,处理请求及响应,并且监听端口
// 用于 new Koa()
// koa_learn.js 中 修改 require('./koa')
class Application {
/*
const main = (ctx,next) => {
ctx.body = 'xxx'
// 类似于 express 的 res.end('xxx')
}
对应 app.use(main) 传入一个函数
*/
use(fn) {
}
/*
const port = 3000
app.listen(port, () => {
console.log(`server on http://localhost:${port}`)
})
*/
listen(fn) {
}
}
module.exports = Application
listen
监听端口需要启动node
服务koa
默认对node
原生的http
服务进行封装
const http = require('http')
class Application {
// todo...
listen(fn) {
// 创建一个 HTTP 服务器
http.createServer()
/*
example:
http.createServer((req,res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
data: 'Hello World!'
}));
}).listen(3000)
*/
}
}
module.exports = Application
- 避免
http.createServer
中回调形式,封装一个方法handleRequest
,用于处理请求
// todo...
handleRequest() {
console.log(this)
}
listen() {
// 创建一个 HTTP 服务器
http.createServer(this.handleRequest)
}
// todo...
handleRequest
中this
不是总是指向Application
// 验证
const http = require('http')
class Application {
handleRequest() {
console.log(this);
}
listen() {
let server = http.createServer(this.handleRequest)
server.listen(...arguments)
}
}
const app = new Application()
app.handleRequest()
// 刷新页面
// Application {}
// Server {}
- 将
handleRequest
改为箭头函数形式避免this
指向问题
// todo...
handleRequest = () => {
console.log(this);
}
listen() {
let server = http.createServer(this.handleRequest)
server.listen(...arguments)
}
// todo...
- 至少目前可以跑起来!
app.use() 与 handleRequest()
// todo...
// app.use() 注册(加载)中间件函数
app.use(fn) {
// 保存中间件函数
this.fn = fn
}
// todo...
handleRequest = (req, res) => {
// 执行中间件函数
this.fn(req, res)
}
// todo...
8.3 context 上下文实现
koa
中间件函数中第一个参数ctx
是koa
封装的一个对象,内部包含了node
中http
模块中原生的req
和res
// koa_learn.js
const Koa = require('koa')
// todo...
const main = ctx => {
console.log(ctx)
ctx.body = '你好'
}
// todo...
/*
{
request: {
method: 'GET',
url: '/',
header: {
host: 'localhost:3000',
connection: 'keep-alive',
//...
accept: '...',
cookie: '...',
}
},
response: {
status: 404,
message: 'Not Found',
header: '...'
},
req: 'orginal node req'
res: 'orginal node res',
app originalUrl socket ...
}
*/
-
ctx.req
与ctx.request
区别 -
前者是
context
提供的node
原生Http
请求对象,后者是context
是koa
封装的响应对象,且ctx.request
对象可以获取原生req
对象 -
当访问
http://localhost:3000/users?id=1
,ctx.request.path
返回的是/user
,而ctx.req.url
返回的是/user?id=1
- 默认访问
ctx.path
属性会被代理访问ctx.request.path
属性
- 默认访问
// todo...
const main = ctx => {
// 基本 ctx 实现围绕以下属性展开 [需要特别留意]
console.log(ctx.req.path) // undefined
console.log(ctx.request.path) // /user
console.log(ctx.path) // /user
console.log(ctx.req.url) // /user
console.log(ctx.request.req.url) // /user
// todo...
}
// todo...
context.js request.js response.js
基本结构搭建
context
对象
// context.js 基本结构搭建
const context = {}
module.exports = ctx
request
对象
const request = {}
module.exports = request
response
对象
const response = {}
module.exports = response
- 注意:
- 每次请求的上下文
ctx
是独立的 - 每个创建的应用
app
对象所使用的上下文是独立的
- 每次请求的上下文
// application.js 部分代码
const context = require('./context')
const request = require('./request')
const response = require('./response')
class Application {
constructor() {
// 原型和原型链知识要掌握好来!
// console.log(this.context.__proto__ == context) // true
// [保证创建的 app 对象所使用的上下文是独立的]
this.context = Object.create(context)
this.request = Object.create(request)
this.response = Object.create(response)
}
handlerRequest = (req, res) => {
// [每次请求上下文是独立的]
// 编写一个创建上下文方法 createContext
let ctx = this.createContext(req, res)
this.fn(ctx)
}
createContext(req, res) {
// console.log(ctx.__proto__.__proto__ == context) // true
let ctx = Object.create(this.context)
let request = Object.create(this.request)
let response = Object.create(this.response)
// ctx.req 原生 req 对象 ctx.req = req
// ctx.req 和 ctx.request.req 是相同的
ctx.request = request // 自定义封装
ctx.request.req = ctx.req = req // 原生
// ctx.res 原生 res 对象 同理
ctx.response = response
ctx.response.res = ctx.res = res
return ctx
}
}
- 输出
undefined
原因
let request = Object.create(this.request)
ctx.request = request
// request 目前是空文件 当然是 undefined 啦
// ctx.path 没有设置
request.js
设置
const request = {
url: '/xxx'
// [原型链查找机制]
// 但是 url 是动态获取的,不能写死
}
8.3.1 委托模式
Koa
使用委托模式,把外层暴露的自身对象将请求委托给内部的node
原生对象进行处理。url
设置- 访问
ctx.request.url
实际上访问ctx.req.url
- 访问
path
和query
等属性设置同理
const url = require('url')
const qs = require('querystring');
const request = {
// getter 读取会调用
get url() {
// 为了在取值时能够快速获取 [ 原生的 req ] 所以在 request 身上加上 req 属性
return this.req.url
},
get path() {
// 解析出根路由
let { pathname } = url.parse(this.req.url)
return pathname
},
get query() {
// 解析参数
let { query } = url.parse(this.req.url, true)
// 对象 --> 字符串
return qs.stringify(query)
}
}
context.js
设置- 访问
ctx.path
实际上访问ctx.request.path
- 访问
const ctx = {
get path() {
// this 指向 ctx
// 不够完美 proxy? 函数?
return this.request.path
}
}
module.exports = ctx
koa
使用delegate
处理- 其中使用
Object.prototype.__defineGetter
可以进行数据劫持,但该特性已弃用,一些浏览器仍然支持,有着较好的兼容性 - 我们可以使用
Object.defineProperty
和Proxy
进行数据劫持,下述仍通过__defineGetter__
实现
- 其中使用
const context = {
}
// 请注意该方法是非标准的
// defineGetter('request','path')
function defineGetter(target, key) {
/*
例如:ctx.path ==> ctx.request.path
ctx.url ==> ctx.request.url
ctx.query ==> ctx.request.query
*/
context.__defineGetter__(key, function() {
// 相当于 defineProperty getter 方法 当然也可以使用 defineProperty 实现,不过我们还是按照源码的实现方式来
// this 指向 ctx
// 例如 ctx.request.url
// console.log(this.__proto__.__proto__ == context); // true
return this[target][key]
})
}
defineGetter('request', 'path')
defineGetter('request', 'url')
defineGetter('request', 'query')
module.exports = ctx
- 源码中的实现(核心部分)
delegate(proto, 'request')
.getter('origin')
.getter('href')
// ...
// delegates 实现
module.exports = Delegator
function Delegator(proto, target) {
if (!(this instanceof Delegator)) return new Delegator(proto, target);
this.proto = proto;
this.target = target;
this.getters = [];
}
// name 相当于 key
Delegator.prototype.getter = function(name){
// proto 相当于 context
var proto = this.proto;
var target = this.target;
this.getters.push(name);
proto.__defineGetter__(name, function(){
return this[target][name];
});
return this;
};
8.3.2 ctx.body 实现
// response.js
/*
ctx.response.body = 'hello world'
*/
const response = {
// 类似于 vue2 数据代理 _body 相当于中间变量
_body: undefined,
// getter
get body() {
return this._body
},
// setter
set body(content) {
this._body = content
}
}
module.exports = response
// context.js
// ctx.body ==访问==> ctx.response.body
defineGetter('response', 'body')
// ctx.body = 'hello'
defineSetter('response', 'body')
function defineSetter(target, key) {
// 设置 ctx.body = 'hello' ==相当于==> 设置ctx.response.body = 'hello'
context.__defineSetter__(key, function (value) {
this[target][key] = value
})
}
- 响应请求
// application.js
handleRequest = (req, res) => {
// 每次请求上下文是独立的
// 创建上下文方法 createContext
let ctx = this.createContext(req, res)
this.fn(ctx)
// 防止中文乱码
res.writeHead(200, {
'content-type': 'text/html;charset=utf8'
});
// 结束响应过程 返回结果响应给用户
res.end(ctx.body)
}
- 此时
koa_learn.js
中main
中间件函数设置ctx.body
为“你好”,访问localhost:3000
页面显示“你好” - 如果不设置
ctx.body
,页面无任何显示信息,而koa
中页面提示Not Found
,ctx.status
(响应状态码)为 404,如果设置ctx.body
,则设置ctx.status
为 200
// 完善
// application.js
handleRequest = (req, res) => {
let ctx = this.createContext(req, res)
res.statusCode = 404
this.fn(ctx)
res.writeHead(res.statusCode, {
'content-type': 'text/html;charset=utf8'
});
if (ctx.body) {
res.end(ctx.body)
} else {
res.end('Not Found')
}
}
// response.js
const response = {
_body: undefined,
// todo...
set body(content) {
// 设置 ctx.body 则设置状态码为 200
this.res.statusCode = 200
this._body = content
}
}
8.4 koa 中间件的实现原理
- 示例回顾(洋葱模型回顾)
const one = async (ctx, next) => {
console.log('first in')
await next() // 执行权转交给下一个中间件
console.log('first out')
}
const two = async (ctx, next) => {
console.log('second in')
await next()
console.log('second out')
}
const three = async (ctx, next) => {
console.log('third in')
await next()
console.log('third out')
}
app.use(one)
app.use(two)
app.use(three)
/*
first in
second in
third in
third out
second out
first out
*/
koa
异步操作基于Promise
实现,koa
内部将所有中间件进行组合操作,koa
中间件需要加上await next()
,否则异步逻辑可能出错
// application.js
class Application {
constructor() {
// todo...
// 使用数组存放中间件
this.middlewares = []
}
use(middleware) {
this.middlewares.push(middleware)
}
handleRequest = (req, res) => {
let ctx = this.createContext(req, res)
res.statusCode = 404
// compose 组合中间件 返回 promise 对象
this.compose(ctx).then(() => {
res.writeHead(res.statusCode, {
'content-type': 'text/html;charset=utf8'
});
// res.end(ctx.body)
if (ctx.body) {
res.end(ctx.body)
} else {
res.end('Not Found')
}
})
}
compose(ctx) {
function dispatch(i) {
// todo...
}
return dispatch(0); // 执行第一个中间件
}
}
8.4.1 compose 逻辑编写
compose(ctx) {
function dispatch(i) {
// case: 没有注册中间件 / 中间件全部注册了
if(this.middlewares.length === i) {
return Promise.resolve()
}
let middleware = this.middlewares[i]
// 执行
middleware(ctx, () => {
// next() 取出下一个中间件执行
dispatch(i+1)
})
}
return dispatch(0); // 执行第一个中间件
}
- 返回
Promise
对象
compose(ctx) {
const that = this
// 或者将 dispatch 写成箭头函数形式
/*
const dispatch = i => {
// todo...
}
*/
function dispatch(i) {
// 没有注册中间件 / 中间件全部注册了
if(that.middlewares.length === i) {
return new Promise.resolve()
}
let middleware = that.middlewares[i]
// 执行 ctx next ==> 回调函数
return Promise.resolve(middleware(ctx, () => {
// next() 相当于【取出下一个中间件执行】
// 递归
dispatch(i+1)
}))
}
return dispatch(0); // 执行第一个中间件
}
- 验证结果
- 优化完善
- 不允许一个中间件多次调用
next()
,否则抛出异常Error: next() called multiple times
- 不允许一个中间件多次调用
// 不允许一个中间件多次调用 next()
// Error: next() called multiple times
const one = async (ctx, next) => {
console.log('first in')
await next()
await next()
console.log('first out')
}
// Application.js
compose(ctx) {
let index = -1
const dispatch = (i) => {
if(index <= i) {
// 多次调用
return Promise.reject(new Error('next() called multiple times')
}
index = i
// 没有注册中间件 / 中间件全部注册了
if (this.middlewares.length === i) {
return Promise.resolve()
}
let middleware = this.middlewares[i]
// 执行
return Promise.resolve(middleware(ctx, () => {
// next() 取出下一个中间件执行
dispatch(i + 1)
}))
}
return dispatch(0); // 执行第一个中间件
}
8.5 捕获异常错误
// koa_learn.js
// 订阅 error 事件
app.on('error', (err) => {
console.log(err)
})
// application.js
const EventEmitter = require('events')
class Application extends EventEmitter {
constructor() {
// 调用父类 EventEmitter 构造方法
super()
}
handleRequest = (req, res) => {
// todo...
this.compose(ctx).then(() => {
// todo...
},() => {
res.statusCode = 500
res.end('Internal Server Error')
}).catch((e) => {
// 发射 error 事件
this.emit('error', e)
})
}
}
- 上述错误处理存在问题:
unhandledPromiseRejection
,使用catch
捕获异常,但catch
无法捕获异常,导致error
事件无法监听。这个问题仍然存在,欢迎讨论!不过大致源码的核心内容我们已经实现完成啦! koa
作为Node.js Web
应用程序框架,开发后端是相当不错的!所以后续还会出一期项目编写与讲解,尽请期待!
关于
koa
学习内容就这么多啦,如果您觉得内容不错的话,望您能关注🤞点赞👍收藏❤️一键三连!