跳到主要内容

狭义中间件-context 代理

koa-view 实现

初始化实例代理上下文 context 实现

前言

狭义中间件区别于请求/响应拦截的另一种方式是上下文 context 代理。

上下文 context 代理分成两种

  • 实例代理上下文 context
  • 请求过程代理上下文 context

这里先将第一种代理方式——实例代理上下文 context 实现步骤,实例代理的比较有代表性的中间件是官方提 koa-ejs 中间件,把渲染的方法挂载在 Koa 实例 appapp.context 属性中。

常见化实例代理上下文 context 实现步骤

  • 初始化一个 Koa 实例 let app = new Koa()
  • 将需要的属性或者方法 demo 挂载在 app.context 上,app.context.demo
  • app.use()中间件直接使用 ctx.demo 方法或属性

这里我们实现最简单的模板渲染中间件 view,模仿 koa-ejs 的基本能力。

实现步骤

view 的实现步骤

  • step 01 初始化一个 Koa 实例 let app = new Koa()
  • step 02 将需要的属性或者方法 view 挂载在 app.context 上,app.context.view
  • step 03 在 app.use() 中间件直接使用 ctx.view 方法或属性渲染模板

实现源码

demo 源码

解读

const path = require('path');
const fs = require('fs');

function view(app, opts = {}) {
const { baseDir = '' } = opts;

app.context.view = function (page = '', obj = {}) {
let ctx = this;
let filePath = path.join(baseDir, page);
if (fs.existsSync(filePath)) {
let tpl = fs.readFileSync(filePath, 'binary');
ctx.body = tpl;
} else {
ctx.throw(404);
}
};
}

module.exports = view;

使用

const Koa = require('koa');
const path = require('path');
const view = require('./index');
const app = new Koa();

view(app, {
baseDir: path.join(__dirname, 'views')
});

app.use(async ctx => {
await ctx.view(`${ctx.path}.html`, {
title: 'index page'
});
});

app.listen(3000, () => {
console.log('[demo] view is starting at port 3000');
});

koa-jsonp 实现

初始化时候,实例代理上下文 context 实现

前言

实例代理的还有另外比较有代表性的中间件是官方提供 koa-safe-jsonp 中间件,把 jsonp 的方法挂载在Koa实例appapp.context 属性中。

常见实例代理上下文 context 实现步骤

  • 初始化一个Koa实例 let app = new Koa()
  • 将需要的属性或者方法 demo 挂载在 app.context 上,app.context.demo
  • app.use()中间件直接使用 ctx.demo 方法或属性

这里我们实现最简单的模板渲染中间件 jsonp,模仿koa-safe-jsonp的基本能力。

实现步骤

jsonp 的实现步骤

  • step 01 初始化一个Koa实例 let app = new Koa()
  • step 02 将需要的属性或者方法 jsonp 挂载在 app.context 上,app.context.jsonp
  • step 03 在app.use()中间件直接使用 ctx.jsonp 方法或属性渲染模板
  • step 04 当前请求响应要返回 jsonp 数据时候 ctx.body = ctx.jsonp(result)

实现源码

demo 源码

https://github.com/chenshenhai/koajs-design-note/tree/master/demo/chapter-05-02

解读

function jsonp(app, opts = {}) {
let callback = opts.callback || 'callback';

app.context.jsonp = function (obj = {}) {
let ctx = this;
if (Object.prototype.toString.call(obj).toLowerCase() === '[object object]') {
let jsonpStr = `;${callback}(${JSON.stringify(obj)})`;

// 用text/javascript,让请求支持跨域获取
ctx.type = 'text/javascript';

// 输出jsonp字符串
ctx.body = jsonpStr;
} else {
ctx.throw(500, 'result most be a json');
}
};
}

module.exports = jsonp;

使用

const Koa = require('koa');
const jsonp = require('./index');
const app = new Koa();

jsonp(app, {});

app.use(async ctx => {
await ctx.jsonp({
data: 'this is a demo',
success: true
});
});

app.listen(3000, () => {
console.log('[demo] jsonp is starting at port 3000');
});

koa-bodyparser 实现

前言

狭义中间件的上下文代理,除了在实例化 let app = new Koa() 的时候将属性或者方法挂载到app.context 中,供后续中间件使用。另外一种方式是在请求过程中在顶端中间件(一般在第一个中间件)使用,把数据或者方法挂载代理到ctx 供下游中间件获取和使用。

这里 请求代理上下文实现 最代表性是官方提供的koa-bodyparser 中间件,这里基于官方原版用最简单的方式实现koa-bodyparser最简单功能。

常见请求代理上下文context实现过程

  • 请求代理ctx
  • 直接app.use()
  • 在请求过程中过载方法或者数据到上下文ctx
  • 一般在大部分中间件前加载,供下游中间件获取挂载的数据或方法

实现步骤

  • step 01 app.use()在中间件最顶端
  • step 02 拦截post请求
  • step 03 等待解析表单信息
  • step 04 把表单信息代理到ctx.request.body上
  • step 05 下游中间件都可以在ctx.request.body中获取表单数据

实现源码

demo源码

https://github.com/chenshenhai/koajs-design-note/tree/master/demo/chapter-05-03

依赖

请求体数据流解析方法

module.exports = readStream;

function readStream(req) {
return new Promise((resolve, reject) => {
try {
streamEventListen(req, (data, err) => {
if (data && !isError(err)) {
resolve(data);
} else {
reject(err);
}
});
} catch (err) {
reject(err);
}
});
}

function isError(err) {
return Object.prototype.toString.call(err).toLowerCase() === '[object error]';
}

function streamEventListen(req, callback) {
let stream = req.req || req;
let chunk = [];
let complete = false;

// attach listeners
stream.on('aborted', onAborted);
stream.on('close', cleanup);
stream.on('data', onData);
stream.on('end', onEnd);
stream.on('error', onEnd);

function onAborted() {
if (complete) {
return;
}
callback(null, new Error('request body parse aborted'));
}

function cleanup() {
stream.removeListener('aborted', onAborted);
stream.removeListener('data', onData);
stream.removeListener('end', onEnd);
stream.removeListener('error', onEnd);
stream.removeListener('close', cleanup);
}

function onData(data) {
if (complete) {
return;
}
if (data) {
chunk.push(data.toString());
}
}

function onEnd(err) {
if (complete) {
return;
}

if (isError(err)) {
callback(null, err);
return;
}

complete = true;
let result = chunk.join('');
chunk = [];
callback(result, null);
}
}

解读

const readStream = require('./lib/read_stream');
let strictJSONReg = /^[\x20\x09\x0a\x0d]*(\[|\{)/;

let jsonTypes = [
'application/json'
];

let formTypes = [
'application/x-www-form-urlencoded'
];

let textTypes = [
'text/plain'
];

function parseQueryStr(queryStr) {
let queryData = {};
let queryStrList = queryStr.split('&');
for (let [ index, queryStr ] of queryStrList.entries()) {
let itemList = queryStr.split('=');
queryData[ itemList[0] ] = decodeURIComponent(itemList[1]);
}
return queryData;
}

function bodyParser(opts = {}) {
return async function(ctx, next) {
if (!ctx.request.body && ctx.method === 'POST') {
let body = await readStream(ctx.request.req);
let result = body;
if (ctx.request.is(formTypes)) {
result = parseQueryStr(body);
} else if (ctx.request.is(jsonTypes)) {
if (strictJSONReg.test(body)) {
try {
result = JSON.parse(body);
} catch (err) {
ctx.throw(500, err);
}
}
} else if (ctx.request.is(textTypes)) {
result = body;
}

ctx.request.body = result;
}
await next();
};
}

module.exports = bodyParser;

使用

const Koa = require('koa');
const fs = require('fs');
const path = require('path');
const body = require('../index');
const app = new Koa();

app.use(body());

app.use(async(ctx, next) => {
if (ctx.url === '/') {
// 当GET请求时候返回表单页面
let html = fs.readFileSync(path.join(__dirname, './index.html'), 'binary');
ctx.body = html;
} else if (ctx.url === '/post' && ctx.method === 'POST') {
// 当POST请求的时候,解析POST表单里的数据,并显示出来
ctx.body = ctx.request.body;
} else {
// 其他请求显示404
ctx.body = '<h1>404!!! o(╯□╰)o</h1>';
}

await next();
});

app.listen(3000, () => {
console.log('[demo] is starting at port 3000');
});
<html>
<head>
<title>example</title>
</head>
<body>
<div>
<p>form post demo</p>
<form method="POST" action="/post">
<span>data</span>
<textarea name="userName" ></textarea><br/>
<button type="submit">submit</button>
</form>
</div>
</body>
</html>