回顾一下上篇讲到的内容,上篇讲了:
- 运行环境
- Config 配置
- 中间件(Middleware)
- 路由
- 控制器(Controller)
[h1]服务(Service) [/h1]Service 就是在复杂业务场景下用于做业务逻辑封装的一个抽象层
[h2]使用场景[/h2]- 复杂数据的处理,比如要展现的信息需要从数据库获取,还要经过一定的规则计算,才能返回用户显示。或者计算完成后,更新到数据库。
- 第三方服务的调用,比如 GitHub 信息获取等。
[h2]定义 Service[/h2]- // 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 实例,由于它继承于,故拥有下列属性方便我们进行开发:
- : 当前请求的上下文 Context 对象的实例
- : 当前应用 Application 对象的实例
- :应用定义的 Service
- :应用运行时的配置项
- :logger 对象,上面有四个方法(,,,),分别代表打印四个不同级别的日志,使用方法和效果与 context logger 中介绍的一样,但是通过这个 logger 对象记录的日志,在日志前面会加上打印该日志的文件路径,以便快速定位日志打印位置。
[h3]Service ctx 详解[/h3]- 发起网络调用。
- this.ctx.service.otherService
复制代码 调用其他 Service。
- 发起数据库调用等, db 可能是其他插件提前挂载到 app 上的模块。
[h3]注意事项[/h3]- 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 不是单例,是请求级别 的对象,框架在每次请求中首次访问时延迟实例化,所以 Service 中可以通过 this.ctx 获取到当前请求的上下文。
[h2]使用 Service[/h2]- // 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。
- 它没有,只能声明跟其他插件的依赖,而不能决定其他插件的开启与否。
他们的关系是:
- 应用可以直接引入 Koa 的中间件。
- 插件本身可以包含中间件。
- 多个插件可以包装为一个上层框架。
[h2]使用插件[/h2]插件一般通过 npm 模块的方式进行复用:建议通过 ^ 的方式引入依赖,并且强烈不建议锁定版本。- { "dependencies": { "egg-mysql": "^3.0.0" }}
复制代码 然后需要在应用或框架的中声明:- // config/plugin.js// 使用 mysql 插件exports.mysql = { enable: true, package: 'egg-mysql',};
复制代码 就可以直接使用插件提供的功能:- app.mysql.query(sql, values);
复制代码 egg-mysql 插件文档
[h3]参数介绍[/h3]中的每个配置项支持:
- - 是否开启此插件,默认为 true
- - npm 模块名称,通过 npm 模块形式引入插件
- - 插件绝对路径,跟 package 配置互斥
- - 只有在指定运行环境才能开启,会覆盖插件自身 package.json 中的配置
[h3]开启和关闭[/h3]在上层框架内部内置的插件,应用在使用时就不用配置 package 或者 path,只需要指定 enable 与否:- // 对于内置插件,可以用下面的简洁方式开启或关闭exports.onerror = false;
复制代码 [h3]根据环境配置[/h3]同时,我们还支持这种模式,会根据运行环境加载插件配置。
比如定义了一个开发环境使用的插件,只希望在本地环境加载,可以安装到。- // npm i egg-dev --save-dev// package.json{ "devDependencies": { "egg-dev": "*" }}
复制代码 然后在中声明:- // config/plugin.local.jsexports.dev = { enable: true, package: 'egg-dev',};
复制代码 这样在生产环境可以不需要下载的包了。
注意:
[h3]package 和 path[/h3]- 是方式引入,也是最常见的引入方式
- 是绝对路径引入,如应用内部抽了一个插件,但还没达到开源发布独立 npm 的阶段,或者是应用自己覆盖了框架的一些插件
- // config/plugin.jsconst path = require('path');exports.mysql = { enable: true, path: path.join(__dirname, '../lib/plugin/egg-mysql'),};
复制代码 [h2]插件配置[/h2]插件一般会包含自己的默认配置,应用开发者可以在覆盖对应的配置:- // config/config.default.jsexports.mysql = { client: { host: 'mysql.com', port: '3306', user: 'test_user', password: 'test_password', database: 'test', },};
复制代码 [h2]插件列表[/h2]框架默认内置了企业级应用常用的插件:
- 统一异常处理
- Session 实现
- i18n 多语言
- watcher 文件和文件夹监控
- multipart 文件流式上传
- security 安全
- development 开发环境配置
- logrotator 日志切分
- schedule 定时任务
- static 静态服务器
- jsonp jsonp 支持
- view 模板引擎
更多社区的插件可以 GitHub 搜索。
插件开发详情见 插件开发
[h1]定时任务 [/h1]虽然我们通过框架开发的 HTTP Server 是请求响应模型的,但是仍然还会有许多场景需要执行一些定时任务
- 定时上报应用状态。
- 定时从远程接口更新本地缓存。
- 定时进行文件切割、临时文件删除。
[h2]编写定时任务[/h2]所有的定时任务都统一存放在目录下,每一个文件都是一个独立的定时任务,可以配置定时任务的属性和要执行的方法。
在目录下创建一个文件- 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 分钟执行一次,将远程数据请求回来挂载到上。
[h3]任务[/h3]- 或同时支持和。
- 的入参为,匿名的 Context 实例,可以通过它调用等。
[h3]定时方式[/h3]定时任务可以指定 interval 或者 cron 两种不同的定时方式。
interval通过参数来配置定时任务的执行时机,定时任务将会每间隔指定的时间执行一次。interval 可以配置成
- 数字类型,单位为毫秒数,例如 5000
- 字符类型,会通过 ms 转换成毫秒数,例如 5s。
- module.exports = { schedule: { // 每 10 秒执行一次 interval: '10s', },};
复制代码 cron通过 schedule.cron 参数来配置定时任务的执行时机,定时任务将会按照 cron 表达式在特定的时间点执行。cron 表达式通过进行解析。
注意: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 * * *', },};
复制代码 [h3]类型 type[/h3]worker 和 all。worker 和 all 都支持上面的两种定时方式,只是当到执行时机时,会执行定时任务的 worker 不同:
- 类型:每台机器上只有一个 worker 会执行这个定时任务,每次执行定时任务的 worker 的选择是随机的。
- 类型:每台机器上的每个 worker 都会执行这个定时任务。
[h3]其他参数[/h3]除了刚才介绍到的几个参数之外,定时任务还支持这些参数:
- : 配置 cron 的时区等,参见 cron-parser 文档
- :配置了该参数为 true 时,这个定时任务会在应用启动并 ready 后立刻执行一次这个定时任务。
- :配置该参数为 true 时,这个定时任务不会被启动。
- :数组,仅在指定的环境下才启动该定时任务。
[h3]执行日志[/h3]执行日志会输出到- ${appInfo.root}/logs/{app_name}/egg-schedule.log
复制代码 ,默认不会输出到控制台,可以通过- config.customLogger.scheduleLogger
复制代码 来自定义。- // config/config.default.jsconfig.customLogger = { scheduleLogger: { // consoleLevel: 'NONE', // file: path.join(appInfo.root, 'logs', appInfo.name, 'egg-schedule.log'), },};
复制代码 [h3]动态配置定时任务[/h3]- 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]我们可以通过- app.runSchedule(schedulePath)
复制代码 来运行一个定时任务。接受一个定时任务文件路径(目录下的相对路径或者完整的绝对路径),执行对应的定时任务,返回一个 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);});
复制代码- 应用启动时,手动执行定时任务进行系统初始化,等初始化完毕后再启动应用。参见应用启动自定义章节,我们可以在中编写初始化逻辑。
- module.exports = app => { app.beforeStart(async () => { // 保证应用启动监听端口前数据已经准备好了 // 后续数据的更新由定时任务自动触发 await app.runSchedule('update_cache'); });};
复制代码 [h1]框架扩展 [/h1]框架提供了多种扩展点扩展自身的功能:Application、Context、Request、Response、Helper。
[h2]Application[/h2][h3]访问方式[/h3]- Controller,Middleware,Helper,Service 中都可以通过访问到 Application 对象,例如访问配置对象。
- 在 app.js 中 app 对象会作为第一个参数注入到入口函数中
- // app.jsmodule.exports = app => { // 使用 app 对象};
复制代码 [h3]扩展方式[/h3]框架会把- app/extend/application.js
复制代码 中定义的对象与 Koa Application 的 prototype 对象进行合并,在应用启动时会基于扩展后的 prototype 生成 app 对象。- // app/extend/application.jsmodule.exports = { foo(param) { // this 就是 app 对象,在其中可以调用 app 上的其他方法,或访问属性 },};
复制代码 属性扩展
一般来说属性的计算只需要进行一次,那么一定要实现缓存,否则在多次访问属性时会计算多次,这样会降低应用性能。
推荐的方式是使用 Symbol + Getter 的模式。- // 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,例如。
- controller 有两种写法,类的写法通过,方法的写法直接通过入参。
- helper,service 中的 this 指向 helper,service 对象本身,使用 this.ctx 访问 context 对象,例如
- this.ctx.cookies.get('foo')
复制代码 。
[h3]扩展方式[/h3]框架会把中定义的对象与 Koa Context 的 prototype 对象进行合并,在处理请求时会基于扩展后的 prototype 生成 ctx 对象。- // app/extend/context.jsmodule.exports = { foo(param) { // this 就是 ctx 对象,在其中可以调用 ctx 上的其他方法,或访问属性 },};
复制代码 属性扩展同 Application
[h2]Request[/h2]Request 对象和 Koa 的 Request 对象相同,是 请求级别 的对象,它提供了大量请求相关的属性和方法供使用。
[h3]访问方式[/h3]上的很多属性和方法都被代理到对象上,对于这些属性和方法使用和使用去访问它们是等价的,例如- ctx.url === ctx.request.url
复制代码 。
[h3]扩展方式[/h3]框架会把中定义的对象与内置的 prototype 对象进行合并,在处理请求时会基于扩展后的 prototype 生成对象。- // app/extend/request.jsmodule.exports = { get foo() { return this.get('x-request-foo'); },};
复制代码 [h2]Response[/h2]Response 对象和 Koa 的 Response 对象相同,是 请求级别 的对象,它提供了大量响应相关的属性和方法供使用。
[h3]访问方式[/h3]上的很多属性和方法都被代理到对象上,对于这些属性和方法使用和使用去访问它们是等价的,例如和- ctx.response.status = 404
复制代码 是等价的。
[h3]扩展方式[/h3]框架会把中定义的对象与内置的 prototype 对象进行合并,在处理请求时会基于扩展后的 prototype 生成 response 对象。- // app/extend/response.jsmodule.exports = { set foo(value) { this.set('x-response-foo', value); },};
复制代码 就可以这样使用啦:- this.response.foo = 'bar';
复制代码 [h2]Helper[/h2]Helper 函数用来提供一些实用的 utility 函数。
它的作用在于我们可以将一些常用的动作抽离在 helper.js 里面成为一个独立的函数,这样可以用 JavaScript 来写复杂的逻辑,避免逻辑分散各处。另外还有一个好处是 Helper 这样一个简单的函数,可以让我们更容易编写测试用例。
框架内置了一些常用的 Helper 函数。我们也可以编写自定义的 Helper 函数。
[h3]访问方式[/h3]通过访问到 helper 对象,例如:- // 假设在 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]框架会把中定义的对象与内置的 prototype 对象进行合并,在处理请求时会基于扩展后的 prototype 生成对象。- // app/extend/helper.jsmodule.exports = { foo(param) { // this 是 helper 对象,在其中可以调用其他 helper 方法 // this.ctx => context 对象 // this.app => application 对象 },};
复制代码 [h1]启动自定义 [/h1]框架提供了统一的入口文件()进行启动过程自定义,这个文件返回一个 Boot 类,我们可以通过定义 Boot 类中的生命周期方法来执行启动应用过程中的初始化工作。
框架提供了这些生命周期函数供开发人员处理:
- 配置文件即将加载,这是最后动态修改配置的时机()
- 配置文件加载完成()
- 文件加载完成()
- 插件启动完毕()
- worker 准备就绪()
- 应用启动完成()
- 应用即将关闭()
- // 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 版本为。
框架内置了 egg-cluster 来启动 Master 进程,Master 有足够的稳定性,不再需要使用 pm2 等进程守护模块。
同时,框架也提供了 egg-scripts 来支持线上环境的运行和停止。- { "scripts": { "start": "egg-scripts start --daemon", "stop": "egg-scripts stop" }}
复制代码 这样我们就可以通过 npm start 和 npm stop 命令启动或停止应用。
[h3]启动命令[/h3]- egg-scripts start --port=7001 --daemon --title=egg-server-showcase
复制代码 支持以下参数:
- 端口号,默认会读取环境变量,如未传递将使用框架内置端口 7001。
- 是否允许在后台模式,无需。若使用 Docker 建议直接前台运行。
- 框架运行环境,默认会读取环境变量
- process.env.EGG_SERVER_ENV
复制代码 , 如未传递将使用框架内置环境 prod。
- 框架 worker 线程数,默认会创建和 CPU 核数相当的 app worker 数,可以充分的利用 CPU 资源。
- --title=egg-server-showcase
复制代码 用于方便 ps 进程时 grep 用,默认为。
- 如果应用使用了自定义框架,可以配置 package.json 的 egg.framework 或指定该参数。
- 忽略启动期的报错。
- 指定 HTTPS 所需密钥文件的完整路径。
- 指定 HTTPS 所需证书文件的完整路径。
更多参数可查看 egg-scripts 和 egg-cluster 文档。
启动配置项- // config/config.default.jsexports.cluster = { listen: { port: 7001, hostname: '127.0.0.1', // path: '/var/run/egg.sock', }}
复制代码 [h3]停止命令[/h3]- egg-scripts stop [--title=egg-server]
复制代码 该命令将杀死 master 进程,并通知 worker 和 agent 优雅退出。用于杀死指定的 egg 应用,未传递则会终止所有的 Egg 应用。
[h1]日志 [/h1]框架内置了强大的企业级日志支持,由 egg-logger 模块提供。
- 日志分级
- 统一错误日志,所有 logger 中使用 .error() 打印的 ERROR 级别日志都会打印到统一的错误日志文件中,便于追踪
- 启动日志和运行日志分离
- 自定义日志
- 多进程日志
- 自动切割日志
- 高性能
[h2]日志路径[/h2]- 所有日志文件默认都放在
- ${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}.jsexports.logger = { dir: '/path/to/your/custom/log/dir',};
复制代码 [h2]日志分类[/h2]框架内置了几种日志,分别在不同的场景下使用:
- appLogger,例如,应用相关日志,供应用开发者使用的日志。我们在绝大数情况下都在使用它。
- coreLogger框架内核、插件日志。
- errorLogger实际一般不会直接使用它,任何 logger 的 .error() 调用输出的日志都会重定向到这里,重点通过查看此日志定位异常。
- agentLoggeragent 进程日志,框架和使用到 agent 进程执行任务的插件会打印一些日志到这里。
如果想自定义以上日志文件名称,可以在 config 文件中覆盖默认值:- // 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 行为相关的日志。
每行日志会自动记录上当前请求的一些基本信息, 如- [$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'));
复制代码 对于框架开发者和插件开发者会使用到的还有。
[h3]App Logger[/h3]如果我们想做一些应用级别的日志记录,如记录启动阶段的一些数据信息,可以通过 App Logger 来完成。- // 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 还有。- // app.jsmodule.exports = app => { app.coreLogger.info('启动耗时 %d ms', Date.now() - start);};
复制代码 [h3]Agent Logger[/h3]在开发框架和插件时有时会需要在 Agent 进程运行代码,这时使用。- // 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]默认编码为,可通过如下方式覆盖:- // config/config.${env}.jsexports.logger = { encoding: 'gbk',};
复制代码 [h2]日志文件格式[/h2]- // config/config.${env}.jsexports.logger = { outputJSON: true,};
复制代码 [h2]日志级别[/h2]日志分为,,,和5 个级别。
日志打印到文件中的同时,为了方便开发,也会同时打印到终端中。
[h3]文件日志级别[/h3]默认只会输出及以上(和)的日志到文件中。
打印所有级别日志到文件中:- // config/config.${env}.jsexports.logger = { level: 'DEBUG',};
复制代码 关闭所有打印到文件的日志:- // config/config.${env}.jsexports.logger = { level: 'NONE',};
复制代码 生产环境打印 debug 日志
为了避免一些插件的调试日志在生产环境打印导致性能问题,生产环境默认禁止打印 DEBUG 级别的日志,如果确实有需求在生产环境打印 DEBUG 日志进行调试,需要打开配置项。- // config/config.prod.jsexports.logger = { level: 'DEBUG', allowDebugAtProd: true,};
复制代码 [h3]终端日志级别[/h3]默认只会输出及以上(和)的日志到终端中。(注意:这些日志默认只在 local 和 unittest 环境下会打印到终端): 输出到终端日志的级别,默认为打印所有级别日志到终端:- // config/config.${env}.jsexports.logger = { consoleLevel: 'DEBUG',};
复制代码 关闭所有打印到终端的日志:- // config/config.${env}.jsexports.logger = { consoleLevel: 'NONE',};
复制代码 基于性能的考虑,在正式环境下,默认会关闭终端日志输出。如有需要,你可以通过下面的配置开启。(不推荐)- // config/config.${env}.jsexports.logger = { disableConsoleAfterReady: false,};
复制代码 [h2]日志切割[/h2]框架对日志切割的支持由 egg-logrotator 插件提供。
[h3]按天切割[/h3]这是框架的默认日志切割方式,在每日按照文件名进行切割。
以 appLog 为例,当前写入的日志为,当凌晨 00:00 时,会对日志进行切割,把过去一天的日志按- example-app-web.log.YYYY-MM-DD
复制代码 的形式切割为单独的文件。
[h3]按照文件大小切割[/h3]- // 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]这和默认的按天切割非常类似,只是时间缩短到每小时。- // 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 初始化到。同时增加了一个方法,它等价于- app.httpclient.request(url, options)
复制代码 。- // 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 中同样提供了和,保持跟 app 下的使用体验一致。这样就可以在有 Context 的地方(如在 controller 中)非常方便地使用方法完成一次 HTTP 请求。- // 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]- // 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]- 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]- 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]- const result = await ctx.curl('https://httpbin.org/delete', { // 必须指定 method method: 'DELETE', // 明确告诉 HttpClient 以 JSON 格式处理响应 body dataType: 'json',});
复制代码 [h2]options 参数详解[/h2]- httpclient.request(url, options)
复制代码 HttpClient 默认全局配置,应用可以通过 config/config.default.js 覆盖此配置。
常用
- 需要发送的请求数据,根据 method 自动选择正确的数据处理方式。
- :通过处理,并设置为 body 发送。
- 其他:通过
- querystring.stringify(data)
复制代码 处理,并设置为 body 发送。
- ,:通过
- querystring.stringify(data)
复制代码 处理后拼接到 url 的 query 参数上。
- ,和等:需要根据做进一步判断处理。
- 设置请求方法,默认是 GET。支持 GET、POST、PUT、DELETE、PATCH 等所有 HTTP 方法。
- 设置请求数据格式,默认是 undefined,HttpClient 会自动根据 data 和 content 参数自动设置。data 是 object 的时候默认设置的是 form。支持 json 格式。
- 设置响应数据格式,默认不对响应数据做任何处理,直接返回原始的 buffer 格式数据。支持 text 和 json 两种格式。
- 自定义请求头。
- 请求超时时间,默认是,即创建连接超时是 5 秒,接收响应超时是 5 秒。
[h2]调试辅助(对 ctx.curl 进行抓包)[/h2]如果你需要对 HttpClient 的请求进行抓包调试,可以添加以下配置到:- // 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。
最后通过以下指令启动应用:- 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 的请求,都可以你的抓包工具中查看到。
推荐阅读
Eggjs入门系列-基础全面讲解(中)
Eggjs入门系列-基础全面讲解(上)
React源码解析-React 导出了些什么
Nginx入门-基本操作一览
新手拿到一台云服务器之后怎么连接上服务器?怎么添加用户?怎么连接FTP上传文件?怎么安装Node?
看了那么多,TCP/IP究竟是什么(二)
看了那么多,TCP/IP究竟是什么(一)
TCP三次握手和四次分手
vuejs 中双向绑定的模拟实现
云影sky
关注回复关键词送学习资料,帮你快速掌握刚需技能
关注
您的在看是我创作的动力
|
|