[评论送书 ]手撕源码,实现一个Koa。,web开发面试题

定义context.js

const context={

}

module.exports=context

定义request.js

const request={

}

module.exports=request

定义response.js

const resposne={

}

module.exports=response

use中封装ctx

我们在上面导出koa章节中可以看到,在app.use的时候,我们传的参数是(request,response),源koa传的的ctx,所以我们就知道了,koa是在app.use的时候创建了一个ctx。

在本章开头的时候,我们又提到每次请求的ctx都是全新的ctx。

综合以上两点,我们可以基本编写出下面的代码。(为了代码的清晰易读,我们封装了一个createcontext函数来创建上下文。)

const Context = require(‘./context’)

const Request = require(‘./request’)

const Response = require(‘./response’)

class Application {

constructor(){

this.context = Object.create(Context);

this.request = Object.create(Request);

this.response = Object.create(Response);

}

use(fn) {

this.fn = fn

}

createContext = (req, res) => {

const ctx = Object.create(this.context);

const request = Object.create(this.request);

const response = Object.create(this.response);

ctx.app = request.app = response.app = this

ctx.request = request;

ctx.request.req = ctx.req = req;

ctx.response = response;

ctx.response.res = ctx.res = res;

ctx.originalUrl = request.originalUrl = req.url

ctx.state = {}

return ctx

}

callback = (req, res) => {

let ctx = this.createContext(req, res)

this.fn(ctx)

}

listen() {

const server = http.createServer(this.callback);

console.log(…arguments)

server.listen(…arguments)

}

}

首先我们在constructor中定义了一个context对象,这里会在constructor定义是因为Koa的app上默认导出context属性。

app.context 是从其创建 ctx 的原型。您可以通过编辑 app.contextctx 添加其他属性。这对于将 ctx 添加到整个应用程序中使用的属性或方法非常有用,这可能会更加有效(不需要中间件)和/或 更简单(更少的 require()),而更多地依赖于ctx,这可以被认为是一种反模式。

例如,要从 ctx 添加对数据库的引用:

app.context.db = db();

app.use(async ctx => {

console.log(ctx.db);

});

然后再callback中,我们针对response和request进行了二次封装。

再来看看这段代码:

app.context.db = db();

app.use(async ctx => {

console.log(ctx.db);

});

再使用use之前,通过app.context对context进行了修改。当使用use函数的时候,是不是直接进入了callback函数,此时的this.context已经是修改过的了。

测试

const Koa=require(‘./application.js’)

const app=new Koa()

app.use((ctx) => {

// 测试1

ctx.response.res.end(" hello my koa")

// 测试2

ctx.res.end(" hello my koa")

})

app.listen(3000,()=>{

console.log(‘3000’)

})

正常访问!

image.png

封装request.js


明确一个事实:request类的属性是通过getter和setter设置的。为什么会这样设置?这样设置的好处是可以方便的设置和获取到值。是不是有点懵逼!请听我细细道来。

先来看一下Koa中request类所绑定的属性,官方链接

我这里简单的列举几个:

request.header=

设置请求头对象。

request.headers

请求头对象。别名为 request.header.

request.headers=

设置请求头对象。别名为 request.header=

request.url

获取请求 URL.

request.url=

设置请求 URL, 对 url 重写有用。

request.originalUrl

获取请求原始URL。

  1. 这里对于每个属性都有设置和获取的功能,使用getter和setter可以很好的实现。

get url () {

return this.req.url

},

  1. 这里的每个属性是如何设置的,如果我们对request本身设置有效吗?

const request={

url:‘’,

header:{

}

}

const request={

set url (val) {

this.req.url = val

}

get url () {

return this.req.url

},

}

request.socket的getter

socket在这里指套接字。套接字的概念这里不赘述!

get socket () {

return this.req.socket

},

request.protocol的getter

返回请求协议,“https” 或 “http”。当 app.proxytrue 时支持 X-Forwarded-Proto

先判断套接字中是否存在encrypted(加密),如果加密,就是https,

X-Forwarded-Proto用来确定客户端与代理服务器或者负载均衡服务器之间的连接所采用的传输协议(HTTP 或 HTTPS)

X-Forwarded-Proto: https

X-Forwarded-Proto: http

get protocol () {

if (this.socket.encrypted) return ‘https’

if (!this.app.proxy) return ‘http’

const proto = this.get(‘X-Forwarded-Proto’)

return proto ? proto.split(/\s*,\s*/, 1)[0] : ‘http’

},

这里有一个get函数,主要时根据字段,从请求头中获取数据。

get (field) {

const req = this.req

switch (field = field.toLowerCase()) {

case ‘referer’:

case ‘referrer’:

return req.headers.referrer || req.headers.referer || ‘’

default:

return req.headers[field] || ‘’

}

},

request.host的getter

存在时获取主机(hostname:port)。当 app.proxytrue 时支持 X-Forwarded-Host,否则使用 Host

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]

},

request.origin的getter

获取URL的来源,包括 protocolhost

例如我请求:http://localhost:3000/index?a=3,

origin返回的是http://localhost:3000

get origin () {

return ${this.protocol}://${this.host}

},

request.href的getter

获取完整的请求URL,包括 protocolhosturl

href支持解析 GET http://example.com/foo

例如我访问http://localhost:3000/index?a=3

href返回http://localhost:3000/index?a=3

get href () {

if (/^https?😕//i.test(this.originalUrl)) return this.originalUrl

return this.origin + this.originalUrl

},

注意:这里的this.originalUrl在封装ctx的时候已经绑定过了

image.png

request.header 的getter和setter

请求头对象。这与 node http.IncomingMessage 上的 headers 字段相同

get header () {

return this.req.headers

},

set header (val) {

this.req.headers = val

},

request的属性是很多的,我们就不展开了,反正知道了原理,大家慢慢自己加吧。

封装response.js


对比request的封装,response的封装稍微有些不同,因为,对于request来说大部分的封装是getter,而response的封装大部分都是setter

在request部分我们阐述了三个使用getter和setter的原因。在resposne中最主要的原因我觉得是改变set的对象。

其实想一想很简单,例如在网络请求中我们会经常遇到各种状态:404 200等等,这些在node的http模块中,是用resposne.status进行改变的。假设我们在koa的response直接设置,你觉得会有用吗?简单概括一句话:koa的request和respsone是对nodehttp模块的二次封装,并且底层还是对nodehttp模块的操作。

response.status的getterh和setter

获取响应状态。默认情况下,response.status 设置为 404 而不是像 node 的 res.statusCode 那样默认为 200

默认’404’,这里的默认是在何时默认的时候呢,其实是在接收到请求后就设置为404,也就是说在callback的时候开始设置为404。(注意:http中res.statusCode用来标记状态码,在Koa中这个被封装成status

callback = (req, res) => {

let ctx = this.createContext(req, res)

const res = ctx.res

res.statusCode = 404

this.fn(ctx)

}

response.status的实现

get status () {

return this.res.statusCode

},

set status (code) {

if (this.headerSent) return

assert(Number.isInteger(code), ‘status code must be a number’)

assert(code >= 100 && code <= 999, invalid status code: ${code})

this._explicitStatus = true

this.res.statusCode = code

if (this.req.httpVersionMajor < 2) this.res.statusMessage = statuses[code]

if (this.body && statuses.empty[code]) this.body = null

},

response.body的getter和setter


首先我们要知道body是用来干嘛的。body是用来设置响应主体的,也就是返回响应的内容的。这些内容支持以下格式:

  • string 写入
  • Buffer 写入
  • Stream 管道
  • Object || Array JSON-字符串化
  • null 无内容响应
  1. nodehttp中是 res.end(“我是返回内容”) 返回响应内容的。在koa中我们是通过ctx.body=“” 来设置响应内容的。这里有人会问了,ctx.body和resopnse.body 有啥关系。其实他们是一个东西,ctx里面封装了response.body。

  2. koa中通过设置ctx.body,就能返回内容,其实本质还是使用了res.end(),通过res.end(ctx.body)来返回内容。res.end的调用时机在这里是放在callback中(具体的原因我们后面会说到

const response = {

_body: undefined,

get body() {

return this._body

},

set body(originContent) {

this.res.statusCode = 200;

this._body = originContent;

}

};

封装context.js


先谈谈Koa用到的delegates。这是一个实现了代理模式的包。对于Koa来说,context就是response和request的代理,通过ctx可以直接拿到request和response的属性和方法。

下面的是Koa主要用到的两个方法。其实最终的效果和封装request和response的效果一致。

__defineGetter__ 方法可以将一个函数绑定在当前对象的指定属性上,当那个属性的值被读取时,你所绑定的函数就会被调用。

__defineSetter__ 方法可以将一个函数绑定在当前对象的指定属性上,当那个属性被赋值时,你所绑定的函数就会被调用。

(这两个方法已废弃: 该特性已经从 Web 标准中删除,虽然一些浏览器目前仍然支持它,但也许会在未来的某个时间停止支持,请尽量不要使用该特性。)

Delegator.prototype.setter = function (name) {

var proto = this.proto;

var target = this.target;

this.setters.push(name);

proto.defineSetter(name, function (val) {

return this[target][name] = val;

});

return this;

};

Delegator.prototype.getter = function (name) {

var proto = this.proto;

var target = this.target;

this.getters.push(name);

proto.defineGetter(name, function () {

return this[target][name];

});

return this;

};

这里我们将delegates的核心逻辑抽离,封装context

function defineGetter(target, key) {

context.defineGetter(key, function () {

return this[target][key]

})

}

function defineSetter(target, key) {

context.defineSetter(key, function (value) {

this[target][key] = value

})

}

const context = {};

defineGetter(‘request’, ‘path’)

defineGetter(‘response’, ‘body’)

)

module.exports = context;

这里我们就列了两个,其他的不再赘述。

ctx.body再追述


在上面我们谈到了response.body以及ctx通过代理模式,拿到了response.body.

在Koa的源码中,针对不同格式的内容进行了不同的处理.大家简单看一下就可以。

response = {

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

}

return

}

// 内容存在(设置了内容),这是状态码为200

if (!this._explicitStatus) this.status = 200

// 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’

}

},

ctx.body最终是在res.end()返回的,这个时机是在callback中调用的。

我们通过app.use来传入我们要执行的方法,这个方法里面有ctx.body的赋值。

app.use((ctx) => {

console.log(ctx.request.href)

ctx.body=“123”

})

在callback中我们先创建了上下文,然后我们调用了传入的方法。

callback = (req, res) => {

let ctx = this.createContext(req, res)

this.fn(ctx)

}

那么我们是不是应该在fn执行结束之后,调用res.end(),因为这个时候body才被赋值

callback = (req, res) => {

let ctx = this.createContext(req, res)

this.fn(ctx)

let body = ctx.body;

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)

结尾

学习html5、css、javascript这些基础知识,学习的渠道很多,就不多说了,例如,一些其他的优秀博客。但是本人觉得看书也很必要,可以节省很多时间,常见的javascript的书,例如:javascript的高级程序设计,是每位前端工程师必不可少的一本书,边看边用,了解js的一些基本知识,基本上很全面了,如果有时间可以读一些,js性能相关的书籍,以及设计者模式,在实践中都会用的到。

资料领取方式:戳这里免费获取

s) => {

let ctx = this.createContext(req, res)

this.fn(ctx)

}

那么我们是不是应该在fn执行结束之后,调用res.end(),因为这个时候body才被赋值

callback = (req, res) => {

let ctx = this.createContext(req, res)

this.fn(ctx)

let body = ctx.body;

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-toT9QmmA-1712271519317)]

[外链图片转存中…(img-6KRdHJgX-1712271519317)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

[外链图片转存中…(img-Q00T44ZZ-1712271519317)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)

结尾

学习html5、css、javascript这些基础知识,学习的渠道很多,就不多说了,例如,一些其他的优秀博客。但是本人觉得看书也很必要,可以节省很多时间,常见的javascript的书,例如:javascript的高级程序设计,是每位前端工程师必不可少的一本书,边看边用,了解js的一些基本知识,基本上很全面了,如果有时间可以读一些,js性能相关的书籍,以及设计者模式,在实践中都会用的到。

资料领取方式:戳这里免费获取

html5

  • 26
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值