Eggjs入门系列-基础全面讲解(下)

论坛 期权论坛 期权     
云影sky   2019-7-28 01:25   3905   0
回顾一下上篇讲到的内容,上篇讲了:
  • 运行环境
  • Config 配置
  • 中间件(Middleware)
  • 路由
  • 控制器(Controller)
[h1]服务(Service) [/h1]Service 就是在复杂业务场景下用于做业务逻辑封装的一个抽象层
[h2]使用场景[/h2]
  • 复杂数据的处理,比如要展现的信息需要从数据库获取,还要经过一定的规则计算,才能返回用户显示。或者计算完成后,更新到数据库。
  • 第三方服务的调用,比如 GitHub 信息获取等。
[h2]定义 Service[/h2]
  1. // app/service/user.jsconst Service = require('egg').Service;class UserService extends Service {  async find(uid) {    const user = await this.ctx.db.query('select * from user where uid = ?', uid);    return user;  }}module.exports = UserService;
复制代码
[h3]属性[/h3]每一次用户请求,框架都会实例化对应的 Service 实例,由于它继承于
  1. egg.Service
复制代码
,故拥有下列属性方便我们进行开发:
    1. this.ctx
    复制代码
    : 当前请求的上下文 Context 对象的实例
    1. this.app
    复制代码
    : 当前应用 Application 对象的实例
    1. this.service
    复制代码
    :应用定义的 Service
    1. this.config
    复制代码
    :应用运行时的配置项
    1. this.logger
    复制代码
    :logger 对象,上面有四个方法(
    1. debug
    复制代码
    1. info
    复制代码
    1. warn
    复制代码
    1. error
    复制代码
    ),分别代表打印四个不同级别的日志,使用方法和效果与 context logger 中介绍的一样,但是通过这个 logger 对象记录的日志,在日志前面会加上打印该日志的文件路径,以便快速定位日志打印位置。
[h3]Service ctx 详解[/h3]
    1. this.ctx.curl
    复制代码
    发起网络调用。
    1. this.ctx.service.otherService
    复制代码
    调用其他 Service。
    1. this.ctx.db
    复制代码
    发起数据库调用等, db 可能是其他插件提前挂载到 app 上的模块。
[h3]注意事项[/h3]
  • Service 文件必须放在
    1. app/service
    复制代码
    目录,可以支持多级目录,访问的时候可以通过目录名级联访问。

  1. app/service/biz/user.js => ctx.service.biz.user // 多级目录,依据目录名级联访问app/service/sync_user.js => ctx.service.syncUser // 下划线自动转换为自动驼峰app/service/HackerNews.js => ctx.service.hackerNews // 大写自动转换为驼峰
复制代码
  • 一个 Service 文件只能包含一个类, 这个类需要通过 module.exports 的方式返回。
  • Service 需要通过 Class 的方式定义,父类必须是 egg.Service。
  • Service 不是单例,是请求级别 的对象,框架在每次请求中首次访问
    1. ctx.service.xx
    复制代码
    时延迟实例化,所以 Service 中可以通过 this.ctx 获取到当前请求的上下文。
[h2]使用 Service[/h2]
  1. // app/controller/user.jsconst Controller = require('egg').Controller;class UserController extends Controller {  async info() {    const { ctx } = this;    const userId = ctx.params.id;    const userInfo = await ctx.service.user.find(userId);    ctx.body = userInfo;  }}module.exports = UserController;// app/service/user.jsconst Service = require('egg').Service;class UserService extends Service {  // 默认不需要提供构造函数。  // constructor(ctx) {  //   super(ctx); 如果需要在构造函数做一些处理,一定要有这句话,才能保证后面 `this.ctx`的使用。  //   // 就可以直接通过 this.ctx 获取 ctx 了  //   // 还可以直接通过 this.app 获取 app 了  // }  async find(uid) {    // 假如 我们拿到用户 id 从数据库获取用户详细信息    const user = await this.ctx.db.query('select * from user where uid = ?', uid);    // 假定这里还有一些复杂的计算,然后返回需要的信息。    const picture = await this.getPicture(uid);    return {      name: user.user_name,      age: user.age,      picture,    };  }  async getPicture(uid) {    const result = await this.ctx.curl(`http://photoserver/uid=${uid}`, { dataType: 'json' });    return result.data;  }}module.exports = UserService;
复制代码
[h1]插件 [/h1][h2]为什么要插件[/h2]在使用 Koa 中间件过程中发现了下面一些问题:
  • 中间件加载其实是有先后顺序的,但是中间件自身却无法管理这种顺序,只能交给使用者。这样其实非常不友好,一旦顺序不对,结果可能有天壤之别。
  • 中间件的定位是拦截用户请求,并在它前后做一些事情,例如:鉴权、安全检查、访问日志等等。但实际情况是,有些功能是和请求无关的,例如:定时任务、消息订阅、后台逻辑等等。
  • 有些功能包含非常复杂的初始化逻辑,需要在应用启动的时候完成。这显然也不适合放到中间件中去实现。
[h2]中间件、插件、应用的关系[/h2]一个插件其实就是一个『迷你的应用』,和应用(app)几乎一样:+
  • 它包含了 Service、中间件、配置、框架扩展等等。
  • 它没有独立的 Router 和 Controller。
  • 它没有
    1. plugin.js
    复制代码
    ,只能声明跟其他插件的依赖,而不能决定其他插件的开启与否。
他们的关系是:
  • 应用可以直接引入 Koa 的中间件。
  • 插件本身可以包含中间件。
  • 多个插件可以包装为一个上层框架。
[h2]使用插件[/h2]插件一般通过 npm 模块的方式进行复用:
  1. npm i egg-mysql --save
复制代码
建议通过 ^ 的方式引入依赖,并且强烈不建议锁定版本。
  1. {  "dependencies": {    "egg-mysql": "^3.0.0"  }}
复制代码
然后需要在应用或框架的
  1. config/plugin.js
复制代码
中声明:
  1. // config/plugin.js// 使用 mysql 插件exports.mysql = {  enable: true,  package: 'egg-mysql',};
复制代码
就可以直接使用插件提供的功能:
  1. app.mysql.query(sql, values);
复制代码
egg-mysql 插件文档
[h3]参数介绍[/h3]
  1. plugin.js
复制代码
中的每个配置项支持:
    1. {Boolean} enable
    复制代码
    - 是否开启此插件,默认为 true
    1. {String} package
    复制代码
    - npm 模块名称,通过 npm 模块形式引入插件
    1. {String} path
    复制代码
    - 插件绝对路径,跟 package 配置互斥
    1. {Array} env
    复制代码
    - 只有在指定运行环境才能开启,会覆盖插件自身 package.json 中的配置
[h3]开启和关闭[/h3]在上层框架内部内置的插件,应用在使用时就不用配置 package 或者 path,只需要指定 enable 与否:
  1. // 对于内置插件,可以用下面的简洁方式开启或关闭exports.onerror = false;
复制代码
[h3]根据环境配置[/h3]同时,我们还支持
  1. plugin.{env}.js
复制代码
这种模式,会根据运行环境加载插件配置。
比如定义了一个开发环境使用的插件
  1. egg-dev
复制代码
,只希望在本地环境加载,可以安装到
  1. devDependencies
复制代码
  1. // npm i egg-dev --save-dev// package.json{  "devDependencies": {    "egg-dev": "*"  }}
复制代码
然后在
  1. plugin.local.js
复制代码
中声明:
  1. // config/plugin.local.jsexports.dev = {  enable: true,  package: 'egg-dev',};
复制代码
这样在生产环境可以
  1. npm i --production
复制代码
不需要下载
  1. egg-dev
复制代码
的包了。
注意:
  • 不存在
    1. plugin.default.js
    复制代码
  • 只能在应用层使用,在框架层请勿使用。
[h3]package 和 path[/h3]
    1. package
    复制代码
    1. npm
    复制代码
    方式引入,也是最常见的引入方式
    1. path
    复制代码
    是绝对路径引入,如应用内部抽了一个插件,但还没达到开源发布独立 npm 的阶段,或者是应用自己覆盖了框架的一些插件
  1. // config/plugin.jsconst path = require('path');exports.mysql = {  enable: true,  path: path.join(__dirname, '../lib/plugin/egg-mysql'),};
复制代码
[h2]插件配置[/h2]插件一般会包含自己的默认配置,应用开发者可以在
  1. config.default.js
复制代码
覆盖对应的配置:
  1. // config/config.default.jsexports.mysql = {  client: {    host: 'mysql.com',    port: '3306',    user: 'test_user',    password: 'test_password',    database: 'test',  },};
复制代码
[h2]插件列表[/h2]框架默认内置了企业级应用常用的插件:
    1. onerror
    复制代码
    统一异常处理
    1. Session
    复制代码
    Session 实现
  • i18n 多语言
  • watcher 文件和文件夹监控
  • multipart 文件流式上传
  • security 安全
  • development 开发环境配置
  • logrotator 日志切分
  • schedule 定时任务
  • static 静态服务器
  • jsonp jsonp 支持
  • view 模板引擎
更多社区的插件可以 GitHub 搜索
  1. egg-plugin
复制代码

插件开发详情见 插件开发
[h1]定时任务 [/h1]虽然我们通过框架开发的 HTTP Server 是请求响应模型的,但是仍然还会有许多场景需要执行一些定时任务
  • 定时上报应用状态。
  • 定时从远程接口更新本地缓存。
  • 定时进行文件切割、临时文件删除。
[h2]编写定时任务[/h2]所有的定时任务都统一存放在
  1. app/schedule
复制代码
目录下,每一个文件都是一个独立的定时任务,可以配置定时任务的属性和要执行的方法。
  1. app/schedule
复制代码
目录下创建一个
  1. update_cache.js
复制代码
文件
  1. const Subscription = require('egg').Subscription;class UpdateCache extends Subscription {  // 通过 schedule 属性来设置定时任务的执行间隔等配置  static get schedule() {    return {      interval: '1m', // 1 分钟间隔      type: 'all', // 指定所有的 worker 都需要执行    };  }  // subscribe 是真正定时任务执行时被运行的函数  async subscribe() {    const res = await this.ctx.curl('http://www.api.com/cache', {      dataType: 'json',    });    this.ctx.app.cache = res.data;  }}module.exports = UpdateCache;
复制代码
还可以简写为
  1. module.exports = {  schedule: {    interval: '1m', // 1 分钟间隔    type: 'all', // 指定所有的 worker 都需要执行  },  async task(ctx) {    const res = await ctx.curl('http://www.api.com/cache', {      dataType: 'json',    });    ctx.app.cache = res.data;  },};
复制代码
这个定时任务会在每一个 Worker 进程上每 1 分钟执行一次,将远程数据请求回来挂载到
  1. app.cache
复制代码
上。
[h3]任务[/h3]
    1. task
    复制代码
    1. subscribe
    复制代码
    同时支持
    1. generator functio
    复制代码
    1. async function
    复制代码

    1. task
    复制代码
    的入参为
    1. ctx
    复制代码
    ,匿名的 Context 实例,可以通过它调用
    1. service
    复制代码
    等。
[h3]定时方式[/h3]定时任务可以指定 interval 或者 cron 两种不同的定时方式。
interval通过
  1. schedule.interval
复制代码
参数来配置定时任务的执行时机,定时任务将会每间隔指定的时间执行一次。interval 可以配置成
  • 数字类型,单位为毫秒数,例如 5000
  • 字符类型,会通过 ms 转换成毫秒数,例如 5s。
  1. module.exports = {  schedule: {    // 每 10 秒执行一次    interval: '10s',  },};
复制代码
cron通过 schedule.cron 参数来配置定时任务的执行时机,定时任务将会按照 cron 表达式在特定的时间点执行。cron 表达式通过
  1. cron-parser
复制代码
进行解析。
注意:cron-parser 支持可选的秒(linux crontab 不支持)。
  1. *    *    *    *    *    *┬    ┬    ┬    ┬    ┬    ┬│    │    │    │    │    |│    │    │    │    │    └ day of week (0 - 7) (0 or 7 is Sun)│    │    │    │    └───── month (1 - 12)│    │    │    └────────── day of month (1 - 31)│    │    └─────────────── hour (0 - 23)│    └──────────────────── minute (0 - 59)└───────────────────────── second (0 - 59, optional)
复制代码
  1. module.exports = {  schedule: {    // 每三小时准点执行一次    cron: '0 0 */3 * * *',  },};
复制代码
[h3]类型 type[/h3]worker 和 all。worker 和 all 都支持上面的两种定时方式,只是当到执行时机时,会执行定时任务的 worker 不同:
    1. worker
    复制代码
    类型:每台机器上只有一个 worker 会执行这个定时任务,每次执行定时任务的 worker 的选择是随机的。
    1. all
    复制代码
    类型:每台机器上的每个 worker 都会执行这个定时任务。
[h3]其他参数[/h3]除了刚才介绍到的几个参数之外,定时任务还支持这些参数:
    1. cronOptions
    复制代码
    : 配置 cron 的时区等,参见 cron-parser 文档
    1. immediate
    复制代码
    :配置了该参数为 true 时,这个定时任务会在应用启动并 ready 后立刻执行一次这个定时任务。
    1. disable
    复制代码
    :配置该参数为 true 时,这个定时任务不会被启动。
    1. env
    复制代码
    :数组,仅在指定的环境下才启动该定时任务。
[h3]执行日志[/h3]执行日志会输出到
  1. ${appInfo.root}/logs/{app_name}/egg-schedule.log
复制代码
,默认不会输出到控制台,可以通过
  1. config.customLogger.scheduleLogger
复制代码
来自定义。
  1. // config/config.default.jsconfig.customLogger = {  scheduleLogger: {    // consoleLevel: 'NONE',    // file: path.join(appInfo.root, 'logs', appInfo.name, 'egg-schedule.log'),  },};
复制代码
[h3]动态配置定时任务[/h3]
  1. module.exports = app => {  return {    schedule: {      interval: app.config.cacheTick,      type: 'all',    },    async task(ctx) {      const res = await ctx.curl('http://www.api.com/cache', {        contentType: 'json',      });      ctx.app.cache = res.data;    },  };};
复制代码
[h3]手动执行定时任务[/h3]我们可以通过
  1. app.runSchedule(schedulePath)
复制代码
来运行一个定时任务。
  1. app.runSchedule
复制代码
接受一个定时任务文件路径(
  1. app/schedule
复制代码
目录下的相对路径或者完整的绝对路径),执行对应的定时任务,返回一个 Promise。
  • 通过手动执行定时任务可以更优雅的编写对定时任务的单元测试。
  1. const mm = require('egg-mock');const assert = require('assert');it('should schedule work fine', async () => {  const app = mm.app();  await app.ready();  await app.runSchedule('update_cache');  assert(app.cache);});
复制代码
  • 应用启动时,手动执行定时任务进行系统初始化,等初始化完毕后再启动应用。参见应用启动自定义章节,我们可以在
    1. app.js
    复制代码
    中编写初始化逻辑。
  1. module.exports = app => {  app.beforeStart(async () => {    // 保证应用启动监听端口前数据已经准备好了    // 后续数据的更新由定时任务自动触发    await app.runSchedule('update_cache');  });};
复制代码
[h1]框架扩展 [/h1]框架提供了多种扩展点扩展自身的功能:Application、Context、Request、Response、Helper。
[h2]Application[/h2][h3]访问方式[/h3]
    1. ctx.app
    复制代码
  • Controller,Middleware,Helper,Service 中都可以通过
    1. this.app
    复制代码
    访问到 Application 对象,例如
    1. this.app.config
    复制代码
    访问配置对象。
  • 在 app.js 中 app 对象会作为第一个参数注入到入口函数中
  1. // app.jsmodule.exports = app => {  // 使用 app 对象};
复制代码
[h3]扩展方式[/h3]框架会把
  1. app/extend/application.js
复制代码
中定义的对象与 Koa Application 的 prototype 对象进行合并,在应用启动时会基于扩展后的 prototype 生成 app 对象。
  1. // app/extend/application.jsmodule.exports = {  foo(param) {    // this 就是 app 对象,在其中可以调用 app 上的其他方法,或访问属性  },};
复制代码
属性扩展
一般来说属性的计算只需要进行一次,那么一定要实现缓存,否则在多次访问属性时会计算多次,这样会降低应用性能。
推荐的方式是使用 Symbol + Getter 的模式。
  1. // app/extend/application.jsconst BAR = Symbol('Application#bar');module.exports = {  get bar() {    // this 就是 app 对象,在其中可以调用 app 上的其他方法,或访问属性    if (!this[BAR]) {      // 实际情况肯定更复杂      this[BAR] = this.config.xx + this.config.yy;    }    return this[BAR];  },};
复制代码
[h2]Context[/h2]Context 指的是 Koa 的请求上下文,这是请求级别 的对象,每次请求生成一个 Context 实例,通常我们也简写成 ctx。在所有的文档中,Context 和 ctx 都是指 Koa 的上下文对象。
[h3]访问方式[/h3]
  • middleware 中返回函数的第一个参数就是 ctx,例如
    1. ctx.cookies.get('foo')
    复制代码

  • controller 有两种写法,类的写法通过
    1. this.ctx
    复制代码
    ,方法的写法直接通过
    1. ctx
    复制代码
    入参。
  • helper,service 中的 this 指向 helper,service 对象本身,使用 this.ctx 访问 context 对象,例如
    1. this.ctx.cookies.get('foo')
    复制代码

[h3]扩展方式[/h3]框架会把
  1. app/extend/context.js
复制代码
中定义的对象与 Koa Context 的 prototype 对象进行合并,在处理请求时会基于扩展后的 prototype 生成 ctx 对象。
  1. // app/extend/context.jsmodule.exports = {  foo(param) {    // this 就是 ctx 对象,在其中可以调用 ctx 上的其他方法,或访问属性  },};
复制代码
属性扩展同 Application
[h2]Request[/h2]Request 对象和 Koa 的 Request 对象相同,是 请求级别 的对象,它提供了大量请求相关的属性和方法供使用。
[h3]访问方式[/h3]
  1. ctx.request
复制代码
  1. ctx
复制代码
上的很多属性和方法都被代理到
  1. request
复制代码
对象上,对于这些属性和方法使用
  1. ctx
复制代码
和使用
  1. request
复制代码
去访问它们是等价的,例如
  1. ctx.url === ctx.request.url
复制代码

[h3]扩展方式[/h3]框架会把
  1. app/extend/request.js
复制代码
中定义的对象与内置
  1. request
复制代码
的 prototype 对象进行合并,在处理请求时会基于扩展后的 prototype 生成
  1. request
复制代码
对象。
  1. // app/extend/request.jsmodule.exports = {  get foo() {    return this.get('x-request-foo');  },};
复制代码
[h2]Response[/h2]Response 对象和 Koa 的 Response 对象相同,是 请求级别 的对象,它提供了大量响应相关的属性和方法供使用。
[h3]访问方式[/h3]
  1. ctx.response
复制代码
  1. ctx
复制代码
上的很多属性和方法都被代理到
  1. response
复制代码
对象上,对于这些属性和方法使用
  1. ctx
复制代码
和使用
  1. response
复制代码
去访问它们是等价的,例如
  1. ctx.status = 404
复制代码
  1. ctx.response.status = 404
复制代码
是等价的。
[h3]扩展方式[/h3]框架会把
  1. app/extend/response.js
复制代码
中定义的对象与内置
  1. response
复制代码
的 prototype 对象进行合并,在处理请求时会基于扩展后的 prototype 生成 response 对象。
  1. // app/extend/response.jsmodule.exports = {  set foo(value) {    this.set('x-response-foo', value);  },};
复制代码
就可以这样使用啦:
  1. this.response.foo = 'bar';
复制代码
[h2]Helper[/h2]Helper 函数用来提供一些实用的 utility 函数。
它的作用在于我们可以将一些常用的动作抽离在 helper.js 里面成为一个独立的函数,这样可以用 JavaScript 来写复杂的逻辑,避免逻辑分散各处。另外还有一个好处是 Helper 这样一个简单的函数,可以让我们更容易编写测试用例。
框架内置了一些常用的 Helper 函数。我们也可以编写自定义的 Helper 函数。
[h3]访问方式[/h3]通过
  1. ctx.helper
复制代码
访问到 helper 对象,例如:
  1. // 假设在 app/router.js 中定义了 home routerapp.get('home', '/', 'home.index');// 使用 helper 计算指定 url pathctx.helper.pathFor('home', { by: 'recent', limit: 20 })// => /?by=recent&limit=20
复制代码
[h3]扩展方式[/h3]框架会把
  1. app/extend/helper.js
复制代码
中定义的对象与内置
  1. helper
复制代码
的 prototype 对象进行合并,在处理请求时会基于扩展后的 prototype 生成
  1. helper
复制代码
对象。
  1. // app/extend/helper.jsmodule.exports = {  foo(param) {    // this 是 helper 对象,在其中可以调用其他 helper 方法    // this.ctx => context 对象    // this.app => application 对象  },};
复制代码
[h1]启动自定义 [/h1]框架提供了统一的入口文件(
  1. app.js
复制代码
)进行启动过程自定义,这个文件返回一个 Boot 类,我们可以通过定义 Boot 类中的生命周期方法来执行启动应用过程中的初始化工作。
框架提供了这些生命周期函数供开发人员处理:
  • 配置文件即将加载,这是最后动态修改配置的时机(
    1. configWillLoad
    复制代码

  • 配置文件加载完成(
    1. configDidLoad
    复制代码

  • 文件加载完成(
    1. didLoad
    复制代码

  • 插件启动完毕(
    1. willReady
    复制代码

  • worker 准备就绪(
    1. didReady
    复制代码

  • 应用启动完成(
    1. serverDidReady
    复制代码

  • 应用即将关闭(
    1. beforeClose
    复制代码

  1. // app.jsclass AppBootHook {  constructor(app) {    this.app = app;  }  configWillLoad() {    // 此时 config 文件已经被读取并合并,但是还并未生效    // 这是应用层修改配置的最后时机    // 注意:此函数只支持同步调用    // 例如:参数中的密码是加密的,在此处进行解密    this.app.config.mysql.password = decrypt(this.app.config.mysql.password);    // 例如:插入一个中间件到框架的 coreMiddleware 之间    const statusIdx = this.app.config.coreMiddleware.indexOf('status');    this.app.config.coreMiddleware.splice(statusIdx + 1, 0, 'limit');  }  async didLoad() {    // 所有的配置已经加载完毕    // 可以用来加载应用自定义的文件,启动自定义的服务    // 例如:创建自定义应用的示例    this.app.queue = new Queue(this.app.config.queue);    await this.app.queue.init();    // 例如:加载自定义的目录    this.app.loader.loadToContext(path.join(__dirname, 'app/tasks'), 'tasks', {      fieldClass: 'tasksClasses',    });  }  async willReady() {    // 所有的插件都已启动完毕,但是应用整体还未 ready    // 可以做一些数据初始化等操作,这些操作成功才会启动应用    // 例如:从数据库加载数据到内存缓存    this.app.cacheData = await this.app.model.query(QUERY_CACHE_SQL);  }  async didReady() {    // 应用已经启动完毕    const ctx = await this.app.createAnonymousContext();    await ctx.service.Biz.request();  }  async serverDidReady() {    // http / https server 已启动,开始接受外部请求    // 此时可以从 app.server 拿到 server 的实例    this.app.server.on('timeout', socket => {      // handle socket timeout    });  }}module.exports = AppBootHook;
复制代码
[h1]应用部署 [/h1]在本地开发时,我们使用 egg-bin dev 来启动服务,但是在部署应用的时候不可以这样使用。因为 egg-bin dev 会针对本地开发做很多处理,而生产运行需要一个更加简单稳定的方式。
[h2]部署[/h2]服务器需要预装 Node.js,框架支持的 Node 版本为
  1. >= 8.0.0
复制代码

框架内置了 egg-cluster 来启动 Master 进程,Master 有足够的稳定性,不再需要使用 pm2 等进程守护模块。
同时,框架也提供了 egg-scripts 来支持线上环境的运行和停止。
  1. npm i egg-scripts --save
复制代码
  1. {  "scripts": {    "start": "egg-scripts start --daemon",    "stop": "egg-scripts stop"  }}
复制代码
这样我们就可以通过 npm start 和 npm stop 命令启动或停止应用。
[h3]启动命令[/h3]
  1. egg-scripts start --port=7001 --daemon --title=egg-server-showcase
复制代码
支持以下参数:
    1. --port=7001
    复制代码
    端口号,默认会读取环境变量
    1. process.env.PORT
    复制代码
    ,如未传递将使用框架内置端口 7001。
    1. --daemon
    复制代码
    是否允许在后台模式,无需
    1. nohup
    复制代码
    。若使用 Docker 建议直接前台运行。
    1. --env=prod
    复制代码
    框架运行环境,默认会读取环境变量
    1. process.env.EGG_SERVER_ENV
    复制代码
    , 如未传递将使用框架内置环境 prod。
    1. --workers=2
    复制代码
    框架 worker 线程数,默认会创建和 CPU 核数相当的 app worker 数,可以充分的利用 CPU 资源。
    1. --title=egg-server-showcase
    复制代码
    用于方便 ps 进程时 grep 用,默认为
    1. egg-server-${appname}
    复制代码

    1. --framework=yadan
    复制代码
    如果应用使用了自定义框架,可以配置 package.json 的 egg.framework 或指定该参数。
    1. --ignore-stderr
    复制代码
    忽略启动期的报错。
    1. --https.key
    复制代码
    指定 HTTPS 所需密钥文件的完整路径。
    1. --https.cert
    复制代码
    指定 HTTPS 所需证书文件的完整路径。
更多参数可查看 egg-scripts 和 egg-cluster 文档。
启动配置项
  1. // config/config.default.jsexports.cluster = {  listen: {    port: 7001,    hostname: '127.0.0.1',    // path: '/var/run/egg.sock',  }}
复制代码
[h3]停止命令[/h3]
  1. egg-scripts stop [--title=egg-server]
复制代码
该命令将杀死 master 进程,并通知 worker 和 agent 优雅退出。
  1. --title=egg-server
复制代码
用于杀死指定的 egg 应用,未传递则会终止所有的 Egg 应用。
[h1]日志 [/h1]框架内置了强大的企业级日志支持,由 egg-logger 模块提供。
  • 日志分级
  • 统一错误日志,所有 logger 中使用 .error() 打印的 ERROR 级别日志都会打印到统一的错误日志文件中,便于追踪
  • 启动日志和运行日志分离
  • 自定义日志
  • 多进程日志
  • 自动切割日志
  • 高性能
[h2]日志路径[/h2]
  • 所有日志文件默认都放在
    1. ${appInfo.root}/logs/${appInfo.name}
    复制代码
    路径下,例如
    1. /home/admin/logs/example-app
    复制代码

  • 在本地开发环境 (env: local) 和单元测试环境 (env: unittest),为了避免冲突以及集中管理,日志会打印在项目目录下的 logs 目录,例如
    1. /path/to/example-app/logs/example-app
    复制代码

如果想自定义日志路径:
  1. // config/config.${env}.jsexports.logger = {  dir: '/path/to/your/custom/log/dir',};
复制代码
[h2]日志分类[/h2]框架内置了几种日志,分别在不同的场景下使用:
  • appLogger
    1. ${appInfo.name}-web.log
    复制代码
    ,例如
    1. example-app-web.log
    复制代码
    ,应用相关日志,供应用开发者使用的日志。我们在绝大数情况下都在使用它。
  • coreLogger
    1. egg-web.log
    复制代码
    框架内核、插件日志。
  • errorLogger
    1. common-error.log
    复制代码
    实际一般不会直接使用它,任何 logger 的 .error() 调用输出的日志都会重定向到这里,重点通过查看此日志定位异常。
  • agentLogger
    1. egg-agent.log
    复制代码
    agent 进程日志,框架和使用到 agent 进程执行任务的插件会打印一些日志到这里。
如果想自定义以上日志文件名称,可以在 config 文件中覆盖默认值:
  1. // config/config.${env}.jsmodule.exports = appInfo => {  return {    logger: {      appLogName: `${appInfo.name}-web.log`,      coreLogName: 'egg-web.log',      agentLogName: 'egg-agent.log',      errorLogName: 'common-error.log',    },  };};
复制代码
[h2]如何打印日志[/h2][h3]Context Logger[/h3]用于记录 Web 行为相关的日志。
每行日志会自动记录上当前请求的一些基本信息, 如
  1. [$userId/$ip/$traceId/${cost}ms $method $url]
复制代码
  1. ctx.logger.debug('debug info');ctx.logger.info('some request data: %j', ctx.request.body);ctx.logger.warn('WARNNING!!!!');// 错误日志记录,直接会将错误日志完整堆栈信息记录下来,并且输出到 errorLog 中// 为了保证异常可追踪,必须保证所有抛出的异常都是 Error 类型,因为只有 Error 类型才会带上堆栈信息,定位到问题。ctx.logger.error(new Error('whoops'));
复制代码
对于框架开发者和插件开发者会使用到的
  1. Context Logger
复制代码
还有
  1. ctx.coreLogger
复制代码

[h3]App Logger[/h3]如果我们想做一些应用级别的日志记录,如记录启动阶段的一些数据信息,可以通过 App Logger 来完成。
  1. // app.jsmodule.exports = app => {  app.logger.debug('debug info');  app.logger.info('启动耗时 %d ms', Date.now() - start);  app.logger.warn('warning!');  app.logger.error(someErrorObj);};
复制代码
对于框架和插件开发者会使用到的 App Logger 还有
  1. app.coreLogger
复制代码
  1. // app.jsmodule.exports = app => {  app.coreLogger.info('启动耗时 %d ms', Date.now() - start);};
复制代码
[h3]Agent Logger[/h3]在开发框架和插件时有时会需要在 Agent 进程运行代码,这时使用
  1. agent.coreLogger
复制代码
  1. // agent.jsmodule.exports = agent => {  agent.logger.debug('debug info');  agent.logger.info('启动耗时 %d ms', Date.now() - start);  agent.logger.warn('warning!');  agent.logger.error(someErrorObj);};
复制代码
[h2]日志文件编码[/h2]默认编码为
  1. utf-8
复制代码
,可通过如下方式覆盖:
  1. // config/config.${env}.jsexports.logger = {  encoding: 'gbk',};
复制代码
[h2]日志文件格式[/h2]
  1. // config/config.${env}.jsexports.logger = {  outputJSON: true,};
复制代码
[h2]日志级别[/h2]日志分为
  1. NONE
复制代码
  1. DEBUG
复制代码
  1. INFO
复制代码
  1. WARN
复制代码
  1. ERROR
复制代码
5 个级别。
日志打印到文件中的同时,为了方便开发,也会同时打印到终端中。
[h3]文件日志级别[/h3]默认只会输出
  1. INFO
复制代码
及以上(
  1. WARN
复制代码
  1. ERROR
复制代码
)的日志到文件中。
打印所有级别日志到文件中:
  1. // config/config.${env}.jsexports.logger = {  level: 'DEBUG',};
复制代码
关闭所有打印到文件的日志:
  1. // config/config.${env}.jsexports.logger = {  level: 'NONE',};
复制代码
生产环境打印 debug 日志
为了避免一些插件的调试日志在生产环境打印导致性能问题,生产环境默认禁止打印 DEBUG 级别的日志,如果确实有需求在生产环境打印 DEBUG 日志进行调试,需要打开
  1. allowDebugAtProd
复制代码
配置项。
  1. // config/config.prod.jsexports.logger = {  level: 'DEBUG',  allowDebugAtProd: true,};
复制代码
[h3]终端日志级别[/h3]默认只会输出
  1. INFO
复制代码
及以上(
  1. WARN
复制代码
  1. ERROR
复制代码
)的日志到终端中。(注意:这些日志默认只在 local 和 unittest 环境下会打印到终端)
  1. logger.consoleLevel
复制代码
: 输出到终端日志的级别,默认为
  1. INFO
复制代码
打印所有级别日志到终端:
  1. // config/config.${env}.jsexports.logger = {  consoleLevel: 'DEBUG',};
复制代码
关闭所有打印到终端的日志:
  1. // config/config.${env}.jsexports.logger = {  consoleLevel: 'NONE',};
复制代码
基于性能的考虑,在正式环境下,默认会关闭终端日志输出。如有需要,你可以通过下面的配置开启。(不推荐)
  1. // config/config.${env}.jsexports.logger = {  disableConsoleAfterReady: false,};
复制代码
[h2]日志切割[/h2]框架对日志切割的支持由 egg-logrotator 插件提供。
[h3]按天切割[/h3]这是框架的默认日志切割方式,在每日
  1. 00:00
复制代码
按照
  1. .log.YYYY-MM-DD
复制代码
文件名进行切割。
以 appLog 为例,当前写入的日志为
  1. example-app-web.log
复制代码
,当凌晨 00:00 时,会对日志进行切割,把过去一天的日志按
  1. example-app-web.log.YYYY-MM-DD
复制代码
的形式切割为单独的文件。
[h3]按照文件大小切割[/h3]
  1. // config/config.${env}.jsconst path = require('path');module.exports = appInfo => {  return {    logrotator: {      filesRotateBySize: [        path.join(appInfo.root, 'logs', appInfo.name, 'egg-web.log'),      ],      maxFileSize: 2 * 1024 * 1024 * 1024,    },  };};
复制代码
[h3]按照小时切割[/h3]这和默认的按天切割非常类似,只是时间缩短到每小时。
  1. // config/config.${env}.jsconst path = require('path');module.exports = appInfo => {  return {    logrotator: {      filesRotateByHour: [        path.join(appInfo.root, 'logs', appInfo.name, 'common-error.log'),      ],    },  };};
复制代码
[h2]性能[/h2]通常 Web 访问是高频访问,每次打印日志都写磁盘会造成频繁磁盘 IO,为了提高性能,我们采用的文件日志写入策略是:
日志同步写入内存,异步每隔一段时间(默认 1 秒)刷盘
更多详细请参考 egg-logger 和 egg-logrotator。
[h1]HttpClient [/h1]框架基于 urllib 内置实现了一个 HttpClient,应用可以非常便捷地完成任何 HTTP 请求。
[h2]通过 app 使用 HttpClient[/h2]架在应用初始化的时候,会自动将 HttpClient 初始化到
  1. app.httpclient
复制代码
。同时增加了一个
  1. app.curl(url, options)
复制代码
方法,它等价于
  1. app.httpclient.request(url, options)
复制代码
  1. // app.jsmodule.exports = app => {  app.beforeStart(async () => {    // 示例:启动的时候去读取 https://registry.npm.taobao.org/egg/latest 的版本信息    const result = await app.curl('https://registry.npm.taobao.org/egg/latest', {      dataType: 'json',    });    app.logger.info('Egg latest version: %s', result.data.version);  });};
复制代码
[h2]通过 ctx 使用 HttpClient[/h2]框架在 Context 中同样提供了
  1. ctx.curl(url, options)
复制代码
  1. ctx.httpclient
复制代码
,保持跟 app 下的使用体验一致。这样就可以在有 Context 的地方(如在 controller 中)非常方便地使用
  1. ctx.curl()
复制代码
方法完成一次 HTTP 请求。
  1. // app/controller/npm.jsclass NpmController extends Controller {  async index() {    const ctx = this.ctx;    // 示例:请求一个 npm 模块信息    const result = await ctx.curl('https://registry.npm.taobao.org/egg/latest', {      // 自动解析 JSON response      dataType: 'json',      // 3 秒超时      timeout: 3000,    });    ctx.body = {      status: result.status,      headers: result.headers,      package: result.data,    };  }}
复制代码
[h2]基本 HTTP 请求[/h2][h3]GET[/h3]
  1. // app/controller/npm.jsclass NpmController extends Controller {  async get() {    const ctx = this.ctx;    const result = await ctx.curl('https://httpbin.org/get?foo=bar');    ctx.status = result.status;    ctx.set(result.headers);    ctx.body = result.data;  }}
复制代码
[h3]POST[/h3]
  1. const result = await ctx.curl('https://httpbin.org/post', {  // 必须指定 method  method: 'POST',  // 通过 contentType 告诉 HttpClient 以 JSON 格式发送  contentType: 'json',  data: {    hello: 'world',    now: Date.now(),  },  // 明确告诉 HttpClient 以 JSON 格式处理返回的响应 body  dataType: 'json',});
复制代码
[h3]PUT[/h3]
  1. const result = await ctx.curl('https://httpbin.org/put', {  // 必须指定 method  method: 'PUT',  // 通过 contentType 告诉 HttpClient 以 JSON 格式发送  contentType: 'json',  data: {    update: 'foo bar',  },  // 明确告诉 HttpClient 以 JSON 格式处理响应 body  dataType: 'json',});
复制代码
[h3]DELETE[/h3]
  1. const result = await ctx.curl('https://httpbin.org/delete', {  // 必须指定 method  method: 'DELETE',  // 明确告诉 HttpClient 以 JSON 格式处理响应 body  dataType: 'json',});
复制代码
[h2]options 参数详解[/h2]
  1. httpclient.request(url, options)
复制代码
HttpClient 默认全局配置,应用可以通过 config/config.default.js 覆盖此配置。
常用
    1. data: Object
    复制代码
    需要发送的请求数据,根据 method 自动选择正确的数据处理方式。
      1. contentType = json
      复制代码
      :通过
      1. JSON.stringify(data)
      复制代码
      处理,并设置为 body 发送。
    • 其他:通过
      1. querystring.stringify(data)
      复制代码
      处理,并设置为 body 发送。
      1. GET
      复制代码
      1. HEAD
      复制代码
      :通过
      1. querystring.stringify(data)
      复制代码
      处理后拼接到 url 的 query 参数上。
      1. POST
      复制代码
      1. PUT
      复制代码
      1. DELETE
      复制代码
      等:需要根据
      1. contentType
      复制代码
      做进一步判断处理。
    1. files: Mixed
    复制代码
    1. method: String
    复制代码
    设置请求方法,默认是 GET。支持 GET、POST、PUT、DELETE、PATCH 等所有 HTTP 方法。
    1. contentType: String
    复制代码
    设置请求数据格式,默认是 undefined,HttpClient 会自动根据 data 和 content 参数自动设置。data 是 object 的时候默认设置的是 form。支持 json 格式。
    1. dataType: String
    复制代码
    设置响应数据格式,默认不对响应数据做任何处理,直接返回原始的 buffer 格式数据。支持 text 和 json 两种格式。
    1. headers: Object
    复制代码
    自定义请求头。
    1. timeout: Number|Array
    复制代码
    请求超时时间,默认是
    1. [ 5000, 5000 ]
    复制代码
    ,即创建连接超时是 5 秒,接收响应超时是 5 秒。
[h2]调试辅助(对 ctx.curl 进行抓包)[/h2]如果你需要对 HttpClient 的请求进行抓包调试,可以添加以下配置到
  1. config.local.js
复制代码
  1. // config.local.jsmodule.exports = () => {  const config = {};  // add http_proxy to httpclient  if (process.env.http_proxy) {    config.httpclient = {      request: {        enableProxy: true,        rejectUnauthorized: false,        proxy: process.env.http_proxy,      },    };  }  return config;}
复制代码
然后启动你的抓包工具,如 charles 或 fiddler。
最后通过以下指令启动应用:
  1. http_proxy=http://127.0.0.1:8888 npm run dev
复制代码
windows 下可以用cmder 或者 git bash
  1. set http_proxy=http://127.0.0.1:8888 && npm run dev
复制代码
然后就可以正常操作了,所有经过 HttpClient 的请求,都可以你的抓包工具中查看到。




推荐阅读
Eggjs入门系列-基础全面讲解(中)
Eggjs入门系列-基础全面讲解(上)
React源码解析-React 导出了些什么
Nginx入门-基本操作一览
新手拿到一台云服务器之后怎么连接上服务器?怎么添加用户?怎么连接FTP上传文件?怎么安装Node?
看了那么多,TCP/IP究竟是什么(二)
看了那么多,TCP/IP究竟是什么(一)
TCP三次握手和四次分手
vuejs 中双向绑定的模拟实现



云影sky
关注回复关键词送学习资料,帮你快速掌握刚需技能


关注


您的在看是我创作的动力
分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:10
帖子:2
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP