koa和koa-router的使用及源码实现

x33g5p2x  于2021-09-19 转载在 其他  
字(9.4k)|赞(0)|评价(0)|浏览(628)

1. 前言

鉴于之前使用expresskoa的经验,最近想尝试构建出一个koa精简版,利用最少的代码实现koa和koa-router,同时也梳理一下Node.js网络框架开发的核心内容。

实现的源代码将会放在文末,配有详细的注释。

2. 核心设计

2.1 API调用

mini-koa的API设计中,参考koa和koa-routerAPI调用方式。

Node.js的网络框架封装其实并不复杂,其核心点在于http/httpscreateServer方法上,这个方法是http请求的入口。

首先,我们先回顾一下用Node.js来启动一个简单服务。

  1. const http = require('http')
  2. const app = http.createServer((request, response) => {
  3. response.end('hello Node.js')
  4. })
  5. app.listen(3333, () => {
  6. console.log('App is listening at port 3333...')
  7. })

2.2 路由原理

既然我们知道Node.js的请求入口在createServer方法上,那么我们可以在这个方法中找出请求的地址,然后根据地址映射出监听函数(通过get/post等方法添加的路由函数)即可。

其中,路由列表的格式设计如下:

  1. // binding的格式
  2. {
  3. '/': [fn1, fn2, ...],
  4. '/user': [fn, ...],
  5. ...
  6. }
  7. // fn/fn1/fn2的格式
  8. {
  9. method: 'get/post/use/all',
  10. fn: '路由处理函数'
  11. }

3. 难点分析

3.1 next()方法设计

我们知道在koa中是可以添加多个url监听函数的,其中决定是否传递到下一个监听函数的关键在于是否调用了next()函数。如果调用了next()函数则先把路由权转移到下一个监听函数中,处理完毕再返回当前路由函数。

mini-koa中,我把next()方法设计成了一个返回Promise fullfilled的函数(这里简单设计,不考虑next()传参的情况),用户如果调用了该函数,那么就可以根据它的值来决定是否转移路由函数处理权。

判断是否转移路由函数处理权的代码如下:

  1. let isNext = false
  2. const next = () => {
  3. isNext = true
  4. return Promise.resolve()
  5. }
  6. await router.fn(ctx, next)
  7. if (isNext) {
  8. continue
  9. } else {
  10. // 没有调用next,直接中止请求处理函数
  11. return
  12. }

3.2 use()方法设计

mini-koa提供use方法,可供扩展日志记录/session/cookie处理等功能。

use方法执行的原理是根据请求地址在执行特定路由函数之前先执行mini-koa调用use监听的函数

所以这里的关键点在于怎么找出use监听的函数列表,假设现有监听情况如下:

  1. app.use('/', fn1)
  2. app.use('/user', fn2)

如果访问的url/user/add,那么fn1和fn2都必须要依次执行。

我采取的做法是先根据/字符来分割请求url,然后循环拼接,查看路由绑定列表(binding)中有没有要use的函数,如果发现有,添加进要use的函数列表中,没有则继续下一次循环。

详细代码如下:

  1. // 默认use函数前缀
  2. let prefix = '/'
  3. // 要预先调用的use函数列表
  4. let useFnList = []
  5. // 分割url,使用use函数
  6. // 比如item为/user/a/b映射成[('user', 'a', 'b')]
  7. const filterUrl = url.split('/').filter(item => item !== '')
  8. // 该reduce的作用是找出本请求要use的函数列表
  9. filterUrl.reduce((cal, item) => {
  10. prefix = cal
  11. if (this.binding[prefix] && this.binding[prefix].length) {
  12. const filters = this.binding[prefix].filter(router => {
  13. return router.method === 'use'
  14. })
  15. useFnList.push(...filters)
  16. }
  17. return (
  18. '/' +
  19. [cal, item]
  20. .join('/')
  21. .split('/')
  22. .filter(item => item !== '')
  23. .join('/')
  24. )
  25. }, prefix)

3.3 ctx.body响应

通过ctx.body = '响应内容'的方式可以响应http请求。它的实现原理是利用了ES6Object.defineProperty函数,通过设置它的setter/getter函数来达到数据追踪的目的。

详细代码如下:

  1. // 追踪ctx.body赋值
  2. Object.defineProperty(ctx, 'body', {
  3. set(val) {
  4. // set()里面的this是ctx
  5. response.end(val)
  6. },
  7. get() {
  8. throw new Error(`ctx.body can't read, only support assign value.`)
  9. }
  10. })

3.4 子路由mini-koa-router设计

子路由mini-koa-router设计这个比较简单,每个子路由维护一个路由监听列表,然后通过调用mini-koaaddRoutes函数添加到主路由列表上。

mini-koaaddRoutes实现如下:

  1. addRoutes(router) {
  2. if (!this.binding[router.prefix]) {
  3. this.binding[router.prefix] = []
  4. }
  5. // 路由拷贝
  6. Object.keys(router.binding).forEach(url => {
  7. if (!this.binding[url]) {
  8. this.binding[url] = []
  9. }
  10. this.binding[url].push(...router.binding[url])
  11. })
  12. }

4. 用法

使用示例如下:

  1. // examples_server.js
  2. const { Koa, KoaRouter } = require('../index')
  3. const app = new Koa()
  4. // 路由用法
  5. const userRouter = new KoaRouter({
  6. prefix: '/user'
  7. })
  8. // 中间件函数
  9. app.use(async (ctx, next) => {
  10. console.log(`请求url, 请求method: `, ctx.req.url, ctx.req.method)
  11. await next()
  12. })
  13. // 方法示例
  14. app.get('/get', async ctx => {
  15. ctx.body = 'hello ,app get'
  16. })
  17. app.post('/post', async ctx => {
  18. ctx.body = 'hello ,app post'
  19. })
  20. app.all('/all', async ctx => {
  21. ctx.body = 'hello ,/all 支持所有方法'
  22. })
  23. // 子路由使用示例
  24. userRouter.post('/login', async ctx => {
  25. ctx.body = 'user login success'
  26. })
  27. userRouter.get('/logout', async ctx => {
  28. ctx.body = 'user logout success'
  29. })
  30. userRouter.get('/:id', async ctx => {
  31. ctx.body = '用户id: ' + ctx.params.id
  32. })
  33. // 添加路由
  34. app.addRoutes(userRouter)
  35. // 监听端口
  36. app.listen(3000, () => {
  37. console.log('> App is listening at port 3000...')
  38. })

5. 总结

本次实现的精简版mini-koa,虽然跟常用的koa框架有很大区别,但是也实现了最基本的API调用和原理。

造轮子是一件难能可贵的事,程序员在学习过程中不应该一直崇尚拿来主义,学习到一定程度后,在自己的个人项目中,可以秉持能造就造的态度,去尝试理解和挖掘源码背后的原理和思想。

当然,通常来说,自己造的轮子本身不具备多大的实用性,没有经历过社区大量的测试和实际应用场景的打磨,但是能加深自己的理解和提高自己的能力也是一件值得坚持的事。

附录:源代码

mini-koa-router.js:

  1. /** * koa-router精简版 * @author Mask */
  2. class KoaRouter {
  3. /** * 构造函数 * @param {object} props 路由参数配置 */
  4. constructor(props) {
  5. // 路由前缀
  6. this.prefix = props.prefix || '/'
  7. // 路由监听列表
  8. this.binding = {}
  9. }
  10. /** * * @param {string} method 请求方法 * @param {string} url 请求的路由 * @param {function} callback 请求回调函数 */
  11. request(method, url, callback) {
  12. if (typeof url === 'function') {
  13. // 简单判断没有传入url
  14. callback = url
  15. url = '/'
  16. }
  17. if (this.prefix) {
  18. // 添加路由实例有前缀
  19. url =
  20. '/' +
  21. [this.prefix, url]
  22. .join('/')
  23. .split('/')
  24. .filter(item => item)
  25. .join('/')
  26. }
  27. if (!this.binding[url]) {
  28. this.binding[url] = []
  29. }
  30. this.binding[url].push({
  31. method: method,
  32. fn: callback
  33. })
  34. }
  35. /** * 中间件函数,可用作日志记录等等 * @param {string} url 请求的路由 * @param {function} callback 请求回调函数 */
  36. use(url, callback) {
  37. this.request('use', url, callback)
  38. }
  39. /** * 监听所有的请求方法,包括get/post等等 * @param {string} url 请求的路由 * @param {function} callback 请求回调函数 */
  40. all(url, callback) {
  41. this.request('all', url, callback)
  42. }
  43. /** * 监听get请求 * @param {string} url 请求的路由 * @param {function} callback 请求回调函数 */
  44. get(url, callback) {
  45. this.request('get', url, callback)
  46. }
  47. /** * 监听post请求 * @param {string} url 请求的路由 * @param {function} callback 请求回调函数 */
  48. post(url, callback) {
  49. this.request('post', url, callback)
  50. }
  51. }
  52. module.exports = KoaRouter

mini-koa.js:

  1. const http = require('http')
  2. /** * 解析url的查询参数,比如/a?name=123&pwd=456 解析成 {name: 123, pwd: 456} * @param {string} url 请求路径 */
  3. const parseUrlParams = url => {
  4. const query = {}
  5. const index = url.indexOf('?')
  6. if (index < 0) {
  7. return query
  8. }
  9. url = url.substring(index + 1)
  10. url.split('&').forEach(function(item) {
  11. let obj = item.split('=')
  12. query[obj[0]] = obj[1] || undefined
  13. })
  14. return query
  15. }
  16. /** * Koa精简版 * @author Mask */
  17. class Koa {
  18. /** * 构造函数 */
  19. constructor() {
  20. // 路由监听列表
  21. this.binding = {}
  22. // 监听实例
  23. this.httpApp = null
  24. // 初始化
  25. this.init()
  26. }
  27. /** * 初始化 */
  28. init() {
  29. // 这里要绑定this,不然requestServer里面的this是Server实例
  30. this.httpApp = http.createServer(this._requestServer.bind(this))
  31. }
  32. /** * http请求函数 * @param {*} request * @param {*} response */
  33. async _requestServer(request, response) {
  34. // 本次请求的环境
  35. const ctx = {}
  36. request.query = {}
  37. request.params = {}
  38. ctx.req = request
  39. ctx.request = request
  40. ctx.res = response
  41. ctx.response = response
  42. ctx.query = request.query
  43. ctx.params = request.params
  44. // 设置一些默认响应头
  45. response.statusCode = 200
  46. response.setHeader('Content-Type', 'text/plain;charset=utf-8')
  47. response.setHeader('Access-Control-Allow-Origin', '*')
  48. response.setHeader(
  49. 'Access-Control-Allow-Methods',
  50. 'PUT,POST,GET,DELETE,OPTIONS'
  51. )
  52. // 追踪ctx.body赋值
  53. Object.defineProperty(ctx, 'body', {
  54. set(val) {
  55. // set()里面的this是ctx
  56. response.end(val)
  57. },
  58. get() {
  59. throw new Error(`ctx.body can't read, only support assign value.`)
  60. }
  61. })
  62. // 解析url,获取查询参数,类似/a?name=123&pwd=456
  63. const method = request.method
  64. const rawUrl = request.url
  65. const resUrl = rawUrl.match(/(\/[^?&=]*)/i)
  66. let url = rawUrl
  67. if (resUrl) {
  68. url = resUrl[1]
  69. }
  70. // 解析参数,需要重新指向ctx.query,不然追踪会断掉
  71. request.query = parseUrlParams(rawUrl)
  72. ctx.query = request.query
  73. // 默认use函数前缀
  74. let prefix = '/'
  75. // 要预先调用的use函数列表
  76. let useFnList = []
  77. // 分割url,使用use函数
  78. // 比如item为/user/a/b映射成[('user', 'a', 'b')]
  79. const filterUrl = url.split('/').filter(item => item !== '')
  80. // 该reduce的作用是找出本请求要use的函数列表
  81. filterUrl.reduce((cal, item) => {
  82. prefix = cal
  83. if (this.binding[prefix] && this.binding[prefix].length) {
  84. const filters = this.binding[prefix].filter(router => {
  85. return router.method === 'use'
  86. })
  87. useFnList.push(...filters)
  88. }
  89. return (
  90. '/' +
  91. [cal, item]
  92. .join('/')
  93. .split('/')
  94. .filter(item => item !== '')
  95. .join('/')
  96. )
  97. }, prefix)
  98. // 1 调用use函数列表,可以做日志记录等等
  99. if (useFnList.length) {
  100. for (let i = 0, length = useFnList.length; i < length; i++) {
  101. let router = useFnList[i]
  102. let isNext = false
  103. const next = () => {
  104. isNext = true
  105. return Promise.resolve()
  106. }
  107. await router.fn(ctx, next)
  108. if (isNext) {
  109. continue
  110. } else {
  111. // 没有调用next,直接中止请求处理函数
  112. return
  113. }
  114. }
  115. }
  116. // 2 遍历特定的路由监听函数
  117. const routerList = []
  118. // 2.1 添加具体匹配路由函数
  119. if (this.binding[url] && this.binding[url].length) {
  120. routerList.push(...this.binding[url])
  121. }
  122. // 2.2 添加模糊路由监听函数,比如请求的url为'/post/123',可以映射到'/post/:id'监听上
  123. let bindingUrlList = Object.keys(this.binding).map(item => {
  124. // 比如item为/user/a/b映射成['user', 'a', 'b']
  125. return item.split('/').filter(i => i !== '')
  126. })
  127. // 模糊判断,过滤路由参数长度不同的项
  128. bindingUrlList = bindingUrlList.filter(item => {
  129. return item.length === filterUrl.length
  130. })
  131. // 具体过滤,存在的路由监听函数是否匹配
  132. filterUrl.forEach((key, index) => {
  133. bindingUrlList = bindingUrlList.filter(item => {
  134. if (item[index].startsWith(':')) {
  135. // 这一项参数是查询参数(类似:id),挂载到request.params上
  136. let variableName = item[index].replace(':', '')
  137. request.params[variableName] = key
  138. return true
  139. } else if (item[index] === key) {
  140. // 值相等,不是查询参数
  141. return true
  142. } else {
  143. // 只有长度一致
  144. return false
  145. }
  146. })
  147. })
  148. // 根据过滤后的模糊路由来添加路由监听函数
  149. bindingUrlList.forEach(item => {
  150. let url = '/' + item.join('/')
  151. if (this.binding[url] && this.binding[url].length) {
  152. routerList.push(...this.binding[url])
  153. }
  154. routerList.push(...this.binding[url])
  155. })
  156. // 3 执行匹配路由
  157. if (routerList.length) {
  158. // 执行
  159. for (let i = 0, length = routerList.length; i < length; i++) {
  160. let router = routerList[i]
  161. if (router.method === method.toLowerCase() || router.method === 'all') {
  162. // 新的ctx
  163. let isNext = false
  164. const next = () => {
  165. isNext = true
  166. return Promise.resolve()
  167. }
  168. await router.fn(ctx, next)
  169. // 如果调用了next,则传递到下一个
  170. if (isNext) {
  171. continue
  172. } else {
  173. // 没有调用next,直接中止请求处理函数
  174. return
  175. }
  176. }
  177. }
  178. // 函数没有中断,不支持的方法
  179. response.statusCode = 404
  180. ctx.body = `不支持的方法 - ${method}`
  181. } else {
  182. // 没有监听
  183. response.statusCode = 404
  184. ctx.body = `${url}不存在`
  185. }
  186. }
  187. /** * * @param {string} method 请求方法 * @param {string} url 请求的路由 * @param {function} callback 请求回调函数 */
  188. request(method, url, callback) {
  189. if (typeof url === 'function') {
  190. // 简单判断没有传入url
  191. callback = url
  192. url = '/'
  193. }
  194. if (!this.binding[url]) {
  195. this.binding[url] = []
  196. }
  197. this.binding[url].push({
  198. method: method,
  199. fn: callback
  200. })
  201. }
  202. /** * 中间件函数,可用作日志记录等等 * @param {string} url 请求的路由 * @param {function} callback 请求回调函数 */
  203. use(url, callback) {
  204. this.request('use', url, callback)
  205. }
  206. /** * 监听所有的请求方法,包括get/post等等 * @param {string} url 请求的路由 * @param {function} callback 请求回调函数 */
  207. all(url, callback) {
  208. this.request('all', url, callback)
  209. }
  210. /** * 监听get请求 * @param {string} url 请求的路由 * @param {function} callback 请求回调函数 */
  211. get(url, callback) {
  212. this.request('get', url, callback)
  213. }
  214. /** * 监听post请求 * @param {string} url 请求的路由 * @param {function} callback 请求回调函数 */
  215. post(url, callback) {
  216. this.request('post', url, callback)
  217. }
  218. /** * 监听端口 * @param {...any} args */
  219. listen(...args) {
  220. this.httpApp.listen(...args)
  221. }
  222. /** * 添加子路由 * @param {MoaRouter} router */
  223. addRoutes(router) {
  224. if (!this.binding[router.prefix]) {
  225. this.binding[router.prefix] = []
  226. }
  227. // 路由拷贝
  228. Object.keys(router.binding).forEach(url => {
  229. if (!this.binding[url]) {
  230. this.binding[url] = []
  231. }
  232. this.binding[url].push(...router.binding[url])
  233. })
  234. }
  235. }
  236. module.exports = Koa

作者:mask.qi

相关文章