一文看懂 Eggjs-基础全面讲解(上)

8,148 阅读8分钟

Egg 继承于 Koa,Egg 选择了 Koa 作为其基础框架,在它的模型基础上,进一步对它进行了一些增强。

执行流程分析

Koa 的中间件选择了洋葱圈模型。

中间件洋葱图

所有的请求经过一个中间件的时候都会执行两次,对比 Express 形式的中间件,Koa 的模型可以非常方便的实现后置处理逻辑,可以看到执行是从前到后再从后到前。

中间件执行顺序图

中间件执行顺序图

目录结构分析

egg-project
├── package.json
├── app.js (可选)
├── agent.js (可选)
├── app
|   ├── router.js
│   ├── controller
│   |   └── home.js
│   ├── service (可选)
│   |   └── user.js
│   ├── middleware (可选)
│   |   └── response_time.js
│   ├── schedule (可选)
│   |   └── my_task.js
│   ├── public (可选)
│   |   └── reset.css
│   ├── view (可选)
│   |   └── home.tpl
│   └── extend (可选)
│       ├── helper.js (可选)
│       ├── request.js (可选)
│       ├── response.js (可选)
│       ├── context.js (可选)
│       ├── application.js (可选)
│       └── agent.js (可选)
├── config
|   ├── plugin.js
|   ├── config.default.js
│   ├── config.prod.js
|   ├── config.test.js (可选)
|   ├── config.local.js (可选)
|   └── config.unittest.js (可选)
└── test
    ├── middleware
    |   └── response_time.test.js
    └── controller
        └── home.test.js

由框架约定的目录:

  • app/router.js 用于配置 URL 路由规则
  • app/controller/** 用于解析用户的输入,处理后返回相应的结果
  • app/service/** 用于编写业务逻辑层,可选
  • app/middleware/** 用于编写中间件,可选
  • app/public/** 用于放置静态资源,可选
  • app/extend/** 用于框架的扩展,可选
  • config/config.{env}.js 用于编写配置文件
  • config/plugin.js 用于配置需要加载的插件
  • test/** 用于单元测试
  • app.jsagent.js 用于自定义启动时的初始化工作,可选

由内置插件约定的目录:

  • app/public/** 用于放置静态资源,可选
  • app/schedule/** 用于定时任务

文件加载顺序

Egg 将应用、框架和插件都称为加载单元(loadUnit)

文件 应用 框架 插件
package.json ✔︎ ︎ ✔
config/plugin.{env}.js ✔︎ ✔︎
config/config.{env}.js ✔︎ ✔︎ ✔︎
app/extend/application.js ✔︎ ︎ ✔︎
app/extend/request.js ✔︎ ✔︎ ✔︎
app/extend/response.js ✔︎ ✔︎ ✔︎
app/extend/context.js ✔︎ ✔︎ ✔︎
app/extend/helper.js ✔︎ ✔︎ ✔︎
agent.js ✔︎ ✔︎ ✔︎
app.js ✔︎ ✔︎ ✔︎
app/service ✔︎ ✔︎ ✔︎
app/middleware ✔︎ ✔︎ ✔︎
app/controller ✔︎
app/router.js ✔︎

文件按表格内的顺序自上而下加载,Egg 会遍历所有的 loadUnit 加载上述的文件(应用、框架、插件各有不同),加载时有一定的优先级。

  • 按插件 => 框架 => 应用依次加载
  • 插件之间的顺序由依赖关系决定,被依赖方先加载,无依赖按 object key 配置顺序加载
  • 框架按继承顺序加载,越底层越先加载

生命周期

  • 配置文件即将加载,这是最后动态修改配置的时机(configWillLoad
  • 配置文件加载完成(configDidLoad
  • 文件加载完成(didLoad
  • 插件启动完毕(willReady
  • worker 准备就绪(didRead
  • 应用启动完成(serverDidReady
  • 应用即将关闭(beforeClose

定义如下:

// app.js or agent.js
class AppBootHook {
  constructor(app) {
    this.app = app;
  }
  configWillLoad() {
    // Ready to call configDidLoad,
    // Config, plugin files are referred,
    // this is the last chance to modify the config.
    console.log('configWillLoad');
  }
  configDidLoad() {
    // Config, plugin files have been loaded.
    console.log('configDidLoad');
  }
  async didLoad() {
    // All files have loaded, start plugin here.
    console.log('didLoad');
  }
  async willReady() {
    // All plugins have started, can do some thing before app ready
    console.log('willReady');
  }
  async didReady() {
    // Worker is ready, can do some things
    // don't need to block the app boot.
    console.log('didReady');
  }
  async serverDidReady() {
    // Server is listening.
    console.log('serverDidReady');
  }
  async beforeClose() {
    // Do some thing before app close.
    console.log('configWillLoad');
  }
}
module.exports = AppBootHook;

启动时候执行的顺序

框架内置基础对象*

doc

框架中内置的一些基础对象,包括从 Koa 继承而来的 4 个对象

  • Application
  • Context
  • Request
  • Response

以及框架扩展的一些对象

  • Controller
  • Service
  • Helper
  • Config
  • Logger

Application

Application 是全局应用对象,在一个应用中,只会实例化一个,它继承自 Koa.Application,在它上面我们可以挂载一些全局的方法和对象。

事件

  • server 该事件一个 worker 进程只会触发一次,在 HTTP 服务完成启动后,会将 HTTP server 通过这个事件暴露出来给开发者
  • error 运行时有任何的异常被 onerror 插件捕获后,都会触发 error 事件
  • requestresponse 应用收到请求和响应请求时,分别会触发 request 和 response 事件
// app.js
module.exports = app => {
  app.once('server', server => {
    // websocket
    console.log('server', server);
    
  });
  app.on('error', (err, ctx) => {
    // report error
    console.log('error', err);
  });
  app.on('request', ctx => {
    // log receive request
    console.log('request');
  });
  app.on('response', ctx => {
    // ctx.starttime is set by framework
    const used = Date.now() - ctx.starttime;
    // log total cost
    console.log('used', used);
  });
};

// 初始化的时候会打印 server Server 
// request
// used 6
// request
// used 1

获取方式

几乎所有被框架 Loader 加载的文件(Controller,Service,Schedule 等),都可以 export 一个函数,这个函数会被 Loader 调用,并使用 app 作为参数

  • 启动自定义脚本
// app.js
module.exports = app => {
  app.cache = new Cache();
};
  • Controller 文件
// app/controller/user.js
class UserController extends Controller {
  async fetch() {
    this.ctx.body = this.app.cache.get(this.ctx.query.id);
  }
}

和 Koa 一样,在 Context 对象上,可以通过 ctx.app 访问到 Application 对象。也就是说

结果为 true

// app/controller/user.js
class UserController extends Controller {
  async fetch() {
    this.ctx.body = this.ctx.app.cache.get(this.ctx.query.id);
  }
}

Context

Context 是一个请求级别的对象,继承自 Koa.Context

在每一次收到用户请求时,框架会实例化一个 Context 对象,这个对象封装了这次用户请求的信息,并提供了许多便捷的方法来获取请求参数或者设置响应信息

获取方式


// Koa v1
function* middleware(next) {
  // this is instance of Context
  console.log(this.query);
  yield next;
}

// Koa v2
async function middleware(ctx, next) {
  // ctx is instance of Context
  console.log(ctx.query);
}

除了在请求时可以获取 Context 实例之外, 在有些非用户请求的场景下我们需要访问 service / model 等 Context 实例上的对象,可以用Application.createAnonymousContext() 方法创建一个匿名 Context 实例。

// app.js
module.exports = app => {
  app.beforeStart(async () => {
    const ctx = app.createAnonymousContext();
    // preload before app start
    await ctx.service.posts.load();
  });
}

在定时任务中的每一个 task 都接受一个 Context 实例作为参数

// app/schedule/refresh.js
exports.task = async ctx => {
  await ctx.service.posts.refresh();
};

Request & Response

  • Request 是一个请求级别的对象,继承自 Koa.Request。封装了 Node.js 原生的 HTTP Request 对象,提供了一系列辅助方法获取 HTTP 请求常用参数
  • Response 是一个请求级别的对象,继承自 Koa.Response。封装了 Node.js 原生的 HTTP Response 对象,提供了一系列辅助方法设置 HTTP 响应

获取方式

可以在 Context 的实例上获取到当前请求的 Request(ctx.request) 和 Response(ctx.response) 实例。

// app/controller/user.js
class UserController extends Controller {
  async fetch() {
    const { app, ctx } = this;
    const id = ctx.request.query.id;
    ctx.response.body = app.cache.get(id);
  }
}
  • Koa 会在 Context 上代理一部分 Request 和 Response 上的方法和属性,参见 Koa.Context
  • 如上面例子中的 ctx.request.query.idctx.query.id 是等价的,ctx.response.body=ctx.body= 是等价的
  • 需要注意的是,获取 POST 的 body 应该使用 ctx.request.body,而不是 ctx.body

Controller

框架提供了一个 Controller 基类,并推荐所有的 Controller 都继承于该基类实现。

这个 Controller 基类有下列属性:

  • ctx - 当前请求的 Context 实例
  • app - 应用的 Application 实例
  • config - 应用的配置
  • service - 应用所有的 service
  • logger - 为当前 controller 封装的 logger 对象

在 Controller 文件中,可以通过两种方式来引用 Controller 基类:

// app/controller/user.js

// 从 egg 上获取(推荐)
const Controller = require('egg').Controller;
class UserController extends Controller {
  // implement
}
module.exports = UserController;

// 从 app 实例上获取
module.exports = app => {
  return class UserController extends app.Controller {
    // implement
  };
};

Service

框架提供了一个 Service 基类,并推荐所有的 Service 都继承于该基类实现。

Service 基类的属性和 Controller 基类属性一致,访问方式也类似:

// app/service/user.js

// 从 egg 上获取(推荐)
const Service = require('egg').Service;
class UserService extends Service {
  // implement
}
module.exports = UserService;

// 从 app 实例上获取
module.exports = app => {
  return class UserService extends app.Service {
    // implement
  };
};

Helper

Helper 用来提供一些实用的 utility 函数。它的作用在于我们可以将一些常用的动作抽离在 helper.js 里面成为一个独立的函数,这样可以用 JavaScript 来写复杂的逻辑,避免逻辑分散各处,同时可以更好的编写测试用例。

Helper 自身是一个类,有和 Controller 基类一样的属性,它也会在每次请求时进行实例化,因此 Helper 上的所有函数也能获取到当前请求相关的上下文信息。

获取方式

可以在 Context 的实例上获取到当前请求的 Helper(ctx.helper) 实例。

// app/controller/user.js
class UserController extends Controller {
  async fetch() {
    const { app, ctx } = this;
    const id = ctx.query.id;
    const user = app.cache.get(id);
    ctx.body = ctx.helper.formatUser(user);
  }
}

自定义 helper 方法

// app/extend/helper.js
module.exports = {
  formatUser(user) {
    return only(user, [ 'name', 'phone' ]);
  }
};

Config

推荐应用开发遵循配置和代码分离的原则,将一些需要硬编码的业务配置都放到配置文件中,同时配置文件支持各个不同的运行环境使用不同的配置,使用起来也非常方便,所有框架、插件和应用级别的配置都可以通过 Config 对象获取到。

获取方式

通过 app.config 从 Application 实例上获取到 config 对象,也可以在 Controller, Service, Helper 的实例上通过 this.config 获取到 config 对象。

Logger

每一个 logger 对象都提供了 4 个级别的方法:

  • logger.debug()
  • logger.info()
  • logger.warn()
  • logger.error()

获取方式

  • app.logger 如果我们想做一些应用级别的日志记录,如记录启动阶段的一些数据信息,记录一些业务上与请求无关的信息,都可以通过 App Logger 来完成
  • app.coreLogger 在开发应用时都不应该通过 CoreLogger 打印日志,而框架和插件则需要通过它来打印应用级别的日志,这样可以更清晰的区分应用和框架打印的日志,通过 CoreLogger 打印的日志会放到和 Logger 不同的文件中
  • ctx.logger 从 Context 实例上获取到它,从访问方式上我们可以看出来,Context Logger 一定是与请求相关的,它打印的日志都会在前面带上一些当前请求相关的信息(如[$userId/$ip/$traceId/${cost}ms $method $url])
  • this.logger 可以在 Controller 和 Service 实例上通过 this.logger 获取到它们,它们本质上就是一个 Context Logger,不过在打印日志的时候还会额外的加上文件路径