使用koa
Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。
Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。
必修的 hello world 应用:
代码语言:javascript复制const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
ctx-上下文
和express直接调用 req
和 res
不同,app.use用的是一个上下文对象 ctx
。
Ctx 将 node 的 request
和 response
对象封装到单个对象中,为编写 Web 应用程序和 API 提供了许多有用的方法。 这些操作在 HTTP 服务器开发中频繁使用,它们被添加到此级别而不是更高级别的框架,这将强制中间件重新实现此通用功能。
每个 请求都将创建一个 Context
,并在中间件中作为接收器引用,或者 ctx
标识符,如以下代码片段所示:
app.use(async ctx => {
ctx; // 这是 Context
ctx.request; // 这是 koa Request
ctx.response; // 这是 koa Response
ctx.cookies; // 获取和设置cookie ctx.cookies(name,value,[oprions])
ctx.throw; // 比如返回500错误
});
如果你想返回数据:
代码语言:javascript复制app.use(async ctx => {
ctx.body = [{
name:'djtao',
job:'coder'
}];
});
如果返回html:
代码语言:javascript复制app.use(async ctx => {
if(ctx.url==='/html'){
let person={
name:'djtao',
job:'coder'
};
ctx.type='text/html;charset=utf-8';
ctx.body=`<div>
<p>名字:${person.name}</p>
<p>职业:${person.job}</p>
</div>`
}
});
为方便起见,许多上下文的访问器和方法直接委托给它们的 ctx.request
或 ctx.response
,不然的话它们是相同的。 例如 ctx.type
和 ctx.length
委托给 response
对象, ctx.path
和 ctx.method
委托给 request
。
中间件-洋葱模型
中间件是一个简单函数,参数除了ctx外,还有一个参数就是next——它会把流程的控制权交给下一个中间件。
koa中间件的业务逻辑依赖的是著名的洋葱模型:
代码语言:javascript复制const Koa = require('koa');
const app = new Koa();
// #1
app.use(async (ctx, next)=>{
console.log(1)
await next();
console.log(1)
});
// #2
app.use(async (ctx, next) => {
console.log(2)
await next();
console.log(2)
})
app.use(async (ctx, next) => {
console.log(3)
})
app.listen(3000);
打印的顺序是:12321。类似栈,也就是说,是#1嵌套#2,#2再嵌套#3的逻辑。
在每个中间件的操作中,ctx都得到了继承。
常用中间件
koa的好处在于,把非核心的业务全部交给中间件去做。
以下介绍常用中间件:
koa-static
我想调用一个静态html时:
代码语言:javascript复制const koaStatic= require('koa-static');
app.use(koaStatic(__dirname '/public'));
那么,直接访问的是根目录下的/public/index.html
koa-router
路由:
代码语言:javascript复制const Router=require('koa-router');
const router=new Router();
router.get('/string',async (ctx,next)=>{
ctx.body='string';
})
router.get('/json',async (ctx,next)=>{
ctx.body=[{
name:'djtao',
job:'coder'
}]
})
app
.use(router.routes()).use(router.allowedMethods())
问题来了:如果是渲染静态html呢?
koa-views
koa-views对需要进行视图模板渲染的应用是个不可缺少的中间件,支持ejs, nunjucks等众多模板引擎。
代码语言:javascript复制const views=require('koa-views');
// 必须在router之前使用
app.use(views(__dirname '/public', {
// map: {
// html: 'underscore' 这里是渲染的模板引擎,为空时默认渲染html。
// }
}));
app.use(koaStatic(__dirname '/public'));
router.get('/html',async (ctx,next)=>{
await ctx.render('index'); // 渲染index文件
});
这样就完成了静态资源加载。图片标签也可以正常显示。
koa-bodypaser
这个中间件可以把post请求解析道ctx.request.body上。
代码语言:javascript复制const bodyparser=require('koa-bodyparser')
app.use(bodyparser())
router.post('/post', async (ctx,next)=>{
console.log(ctx.request.body)
ctx.body=ctx.request.body;
})
在命令行模拟登录请求:
代码语言:javascript复制curl -d "username=djtao&password=1234" http://localhost:3000/post
返回结果为:
!image-20190614214321477
手撸koa
koa的使用体会
- ctx太好用了,集成了很多功能(res,req)。
- 中间件(洋葱模型)
如果手撸一个名为Doa的框架,核心就是实现以上功能。
先搭建架构。
架构
万事先起架构。
代码语言:javascript复制// doa
const http=require('http')
const server=(callback)=>{
return http.createServer(callback);
}
class Doa{
constructor(props){
// super(props)
this.middleweres=[];
}
listen(port,callback){
const _server=server((req,res)=>{
this.middleweres[0](req,res)
});
_server.listen(port,(err)=>{
callback(err)
})
}
// 安装中间件
use(middlewere){
this.middleweres.push(middlewere);
}
}
module.exports=Doa;
测试用例
代码语言:javascript复制const Doa=require('./index.js')
const app=new Doa();
app.use((ctx,next)=>{
console.log(ctx)
})
app.listen('3000',()=>{
console.log('aaa')
})
那么listen就实现了。
ctx实现
代理原理
res和req在某种情况下建立了绑定关系。
首先了解下get和set:我有一个对象,内有info字段,如果我要用一个name方法代理到设置或者是读取info的name字段——也就是说用Djtao.name访问读写info.name:
代码语言:javascript复制const djtao={
info:{name:'djtao',desc:'666'},
get name(){
return info.name;
}
set name(val){
return this.info.name=val;
}
}
console.log(djtao.name)
djtao.name=`dangjingtao`;
console.log(djtao.name)
同理,res,req都可以代理到一个属性。一个可读写的字段对应两个方法(get/set)。
实现req,res和ctx
koa的req实现 https://github.com/koajs/koa/blob/master/lib/
从源码来看,koa几个lib文件写的可读性相当高。也是一个字段对应get和set个方法。
所以新建request.js/response.js/ctx.js,回忆相应对象的常用方法:
- req=>对应常用方法是:
method
/url
,而且是只读的。
// request.js
module.exports={
get url(){
return this.req.url
},
get method(){
return this.req.method.toLowerCase()
}
}
- res=>对应常用方法是:
body
,他是可读写的。
// response.js
module.exports={
get body(){
return this._body
},
set body(val){
this._body=val
}
}
- ctx=>对应常用的是
body
(可读写)/url
(只读)/method
(只读)
// context.js
module.exports={
get url(){
return this.request.url;
},
get body(){
return this.request.body;
},
set body(val){
this.request.body=val
},
get method(){
return this.request.method
}
}
- 然后在koa.js中引入上述三个文件,创建新的方法:
// doa.js
const context=require('./context');
const request=require('./request');
const response=require('./response');
class Doa{
// ...
// 自创的方法
createCtx(req,res){
// Object.create(对象):实现一个新的对象,原型链为参数。
const ctx=Object.create(context);
ctx.request=Object.create(request);
ctx.response=Object.create(response);
// 把原生的res和req代理到上下文的req和res
ctx.req=ctx.request.req=req;
ctx.res=ctx.request.res=res;
return ctx;
}
// 在上下文环境中创建ctx
listen(port,callback){
const _server=server((req,res)=>{
// 创建上下文环境
const ctx=this.createCtx(req,res);
this.middlewere(ctx);
res.end(ctx.body);
});
_server.listen(port,(err)=>{
callback(err)
})
}
}
测试
经过上面的一系列神操作,就把核心的方法api实现了。
测试用例:
代码语言:javascript复制const Doa=require('./index.js')
const app=new Doa();
app.use((ctx,next)=>{
console.log(ctx)
ctx.body='aaa'
})
app.listen('3000',()=>{
console.log('aaa')
})
洋葱模型实现
函数嵌套合并
合并函数是设计模式里的一个常见概念:
操作:
代码语言:javascript复制const add=(x,y)=>x y;
const square=z=>z*z;
// const fn=(x,y)==>square(add(x,y)); 比较low,过于硬核
const compose=(fn1,fn2)=>(...args)=>fn2(fn1(...args));
const fn=compose(add,square);
console.log(fn(1,2)) //(1 2)*(1 2)=9
上述代码只能适应两个变量,如果是需要多个函数合并,有更加优雅的操作,通过for循环,把解构出来的数组迭代:
代码语言:javascript复制const compose=(...[first,...other])
// compose(a,b,c)=>>
// first=>>a
// other=>>[b,c]
所以完整的写法是:
代码语言:javascript复制const add=(x,y)=>x y;
const square=z=>z*z;
const compose=(...[first,...other])=>(...args)=>{
let ret=first(...args);
other.forEach(fn=>{
ret=fn(ret);
})
return ret;
}
const fn=compose(add,square);
console.log(fn(1,2)) //(1 2)*(1 2)=9
中间件是一个数列;
compose异步版
假设我有这样一组函数fn1-3:
代码语言:javascript复制async function fn1(next){
console.log('<fn1>')
await next();
console.log('</fn1>')
}
async function fn2(next){
console.log('<fn2>')
await delay(); //
await next();
console.log('</fn2>')
}
async function fn3(next){
console.log('<fn3>')
// await next(); fn3是最里层
console.log('</fn3>')
}
const delay=()=>{
return Promise.resolve(resolve=>{
setTimout(()=>{
resolve()
},2000)
})
}
const middleweres=[fn1,fn2,fn3];
const finalfn=compose(middleweres);
那么,compose应当如何写呢?
代码语言:javascript复制function compose(middleweres){
return function(){
// 如果有下一个,返回下一个带参数的promise,否则结束。
function dispatch(i){
let fn=middleweres[i];
if(!fn){
return Promise.resolve();
}
// 注意此处,把下个中间件作为next的函数内容。
return Promise.resolve(fn(function next(){
return dispatch(i 1)
}))
}
return dispatch(0);
}
}
打印结果如下:
整合
整合上述内容到doa中:
代码语言:javascript复制compose(middleweres) {
return function (ctx) {
// 如果有下一个,返回下一个带参数的promise,否则结束。
function dispatch(i) {
let fn = middleweres[i];
if (!fn) {
return Promise.resolve();
}
return Promise.resolve(ctx,fn(function next() {
return dispatch(i 1)
}))
}
return dispatch(0);
}
}
参考阅读:Express 中间件实现 https://github.com/nanjixiong218/analys-middlewares/tree/master/src
手撸常见中间件
中间件就是一个异步函数。koa中间件的规范:
- 一个async函数 接收ctx和next两个参数
- 任务结束需要执行next
中间件常见任务:
- 请求拦截
- 路由
- 日志
- 静态文件服务
static中间件
static的实现需求是:访问 /public
,则直接解析静态资源或列表。
module.exports = (dirPath = "./public") => {
return async (ctx, next) => {
if (ctx.url.indexOf("/public") === 0) {
// public开头 读取文件
const url = path.resolve(__dirname, dirPath);
const fileBaseName = path.basename(url);
const filepath = url ctx.url.replace("/public", "");
console.log(filepath);
// console.log(ctx.url,url, filepath, fileBaseName)
try {
stats = fs.statSync(filepath);
if (stats.isDirectory()) {
const dir = fs.readdirSync(filepath);
const ret = ['<div style="padding-left:20px">'];
dir.forEach(filename => {
console.log(filename);
// 简单认为不带小数点的格式,就是文件夹,实际应该用statSync
if (filename.indexOf(".") > -1) {
ret.push(
`<p><a style="color:black" href="${
ctx.url
}/${filename}">${filename}</a></p>`
);
} else {
// 文件
ret.push(
`<p><a href="${ctx.url}/${filename}">${filename}</a></p>`
);
}
});
ret.push("</div>");
ctx.body = ret.join("");
} else {
console.log("文件");
const content = fs.readFileSync(filepath);
ctx.body = content;
}
} catch (e) {
// 报错了 文件不存在
ctx.body = "404, not found";
}
} else {
// 否则不是静态资源,直接去下一个中间件
await next();
}
}
}
测试用例
代码语言:javascript复制// 使用
const static = require('./static')
app.use(static(__dirname '/public'));
访问/public路由,就看到结果了。
router中间件
看需求:
代码语言:javascript复制const Doa = require('./doa')
const Router = require('./router')
const app = new Doa()
const router = new Router();
router.get('/index', async ctx => { ctx.body = 'index page'; });
router.get('/post', async ctx => { ctx.body = 'post page'; });
router.get('/list', async ctx => { ctx.body = 'list page'; });
router.post('/index', async ctx => { ctx.body = 'post page'; });
// 路由实例输出父中间件
router.routes() app.use(router.routes());
那么和手撸express一样:
代码语言:javascript复制class Router {
constructor() {
this.stack = [];
}
// 每次定义一个路由,都注册一次
register(path, methods, middleware) {
let route = { path, methods, middleware }
this.stack.push(route);
}
// 现在只支持get和post,其他的同理
get(path, middleware) {
this.register(path, 'get', middleware);
}
post(path, middleware) {
this.register(path, 'post', middleware);
}
//调用
routes() {
let stock = this.stack;
return async function (ctx, next) {
let currentPath = ctx.url;
let route;
for (let i = 0; i < stock.length; i ) {
let item = stock[i];
if (currentPath === item.path && item.methods.indexOf(ctx.method) >= 0) {
// 判断path和method
route = item.middleware; break;
}
}
if (typeof route === 'function') {
route(ctx, next);
return;
}
await next();
};
}
}
module.exports = Router;
没什么很难的逻辑。