万万万万万万没想到会来到js第十篇,Node Js第四篇,第十篇写Koa框架及Eggjs
koa
简介/安装
Koa是一个类似于Express的Web开发框架,创始人也是同一个人。它的主要特点是,使用了ES6的Generator函数,进行了架构的重新设计。也就是说,Koa的原理和内部结构很像Express,但是语法和内部结构进行了升级。
官方faq有这样一个问题:”为什么koa不是Express 4.0?“,回答是这样的:”Koa与Express有很大差异,整个设计都是不同的,所以如果将Express 3.0按照这种写法升级到4.0,就意味着重写整个程序。所以,我们觉得创造一个新的库,是更合适的做法。“
一个Koa应用就是一个对象,包含了一个middleware数组,这个数组由一组Generator函数组成。这些函数负责对HTTP请求进行各种加工,比如生成缓存、指定代理、请求重定向等等。
初始化文件夹
1 | npm init |
安装koa
1 | npm install koa |
最简单的demo
1 | const Koa = require('koa') |
核心概念
ctx:koa将node的request和response对象封装进ctx,得到ctx.request、cox.response。特别的,ctx将常用的属性做了进一步简化,可以由ctx直接访问,如ctx.request,url可以简化为ctx.request
next:next参数将处理的控制权转交下一个中间件,响应结束时再由中间件逐层传递回来,也是著名的洋葱模型
request对象:表示HTTP请求。
response对象:表示HTTP回应。
ctx对象的属性:
- request:指向Request对象
- response:指向Response对象
- req:指向Node的request对象
- res:指向Node的response对象
- app:指向App对象
- state:用于在中间件传递信息。
request对象的属性:
(1) this.request.header:返回一个对象,包含所有HTTP请求的头信息。也可以写成this.request.headers
。
(2) this.request.method:返回HTTP请求的方法,该属性可读写。
(3)this.request.length:返回HTTP请求的Content-Length属性,取不到值,则返回undefined。
(4)this.request.path:返回HTTP请求的路径,该属性可读写。
(5)this.request.href:返回HTTP请求的完整路径,包括协议、端口和url。
(6)this.request.querystring:返回HTTP请求的查询字符串,不含问号。该属性可读写。
(7)this.request.ip:返回发出HTTP请求的IP地址。
(8)this.request.fresh:返回一个布尔值,表示缓存是否代表了最新内容。通常与If-None-Match、ETag、If-Modified-Since、Last-Modified等缓存头,配合使用。
(9)this.request.query:返回一个对象,包含了HTTP请求的查询字符串。如果没有查询字符串,则返回一个空对象。该属性可读写。
(10)this.request.host:返回HTTP请求的主机(含端口号)。
(11)this.request.hostname:返回HTTP的主机名(不含端口号)。
(12)this.request.search:返回HTTP请求的查询字符串,含问号。该属性可读写。
(13)this.request.type:返回HTTP请求的Content-Type属性。
(14)this.request.charset:返回HTTP请求的字符集。
(15)this.request.protocol:返回HTTP请求的协议,https或者http。
(16)this.request.secure:返回一个布尔值,表示当前协议是否为https。
(17)this.request.is(types…):返回指定的类型字符串,表示HTTP请求的Content-Type属性是否为指定类型。
(18)this.request.accepts(types):检查HTTP请求的Accept属性是否可接受,如果可接受,则返回指定的媒体类型,否则返回false。
(19)this.request.acceptsEncodings(encodings):该方法根据HTTP请求的Accept-Encoding字段,返回最佳匹配,如果没有合适的匹配,则返回false。
(20)this.request.acceptsCharsets(charsets):该方法根据HTTP请求的Accept-Charset字段,返回最佳匹配,如果没有合适的匹配,则返回false。
(21)this.request.acceptsLanguages(langs):该方法根据HTTP请求的Accept-Language字段,返回最佳匹配,如果没有合适的匹配,则返回false。
(22)this.request.socket:返回HTTP请求的socket。
(23)this.request.get(field):返回HTTP请求指定的字段。
response对象的属性:
(1)this.response.header:返回HTTP回应的头信息。
(2)this.response.socket:返回HTTP回应的socket。
(3)this.response.status:返回HTTP回应的状态码。默认情况下,该属性没有值。该属性可读写,设置时等于一个整数。
(4)this.response.message:返回HTTP回应的状态信息。该属性与this.response.message
是配对的。该属性可读写。
(5)this.response.length:返回HTTP回应的Content-Length字段。该属性可读写,如果没有设置它的值,koa会自动从this.request.body推断。
(6)this.response.body: 返回HTTP回应的信息体。该属性可读写,它的值可能有以下几种类型。
字符串:Content-Type字段默认为text/html或text/plain,字符集默认为utf-8,Content-Length字段同时设定。
二进制Buffer:Content-Type字段默认为application/octet-stream,Content-Length字段同时设定。
Stream:Content-Type字段默认为application/octet-stream。
JSON对象:Content-Type字段默认为application/json。
null(表示没有信息体)
(7)this.response.get(field):返回HTTP回应的指定字段。
(8)this.response.set():设置HTTP回应的指定字段。
(9)this.response.remove(field):移除HTTP回应的指定字段。
(10)this.response.is(types…):该方法类似于this.request.is()
,用于检查HTTP回应的类型是否为支持的类型。
它可以在中间件中起到处理不同格式内容的作用。
(11)this.response.redirect(url, [alt]):该方法执行302跳转到指定网址。如果redirect方法的第一个参数是back,将重定向到HTTP请求的Referrer字段指定的网址,如果没有该字段,则重定向到第二个参数或“/”网址。
(12)this.response.attachment([filename]):该方法将HTTP回应的Content-Disposition字段,设为“attachment”,提示浏览器下载指定文件。
(13)this.response.headerSent:该方法返回一个布尔值,检查是否HTTP回应已经发出。
(14)this.response.lastModified:该属性以Date对象的形式,返回HTTP回应的Last-Modified字段(如果该字段存在)。该属性可写。
(15)this.response.etag:该属性设置HTTP回应的ETag字段。
(16)this.response.vary(field):该方法将参数添加到HTTP回应的Vary字段。
中间件
Koa的中间件很像Express的中间件,也是对HTTP请求进行处理的函数,但是必须是一个Generator函数。而且,Koa的中间件是一个级联式(Cascading)的结构,也就是说,属于是层层调用,第一个中间件调用第二个中间件,第二个调用第三个,以此类推。上游的中间件必须等到下游的中间件返回结果,才会继续执行,这点很像递归。
中间件通过当前应用的use方法注册。
app.use
方法的参数就是中间件,它是一个Generator函数,最大的特征就是function命令与参数之间,必须有一个星号。Generator函数的参数next,表示下一个中间件。
洋葱模型
实例
1 | //打印时间戳 |
源码koa-compose
koa中比较重要的点:
- context的保存和传递
- 中间件的管理和next的实现
1.app.listen使用了this.callback()来生成node的httpServer的回调函数。
1 | listen(...args) { |
中间件引擎
1 | callback() { |
使用compose函数处理中间件。compose中有dispatch
函数,它将遍历整个middleware
,然后将context
和dispatch(i + 1)
传给middleware
中的方法。
1 | function compose (middleware) { |
路由
使用koa-router处理URL
安装
1 | npm i koa-router --save |
实例
1 | const Koa = require('koa'); |
处理请求
使用koa-router处理请求,如get、post
post请求使用koa-bodyparser处理body中的数据
1 | npm i koa-bodyparser --save |
1 | const Koa = require('koa'); |
日志
koa-logger
这个库比较简单,记录请求的基本信息,比如请求的方法、URl、用时等。作为中间件中使用,注意:推荐放在所有的中间件之前,这个跟 koa 的洋葱模型有关。假如不是第一个,计算时间会不准确。
1 | var logger = require('koa-logger'); |
默认情况下,日志是通过 console
的方式直接输出到控制台中,假如我们需要对日志做自定义的操作,比如写入到日志文件中等。可以通过类似完成
koa-log4js
koa-logger
比较轻量,也暴露了相对灵活的接口。但在实际业务中使用,我个人推荐使用 koa-log4js
。主要理由如下:
koa-logger
看起来只支持中间件的使用方式,而不支持上报特定日志的功能。- 内置的功能比较少。比如日志的分类和落盘等。
koa-log4js[2] 对 log4js-node[3] 做了一层包装,从而支持 Koa
日志的中间件。它的配置和 log4js-node
是保持一致的。所以假如你用 log4js-node
的话,使用上应该是一致的。
安装
1 | npm i --save koa-log4 |
在根目录新建一个文件夹 log
。并且新建一个文件夹 utils
,在其中新建文件 logger.js
1 | const path = require('path'); |
categories配置日志类别。必须配置默认日志类别,用于没有命中的情况下的兜底行为。该配置为一个对象,key
值为分类名称。其中每个类别都有两个配置 appenders
是一个字符串数组,是输出配置(后文中会详解),可以指定多个,至少要有一个。level
是上文日志级别。
appenders
配置输出日志,该配置的 key
值为自定义的名称(可以给 categories
中的 appenders
使用),属性值为一个对象,配置输出类型。out
指的是通过 console
输出,这个可以作为我们的一个兜底。access
中 type
为 dataFile
,指的是输出文件,然后配置文件的命名和输出路径。
在 app.js
以及routes/index.js
中加入:
1 | // app.js |
可以看到在 log
文件夹中输出两个文件:access-log和application-log两个日志文件
日志的分级,主要作用是更好的展示日志(不同颜色)、有选择的落盘日志,比如在生产中避免一些 debug
的敏感日志被泄露。log4js-node
默认有九个分级(你可以通过 levels
进行修改)
1 | { |
默认只会输出级别相等或者级别高的日志。比如你配置了 WARN
,就不会输出 INFO
的日志。可以在下面配置的 categories
中配置不同的类型日志的日志级别。
cookie、session操作koa-session
koa可以直接操作cookie
1 | router.post('/post/result',async(ctx,next)=>{ |
安装koa-session
1 | npm i koa-session |
实例
1 | const session = require('koa-session') |
CSRF攻击koa-csrf
CSRF攻击是指用户的session被劫持,用来冒充用户的攻击。
koa-csrf插件用来防止CSRF攻击。原理是在session之中写入一个秘密的token,用户每次使用POST方法提交数据的时候,必须含有这个token,否则就会抛出错误。
1 | var koa = require('koa'); |
POST请求含有token,可以是以下几种方式之一,koa-csrf插件就能获得token。
- 表单的_csrf字段
- 查询字符串的_csrf字段
- HTTP请求头信息的x-csrf-token字段
- HTTP请求头信息的x-xsrf-token字段
数据压缩koa-compress
koa-compress模块可以实现数据压缩。
1 | app.use(require('koa-compress')()) |
koa-connect
安装
1 | npm install koa-connect |
使用
1 | import k2c from 'koa2-connect' |
源码
koa2有四个核心文件:application.js、context.js、request.js、response.js。
application.js:application.js是koa的入口文件,它向外导出了创建class实例的构造函数,它继承了events,这样就会赋予框架事件监听和事件触发的能力。application还暴露了一些常用的api,比如toJSON、listen、use等等。
Context.js:这部分就是koa的应用上下文ctx,其实就一个简单的对象暴露,里面的重点在delegate,这个就是代理,这个就是为了开发者方便而设计的,比如我们要访问ctx.repsponse.status但是我们通过delegate,可以直接访问ctx.status访问到它。
Request.js、Response.js : 这两部分就是对原生的res、req的一些操作了,大量使用es6的get和set的一些语法,去取headers或者设置headers、还有设置body等等
基于此,如果要实现koa框架需要四个模块:
- 封装node http server、创建Koa类构造函数
- 构造request、response、context对象
- 中间件机制和剥洋葱模型的实现
- 错误捕获和错误处理
资源
koa资源库:https://github.com/huaize2020/awesome-koa
https://github.com/airuikun/blog/issues/2
eggjs
web应用离不开session、视图模版、路由、文件上传、日志管理,这些koa都不提供,需要自行去官方的中间件网站去找,100个人可能有100种搭配
而eggjs是基于koajs,解决了上述问题,将社区最佳实践整合进koajs,并且将多进程启动、开发时的热更新等问题一并解决,对开发者很友好,开箱即是最佳/较佳配置
目录结构
app/router.js
:用于配置URL路由规则
app/controller/**
:用于解析用户的输入,处理返回相应的结果
app/service/**
:用于编写业务逻辑层,可选
app/middleware/**
:用于编写中间件,
app/public/**
:用于放置静态资源
app/extend/**
:用于框架的扩展
config/config.{env}.js
:用于编写配置文件
config/plugin.js
:用于配置需要加载的文件
test/**
:用于单元测试
app.js
和agent.js
:用于自定义的初始化工作
内置对象
eggjs继承了koa的application、context、request、response对象,并且扩展了一些新的全局对象,controller、service、logger、config、helper
每个controller下面都有以下属性:
ctx:当前请求的context实例
app:应用的application实例
config:应用的配置
service:应用所有的service
logger:为当前controller封装的logger对象
推荐从egg对象上获取controller基类,也可以从app实例上获取
1 | //从egg上获取 |
Service基类与controller基类基本相同,获取方式也相同
路由Router
Router的请求用来描述URL与具体承担执行动作的controller的关系,框架约定了app/router.js
文件用于统一所有路由规则
路由定义时需指定:
1.请求方法/请求动作,包括head、options、get、post、delete、put、patch、redirect等
2.路由名称,给路由设定一个别名
3.中间件,在router里可以配置多个中间件,串联执行
4.控制器,指定路由映射到具体到控制器上
特别地,Restful风格的CRUD的路由配置如下
1 | module.exports = app =>{ |
控制器controller
控制器与路由对应,实现控制器的服务
1 | //router.js |
服务(service)
service是复杂场景下用于做业务逻辑封装的一个抽象层,有利于:
1.保持controller的逻辑更加简洁
2.保持业务逻辑的独立性,抽象出来的service可以被多个controller重复调用
3.将逻辑与展现分离,更容易编写测试用例
使用场景:
复杂数据的处理,如需要查数据库、按一定规则计算或者计算完成之后更新到数据库
调用第三方的服务时
定义service
1 | //app/service/user.js |
在controller中调用对应的service
1 | const Controller = require('egg').Controller; |
中间件
我们约定中间件是一个放置在app/middleware
目录下的单独文件,它接受两个参数,
Options:中间件的配置,框架会将config传递进来
app:当前应用application的实例
中间件实例
1 | //app/middleware/gzip.js |
在单个router中或者全局实例化和挂载
1 | //单个路由加载 |
全局加载
1 | //config.default.js |
插件
koa的中间件系统有其固有的缺点:
1.中间件的顺序不可固定,使用先后顺序的不同,结果可能有天壤之别
2.有些功能是与请求无关的,如定时任务、消息订阅,中间件处理起来麻烦
3.初始化逻辑复杂,需要在应用启动的时候完成
一个插件就像一个mini的应用,有service,中间件,配置等,没有路由和controller,没有plugin
插件一般提供npm的方式安装
1 | npm i egg-mysql --save |
在package.json中引入依赖
1 | { |
在plugin.js中声明
1 | exports.mysql = { |
上传文件
config
1 | config.multipart = { |
1 | npm install await-stream-ready stream-wormhole dayjs |
1 | const fs = require('fs'); |
定时任务
有一些任务是需要定时运行的,比如
1.定时上报任务状态
2.定时从远程接口更新本地缓存
3.定时进行文件切割、文件删除等
所有的定时任务放在app/schedule
目录下,每一个文件都是独立的定时任务,可以配置定时任务的属性和要执行的方法
比如,定义一个更新远程数据到内存缓存的定时任务
1 | //app/schedule/update_cache.js |
多进程模型与进程间通信
Node官方提供了cluster模块,用于多核计算
原生的Node-cluster特点:
在服务器上同时启动多个进程;
每个进程都跑同一份源代码,
更神奇的是,这些进程可以同时监听同一个端口,
其中,负责启动其他进程的叫做Master进程,他好比是包工头,不做具体的工作,只负责启动其他进程
其他被启动的叫Worker进程,就是干活的工人,它们接收请求,对外提供服务
Worker进程的数量一般由服务器的CPU核数决定,这样可以完美利用多核资源
egg在此基础上进行了别的考虑:
进程崩溃
work异常退出时如何处理?多个worker进程之间如何共享资源和调度?
Nodejs进程退出可以分为两类:
1是代码抛出了异常但未被捕获,进程将会退出。当一个worker进程遇到未捕获的异常时,它已经处于一个不确定状态,我们应该让这个进程优雅退出:
关闭异常worker进程的所有tcp server。断开和Master的IPC通道,不再接受新的用户请求
Master立刻fork一个进行中的worker进程,保证在线的工人总数不变
异常worker等待一段时间,处理完已经接受的请求之后退出
2是进程崩溃或者系统异常,不像未捕获异常时,当前进程直接退出,Master直接fork一个新的worker
进程守护
有些工作不需要每个worker都去做,如果都做,一来是浪费资源,更重要的是可能会导致多进程间资源访问冲突。
对于这一类后台运行逻辑,全部放到一个单独的进程去执行,这个进程就叫做Agent Worker。Agent就好比Master给其他Worker请的一个秘书,它不对外提供服务,只给App Worker打工,专门处理一些公共事务。
所以框架启动时进程的启动顺序就会变成:
1.master启动后先fork Agent进程
2.Agent初始化成功之后,通过IPC通道通知Master
3.Master再fork多个App worker
4.App Worker初始化成功,通知Master
5.所有进程初始化成功后,Master通知Agent和Worker启动成功
进程通信
虽然每个Worker进程是相对独立的,但是它们之间始终还是需要通讯的,称为IPC通讯。
Node cluster提供的IPC通道只存在于Master和Worker/Agent之间,Worker之间、Worker与Agent之间是没有的,要想相互通信只能通过master转发,这是不太方便的
Egg封装了messenger对象挂载在app/agent上,能够相互通信
方法
1 | app.messenger.broadcast(action,data) |