koa 基础入门与源码学习

1 篇文章 0 订阅
1 篇文章 0 订阅

Koa 学习笔记

本期内容主要为 koa 基础与源码学习,后续会开一期 koa 项目实战。本文基础部分目录结构按照阮一峰老师的 koa 教程(有作修动)。望本文能对您有所帮助!☀️


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 Contextnode 中原生的 requestresponse 对象封装到单个对象 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 RequestAccept 字段),然后使用 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.pathctx.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 RequestHTTP 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 文件上传

6.4 koa 更多模块使用


7. koa 源码目录解析

  • powershell 中使用命令
npm install koa # 安装 koa
  • node_modules 目录下找到 koa 目录

在这里插入图片描述

  • koa/lib

    • application.js (作为 koa 的入口文件,封装了 contextrequestresponse,以及最核心的中间件处理流程)
    • 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...
  • handleRequestthis 不是总是指向 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 中间件函数中第一个参数 ctxkoa 封装的一个对象,内部包含了 nodehttp 模块中原生的 reqres
// 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.reqctx.request 区别

  • 前者是 context 提供的 node 原生 Http 请求对象,后者是 contextkoa 封装的响应对象,且 ctx.request 对象可以获取原生 req 对象

  • 当访问 http://localhost:3000/users?id=1ctx.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
  • pathquery 等属性设置同理
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.definePropertyProxy 进行数据劫持,下述仍通过 __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.jsmain 中间件函数设置 ctx.body 为“你好”,访问 localhost:3000 页面显示“你好”
  • 如果不设置 ctx.body,页面无任何显示信息,而 koa 中页面提示 Not Foundctx.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 学习内容就这么多啦,如果您觉得内容不错的话,望您能关注🤞点赞👍收藏❤️一键三连!

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值