本文分为三个部分:入门,源码解析,手写源码
入门
什么是 Koa
- koa是基于nodejs平台的下一代web开发框架
- express原班人马打造,更精简
- Async + await 处理异步
- 洋葱圈型的中间件机制
创建一个最简单的 koa
初始化一个 npm 项目
npm init -y
安装 koa 依赖 npm install koa --save
根目录下 app.js
const Koa = require('koa')
const app = new Koa()
app.use(async (ctx, next) => {
// ctx 是封装了 request 和 response 的上下文
ctx.body = 'hello koa'
// next 是下一个中间件
})
app.listen(3000)
根目录下运行 node app.js ,浏览器输入 localhost:3000,输出 hello koa,服务已经启动成功
const Koa = require('koa')
const app = new Koa()
// 135642
app.use(async (ctx, next) => {
ctx.body = '1'
next()
ctx.body = ctx.body + '2'
})
app.use(async (ctx, next) => {
ctx.body += '3'
next()
ctx.body += '4'
})
app.use(async (ctx, next) => {
ctx.body += '5'
next()
ctx.body += '6'
})
// app.use(async (ctx) => {
// ctx.body = 'hello koa'
// })
app.listen(3000)
浏览器输出 135642
若将 第一个的 next() 注释掉,则只会输出 12
next() 的作用就是执行下面的中间件
(这个很重要,面试很常考👍👍)
解决异步的情况
const Koa = require('koa')
const app = new Koa()
app.use(async (ctx, next) => {
ctx.body = '1'
setTimeout(() => {
next()
}, 2000)
ctx.body = ctx.body + '2'
})
app.use(async (ctx, next) => {
ctx.body += '3'
next()
ctx.body += '4'
})
app.use(async (ctx, next) => {
ctx.body += '5'
next()
ctx.body += '6'
})
// app.use(async (ctx) => {
// ctx.body = 'hello koa'
// })
app.listen(3000)
此时浏览器就是输出 12
const Koa = require('koa')
const app = new Koa()
// 135642
function delay () {
return new Promise((reslove, reject) => {
setTimeout(() => {
reslove()
}, 2000)
})
}
app.use(async (ctx, next) => {
ctx.body = '1'
await next()
ctx.body += '2'
})
app.use(async (ctx, next) => {
ctx.body += '3'
await delay()
await next()
ctx.body += '4'
})
app.use(async (ctx, next) => {
ctx.body += '5'
await next()
ctx.body += '6'
})
// app.use(async (ctx) => {
// ctx.body = 'hello koa'
// })
app.listen(3000)
此时浏览器输出 135642
对于请求的处理和响应
koa-router(路由处理)
npm install koa-router --save
var Koa = require('koa')
var Router = require('koa-router')
var app = new Koa()
var router = new Router()
// 实例化
router.get('/', async (ctx) => {
ctx.body = 'home'
})
router.get('/news', async (ctx) => {
ctx.body = 'news'
})
//启动路由
app.use(router.routes()).use(router.allowedMethods())
// 设置响应头
// 监听在3000端口
app.listen(3000)
get传值
// get传值
router.get('/newsContent', async (ctx) => {
// http://localhost:3000/newsContent?aid=123&name=zs
console.log(ctx.query) // 推荐方式
// { aid: '123', name: 'zs' }
console.log(ctx.querystring)
// aid=123&name=zs
// console.log(ctx.request)
/**
* {
method: 'GET',
url: '/newsContent?aid=123&name=zs',
header: {
host: 'localhost:3000',
connection: 'keep-alive',
'cache-control': 'max-age=0',
'upgrade-insecure-requests': '1',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36',
'sec-fetch-user': '?1',
accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,im
age/apng,*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'sec-fetch-site': 'none',
'sec-fetch-mode': 'navigate',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'en,zh;q=0.9,zh-CN;q=0.8'
}
}
*/
console.log(ctx.request.url)
// /newsContent?aid=123&name=zs
console.log(ctx.request.query)
//[Object: null prototype] { aid: '123', name: 'zs' }
console.log(ctx.request.querystring)
//aid=123&name=zs
ctx.body = '新闻详情'
})
动态匹配
router.get('/page/:id', async (ctx) => {
// http://localhost:3000/page/112
console.log(ctx.params)
// {id: 112}
ctx.body = '新闻详情'
})
router.get('/pages/:id/:bid', async (ctx) => {
// http://localhost:3000/pages/112/qwe
console.log(ctx.params)
// { id: '112', bid: 'qwe' }
ctx.body = '新闻详情'
})
gitee地址
koa 中间件应用
var Koa = require('koa')
var Router = require('koa-router')
var app = new Koa()
var router = new Router()
// 运行程序时,要注释掉无关的代码,否则容易报错.
// 应用中间件
/**
* 会匹配任何的路由,无论访问任何的路由,都会打印出 middleware
*/
// 无 next(),匹配终止
app.use(async (ctx) => {
ctx.body = 'middleware'
})
// 有 next() 就会接着向下执行
app.use(async (ctx, next) => {
console.log(new Date())
// 2020-04-06T01:53:02.529Z
//2020-04-06T01:53:10.619Z
await next()
})
router.get('/', async (ctx) => {
console.log('home 01')
ctx.body = 'home'
})
router.get('/news', async (ctx) => {
console.log('news')
ctx.body = 'news'
})
router.get('/login', async (ctx) => {
ctx.body = 'login'
})
// 应用中间件
// 路由中间件
router.get('/pages', async (ctx, next) => {
console.log('这是一个页面1')
// 没有 await next (), ctx.body 没有值 界面会显示 Not Found
// 程序没有办法继续执行
await next()
// next() 会继续向下匹配,之后在界面打印出 page,控制台也会输出 这是一个页面1
})
router.get('/pages', async (ctx) => {
ctx.body = 'page'
})
// 路由中间件
// 中间件的执行与放置的位置无关
router.get('/', async (ctx) => {
console.log('home 01')
ctx.body = 'home'
})
router.get('/news', async (ctx) => {
console.log('news')
ctx.body = 'news'
})
router.get('/login', async (ctx) => {
ctx.body = 'login'
})
app.use(async (ctx, next) => {
console.log('middleware 01')
next()
/**
* middleware 01
home 01
*/
/**
* 无论放置在哪里,都会先执行中间件,之后继续匹配路由
*/
})
// 中间件的执行与放置的位置无关
// 寻找 404 界面
router.get('/', async (ctx) => {
console.log('home 01')
ctx.body = 'home'
})
router.get('/news', async (ctx) => {
console.log('news')
ctx.body = 'news'
})
router.get('/login', async (ctx) => {
ctx.body = 'login'
})
app.use(async (ctx, next) => {
console.log('middleware 01')
next()
/**
* middleware 01
home 01
*/
/**
* 无论放置在哪里,都会先执行中间件,之后继续匹配路由
*/
if(ctx.status === 404) {
ctx.status = 404
ctx.body = '404 page'
} else {
console.log(ctx.url)
}
/**
* 访问 /news 路由
* 控制台输出
* middleware 01
news
/news
1.先执行中间件 输出 middleware 01
2.存在 next (), 继续向下匹配路由
3.存在 /news 界面,控制台输出 news
4.ctx.body 有值,控制台输出 /news
访问 /*** 路由
1.先执行中间件 输出 middleware 01
2.存在 next (), 继续向下匹配路由
3.匹配不到,ctx.body = 404
4.返回 404 page
*/
})
// 中间件的执行顺序
app.use(async (ctx, next) => {
console.log('middleware 01')
await next()
console.log('return middleware 01')
})
app.use(async (ctx, next) => {
console.log('middleware 02')
await next()
console.log('return middleware 02')
})
router.get('/news', async (ctx) => {
console.log('匹配到 news page')
ctx.body = 'news'
})
/**
* middleware 01
middleware 02
匹配到 news page
return middleware 02
return middleware 01
1.先执行中间件 输出 middleware 01
2.遇到 next() ,继续执行
3.碰到中间件 输出 middleware 02
4.无中间件 匹配路由,匹配到 /news
5.输出 匹配到 news page
6. ctx.body 有值,页面输出 news
7.返回最后执行的中间件 ,输出 return middleware 02
8.接着返回 输出 return middleware 01
*/
// 中间件的执行顺序
//启动路由
app.use(router.routes()).use(router.allowedMethods())
// 设置响应头
// 监听在3000端口
app.listen(3000)
ejs 模板
npm install koa-views --save
npm install ejs --save
var Koa = require('koa')
var Router = require('koa-router')
var views = require('koa-views')
var app = new Koa()
var router = new Router()
// 配置模板引擎
app.use(views(__dirname + '/'), {
map: {
html: 'ejs'
}
})
router.get('/', async (ctx) => {
await ctx.render('./views/index.ejs')
})
router.get('/news', async (ctx) => {
ctx.body = 'news'
})
//启动路由
app.use(router.routes()).use(router.allowedMethods())
// 设置响应头
// 监听在3000端口
app.listen(3000)
post 请求
npm install koa-bodyparser --save
var Koa = require('koa')
var Router = require('koa-router')
var views = require('koa-views')
var bodyParser = require('koa-bodyparser')
var app = new Koa()
var router = new Router()
// 配置模板引擎
app.use(views(__dirname + '/'), {
map: {
html: 'ejs'
}
})
app.use(bodyParser())
router.get('/', async (ctx) => {
await ctx.render('./views/index.ejs')
})
router.post('/doAdd', async (ctx, next) => {
// 获取表单提交数据
ctx.body = ctx.request.body
console.log(ctx.body)
// { username: 'zasss', password: '123456' }
})
//启动路由
app.use(router.routes()).use(router.allowedMethods())
// 设置响应头
// 监听在3000端口
app.listen(3000)
静态资源
npm install koa-static --save
var Koa = require('koa')
var Router = require('koa-router')
var views = require('koa-views')
var bodyParser = require('koa-bodyparser')
// 引入
const static = require('koa-static')
var app = new Koa()
var router = new Router()
app.use(views(__dirname + '/'), {
map: {
html: 'ejs'
}
})
app.use(bodyParser())
// 静态资源
app.use(static('static'))
// app.use(static('public'))
// 静态资源 可以配置多个
router.get('/', async (ctx) => {
await ctx.render('./views/index.ejs')
})
router.post('/doAdd', async (ctx, next) => {
ctx.body = ctx.request.body
console.log(ctx.body)
})
//启动路由
app.use(router.routes()).use(router.allowedMethods())
// 设置响应头
// 监听在3000端口
app.listen(3000)
koa-cookie
/**
* 1 保存用户信息
* 2 浏览器历史记录
* 3 猜你喜欢的功能
* 4 10天免登陆
* 5 多个页面之间的数据传递
* 6 cookie 实现购物车功能
*
*/
var Koa = require('koa')
var Router = require('koa-router')
var views = require('koa-views')
var bodyParser = require('koa-bodyparser')
// 引入
const static = require('koa-static')
var app = new Koa()
var router = new Router()
app.use(views(__dirname + '/'), {
map: {
html: 'ejs'
}
})
app.use(bodyParser())
// 静态资源
app.use(static('static'))
// app.use(static('public'))
// 静态资源 可以配置多个
// 演示时,注释无关代码
// 设置 cookie
router.get('/', async (ctx) => {
// 访问 / 路由时设置 cookies
// maxAge: 设置过期时间
// path 只有访问特定的路由才可以访问
// domin 域名
// httpOnly 只有服务器端才可以访问
ctx.cookies.set('userInfo', 'zhangsan', {
maxAge: 60 * 1000 * 60,
// expires: '2020-04-07',
// path: '/news',
// domin: '',
// httpOnly: true
})
await ctx.render('./views/index.ejs')
})
router.get('/news', async (ctx) => {
// 访问 /news 路由的时候,可以取到相对应的 cookies
console.log(ctx.cookies.get('userInfo'))
await ctx.render('./views/index.ejs')
})
router.get('/login', async (ctx) => {
// 访问 /login 路由的时候,可以取到相对应的 cookies
console.log(ctx.cookies.get('userInfo'))
ctx.body = ctx.cookies.get('userInfo')
// await ctx.render('./views/index.ejs')
})
// 设置 cookie
// 设置中文 cookie
router.get('/', async (ctx) => {
// value 设置为中文 argument value is invalid
// 无法访问
// koa 中 设置中文 Cookies
var name = new Buffer('张三').toString('base64')
ctx.cookies.set('userInfo', name, {
maxAge: 60 * 1000 * 60,
})
await ctx.render('./views/index.ejs')
})
router.get('/news', async (ctx) => {
// 访问 /news 路由的时候,可以取到相对应的 cookies
var data = ctx.cookies.get('userInfo')
var userinfo = new Buffer(data, 'base64').toString()
console.log(userinfo)
// console.log(ctx.cookies.get('userInfo'))
await ctx.render('./views/index.ejs')
})
// 设置中文 cookie
//启动路由
app.use(router.routes()).use(router.allowedMethods())
// 设置响应头
// 监听在3000端口
app.listen(3000)
koa-session
/**
* session 是一种记录客户状态的机制
* 存储在服务端的
*/
var Koa = require('koa')
var Router = require('koa-router')
var views = require('koa-views')
var bodyParser = require('koa-bodyparser')
// 引入
const static = require('koa-static')
const session = require('koa-session')
var app = new Koa()
var router = new Router()
app.use(views(__dirname + '/'), {
map: {
html: 'ejs'
}
})
app.use(bodyParser())
// 静态资源
app.use(static('static'))
// app.use(static('public'))
// 静态资源 可以配置多个
app.keys = ['some secret hurr']
const CONFIG = {
key: 'koa:sess',
maxAge: 9000, // 过期时间
autoCommit: true,
overwrite: true,
httpOnly: true,
signed: true,
rolling: false,
renew: true,
sameSite: null,
}
app.use(session(CONFIG, app))
router.get('/', async (ctx) => {
console.log(ctx.session.userInfo)
await ctx.render('./views/index.ejs')
})
router.get('/login', async (ctx) => {
ctx.session.userInfo = '张三'
await ctx.render('./views/index.ejs')
})
router.get('/page', async (ctx, next) => {
console.log(ctx.session.userInfo)
})
//启动路由
app.use(router.routes()).use(router.allowedMethods())
// 设置响应头
// 监听在3000端口
app.listen(3000)
好了,简单的结束了。接下来要进入头脑风暴了。
koa 源码解析
著名的洋葱圈
👉👉食堂牛肉烩面里的洋葱,我觉得巨好吃🤤🤤🤤
由 package.json 文件的 main 字段可以得出,入口文件为 lib/application.js。
class Application extends Emitter {}
Application 类继承自 Emitter 类。
class Emitter 是 node 内置模块。
class Application extends Emitter {
constructor(options) {
super();
options = options || {};
this.proxy = options.proxy || false;
this.subdomainOffset = options.subdomainOffset || 2;
this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For';
this.maxIpsCount = options.maxIpsCount || 0;
this.env = options.env || process.env.NODE_ENV || 'development';
if (options.keys) this.keys = options.keys;
// 保存通过app.use(middleware)注册的中间件
this.middleware = [];
// Object.create => 都是对象
// Context对象里就封装了上下文的所有变量和对象。
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
/**
* 调用原生 http 模块开启服务并监听端口
*/
listen(...args) {
// this.callback()返回的就是回调函数,所以callback是一个高阶函数。
const server = http.createServer(this.callback());
return server.listen(...args);
}
/**
* 挂载中间件
* use方法很简单,接受一个函数作为参数,并加入middleware数组。由于koa最开始支持使用generator函数作为中间件使用,但将在3.x的版本中放弃这项支持,因此koa2中对于使用generator函数作为中间件的行为给与未来将被废弃的警告,但会将generator函数转化为async函数。返回this便于链式调用。
* @param {Function} fn
* @return {Application} self
* @api public
* 只是把传入的回调函数先存到middleware数组里。
*/
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/blob/master/docs/migration.md');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
// 用app.use的时候,实际上就是将函数push进middleware数组中,等待之后的调用。
this.middleware.push(fn);
return this;
}
/**
* 执行返回 http 请求的回调函数
*
* 把handleRequest返回出去,供http.createServer回调,注意这里形成了一个闭包,能获得fn
* @returns fn
*/
callback() {
// compose 函数将中间件数组转换成执行链函数 fn
// 创建洋葱模型的入口函数
const fn = compose(this.middleware);
if (!this.listenerCount('error')) this.on('error', this.onerror);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
}
koa-compose 源码(洋葱模型实现)
compose 函数将中间件数组转换成执行链函数 fn
(源码部分在 node_modules/koa-compose)
/**
* 接收一个middleware数组为参数,返回一个函数
*/
function compose (middleware) {
// 对入参是不是数组和校验数组每一项是不是函数。
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
/**
* @param {Object} 传入对象 context
* @return {Promise} 返回Promise
*/
return function (context, next) {
// last called middleware #
let index = -1
// 返回第一个 use 的中间件函数
return dispatch(0)
// 每个 dispatch 的返回值也是一个 Promise,因此我们的中间件中可以方便地使用async函数进行定义,内部使用await next()调用“下游”,然后控制流回“上游”,这是更准确也更友好的中间件模型
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
// 获取当前传入下标的中间件函数
let fn = middleware[i]
// 防止最后一个中间件执行next进行无限循坏
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
// 中间件的执行是异步的,并且中间件执行完毕后返回的是一个Promise
// 洋葱模型的触发方式,先执行当前的中间,并将下一个中间作为第二个参数(next)传入当前中间件中
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
类似于这样的结构
const [fn1, fn2, fn3] = this.middleware;
// 先执行当前的中间,并将下一个中间作为第二个参数(next)传入当前中间件中
const fnMiddleware = function (context) {
return Promise.resolve(
fn1(context, function next() {
return Promise.resolve(
fn2(context, function next() {
return Promise.resolve(
fn3(context, function next() {
return Promise.resolve();
})
)
})
)
})
);
};
fnMiddleware(ctx).then(handleResponse).catch(onerror);
koa-convert 源码(转换 generator 函数)
koa1中主要是generator函数。koa2中会自动转换generator函数。
app.use
时有一层判断,是否是generator
函数,如果是则用koa-convert
暴露的方法convert
来转换重新赋值,再存入middleware
,后续再使用。
(源码部分在 node_modules/koa-convert)
function convert (mw) {
if (typeof mw !== 'function') {
throw new TypeError('middleware must be a function')
}
if (mw.constructor.name !== 'GeneratorFunction') {
// assume it's Promise-based middleware
return mw
}
const converted = function (ctx, next) {
// 转换重新赋值,再存入middleware,后续再使用
return co.call(ctx, mw.call(ctx, createGenerator(next)))
}
converted._name = mw._name || mw.name
return converted
}
function * createGenerator (next) {
return yield next()
}
co 源码
首先要明白 generator 函数是不会自动执行的,需要一步步调用 next()。 co 就是让 generator向 async、await 函数一样自动执行。
(源码部分在 node_modules/co)
function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1)
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.apply(ctx, args);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
onFulfilled();
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
// 反复执行调用自己
function next(ret) {
// 检查当前是否为 Generator 函数的最后一步,如果是就返回
if (ret.done) return resolve(ret.value);
// 确保返回值是promise对象
var value = toPromise.call(ctx, ret.value);
// 使用 then 方法,为返回值加上回调函数,然后通过 onFulfilled 函数再次调用 next 函数
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
// 在参数不符合要求的情况下(参数非 Thunk 函数和 Promise 对象),将 Promise 对象的状态改为 rejected,从而终止执行。
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
});
}
小结👉👉👉:
koa-compose
是将app.use
添加到middleware
数组中的中间件(函数),通过使用Promise
串联起来,next()
返回的是一个promise
。
koa-convert
判断app.use
传入的函数是否是generator
函数,如果是则用koa-convert
来转换,最终还是调用的co
来转换。
co
源码实现原理:其实就是通过不断的调用generator
函数的next()
函数,来达到自动执行generator
函数的效果(类似async、await函数的自动自行
)。
以上是整个 koa 洋葱模型的核心概念。
application.js 文件中还定义了一些用于请求和响应的函数
handleRequest — 请求真正的回调函数
/**
* 请求真正的回调函数
* 核心在于执行fnMiddleware,这个其实就是 const fn = compose(this.middleware)
* compose会串联顺序执行中间件next之前的代码,完成后倒序执行各中间件next之后的代码
*/
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
// 一开始就将res的statusCode定义为404
// 如果在我们没有设置body的情况下,默认就会返回404。
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
respond — 请求在经过所有中间件全部处理后的自动响应函数
function respond(ctx) {
// 当 ctx 的 respond 为false 直接返回
if (false === ctx.respond) return;
// 当请求是scoket将根据socket的writable,否则都未true
if (!ctx.writable) return;
const res = ctx.res;
let body = ctx.body;
const code = ctx.status;
// ignore body
if (statuses.empty[code]) {
// strip headers
ctx.body = null;
return res.end();
}
// 请求是HEAD的一些处理
if ('HEAD' === ctx.method) {
if (!res.headersSent && !ctx.response.has('Content-Length')) {
const { length } = ctx.response;
if (Number.isInteger(length)) ctx.length = length;
}
return res.end();
}
// status body
if (null == body) {
if (ctx.response._explicitNullBody) {
ctx.response.remove('Content-Type');
ctx.response.remove('Transfer-Encoding');
return res.end();
}
if (ctx.req.httpVersionMajor >= 2) {
body = String(code);
} else {
body = ctx.message || String(code);
}
if (!res.headersSent) {
ctx.type = 'text';
ctx.length = Buffer.byteLength(body);
}
return res.end(body);
}
// responses
// 处理 Buffer 类型返回
if (Buffer.isBuffer(body)) return res.end(body);
// 处理字符串类型返回
if ('string' === typeof body) return res.end(body);
// 处理 Stream 类型返回
if (body instanceof Stream) return body.pipe(res);
// body: json 对象处理,转为JSON字符串返回
body = JSON.stringify(body);
if (!res.headersSent) {
ctx.length = Buffer.byteLength(body);
}
res.end(body);
}
createContext — 根据 req,res 创建上下文对象
/**
* 根据 req,res 创建上下文对象
* @api private
* 每次回调过来都是创建新的context、request和response实例,这样本质上也对应了HTTP是无状态的。
*/
createContext(req, res) {
const context = Object.create(this.context);
const request = context.request = Object.create(this.request);
const response = context.response = Object.create(this.response);
context.app = request.app = response.app = this;
context.req = request.req = response.req = req;
context.res = request.res = response.res = res;
// 注意context、request、response都有成员变量双向绑定在一起,从一个对象上能获取另外两个对象,后面可以看到很多context上的方法都被代理到了request和response上。
request.ctx = response.ctx = context;
request.response = response;
response.request = request;
context.originalUrl = request.originalUrl = req.url;
//公共的存储内容,后端模板也会把这里的属性作为视图的上下文用于渲染
context.state = {};
return context;
}
context.js
// COOKIES是一个Symbol类型的私有变量
const COOKIES = Symbol('context#cookies');
const proto = module.exports = {
/**
* util.inspect() implementation, which
* just returns the JSON output.
*
* @return {Object}
* @api public
*/
inspect() {
if (this === proto) return this;
return this.toJSON();
},
/**
* 获取当前ctx的内容
*/
toJSON() {
return {
request: this.request.toJSON(),
response: this.response.toJSON(),
app: this.app.toJSON(),
originalUrl: this.originalUrl,
req: '<original node req>',
res: '<original node res>',
socket: '<original node socket>'
};
},
/**
* http-assert,对http-errors的封装,一些基本的断言并设置http返回体
*/
assert: httpAssert,
throw(...args) {
throw createError(...args);
},
/**
*
* @param {Error} err
* @api private
*/
onerror(err) {
// 没有错误时什么也不做
if (null == err) return;
// err不是Error实例时,使用err创建一个Error实例
const isNativeError =
Object.prototype.toString.call(err) === '[object Error]' ||
err instanceof Error;
if (!isNativeError) err = new Error(util.format('non-error thrown: %j', err));
// headerSent变量表示响应头是否发送,如果响应头已经发送,或者响应处于不可写状态,将无法在响应中添加错误信息,直接退出该函数,否则需要将之前写入的响应头部信息清空。
let headerSent = false;
if (this.headerSent || !this.writable) {
headerSent = err.headerSent = true;
}
// 触发koa实例app的error事件
this.app.emit('error', err, this);
// nothing we can do here other
// than delegate to the app-level
// handler and log.
if (headerSent) {
return;
}
const { res } = this;
// 移除所有设置过的响应头
if (typeof res.getHeaderNames === 'function') {
res.getHeaderNames().forEach(name => res.removeHeader(name));
} else {
res._headers = {}; // Node < 7.7
}
// 设置错误头部
this.set(err.headers);
// 设置错误时的Content-Type
this.type = 'text';
let statusCode = err.status || err.statusCode;
// 找不到文件错误码设为404
if ('ENOENT' === err.code) statusCode = 404;
// 不能被识别的错误将错误码设为500
if ('number' !== typeof statusCode || !statuses[statusCode]) statusCode = 500;
// respond
const code = statuses[statusCode];
const msg = err.expose ? err.message : code;
this.status = err.status = statusCode;
this.length = Buffer.byteLength(msg);
res.end(msg);
},
// 处理Cookie
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;
}
};
/**
* 本质上是一个构造函数,接受两个参数,第一个参数是代理对象,第二个参数是被代理的对象
*
* context使用了delegates这个库。主要是将context中的一些值和函数代理到request和response中,这样实际上我们调用ctx.hostname获取值的时候,实际上是调用了req.hostname。从而方便调用。
*/
delegate(proto, 'response')
.method('attachment')
.method('redirect')
.method('remove')
.method('vary')
.method('has')
.method('set')
.method('append')
.method('flushHeaders')
.access('status')
.access('message')
.access('body')
.access('length')
.access('type')
.access('lastModified')
.access('etag')
.getter('headerSent')
.getter('writable');
delegate(proto, 'request')
.method('acceptsLanguages')
.method('acceptsEncodings')
.method('acceptsCharsets')
.method('accepts')
.method('get')
.method('is')
.access('querystring')
.access('idempotent')
.access('socket')
.access('search')
.access('method')
.access('query')
.access('path')
.access('url')
.access('accept')
.getter('origin')
.getter('href')
.getter('subdomains')
.getter('protocol')
.getter('host')
.getter('hostname')
.getter('URL')
.getter('header')
.getter('headers')
.getter('secure')
.getter('stale')
.getter('fresh')
.getter('ips')
.getter('ip');
request.js 和 response.js 是对原生 req 和 res 的封装,同时也提供一些额外的值和函数。
整体流程🔎🔎:
初始化阶段: new
初始化一个实例,use
搜集中间件到middleware数组,listen
合成中间件fnMiddleware
,返回一个callback函数给http.createServer
,开启服务器,等待http请求。
请求阶段: 每次请求,createContext
生成一个新的ctx
,传给fnMiddleware
,触发中间件的整个流程。
响应阶段: 整个中间件完成后,调用respond
方法,对请求做最后的处理,返回响应给客户端。
手写 koa
koa 👉👉👉👉👉 kao
模仿 koa,搭建最基本的骨架:
kao/application.js
const http = require('http');
class Application {
constructor() {
// 回调函数
this.callbackFn = null;
}
use(fn) {
this.callbackFn = fn;
}
callback() {
return (req, res) => this.callbackFn(req, res)
}
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
}
module.exports = Application;
测试文件kao/index.js
const Kao = require('./application');
const app = new Kao();
app.use(async (req, res) => {
res.writeHead(200);
res.end('hello world');
});
app.listen(3001, () => {
console.log('server start at 3001');
});
通过new
实例一个对象,use
注册回调函数,listen
启动server并传入回调。
使用 get 和 set 来处理原生请求,进行代理
kao/request.js
module.exports = {
get header() {
return this.req.headers;
},
set header(val) {
this.req.headers = val;
},
get url() {
return this.req.url;
},
set url(val) {
this.req.url = val;
},
}
当访问request.url
时,其实就是在访问原生的req.url
。
kao/response.js
module.exports = {
get status() {
return this.res.statusCode;
},
set status(code) {
this.res.statusCode = code;
},
get body() {
return this._body;
},
set body(val) {
// 源码里有对val类型的各种判断,这里省略
/* 可能的类型
1. string
2. Buffer
3. Stream
4. Object
*/
this._body = val;
}
}
处理上下文对象 context
const delegate = require('delegates');
const proto = module.exports = {
// context自身的方法
toJSON() {
return {
request: this.request.toJSON(),
response: this.response.toJSON(),
app: this.app.toJSON(),
originalUrl: this.originalUrl,
req: '<original node req>',
res: '<original node res>',
socket: '<original node socket>'
};
},
}
// delegates 原理就是__defineGetter__和__defineSetter__
// method是委托方法,getter委托getter,access委托getter和setter。
// proto.status => proto.response.status
delegate(proto, 'response')
.access('status')
.access('body')
// proto.url = proto.request.url
delegate(proto, 'request')
.access('url')
.getter('header')
真正注入原生对象,是在application.js
里的createContext
方法中注入
const http = require('http');
const context = require('./context');
const request = require('./request');
const response = require('./response');
class Application {
constructor() {
this.callbackFn = null;
// 每个Kao实例的context request respones
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
use(fn) {
this.callbackFn = fn;
}
callback() {
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx)
};
return handleRequest;
}
handleRequest(ctx) {
const handleResponse = () => respond(ctx);
// callbackFn是个async函数,最后返回promise对象
return this.callbackFn(ctx).then(handleResponse);
}
createContext(req, res) {
// 针对每个请求,都要创建ctx对象
// 每个请求的ctx request response
// ctx代理原生的req res就是在这里代理的
let ctx = Object.create(this.context);
ctx.request = Object.create(this.request);
ctx.response = Object.create(this.response);
ctx.req = ctx.request.req = req;
ctx.res = ctx.response.res = res;
ctx.app = ctx.request.app = ctx.response.app = this;
return ctx;
}
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
}
module.exports = Application;
function respond(ctx) {
// 根据ctx.body的类型,返回最后的数据
/* 可能的类型,代码删减了部分判断
1. string
2. Buffer
3. Stream
4. Object
*/
let content = ctx.body;
if (typeof content === 'string') {
ctx.res.end(content);
}
else if (typeof content === 'object') {
ctx.res.end(JSON.stringify(content));
}
}
代码中使用了Object.create
的方法创建一个全新的对象,通过原型链继承原来的属性。这样可以有效的防止污染原来的对象。
createContext
在每次http请求时都会调用,每次调用都新生成一个ctx
对象,并且代理了这次http请求的原生的对象。
respond
才是最后返回http响应的方法。根据执行完所有中间件后ctx.body
的类型,调用res.end
结束此次http请求。
洋葱圈模型的真正实现是 koa-compose,原理前面的源码分析已经讲过了。这里我们直接使用。
kao/compose.js
function compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
// 一个中间件里多次调用next
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
// fn就是当前的中间件
let fn = middleware[i]
if (i === middleware.length) fn = next // 最后一个中间件如果也next时进入(一般最后一个中间件是直接操作ctx.body,并不需要next了)
if (!fn) return Promise.resolve() // 没有中间件,直接返回成功
try {
/*
* 使用了bind函数返回新的函数,类似下面的代码
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1)
}))
*/
// dispatch.bind(null, i + 1)就是中间件里的next参数,调用它就可以进入下一个中间件
// fn如果返回的是Promise对象,Promise.resolve直接把这个对象返回
// fn如果返回的是普通对象,Promise.resovle把它Promise化
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
// 中间件是async的函数,报错不会走这里,直接在fnMiddleware的catch中捕获
// 捕获中间件是普通函数时的报错,Promise化,这样才能走到fnMiddleware的catch方法
return Promise.reject(err)
}
}
}
}
module.exports = compose;
修改 kao/application.js
class Application {
constructor() {
this.middleware = []; // 存储中间件
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
use(fn) {
this.middleware.push(fn); // 存储中间件
}
compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
callback() {
// 合成所有中间件
const fn = this.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);
// 执行中间件并把最后的结果交给respond
return fnMiddleware(ctx).then(handleResponse);
}
createContext(req, res) {
// 针对每个请求,都要创建ctx对象
let ctx = Object.create(this.context);
ctx.request = Object.create(this.request);
ctx.response = Object.create(this.response);
ctx.req = ctx.request.req = req;
ctx.res = ctx.response.res = res;
ctx.app = ctx.request.app = ctx.response.app = this;
return ctx;
}
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
}
module.exports = Application;
function respond(ctx) {
let content = ctx.body;
if (typeof content === 'string') {
ctx.res.end(content);
}
else if (typeof content === 'object') {
ctx.res.end(JSON.stringify(content));
}
}
修改 index.js 进行测试
const Kao = require('./application');
const app = new Kao();
app.use(async (ctx, next) => {
console.log('1-start');
await next();
console.log('1-end');
})
app.use(async (ctx) => {
console.log('2-start');
ctx.body = 'hello tc';
console.log('2-end');
});
app.listen(3001, () => {
console.log('server start at 3001');
});
// 1-start 2-start 2-end 1-end
错误处理机制
因为compose
组合之后的函数返回的仍然是Promise对象,所以我们可以在catch
捕获异常
kao/application.js
handleRequest(ctx, fnMiddleware) {
const handleResponse = () => respond(ctx);
const onerror = err => ctx.onerror(err);
// catch捕获,触发ctx的onerror方法
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
kao/context.js
const proto = module.exports = {
// context自身的方法
onerror(err) {
// 中间件报错捕获
const { res } = this;
if ('ENOENT' == err.code) {
err.status = 404;
} else {
err.status = 500;
}
this.status = err.status;
res.end(err.message || 'Internal error');
}
}
kao/index.js
const Kao = require('./application');
const app = new Kao();
app.use(async (ctx) => {
// 报错可以捕获
a.b.c = 1;
ctx.body = 'hello tc';
});
app.listen(3001, () => {
console.log('server start at 3001');
});
让Application
继承原生的Emitter
,从而实现error
监听,实现框架层发生错误的捕获机制
kao/application.js
const Emitter = require('events');
// 继承Emitter
class Application extends Emitter {
constructor() {
// 调用super
super();
this.middleware = [];
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
}
kao/context.js
const proto = module.exports = {
onerror(err) {
const { res } = this;
if ('ENOENT' == err.code) {
err.status = 404;
} else {
err.status = 500;
}
this.status = err.status;
// 触发error事件
this.app.emit('error', err, this);
res.end(err.message || 'Internal error');
}
}
kao/index.js
const Kao = require('./application');
const app = new Kao();
app.use(async (ctx) => {
// 报错可以捕获
a.b.c = 1;
ctx.body = 'hello tc';
});
app.listen(3001, () => {
console.log('server start at 3001');
});
// 监听error事件
app.on('error', (err) => {
console.log(err.stack);
});
ok,以上就是本文的全部内容。建议阅读本文的同时,下载 koa 源码。多翻翻书,总是有好处的。
写着写着有点饿了,不说了,干饭去了。
参考文章:
https://juejin.cn/post/6844904088220467213
https://segmentfault.com/a/1190000019603834
https://zhuanlan.zhihu.com/p/90677000