不完全的koa~
- lib > 目录
- request.js
- response.js
- context.js
- body-parser.js
- cookie-parser.js
- koa-router.js
- koa-static.js
- koa.js
koa.js > 主入口文件
const http = require('http');
const request = require('./lib/request');
const response = require('./lib/response');
const context = require('./lib/context');
const fs = require('fs');
const Stream = require('stream');
const EventEmitter = require('events');
class Koa extends EventEmitter {
constructor() {
super();
this.middleware = []; // 存放中间件
this.context = Object.create(context); // 创建原型为context的对象
this.request = Object.create(request); // 创建原型为request的对象
this.response = Object.create(response); // 创建原型为response的对象
}
use(fn) {
this.middleware.push(fn); // 将中间件存入
}
createContext(req, res) {
const ctx = Object.create(this.context); // 创建koa真正的context对象,原型为this.context
const request = Object.create(this.request); // 创建request对象,原型为this.request
const response = Object.create(this.response); // 创建response对象,原型为this.response
ctx.request = request; // 将request对象挂载到ctx的request
ctx.req = ctx.request.req = req; // 将原生req对象挂载到ctx.request.req再挂载到ctx.req,共享一个req
ctx.response = response; // 将response对象挂载到ctx的response
ctx.res = ctx.response.res = res; // 将原生res对象挂载到ctx.response.res再挂载到ctx.res,共享一个res
return ctx; // 返回创建的ctx对象
}
compose(ctx) {
const middleware = this.middleware; // 获取全部中间件
let index = -1; // 初始化index
function next(idx) { // 定义递归函数
if (index >= idx) return Promise.reject(new Error('next() call multiple times')); // 如果重复调用next方法就掏出错误
index = idx;
if (middleware.length === idx) return Promise.resolve('Not Found~'); // idx大于中间件长度,没有匹配到对应中间件
return Promise.resolve(middleware[idx](ctx, () => next(idx + 1))); // 调用中间件,交传入ctx和next函数,只有调用next才能继续执行下一个中间件
}
return next(0); // 默认从0索引调用
}
handleBody(ctx, body) { // 处理body响应体
if (!body) { // 没有body响应体,直接返回状态码404和响应体Not Found
ctx.res.statusCode = 404;
return ctx.res.end('Not Found');
}
if (typeof body === 'string') { // body响应体类型为string,设置响应类型为html
ctx.res.setHeader('Content-Type', 'text/html;charset=utf-8');
ctx.res.end(body);
} else if (body instanceof Stream) { // body响应体类型为stream,设置响应类型为json
ctx.res.setHeader('Content-Type', 'application/json;charset=utf-8');
body.pipe(ctx.res);
} else if (typeof body === 'object') { // body响应休类型为object,设置响应类型为JSON字符串
ctx.res.setHeader('Content-Type', 'application/json;charset=utf-8');
ctx.res.end(JSON.stringify(body));
} else if (typeof body === 'number') { // body响应体类型为number,设置响应类型为palin纯文本,并将数据转换成字符串
ctx.res.setHeader('Content-Type', 'text/plain;charset=utf-8');
ctx.res.end(body + '');
} else if (body instanceof Buffer) { // body响应体类型为buffer,设置响应类型为palin纯文本,并将数据转换成字符串
ctx.res.setHeader('Content-Type', 'text/plain;charset=utf-8');
ctx.res.end(body.toString());
}
}
handleRequest(req, res) {
// const middleware = this.middleware;
const ctx = this.createContext(req, res); // 调用创建ctx对象
this.compose(ctx).then(() => { // 调用compose方法,返回一个promise执行then监听变化
let body = ctx.body; // 获取body响应体
this.handleBody(ctx, body); // 调用handleBody方法处理响应体
}).catch(err => {
this.emit('error', err); // 处理错误
})
// function next(idx) {
// if (middleware.length === idx) return Promise.resolve('Not Found~');
// return Promise.resolve(middleware[idx](ctx, () => next(idx + 1)));
// }
// next(0);
}
listen(port) { // koa的监听方法
// 创建一个http Server 服务器
http.createServer((req, res) => {
this.handleRequest(req, res); // 调用handleRequest方法处理req和res对象
}).listen(port); // 监听端口号
}
}
module.exports = Koa;
context.js
const context = { // 定义context对象
}
function defineGetter(target, key) { // get代理方法
context.__defineGetter__(key, function () {
return this[target][key]; // 将context上的属性代理到target上>context.pathname == context.request.pathname
})
}
function defineSetter(target, key) { // set代理方法
context.__defineSetter__(key, function (value) {
this[target][key] = value;
})
}
defineGetter('request', 'url'); // 将requst上的url方法代理到context上
defineGetter('request', 'pathname'); // 将requst上的pathname方法代理到context上
defineGetter('request', 'query'); // 将requst上的query方法代理到context上
defineGetter('request', 'method'); // 将requst上的method方法代理到context上
defineGetter('request', 'body'); // 将requst上的body方法代理到context上 - 获取
defineSetter('request', 'body'); // 将request上的body方法代理到context上 - 设置
module.exports = context;
request.js
const url = require('url');
const request = {
get url() {
return this.req.url; // 获取url地址
},
get pathname() {
return url.parse(this.req.url).pathname; // 获取请求路径
},
get query() {
return url.parse(this.req.url,true).query; // 获取query查询字符串
},
get method() {
return this.req.method; // 获取请求方法
},
get body() {
return this.req.body; // 获取请求体
},
set body(value) {
this.req.body = value; // 设置请求休
},
get request() {
return this.req; // 获取原生req对象
}
}
module.exports = request;
response.js
const response = {
_body: undefined,
get body() {
return this._body;
},
set body(value) {
this._body = value;
},
get response() {
return this.res;
}
}
module.exports = response;
koa-router.js > 路由实例
此处仅为一个简单路由,只能处理单层路径
class Layer { // 用来放在单条路由规则
constructor(method, path, callback) {
this.method = method;
this.path = path;
this.callback = callback;
}
match(requestPath, requestMethod) { // 匹配路由方法
return requestPath === this.path && requestMethod == this.method;
}
}
class KoaRouter {
constructor() {
this.stack = []; // 存放router实例
}
post(pathname, callback) {
let layer = new Layer('POST', pathname, callback);
this.stack.push(layer);
}
get(pathname, callback) {
let layer = new Layer('GET', pathname, callback);
this.stack.push(layer);
}
put(pathname, callback) {
let layer = new Layer('PUT', pathname, callback);
this.stack.push(layer);
}
delete(pathname, callback) {
let layer = new Layer('DELETE', pathname, callback);
this.stack.push(layer);
}
routes() {
return async (ctx, next) => { // 注册routes中间件
let requestPath = ctx.pathname; // 获取请求路径
let requestMethod = ctx.method; // 获取施救方法
// 用请求路径和请求方法过滤匹配到的路由
let matchLayers = this.stack.filter(layer => layer.match(requestPath, requestMethod));
this.compose(matchLayers, ctx, next); // 用cmpose方法处理匹配到的路由
}
}
compose(matchLayers, ctx, next) {
function dispatch(index) {
if (index === matchLayers.length) return next(); // 索引越界,交给下一个中间件处理
return Promise.resolve(matchLayers[index].callback(ctx, () => dispatch(++index))); // 递归处理路由
}
return dispatch(0);
}
}
module.exports = KoaRouter;
koa-static.js > 静态路由中间件
const path = require('path');
const crypto = require('crypto');
const fs = require('fs').promises;
const { readFileSync } = require('fs');
// 缓存文件方法
function cacheFile(req, res, filePath, fileStat) {
res.setHeader('Cache-Control', 'max-age=10'); // 设定强制缓存,时间为10秒
const lastModifyed = fileStat.ctime.toGMTString(); // 获取文件最后修改时间
const etag = crypto.createHash('md5').update(readFileSync(filePath)).digest('base64'); // 将文件内容时间摘要(通常不会对整个文件进行摘要,因为内容太多影响性能,一般选择其中一部分,由于这里只是学习使用,就不用考虑太多!)
res.setHeader('Last-Modified', lastModifyed); // 设定最后修改时间的响应头
res.setHeader('Etag', etag); // 设置Etag响应头
const ifModifiedSince = req.headers['if-modified-since']; // 获取上一次修改时间
const ifNoneMatch = req.headers['if-none-match']; // 与Etag搭配使用的请求头,对比Etag值,如果相同则表示文件没变化,可走缓存,否则重新加载
if (ifModifiedSince !== lastModifyed) { // 判断文件的最后修改时间和请求头里最后的修改时间是否相同
return false;
}
if (etag !== ifNoneMatch) { // 判断 Etag和ifNoneMatch值是否相同
return false;
}
return true;
}
function KoaStatic(staticPath) { // 静态文件服务中间件
return async (ctx, next) => {
let filePath = path.join(staticPath, ctx.pathname); // 将静态目录和路由路径合并得到资源地址
try {
let fileStat = await fs.stat(filePath); // 获取文件状态信息(是否存在此文件或文件夹)
if (fileStat.isDirectory()) { // 判断是否为目录
filePath = path.join(filePath, 'index.html'); // 如果为目录则默认加上 index.html 路径
}
if (cacheFile(ctx.request.req, ctx.response.res, filePath, fileStat)) { // 如果文件符合缓存策略就使用缓存,返回状态码304
ctx.res.statusCode = 304;
}
ctx.body = await fs.readFile(filePath, 'utf-8'); // 用fs模块读取此文件内容
} catch {
await next(); // 如果找不到资源就执行下一个中间件继续处理
}
}
}
module.exports = KoaStatic;
body-parser.js
const querystring = require('querystring');
function KoaBodyParser() { // body-parser中间件
return async (ctx, next) => { // 中间件需要返回一个函数
// 监听请求,将数据存放到ctx.request.body上
ctx.request.body = await new Promise((resolve, reject) => {
const dataBuffer = []; // 定义一个buffer缓冲区
let contentType = ctx.request.req.headers['content-type']; // 获取请求数据类型
ctx.req.on('data', chunk => {
dataBuffer.push(chunk); // 建立数据传输通道,将buffer数据存入buffer缓冲区
})
ctx.req.on('end', () => { // 数据传输完成或者get请求没有请求体,会直接触发end事件
let resultBody;
if (contentType === 'application/x-www-form-urlencoded') {
resultBody = querystring.parse(Buffer.concat(dataBuffer).toString()); // 处理单urlencoded类型数据
} else if (contentType && contentType.includes('multipart/form-data')) {
resultBody = Buffer.concat(dataBuffer).toString(); // 处理form-data数据
} else if (contentType === 'text/plain') {
resultBody = Buffer.concat(dataBuffer).toString(); // 处理plain纯文本数据
}
resolve(resultBody); // 将数据返回
})
})
await next(); // 当没有监听到请求就直接执行一下个中间件
}
}
// parseBody(data) {
// }
module.exports = KoaBodyParser;
cookie-parser.js
const crypto = require('crypto');
function CookiePaser() {
return async (ctx, next) => {
ctx.cookies = {}; // 定义cookies对象
const sign = value => { // 签名方法
// 采用sha1加密,密码为xin(自定义)
return crypto.createHmac('sha1', 'xin').update(value).digest('base64')
.replace(/\+/g, '-').replace(/\=/g, '').replace(/\//g, '_'); // 将\替换成-、-删除、/替换成_
}
ctx.cookies.get = function (key, options = {}) { // cookies的get方法
let cookieArr = ctx.req.headers['cookie'].split('; '); // 将cookie以; 分割成数组
cookieArr.forEach(item => { // 遍历cookies
ctx.cookies[item.split('=')[0]] = item.split('=')[1]; // 将cookie条目用=切割,左边0索引为key,右边1索引为value
})
if (options.signed) { // 当有signed选项
if (ctx.cookies[key + '.sig'] === sign(`${ctx.cookies[key]}`)) { // 对.sig的cookie和原cookie重要加密看是否被 修改
return ctx.cookies[key]; // 如果没有被修改就返回
} else {
return '';
}
}
return ctx.cookies[key] || ''; // 返回匹配到的cookie
}
let cookiesArr = [];
ctx.cookies.set = function (key, value, options) { // cookie的set方法
function setCookie(key, value, options = {}) {
const { domain, path, expires, maxAge, httpOnly, secure, signed } = options;
const parts = [`${key}=${value}`];
if (domain) parts.push(`Domain=${domain}`);
if (path) parts.push(`Path=${path}`);
if (expires) parts.push(`Expires=${expires}`);
if (maxAge) parts.push(`Max-Age=${maxAge}`);
if (httpOnly) parts.push('httpOnly');
if (secure) parts.push('securt');
if (signed) cookiesArr.push(`${key}.sig=${sign(String(value))}`);
const cookies = parts.join('; ');
return cookies;
}
cookiesArr.push(setCookie(key, value, options));
ctx.res.setHeader('Set-Cookie', cookiesArr);
}
await next();
}
}
module.exports = CookiePaser;