Koa原理和封装
相关文章
最基础
实现一个简单的koa2框架
实现一个简版koa
koa实践及其手撸
Koa源码只有4个js文件
- application.js:简单封装http.createServer()并整合context.js
- context.js:代理并整合request.js和response.js
- request.js:基于原生req封装的更好用
- response.js:基于原生res封装的更好用
如果我们要封装一个Koa,
需要实现use加载中间件,
next下一个中间件,并且是环形的,
中间件是promise的
ctx=>对应常用的是 body(可读写)/ url(只读)/ method(只读)
// request.js
const request = {
get url() {
return this.req.url;
},
set url(val) {
this.req.url = val;
}
};
module.exports = request;
// response.js
const response = {
get body() {
return this._body;
},
set body(data) {
this._body = data;
},
get status() {
return this.res.statusCode;
},
set status(statusCode) {
if (typeof statusCode !== 'number') {
throw new Error('statusCode 必须为一个数字');
}
this.res.statusCode = statusCode;
}
};
module.exports = response;
// context.js
const context = {
get url() {
return this.request.url;
},
set url(val) {
this.request.url = val;
},
get body() {
return this.response.body;
},
set body(data) {
this.response.body = data;
},
get status() {
return this.response.statusCode;
},
set status(statusCode) {
if (typeof statusCode !== 'number') {
throw new Error('statusCode 必须为一个数字');
}
this.response.statusCode = statusCode;
}
};
module.exports = context;
const Emitter = require('events');
const http = require('http');
// 引入 context request, response 模块
const context = require('./context');
const request = require('./request');
const response = require('./response');
class Application extends Emitter {
/* 构造函数 */
constructor() {
super();
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
// 保存所有的中间件函数
this.middlewares = [];
}
// 开启 http server 并且传入参数 callback
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
use(fn) {
// this.callbackFunc = fn;
// 把所有的中间件函数存放到数组里面去
this.middlewares.push(fn);
return this;
}
callback() {
return (req, res) => {
// 创建ctx
const ctx = this.createContext(req, res);
// 响应内容
const response = () => this.responseBody(ctx);
// 响应时 调用error函数
const onerror = (err) => this.onerror(err, ctx);
//调用 compose 函数,把所有的函数合并
const fn = this.compose();
return fn(ctx).then(response).catch(onerror);
}
}
/**
监听失败,监听的是上面的catch
*/
onerror(err) {
if (!(err instanceof Error)) throw new TypeError(util.format('non-error thrown: %j', err));
if (404 == err.status || err.expose) return;
if (this.silent) return;
const msg = err.stack || err.toString();
console.error();
console.error(msg.replace(/^/gm, ' '));
console.error();
}
/*
构造ctx
@param {Object} req实列
@param {Object} res 实列
@return {Object} ctx实列
*/
createContext(req, res) {
// 每个实列都要创建一个ctx对象
const ctx = Object.create(this.context);
// 把request和response对象挂载到ctx上去
ctx.request = Object.create(this.request);
ctx.response = Object.create(this.response);
ctx.req = ctx.request.req = req;
ctx.res = ctx.response.res = res;
return ctx;
}
/*
响应消息
@param {Object} ctx 实列
*/
responseBody(ctx) {
const content = ctx.body;
if (typeof content === 'string') {
ctx.res.setHeader('Content-Type', 'text/pain;charset=utf-8')
ctx.res.end(content);
} else if (typeof content === 'object') {
ctx.res.setHeader('Content-Type', 'text/json;charset=utf-8')
ctx.res.end(JSON.stringify(content));
}
}
/*
把传进来的所有的中间件函数合并为一个中间件
@return {function}
*/
compose(){
let middlewares = this.middlewares
return function(ctx){
return dispatch(0)
function dispatch(i){
let fn = middlewares[i]
if(!fn){
return Promise.resolve()
}
return Promise.resolve(fn(ctx, function next(){
return dispatch(i+1)
}))
}
}
}
}
module.exports = Application;
// 使用
const testKoa = require('./application');
const app = new testKoa();
app.use((ctx) => {
str += 'hello world'; // 没有声明该变量, 所以直接拼接字符串会报错
ctx.body = str;
});
app.on('error', (err, ctx) => { // 捕获异常记录错误日志
console.log(err);
});
app.listen(3000, () => {
console.log('listening on 3000');
});
优化
如果有一个中间件写了两个next,会执行两次,需要通过判断next的总执行次数和中间件的长度,如果不一样,就要报错
环形【洋葱】有什么好处
上面的洋葱圈可能没看懂,上一个简易版的
var arr = [function(next){
console.log(1)
next()
console.log(2)
},function(next){
console.log(3)
next()
console.log(4)
}]
var i = 0;
function init(){
arr[i](function(){
i++
if(arr[i]){
init()
}
})
}
init()
// 1342
为什么是1342
上面的代码打个断点就知道了
// 这样应该看得懂吧
function(){
console.log(1)
var next = function(){
console.log(3)
var next = ...
console.log(4)
}
next()
console.log(2)
}
在以前不是express设计的框架,整个请求到响应结束是链结构的,一个修改响应的插件就需要放到最后面,但是有个环形的设计,只要把要修改响应的代码写到next执行后就行了,对于开发者也是,获取请求的数据,修改请求的数据,next,查数据库,响应body
文件访问中间件
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();
}
}
}
// 使用
const static = require('./static')
app.use(static(__dirname + '/public'));
路由中间件
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;
// 使用
const Koa = require('Koa')
const Router = require('./router')
const app = new Koa()
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'; });
// 路由实例输出父中间件
app.use(router.routes());
下一篇mongodb插件mongoose的使用