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

回顾一下上篇讲到的内容,上篇讲了:javascript

服务(Service)

Service 就是在复杂业务场景下用于作业务逻辑封装的一个抽象层html

使用场景

  • 复杂数据的处理,好比要展示的信息须要从数据库获取,还要通过必定的规则计算,才能返回用户显示。或者计算完成后,更新到数据库。
  • 第三方服务的调用,好比 GitHub 信息获取等。

定义 Service

// app/service/user.js
const 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;
复制代码

属性

每一次用户请求,框架都会实例化对应的 Service 实例,因为它继承于 egg.Service,故拥有下列属性方便咱们进行开发:java

  • this.ctx: 当前请求的上下文 Context 对象的实例
  • this.app: 当前应用 Application 对象的实例
  • this.service:应用定义的 Service
  • this.config:应用运行时的配置项
  • this.logger:logger 对象,上面有四个方法(debuginfowarnerror),分别表明打印四个不一样级别的日志,使用方法和效果与 context logger 中介绍的同样,可是经过这个 logger 对象记录的日志,在日志前面会加上打印该日志的文件路径,以便快速定位日志打印位置。

Service ctx 详解

  • this.ctx.curl 发起网络调用。
  • this.ctx.service.otherService 调用其余 Service。
  • this.ctx.db 发起数据库调用等, db 多是其余插件提早挂载到 app 上的模块。

注意事项

  • Service 文件必须放在 app/service 目录,能够支持多级目录,访问的时候能够经过目录名级联访问。
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 不是单例,是 请求级别 的对象,框架在每次请求中首次访问 ctx.service.xx 时延迟实例化,因此 Service 中能够经过 this.ctx 获取到当前请求的上下文。

使用 Service

// app/controller/user.js
const 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.js
const 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;
复制代码

插件

为何要插件

在使用 Koa 中间件过程当中发现了下面一些问题:node

  • 中间件加载实际上是有前后顺序的,可是中间件自身却没法管理这种顺序,只能交给使用者。这样其实很是不友好,一旦顺序不对,结果可能有天壤之别。
  • 中间件的定位是拦截用户请求,并在它先后作一些事情,例如:鉴权、安全检查、访问日志等等。但实际状况是,有些功能是和请求无关的,例如:定时任务、消息订阅、后台逻辑等等。
  • 有些功能包含很是复杂的初始化逻辑,须要在应用启动的时候完成。这显然也不适合放到中间件中去实现。

中间件、插件、应用的关系

一个插件其实就是一个『迷你的应用』,和应用(app)几乎同样:+mysql

  • 它包含了 Service、中间件、配置、框架扩展等等。
  • 它没有独立的 Router 和 Controller。
  • 它没有 plugin.js,只能声明跟其余插件的依赖,而不能决定其余插件的开启与否。

他们的关系是:linux

  • 应用能够直接引入 Koa 的中间件。
  • 插件自己能够包含中间件。
  • 多个插件能够包装为一个上层框架。

使用插件

插件通常经过 npm 模块的方式进行复用:git

npm i egg-mysql --save
复制代码

建议经过 ^ 的方式引入依赖,而且强烈不建议锁定版本。github

{
  "dependencies": {
    "egg-mysql": "^3.0.0"
  }
}
复制代码

而后须要在应用或框架的 config/plugin.js 中声明:web

// config/plugin.js
// 使用 mysql 插件
exports.mysql = {
  enable: true,
  package: 'egg-mysql',
};
复制代码

就能够直接使用插件提供的功能:app.mysql.query(sql, values);sql

egg-mysql 插件文档

参数介绍

plugin.js 中的每一个配置项支持:

  • {Boolean} enable - 是否开启此插件,默认为 true
  • {String} package - npm 模块名称,经过 npm 模块形式引入插件
  • {String} path - 插件绝对路径,跟 package 配置互斥
  • {Array} env - 只有在指定运行环境才能开启,会覆盖插件自身 package.json 中的配置

开启和关闭

在上层框架内部内置的插件,应用在使用时就不用配置 package 或者 path,只须要指定 enable 与否:

// 对于内置插件,能够用下面的简洁方式开启或关闭
exports.onerror = false;
复制代码

根据环境配置

同时,咱们还支持 plugin.{env}.js 这种模式,会根据运行环境加载插件配置。

好比定义了一个开发环境使用的插件 egg-dev,只但愿在本地环境加载,能够安装到 devDependencies

// npm i egg-dev --save-dev
// package.json
{
  "devDependencies": {
    "egg-dev": "*"
  }
}
复制代码

而后在 plugin.local.js 中声明:

// config/plugin.local.js
exports.dev = {
  enable: true,
  package: 'egg-dev',
};
复制代码

这样在生产环境能够 npm i --production 不须要下载 egg-dev 的包了。

注意:

  • 不存在 plugin.default.js
  • 只能在应用层使用,在框架层请勿使用。

package 和 path

  • packagenpm 方式引入,也是最多见的引入方式
  • path 是绝对路径引入,如应用内部抽了一个插件,但还没达到开源发布独立 npm 的阶段,或者是应用本身覆盖了框架的一些插件
// config/plugin.js
const path = require('path');
exports.mysql = {
  enable: true,
  path: path.join(__dirname, '../lib/plugin/egg-mysql'),
};
复制代码

插件配置

插件通常会包含本身的默认配置,应用开发者能够在 config.default.js 覆盖对应的配置:

// config/config.default.js
exports.mysql = {
  client: {
    host: 'mysql.com',
    port: '3306',
    user: 'test_user',
    password: 'test_password',
    database: 'test',
  },
};
复制代码

插件列表

框架默认内置了企业级应用经常使用的插件:

更多社区的插件能够 GitHub 搜索 egg-plugin

插件开发详情见 插件开发

定时任务

虽然咱们经过框架开发的 HTTP Server 是请求响应模型的,可是仍然还会有许多场景须要执行一些定时任务

  • 定时上报应用状态。
  • 定时从远程接口更新本地缓存。
  • 定时进行文件切割、临时文件删除。

编写定时任务

全部的定时任务都统一存放在 app/schedule 目录下,每个文件都是一个独立的定时任务,能够配置定时任务的属性和要执行的方法。

app/schedule 目录下建立一个 update_cache.js 文件

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;
复制代码

还能够简写为

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 分钟执行一次,将远程数据请求回来挂载到 app.cache 上。

任务

  • tasksubscribe 同时支持 generator functioasync function
  • task 的入参为 ctx,匿名的 Context 实例,能够经过它调用 service 等。

定时方式

定时任务能够指定 interval 或者 cron 两种不一样的定时方式。

interval

经过 schedule.interval 参数来配置定时任务的执行时机,定时任务将会每间隔指定的时间执行一次。interval 能够配置成

  • 数字类型,单位为毫秒数,例如 5000
  • 字符类型,会经过 ms 转换成毫秒数,例如 5s。
module.exports = {
  schedule: {
    // 每 10 秒执行一次
    interval: '10s',
  },
};
复制代码
cron

经过 schedule.cron 参数来配置定时任务的执行时机,定时任务将会按照 cron 表达式在特定的时间点执行。cron 表达式经过 cron-parser 进行解析。

注意:cron-parser 支持可选的秒(linux crontab 不支持)。

*    *    *    *    *    *
┬    ┬    ┬    ┬    ┬    ┬
│    │    │    │    │    |
│    │    │    │    │    └ 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)
复制代码
module.exports = {
  schedule: {
    // 每三小时准点执行一次
    cron: '0 0 */3 * * *',
  },
};
复制代码

类型 type

worker 和 all。worker 和 all 都支持上面的两种定时方式,只是当到执行时机时,会执行定时任务的 worker 不一样:

  • worker 类型:每台机器上只有一个 worker 会执行这个定时任务,每次执行定时任务的 worker 的选择是随机的。
  • all 类型:每台机器上的每一个 worker 都会执行这个定时任务。

其余参数

除了刚才介绍到的几个参数以外,定时任务还支持这些参数:

  • cronOptions: 配置 cron 的时区等,参见 cron-parser 文档
  • immediate:配置了该参数为 true 时,这个定时任务会在应用启动并 ready 后马上执行一次这个定时任务。
  • disable:配置该参数为 true 时,这个定时任务不会被启动。
  • env:数组,仅在指定的环境下才启动该定时任务。

执行日志

执行日志会输出到 ${appInfo.root}/logs/{app_name}/egg-schedule.log,默认不会输出到控制台,能够经过 config.customLogger.scheduleLogger 来自定义。

// config/config.default.js
config.customLogger = {
  scheduleLogger: {
    // consoleLevel: 'NONE',
    // file: path.join(appInfo.root, 'logs', appInfo.name, 'egg-schedule.log'),
  },
};
复制代码

动态配置定时任务

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;
    },
  };
};
复制代码

手动执行定时任务

咱们能够经过 app.runSchedule(schedulePath) 来运行一个定时任务。app.runSchedule 接受一个定时任务文件路径(app/schedule 目录下的相对路径或者完整的绝对路径),执行对应的定时任务,返回一个 Promise。

  • 经过手动执行定时任务能够更优雅的编写对定时任务的单元测试。
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);
});
复制代码
  • 应用启动时,手动执行定时任务进行系统初始化,等初始化完毕后再启动应用。参见应用启动自定义章节,咱们能够在 app.js 中编写初始化逻辑。
module.exports = app => {
  app.beforeStart(async () => {
    // 保证应用启动监听端口前数据已经准备好了
    // 后续数据的更新由定时任务自动触发
    await app.runSchedule('update_cache');
  });
};
复制代码

框架扩展

框架提供了多种扩展点扩展自身的功能:Application、Context、Request、Response、Helper。

Application

访问方式

  • ctx.app
  • Controller,Middleware,Helper,Service 中均可以经过 this.app 访问到 Application 对象,例如this.app.config 访问配置对象。
  • 在 app.js 中 app 对象会做为第一个参数注入到入口函数中
// app.js
module.exports = app => {
  // 使用 app 对象
};
复制代码

扩展方式

框架会把 app/extend/application.js 中定义的对象与 Koa Application 的 prototype 对象进行合并,在应用启动时会基于扩展后的 prototype 生成 app 对象。

// app/extend/application.js
module.exports = {
  foo(param) {
    // this 就是 app 对象,在其中能够调用 app 上的其余方法,或访问属性
  },
};
复制代码

属性扩展

通常来讲属性的计算只须要进行一次,那么必定要实现缓存,不然在屡次访问属性时会计算屡次,这样会下降应用性能。

推荐的方式是使用 Symbol + Getter 的模式。

// app/extend/application.js
const 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];
  },
};
复制代码

Context

Context 指的是 Koa 的请求上下文,这是 请求级别 的对象,每次请求生成一个 Context 实例,一般咱们也简写成 ctx。在全部的文档中,Context 和 ctx 都是指 Koa 的上下文对象。

访问方式

  • middleware 中返回函数的第一个参数就是 ctx,例如 ctx.cookies.get('foo')
  • controller 有两种写法,类的写法经过 this.ctx,方法的写法直接经过 ctx 入参。
  • helper,service 中的 this 指向 helper,service 对象自己,使用 this.ctx 访问 context 对象,例如 this.ctx.cookies.get('foo')

扩展方式

框架会把 app/extend/context.js 中定义的对象与 Koa Context 的 prototype 对象进行合并,在处理请求时会基于扩展后的 prototype 生成 ctx 对象。

// app/extend/context.js
module.exports = {
  foo(param) {
    // this 就是 ctx 对象,在其中能够调用 ctx 上的其余方法,或访问属性
  },
};
复制代码

属性扩展同 Application

Request

Request 对象和 Koa 的 Request 对象相同,是 请求级别 的对象,它提供了大量请求相关的属性和方法供使用。

访问方式

ctx.request

ctx 上的不少属性和方法都被代理到 request 对象上,对于这些属性和方法使用 ctx 和使用 request 去访问它们是等价的,例如 ctx.url === ctx.request.url

扩展方式

框架会把 app/extend/request.js 中定义的对象与内置 request 的 prototype 对象进行合并,在处理请求时会基于扩展后的 prototype 生成 request 对象。

// app/extend/request.js
module.exports = {
  get foo() {
    return this.get('x-request-foo');
  },
};
复制代码

Response

Response 对象和 Koa 的 Response 对象相同,是 请求级别 的对象,它提供了大量响应相关的属性和方法供使用。

访问方式

ctx.response

ctx 上的不少属性和方法都被代理到 response 对象上,对于这些属性和方法使用 ctx 和使用 response 去访问它们是等价的,例如 ctx.status = 404ctx.response.status = 404 是等价的。

扩展方式

框架会把 app/extend/response.js 中定义的对象与内置 response 的 prototype 对象进行合并,在处理请求时会基于扩展后的 prototype 生成 response 对象。

// app/extend/response.js
module.exports = {
  set foo(value) {
    this.set('x-response-foo', value);
  },
};
复制代码

就能够这样使用啦:this.response.foo = 'bar';

Helper

Helper 函数用来提供一些实用的 utility 函数。

它的做用在于咱们能够将一些经常使用的动做抽离在 helper.js 里面成为一个独立的函数,这样能够用 JavaScript 来写复杂的逻辑,避免逻辑分散各处。另外还有一个好处是 Helper 这样一个简单的函数,可让咱们更容易编写测试用例。

框架内置了一些经常使用的 Helper 函数。咱们也能够编写自定义的 Helper 函数。

访问方式

经过 ctx.helper 访问到 helper 对象,例如:

// 假设在 app/router.js 中定义了 home router
app.get('home', '/', 'home.index');

// 使用 helper 计算指定 url path
ctx.helper.pathFor('home', { by: 'recent', limit: 20 })
// => /?by=recent&limit=20
复制代码

扩展方式

框架会把 app/extend/helper.js 中定义的对象与内置 helper 的 prototype 对象进行合并,在处理请求时会基于扩展后的 prototype 生成 helper 对象。

// app/extend/helper.js
module.exports = {
  foo(param) {
    // this 是 helper 对象,在其中能够调用其余 helper 方法
    // this.ctx => context 对象
    // this.app => application 对象
  },
};
复制代码

启动自定义

框架提供了统一的入口文件(app.js)进行启动过程自定义,这个文件返回一个 Boot 类,咱们能够经过定义 Boot 类中的生命周期方法来执行启动应用过程当中的初始化工做。

框架提供了这些生命周期函数供开发人员处理:

  • 配置文件即将加载,这是最后动态修改配置的时机(configWillLoad
  • 配置文件加载完成(configDidLoad
  • 文件加载完成(didLoad
  • 插件启动完毕(willReady
  • worker 准备就绪(didReady
  • 应用启动完成(serverDidReady
  • 应用即将关闭(beforeClose
// app.js
class 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;
复制代码

应用部署

在本地开发时,咱们使用 egg-bin dev 来启动服务,可是在部署应用的时候不能够这样使用。由于 egg-bin dev 会针对本地开发作不少处理,而生产运行须要一个更加简单稳定的方式。

部署

服务器须要预装 Node.js,框架支持的 Node 版本为 >= 8.0.0

框架内置了 egg-cluster 来启动 Master 进程,Master 有足够的稳定性,再也不须要使用 pm2 等进程守护模块。

同时,框架也提供了 egg-scripts 来支持线上环境的运行和中止。

npm i egg-scripts --save
复制代码
{
  "scripts": {
    "start": "egg-scripts start --daemon",
    "stop": "egg-scripts stop"
  }
}
复制代码

这样咱们就能够经过 npm start 和 npm stop 命令启动或中止应用。

启动命令

egg-scripts start --port=7001 --daemon --title=egg-server-showcase
复制代码

支持如下参数:

  • --port=7001 端口号,默认会读取环境变量 process.env.PORT,如未传递将使用框架内置端口 7001。
  • --daemon 是否容许在后台模式,无需 nohup。若使用 Docker 建议直接前台运行。
  • --env=prod 框架运行环境,默认会读取环境变量 process.env.EGG_SERVER_ENV, 如未传递将使用框架内置环境 prod。
  • --workers=2 框架 worker 线程数,默认会建立和 CPU 核数至关的 app worker 数,能够充分的利用 CPU 资源。
  • --title=egg-server-showcase 用于方便 ps 进程时 grep 用,默认为 egg-server-${appname}
  • --framework=yadan 若是应用使用了自定义框架,能够配置 package.json 的 egg.framework 或指定该参数。
  • --ignore-stderr 忽略启动期的报错。
  • --https.key 指定 HTTPS 所需密钥文件的完整路径。
  • --https.cert 指定 HTTPS 所需证书文件的完整路径。

更多参数可查看 egg-scriptsegg-cluster 文档。

启动配置项

// config/config.default.js

exports.cluster = {
  listen: {
    port: 7001,
    hostname: '127.0.0.1',
    // path: '/var/run/egg.sock',
  }
}
复制代码

中止命令

egg-scripts stop [--title=egg-server]
复制代码

该命令将杀死 master 进程,并通知 worker 和 agent 优雅退出。

--title=egg-server 用于杀死指定的 egg 应用,未传递则会终止全部的 Egg 应用。

日志

框架内置了强大的企业级日志支持,由 egg-logger 模块提供。

  • 日志分级
  • 统一错误日志,全部 logger 中使用 .error() 打印的 ERROR 级别日志都会打印到统一的错误日志文件中,便于追踪
  • 启动日志和运行日志分离
  • 自定义日志
  • 多进程日志
  • 自动切割日志
  • 高性能

日志路径

  • 全部日志文件默认都放在 ${appInfo.root}/logs/${appInfo.name} 路径下,例如 /home/admin/logs/example-app
  • 在本地开发环境 (env: local) 和单元测试环境 (env: unittest),为了不冲突以及集中管理,日志会打印在项目目录下的 logs 目录,例如 /path/to/example-app/logs/example-app

若是想自定义日志路径:

// config/config.${env}.js
exports.logger = {
  dir: '/path/to/your/custom/log/dir',
};
复制代码

日志分类

框架内置了几种日志,分别在不一样的场景下使用:

  • appLogger ${appInfo.name}-web.log,例如 example-app-web.log,应用相关日志,供应用开发者使用的日志。咱们在绝大数状况下都在使用它。
  • coreLogger egg-web.log 框架内核、插件日志。
  • errorLogger common-error.log 实际通常不会直接使用它,任何 logger 的 .error() 调用输出的日志都会重定向到这里,重点经过查看此日志定位异常。
  • agentLogger egg-agent.log agent 进程日志,框架和使用到 agent 进程执行任务的插件会打印一些日志到这里。

若是想自定义以上日志文件名称,能够在 config 文件中覆盖默认值:

// config/config.${env}.js
module.exports = appInfo => {
  return {
    logger: {
      appLogName: `${appInfo.name}-web.log`,
      coreLogName: 'egg-web.log',
      agentLogName: 'egg-agent.log',
      errorLogName: 'common-error.log',
    },
  };
};
复制代码

如何打印日志

Context Logger

用于记录 Web 行为相关的日志。

每行日志会自动记录上当前请求的一些基本信息, 如 [$userId/$ip/$traceId/${cost}ms $method $url]

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'));
复制代码

对于框架开发者和插件开发者会使用到的 Context Logger 还有 ctx.coreLogger

App Logger

若是咱们想作一些应用级别的日志记录,如记录启动阶段的一些数据信息,能够经过 App Logger 来完成。

// app.js
module.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 还有 app.coreLogger

// app.js
module.exports = app => {
  app.coreLogger.info('启动耗时 %d ms', Date.now() - start);
};
复制代码

Agent Logger

在开发框架和插件时有时会须要在 Agent 进程运行代码,这时使用 agent.coreLogger

// agent.js
module.exports = agent => {
  agent.logger.debug('debug info');
  agent.logger.info('启动耗时 %d ms', Date.now() - start);
  agent.logger.warn('warning!');

  agent.logger.error(someErrorObj);
};
复制代码

日志文件编码

默认编码为 utf-8,可经过以下方式覆盖:

// config/config.${env}.js
exports.logger = {
  encoding: 'gbk',
};
复制代码

日志文件格式

// config/config.${env}.js
exports.logger = {
  outputJSON: true,
};
复制代码

日志级别

日志分为 NONEDEBUGINFOWARNERROR 5 个级别。

日志打印到文件中的同时,为了方便开发,也会同时打印到终端中。

文件日志级别

默认只会输出 INFO 及以上(WARNERROR)的日志到文件中。

打印全部级别日志到文件中:

// config/config.${env}.js
exports.logger = {
  level: 'DEBUG',
};
复制代码

关闭全部打印到文件的日志:

// config/config.${env}.js
exports.logger = {
  level: 'NONE',
};
复制代码

生产环境打印 debug 日志

为了不一些插件的调试日志在生产环境打印致使性能问题,生产环境默认禁止打印 DEBUG 级别的日志,若是确实有需求在生产环境打印 DEBUG 日志进行调试,须要打开 allowDebugAtProd 配置项。

// config/config.prod.js
exports.logger = {
  level: 'DEBUG',
  allowDebugAtProd: true,
};
复制代码

终端日志级别

默认只会输出 INFO 及以上(WARNERROR)的日志到终端中。(注意:这些日志默认只在 local 和 unittest 环境下会打印到终端)

logger.consoleLevel: 输出到终端日志的级别,默认为 INFO

打印全部级别日志到终端:

// config/config.${env}.js
exports.logger = {
  consoleLevel: 'DEBUG',
};
复制代码

关闭全部打印到终端的日志:

// config/config.${env}.js
exports.logger = {
  consoleLevel: 'NONE',
};
复制代码

基于性能的考虑,在正式环境下,默认会关闭终端日志输出。若有须要,你能够经过下面的配置开启。(不推荐)

// config/config.${env}.js
exports.logger = {
  disableConsoleAfterReady: false,
};
复制代码

日志切割

框架对日志切割的支持由 egg-logrotator 插件提供。

按天切割

这是框架的默认日志切割方式,在每日 00:00 按照 .log.YYYY-MM-DD 文件名进行切割。

以 appLog 为例,当前写入的日志为 example-app-web.log,当凌晨 00:00 时,会对日志进行切割,把过去一天的日志按 example-app-web.log.YYYY-MM-DD 的形式切割为单独的文件。

按照文件大小切割

// config/config.${env}.js
const path = require('path');

module.exports = appInfo => {
  return {
    logrotator: {
      filesRotateBySize: [
        path.join(appInfo.root, 'logs', appInfo.name, 'egg-web.log'),
      ],
      maxFileSize: 2 * 1024 * 1024 * 1024,
    },
  };
};
复制代码

按照小时切割

这和默认的按天切割很是相似,只是时间缩短到每小时。

// config/config.${env}.js
const path = require('path');

module.exports = appInfo => {
  return {
    logrotator: {
      filesRotateByHour: [
        path.join(appInfo.root, 'logs', appInfo.name, 'common-error.log'),
      ],
    },
  };
};
复制代码

性能

一般 Web 访问是高频访问,每次打印日志都写磁盘会形成频繁磁盘 IO,为了提升性能,咱们采用的文件日志写入策略是:

日志同步写入内存,异步每隔一段时间(默认 1 秒)刷盘

更多详细请参考 egg-loggeregg-logrotator

HttpClient

框架基于 urllib 内置实现了一个 HttpClient,应用能够很是便捷地完成任何 HTTP 请求。

经过 app 使用 HttpClient

架在应用初始化的时候,会自动将 HttpClient 初始化到 app.httpclient。 同时增长了一个 app.curl(url, options) 方法,它等价于 app.httpclient.request(url, options)

// app.js
module.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);
  });
};
复制代码

经过 ctx 使用 HttpClient

框架在 Context 中一样提供了 ctx.curl(url, options)ctx.httpclient,保持跟 app 下的使用体验一致。 这样就能够在有 Context 的地方(如在 controller 中)很是方便地使用 ctx.curl() 方法完成一次 HTTP 请求。

// app/controller/npm.js
class 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,
    };
  }
}
复制代码

基本 HTTP 请求

GET

// app/controller/npm.js
class 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;
  }
}
复制代码

POST

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',
});
复制代码

PUT

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',
});
复制代码

DELETE

const result = await ctx.curl('https://httpbin.org/delete', {
  // 必须指定 method
  method: 'DELETE',
  // 明确告诉 HttpClient 以 JSON 格式处理响应 body
  dataType: 'json',
});
复制代码

options 参数详解

httpclient.request(url, options)

HttpClient 默认全局配置,应用能够经过 config/config.default.js 覆盖此配置。

经常使用

  • data: Object 须要发送的请求数据,根据 method 自动选择正确的数据处理方式。
    • GETHEAD:经过 querystring.stringify(data) 处理后拼接到 url 的 query 参数上。
    • POSTPUTDELETE 等:须要根据 contentType 作进一步判断处理。
      • contentType = json:经过 JSON.stringify(data) 处理,并设置为 body 发送。
      • 其余:经过 querystring.stringify(data) 处理,并设置为 body 发送。
  • files: Mixed
  • method: String 设置请求方法,默认是 GET。 支持 GET、POST、PUT、DELETE、PATCH 等全部 HTTP 方法。
  • contentType: String 设置请求数据格式,默认是 undefined,HttpClient 会自动根据 data 和 content 参数自动设置。 data 是 object 的时候默认设置的是 form。支持 json 格式。
  • dataType: String 设置响应数据格式,默认不对响应数据作任何处理,直接返回原始的 buffer 格式数据。 支持 text 和 json 两种格式。
  • headers: Object 自定义请求头。
  • timeout: Number|Array 请求超时时间,默认是 [ 5000, 5000 ],即建立链接超时是 5 秒,接收响应超时是 5 秒。

调试辅助(对 ctx.curl 进行抓包)

若是你须要对 HttpClient 的请求进行抓包调试,能够添加如下配置到 config.local.js

// config.local.js
module.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。

最后经过如下指令启动应用:

http_proxy=http://127.0.0.1:8888 npm run dev
复制代码

windows 下能够用cmder 或者 git bash

set http_proxy=http://127.0.0.1:8888 && npm run dev
复制代码

而后就能够正常操做了,全部通过 HttpClient 的请求,均可以你的抓包工具中查看到。

相关文章
相关标签/搜索