Node.js躬行记(20)——KOA源码分析(下)

简介: Node.js躬行记(20)——KOA源码分析(下)

  在上一篇中,主要分析了package.json和application.js文件,本文会分析剩下的几个文件。


一、context.js


  在context.js中,会处理错误,cookie,JSON格式化等。


1)cookie


  在处理cookie时,创建了一个Symbol类型的key,注意Symbol具有唯一性的特点,即 Symbol('context#cookies') === Symbol('context#cookies') 得到的是 false。

const Cookies = require('cookies')
const COOKIES = Symbol('context#cookies')
const proto = module.exports = {
  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
  }
}

  get在读取cookie,会初始化Cookies实例。


2)错误


  在默认的错误处理函数中,会配置头信息,触发错误事件,配置响应码等。

 onerror (err) {


// 可以绕过KOA的错误处理
    if (err == null) return
    // 在处理跨全局变量时,正常的“instanceof”检查无法正常工作
    // See https://github.com/koajs/koa/issues/1466
    // 一旦jest修复,可能会删除它 https://github.com/facebook/jest/issues/2549.
    const isNativeError =
      Object.prototype.toString.call(err) === '[object Error]' ||
      err instanceof Error
    if (!isNativeError) err = new Error(util.format('non-error thrown: %j', err))
    let headerSent = false
    if (this.headerSent || !this.writable) {
      headerSent = err.headerSent = true
    }
    // 触发error事件,在application.js中创建过监听器
    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
    // 首先取消设置所有header
    /* istanbul ignore else */
    if (typeof res.getHeaderNames === 'function') {
      res.getHeaderNames().forEach(name => res.removeHeader(name))
    } else {
      res._headers = {} // Node < 7.7
    }
    // 然后设置那些指定的
    this.set(err.headers)
    // 强制 text/plain
    this.type = 'text'
    let statusCode = err.status || err.statusCode
    // ENOENT support
    if (err.code === 'ENOENT') statusCode = 404
    // default to 500
    if (typeof statusCode !== 'number' || !statuses[statusCode]) statusCode = 500
    // 响应数据
    const code = statuses[statusCode]
    const msg = err.expose ? err.message : code
    this.status = err.status = statusCode
    this.length = Buffer.byteLength(msg)
    res.end(msg)
  },


3)属性委托


  在package.json中依赖了一个名为delegates的库,看下面这个示例。

  request是context的一个属性,在调delegate(context, 'request')函数后,就能直接context.querystring这么调用了。


const delegate = require('delegates')
const context = {
  request: {
    querystring: 'a=1&b=2'
  }
}
delegate(context, 'request')
  .access('querystring')
console.log(context.querystring)// a=1&b=2

  在KOA中,ctx可以直接调用request与response的属性和方法就是通过delegates实现的。


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')
  ...


二、request.js和response.js


  request.js和response.js就是为Node原生的req和res做一层封装。在request.js中都是些HTTP首部、IP、URL、缓存等。

/**
   * 获取 WHATWG 解析的 URL,并缓存起来
   */
  get URL () {
    /* istanbul ignore else */
    if (!this.memoizedURL) {
      const originalUrl = this.originalUrl || '' // avoid undefined in template string
      try {
        this.memoizedURL = new URL(`${this.origin}${originalUrl}`)
      } catch (err) {
        this.memoizedURL = Object.create(null)
      }
    }
    return this.memoizedURL
  },
  /**
   * 检查请求是否新鲜(有缓存),也就是
   * If-Modified-Since/Last-Modified 和 If-None-Match/ETag 是否仍然匹配
   */
  get fresh () {
    const method = this.method
    const s = this.ctx.status
    // GET or HEAD for weak freshness validation only
    if (method !== 'GET' && method !== 'HEAD') return false
    // 2xx or 304 as per rfc2616 14.26
    if ((s >= 200 && s < 300) || s === 304) {
      return fresh(this.header, this.response.header)
    }
    return false
  },

  response.js要复杂一点,会配置状态码、响应正文、读取解析的响应内容长度、302重定向等。

/**
   * 302 重定向
   * Examples:
   *    this.redirect('back');
   *    this.redirect('back', '/index.html');
   *    this.redirect('/login');
   *    this.redirect('http://google.com');
   */
  redirect (url, alt) {
    // location
    if (url === 'back') url = this.ctx.get('Referrer') || alt || '/'
    this.set('Location', encodeUrl(url))
    // status
    if (!statuses.redirect[this.status]) this.status = 302
    // html
    if (this.ctx.accepts('html')) {
      url = escape(url)
      this.type = 'text/html; charset=utf-8'
      this.body = `Redirecting to <a href="${url}">${url}</a>.`
      return
    }
    // text
    this.type = 'text/plain; charset=utf-8'
    this.body = `Redirecting to ${url}.`
  },
  /**
   * 设置响应正文
   */
  set body (val) {
    const original = this._body
    this._body = val
    // no content
    if (val == null) {
      if (!statuses.empty[this.status]) {
        if (this.type === 'application/json') {
          this._body = 'null'
          return
        }
        this.status = 204
      }
      if (val === null) this._explicitNullBody = true
      this.remove('Content-Type')
      this.remove('Content-Length')
      this.remove('Transfer-Encoding')
      return
    }
    // set the status
    if (!this._explicitStatus) this.status = 200
    // set the content-type only if not yet set
    const setType = !this.has('Content-Type')
    // string
    if (typeof val === 'string') {
      if (setType) this.type = /^\s*</.test(val) ? 'html' : 'text'
      this.length = Buffer.byteLength(val)
      return
    }
    // buffer
    if (Buffer.isBuffer(val)) {
      if (setType) this.type = 'bin'
      this.length = val.length
      return
    }
    // stream
    if (val instanceof Stream) {
      onFinish(this.res, destroy.bind(null, val))
      if (original !== val) {
        val.once('error', err => this.ctx.onerror(err))
        // overwriting
        if (original != null) this.remove('Content-Length')
      }
      if (setType) this.type = 'bin'
      return
    }
    // json
    this.remove('Content-Length')
    this.type = 'json'
  },
三、单元测试
  KOA的单元测试做的很细致,每个方法和属性都给出了相应的单测,它内部的写法很容易单测,非常值得借鉴。
  采用的单元测试框架是Jest,HTTP请求的测试库是SuperTest。断言使用了Node默认提供的assert模块。
const request = require('supertest')
const assert = require('assert')
const Koa = require('../..')
// request.js
describe('app.request', () => {
  const app1 = new Koa()
  // 声明request的message属性
  app1.request.message = 'hello'
  it('should merge properties', () => {
    app1.use((ctx, next) => {
      // 判断ctx中的request.message是否就是刚刚赋的那个值
      assert.strictEqual(ctx.request.message, 'hello')
      ctx.status = 204
    })
    // 发起GET请求,地址是首页,期待响应码是204
    return request(app1.listen())
      .get('/')
      .expect(204)
  })
})
相关文章
|
16天前
|
JavaScript 中间件 API
Node.js进阶:Koa框架下的RESTful API设计与实现
【10月更文挑战第28天】本文介绍了如何在Koa框架下设计与实现RESTful API。首先概述了Koa框架的特点,接着讲解了RESTful API的设计原则,包括无状态和统一接口。最后,通过一个简单的博客系统示例,详细展示了如何使用Koa和koa-router实现常见的CRUD操作,包括获取、创建、更新和删除文章。
35 4
|
6月前
|
前端开发 JavaScript 中间件
基于最新koa的Node.js后端API架构与MVC模式
基于最新koa的Node.js后端API架构与MVC模式
126 1
|
6月前
|
SQL 前端开发 JavaScript
前端vite+vue3结合后端node+koa——实现代码模板展示平台(支持模糊搜索+分页查询)
前端vite+vue3结合后端node+koa——实现代码模板展示平台(支持模糊搜索+分页查询)
153 4
|
6月前
|
安全 关系型数据库 MySQL
node实战——后端koa结合jwt连接mysql实现权限登录(node后端就业储备知识)
node实战——后端koa结合jwt连接mysql实现权限登录(node后端就业储备知识)
108 3
|
6月前
|
编解码 前端开发 JavaScript
node实战——koa实现文件下载和图片/pdf/视频预览(node后端储备知识)
node实战——koa实现文件下载和图片/pdf/视频预览(node后端储备知识)
289 1
|
6月前
|
存储 缓存 NoSQL
node实战——koa给邮件发送验证码并缓存到redis服务(node后端储备知识)
node实战——koa给邮件发送验证码并缓存到redis服务(node后端储备知识)
88 0
|
6月前
|
开发框架 JavaScript 前端开发
比较两个突出的node.js框架:koa和express
接上文讲述了 koa框架,这边文章比较一下这两个突出的node.js框架:koa和express
|
6月前
|
JavaScript 中间件 测试技术
|
2月前
|
JavaScript
NodeJs的安装
文章介绍了Node.js的安装步骤和如何创建第一个Node.js应用。包括从官网下载安装包、安装过程、验证安装是否成功,以及使用Node.js监听端口构建简单服务器的示例代码。
NodeJs的安装
|
1月前
|
JavaScript 开发工具 git
已安装nodejs但是安装hexo报错
已安装nodejs但是安装hexo报错
26 2