概述nodeJS企业级框架egg
Author:zhoulujun Date:
https://eggjs.org/ 官方文档,应付日常开发足够了。本篇还是来分析下egg
回顾express和koa
使用express.js启动一个简单的服务器
const express = require('express') const app = express() const router = express.Router() app.use(async (req, res, next) => { console.log('I am the first middleware') next() console.log('first middleware end calling') }) app.use((req, res, next) => { if (err) { console.log('second middleware catch error', err) res.status(500).send('server Error') return } console.log('I am the second middleware') next() console.log('second middleware end calling') }) router.get('/api/test1', async(req, res, next) => { console.log('I am the router middleware => /api/test1') res.status(200).send('hello') }) app.use('/', router) app.listen(3000)
换算成等价的koa2
const koa = require('koa') const Router = require('koa-router') const app = new koa() const router = Router() app.use(async(ctx, next) => { console.log('I am the first middleware') await next() console.log('first middleware end calling') }) app.use(async (ctx, next) => { console.log('I am the second middleware') await next() console.log('second middleware end calling') }) router.get('/api/test1', async(ctx, next) => { console.log('I am the router middleware => /api/test1') ctx.body = 'hello' }) app.use(router.routes()) app.listen(3000)
二者的使用区别通过表格展示如
日常一些简单的开发需求,两个框架都能应付,但是中间,让我头痛
koa简化了,但是还是没有egg理想
//- router.js module.exports = app => { const { router, controller } = app; router.get('/', controller.home.index); }; //- app/controller/home.js const { Controller } = require('egg'); class HomeController extends Controller { async index() { this.ctx.body = 'Hello world'; } } module.exports = HomeController;
整体上,简洁干练
egg 简介
egg 是阿里开源的一个强约束的Node框架,为企业级框架和应用而生,相较于 express 和 koa ,有更加严格的目录结构和规范,使得团队可以在基于 egg 定制化自己的需求或者根据 egg 封装出适合自己团队业务的更上层框架
可以看到 egg 处于的是一个中间层的角色,基于 koa ,不同于 koa 以 middleware 为主要生态,egg 根据不同的业务需求和场景,加入了 plugin , extends 等这些功能,可以让开发者摆脱在使用 middleware 功能时无法控制使用顺序的被动状态,而且还可以增加一些请求无关的一些功能。
egg 中内置插件系列:
onerror 统一异常处理
Session Session 实现
i18n 多语言
watcher 文件和文件夹监控
multipart 文件流式上传
security 安全
development 开发环境配置
logrotator 日志切分
schedule 定时任务
static 静态服务器
jsonp jsonp 支持
view 模板引擎
加载插件的逻辑是在 egg-core 里面的 plugin.js 文件
扩展内置对象
在对内置对象进行扩展的时候,实质上执行的是 extend.js 文件,扩展的对象包括如下几个:
Application
Context
Request
Response
Helper
通过阅读 extend.js 文件可以知道,其实最后每个对象的扩展都是直接调用的 loadExtends 这个函数。
egg功能加载
加载中间件:对中间件的加载主要是执行的 egg-core 中的 middleware.js 文件,里面的代码思想也是和上面加载内置对象是一样的,也是将插件中的中间件和应用中的中间件路径全部获取到,然后进行遍历。
遍历完成之后执行中间件就和 koa 一样了,调用 co 进行包裹遍历。
加载控制器 :对控制器的加载主要是执行的 egg-core 中的 controller.js 文件
egg 的官方文档中,插件的开发这一节提到:
插件没有独立的 router 和 controller
所以在加载 controller 的时候,主要是 load 应用里面的 controller 即可。
加载 service:加载 service 的逻辑是 egg-core 中的 service.js
加载路由 :加载路由的逻辑主要是 egg-core 中的 router.js 文件
加载配置:直接加载配置文件并提供可配置的方法。
框架结构概述
app:核心目录,app目录下又按照设计模式分为了数个更细粒度的子目录。
controller:存放controller层的处理文件的位置
extend:存放继承一些自定义公共方法的位置,这个在本节的下面详细说下
middleware:存放自定义中间件文件,所谓的appMiddleware
public:存放项目静态资源的位置
service:Egg框架抽象出来的一个概念,可以认为是带有逻辑处理的model层
view:存放页面模板文件的位置
router.js:编写路由的位置
config:核心目录,配置文件相关,其中config.default.js中存放的是和当前Node环境无关的配置;config.[env].js文件则存放和Node执行环境相关的配置;plugin.js存放的则是各个插件的package名称和是否开启的配置。这里的Node执行环境,后面会说明。
logs:日志文件输出的目录。
index.js:项目的入口文件。
egg开发
用比较简单的伪代码表示如下
const app = express(); // nodejs启动时,app函数内部被express增加了能力,如中间件的调用 app.use(middleware); // 中间件 app.use(router); // 路由 app.engine('ejs'); // 模板引擎 app.statifc('public') // 静态文件服务 // ... 还有代理以及其他许多属性与方法 const server = http.createServer( function app(req, res){ // 此app函数即为express所构造 // http请求时,req, res被混入许多属性与方法,做了很多处理 // 串行匹配运行按顺序注册的各注册的中间件如: // 1、日志、cookie、bodyparser等开发者自己注册的中间件 // 2、router中间件 // 3、静态文件服务 // 4、模板引擎处理 // 经过匹配的中间件处理后输出返回 } ); server.listen(8000);
上面的1、2、3、4顺序即为开发者注册时的顺序(故我们平时在开发时express注册中间件时是有先后顺序的)。express最主管理与运行中间件的能力,接下来深入内部看看connect这个中间件机制是怎么实现的。
最为核心的中间件框架
//connect.js 的简要内容 function createServer(){ // app是用于http.createServer的回调函数 function app(req, res, next){ // 运行时调用handle函数 app.handle(req, res, next); } mixin(app, proto, false); // 初始化一个stack数组 app.stack = []; return app; } // use调用时往app的stack数组中push一个对象(中间件),标识path与回调函数 proto.use = function(route, fn){ var path = route, handle = fn; //... 省略其他 this.stack.push({ route: path, handle }); }; // handle方法,串行取出stack数组中的中间件,逐个运行 proto.handle = function(req, res, out){ var index = 0; var stack = this.stack; var done = out || finalhandler(req, res, { onerror: logerror }); // 遍历stack,逐个取出中间件运行 function next(err){ var layer = stack[index++]; // 遍历完成为止 if(layer === undefined){ return done(); } var route = pathFormat(layer.route); var pathname = pathFormat(urlParser(req.url).pathname || '/'); // 匹配中间件,不匹配的不运行 if(route !== '' && pathname !== route){ next(err); return; } // 调用中间件 call(layer.handle, err, req, res, next); } next(); };
不难看出,app.use中间件时,只是把它放入一个数组中。当http请求时,app会从数组中逐个取出,进行匹配过滤,逐个运行。遍历完成后,运行finalhandler,结束一个http请求。可以从http请求的角度思考,一次请求它经历经历了多少东西。express的这个中间件架构就是负责管理与调用这些注册的中间件。中间件顺序执行,通过next来继续下一个,一旦没有继续next,则流程结束。
异步串行流程控制
为了用串行化流程控制让几个异步任务按顺序执行,需要先把这些任务按预期的执行顺序放 到一个数组中。如图,所示,这个数组将起到队列的作用:完成一个任务后按顺序从数组中取 出下一个
数组中的每个任务都是一个函数。任务完成后应该调用一个处理器函数,告诉它错误状态和 结果。如果有错误,处理器函数会终止执行;如果没有错误,处理器就从队列中取出下一个任务 执行它
Router是一个内置在app函数上的中间件
来看下简化后的router.js
//express创建时运行 app.init = function(){ // ... 省略其它代码 this._router = new Router(); this.usedRouter = false; // app调用router时初始化router中间件 Object.defineProperty(this, 'router', { configurable : true, enumerable : true, get: function () { this.usedRouter = true; return this._router.middlewareInit.bind(this._router); } }) }; // methods是一个数组,['get','post','put','delete',...] methods.forEach(method => { app[method] = function (path) { // 如果首次调用则放入路由中间价 if(!this.usedRouter){ this.use(this.router); } // 加入stack this._router.addRoute(method, path, Array.prototype.slice.call(arguments, 1)) } });
usedRouter是个开关,未开启则不加入router中间件,因为应用理论上也是可能不用到router的。当app[method] 如app.get('/user', fn)调用后,则触发this.use(this.router) 使用router中间件,同时把usedRouter设置为true。之后往router对象中加入fn回调函数。
router实际上也是一个异步串行流程控制,简化版的代码如下
Router.prototype.addRoute = function(method, path, handles){ let layer = { path, handles }; this.map[method] = this.map[method] || []; this.map[method].push(layer); }; Router.prototype.middlewareInit = function(req, res, out){ let index = 0; let method = req.method.toLowerCase() || 'get'; let stack = this.map[method]; function next(err) { let layer = stack[index++]; let hasError = Boolean(err); // 如果没有了则结束中间件,走下一个中间件 if(!layer){ return hasError ? out(err) : out(); } let route = utils.pathFormat(layer.path); let pathname = utils.pathFormat(urlParser(req.url).pathname || '/'); // 进行过滤 if(route!== '' && route !== pathname){ return next(err); } executeHandles(layer.handles, err, req, res, next); } next(); };
router跟connect非常类似,上述理解了connect,router就很清晰了。一图以蔽之:
实际上router还有细分,某个router还是可以继续做类似的串行流程控制;与中间件相同,每个router一旦停止了next,流程就结束了。
request经过router可以请求一个数据,或者一个网页;网页的话是怎么返回的呢,接下来看下view的render;
参考内容:
NodeJS express框架核心原理全揭秘 https://zhuanlan.zhihu.com/p/56947560
结合源码解密 egg 运行原理 https://zhuanlan.zhihu.com/p/29102746
深夜放毒——阿里开源的企业级Node框架Egg使用指南 https://cnodejs.org/topic/580a6a7e541dfb7b50f40a60
再也不怕面试官问你express和koa的区别了 https://zhuanlan.zhihu.com/p/87079561
转载本站文章《概述nodeJS企业级框架egg》,
请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/JS-Server/8527.html