一、前言
本篇文章主要面对的人群是1到3年初级初中级前端工程师
, 希望和你一起学习。
为什么要学源码 ?
当我学了 Vue、React、Node 的日常开发和应用后, 好像遇到了一些瓶颈, 不知道如何继续深入下去。
网络上前端学习路线层出不穷, 也有借着前端学习路线贩卖焦虑的公众号推文。
我在结合众多前端路线的基础上, 慢慢的也有了自己的想法: 前端工程师首先需要是软件工程师, 对于非计算机科班出身的前端码农来说,有一些场景你是否熟悉,比如在日常工作或深入学习前端知识时,自己总结的某条经验或分析许久晦涩难懂的知识, 其实是对应计算机领域早有的某条理论,也就是说我们需要 “知其然,也要知其所以然” ,毕竟前端的技术发展都是基于计算机基础。其次,需要有自己深入探究的技术领域,而学习框架源码是跟作者对话,体会优秀技术人的思路和逻辑,能够更快地提升技术水平。
为什么是 Koa ?
看源码只是一种方法、手段,而不是目的,本次分享的是 Koa 源码解读。
在 Node 学习过程中,Koa 框架占据举足轻重的一环,身为 HTTP 中间件框架,使得使 Web 应用程序和 API 更易于编写。而在面试中这一环也被时常提起, 如 “你是否了解 Node 中间件机制 ?”, “koa 洋葱模型怎么实现的?”。
而在学习成本来看,Koa 不同于 Vue、React 这种大型框架,而是代码精简、整体实现简单、巧妙、易于扩展小型框架。
企业级 Node 框架和应用的 Egg.js
也是基于 Koa 的封装,
Koa 是什么 ?有哪些部分组成 ?
Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。
当我们从github Koa 仓库克隆下 Koa 框架源码, 通过 lib 文件可以看到只有application.js
、context.js
、request.js
、response.js
四部分, Koa 正是由这四部分组成。
它们有什么联系 ?
- 通过 Koa 源码 的 package.json 文件,得到入口文件
lib/application.js
, 可知application.js
是 Koa 入口,也是应用库, 将 context、request、response 挂载到 Application 实例上 context.js
是上下文库request.js
是 request 库response.js
是 response 库
二、武功秘籍
1. 使用框架
学习一个新框架之初, 肯定是要以会使用框架作为目标, 而我学习 Koa 之初, 也是要求自己在短时间内以此为目标, 可以选择自己感兴趣的点切入, 也可以通过某个应用场景切入, 不太推荐直接沿着官方文档一路学下去, 没有一个兴趣点或应用场景支撑, 不仅体会不到 Koa 的真实优缺点, 也会随时间慢慢忘记所学的东西。所以我鼓励大家学东西时带着目的去学, 这样才能在比较短的时间内学习到更能帮助你的知识。
关于怎么使用 Koa,目前掘金上有很多文章,本篇不再介绍,也可以查看我的koa2 搭建项目来学习。
2. 理解源码
当我们在使用框架的过程中,常常会遇到千奇百怪的问题,而官方文档往往是更偏向基础的教程,也会遇到网上搜索不到答案的问题。
再有就是笔者在使用 Vue 时,有时和大佬写的 Vue 代码差别很大,常常会发出这样的感叹,“原来 Vue 还可以这样写”,“感觉自己学的 Vue 和大佬学的不是一个”,“我真的会 Vue 吗?”。究其原因,除了大佬有比较丰富的基础知识外,也肯定对源码有所涉猎,而我们遇到解决不了的问题,也有了一条新思路 —— 在源码里找答案。
只有扎实掌握 Koa 的源码,在面对复杂需求时才能游刃有余。
3. 实现框架
当我们在读完源码后,就会很好的体会到框架编码风格,这时我们可能会想 Koa 框架精简,设计巧妙,能不能自己实现一个简易版本的 Koa,当然自己实现一个简易版本的 Koa,才能更好理解这门框架巧妙的设计
怎样去实现 Koa 呢,首先需要摸清作者的惯用手法,提纲挈领,找到入口, 绘制架构图。通过流程图或架构图的方式去理清整个框架脉络,再去一一实现功能。
当然也可以 “先临摹,再想着自创方式” 去实现,毕竟 “天下代码一大抄” ,哦,文人的事不叫 “抄”,叫“借鉴”。
三、Koa之第二式
今天我们主要学的是 Koa 武功秘籍中的第二式 —— 理解源码, 我们今天讲的主要是 Koa2 版本, 我们通过使用框架 Koa,已经基本会了最简单的使用:
const Koa = require("koa");
const app = new Koa();
app.use(async (ctx) => {
ctx.body = "Hello World";
});
app.listen(3000);
通过这个简单示例, 我们试着去分析下:
- 凭借前端工程化的经验可以猜测得知 Koa 的入口文件应该在
package.json
的main
属性有标明; - Koa 实例是通过
new
关键字创建的, 也就是说在入口文件中, 默认导出的就是 Koa 应用类; - app 是 Koa 实例, 这个实例上挂载了
use
、listen
等方法。
我们带着猜想去看 Koa 源码,可以选择直接从github 上拉下 koa 框架源码,也可以随使用框架 Koa,安装 Koa 依赖从 node_modules/koa/**
查看
在 Koa 项目源码的 package.json
文件中查看到
{
"main": "lib/application.js"
}
验证了猜测 1, 我们继续去查看 lib/application.js
文件, 默认导出的正是 Application
应用类, 也就是 Koa 类。
我们在 Application
应用类中不仅发现了 listen
、use
方法, 还发现了onerror
,callback
等方法。
当我们去看这些方法时, 发现里面的每条语法好像都不是很难, 但组合在一起好像看不懂。
因为在 Koa 框架源码中会有一些代码健壮性处理及一些默认值的配置可能会增加学习者的负担, 今天我们就只学习比较重要的部分。
1. application 模块
这个入口文件主要做了哪些事?
- 构建 Application 实例,koa 入口
- 创建一个 http 服务, 成功时回调
- 回调函数是通过 use 注册传入的函数
- 将 context、request、response 挂载到 Application 实例上
- 使用 koa 时,会有 ctx 对象,自己构造 ctx 对象
- 将 ctx 对象传递给回调函数
- 引入
// 判断是否为 genterator 方法
const isGeneratorFunction = require("is-generator-function");
// 设置debug的匿名空间
const debug = require("debug")("koa:application");
// 当请求完成时执行的一个回调
const onFinished = require("on-finished");
// 引入 response
const response = require("./response");
// 中间件机制、剥洋葱模型
const compose = require("koa-compose");
// 引入 context
const context = require("./context");
// 引入 requiest
const request = require("./request");
// 用于判断http状态的工具包
const statuses = require("statuses");
// Node 原生事件驱动模块
const Emitter = require("events");
// Node 原生工具包模块
const util = require("util");
// Node 原生Stream
const Stream = require("stream");
// Node原生http 模块
const http = require("http");
// 用于返回对象指定的属性
const only = require("only");
// 将基于koa生成器中间价转换成基于Promise的中间件
const convert = require("koa-convert");
// 给出一些信息
const deprecate = require("depd")("koa");
// 用于创建Http Error 的模块
const { HttpError } = require("http-errors");
- 应用类
module.exports = class Application extends Emitter {
constructor(options) {
// 调用父类进行构造
super();
// 设置一些初始值
options = options || {};
// 中间件队列数组
this.middleware = [];
// 创建一份context/request/response本地库挂载到koa实例上
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
// 将调试的inspect方法挂载到custom上
if (util.inspect.custom) {
this[util.inspect.custom] = this.inspect;
}
}
// 监听端口,启动服务
listen(...args) {
debug("listen");
// 利用http模块创建服务,传入回调函数,以及监听端口
const server = http.createServer(this.callback());
return server.listen(...args);
}
// 返回this对象上三个属性
toJSON() {
return only(this, ["subdomainOffset", "proxy", "env"]);
}
inspect() {
return this.toJSON();
}
// 收集中间件,推入中间件队列
use(fn) {
if (typeof fn !== "function")
throw new TypeError("middleware must be a function!");
// 判断是否是 generators 函数,v3版本将会启用 generators 函数
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"
);
// 将中间件转换成promise形式
fn = convert(fn);
}
debug("use %s", fn._name || fn.name || "-");
this.middleware.push(fn);
return this;
}
callback() {
// 将中间件队列交给洋葱模块
const fn = compose(this.middleware);
if (!this.listenerCount("error")) this.on("error", this.onerror);
// 处理request
const handleRequest = (req, res) => {
// 构建ctx对象
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
// 用于处理请求
handleRequest(ctx, fnMiddleware) {
// 通过传递过来的ctx,获取原生的可写流
const res = ctx.res;
// 设置默认的statusCode 404
res.statusCode = 404;
const onerror = (err) => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
// 构建上下文对象
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));
// 将this赋值给context/request/response的app
context.app = request.app = response.app = this;
// 将传入的req赋值给 context/request/response的req
context.req = request.req = response.req = req;
// 将传入的res赋值给 context/request/response的res
context.res = request.res = response.res = res;
// 将context 赋值给 request/ response的ctx
request.ctx = response.ctx = context;
// 将response 赋值给request的response
request.response = response;
// 将request 赋值给 response的request
response.request = request;
// 将req的url 赋值给 context的originalUrl和request的originalUrl
context.originalUrl = request.originalUrl = req.url;
// 挂载state
context.state = {};
return context;
}
// 处理异常
onerror(err) {
if (404 === err.status || err.expose) return;
if (this.silent) return;
const msg = err.stack || err.toString();
console.error(`\n${msg.replace(/^/gm, " ")}\n`);
}
static get default() {
return Application;
}
};
// 响应请求
function respond(ctx) {
// allow bypassing koa 通过设置ctx.respond 去绕过koa
if (false === ctx.respond) return;
// 判断ctx原型链上的writable属性
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();
}
// body: json
body = JSON.stringify(body);
if (!res.headersSent) {
ctx.length = Buffer.byteLength(body);
}
res.end(body);
}
// 将http异常处理挂载到koa的HttpError 上
module.exports.HttpError = HttpError;
application.js
是Koa 入口, 同时将另外三大模块也挂载在了自己的实例上。application.js
向外导出了继承自Emitter
实例的构造函数。使得 Koa 框架有了事件监听和事件触发的能力。use
方法作用主要是将传入的函数推到中间件队列中, 同时返回Koa实例, 方便链式调用。listen
方法实质是对http.createServer
进行了封装, 同时创建成功后调用下Application
类的callback方法, 在callback
方法中将中间件合并,上下文处理, 对Request
和response
处理。- 至于
compose
洋葱模型并未在 Koa 源码中, 这次解析就先不看koa-compose
了, 在下一篇源码实现之Koa上会进行解析, 并实现。
2. context 模块
- 引入
// Node 原生工具包模块
const util = require("util");
// http 失败处理
const createError = require("http-errors");
const httpAssert = require("http-assert");
// 设置代理库 委托代理
const delegate = require("delegates");
// http 状态工具包
const statuses = require("statuses");
// 操作cookie
const Cookies = require("cookies");
// 强调唯一性, 只能在当前模块内访问,其他地方无法访问
const COOKIES = Symbol("context#cookies");
- delegate 委托代理
/**
* Response delegation.
*/
delegate(proto, "response") // 这样就可以在ctx上访问ctx.response
.method("attachment")
.method("redirect")
.method("remove")
.method("vary")
.method("has")
.method("set")
.method("append")
.method("flushHeaders")
.access("status")
.access("message")
.access("body")
.access("length")
.access("type")
.access("lastModified")
.access("etag")
.getter("headerSent")
.getter("writable");
/**
* Request delegation.
*/
delegate(proto, "request") // 这样就可以在ctx上访问ctx.request
.method("acceptsLanguages")
.method("acceptsEncodings")
.method("acceptsCharsets")
.method("accepts")
.method("get")
.method("is")
.access("querystring")
.access("idempotent")
.access("socket")
.access("search")
.access("method")
.access("query")
.access("path")
.access("url")
.access("accept")
.getter("origin")
.getter("href")
.getter("subdomains")
.getter("protocol")
.getter("host")
.getter("hostname")
.getter("URL")
.getter("header")
.getter("headers")
.getter("secure")
.getter("stale")
.getter("fresh")
.getter("ips")
.getter("ip");
- 可以通过
node_modules
找到delegate
库,可知
- method 函数是注册函数
- getter 函数是注册只读属性
- access 函数是注册可读可写属性
- ctx 主要的功能是代理 request 和 response 的功能,提供了对 request 和 response 对象的便捷访问能力。比如我们要访问ctx.response.status但是我们通过delegate,可以直接访问ctx.status访问到它。
3. request 模块
request
模块是对 request对象进行抽象和封装,将一些属性进行了特殊处理。request
模块相对比较简单, 代码量较多, 就不再做过多解读。
3. response 模块
response
模块是对 response对象进行抽象和封装,将一些属性进行了特殊处理。response
模块相对比较简单, 代码量较多, 就不再做过多解读。
四、总结
本文从为什么学源码、怎样学源码及怎么去学习一个新框架的角度展开描述。
Koa 只是一个框架, 本文的学习方法可以适用在很多框架中, 当然你也有更好的学习方法, 也欢迎从评论区说出来, 一道成长。
同时,本篇文章是 Koa 系列第二篇,第三篇正在路上源码实现之Koa,欢迎点赞、关注、支持一波。感谢!!!