• home > webfront > ECMAS > nodejs >

    概述nodeJS企业级框架egg

    Author:zhoulujun Date:

    https: eggjs org 官方文档,应付日常开发足够了。本篇还是来分析下egg回顾express和koa使用express js启动一个简单的服务器constexpress=

    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与express二者的使用区别通过表格展示如

    日常一些简单的开发需求,两个框架都能应付,但是中间,让我头痛

    express中间件的执行逻辑

    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 封装出适合自己团队业务的更上层框架

    v2-8966bd88d6c3773ccd133969f1f39be3_720w.png

    可以看到 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,则流程结束

    异步串行流程控制

    为了用串行化流程控制让几个异步任务按顺序执行,需要先把这些任务按预期的执行顺序放 到一个数组中。如图,所示,这个数组将起到队列的作用:完成一个任务后按顺序从数组中取 出下一个

    v2-5b5b41971e5c894405df35e55d2fe39f_720w.jpg

    数组中的每个任务都是一个函数。任务完成后应该调用一个处理器函数,告诉它错误状态和 结果。如果有错误,处理器函数会终止执行;如果没有错误,处理器就从队列中取出下一个任务 执行它

    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就很清晰了。一图以蔽之:

    egg工作流程——中间件


    实际上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