# Koa
# node基础复习
# node api 三种调用方式
// 1.一般 node调用api使用的是callback方式
fun('./index1.js', (err, data) => {
console.log(err ? 'read err' : data)
})
// 模拟实现
function fun(arg, callback) {
try {
aaa() // 执行一些内部操作
callback(null, 'result') // 如果执行成功,err设置为null, 结果通过第二参数返回
} catch(e) {
callback(e)
}
}
// 通过 promisify 改造后的fun函数
const { promisify } = require('util')
const promisefun = promisify(fun)
// 2.promise方式调用
promisefun('./index1.js').then((data) => {
console.log(data)
}, (err) => {
// 如果后面有.catch 这里的优先级会高一点
console.log(err)
})
// 或者
promisefun('./index1.js').then((data) => {
console.log(data)
}).catch(err => {
console.log('read err')
})
// 3.通过async/await 调用promise函数
// await 需要用 async 函数包裹
setTimeout(async () => {
try {
let data = await promisefun('./index1.js')
console.log(data)
} catch(e) {
console.log('read err')
}
}, 0)
# util模块内置 promisify 实现
promisify 可以把老的callback方式,转换为promise函数,怎么实现的呢?
// 普通callback方式
function fun(arg, callback) {
try {
aaa() // 执行一些内部操作
callback(null, 'result', 'result2') // 如果执行成功,err设置为null, 结果通过第二参数返回
} catch(e) {
callback(e)
}
}
// promisify模拟实现
function promisify(fun) {
// 生成的函数,会接收一个参数arg,数据和错误,需要我们在promise内部用reject或resolve传出结果
return function(...args) {
// 将传入的参数保存到args数组
return new Promise((resolve, reject) => {
// 将callback函数push到参数数组里,再间接调用fun
args.push((err, result) => {
// 如果fun函数执行成功会执行该函数并传入 (null, result)
// 如果fun函数执行错误会执行该函数并传入 (err)
// resolve() 只能接受并处理一个参数,多余的参数会被忽略掉。 spec上就是这样规定。
// 如果回调函数,传出了多个参数,可以将该函数result换为 ...result
// 然后resove时判断下,如果 result数组长度为0 直接resolve(result[0]),否则resove(result数组),接收参数时需要注意
err ? reject(err) : resolve(result)
})
fun.apply(null, args)
})
}
}
// 测试
let promisefun = promisify(fun)
promisefun('./index1.js').then((data) => {
console.log(data)
}, (err) => {
// 如果后面有.catch 这里的优先级会高一点
console.log('read err')
})
# Koa
Koa是由 Express 原班人马打造的致力于成为一个更小、更富有表现力、更健壮的 web 开发框架。
官方解释:Expressive middleware for node.js using ES2017 async functions
github: koajs/koa (opens new window)
# 特点
中间件机制、请求、响应处理
- 轻量、无捆绑
- 中间件构架
- 优雅的API设计
- 增强的错误处理
# Koa1与Koa2的区别
Koa1使用generate,yield next方式执行promise异步操作,而Koa开始,使用aysnc/await来处理异步
# node的不足
- 令人困惑的req和res
- res.end()
- res.writeHeader、res.setHeader
- 描述复杂业务逻辑时不够优雅
- 流程描述:比如a账号扣钱、b账号加钱
- 切面描述(AOP) 比如鉴权、日志、加判断在某个时间开始打折促销,axios里的拦截。AOP实现分为语言级、框架级
// 利用fs,渲染静态html、JSON字符串返回
const http = require('http')
const fs = require('fs')
const server = http.createServer((req, res)=> {
const { url, method } = req
console.log('url, method: ', url, method)
if (url === '/' && method === 'GET') {
fs.readFile('index.html', (err, data) => {
if (err) throw err
res.statusCode = 200
res.setHeader('Content-Type', 'text/html')
res.end(data)
})
} else if (url === '/users' && method === 'GET') {
res.writeHead(200, {
'Content-Type': 'application/json'
})
res.end(JSON.stringify({
name: 'guoqzuo'
}))
}
})
server.listen(3003)
# koa优雅处理http
运行下面的代码,访问http://127.0.0.1 就可以看到 {name: 'Tom'} 内容
// 需要先 npm install koa --save
const Koa = require('koa')
const app = new Koa()
app.use((ctx, next) => {
ctx.body = {
name: 'Tom'
}
})
app.listen(3000)
# ctx与next
下面的例子访问 http://127.0.0.1 为 {name: 'Tom'},访问 http://127.0.0.1/html 内容为 '你的名字是Tom'
const Koa = require('koa')
const app = new Koa()
app.use((ctx, next) => {
ctx.body = {
name: 'Tom'
}
next() // 执行下一个中间件
})
app.use((ctx, next) => {
console.log(ctx.url)
if (ctx.url === '/html') {
ctx.body = `你的名字是${ctx.body.name}`
}
})
app.listen(3000)
# await next()
const Koa = require('koa')
const app = new Koa()
// 也会被请求 /favicon.ico
app.use(async (ctx, next) => {
// log日志
let dateS = +(new Date())
await next() // 先去处理后面的中间件,都处理完后再向下执行
let dateE = +(new Date())
console.log(`请求耗时${dateE - dateS}ms`)
})
app.use((ctx, next) => {
ctx.body = {
name: 'Tom'
}
next()
})
app.use((ctx, next) => {
console.log(ctx.url)
if (ctx.url === '/html') {
ctx.body = `你的名字是${ctx.body.name}`
}
})
app.listen(3000)
# Koa原理
# node与koa开启http服务方法
// node http服务
const http = require('http')
const server = http.createServer(() => {
res.writeHead(200)
res.end('hello')
})
server.llsten(3000, () => {
console.log('监听端口3000')
})
// koa http服务
const Koa = require('koa')
const app = new Koa()
app.use((ctx, next) => {
ctx.body = {
name: 'Tom'
}
})
app.listen(3000)
# 创建mykoa.js来模拟实现koa
先写好使用demo
const MyKoa = require('./myKoa')
const app = new MyKoa()
// koa调用
// app.use((ctx, next) => {
// ctx.body = {
// name: 'Tom'
// }
// })
// 先暂时简单点
app.use((req, res) => {
console.log('执行了app.use')
res.end('hello')
})
app.listen(3000, (err, data) => {
console.log('监听端口3000')
})
myKoa.js实现
// myKoa.js
const http = require('http')
class MyKoa {
// app.use 调用 app.use(callback)
use(callback) {
this.callback = callback
}
listen(...args) {
console.log(args)
const server = http.createServer((req, res) => {
this.callback(req, res)
})
server.listen(...args)
}
}
module.exports = MyKoa
# 简化API:ctx参数(context)
一般app.use回调函数参数为 ctx和next,这里的ctx是context上下文的简写,主要是为了简化API而引入的。将原始请求对象req和响应对象res封装并挂载到context上,并在context上设置getter和setter属性,从而简化操作
# getter和setter作用
- echarts中对于对象层级很深的属性,options.a.b.c,可以直接创建一个getter,这样写-法更优雅
- vue2.0双向绑定
// 更近一步 将app.use((req, res) => {}) => app.use(ctx => {})
app.use(ctx => {
ctx.body = 'hello'
})
先看看koa源码 koa -response源码 (opens new window)
response最核心的一个方法是 set body方法,ctx.body 默认接收是json数据,如果传入了buffer、string、流都会有相应的处理
# 封装request、response、context
// demo app.js
const MyKoa = require('./myKoa')
const app = new MyKoa()
app.use((ctx) => {
// console.log(ctx)
ctx.body = 'hello'
})
app.listen(3000, (err, data) => {
console.log('监听端口3000')
})
// myKoa.js
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')
class MyKoa {
// app.use 调用 app.use(callback)
use(callback) {
this.callback = callback
}
listen(...args) {
console.log(args)
const server = http.createServer((req, res) => {
//this.callback(req, res)
// 需要先创建上下文
let ctx = this.createContext(req, res)
this.callback(ctx)
res.end(ctx.body)
})
server.listen(...args)
}
// 将res和req封装到contxt
createContext(req, res) {
// 先继承一些我们写的对象
const ctx = Object.create(context)
ctx.request = Object.create(request)
ctx.response = Object.create(response)
ctx.req = ctx.request.req = req
ctx.res = ctx.response.res = res
return ctx
}
}
module.exports = MyKoa
request.js
module.exports = {
get url() {
return this.req.url
},
get method() {
return this.req.method.toLowerCase()
}
}
response.js
module.exports = {
get body() {
return this._body
},
set body(val) {
this._body = val
}
}
context.js
module.exports = {
get rul() {
return this.request.url
},
get body() {
return this.response.body
},
set body(val) {
this.response.body = val
},
get method() {
return this.request.method
}
}
# 优雅的流程描述与切面描述(中间件机制)
koa中间件机制是:利用compose函数组合,将一组需要顺序执行的函数复合为一个函数,外层函数的参数是内层函数的返回值。洋葱圈模型可以形象的表示这种机制机制,是koa源码的精髓和难点。
compose是函数式编程里的一个概念,是多个函数的组合。
# compose函数合成
const add = (x, y) => x + y
const square = z => z * z
const fn = (x, y) => square(add(x, y)) // 将两个函数合成一个函数
console.log(fn(1, 2)) // 9
// 更好的写法 => 封装成一个通用方法
const compose = (fn1, fn2) => (...args) => fn2(fn1(...args))
const fn = compose(add, square)
console.log(fn(1, 2)) // 9
// 再次扩展,不固定个数的函数封装
const compose = (...fns) => (...args) => {
let ret
// 依次执行每个函数
fns.forEach((fn, index) => {
ret = index === 0 ? fn(...args) : fn(ret)
})
return ret
}
const fn = compose(add, square)
console.log(fn(1, 2)) // 9
# compose异步洋葱圈
先来看测试demo,怎么实现下面的compose函数呢?
async function fn1(next) {
console.log('start fn1')
await next()
console.log('end fn1')
}
async function fn2(next) {
console.log('start fn2')
await next()
console.log('end fn2')
}
function fn3(next) {
console.log('start fn3')
}
const finalFn = compose([fn1, fn2, fn3]) // [fn1, fn2, fn3] middlewares
finalFn()
// 打印结果
// start fn1
// start fn2
// start fn3
// end fn2
// end fn1
compose函数实现
async function fn1(next) {
console.log('start fn1')
await delay()
await next()
console.log('end fn1')
}
async function fn2(next) {
console.log('start fn2')
await delay()
await next()
console.log('end fn2')
}
async function fn3(next) {
console.log('start fn3')
await delay()
await next()
console.log('end fn3')
}
function delay() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
}, 2000)
})
}
function compose(fns) {
return function() {
return dispatch(0)
function dispatch(i) {
let fn = fns[i]
if (!fn) {
return Promise.resolve()
}
return Promise.resolve(
fn(() => {
// dispatch(i + 1)
return dispatch(i + 1)
})
)
}
}
}
const finalFn = compose([fn1, fn2, fn3]) // [fn1, fn2, fn3] middlewares
finalFn()
// 执行结果
// start fn1
// 2s
// start fn2
// 2s
// start fn3
// 2s
// end fn3
// end fn2
// end fn1
// 思考:将next的函数里面 return dispatch(i + 1) 改为 dispatch(i + 1)
// await next() 时,dispatch(i + 1) 一开始执行,await就向下执行了,并没有等到dispatch(i + 1)完全执行完
// 执行结果
// start fn1
// 2s
// start fn2
// end fn1
// 2s
// start fn3
// end fn2
// 2s
// end fn3
# await/async 执行顺序问题
在上面的例子中,我们发现将next的函数里面 return dispatch(i + 1) 改为 dispatch(i + 1),会导致await没有按预期等待。这里用一个demo来理解async/await的执行顺序问题,await 后面的内容如果函数返回值为promise,则等待promise执行完再向下执行,如果返回值非promise,await不会等待(await下面的代码和await等待的函数会同步执行)
(async () => {
await test() // await fn()
console.log('异步执行完成')
})()
async function test() {
fn() // return fn() 或 await fn()
}
async function fn(next) {
console.log('start fn')
await delay()
console.log('end fn')
}
function delay() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
}, 2000)
})
}
// return fn() 或 await fn() 结果
// start fn
// end fn
// 异步执行完成
// fn() 结果
// start fn
// 异步执行完成
// end fn
参考:async/await函数的执行顺序的理解 - csdn (opens new window)
# 将compose应用到myKoa中
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')
class MyKoa {
// app.use 调用 app.use(callback)
constructor() {
this.middlewares = []
}
use(middleware) {
this.middlewares.push(middleware)
return this // 支持链式调用 app.use().use()
}
listen(...args) {
console.log(args)
const server = http.createServer(async (req, res) => {
// 需要先创建上下文
let ctx = this.createContext(req, res)
// 组合函数
let fn = this.compose(this.middlewares)
await fn(ctx)
// 这里简单的处理了下ctx.body 但实际要有很多处理
let bodyType = typeof ctx.body
let result = bodyType === 'object' ? JSON.stringify(ctx.body) : ctx.body
// 解决中文乱码的问题
res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
res.end(result)
})
server.listen(...args)
}
createContext(req, res) {
// 先继承一些我们写的对象
const ctx = Object.create(context)
ctx.request = Object.create(request)
ctx.response = Object.create(response)
ctx.req = ctx.request.req = req
ctx.res = ctx.response.res = res
return ctx
}
compose(fns) {
return function(ctx) {
return dispatch(0)
function dispatch(i) {
let fn = fns[i]
if (!fn) {
return Promise.resolve()
}
return Promise.resolve(
fn(ctx, () => {
// dispatch(i + 1)
return dispatch(i + 1)
})
)
}
}
}
}
module.exports = MyKoa
用一个demo来测试下,也可以使用上面的 koa优雅处理http - await next() 里面的例子来测试
const delay = () => Promise.resolve(resolve => setTimeout(() => resolve(), 2000))
const Koa = require('./myKoa2')
const app = new Koa()
app.use(async (ctx, next) => {
ctx.body = '1'
await next()
ctx.body += '5'
})
app.use(async (ctx, next) => {
ctx.body += '2'
await next()
ctx.body += '4'
})
app.use((ctx, next) => {
ctx.body += '3'
next()
}).use((ctx, next) => {
// 试试链式调用
ctx.body += 'end'
})
app.listen(3000)
// 访问网页内容为 123end45
# koa compose源码
源码地址: koa compose - github (opens new window)
# 常见koa中间件的实现
我们可以自己来实现一个中间件,koa中间件规范:
- 一个async函数
- 接收ctx和next两个参数
- 任务结束需要执行next
const mid = async (ctx, next) => {
// 来到中间件,洋葱圈左边
next() // 进入其他中间件
// 再次来到中间件,洋葱圈右边
}
中间件常见任务
- 请求拦截
- 路由
- 日志
- 静态文件服务
# 请求拦截中间件
现在动手实现一个请求拦截的中间件
const Koa = require('koa')
cosnt app = new Koa()
cosnt intercept = require('./intercept')
// 请求拦截中间件
app.use(intercept)
app.use((ctx, next) => {
ctx.body = 'hello'
})
app.listen(3000)
来看看intercept.js的实现
async function intercept(ctx, next) {
let { res, req } = ctx
const blacklist = [
'127.0.0.1',
'192.168.1.2'
]
const ip = getClientIp(req)
if (blacklist.includes(ip)) {
ctx.body = '您无权限访问'
// 如果不执行next,就无法进入到下一个中间件
} else {
await next()
}
}
// 获取当前IP
function getClientIp(req) {
let curIp = (
req.headers['x-forwarded-for'] || // 是否有反向代理 IP
req.connection.remoteAddress || // 判断 connection 的远程 IP
req.socket.remoteAddress || // 判断后端的 socket 的 IP
req.connection.socket.remoteAddress
)
curIp.startsWith('::ffff:') && (curIp = curIp.split('::ffff:')[1])
console.log('当前ip是', curIp)
return curIp
}
module.exports = intercept
# 路由中间件 router
来实现一个路由中间件,先来看一个测试demo
const Koa = require('koa')
cosnt app = new Koa()
cosnt Router = require('./router')
const router = new Router()
router.get('/', aysnc ctx => { ctx.body = 'home page'} )
router.get('/index', aysnc ctx => { ctx.body = 'index page'} )
router.get('/post', aysnc ctx => { ctx.body = 'post page'} )
router.get('/list', aysnc ctx => { ctx.body = 'list page'} )
router.post('/config', aysnc ctx => {
ctx.body = {
code: 200,
msg: 'ok',
data: { a: 1 }
}
)
// 请求拦截中间件
app.use(router.routes())
app.use((ctx, next) => {
ctx.body = '404'
})
app.listen(3000)
router.js 实现 router.routes()函数返回一个中间件函数
class Router {
constructor() {
this.stack = []
}
register(path, methods, middleware) {
let route = { path, methods, middleware }
this.stack.push(route)
}
get(path, middleware) {
// 注册路由
this.register(path, 'get', middleware)
}
post(path, middleware) {
// 注册路由
this.register(path, 'post', middleware)
}
routes() {
// 返回一个中间件回调函数 (ctx, next) => { 进行路由处理 }
let stock = this.stack
return async (ctx, next) => {
if (ctx.url === '/favicon.ico') {
await next()
return
}
const len = stock.length
let route
for(let i = 0; i < len; i++) {
let item = stock[i]
console.log(ctx.url, item, ctx.method)
if (ctx.url === item.path && item.methods.includes(ctx.method.toLowerCase())) {
route = item.middleware
break
}
}
console.log('route', route)
if (typeof route === 'function') {
// 如果匹配到了路由
route(ctx, next)
} else {
await next()
}
}
}
}
module.exports = Router
# 静态文件服务中间件
koa-staic,配置静态文件目录,默认为static获取文件或目录信息,静态文件读取,先来看看使用demo
const Koa = require('koa')
const app = new Koa()
const static = require('./static')
app.use(static(__dirname + '/public'))
app.listen(3000, () => {
console.log('服务已开启,端口号3000')
})
static.js 实现
const fs = require('fs')
const path = require('path')
// console.log(path, '*' + path.resolve)
function static(dirPath = './pbulic') {
return async (ctx, next) => {
// 校验是否是static目录
if (ctx.url.startsWith('/public')) {
// 将当前路径和用户指定的路径合并为一个绝对路径
let url = path.resolve(__dirname, dirPath)
console.log(url)
// /Users/kevin/Desktop/feclone/fedemo/src/node/node视频教程笔记/1_koa/静态文件服务中间件/public
console.log(ctx.url) // /public/2sdf/323
let filePath = url + ctx.url.replace('/public', '')
try {
let stat = fs.statSync(filePath) // https://nodejs.org/docs/latest/api/fs.html#fs_fs_statsync_path_options
if (stat.isDirectory()) {
// 如果是目录,列出文件
let dir = fs.readdirSync(filePath)
console.log(dir)
if (dir.length === 0) {
ctx.body = '目录为空'
return
}
let htmlArr = ['<div style="margin:30px;">']
dir.forEach(filename => {
htmlArr.push(
filename.includes('.') ?
`<p><a style="color:black" href="${ctx.url}/${filename}">${filename}</a></p>` :
`<p><a href="${ctx.url}/${filename}">${filename}</a></p>`
)
})
htmlArr.push('</di>')
ctx.body = htmlArr.join('')
} else {
// 如果是文件
let content = fs.readFileSync(filePath)
console.log(content)
ctx.body = content.toString()
}
} catch(e) {
console.error(e)
// ctx.body = ctx.url + '文件或目录不存在'
ctx.body = 'Not Found'
}
} else {
// 非静态资源,执行下一个中间件
await next()
}
}
}
module.exports = static
# 日志服务中间件
基于上面的例子,我们增加一个日志服务中间件,用于记录访问记录
const Koa = require('koa')
const app = new Koa()
const static = require('./static')
const Logger = require('./log')
const logger = new Logger()
// 日志中间件
app.use(logger.log())
app.use(static(__dirname + '/public'))
app.listen(3000, () => {
console.log('服务已开启,端口号3000')
})
log.js 实现demo
class Logger {
constructor() {
this.logs = []
}
log() {
return async (ctx, next) => {
// 记录进入时间
let temp = {}
let startTime = +(new Date())
let endTime
await next()
endTime = +(new Date()) // 结束时间
Object.assign(temp, {
startTime,
endTime,
url: ctx.url,
resTime: (endTime - startTime) + 'ms'
})
this.logs.push(temp)
console.log(this.showLogs())
}
}
showLogs() {
console.log(this.logs)
}
}
module.exports = Logger