苏莉koa 2019-12-20
相关文章
最基础
实现一个简单的koa2框架
实现一个简版koa
koa实践及其手撸
Koa源码只有4个js文件
如果我们要封装一个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的使用