egg 学习与分享
什么是 egg
egg.js是基于koa为底层,由阿里nodejs团队封装的企业级Web应用解决方案,以约束和规范化团队开发,帮助开
发团队和开发人员降低开发和维护成本为核心设计理念的优秀解决方案。
官方文档地址:
https://eggjs.org/zh-cn/index.html
创建一个 egg 应用
创建工程目录,进入工程目录下执行以下命令,初始化一个基本的 egg 工程结构。
npm init egg --type=simple
查看工程目录
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XqPRYGFg-1573725081950)(/home/xiehuafeng/.config/Typora/typora-user-images/1573093330140.png)]
- app/controller/** 用于解析用户的输入,处理后返回相应的结果
- app/router.js 用于配置 URL 路由规则
- config/config.{env}.js 用于编写配置文件
- config/plugin.js 用于配置需要加载的插件
- test/** 用于单元测试
在项目工程根目录执行以下命令,启动项目。
npm install
npm run dev
访问 http://localhost:7001,可得到结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IbZ9JDnt-1573725081951)(/home/xiehuafeng/.config/Typora/typora-user-images/1573094226305.png)]
我们通常使用vscode作egg工程开发,开发过程当中为了方便调试工程,可以在项目根目录新建 .vscode 目录,然后新建 launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Egg",
"type": "node",
"request": "launch",
"cwd": "/home/xiehuafeng/gitee/shared_documents/nodejs/helloWorld", //工程所在目录
"runtimeExecutable": "npm",
"windows": { "runtimeExecutable": "npm.cmd" },
"runtimeArgs": [ "run", "debug" ],
"console": "integratedTerminal",
"protocol": "auto",
"restart": true,
"port": 9999
}
]
}
这样就可以直接使用 F5 对代码进行断点调试。
内置对象
- Application 代表着这个应用的全局对象,在一个应用中,只会实例化一个。挂载一些全局的方法和对象。
- Context 在每次收到用户请求的时候,会实例化一个context对象,这个对象封装了这次请求的信息,并提供了许多便捷的方法来获取请求参数和相应信息。
- Request 是一个请求级别的对象,继承自 Koa.Request。封装了 Node.js 原生的 HTTP Request 对象,提供了一系列辅助方法获取 HTTP 请求常用参数。
- Response 是一个请求级别的对象,继承自 Koa.Response。封装了 Node.js 原生的 HTTP Response 对象,提供了一系列辅助方法设置 HTTP 响应。
- Controller 框架提供的 Controller 基类,并推荐所有的 Controller 都继承于这个基类实现。
- Service 框架提供的 Service 基类,并推荐所有的 Service 都继承于这个基类实现。
- Helper 用来提供一些实用的 utility 函数。它的作用在于我们可以将一些常用的动作抽离在 helper.js 里面成为一个独立的函数,这样可以用 JavaScript 来写复杂的逻辑,避免逻辑分散各处,同时可以更好的编写测试用例。
- Config 通过这个对象可以读取到配置文件中配置的内容
- Logger 打印日志的对象
路由(Router)
路由描述请求 URL 和具体承担执行动作的 Controller 的对应关系 。框架约定/app/router.js
目录用于配置路由规则。
'use strict';
/**
* @param {Egg.Application} app - egg application
*/
module.exports = app => {
const { router, controller } = app;
router.get('/', controller.home.index);
};
Router 的完整定义
router.verb('path-match', app.controller.action);
router.verb('router-name', 'path-match', app.controller.action);
router.verb('path-match', middleware1, ..., middlewareN, app.controller.action);
router.verb('router-name', 'path-match', middleware1, ..., middlewareN, app.controller.action);
Controller
'use strict';
const Controller = require('egg').Controller;
class HomeController extends Controller {
async index() {
const { ctx } = this;
ctx.body = 'hi, egg';
}
}
module.exports = HomeController;
Controller 有以下属性
- ctx:当前请求的 Context 实例
- app:应用的 Application 实例
- config:应用的配置
- service:应用所有的 service
- logger:为当前 controller 封装的 logger 对象
Service
框架提供了 Service 基类,并推荐所有的 Service 都继承于这个基类实现。
例如定义一个获取 “hi, egg” 的 Service。
新建 /app/service/home.js
文件。遵循框架的约定
const Service = require("egg").Service;
class HomeService extends Service {
async getMessage() {
return "hi, egg";
}
}
module.exports = HomeService;
Controller 上调用 Service
const Service = require("egg").Service;
class HomeService extends Service {
async getMessage() {
return "hi, egg";
}
}
module.exports = HomeService;
配置(Config)
condif/config.default.js
文件如下:
'use strict';
module.exports = appInfo => {
const config = exports = {};
config.keys = appInfo.name + '_1573093135791_9959';
config.middleware = [];
const userConfig = {
// myAppName: 'egg',
cluster: {
listen: {
port: 9001
}
}
};
return {
...config,
...userConfig,
};
};
egg 还可以使用别的方式定义配置文件。例如直接把配置赋值给 module.exports
,这里不举例了。
程序执行之后会在run目录下打印出程序所有的配置项,方便检查配置项。
所有配置项都会加载到前文提及的 config 全局对象当中,无论在 Controller 和 Service 中都可以直接通过 this.config
获取到config对象。
例如
class HomeController extends Controller {
async index() {
console.log(this.config.cluster.listen.port);
}
}
module.exports = HomeController;
中间件(middleware)
在 /config/config.default.js
文件中添加配置:
config.middlewaredemo = {
name: "this is a middleware"
};
定义一个中间件 /app/middleware/middlewaredemo.js
module.exports = (options, app) => {
return async function middlewaredemo(ctx, next) {
console.log(app.name);
console.log(options.name);
console.log("enter middleware");
await next();
console.log("leave middleware");
}
}
两个参数:
options: 中间件的配置项,框架会将 app.config[${middlewareName}]
传递进来。
app:当前应用 Application 的实例。
应用当中使用中间件,需要在配置文件中 /config/config.default.js
添加配置
config.middleware = ["middlewaredemo"];
这样配置是对所有的请求都生效,针对某个请求,可以在路由中配置中间件。
module.exports = app => {
const { router, controller, config } = app;
const middlewaredemo = app.middleware.middlewaredemo(config.middlewaredemo, app);
router.get('/middlewaredemo', middlewaredemo, controller.home.index);
router.get('/', controller.home.index);
};
插件(Plugin)
以 mysql 为例引入插件。在 package.json
中加入
{
"dependencies": {
"egg-mysql": "^3.0.0"
}
}
在 /config/plugin.js
中声明
module.exports = {
mysql: {
enable: true,
package: 'egg-mysql'
}
};
plugin.js
中的每个配置项支持:
{Boolean} enable
- 是否开启此插件,默认为 true{String} package
-npm
模块名称,通过npm
模块形式引入插件{String} path
- 插件绝对路径,跟 package 配置互斥{Array} env
- 只有在指定运行环境才能开启,会覆盖插件自身package.json
中的配置
配置文件中添加配置
config.mysql = {
client: {
host: 'localhost',
port: '3306',
user: 'root',
password: '123456',
database: 'scm',
}
};
在service 层就可以做数据库的操作
async addUser() {
this.app.mysql.query('insert into user values(?)', "xiehuafeng");
}
正常开发过程当中,我们通常会使用 sequelize 作为数据库操作的框架。简单介绍他的使用。
代码根目录执行 npm install --save egg-sequelize mysql2
添加依赖。
在 /config/plugin.js
中声明
module.exports = {
sequelize: {
enable: true,
package: 'egg-sequelize',
}
};
有了这个框架之后,我们可以将数据库的操作独立为model层。
新建 app/model/user.js 文件,定义 User 对象和 数据表的映射关系。
module.exports = app => {
const {STRING, INTEGER, DATE} = app.Sequelize;
const User = app.model.define("user", {
name: STRING(64)
});
return User;
}
这样在做数据库操作可以直接操作 User 对象。
例如 上面例子可以直接改成
async addUser() {
this.ctx.model.User.save({"name": "xiehuafeng"});
}
定时任务 (Subscription)
在 app/schedule/
目录下定义定时任务文件,如,定义 echoTask.js
文件
const Subscription = require("egg").Subscription;
class echoTask extends Subscription {
static get schedule() {
return {
interval: "1s",
type: "all"
};
}
async subscribe() {
console.log("hello");
}
}
module.exports = echoTask;
或者使用下面这种写法
module.exports = app => {
return {
schedule: {
cron: "* * * * * *",
type: "all"
},
async task(ctx) {
console.log("hello");
}
}
}
自定义拓展
上文提到过框架本身自带一些全局对象,我们可以根据自己的需要对这些全局对象做一些拓展。
例如 helper 对象,我们要添加一个工具函数在这个对象上。
可以定义一个 app/extends/helper.js
文件
module.exports = {
sayHello() {
console.log("hello world");
}
}
这个对象会和 helper 的原型合并。
在别的地方可以通过下面方式调用这个函数。
this.ctx.helper.sayHello();
启动自定义
在应用启动和关闭的生命周期里预留的钩子函数,在项目启动或关闭期间我们可以通过这些函数对应用做一些动态的调整。
如下, 在 app.js 定义一个类:
class AppBootHook {
constructor(app) {
this.app = app;
}
//此时 config 文件已经被读取并合并,但是还未生效
//这是应用层修改配置的最后时机
//这个函数只支持同步调用
configWillLoad() {
console.log("configWillLoad");
}
//所有的配置已经加载完毕
configDidLoad() {
console.log("configWillLoad");
}
//所有文件已经加载完毕
//可以用来加载应用自定义的文件,启动自定义的服务
async didLoad() {
console.log("didload");
}
// 所有的插件都已启动完毕,但是应用整体还未 ready
// 可以做一些数据初始化等操作,这些操作成功才会启动应用
async willReady() {
console.log("willRedy");
}
// 应用已经启动完毕
async didReady() {
console.log("didReady");
}
// http / https server 已启动,开始接受外部请求
// 此时可以从 app.server 拿到 server 的实例
async serverDidReady() {
console.log("serverDidReady");
}
//应用关闭之前做些处理
async beforeClose() {
console.log('beforeClose');
}
}
module.exports = AppBootHook;