SSR 开发前端必学 nodejs 框架 -- Koa

·  阅读 1584
SSR 开发前端必学 nodejs 框架 -- Koa

Lynne,一个能吃爱笑永远少女心,注重养生之道的前端搬砖工程师。身处互联网浪潮之中,热爱生活与技术。

写在前面

现在的大环境下,前端不会一点 node 似乎都说不过去了,入行就开始接触 SSR 项目,了解 SSR 基本实现以及怎么用,也知道学习 node,但将近一年才开始去深入了解框架里面的内容。

服务端渲染基本采用 node 的 express 或 koa 框架,为了完善项目工程及业务流程,不得不需要很多中间件来处理,为了维护已有或者开发新的中间件。除此之外,为了更好地理解 SSR,理解同构,学习 node、学习 express 与 koa 势在必行。

声明

一篇 KOA 源码学习心得与新得,出发点是为了开发中间件,有手写 koa 源码。目前在阅读 express 中,试图对比二者的区别。

关于 KOA 的使用与API 用法在 「 官方文档 」koajs.com/),可结合「 中文文档 」koa.bootcss.com/)一起使用。

源码学习思路

Koa的核心文件一共有四个:application.js、context.js、request.js、response.js。所有的代码加起来不到 2000 行,十分轻便,而且大量代码集中在 request.js 和 response.js 对于请求头和响应头的处理,真正的核心代码只有几百行。

另外,为了更直观地梳理 koa 的运行原理和逻辑,还是通过调试来走一遍流程,本文将结合调试源码进行分析。

以下面代码调试为例:

const Koa = require('koa')
const app = new Koa()
const convert = require('koa-convert');

// 日志中间件
app.use(async(ctx, next) => {
  console.log('middleware before await');
  const start = new Date()
  await next();
  console.log('middleware after await');
  const ms = new Date() - start
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
})

app.use(async(ctx, next) => {
  console.log('response');
  ctx.body = "response"
})

app.listen(3000);
复制代码

node 的调试方式比较多,可参考 Node.js 调试大法稍做了解。

个人习惯使用命令行 node inspect index.js,简单直接,进入调试。

一、application

application.js 是 koa 的入口文件,里面导出了 koa 的构造函数,构造函数中包含了 koa 的主要功能实现。

导出一个构造函数 Application,这个构造函数对外提供功能 API 方法,从主要 API 方法入手分析功能实现。

1. listen

application 构造函数通过 node 中 http 模块,实现了 listen 功能:

/**
   * Shorthand for:
   *
   *  http.createServer(app.callback()).listen(...)
   *
   * @param {Mixed} ...
   * @return {Server}
   * @api public
*/

listen (...args) {
  debug('listen')
  const server = http.createServer(this.callback()) // 返回 http.Server 类的新实例,并使用this.callback()回调处理每个单独请求
  return server.listen(...args) // 启动 HTTP 服务器监听连接 实现 KOA 服务器监听连接
}
复制代码

2. use

use 方法将接收到的中间件函数,全部添加到了 this.middleware ,以便后面按顺序调用各个中间件,如果该方法接收了非函数类型将会报错 'middleware must be a function!'。

/**
   * Use the given middleware `fn`.
   *
   * Old-style middleware will be converted.
   *
   * @param {Function} fn
   * @return {Application} self
   * @api public
*/

use(fn) {
  if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
  // 对于 generator 类型的中间件函数,通过 koa-convert 库将其进行转换,以兼容 koa2 中的koa的递归调用。
  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 || '-');
  this.middleware.push(fn);
  return this;
}
复制代码

3. callback

上面 listen 函数在服务启动时,createServer 函数会返回 callback 函数的执行结果。

在服务启动时,callback函数执行将会实现中间件的合并以及监听框架层的错误请求等功能。

然后返回了 handleRequest 的方法,它接收 req 和 res 两个参数,每次服务端收到请求时,会根据 node http 原生的 req 和 res,创建一个新的 koa 的上下文 ctx。

/**
   * Return a request handler callback
   * for node's native http server.
   *
   * @return {Function}
   * @api public
*/

callback () {
  const fn = compose(this.middleware) // 通过 compose 合并中间件,后面结合koa-compose源码分析

  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
}
复制代码

在 application.js 中,通过 compose 将中间件进行了合并,是 koa 的一个核心实现。

可以看到 koa-compose 的源码,实现非常简单,只有几十行:

/** 
 * @param {Array} middleware 参数为 middleware 中间件函数数组, 数组中是一个个的中间件函数
 * @return {Function}
 * @api public
 */

function compose (middleware) { // compose函数需要传入一个函数数组队列 [fn,fn,fn...]
  // 如果传入的不是数组,则抛出错误
  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) {
    // 初始下标为-1
    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)
      }
    }
  }
}
复制代码

compose 接收一个中间件函数的数组,返回了一个闭包函数,闭包中维护了一个 index 去记录当前调用的中间件。

里面创建了一个 dispatch 函数,dispatch(i) 会通过 Promise.resolve() 返回 middleware 中的第 i 项函数执行结果,即第 i + 1 个 app.use() 传入的函数。 app.use() 回调的第二个参数是 next,所以当 app.use() 中的代码执行到 next() 时,便会执行 dispatch.bind(null, i + 1)),即执行下一个 app.use() 的回调。

依次类推,便将一个个 app.use() 的回调给串联了起来,直至没有下一个 next,边会按顺序返回执行每个 app.use() 的 next() 后面的逻辑。最终通过 Promise.resolve() 返回第一个 app.use() 的执行结果。这里可以结合洋葱模型去理解。

4. createContext

再来看 createContext 函数,一大串的赋值骚操作,我们细细解读一下:

  1. 已知从 context.js、request.js、response.js 引入对象context、request和response,并根据这三个对象通过 Object.create() 生成新的context、request和response对象,防止引入的原始对象被污染;

  2. 通过 context.request = Object.create(this.request) 和 context.response = Object.create(this.response) 将 request 和 response 对象挂载到了 context 对象上。这部分对应了 context.js 中delegate 的委托部分(有关 delegate 可见后面 koa 核心库部分的解读),能让 ctx 直接通过 ctx.xxx 去访问到 ctx.request.xxx 和 ctx.response.xxx;

  3. 通过一系列的赋值操作,将原始的 http 请求的 res 和 req,以及 Koa 实例app 等等分别挂载到了 context、request 和 response 对象中,以便于在 context.js、request.js 和response.js 中针对原始的请求、相应参数等做一些系列的处理访问,便于用户使用。

const response = require('./response')
const context = require('./context')
const request = require('./request')
/**
   * Initialize a new context.
   *
   * @api private
*/

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
  request.ctx = response.ctx = context
  request.response = response
  response.request = request
  context.originalUrl = request.originalUrl = req.url
  context.state = {}
  return context
}
复制代码

5. handleRequest

callback 中执行完 createContext 后,会将创建好的 ctx 以及合并中间件后生成的顺序执行函数传给 handleRequest 并执行该函数。

handleRequest 中会通过 onFinished 这个方法监听 res,当 res 完成、关闭或者出错时,便会执行 onerror 回调。 之后返回中间件执行的结果,当中间件全部执行完之后,执行 respond 进行数据返回操作。

/**
   * Handle request in callback.
   *
   * @api private
*/

handleRequest (ctx, fnMiddleware) {
  const res = ctx.res
  res.statusCode = 404
  const onerror = err => ctx.onerror(err)
  const handleResponse = () => respond(ctx)
  onFinished(res, onerror)
  return fnMiddleware(ctx).then(handleResponse).catch(onerror)
}
复制代码

6. toJSON

 /**
   * 返回JSON表示
   * only() - only方法返回对象的白名单属性
   * @return {Object}
   * @api public
 */

  toJSON () {
    return only(this, [
      'subdomainOffset',
      'proxy',
      'env'
    ])
  }

  /**
   * Inspect implementation.
   *
   * @return {Object}
   * @api public
 */

inspect () {
  return this.toJSON()
}
复制代码

7. respond

/**
 * Response helper.
 */

function respond (ctx) {
  // allow bypassing koa
  if (ctx.respond === false) return

  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()
  }

  if (ctx.method === 'HEAD') {
    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 (body == null) {
    if (ctx.response._explicitNullBody) {
      ctx.response.remove('Content-Type')
      ctx.response.remove('Transfer-Encoding')
      ctx.length = 0
      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
  if (Buffer.isBuffer(body)) return res.end(body)
  if (typeof body === 'string') return res.end(body)
  if (body instanceof Stream) return body.pipe(res)

  // body: json
  body = JSON.stringify(body)
  if (!res.headersSent) {
    ctx.length = Buffer.byteLength(body)
  }
  res.end(body)
}
复制代码

二、context.js

1. cookie

context.js 中通过 get 和 set 方法做了 cookie 的设置和读取操作。

// 获取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]
},
// 设置cookie
set cookies (_cookies) {
  this[COOKIES] = _cookies
}
复制代码

2. delegate

context.js 中有大量的 delegate 操作。

通过 delegate,可以让 ctx 能够直接访问其上面 response 和 request 中的属性和方法,即可以通过 ctx.xxx 获取到 ctx.request.xxx 或 ctx.response.xxx 。

delegate 是通过 delegates 这个库实现的,通过 proto.defineGetter 和 proto.defineSetter 去代理对象下面节点的属性和方法等。(proto.defineGetter 和 proto.defineSetter 现已被 mdn 废弃,改用 Object.defineProperty())

const delegate = require('delegates')

delegate(proto, 'response')
  .method('attachment')
  .method('redirect')
  .access('lastModified')
  .access('etag')
  .getter('headerSent')
  .getter('writable');
  // ...

delegate(proto, 'request')
  .method('acceptsLanguages')
  .getter('ip');
  // ...
复制代码

context.js 中导出了一个 context 对象,主要用来在中间件以及其它各部件之间传递信息的,同时 context 对象上挂载了 request 和 response 两大对象。

使用 delegates 库对 request 和 response 对象上面的事件和方法进行了委托,便于用户使用。

3. toJSON()

在每个对象上显式调用.toJSON(),否则迭代将由于getter而失败,并导致clone()等实例程序失败。

/**
   * 返回JSON表示
   *
   * @return {Object}
   * @api public
*/

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>'
  }
}
复制代码

三、request 和 response

1. request

request.js 导出了 request 对象,通过 get() 和 set() 方法对请求头的参数如 header、url、href、method、path、query……做了处理,挂载到了 request 对象上,方便用户获取和设置。

这里基本属于那种一看就能懂的简单代码,本着学习巩固基础的心态,简单举几个例子看看:

 /**
   * Get origin of URL.
   *
   * @return {String}
   * @api public
*/

get origin () { // 协议+主机名
  return `${this.protocol}://${this.host}`
},

/**
  * Get full request URL.
  * @return {String}
  * @api public
*/

get href () { // 正则获取当前URl=域名+url
    // support: `GET http://example.com/foo`
    if (/^https?:///i.test(this.originalUrl)) return this.originalUrl
    return this.origin + this.originalUrl
},
  
/**
   * Get parsed query string.
   *
   * @return {Object}
   * @api public
   */

  get query () {
    const str = this.querystring
    const c = this._querycache = this._querycache || {}
    return c[str] || (c[str] = qs.parse(str))
  },

 /**
   * Parse the "Host" header field host
   * and support X-Forwarded-Host when a
   * proxy is enabled.
   *
   * @return {String} hostname:port
   * @api public
   */

  get host () { // 获取主机名,注意有代理的情况
    const proxy = this.app.proxy
    let host = proxy && this.get('X-Forwarded-Host')
    if (!host) {
      if (this.req.httpVersionMajor >= 2) host = this.get(':authority')
      if (!host) host = this.get('Host')
    }
    if (!host) return ''
    return host.split(/\s*,\s*/, 1)[0]
  },
复制代码

2. response(get 新知识)

同 request.js ,通过 get() 和 set()对响应参数做了处理。此处经大佬点播,源码要多看,不能一直人云亦云,才能发现有意义的问题。

set body (val) {
    const original = this._body
    this._body = val

    // no content
    if (val == null) {
      ...
      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') {
      ...
      return
    }

    // buffer
    if (Buffer.isBuffer(val)) {
      ...
      return
    }

    // stream
    if (val instanceof Stream) {
      ...
      return
    }

    // json
    this.remove('Content-Length')
    this.type = 'json'
},
复制代码

在 response.body 的 set 访问器中,对 response.type 进行了赋值。

如果我们将 type 的设置写在设置 body 的后面:

app.use(async(ctx, next) => {
  console.log('response');

  ctx.body = {'aaa': '1111'};
  ctx.type = 'html'

  console.log(ctx.type, 'type')
})
复制代码

ok,拿到的type是 text/html。

但如果 body 的内容格式匹配不到,写在后面response.type最终被覆盖为json,也就是实际上你会拿到的 type 为 application/json:

app.use(async(ctx, next) => {
  console.log('response');
  ctx.type = 'html'
  ctx.body = {'aaa': '1111'};

  console.log(ctx.type, 'type')
})
复制代码

最合理的顺序肯定是先 赋值 response.type,毕竟 type 处理成 json 格式只是一种兜底逻辑。

四、手写 Koa

1. 封装 node 的 http 模块

以 koa 文档简单示例为参考,新建 application.js,内部创建一个 MyKoa 类,基于 node 的 http 模块,实现 listen 函数:

// application.js

const http = require('http');

class MyKoa {
  listen(...args) {
    const server = http.createServer((req, res) => {
      res.end('mykoa')
    });
    server.listen(...args);
  }
}

module.exports = MyKoa;
复制代码

2. 实现 use 方法和简易 createContext

然后实现 app.use() 方法,由于 app.use() 中内部有 ctx.body,所以我们还需要实现一个简单的 ctx 对象。

1) 创建一个 context.js,内部导出 ctx 对象,分别通过 get 和 set,实现可以获取和设置 ctx.body 的值:

// context.js
module.exports = {
  get body() {
    return this._body;
  },

  set body(value) {
    this._body = value;
  },
}
复制代码
  1. 在 application.js 的 MyKoa 类中添加 use 和 createContext 方法,同时 res.end 返回 ctx.body:
const http = require('http');
const _context = require('./context');

class MyKoa {

  listen(...args) {
    const server = http.createServer((req, res) => {
      const ctx = this.createContext(req, res);
      this.callback();
      res.end(ctx.body);
    });
    server.listen(...args);
  }

  use(callback) {
    this.callback = callback;
  }

  createContext(req, res) {
    const ctx = Object.assign(_context);
    return ctx;
  }
}

module.exports = MyKoa;
复制代码

3. 完善 createContext

要通过 ctx 去访问请求头以及设置响应头等相关信息,例如 ctx.query,ctx.message 等等,就要创建 response.js 和 request.js 对请求头和响应头做处理,将 request 和 response 对象挂载到 ctx 对象上,同时实现一个 delegate 函数让 ctx 能够访问 request 和 response 上面的属性和方法。

  1. 实现简单的 request 和 response,request 中通过 get 方法,能够解析 req.url 中的参数,将其转换为一个对象返回。
// request.js
module.exports = {
  get header() {
    return this.req.headers
  },
  get method() {
    return this.req.method
  },
  get url() {
    return this.req.url
  },
  get query() {
    const arr = this.req.url.split('?');
    if (arr[1]) {
      const obj = {};
      arr[1].split('&').forEach((str) => {
        const param = str.split('=');
        obj[param[0]] = param[1];
      });
      return obj;
    }
    return {};
  },
};
复制代码

response中,通过 get 和 set message,能够获取和设置 res.statusMessage 的值:

// response.js
module.exports = {
  get status() {
    return this.res.statusCode || '';
  },
  get message() {
    return this.res.statusMessage || '';
  },

  set status(code) {
    return this.res.statusCode = code;
  },
  set message(msg) {
    this.res.statusMessage = msg;
  }
};
复制代码
  1. 新建一个 utils.js,导出 delegate 方法,delegate 内部通过 Object.defineProperty ,让传入的对象 obj 能够在属性 property 改变时实时监听,例如 delegate(ctx, 'request') 当 request 对象值改变时,ctx 对 request 代理也能获取最新的值。

然后实现简单的 getter 和 setter,通过 listen 函数,当使用 getter 或者 setter 时,将对应的键添加到 setters 和 getters 中,让 obj 访问对应键时代理到 proterty 对应的键值:

// utils.js
module.exports.delegate = function Delegate(obj, property) {
  let setters = []; 
  let getters = [];
  let listens = [];

  function listen(key) {
    Object.defineProperty(obj, key, {
      get() {
        return getters.includes(key) ? obj[property][key] : obj[key]; // 如果通过 getter 代理了,则返回对应 obj[property][key] 的值,否则返回 obj[key] 的值
      },
      set(val) {
        if (setters.includes(key)) {
          obj[property][key] = val; // 如果通过 setter 代理了,则设置对应 obj[property][key] 的值,否则设置 obj[key] 的值
        } else {
          obj[key] = val;
        }
      },
    });
  }

  this.getter = function (key) {
    getters.push(key);
    if (!listens.includes(key)) { // 防止重复调用listen
      listen(key);
      listens.push(key);
    }
    return this;
  };

  this.setter = function (key) {
    setters.push(key);
    if (!listens.includes(key)) { // 防止重复调用listen
      listen(key);
      listens.push(key);
    }
    return this;
  };
  return this;
};
复制代码
  1. 在 context 使用 delegate 方法对 request 和 response 进行代理:
// context.js
const { delegate } = require('./utils');
const context = (module.exports = {
  get body() {
    return this._body;
  },

  set body(value) {
    this._body = value;
  },
});

// 使用 delegate 对 request 和 response 进行代理
delegate(context, 'request').getter('header');
delegate(context, 'request').getter('method');
delegate(context, 'request').getter('url');
delegate(context, 'request').getter('query');
delegate(context, 'response').getter('status').setter('status');
delegate(context, 'response').getter('message').setter('message');
复制代码
  1. 完善 createContext 函数:

将 req和res 挂载到 ctx 上

// application.js
const http = require('http');
const _context = require('./context');
const _request = require('./request');
const _response = require('./response');

class MyKoa {
  // ...
  createContext(req, res) {
    const ctx = Object.assign(_context);
    const request = Object.assign(_request);
    const response = Object.assign(_response);
    ctx.request = request;
    ctx.response = response;
    ctx.req = request.req = req;
    ctx.res = response.res = res;
    return ctx;
  }
}

module.exports = MyKoa;
复制代码

4. 实现中间件和洋葱模型

最后再来实现 app.use() 中间件的功能:

  1. 按照前面 koa-compose 分析的思路,在 utils.js 中,通过 compose 实现洋葱模型:
// utils.js
module.exports.compose = (middleware) => {
  return (ctx, next) => {
    let index = -1;
    return dispatch(0);
    function dispatch(i) {
      if (i <= index) return Promise.reject(new Error('error'));
      index = i;
      const cb = middleware[i] || next;
      if (!cb) return Promise.resolve();
      try {
        return Promise.resolve(
          cb(ctx, function next() {
            return dispatch(i + 1);
          })
        );
      } catch (error) {
        return Promise.reject(error);
      }
    }
  };
};
复制代码
  1. 在 appcation.js 中,初始化 this.middleware 的数组,并在 use() 函数中将 callback 添加进数组:
// application.js
class MyKoa {
  constructor() {
    this.middleware = [];
  }
  // ...

  use(callback) {
    this.middleware.push(callback);
  }
  // ...
}

module.exports = MyKoa;
复制代码
  1. listen 方法 createServer 中,遇到请求时将中间件合并,中间件执行完毕后返回 res 结果:
// application.js
const { compose } = require('./utils');

class MyKoa {
  listen(...args) {
    const server = http.createServer((req, res) => {
      const ctx = this.createContext(req, res);
      // 获取执行中间件函数
      const fn = compose(this.middleware);
      fn(ctx)
        .then(() => { // 全部中间件执行完毕后,返回相应信息
          res.end(ctx.body);
        })
        .catch((err) => {
          throw err;
        });
    });
    server.listen(...args);
  }
  // ...
}
module.exports = MyKoa;
复制代码

5. 测试

引入我们的 Mykoa 在如下服务中测试一下:

const Koa = require('../my-koa/application');
const app = new Koa();

app.use((ctx, next) => {
  ctx.message = 'ok';
  console.log(1);
  next();
  console.log(2);
});

app.use((ctx, next) => {
  console.log(3);
  next();
  console.log(4);
});

app.use((ctx, next) => {
  console.log(5);
  next();
  console.log(6);
});

app.use((ctx, next) => {
  console.log(ctx.header);
  console.log(ctx.method);
  console.log(ctx.url);
  console.log(ctx.query);
  console.log(ctx.status);
  console.log(ctx.message);
  ctx.body = 'hello, my-koa-demo';
});

app.listen(3000, () => {
  console.log('server is running on 3000...');
});
复制代码

访问 http://localhost:3000/api?name=zlx 接口,返回数据为hello, my-koa-demo 。同时 node 服务器控制台打印内容如下:

1
3
5
{
  host: '127.0.0.1:3000',
  connection: 'keep-alive',
  'cache-control': 'max-age=0',
  'sec-ch-ua': '"Google Chrome";v="95", "Chromium";v="95", ";Not A Brand";v="99"',
  'sec-ch-ua-mobile': '?0',
  'sec-ch-ua-platform': '"macOS"',
  'upgrade-insecure-requests': '1',
  'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36',
  accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
  'sec-fetch-site': 'none',
  'sec-fetch-mode': 'navigate',
  'sec-fetch-user': '?1',
  'sec-fetch-dest': 'document',
  'accept-encoding': 'gzip, deflate, br',
  'accept-language': 'en-US,en;q=0.9'
}
GET
/?a=1&b=2
{ a: '1', b: '2' }
200
ok
6
4
2
复制代码

同时还会打印一条请求路径为 /favicon.ico 的信息,说明发起了两次页面请求。

源码调试 - github.com/Lynn-zuo/no…

学习路线与心得

1. 路线图:

学习 node.js 基本知识(API手册了解,部分理解) --> 通读一本node.js相关的书 --> 学习框架(结合项目了解API用法)--> 阅读框架源码 --> 手写简单框架实现

2. 心得

相比于复杂的前端框架 vue 源码和打包工具 webpack,或 rollup,甚至相对简单的 node 框架 express,koa 的源码堪称简洁明了。

但同样的, koa 提供的功能有限,具体功能要通过调用插件来完成,比如路由配置需要安装 koa-router 中间件来实现,koa 本身可以读取/设置 cookie,但对于 session 的处理需要结合 koa-session 中间件来实现。

阅读 koa 源码 及 express 源码可知:

  1. 不同于 express 可支持处理指定路由 的请求,koa 的 app.use() 方法本身并不支持针对特定路由进行处理,必须通过安装 koa-router 中间件来实现;

  2. 同样都是对中间件进行链式调用,express 在同步请求中调用 next() 也可实现类似 koa 的洋葱圈效果,但无法同样处理异步请求,这是两者框架对于中间件处理的差别。

3. express 阅读中。。

第1遍感觉看了个寂寞,第2遍中...

分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改