Node.js系列八 - koa开发web服务器
1、 Koa初体验
1.1. 认识Koa
Koa官方的介绍:
- koa:next generation web framework for node.js;
- koa:node.js的下一代web框架;
事实上,koa和express是同一个团队开发的一个新的Web框架:
- 目前团队的核心开发者TJ的主要精力也在维护Koa,express已经交给团队维护了;
- Koa旨在为Web应用程序和API提供更小、更丰富和更强大的能力;
- koa相对于express具有更强的异步处理能力
- Koa的核心代码只有1600+行,是一个更加轻量级的框架,我们可以根据需要安装和使用中间件;
1.2. koa初体验
koa与express的基本开发模式是比较相似的
// koa 的Web服务器
const Koa = require('koa');
const app = new Koa();
app.use((ctx, next) => {
console.log("middleware 01");
next();
})
app.use((ctx, next) => {
console.log("middleware 02");
ctx.response.body = "Hello World";
})
app.listen(8000, () => {
console.log("服务器启动成功~");
});
koa注册的中间件提供了两个参数:
- ctx:上下文(Context)对象;
- koa并没有像express一样,将req和res分开,而是将它们作为ctx的属性;
- ctx代表每次请求的上下文对象;
- ctx.request:获取请求对象;
- ctx.response:获取响应对象;
- next:本质上是一个dispatch,类似于express的next;
koa通过创建的app对象,注册中间件只能通过use方法:
- Koa并没有提供methods的方式来注册中间件;
- 也没有提供path中间件来匹配路径;
但是真实开发中我们如何将路径和method分离呢?
- 方式一:根据request自己来判断;
- 方式二:使用第三方路由中间件;
// 方式一:根据request自己判断
app.use((ctx, next) => {
if (ctx.request.path === '/users') {
if (ctx.request.method === 'POST') {
ctx.response.body = "Create User Success~";
} else {
ctx.response.body = "Users List~";
}
} else {
ctx.response.body = "Other Request Response";
}
})
整个代码的逻辑是非常复杂和混乱的,真实开发中我们会使用路由。
1.3. 路由的使用
koa官方并没有给我们提供路由的库,我们可以选择第三方库:koa-router
安装koa-router
npm install koa-router
koa-router基本使用
// user.router.js
const Router = require('koa-router');
const userRouter = new Router();
userRouter.get('/users', (ctx, next) => {
ctx.response.body = "user list~";
});
userRouter.post('/users', (ctx, next) => {
ctx.response.body = "create user info~";
});
module.exports = userRouter;
在app中将router.routes()注册为中间件:
app.use(userRouter.routes());
app.use(userRouter.allowedMethods());
注意 : allowedMethods用于判断某一个method是否支持:
- 如果我们请求 get,那么是正常的请求,因为我们有实现get;
- 如果我们请求 put、delete、patch,那么就自动报错:Method Not Allowed,状态码:405;
- 如果我们请求 link、copy、lock,那么就自动报错:Not Implemented,状态码:501;.
router的前缀
通常一个路由对象是对一组相似路径的封装,那么路径的前缀都是一直的,所以我们可以直接在创建Router时,添加前缀:
const userRouter = new Router({ prefix: '/users' });
userRouter.get('/', (ctx, next) => {
ctx.response.body = "user list~";
});
userRouter.post('/', (ctx, next) => {
ctx.response.body = "create user info~";
});
module.exports = userRouter;
1.4. 请求解析
常见的客户端传递到服务器参数的5种方法:
- 方式一:通过get请求中的URL的params;
- 方式二:通过get请求中的URL的query;
- 方式三:通过post请求中的body的json格式;
- 方式四:通过post请求中的body的x-www-form-urlencoded格式;
- 方式五:通过post请求中的form-data格式;
方式一:params
请求地址:http://localhost:8000/users/123
获取params:
const userRouter = new Router({prefix: "/users"})
userRouter.get("/:id", (ctx, next) => {
console.log(ctx.params.id);
ctx.body = "Hello World";
})
方式二:query
请求地址:http://localhost:8000/login?username=why&password=123
获取query:
app.use((ctx, next) => {
console.log(ctx.request.query);
ctx.body = "Hello World";
})
方式三:json
请求地址:http://localhost:8000/login
body是json格式:
{
"username": "Tom",
"password": "123456"
}
获取json数据:
- 安装依赖:npm install koa-bodyparser;
- 使用 koa-bodyparser的中间件;
app.use(bodyParser());
方式四:x-www-form-urlencoded
请求地址:http://localhost:8000/login
body是x-www-form-urlencoded格式:
获取json数据:(和json是一致的)
- 安装依赖:npm install koa-bodyparser;
- 使用 koa-bodyparser的中间件;
app.use(bodyParser());
方式五:form-data
请求地址:http://localhost:8000/login
body是form-data格式:
解析body中的数据,我们需要使用multer
- 安装依赖:npm install koa-multer;
- 使用 multer中间件;
const upload = multer({});
app.use(upload.any());
app.use((ctx, next) => {
console.log(ctx.req.body);
ctx.body = "Hello World";
});
multer还可以实现文件的上传:
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, "./uploads/")
},
filename: (req, file, cb) => {
cb(null, Date.now() + path.extname(file.originalname))
}
})
const upload = multer({
storage
});
const fileRouter = new Router();
fileRouter.post("/upload", upload.single('avatar'), (ctx, next) => {
console.log(ctx.req.file);
})
app.use(fileRouter.routes());
1.5. 响应方式
输出结果:body
将响应主体设置为以下之一:
- string :字符串数据
- Buffer :Buffer数据
- Stream :流数据
- Object|| Array:对象或者数组
- null :不输出任何内容
如果response.status尚未设置,Koa会自动将状态设置为200或204。
比较常见的输出方式:
ctx.response.body = "Hello World";
ctx.body = {
name: "Tom",
age: 18,
height: 1.88
};
ctx.body = ["abc", "cba", "nba"];
疑惑:ctx.response.body和ctx.body之间的区别:
- 事实上,我们访问ctx.body时,本质上是访问ctx.response.body;
请求状态:status
请求状态我们可以直接给ctx设置,或者给ctx.response设置也是一样的效果:
ctx.status = 201;
ctx.response.status = 204;
1.6. 错误处理
const Koa = require('koa');
const app = new Koa();
app.use((ctx, next) => {
ctx.app.emit('error', new Error("出错啦"), ctx);
})
app.on('error', (err, ctx) => {
console.log(err.message);
ctx.response.body = "出错啦";
})
app.listen(8000, () => {
console.log("错误处理服务启动成功~");
})
1.7. 静态服务器
koa并没有内置部署相关的功能,所以我们需要使用第三方库:
npm install koa-static
部署的过程类似于express:
const Koa = require('koa');
const static = require('koa-static');
const app = new Koa();
app.use(static('./build'));
app.listen(8000, () => {
console.log("静态服务器启动成功~");
});
2、koa 与 express的比较
在学习了两个框架之后,我们应该已经可以发现koa和express的区别:
从架构设计上来说:
- express是完整和强大的,其中帮助我们内置了非常多好用的功能;
- koa是简洁和自由的,它只包含最新的功能,并不会对我们使用其他中间件进行任何的限制。
- 甚至是在app中连最基本的get、post都没有给我们提供;
- 我们需要通过自己或者路由来判断请求方式或者其他功能;
因为express和koa框架他们的核心其实都是中间件:
-
但是事实上它们的中间件的执行机制是不同的,特别是针对某个中间件中包含异步操作时;
-
所以,接下来,我们再来研究一下express和koa中间件的执行顺序问题;
-
当中间件中没有异步操作时,express中间件 与 koa中间件 的执行顺序其实相同,执行结果也相同
-
但是当中间件中包含一步操作时express 中间件实现是基于 Callback 回调函数同步的,它不会去等待异步(Promise)完成
-
Koa 使用的是一个洋葱模型,它的一个特点是级联,通过 await next() 控制调用 “下游” 中间件,直到 “下游” 没有中间件且堆栈执行完毕,最终在流回 “上游” 中间件。
-
同步执行
const express = require('express');
const app = express();
const middleware1 = (req, res, next) => {
req.message = "middleware1";
next();
res.end(req.message);
}
const middleware2 = (req, res, next) => {
req.message = req.message + 'middleware2';
next();
}
const middleware3 = (req, res, next) => {
req.message = req.message + 'middleware3';
}
app.use(middleware1, middleware2, middleware3);
app.listen(8000, () => {
console.log("启动成功~");
})
// middleware1middleware2middleware3
const Koa = require('koa');
const app = new Koa();
const middleware1 = (ctx, next) => {
ctx.message = "middleware1";
next();
ctx.body = ctx.message;
}
const middleware2 = (ctx, next) => {
ctx.message = ctx.message + 'middleware2';
next();
}
const middleware3 = (ctx, next) => {
ctx.message = ctx.message + 'middleware3';
}
app.use(middleware1);
app.use(middleware2);
app.use(middleware3);
app.listen(8001, () => {
console.log("启动成功~");
})
// middleware1middleware2middleware3
- 异步操作
const express = require('express');
const axios = require('axios');
const app = express();
const middleware1 = async (req, res, next) => {
req.message = "middleware1";
await next();
res.end(req.message);
}
const middleware2 = async (req, res, next) => {
req.message = req.message + 'middleware2';
await next();
}
const middleware3 = async (req, res, next) => {
const result = await axios.get('http://123.207.32.32:9001/lyric?id=167876');
req.message = req.message + result.data.lrc.lyric;
console.log(req.message);
}
app.use(middleware1, middleware2, middleware3);
app.listen(8000, () => {
console.log("启动成功~");
})
// middleware1middleware2
// express 中 next()本身是一个同步函数 不等待异步请求的返回
// express 底层是不支持Async/Await 的
const Koa = require('koa');
const axios = require('axios');
const app = new Koa();
const middleware1 = async (ctx, next) => {
ctx.message = "middleware1";
await next();
ctx.body = ctx.message;
}
const middleware2 = async (ctx, next) => {
ctx.message = ctx.message + 'middleware2';
await next();
}
const middleware3 = async (ctx, next) => {
const result = await axios.get('http://123.207.32.32:9001/lyric?id=167876');
console.log(result)
ctx.message = ctx.message + result.data.lrc.lyric;
}
app.use(middleware1);
app.use(middleware2);
app.use(middleware3);
app.listen(8001, () => {
console.log("启动成功~");
})
// Koa2 底层已经原生支持Async/Await