原生node服务器和进行了封装的koa
原生
node里面有很多个模块,有net,http,fs等等。我们使用的web服务器用的是 http 模块。
const http = require('http')
http里面有一个Server的构造函数。也有一个等价于 new Server() 的 createServer。最后通过 listen ,进入循环之中,监听请求。
还有状态码,request,serverResponse等,这里先不讨论。
创建一个服务器最简洁的代码就是:
const http = require('http')
// const server = new http.Server(cb?)
// or
const server = new http.createServer() // cb?
// 可以这样给请求设置回调,也可以在上面创建server时作为参数传进去。
server.on('request', (req, res) => {
res.end('hello')
})
server.listen(3000, () => {
console.log('连接成功')
}) // 可以传3个参数 端口, ip?, 回调?
这样启动服务器,然后访问 localhost:3000 时,页面就会有一个 hello。
如果要处理不同的地址,可以根据 req.url 进行条件分支的判断。
// localhost:3000/login
req.url = '/login'
// localhost:3000/home?a=1
req.url = '/home?a=1'
服务器分为静态资源和动态资源。静态资源就是只要是请求这个地址,永远返回固定的东西。动态资源就是会根据一些条件返回不一样的东西。
平时前端打包后的代码部署到服务器,就是把文件放到启动服务器的电脑里的某个地方,然后请求的时候根据地址去返回文件。
所以简单来看基本上服务器是做两件事,第一分析地址,第二走对应的逻辑返回对应的内容。
// url = localhost:3000/foo.html
const fs = require('fs')
server.on('request', (req, res) => {
const content = fs.readFileSync(`/dist/foo.html`).toString()
// 设置请求头的类型,用于帮助浏览器识别返回的东西是什么类型的文件
// 详细在 mdn 搜 MIME 类型
res.setHeader('Content-Type', 'text/html')
res.send(content)
})
逻辑分支可以使用 map 或者对象进行存储,简化条件分支, 这时路由的简单模型,koa-router 的使用方式和这个很像
const route = {
'/': (req, res) => {},
'/login': (req, res) => {}
}
server.on('request', (req, res) => {
route[req.url](req, res)
})
koa
koa是一个轻量的框架,从使用的方法上看,和原生非常相似。
const Koa = require('koa')
const app = new Koa()
app.use((ctx) => {
ctx.body = 'hello'
})
app.listen(3000, () => {
console.log('连接成功')
})
koa用的是插件的设计。通过use来注册处理的逻辑。通过封装一些对象和简写放在 ctx 里面,简化了一些写法。但是依然可以在 ctx.req,ctx.res 里面拿到原生的对象。
其中listen是一个语法糖,等价于,这个官网有写。
const server = http.createServer()
server.listen(3000, () => {
console.log('连接成功')
})
可以看出koa确实是比较简洁的。然后中间件采用了洋葱模型。设置了多个中间件后,实行的顺序类似于 1 2 3 3 2 1。前提是调用了 next
把注册的中间件组合起来串成一个函数是用了一个 compose 的函数。类似于 函数柯里化的感觉。这个函数本身也比较简短,并不复杂。
koa 还将中间件包装成 Promise,能够更好的处理异步的函数。下面实现一个简单的compose。可以结合下面的使用方法一起看。
// 外面的用法是
// const fn = compose(middleware)
// fn().then(handleResponse)
// 然后开始执行中间件
function compose(middleware = []) {
if (middleware.length > 0) {
return (ctx) => {
// fn 返回这个
return dispatch(0)
function dispatch(i) {
const fn = middleware[i]
if (fn) {
// bind很重要,包装成一个函数传进去当 next 调用。
// 相当于流程控制。
return Promise.resolve(fn(ctx, dispatch.bind(null,i+1)))
} else {
// 保证统一性,最后一个中间件也会有一个 next
return Promise.resolve()
}
}
}
} else {
return Promise.resolve
}
}
如果觉得比较绕就多看几遍,或者打断点调试也行。
使用方法是
// 结果 1,start 2,start 2,over 1,over
app.use(async (ctx, next) => {
console.log(1, 'start')
// 因为封装成了promise, next是dispatch.bind(ctx, 1)
// 即第二个中间件,然后会走完 next 才会继续走。
// 实现了一层层进去再一层层出来的洋葱模型
await next()
console.log(1, 'over')
})
app.use(async (ctx, next) => {
console.log(2, 'start')
// next是dispatch.bind(ctx, 2) 最后一个
// 即 Promise.resolve()
await next()
console.log(2, 'over')
})