最后
如果你已经下定决心要转行做编程行业,在最开始的时候就要对自己的学习有一个基本的规划,还要对这个行业的技术需求有一个基本的了解。有一个已就业为目的的学习目标,然后为之努力,坚持到底。如果你有幸看到这篇文章,希望对你有所帮助,祝你转行成功。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
.createServer(handler)
.listen(
8888,
() => {
console.log(‘listening 127.0.0.1:8888’)
}
)
但是一个服务不可能只有这么几个接口跟方法啊,总不能每加一个就增加一个分支吧,这样 handler
得变得多长多冗余,于是又很容易想到抽离 handler
,将 path
和 method
解耦。
1.2 策略模式解耦
如何解耦呢?从在新手村的代码中可以发现策略模式[2]刚好可以拿来解决这个问题:
const http = require(‘http’)
class Application {
constructor () {
// 收集route和method对应的回调函数
this.$handlers = new Map()
}
// 注册handler
register (method, path, handler) {
let pathInfo = null
if (this.$handlers.has(path)) {
pathInfo = this.$handlers.get(path)
} else {
pathInfo = new Map()
this.$handlers.set(path, pathInfo)
}
// 注册回调函数
pathInfo.set(method, handler)
}
use () {
return (request, response) => {
const { url: path, method } = request
this.KaTeX parse error: Expected 'EOF', got '&' at position 20: …lers.has(path) &̲& this.handlers.get(path).has(method)
? this.$handlers.get(path).get(method)(request, response)
: response.end(‘404 NOT FOUND!’)
}
}
}
const app = new Application()
app.register(‘GET’, ‘/’, (req, res) => {
res.end(‘Hello World!’)
})
app.register(‘GET’, ‘/about’, (req, res) => {
res.end(‘Hello About!’)
})
app.register(‘POST’, ‘/’, (req, res) => {
res.end(‘Post Method!’)
})
http
.createServer(app.use())
.listen(
8888,
() => {
console.log(‘listening 127.0.0.1:8888’)
}
)
1.3 符合DRY原则
但是这个时候就会发现:
-
如果手抖把
method
方法写成了小写,因为Http.Request.method
都是大写,无法匹配到正确的handler
,于是返回'404 NOT FOUND'
。 -
如果我想在响应数据前增加一些操作,比如为每个请求增加一个时间戳,表示请求的时间,就必须修改每个
register
中的handler
函数,不符合DRY原则
此时再修改一下上面的代码,利用 Promise
实现按顺序执行 handler
。
const http = require(‘http’)
class Application {
constructor() {
// 收集route和method对应的回调函数
this.$handlers = new Map()
// 暴露get和post方法
this.get = this.register.bind(this, ‘GET’)
this.post = this.register.bind(this, ‘POST’)
}
// 注册handler
register(method, path, …handlers) {
let pathInfo = null
if (this.$handlers.has(path)) {
pathInfo = this.$handlers.get(path)
} else {
pathInfo = new Map()
this.$handlers.set(path, pathInfo)
}
// 注册回调函数
pathInfo.set(method, handlers)
}
use() {
return (request, response) => {
const { url: path, method } = request
if (
this.$handlers.has(path) &&
this.$handlers.get(path).has(method)
) {
const _handlers = this.$handlers.get(path).get(method)
_handlers.reduce((pre, _handler) => {
return pre.then(() => {
return new Promise((resolve, reject) => {
_handler.call({}, request, response, () => {
resolve()
})
})
})
}, Promise.resolve())
} else {
response.end(‘404 NOT FOUND!’)
}
}
}
}
const app = new Application()
const addTimestamp = (req, res, next) => {
setTimeout(() => {
this.timestamp = Date.now()
next()
}, 3000)
}
app.get(‘/’, addTimestamp, (req, res) => {
res.end(‘Hello World!’ + this.timestamp)
})
app.get(‘/about’, addTimestamp, (req, res) => {
res.end(‘Hello About!’ + this.timestamp)
})
app.post(‘/’, addTimestamp, (req, res) => {
res.end(‘Post Method!’ + this.timestamp)
})
http
.createServer(app.use())
.listen(
8888,
() => {
console.log(‘listening 127.0.0.1:8888’)
}
)
1.4 降低用户心智
但是这样依旧有点小瑕疵,用户总是在重复创建 Promise
,用户可能更希望无脑一点,那我们给用户暴露一个 next
方法,无论在哪里执行 next
就会进入下一个 handler
,岂不美哉!!!
class Application {
// …
use() {
return (request, response) => {
const { url: path, method } = request
if (
this.$handlers.has(path) &&
this.$handlers.get(path).has(method)
) {
const _handlers = this.$handlers.get(path).get(method)
_handlers.reduce((pre, _handler) => {
return pre.then(() => {
return new Promise(resolve => {
// 向外暴露next方法,由用户决定什么时候进入下一个handler
_handler.call({}, request, response, () => {
resolve()
})
})
})
}, Promise.resolve())
} else {
response.end(‘404 NOT FOUND!’)
}
}
}
}
// …
const addTimestamp = (req, res, next) => {
setTimeout(() => {
this.timestamp = new Date()
next()
}, 3000)
}
2 Koa核心源码解析
上面的代码一路下来,基本上已经实现了一个简单中间件框架,用户可以在自定义中间件,然后在业务逻辑中通过 next()
进入下一个 handler
,使得整合业务流程更加清晰。但是它只能推进中间件的执行,没有办法跳出中间件优先执行其他中间件。比如在koa中,一个中间件是类似这样的:
const Koa = require(‘koa’);
let app = new Koa();
const middleware1 = async (ctx, next) => {
console.log(1);
await next();
console.log(2);
}
const middleware2 = async (ctx, next) => {
console.log(3);
await next();
console.log(4);
}
const middleware3 = async (ctx, next) => {
console.log(5);
await next();
console.log(6);
}
app.use(middleware1);
app.use(middleware2);
app.use(middleware3);
app.use(async(ctx, next) => {
ctx.body = ‘hello world’
})
app.listen(8888)
可以看到控制台输出的顺序是1, 3, 5, 6, 4, 2,这就是koa经典的洋葱模型。
接下来我们一步步解析koa的源码[3],可以看到总共只有4个文件,如果去掉注释,合起来代码也就1000多行。
| 文件 | 功能 |
| — | — |
| applicaiton.js | koa程序的入口,管理和调用中间件,处理http.createServer的回调,将请求的request和response代理至context上 |
| request.js | 对http.createServer回调函数中的request的封装,各种getter、setter以及额外属性 |
| response.js | 对http.createServer回调函数中的response的封装,各种getter、setter以及额外属性 |
| context.js | 代理request和response,并向外暴露一些功能 |
创建Koa实例的时候,Koa做的事情其实并不多,设置实例的一些配置,初始化中间件的队列,使用 Object.create
继承 context
、request
和 response
。
2.1 constructor
constructor(options) {
super();
// 实例的各种配置,不用太关注
options = options || {};
this.proxy = options.proxy || false;
this.subdomainOffset = options.subdomainOffset || 2;
this.proxyIpHeader = options.proxyIpHeader || ‘X-Forwarded-For’;
this.maxIpsCount = options.maxIpsCount || 0;
this.env = options.env || process.env.NODE_ENV || ‘development’;
if (options.keys) this.keys = options.keys;
// 最重要的实例属性,用于存放中间
this.middleware = [];
// 继承其他三个文件中的对象
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
因为Koa仅用于中间件的整合以及请求响应的监听,所以我们最关注的Koa的两个实例方法就是 use
和 listen
。一个用来注册中间件,一个用来启动服务并监听端口。
2.2 use
功能非常简单,注册中间件,往实例属性middleware列表中推入中间件。
use(fn) {
if (typeof fn !== ‘function’) throw new TypeError(‘middleware must be a function!’);
// 利用co库转换generator函数,v3版本会移除,直接使用promise以及async…await
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);
// 用于链式注册中间件 app.use(xxx).use(xxx)…
return this;
}
2.3 listen
它的实现非常简单,就是直接调用 http.createServer
创建服务,并直接执行server.listen[4]的一些操作。稍微特殊一点地方是 createServer
传入的参数是调用实例方法 callback
的返回值。
listen(…args) {
debug(‘listen’);
// 创建服务
const server = http.createServer(this.callback());
// 透传参数,执行http模块的server.listen
return server.listen(…args);
}
2.4 callback
-
调用
compose
方法,将所有中间件转换成Promise
执行,并返回一个执行函数。 -
调用父类
Emitter
中的listenerCount
方法判断是否注册了error
事件的监听器,若没有则为error
事件注册onerror
方法。 -
定义传入
createServer
中的处理函数,这个处理函数有2个入参,分别是request
和response
,通过调用createContext
方法把request
和response
封装成ctx
对象,然后把ctx
和第一步的执行函数fn
传入handleRequest
方法中。
callback() {
// 后面会讲解koa-compose,洋葱模型的核心,转换中间件的执行时机。
const fn = compose(this.middleware);
// 继承自Emitter,如果没有error事件的监听器,为error事件注册默认的事件监听方法onerror
if (!this.listenerCount(‘error’)) this.on(‘error’, this.onerror);
//
const handleRequest = (req, res) => {
// 调用createContext方法把req和res封装成ctx对象
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
2.5 createContext
createContext
的作用是将前面讲到的 context
,request
,response
三个文件暴露出来的对象封装在一起,并额外增加app、req、res等,方便在ctx中获取各类信息。
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;
}
2.6 handleRequest
-
获得res,将状态默认置为404
-
定义失败的回调函数和中间件执行成功的回调函数,其中失败回调函数调用
context
中的onerror
函数,不过最终还是触发app中注册的onerror
函数;成功回调函数调用respond
方法,读取ctx
信息,把数据写入res
中并响应请求。 -
使用
on-finished
模块确保一个流在关闭、完成和报错时都会执行相应的回调函数。 -
执行中间件函数
fnMiddleware
,类似于Promise.all
,当全部中间件处理成功后,执行handleResponse
,否则捕获异常。
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
最后
javascript是前端必要掌握的真正算得上是编程语言的语言,学会灵活运用javascript,将对以后学习工作有非常大的帮助。掌握它最重要的首先是学习好基础知识,而后通过不断的实战来提升我们的编程技巧和逻辑思维。这一块学习是持续的,直到我们真正掌握它并且能够灵活运用它。如果最开始学习一两遍之后,发现暂时没有提升的空间,我们可以暂时放一放。继续下面的学习,javascript贯穿我们前端工作中,在之后的学习实现里也会遇到和锻炼到。真正学习起来并不难理解,关键是灵活运用。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
和报错时都会执行相应的回调函数。
- 执行中间件函数
fnMiddleware
,类似于Promise.all
,当全部中间件处理成功后,执行handleResponse
,否则捕获异常。
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
最后
javascript是前端必要掌握的真正算得上是编程语言的语言,学会灵活运用javascript,将对以后学习工作有非常大的帮助。掌握它最重要的首先是学习好基础知识,而后通过不断的实战来提升我们的编程技巧和逻辑思维。这一块学习是持续的,直到我们真正掌握它并且能够灵活运用它。如果最开始学习一两遍之后,发现暂时没有提升的空间,我们可以暂时放一放。继续下面的学习,javascript贯穿我们前端工作中,在之后的学习实现里也会遇到和锻炼到。真正学习起来并不难理解,关键是灵活运用。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
[外链图片转存中…(img-bwQFtRtq-1715666935562)]
[外链图片转存中…(img-Y2J97pOf-1715666935563)]