作者 rocYoung
过去我不了解太阳,那时我过的是冬天——聂鲁达
Koa的用法可以说非常傻瓜,我们快速过一下:
首先映入眼帘的不是假山,是hello world
const Koa = require('koa');
const app = new Koa();
app.use((ctx, next) => {
ctx.body = 'Hello World';
});
app.listen(3000);
不用框架时的写法
let http = require('http')
let server = http.createServer((req, res) => {
res.end('hello world')
})
server.listen(4000)
对比发现,相对原生,Koa多了两个实例上的use、listen方法,和use回调中的ctx、next两个参数。这四个不同,几乎就是Koa的全部了,也是这四个不同让Koa如此强大。
简单!http的语法糖,实际上还是用了http.createServer(),然后监听了一个端口。
比较简单!利用 上下文(context) 机制,将原来的req,res对象合二为一,并进行了大量拓展,使开发者可以方便的使用更多属性和方法,大大减少了处理字符串、提取信息的时间,免去了许多引入第三方包的过程。(例如ctx.query、ctx.path等)
重点!Koa的核心 —— 中间件(middleware)。解决了异步编程中回调地狱的问题,基于Promise,利用 洋葱模型 思想,使嵌套的、纠缠不清的代码变得清晰、明确,并且可拓展,可定制,借助许多第三方中间件,可以使精简的koa更加全能(例如koa-router,实现了路由)。其原理主要是一个极其精妙的 compose 函数。在使用时,用 next() 方法,从上一个中间件跳到下一个中间件。
注:以上加粗部分,下面都有详细介绍。
Koa有多简单?简单到只有四个文件,算上大量的空行和注释,加起来不到1800行代码(有用的也就几百行)。
github.com/koajs/koa/t…(https://github.com/koajs/koa/tree/master/lib)
所以,学习Koa源码并不是一个痛苦的过程。豪不夸张的说,搞定这四个文件,手写下面的100多行代码,你就能完全理解Koa。为了防止大段代码的出现,我会讲的很详细。
模仿官方,我们建立一个koa文件夹,并创建四个文件:application.js,context.js,request.js,response.js。 通过查看package.json可以发现,application.js为入口文件。
context.js是上下文对象相关,request.js是请求对象相关,response.js是响应对象相关。
结合上面hello world,可以明确,Koa是一个类,实例上主要两个方法,use和listen。
上面说过,listen是http的语法糖,所以要引入http模块。
Koa有一套错误处理机制,需要监听实例的error事件。所以要引入events模块继承EventEmitter。再引入另外三个自定义模块。
let http = require('http')
let EventEmitter = require('events')
let context = require('./context')
let request = require('./request')
let response = require('./response')
class Koa extends EventEmitter {
constructor () {
super()
}
use () {
}
listen () {
}
}
module.exports = Koa
这三个模块,其实都是一个对象,为了代码能跑通,这里先简单导出一下。
context.js
let proto = {} // proto同源码定义的变量名
module.exports = proto
request.js
let request = {}
module.exports = request
response.js
let request = {}
module.exports = request
开始写Koa类里面的代码,先实现创建服务的功能:1、listen方法创建一个http服务并监听一个端口。2、use方法把回调传入。
class Koa extends EventEmitter {
constructor () {
super()
this.fn
}
use (fn) {
this.fn = fn // 用户使用use方法时,回调赋给this.fn
}
listen (...args) {
let server = http.createServer(this.fn) // 放入回调
server.listen(...args) // 因为listen方法可能有多参数,所以这里直接解构所有参数就可以了
}
}
这样就可以启动一个服务了,测试一下:
let Koa = require('./application')
let app = new Koa()
app.use((req, res) => { // 还没写中间件,所以这里还不是ctx和next
res.end('hello world')
})
app.listen(3000)
下面先解决ctx,ctx是一个上下文对象,里面绑定了很多请求和相应相关的数据和方法,例如ctx.path、ctx.query、ctx.body()等等等等,极大的为开发提供了便利。
思路是这样的:用户调用use方法时,把这个回调fn存起来,创建一个createContext函数用来创建上下文,创建一个handleRequest函数用来处理请求,用户listen时将handleRequest放进createServer回调中,在函数内调用fn并将上下文对象传入,用户就得到了ctx。
class Koa extends EventEmitter {
constructor () {
super()
this.fn
this.context = context // 将三个模块保存,全局的放到实例上
this.request = request
this.response = response
}
use (fn) {
this.fn = fn
}
createContext(req, res){ // 这是核心,创建ctx
// 使用Object.create方法是为了继承this.context但在增加属性时不影响原对象
const ctx = Object.create(this.context)
const request = ctx.request = Object.create(this.request)
const response = ctx.response = Object.create(this.response)
// 请仔细阅读以下眼花缭乱的操作,后面是有用的
ctx.req = request.req = response.req = req
ctx.res = request.res = response.res = res
request.ctx = response.ctx = ctx
request.response = response
response.request = request
return ctx
}
handleRequest(req,res){ // 创建一个处理请求的函数
let ctx = this.createContext(req, res) // 创建ctx
this.fn(ctx) // 调用用户给的回调,把ctx还给用户使用。
res.end(ctx.body) // ctx.body用来输出到页面,后面会说如何绑定数据到ctx.body
}
listen (...args) {
let server = http.createServer(this.handleRequest.bind(this))// 这里使用bind调用,以防this丢失
server.listen(...args)
}
}
如果不理解Object.create可以看这个例子:
let o1 = {a: 'hello'}
let o2 = Object.create(o1)
o2.b = 'world'
console.log('o1:', o1.b) // 创建出的对象不会影响原对象
console.log('o2:', o2.a) // 创建出的对象会继承原对象的属性
o1: undefined o2: hello
经过上面的操作,用户在ctx上可以用各种姿势取到想要的值。
例如url,可以用ctx.req.url、ctx.request.req.url、ctx.response.req.url取到。
app.use((ctx) => {
console.log(ctx.req.url)
console.log(ctx.request.req.url)
console.log(ctx.response.req.url)
console.log(ctx.request.url)
console.log(ctx.request.path)
console.log(ctx.url)
console.log(ctx.path)
})
访问localhost:3000/abc
/abc /abc /abc /undefined /undefined /undefined /undefined
姿势多,不一定爽,要想爽,我们希望能实现以下两点:
1 修改request
let url = require('url')
let request = {
get url() { // 这样就可以用ctx.request.url上取值了,不用通过原生的req
return this.req.url
},
get path() {
return url.parse(this.req.url).pathname
},
get query() {
return url.parse(this.req.url).query
}
// 。。。。。。
}
module.exports = request
非常简单,使用对象get访问器返回一个处理过的数据就可以将数据绑定到request上了,这里的问题是如何拿到数据,由于前面ctx.request这一步,所以this就是ctx,那this.req就是原生的req,再利用一些第三方模块对req进行处理就可以了,源码上拓展了非常多,这里只举例几个,看懂原理即可。
访问localhost:3000/abc?id=1
/abc?id=1 /abc?id=1 /abc?id=1 /abc?id=1 /abc undefined undefined
2 接下来要实现ctx直接取值,这里是通过一个代理来实现的
let proto = {
}
function defineGetter(prop, name){ // 创建一个defineGetter函数,参数分别是要代理的对象和对象上的属性
proto.__defineGetter__(name, function(){ // 每个对象都有一个__defineGetter__方法,可以用这个方法实现代理,下面详解
return this[prop][name] // 这里的this是ctx(原因下面解释),所以ctx.url得到的就是this.request.url
})
}
defineGetter('request', 'url')
defineGetter('request', 'path')
// .......
module.exports = proto
访问localhost:3000/abc?id=1
/abc?id=1 /abc?id=1 /abc?id=1 /abc?id=1 /abc /abc?id=1 /abc
__defineGetter__方法可以将一个函数绑定在当前对象的指定属性上,当那个属性的值被读取时,你所绑定的函数就会被调用,第一个参数是属性,第二个是函数,由于ctx继承了proto,所以当ctx.url时,触发了__defineGetter__方法,所以这里的this就是ctx。这样,当调用defineGetter方法,就可以将参数一的参数二属性代理到ctx上了。
有个问题,要代理多少个属性就要调用多少遍defineGetter函数么?是的,如果想优雅一点,可以模仿官方源码,提出一个delegates模块,批量代理(其实也没优雅到哪去),这里为了方便展示,还是看懂即可吧。
3 修改response。根据koa的api,输出数据到页面不是res.end('xx')也不是res.send('xx'),而是ctx.body = 'xx'。我们要实现设置ctx.body,还要实现获取ctx.body。
let response = {
get body(){
return this._body // get时返回出去
},
set body(value){
this.res.statusCode = 200 // 只要设置了body,就应该把状态码设置为200
this._body = value // set时先保存下来
}
}
module.exports = response
这样得到的是ctx.response.body,并不是ctx.body,同样,通过context代理一下
修改context
let proto = {
}
function defineGetter (prop, name) {
proto.__defineGetter__(name, function(){
return this[prop][name]
})
}
function defineSetter (prop, name) {
proto.__defineSetter__(name, function(val){ // 用__defineSetter__方法设置值
this[prop][name] = val
})
}
defineGetter('request', 'url')
defineGetter('request', 'path')
defineGetter('response', 'body') // 同样代理response的body属性
defineSetter('response', 'body') // 同理
module.exports = proto
测试一下
app.use((ctx) => {
ctx.body = 'hello world'
console.log(ctx.body)
})
访问localhost:3000
node控制台输出:
hello world
网页显示:hello world
接下来解决一下body的问题,上面说了,一旦给body设置值,状态码就改成200,那么没设置值就应该是404。还有,用户不光会输出字符串,还有可能是文件、页面、json等,这里都要处理,所以改一下handleRequest函数:
let Stream = require('stream') // 引入stream
handleRequest(req,res){
res.statusCode = 404 // 默认404
let ctx = this.createContext(req, res)
this.fn(ctx)
if(typeof ctx.body == 'object'){ // 如果是个对象,按json形式输出
res.setHeader('Content-Type', 'application/json;charset=utf8')
res.end(JSON.stringify(ctx.body))
} else if (ctx.body instanceof Stream){ // 如果是流
ctx.body.pipe(res)
}
else if (typeof ctx.body === 'string' || Buffer.isBuffer(ctx.body)) { // 如果是字符串或buffer
res.setHeader('Content-Type', 'text/htmlcharset=utf8')
res.end(ctx.body)
} else {
res.end('Not found')
}
}
现在只能use一次,我们要实现use多次,并可以在use的回调函数中使用next方法跳到下一个中间件,在此之前,我们先了解一个概念:“洋葱模型”。
当我们多次使用use时
app.use((crx, next) => {
console.log(1)
next()
console.log(2)
})
app.use((crx, next) => {
console.log(3)
next()
console.log(4)
})
app.use((crx, next) => {
console.log(5)
next()
console.log(6)
})
它的执行顺序是这样的:
1 3 5 6 4 2
next方法会调用下一个use,next下面的代码会在下一个use执行完再执行,我们可以把上面的代码想象成这样:
app.use((ctx, next) => {
console.log(1)
// next() 被替换成下一个use里的代码
console.log(3)
// next() 又被替换成下一个use里的代码
console.log(5)
// next() 没有下一个use了,所以这个无效
console.log(6)
console.log(4)
console.log(2)
})
这样的话,理所应当输出135642
这就是洋葱模型了,通过next把执行权交给下一个中间件。
这样,开发者手中的请求数据会像仪仗队一样,乖乖的经过每一层中间件的检阅,最后响应给用户。
既应付了复杂的操作,又避免了混乱的嵌套。
除此之外,koa的中间件还支持异步,可以使用async/await
app.use(async (ctx, next) => {
console.log(1)
await next()
console.log(2)
})
app.use(async (ctx, next) => {
console.log(3)
let p = new Promise((resolve, roject) => {
setTimeout(() => {
console.log('3.5')
resolve()
}, 1000)
})
await p.then()
await next()
console.log(4)
ctx.body = 'hello world'
})
1 3 // 一秒后 3.5 4 2
async函数返回的是一个promise,当上一个use的next前加上await关键字,会等待下一个use的回调resolve了再继续执行代码。
所有现在要做的事有两步:
这里用到了数组和递归,每次use将当前函数存到一个数组中,最后按顺序执行。执行这一步用到一个compose函数,这个函数是重中之重。
constructor () {
super()
// this.fn 改成:
this.middlewares = [] // 需要一个数组将每个中间件按顺序存放起来
this.context = context
this.request = request
this.response = response
}
use (fn) {
// this.fn = fn 改成:
this.middlewares.push(fn) // 每次use,把当前回调函数存进数组
}
compose(middlewares, ctx){ // 简化版的compose,接收中间件数组、ctx对象作为参数
function dispatch(index){ // 利用递归函数将各中间件串联起来依次调用
if(index === middlewares.length) return // 最后一次next不能执行,不然会报错
let middleware = middlewares[index] // 取当前应该被调用的函数
middleware(ctx, () => dispatch(index + 1)) // 调用并传入ctx和下一个将被调用的函数,用户next()时执行该函数
}
dispatch(0)
}
handleRequest(req,res){
res.statusCode = 404
let ctx = this.createContext(req, res)
// this.fn(ctx) 改成:
this.compose(this.middlewares, ctx) // 调用compose,传入参数
if(typeof ctx.body == 'object'){
res.setHeader('Content-Type', 'application/json;charset=utf8')
res.end(JSON.stringify(ctx.body))
} else if (ctx.body instanceof Stream){
ctx.body.pipe(res)
}
else if (typeof ctx.body === 'string' || Buffer.isBuffer(ctx.body)) {
res.setHeader('Content-Type', 'text/htmlcharset=utf8')
res.end(ctx.body)
} else {
res.end('Not found')
}
}
再次测试上面打印123456的例子,可以正确的得到135642
compose(middlewares, ctx){
function dispatch(index){
if(index === middlewares.length) return Promise.resolve() // 若最后一个中间件,返回一个resolve的promise
let middleware = middlewares[index]
return Promise.resolve(middleware(ctx, () => dispatch(index + 1))) // 用Promise.resolve把中间件包起来
}
return dispatch(0)
}
handleRequest(req,res){
res.statusCode = 404
let ctx = this.createContext(req, res)
let fn = this.compose(this.middlewares, ctx)
fn.then(() => { // then了之后再进行判断
if(typeof ctx.body == 'object'){
res.setHeader('Content-Type', 'application/json;charset=utf8')
res.end(JSON.stringify(ctx.body))
} else if (ctx.body instanceof Stream){
ctx.body.pipe(res)
}
else if (typeof ctx.body === 'string' || Buffer.isBuffer(ctx.body)) {
res.setHeader('Content-Type', 'text/htmlcharset=utf8')
res.end(ctx.body)
} else {
res.end('Not found')
}
}).catch(err => { // 监控错误发射error,用于app.on('error', (err) =>{})
this.emit('error', err)
res.statusCode = 500
res.end('server error')
})
}
let http = require('http')
let EventEmitter = require('events')
let context = require('./context')
let request = require('./request')
let response = require('./response')
let Stream = require('stream')
class Koa extends EventEmitter {
constructor () {
super()
this.middlewares = []
this.context = context
this.request = request
this.response = response
}
use (fn) {
this.middlewares.push(fn)
}
createContext(req, res){
const ctx = Object.create(this.context)
const request = ctx.request = Object.create(this.request)
const response = ctx.response = Object.create(this.response)
ctx.req = request.req = response.req = req
ctx.res = request.res = response.res = res
request.ctx = response.ctx = ctx
request.response = response
response.request = request
return ctx
}
compose(middlewares, ctx){
function dispatch (index) {
if (index === middlewares.length) return Promise.resolve()
let middleware = middlewares[index]
return Promise.resolve(middleware(ctx, () => dispatch(index + 1)))
}
return dispatch(0)
}
handleRequest(req,res){
res.statusCode = 404
let ctx = this.createContext(req, res)
let fn = this.compose(this.middlewares, ctx)
fn.then(() => {
if (typeof ctx.body == 'object') {
res.setHeader('Content-Type', 'application/json;charset=utf8')
res.end(JSON.stringify(ctx.body))
} else if (ctx.body instanceof Stream) {
ctx.body.pipe(res)
} else if (typeof ctx.body === 'string' || Buffer.isBuffer(ctx.body)) {
res.setHeader('Content-Type', 'text/htmlcharset=utf8')
res.end(ctx.body)
} else {
res.end('Not found')
}
}).catch(err => {
this.emit('error', err)
res.statusCode = 500
res.end('server error')
})
}
listen (...args) {
let server = http.createServer(this.handleRequest.bind(this))
server.listen(...args)
}
}
module.exports = Koa
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。