相關文章 最基礎 實現一個簡單的koa2框架 實現一個簡版koa koa實踐及其手擼html
Koa源碼只有4個js文件mongodb
- 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(只讀)express
// 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的總執行次數和中間件的長度,若是不同,就要報錯json
環形【洋蔥】有什麼好處 上面的洋蔥圈可能沒看懂,上一個簡易版的segmentfault
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,查數據庫,響應bodypromise
文件訪問中間件app
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的使用