koa
- npm i koa
- 底层基于promise,只是对http模块的一个封装,不会对性能产生影响
- koa的三个核心点
- 增强req/res,为了不破环原有的,自己生成了两个新的对象:request/response
- 把自己产生的两个新对象和原有的req/res统称为ctx
- 中间件机制
- 错误处理
- 增强req/res,为了不破环原有的,自己生成了两个新的对象:request/response
const Koa = require("koa");
// 通过new的方式创建一个应用
const app = new Koa();
// 可以通过app.use来注册回调
// 为什么会出现koa, 原生http模块的req/res功能比较弱
// 如要通过url模块来解析req.url,res.end只能返回buffer或者string
// koa来增强req/res 有自己的中间件机制 错误处理
app.use((ctx) => {
// throw new Error("error")
console.log(ctx.req.url); // 原生的
console.log(ctx.request.url); // koa封装的
console.log(ctx.request.req.url); // koa封装的request上也有req属性对象
console.log(ctx.url); // 一般用这个
console.log(ctx.request.query);
console.log(ctx.query);
ctx.body = 'hello zhuzhu'; // 响应结果
ctx.response.body = 'zhuzhu';
});
app.listen(3000, function() {
console.log('server start on 3000')
});
// 可以采用事件监听的方式捕获错误
app.on('error', function(err) {
console.log(err);
})
koa上下文的实现原理
const EventEmitter = require("events");
const http = require("http");
class Koa extends EventEmitter{
constructor() {
super();
// 保证应用之间互不干扰 否则多个应用共享一个上下文 可能会混乱
this.context = Object.create(context); // this.context.__proto__ = context
this.request = Object.create(request);
this.response = Object.create(response);
this.middlewares = [];
}
use(middleware) {
this.middlewares.push(middleware);
}
_createContext(req, res) { // 创建上下文
// 这样虽然应用之间的上下文独立了,多个请求共享了同一个上下文
// 实际上也应该是独立的
// let ctx = this.context;
let ctx = Object.create(this.context); // ctx.__proto__.__proto__ = context
let request = Object.create(this.request);
let response = Object.create(this.response);
ctx.request = request;
ctx.req = ctx.request.req = req;
ctx.response = response;
ctx.res = ctx.response.res = res;
return ctx;
}
_compose(ctx){
// 需要将中间件的所有的方法拿出来,先调用第一个,第一个调用完毕后,会调用next,再去执行第二个
let index = -1;
const dispatch = (i) => {
if(i <= index){
return Promise.reject('next() called multiple times')
}
index = i;
if(this.middlewares.length === i){
return Promise.resolve();
}
return Promise.resolve(this.middleware[i](ctx,()=> dispatch(i+1)))
}
return dispatch(0)
}
_handleRequest = (req, res)=> {
const ctx = this._createContext(req, res);
// 默认的状态码是404
res.statusCode = 404;
//this.fn(ctx);
this._compose(ctx).then(() => {
// ctx.body还支持流的写法 ctx.body = fs.createReadStream(require('path').resolve(__dirname, 'package.json'))
if(ctx.body instanceof Stream){
ctx.body.pipe(res);
}
else if(typeof ctx.body === 'object') {
res.setHeader("content-type", "application/json;chartset=utf-8")
res.end(JSON.stringify(ctx.body));
}else if(ctx.body) {
res.end(ctx.body);
}else{
res.end("not found");
}
}).catch(err=> {
this.emit('error', err); // 错误处理
})
}
listen(...args) {
const server = http.createServer(this._handleRequest);
server.listen(...args);
}
}
module.exports = Koa;
// context
const context = {
get query() {
return this.request.query;
},
get path() {
return this.request.path;
},
get body() {
return this.response.body;
},
set body(value) {
// 当用户调用ctx.body的时候会更改状态码
// 当往body上赋值时,修改状态码
this.res.statusCode = 200;
this.response.body = value;
}
};
module.exports = context;
// request
const url = require("url");
const request = {
get url() { // Object.defineProperty 属性访问器
// 取值的时候是 ctx.request.url 所以this是ctx.request,这上面有req
// 所以在request上增加req属性的目的是在request对象上通过this获取到req
return this.req.url
},
get path() {
return url.parse(this.url).pathname;
},
get query() {
return url.parse(this.url, true).query;
}
};
module.exports = request;
// response
const response = {
_body: undefined,
get body() {
return this._body
},
set body(value) {
this._body = value;
}
};
module.exports = response;
koa中间件的实现原理
- koa会将多个中间件进行组合处理,内部会将app.use里面传递进来的函数全部包装成promise,并且将这三个promise串联起来,内部会使用promise链链接起来。(第一个等待第二个执行完,第二个等待第三个执行完...)当第一个中间件函数执行完,就整个执行完成
- koa使用时:promise或者next前面必须加上await或者return,这样才有等待的效果
- 必须保证下面的promise完成,所以必须增加await,否则下面没执行完成,就直接继续后面的逻辑了
- 总之 记住next前一定加上await
- 上面的compose方法
koa处理请求
app.use(async (ctx, next) => {
if(ctx.path === '/login' && ctx.method === 'POST'){
let arr = [];
ctx.req.on("data", function(chunk) {
arr.push(chunk);
});
ctx.req.on('end', function() {
// 表单格式默认就是key=value&key=value
console.log(Buffer.concat(arr).toString());
ctx.body = Buffer.concat(arr);
})
}else{
await next();
}
})
/*
注意 这样写 页面不会显示设置的arr buffer 而是显示not found
因为中间件会组合成一个大的promise,第一个中间件完成了就完成了
因为ctx.path === '/login' && ctx.method === 'POST'里面的代码现在的写法并没有等待的效果,是异步执行的
使用koa所有的异步方法都要包装成promise
*/
- 中间件
// 中间件的作用 可以给koa中的属性扩展功能和方法 可以做一些鉴权相关的
/*
bodyParser这个功能(接受post传递过来的请求数据)很多地方可能都会用到
为了避免再用刀的地方就得手动引入
可以把解析的结果挂在ctx上
在前面定义的中间件肯定会优先执行
*/
// 1. 为了复用 将功能代码提取出来
function bodyParser(ctx) {
return new Promise((resolve, reject) => {
let arr = [];
ctx.req.on("data", function(chunk) {
arr.push(chunk);
});
ctx.req.on('end', function() {
// 表单格式默认就是key=value&key=value
console.log(Buffer.concat(arr).toString());
resolve(Buffer.concat(arr).toString());
})
})
}
// 2. 为了不每次手动引入函数 将解析结果挂到ctx上
app.use(async (ctx, next) => {
ctx.request.body = await new Promise((resolve, reject) => {
let arr = [];
ctx.req.on("data", function(chunk) {
arr.push(chunk);
});
ctx.req.on('end', function() {
// 表单格式默认就是key=value&key=value
console.log(Buffer.concat(arr).toString());
resolve(Buffer.concat(arr).toString());
})
});
await next(); // 这个中间件 就是做一件事情 解析请求体 之后继续向下执行
});
// 3. 编写功能时 一般会把功能封装成函数(高阶函数) 作为插件来使用
function bodyParser() {
return async (ctx, next) => {
ctx.request.body = await new Promise((resolve, reject) => {
let arr = [];
ctx.req.on("data", function(chunk) {
arr.push(chunk);
});
ctx.req.on('end', function() {
// 表单格式默认就是key=value&key=value
console.log(Buffer.concat(arr).toString());
resolve(Buffer.concat(arr).toString());
})
});
await next(); // 这个中间件 就是做一件事情 解析请求体 之后继续向下执行
}
}
app.use(bodyParser());
- 功能划分之后 代码就清晰了
const Koa = require("koa");
const app = new Koa();
const path = require("path");
const fs = require("fs");
const bodyParser = require("./middlewares/bodyParser.js");
const static = require('koa-static');
const Router = require('koa-router');
const login = require("./login");
// 路由
const router = new Router();
app.use(bodyParser());
app.use(static(__dirname));
app.use(static(path.resolve(__dirname, 'middleware'))); // 可以使用多次
login(app);
router.get('/login', async(ctx, next) => {
console.log('login-1');
await next();
})
router.get('/login', async(ctx, next) => {
console.log('login-2')
})
app.use(router.routes());
app.listen(3000, function(){
console.log('server start 3000')
});
// 登录相关 login.js
// 业务逻辑可以封装成一个函数,把app传入,就可以去扩展功能
const fs = require("fs");
const path = require("path");
module.exports = function(app) {
// 默认访问/from 就显示一个表单 用户可以填写后提交 解析用户参数
// 两个功能:1.返回表单 2.解析用户参数 用两个app.use来实现
app.use(async (ctx, next) => {
if(ctx.path === '/from' && ctx.method === 'GET'){
// 返回文件
// koa中默认返回一个流 会认为是要下载这个文件
// 需要设置响应头
ctx.set("Content-Type", "text/html;charset=utf-8")
ctx.body = fs.createReadStream(path.resolve(__dirname, 'form.html'));
}else{
await next();
}
})
app.use(async (ctx, next) => {
if(ctx.path === '/login' && ctx.method === 'POST'){
ctx.body = await bodyParser(ctx)
}else{
await next();
}
})
}
// middlewares/bodyParser.js
// buffer没有split分割方法 需要自己扩展
Buffer.prototype.split = function(sep){
let arr = [];
let len = Buffer.from(sep).length; // 分隔符可能是中文 保证取到的分隔符的长度是字节
let offset = 0;
let index = this.indexOf(sep); // 在二进制中查找sep的位置
while(-1 !== (index = this.indexOf(sep, offset))){
arr.push(this.slice(offset, index));
offset = index + len;
}
// 最后找不到分隔符石还需要将最后一段内容放入数组
arr.push(this.slice(offset));
console.log(arr)
return arr;
}
// Buffer.from("aaaa|bbbb|cccc").split("|");
// 这里还可以去增加一些全局的功能, 用app.use直接使用这个插件功能
const querystring = require('querystring'); // node自带
conts uuid = reuqire('uuid'); // 用于产生唯一的文件名
function bodyParser() {
return async (ctx, next) => {
ctx.request.body = await new Promise((resolve, reject) => {
let arr = [];
ctx.req.on("data", function(chunk) {
arr.push(chunk);
});
ctx.req.on('end', function() {
// 表单格式默认就是key=value&key=value
console.log(Buffer.concat(arr).toString());
// resolve(Buffer.concat(arr).toString());
/*
将数据解析成对象格式
用户提交的数据格式有很多种类型
前端最常见的是3种:1.json 2.表单格式 (a)普通字符串 (b)文件格式
*/
let type = ctx.get("Content-Type");
let str = Buffer.concat(arr);
if(type === 'application/x-www-form-urlencoded'){ // 表单格式
resolve(querystring.parse(str.toString()));
}else if(type.startsWith('text/plain')){ // 纯文本
resolve(str.toString());
}else if(type.startsWith('application/json')){ // 对象
resolve(JSON.parse(str.toString()))
}else if(type.startsWith('multipart/form-data')){ // 图片格式
let boundary = '--' + type.split("=")[1];
let lines = str.split(boundary).slice(1,-1); // 前后两段空白不要
let formData = {};
lines.forEach(line => {
let [head, body] = line.split("\r\n\r\n"); // 规范中定义的key和value之间就是用"\r\n\r\n"来分隔
console.log(head.toString());
if(head.includes('filename')){
// 文件需要放到服务器上
// 文件内容
let content = line.slice(head.length + 4, -2);
let dir = path.join(__dirname, 'public');
let filePath = uuid.v4();
let uploadPath = path.join(dir, filePath);
formData[key] = {
filename: uploadPath,
size: content.length
}
fs.writeFileSync(uploadPath, content);
}else{
let key = head.toString().match(/name="(.+?)"/)[1];
let value = body.toString().slice(0, -2); // 去掉后面的换行和回车
formData[key] = value;
}
})
resolve(formData);
}
else{
resolve({});
}
})
});
await next(); // 这个中间件 就是做一件事情 解析请求体 之后继续向下执行
}
}
module.exports = bodyParser;
为了上传图片 要加上 enctype="multipart/form-data"
<form action="/login" method="POST" enctype="multipart/form-data">
<input type="text" name="username"/>
<input type="text" name="password"/>
<input type="file" name="avatar"/>
<button type="submit">提交</button>
</form>
发送请求的时候 content-type中会自行增加一个分隔符,如:
Content-Type: multipart/form-data;boundary=----WebKitFormBoundaryl2HCVTs0JzYvAFm4
- koa官方提供了一些现成的第三方模块
- koa-bodyparser
- koa-static
- koa-router (或者叫做 @koa/router) 是一个包
自实现static中间件
const fs = require('fs').promises;
function static(staticPath){
return async(ctx, next)=> {
try{
let filePath = path.join(staticPath, ctx.path);
let statObj = await fs.stat(filePath);
if(statObj.isDirectory()){ // 如果是文件夹 会查找index.html
filePath = path.join(filePath, 'index.html');
}
ctx.body = await fs.readFile(filePath, 'utf8');
}catch(e){ // 报错说明处理不了 没有这个文件
return next(); // 交给下面的中间件来处理
}
}
}
自实现路由中间件
class Layer{
constructor(path, method, callback){
this.path = path;
this.method = method;
this.callback = callback;
}
match(path, method){
return this.path === path && this.method === method.toLowerCase();
}
}
class Router{
constructor(){
this.stack = [];
}
routes(){ // 返回一个中间件
return async(ctx, next){
let path = ctx.path;
let method = ctx.method;
let layers = this.stack.filter(layer => layer.math(path, method))
this.compose(layers,ctx, next);
}
}
compose(layers, ctx, next){
const dispatch = (i) => {
if(i == layers.length) return next();
let callback = layers[i].callback;
return Promise.resolve(callback(ctx, () => dispatch(i+1)))
}
return dispatch(0);
}
}
['get', 'post'].forEach(method => {
Router.prototype[method] = function(path, callback) {
let layer = new Layer(path, method, callback);
this.stack.push(layer);
}
})