零、Koa 源码目录结构
.├── History.md├── LICENSE├── Readme.md├── dist│ └── koa.mjs├── lib│ ├── application.js # 最核心的模块│ ├── context.js # 上下文对象│ ├── request.js # Koa 自己实现的请求对象│ └── response.js # Koa 自己实现的响应对象└── package.json
一、基本结构
使用:
const Koa = require('./koa')const app = new Koa()app.listen(3000)
application.js
module.exports = class Application {listen(...args) {const server = http.createServer((req, res) => {res.end('My Koa')})return server.listen(...args)}}
二、实现中间件功能
- Koa 会把所有中间件组合成一个大的 Promise
- 当这个 Promise 执行完毕之后,会采用当前的 ctx.body 进行结果响应
- next 前面必须有 await 或者 return next,否则执行顺序达不到预期
- 如果都是同步执行,加不加 await 都无所谓
- 我不知道后续是否有异步逻辑,所以建议写的时候都加上 await next
收集中间件
使用方式:
const Koa = require('./koa')const app = new Koa()app.use((ctx, next) => {ctx.body = 'foo'})app.use((ctx, next) => {ctx.body = 'Koa'})app.listen(3000)
application.js
const http = require('http')module.exports = class Application {constructor () {this.middleware = []}use(fn) {if (typeof fn !== 'function')throw new TypeError('middleware must be a function!')this.middleware.push(fn)}listen(...args) {const server = http.createServer((req, res) => {res.end('My Koa')})return server.listen(...args)}}
调用中间件
const http = require('http')module.exports = class Application {constructor() {this.middleware = [] // 存储用户所有的中间件处理函数}use(fn) {this.middleware.push(fn)}listen(...args) {const server = http.createServer(this.callback())return server.listen(...args)}compose(middleware) {return function () { // 可以配置额外参数const dispatch = i => {if (i >= middleware.length) return Promise.resolve()const fn = middleware[i]return Promise.resolve(fn({}, () => dispatch(i + 1)))}return dispatch(0)}}callback() {const fnMiddleware = this.compose(this.middleware)const handleRequest = (req, res) => {fnMiddleware().then(() => {// 处理结束res.end('end')}).catch(err => {// 发生异常res.end('error')})}return handleRequest}}
三、处理上下文对象
初始化上下文对象
context 上下文对象的使用方式:
/*** Koa Context*/const Koa = require('./koa')const app = new Koa()// Koa Context 将 node 的 request 和 response 对象封装到单个对象中,为编写 Web 应用程序和 API 提供了许多有用的方法。// 每个请求都将创建一个 Context,并在中间件中作为参数引用app.use((ctx, next) => {console.log(ctx) // Context 对象console.log(ctx.req) // Node 的 request 对象console.log(ctx.res) // Node 的 response 对象console.log(ctx.request) // Koa 中封装的请求对象console.log(ctx.request.header) // 获取请求头对象console.log(ctx.request.method) // 获取请求方法console.log(ctx.request.url) // 获取请求路径console.log(ctx.request.path) // 获取不包含查询字符串的请求路径console.log(ctx.request.query) // 获取请求路径中的查询字符串// Request 别名// 完整列表参见:https://koa.bootcss.com/#request-console.log(ctx.header)console.log(ctx.method)console.log(ctx.url)console.log(ctx.path)console.log(ctx.query)// Koa 中封装的响应对象console.log(ctx.response)// Response 别名ctx.status = 200ctx.message = 'Success'ctx.type = 'plain'ctx.body = 'Hello Koa'})app.listen(3000)
context.js
const context = {// get method () {// return this.request.method// },// get url () {// return this.request.url// }}defineProperty('request', 'method')defineProperty('request', 'url')function defineProperty (target, name) {context.__defineGetter__(name, function () {return this[target][name]})// Object.defineProperty(context, name, {// get () {// return this[target][name]// }// })}module.exports = context
request.js
const url = require('url')const request = {get url () {return this.req.url},get path () {return url.parse(this.req.url).pathname},get query () {return url.parse(this.req.url, true).query},get method () {return this.req.method}}module.exports = request
response.js
const response = {set status (value) {this.res.statusCode = value}}module.exports = responsezz
application.js
const http = require('http')const context = require('./context')const request = require('./request')const response = require('./response')class Application {constructor() {this.middleware = [] // 保存用户添加的中间件函数// 为了防止多个实例共享 Request、Response、Context 导致数据被意外修改,这里重新拷贝一份this.request = Object.create(request)this.response = Object.create(response)this.context = Object.create(context)}listen(...args) {const server = http.createServer(this.callback())server.listen(...args)}use(fn) {this.middleware.push(fn)}// 异步递归遍历调用中间件处理函数compose(middleware) {return function (context) {const dispatch = index => {if (index >= middleware.length) return Promise.resolve()const fn = middleware[index]return Promise.resolve(// TODO: 上下文对象fn(context, () => dispatch(index + 1)) // 这是 next 函数)}// 返回第 1 个中间件处理函数return dispatch(0)}}createContext(req, res) {// 为了避免请求之间 context 数据交叉污染,这里为每个请求单独创建 context 请求对象const context = Object.create(this.context)const request = Object.create(this.request)const response = Object.create(this.response)context.app = request.app = response.app = thiscontext.req = request.req = response.req = req // 原生的请求对象context.res = request.res = response.res = res // 原生的响应对象request.ctx = response.ctx = context // 在 Request 和 Response 中也可以拿到 context 上下文对象request.response = response // Request 中也可以拿到 Responseresponse.request = request // Response 中也可以拿到 Requestcontext.originalUrl = request.originalUrl = req.url // 没有经过任何处理的请求路径context.state = {} // 初始化 state 数据对象,用于给模板视图提供数据return context}callback() {const fnMiddleware = this.compose(this.middleware)const handleRequest = (req, res) => {// 1、创建上下文请求对象const context = this.createContext(req, res)// 2、传入上下文对象执行中间件栈fnMiddleware(context).then(() => {console.log('end')res.end('My Koa')}).catch(err => {res.end(err.message)})}return handleRequest}}module.exports = Application
处理上下文对象中的别名
四、处理 ctx.body
application.js
const http = require('http')const { Stream } = require('stream')const context = require('./context')const request = require('./request')const { body } = require('./response')const response = require('./response')class Application {constructor() {this.middleware = [] // 保存用户添加的中间件函数this.context = Object.create(context)this.request = Object.create(request)this.response = Object.create(response)}listen(...args) {const server = http.createServer(this.callback())server.listen(...args)}use(fn) {this.middleware.push(fn)}// 异步递归遍历调用中间件处理函数compose(middleware) {return function (context) {const dispatch = index => {if (index >= middleware.length) return Promise.resolve()const fn = middleware[index]return Promise.resolve(// TODO: 上下文对象fn(context, () => dispatch(index + 1)) // 这是 next 函数)}// 返回第 1 个中间件处理函数return dispatch(0)}}// 构造上下文对象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 = thiscontext.req = request.req = response.req = reqcontext.res = request.res = response.res = resrequest.ctx = response.ctx = contextrequest.response = responseresponse.request = requestcontext.originalUrl = request.originalUrl = req.urlcontext.state = {}return context}callback() {const fnMiddleware = this.compose(this.middleware)const handleRequest = (req, res) => {// 每个请求都会创建一个独立的 Context 上下文对象,它们之间不会互相污染const context = this.createContext(req, res)fnMiddleware(context).then(() => {respond(context)// res.end(context.body)// res.end('My Koa')}).catch(err => {res.end(err.message)})}return handleRequest}}function respond (ctx) {const body = ctx.bodyconst res = ctx.resif (body === null) {res.statusCode = 204return res.end()}if (typeof body === 'string') return res.end(body)if (Buffer.isBuffer(body)) return res.end(body)if (body instanceof Stream) return body.pipe(ctx.res)if (typeof body === 'number') return res.end(body + '')if (typeof body === 'object') {const jsonStr = JSON.stringify(body)return res.end(jsonStr)}}module.exports = Application
context
const context = {// get method () {// return this.request.method// },// get url () {// return this.request.url// }}defineProperty('request', 'method')defineProperty('request', 'url')defineProperty('response', 'body')function defineProperty (target, name) {// context.__defineGetter__(name, function () {// return this[target][name]// })Object.defineProperty(context, name, {get () {return this[target][name]},set (value) {this[target][name] = value}})}module.exports = context
reponse.js
const response = {set status (value) {this.res.statusCode = value},_body: '', // 真正用来存数据的get body () {return this._body},set body (value) {this._body = value}}module.exports = response
