Egg.js学习心得

一、Egg.js 基本概念

1.Egg.js 是什么

https://eggjs.org/

Egg.js 是基于 Node.jsKoa 的企业级 Web 框架,由阿里团队开源,专注于约定优于配置可扩展性

Egg.js 的特点:

  • 基于Koa,支持 async/await,性能优异
  • 强调规范与工程化,框架稳定,测试覆盖率高
  • 适合中大型Web应用和API服务
  • 渐进式开发
  • 提供基于Egg定制上层框架的能力
  • 高度可拓展的插件机制

2.Egg.js 的核心思想

Egg.js 采用 MVC 架构,将业务逻辑进行分层管理,提高代码可维护性。

主要分层包括:

  • Controller(控制器)
  • Service(业务逻辑)
  • Model(数据模型)
  • Router(路由)
  • Middleware(中间件)

二、项目结构

1.安装&快速启动

mkdir egg-example && cd egg-example
npm init egg --type=simple
npm i
npm run dev
open http://localhost:7001

2.基本目录结构

Egg.js 项目初始化后,通常包含如下目录结构:

egg-project
├── package.json -- 框架配置,依赖
├── package-lock.json -- 依赖锁定文件
├── jsconfig.json -- JavaScript 项目配置
├── README.md -- 项目说明文档
├── app -- 源码目录
│ ├── router.js -- 用于配置 URL 路由规则
│ ├── controller -- 用于解析用户的输入,处理后返回相应的结果
│ │ └── home.js
│ ├── public -- 用于放置静态资源(可选)
│ ├── model -- 用于放置领域模型(可选)
│ ├── service -- 用于编写业务逻辑层(可选)
│ ├── middleware -- 用于编写中间件(可选)
│ ├── schedule -- 用于定时任务(可选)
│ ├── view -- 用于放置模板文件(可选)
│ └── extend -- 用于框架的扩展(可选)
├── config -- 配置文件
│ ├── plugin.js -- 用于配置需要加载的插件
│ ├── config.default.js -- 用于编写配置文件
│ ├── config.prod.js -- 生产环境配置(可选)
│ ├── config.test.js -- 测试环境配置(可选)
│ ├── config.local.js -- 本地开发环境配置(可选)
│ └── config.unittest.js -- 单元测试环境配置(可选)
├── test -- 测试文件
│ └── app
│ └── controller -- 用于controller层的单元测试
│ └── home.test.js
├── logs -- 日志文件
│ └── npm i/ -- 运行时日志
├── run -- 运行时生成的文件
│ ├── router.json -- 路由配置缓存
│ ├── agent_config.json -- Agent 配置
│ └── application_config.json -- 应用配置
├── typings -- TypeScript 类型定义(可选)
│ ├── app/
│ │ ├── controller/
│ │ │ └── index.d.ts
│ │ └── index.d.ts
│ └── config/
│ ├── index.d.ts
│ └── plugin.d.ts
├── node_modules -- 依赖包(自动生成)
└── .vscode -- VS Code 配置(可选)
└── settings.json -- 编辑器设置

三、路由(Router

1.路由的作用

路由用于定义URL与控制器方法之间的映射关系

2.路由定义方式

在app/router.js 中定义路由:

import { Application } from 'egg';

export default (app: Application) => {
const { controller, router } = app;

router.get('/', controller.home.index);
router.get('/login', controller.admin.login);
// 路由地址+调用的方法
};

源码router对象中有很多HTTP方法,从egg中解构出Application,这是一个接口,对传入的参数做出约束(启动文件在egg包中,使用过程基本不用动);从传入的app中再解构出controller和router,router就是路由对象

img

3.HTTP请求方式

Egg.js支持常见HTTP方法:

  • GET
  • POST
  • PUT
  • DELETE

其中GET与POST是最常用的请求方式,也是实际开发中最容易遇到参数获取、跨域、安全问题的部分。

(1)GET 请求

① GET 请求的特点
  • 参数通过 URL 查询****字符串 传递
  • 参数会直接暴露在地址栏
  • 请求长度有限制
  • 适合用于 查询数据

示例 URL:

http://localhost:7001/user?id=1&name=tom
② Egg.js 中 GET 参数获取方式

在 Controller 中通过 ctx.query 或 ctx.request.query 获取:

async index() {
const id = this.ctx.query.id;
const name = this.ctx.query.name;
this.ctx.body = { id, name };
}
③ GET 请求可能存在的问题
  • 参数明文暴露,不适合传递敏感信息
  • 容易被浏览器缓存
  • 容易被恶意篡改参数

(2)POST 请求

① POST 请求的特点
  • 参数放在 请求体(body)中
  • 不会显示在 URL 中
  • 参数长度限制较小
  • 适合用于 新增、修改数据
② Egg.js 中 POST 参数获取方式

需要先确保已开启 bodyParser(Egg.js 默认开启):

async create() {
const username = this.ctx.request.body.username;
const password = this.ctx.request.body.password;
this.ctx.body = 'success';
}

前端发送 POST 请求示例(JSON):

fetch('/user', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: 'tom',
password: '123456'
})
});
③ POST请求可能存在的问题
  • 请求体可能被篡改
  • 容易受到CSRF****攻击
  • 参数校验不严谨会导致安全漏洞

4.GET 与 POST 对比总结

对比项 GET POST
参数位置 URL 请求体
安全性 较低 相对较高
是否缓存 不会
使用场景 查询 新增 / 修改

四、跨域问题(CORS

1.什么是跨域

协议、域名或****端口任意一个不同,浏览器就会认为是跨域请求。

示例:

前端:http://localhost:3000
后端:http://localhost:7001

2.Egg.js 解决跨域方式

使用egg-cors插件

安装插件:

npm install egg-cors

启用插件(config/plugin.js):

exports.cors = {
enable: true,
package: 'egg-cors',
};

配置跨域规则(config/config.default.js):

exports.cors = {
origin: '*',
allowMethods: 'GET,POST,PUT,DELETE'
};

五、安全问题

1.CSRF攻击

(1)什么是CSRF

攻击者利用用户已登录状态,伪造请求对服务器进行操作,常发生在POST请求中。

(2)CSRF的特点

  • 攻击一般发起在第三方网站,而不是被攻击的网站。被攻击的网站无法防止攻击发生。
  • 攻击利用受害者在被攻击网站的登录凭证,冒充受害者提交操作;而不是直接窃取数据。
  • 整个过程攻击者并不能获取到受害者的登录凭证,仅仅是“冒用”。
  • 跨站请求可以用各种方式:图片URL、超链接、CORS、Form提交等等。部分请求方式可以直接嵌入在第三方论坛、文章中,难以进行追踪。

(3)Egg.js的CSRF防护

Egg.js默认开启 CSRF 防护,POST请求需要携带 token。

// config/config.default.js
exports.security = {
csrf: {
enable: true,
// 可根据需要配置忽略的 API 路径(如开放接口)
ignore: [],
},
};

2.XSS攻击

(1)什么是 XSS

攻击者通过注入恶意脚本,窃取用户信息或篡改页面。

(2)防护方式

  • 对用户输入进行转义
  • 不信任任何前端传来的数据

**3.**SQL注入

(1)产生原因

直接拼接 SQL 语句,导致恶意注入。

(2)防护方式

  • 使用 ORM(如 Sequelize)
  • 使用参数化查询

六、控制器(Controller

1.Controller的作用

Controller用于接收请求参数、调用 Service、返回响应结果

2.Controller基本写法

控制器文件一般放在app/controller目录下:

const { Controller } = require('egg');

class HomeController extends Controller {
async index() {
const { ctx } = this;
ctx.body = 'hi, egg';
}
}

module.exports = HomeController;

3.ctx 对象

ctx是请求上下文对象,包含:

  • ctx.request:请求信息
  • ctx.response:响应信息
  • ctx.body:响应内容
  • ctx.query:GET 参数
  • ctx.request.body:POST 参数

这里注意,egg基于Koa,Koa是ctx.xxx,在egg中ctx被封装在this里面,使用解构赋值解析出来,如果不使用解构就要使用this.ctx.xxx

4.获取传值

get类型

通过ctx.query获取参数,比如

export default class AdminController extends Controller {
public async index() {
const { ctx } = this;
const userName: string = ctx.query.name;
ctx.body = await ctx.service.login.welcome(userName);
}
}

动态路由

从url中获取数据,但不是使用query的形式,而是使用http://127.0.0.1:7001/user/123这种形式

在配置路由时添加参数,例如router.get(‘/user/:id’, controller.admin.user);

然后在Controller中使用ctx.params获取参数,如

public async user() {
const { ctx } = this;
const id: string = ctx.params.id;
ctx.body = await ctx.service.login.userCenter(id);
}

七、Service(业务逻辑)

1.Service的作用

Service用于封装复杂业务逻辑,避免 Controller 代码臃肿。

好处:

  1. 保持 Controller 中的逻辑更加简洁。
  2. 保持业务逻辑的独立性,抽象出来的 Service 可以被多个 Controller 重复调用。
  3. 将逻辑和展现分离,更容易编写测试用例

2.Service示例

Service文件放在 app/service 目录下:

const Service = require('egg').Service;

class UserService extends Service {
async getUser() {
return { name: 'Tom', age: 18 };
}
}

module.exports = UserService;

Controller 中调用 Service:

const user = await this.ctx.service.user.getUser();

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

  • this.ctx:当前请求的上下文Context 对象的实例,通过它我们可以拿到框架封装好的处理当前请求的各种便捷属性和方法。
  • this.app:当前应用Application****对象的实例,通过它我们可以拿到框架提供的全局对象和方法。
  • this.service:应用定义的Service,通过它我们可以访问到其他业务层,等价于 this.ctx.service。
  • this.config:应用运行时的配置项
  • this.logger:logger对象,上面有四个方法(debug、info、warn、error),分别代表打印四个不同级别的日志。使用方法和效果与context logger中介绍的一样,但是通过这个logger对象记录的日志,在日志前面会加上打印日志的文件路径,以便快速定位日志打印位置。

3.使用场景

  1. 复杂数据的处理,比如要展现的信息需要从数据库获取,还要经过一定的规则计算,才能返回用户显示。或者计算完成后,更新到数据库。
  2. 第三方服务的调用

4.注意事项

  • 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获取到当前请求的上下文。

八、中间件Middleware

Egg的中间件形式和Koa的中间件形式是一样的,都是基于洋葱圈模型。

一个中间件是一个放置在app/middleware目录下的单独文件,它需要exports一个普通的function,接受两个参数:

  • options:中间件的配置项,框架会将app.config[${middlewareName}] 传递进来。
  • app:当前应用Application的实例。

1.中间件的作用

中间件用于在请求到达Controller前或响应返回前做统一处理,如:

  • 日志
  • 权限校验
  • 错误处理

2.中间件示例

在app/middleware目录下创建中间件:

这个中间件的作用是每次请求时打印出时间

import { Context } from 'egg';

// 自定义的中间件
export default function printDate(): any {
return async (ctx: Context, next: () => Promise<any>) => {
ctx.logger.info(new Date());
await next();
};
}

中间件写完之后还需要在config里进行配置,在middleware数组中添加文件名

config.middleware = ['printdate']

3.中间件传值

中间件传值在配置文件中bizConfig中增加中间件同名对象,就可以在中间件中通过options来访问变量

const bizConfig = {
// 中间件传值
printdate: {
configStr: 'this is value from config',
},
};

中间件也需要配置参数

import { Context, Application, EggAppConfig } from 'egg';

// 自定义的中间件
export default function printDate(options: EggAppConfig['printdate'], app: Application): any {
app.logger.warn(options.configStr);
return async (ctx: Context, next: () => Promise<any>) => {
ctx.logger.info(new Date());
await next();
};
}

九、Cookie

Cookie是存储在访问者计算机中的变量,可以让同一个浏览器访问同一个域名的时候共享数据

在Egg.js中,Cookie的操作主要通过ctx.cookies完成。

1.设置Cookie

(1)基本设置方式

在Controller或Service中可以通过ctx.cookies.set设置Cookie:

this.ctx.cookies.set('username', 'tom');

默认情况下:

  • Cookie 为 会话级别
  • 浏览器关闭后失效

常用配置项如下:

this.ctx.cookies.set('token', 'abc123', {
maxAge: 24 * 60 * 60 * 1000, // 有效期(毫秒)
httpOnly: true, // 仅服务器可访问,防止 XSS
signed: true, // 是否加签
});

常见参数说明:

  • maxAge:Cookie 存活时间
  • httpOnly:是否禁止前端 JS 读取(推荐开启)
  • signed:是否启用签名,防止篡改
  • path:Cookie 生效路径
  • secure:仅 HTTPS 传输(生产环境推荐)
const username = this.ctx.cookies.get('username');

如果 Cookie 设置时使用了 signed: true,获取时也必须指定:

const token = this.ctx.cookies.get('token', {
signed: true,
});

签名 Cookie 会对值进行加密校验,防止客户端篡改。

(2)配置 keys

要使用签名 Cookie,必须在 config/config.default.js 中配置 keys:

exports.keys = 'your-secret-key';

如果 keys 未配置,使用 signed Cookie 会报错。

删除 Cookie 本质是设置一个 过期时间

this.ctx.cookies.set('username', null);

或者显式设置 maxAge 为 0:

this.ctx.cookies.set('username', null, {
maxAge: 0,
});
  1. 敏感信息不要明文存 Cookie
  2. 登录态建议:
    1. Cookie + Session
    2. Cookie + JWT
  3. 推荐开启:
    1. httpOnly: true
    2. signed: true
  4. 生产环境建议开启:
    1. secure: true
  • Cookie 存在于 客户端
  • Session 存在于 服务端
  • Egg.js 默认使用 Cookie 存 Session ID
// 登录成功后
this.ctx.cookies.set('userId', user.id, {
maxAge: 7 * 24 * 60 * 60 * 1000,
httpOnly: true,
signed: true,
});
// 后续请求中获取
const userId = this.ctx.cookies.get('userId', {
signed: true,
});

十、Session

Cookie 在 Web 应用中经常承担标识请求方身份的功能,所以 Web 应用在 Cookie 的基础上封装了 Session 的概念,专门用做用户身份识别

Session保存在服务器上

Egg 中使用 Session

框架内置了 Session 插件,给我们提供了 ctx.session 来访问或者修改当前用户的 Session。

class HomeController extends Controller {
async fetchPosts() {
const ctx = this.ctx;

// 获取 Session 上的内容
const userId = ctx.session.userId;
const posts = await ctx.service.post.fetch(userId);

// 修改 Session 的值
ctx.session.visited = ctx.session.visited
? (ctx.session.visited + 1)
: 1;

ctx.body = {
success: true,
posts,
};
}
}

Session 的使用方法非常直观,直接读取它或者修改它就可以了

如果要删除它,直接将它赋值为 null:

ctx.session = null;

可以直接在config中配置session信息

exports.session = {
key: 'EGG_SESS',
maxAge: 24 * 3600 * 1000, // 1
httpOnly: true,
encrypt: true,
};

十、配置文件(Config)

1.配置文件位置

配置文件位于config目录:

  • config.default.js:默认配置
  • config.{env}.js:环境配置

2.配置示例

module.exports = {
keys: 'egg-secret-key',
};

十一、日志(logger)

logger有四种级别:

this.logger.debug('this is degug');
this.logger.info('this is info');
this.logger.warn('this is warn');
this.logger.error('this is error');

十二、插件(Plugin)

1.插件的作用

插件用于 引入第三方功能模块,如数据库、Redis 等。

2.插件配置

在 config/plugin.js 中启用插件:

exports.mysql = {
enable: true,
package: 'egg-mysql',
};