我们都知道,在一个完整的Http请求过程中,我们需要在其中做一些处理,比如说请求路径和参数的处理,cookie的处理,返回的结果处理……对于Koa来说,我们可以使用中间件来进行处理,但是Koa本身是不提供中间件的,因此就需要我们引入第三方的,或者是根据自己实际的业务需求,来实现一些处理过程的中间件。就简单的介绍几个在Koa中会经使用的一些中间件(这里的版本是基于Koa 2.x,如果想了解Koa 1.x可以参考这些思想去找适用于Koa 1.x的中间件)。当然这里有关第三方中间件的介绍过程中,都会简单的聊一些这些中间件的实现思路,顺带着有一些简单的实现demo,当然这些demo只是会介绍一些思路,不会写的很完善,但是能够保证这些实现都是能够运行通过的。
路由
HTTP协议使用URI定位互联网上的资源。正是因为URI的特定功能,在互联网上任意位置的资源都能访问到。在HTTP请求的过程中,我们使用请求方法来告诉服务器我们的请求意图。具体的细节可以去参考《图解HTTP》或者是《HTTP 权威指南》,里面会有更详细的介绍。
路由的原理与实现思路
其实现在在我们的Web服务中,URL来区分应该执行哪些方法,将数据返回给浏览器。这个路径和执行函数的关系映射,我这里姑且称之为路由吧。
之前我们说过,Koa的上下文Context中,会将HTTP的Request和Response对象给封装在Contex上,这个时候我们可以通过一个简单的demo,打个断点,来看一下这个请求中的内容:
基本的代码和断点位置:
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
console.log(ctx);
ctx.body = 'hello wlorld';
});
app.listen(3000);
可以在上图的这个红框的位置加一个断点,接着我用调试模式运行起来我们的代码(这里使用的是VSCode,你也可以使用WebStrom,不会的可以自行百度)。当我们运行起来代码之后,在浏览器中输入:http://localhost:3000/helloworld,这个时候,程序的运行会停止在我们刚刚打的断点的位置这个时候我们看左侧的调试面板,会看到很大的一坨东西如下图所示:
上图中绿框圈的这个地方就是我们请求的URL的地方,我们能够使用这个来构建一套关系映射,用来处理不同的请求,我们知道这个路由的基本思路了,我们就顺着这个思路来自己实现一套代码:
const Koa = require('koa');
const app = new Koa();
function Router(options) {
if (!(this instanceof Router)) {
return new Router(options);
}
const router = function (ctx, next) {
console.log(this.routers);
router.run(ctx, next);
}
router.routers = new Map();
router.get = function (path, fun) {
if (this.routers.has(path)) {
throw new Error('当前的路由已经存在!');
}
this.routers.set(path, fun);
}
router.run = async function (ctx, next) {
const url = ctx.url;
const fun = this.routers.get(url);
if (fun) {
await fun(ctx, next);
} else {
ctx.body = '404 not found!';
}
}
return router; // 因为Koa的中间件是一个函数,因此我们这边需要把这个函数给return出去
}
const router = new Router();
router.get('/helloworld', async (ctx, next) => {
ctx.body = 'HelloWorld';
});
app.use(router);
app.listen(3000);
有了上面的实现了,我们就对路由以及其实现有了一个大致的了解,当然有第三方的路由的存在,我们就没有必要再重新造轮子了,我们直接用就好了。其实Koa中的路由有两个一个是官方提供的路由库(@koa/router),一个是第三方的路由组件(koa2-router)。接下里我们通过两个简单的例子,来分别的演示一下,有关两个路由的基本使用(详细的使用,可以去参考其官方文档)。
@koa/router的使用
安装库:
yarn add @koa/router
本路由库的一个简单的demo:
const Koa = require('koa');
const Router = require('@koa/router');
const app = new Koa();
const router = new Router();
router.get('/helloworld', async (ctx, next) => {
ctx.body = 'HelloWorld';
});
router.post('/post_test', async (ctx, next) => {
ctx.body = 'post test success';
})
app.use(router.routes())
app.listen(3000);
由于@koa/router的get,post等方法能够返回其自身,因此,我们可以世界连续使用,类似于下面的这样的使用方式:
router.get('/helloworld', async (ctx, next) => {
ctx.body = 'HelloWorld';
})
.post('/post_test', async (ctx, next) => {
ctx.body = 'post test success';
})
当然,本库的支持的的方法不仅仅只有get,和post,对于常见的Http请求方法(head,options,get,put,patch,post,delete),其也是支持的。
koa2-router的使用
安装库(这里得注意,是koa2-router不是koa-router):
yarn add koa2-router
本路由库的一个简单的demo:
const Koa = require('koa');
const Router = require('koa2-router');
const app = new Koa();
const router = new Router();
router.get('helloworld', async (ctx, next) => {
ctx.body = 'HelloWorld';
});
router.post('/post_test', async (ctx, next) => {
ctx.body = 'post test success';
})
app.use(router);
app.listen(3000);
其实如果是从源码的角度来看,这个库的具体的实现思路,和我们的上面的有关路由请求库的实现其实是一样的,如果感兴趣的同学可以自己去实现一套自己路由处理库。
静态文件服务
对于整个Http服务来说,本来设计的初衷就是通过Uri来访问远程服务上的文件内容的或者执行远程服务器上的应用程序。对于一个前端人员来说,或许我们对静态文件服务更加的熟悉,通过浏览器打开的本地文件,或者是我们现在的前后端分离的开发模式下,前端的文件也是基本上存放在静态服务上了(有些时候我们是配置了一个nginx,来静态的将请求地址指向某个静态的文件夹下)这个也是一个静态的服务。
content-type
如果了解Http的话,应该都知道,在我们的请求头中会有一个叫做Accept的字段,这个字段会告诉服务器说,这边能够处理哪些媒体类型以及处理媒体类型的优先级。
同样的,在请求返回的时候,浏览器需要知道我应该使用什么形式来进行显示,比如说HTML,PNG,CSS等等,浏览器需要通过某些标识来识别这种资源是吧,但是浏览器通过哪种方式来识别这种资源呢?其实是一个叫做的MIME Type的东西。
MIME(多功能网际邮件扩充协议)意为多目Internet邮件扩展,它设计的最初目的是为了在发送电子邮件时附加多媒体数据,让邮件客户程序能根据其类型进行处理。然而当它被HTTP协议支持之后,它的意义就更为显著了。它使得HTTP传输的不仅是普通的文本,而变得丰富多彩。在Http中,这个字段被放在Http的头部,被称为Content-Type。例如:Content-Type: text/HTML。如下图所示。
那么这个MIME Type应该怎么获取呢?我们使用浏览器可以通过<input type="file" />来进行获取:
但是对于Node来说,自身是不带获取这个MIME的内容的,因此会有第三方的库来进行支持 'mime',或者是'mime-type',这两个库是支持的(其实本质上是他们提供了一个已存在对应关系,就是根据文件的后缀名来进行匹配的)。
静态服务的工作流程与实现
了解完这些基础的东西,我们来简单整理一下一个静态服务的工作流程:
- 根据Url匹配的相对应的规则,将Url转换成服务器本地的文件路径;
- 检测文件是否存在
- 然后读取文件的内容
- 根据文件的后缀名获取,设置content-type
- 将文件的内容返回给浏览器
我们大致了解的静态服务的基本工作流程之后,接下来我们简单的去实现一个简单的Koa的静态服务:
const path = require('path');
const fs = require('fs');
const Koa = require('koa');
const mineTypesMap = {
png: 'image/png',
jpg: 'image/jpeg',
css: 'text/css',
html: 'text/html'
}
function staticFile(sourceDir, targetDir) {
return function(ctx, next) {
const url = ctx.url;
const aa = '';
aa.startsWith
if (url.startsWith(sourceDir)) {
const filePath = path.join(__dirname, url.replace(sourceDir, targetDir));
if (fs.existsSync(filePath)) {
const fileStat = fs.statSync(filePath);
const extname = path.extname(filePath).replace('.', '');
const mimeType = mineTypesMap[extname];
ctx.headers['content-type'] = mimeType;
const content = fs.readFileSync(filePath, 'binary');
ctx.res.writeHead(200);
ctx.res.write(content, 'binary');
ctx.res.end();
console.log(fileStat);
}
console.log('filePath:', filePath);
}
next();
}
}
const app = new Koa();
const router = new Router();
app.use(staticFile('/static', '/public'))
app.listen(3000);
还是句话,其实我们看似很高大上的东西,觉得人家怎么能够写出来这么优秀的东西的人,其实你也可以的,只是我们得把事情的每一步都给做的比较详细而已。
koa-static的使用
对于Koa来说,其实官方已经提供一个能够使用的静态服务,koa-static,我们可以直接使用:
还是需要先进行安装:
npm install koa-static
然后在代码中进行使用:
import Koa from 'koa'; // CJS: require('koa');
import serve from 'koa-static'; // CJS: require('koa-static')
const app = new Koa();
app.use(serve(root, opts)); // root 是我们的静态文件的目录,opts则是相关的一些配置项的内容
下面是Opts中的部分参数的内容
maxage
浏览器缓存最大使用时间(毫秒)。默认为0hidden
允许传输隐藏文件。默认为falseindex
默认文件名,默认为 'index.html'defer
如果为true,则在return next()之后服务,允许任何下游中间件首先响应。gzip
当客户端支持gzip时,如果请求的文件带有gzip,请尝试自动提供gzip版本的文件。gz扩展存在。默认为true。extensions
当URL中没有足够的扩展名时,尝试匹配传递数组中的扩展名以搜索文件。先找到的就上菜。(默认为false)setHeaders
自定义相应的header内容
页面模板
目前来说,我们身为一个前端,做前后端分离的项目越来越多的,很多的项目都是走的是客户端渲染(CSR),渐渐的我们忘了还有一种叫做SSR的东西。
在Express中,生成工具会根据自己的选择生成不同的版本支持不同类型的前段模板的项目,比如说ejs或者是pug的项目。这样在我们配置好之后,能够将请求的结果以HTML的形式直接返回出来。
Koa模板中间件的工作流程
老规矩,我们在使用一个东西之前,我门需要去了解一些这个东西的基本工作原理。对于Koa模板引擎(这里其实是指Koa的模板中间件)其实本质上是给用户 提供一个方法,使用户能够通过本方法,使用我们已经写好的模板,将数据给渲染到页面模板中,生成浏览器可以直接使用的html文件。
那么在这样的一个前提先,我门可以简单的梳理出一个基本的模板中间件的执行过程:
- 定义一个存放模板文件的目录;
- 当用户访问某个路由(即某个url)的时候,进行函数的处理,生成处理结果的数据,一般是一个对象(这是就简单的讲就是一个JSON);
- 使用该查找相对应的模板文件,结合刚才的数据,使用相对应的模板引擎将数据和模板,转换成能用的结果(字符串);
- 就想上面一样,设置文件的内容的解析格式(content-type)为text/html;
- 将生成的结果字符串,返回给浏览器。
模板引擎
其实要想实现一个页面模板中间件,首先我们得选择一个能够实现转换的模板引擎ejs,jade(pug)(其实还有别的,可以自行去了解)。这里我们选择一个比较顺手的,模板引擎,比如说ejs。
ejs模板的介绍
ejs高效的嵌入式JavaScript模板引擎,是一套纯的JavaScript的模板引擎,具有快速开发,语法简单,执行迅速,易于调试等有点。
这里提供一个中文文档的地址:EJS -- 嵌入式 JavaScript 模板引擎 | EJS 中文文档
在NodeJs中使用使用ejs是很简单的,首先得安装ejs模板引擎:
npm install ejs
然后将字符串模板,和数据传递给ejs,运行,这样,通过ejs模板引擎渲染的结果揪出来了。
const ejs = require('ejs');
const people = ['geddy', 'neil', 'alex'];
const html = ejs.render('<%= people.join(", "); %>', { people: people });
console.log(html);
有关ejs模板的其他的使用的内容,可以直接参考我上面给出的文档地址。
对于Koa中的ejs页面模板转化中间件,其实本质上是给ctx对象自定一个render函数,传入页面模板文件的地址,和要渲染的数据,然后使用ejs模板引擎,将其进行转化,并且将转化的结果赋值给body这样的一个过程。接着我们来使用ejs模板引擎实现一个简单的页面模板中间件。
const path = require('path');
const Koa = require('koa');
const Router = require('@koa/router');
const ejs = require('ejs');
console.log(process.cwd())
function ejsTpl(basePath) {
basePath = basePath || process.cwd();
return (ctx, next) => {
console.log('ejs tpls');
ctx.render = function(viewPath, data = {}) {
const fileName = path.join(basePath, viewPath);
ejs.renderFile(fileName, data, (error, str) => {
if (!error) {
ctx.body = str;
} else {
throw error;
}
})
}
next();
}
}
const app = new Koa();
app.use(ejsTpl(path.join(process.cwd(), 'views')));
const router = new Router();
router.get('/hello', async (ctx, next) => {
console.log('hello world');
ctx.render('index.ejs', {name: 'hello world'});
});
app.use(router.routes())
.use(router.allowedMethods());
app.listen(3000, (error) => {
if (!error) {
console.log('koa init success!');
} else {
console.log('koa listen error');
}
})
其中,我们需要在项目中新建一个views/index.ejs文件,具体的目录结构如下:
其中的index.ejs的内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1><%=name %></h1>
</body>
</html>
运行之后在浏览中查看内容如下:
koa-views的使用
在Koa中常用的模板中间件koa-views,我们还是一样的,先进行安装
npm instll koa-views
然后进行引入项目进行使用:
const Koa = require('koa')
const views = require('koa-views')
const path = require('path')
const app = new Koa()
// 加载模板引擎
app.use(views(path.join(__dirname, './views'), {
extension: 'ejs'
}))
app.use( async ( ctx ) => {
await ctx.render('index', {
name: 'hello world',
})
})
app.listen(3000, () => {
console.log('localhost:3000')
})
然后就是添加一个路由。运行结果就和我们上述的结果是一模一样的。
session(Koa-session)
我们都知道http协议是一种无状态的协议,也就是说客户端和服务器之间不需要建立持久的连接,当一个客户端向服务器发起请求后,服务器收到了请求并且返回响应结果,这次的通信就结束了,同时服务器不保留连接的相关信息。所以每次请求都需要包含所需的所有信息,这样消息结构会比较复杂,同时也会导致相同的数据在多个请求里反复传输,协议效率也会因此降低。后来为了解决这个问题人门提出了使用Cookies。
Cookies是一些存储在用户电脑上的小文件。它是被设计用来保存一些站点的用户数据,这样能够让服务器为这样的用户定制内容。页面代码能够获取到Cookie值然后发送给服务器,比如Cookie中存储了所在地理位置,以后每次进入地图就可以默认定位到改地点。
当然Cookies也不是完美的,毕竟数据是存储在浏览器端的,也是因为其存储在客户端,这就涉及到了数据的安全等相关的内容。因此不建议将一些敏感的信息保存到cookie中,而且用户有权限禁止cookie的使用。因此就有人想能不能将数据存储在服务器端。于是产生了session,因此session主要作用就是在服务端存储或者是记录本次连的或者是请求的数据。当会话结束之后,存储的这些数据就会自动释放掉,以节省服务端内存占用。
koa中常用的session处理的中间件,就是Koa-session,
- 安装 koa-session
npm install koa-session --save
- 引入express-session
const session = require('koa-session');
- 设置官方文档提供的中间件
const session_signed_key = ["some secret hurr"]; // 这个是配合signed属性的签名key
const session_config = {
key: 'koa:sess', /** cookie的key。 (默认是 koa:sess) */
maxAge: 4000, /** session 过期时间,以毫秒ms为单位计算 。*/
autoCommit: true, /** 自动提交到响应头。(默认是 true) */
overwrite: true, /** 是否允许重写 。(默认是 true) */
httpOnly: true, /** 是否设置HttpOnly,如果在Cookie中设置了"HttpOnly"属性,那么通过程序(JS脚本、Applet等)将无法读取到Cookie信息,这样能有效的防止XSS攻击。 (默认 true) */
signed: true, /** 是否签名。(默认是 true) */
rolling: true, /** 是否每次响应时刷新Session的有效期。(默认是 false) */
renew: false, /** 是否在Session快过期时刷新Session的有效期。(默认是 false) */
};
4 .使用session
设置值 ctx.session.username = "张三";
获取值 ctx.session.username
其实对于session的使用还是比较简单简单的。
小结
经过以上的分析,我们认识Koa的中间件,另外也大概上了解了部分中间件的实现思路,这个是不是发现,其实真的没有什么大不了的,很多的底层的东西,如果去认真思考,其实我们也是能够实现的。还是那句话,很多高大上东西的背后也不过如此。