《2026年15天学完eggjs第9天》
只建立1 个连接(而不是 n 个)Agent 和 Worker直接通信(不经过 Master)用户代码感知不到多进程覆盖框架默认 ctx.helper,实现仅在模板渲染时生效的工具方法(如简化 HTML 安全处理)。/*** 封装安全 HTML 输出方法(自动添加 safe 标识,无需模板中手动加 | safe)* @param {string} str 待处理的 HTML 字符串* @retur
(多进程/View插件/生命周期)
一、多进程研发模式增强
(一)核心概念
1. Egg.js 多进程模型
Master(老板)→ 管理进程
├─ Agent(前台)→ 1个,管理长连接
└─ Worker(厨师)→ 多个,处理请求
2. 问题:为什么需要 cluster-client?
场景:多个 Worker 都要连接配置中心
❌ 不好:
Worker1 → 连接1
Worker2 → 连接2 } 浪费资源!
Worker3 → 连接3
Worker4 → 连接4
✅ 好:
Agent → 唯一的连接
Worker1,2,3,4 → 通过 Agent 获取数据
3. Leader/Follower 模式
-
Leader(Agent):负责真正的通信
-
Follower(Worker):把请求转发给 Leader
(二)三层架构(核心)
APIClient(可选)→ 提供同步方法、缓存
↓
ClusterClient(框架自动)→ 处理多进程通信
↓
DataClient(你写)→ 负责和远程服务通信
我们只需要写 DataClient部分的代码
(三)代码实现(三步走)
步骤 1:创建 DataClient
文件:`lib/registry_client.js`
const { Base } = require('sdk-base');
class RegistryClient extends Base {
constructor(options) {
super({ initMethod: 'init' });
this._registered = new Map();
}
async init() {
this.ready(true); // 标记准备好
}
// 订阅:监听变化
subscribe(reg, listener) {
this.on(reg.dataId, listener);
}
// 发布:通知所有订阅者
publish(reg) {
this._registered.set(reg.dataId, reg.data);
this.emit(reg.dataId, reg.data);
}
// 获取:读取数据
async getConfig(dataId) {
return this._registered.get(dataId) || [];
}
}
module.exports = RegistryClient;
关键 API:
-
`this.on(event, callback)` - 监听事件
-
`this.emit(event, data)` - 触发事件
-
`this.ready(true)` - 标记初始化完成
步骤 2:Agent 配置(Leader)
文件:`agent.js`
const RegistryClient = require('./lib/registry_client');
module.exports = (agent) => {
// 自动成为 Leader
agent.registryClient = agent.cluster(RegistryClient).create({});
agent.beforeStart(async () => {
await agent.registryClient.ready();
});
};
步骤 3:Worker 配置(Follower)
文件:`app.js`
const RegistryClient = require('./lib/registry_client');
module.exports = (app) => {
// 自动成为 Follower
app.registryClient = app.cluster(RegistryClient).create({});
app.beforeStart(async () => {
await app.registryClient.ready();
// 订阅配置
app.registryClient.subscribe(
{ dataId: 'demo.UserService' },
(data) => console.log('收到更新:', data)
);
});
};
神奇之处:代码几乎一样,但行为不同!
(四)工作流程(一图看懂)
1. Worker 调用:
app.registryClient.getConfig('xxx')
2. Follower 通过 socket 发给 Agent
3. Agent (Leader) 执行真正的操作
4. Agent 返回结果给 Worker
5. Worker 拿到结果
对用户来说:就像本地调用,感知不到多进程!
(五)关键知识点
(我在示例代码里不懂的地方)
-
constructor 和 super 是固定写法
constructor(options) {super({ initMethod: 'init' });} -
agent.cluster() 创建 Leader,app.cluster() 创建 Follower
-
事件机制跨进程
-
Agent 调用 `emit()` → Worker 的 `on()` 回调被触发
-
-
只支持异步方法(因为要通过 socket 通信)
⚠ 常见错误
// ❌ 忘记调用 ready
async init() {
// this.ready(true); // 必须调用!
}
// ❌ subscribe 传 Function
subscribe({ callback: () => {} }, listener); // 无法序列化
// ✅ 只传纯 JSON
subscribe({ dataId: 'xxx' }, listener);
(六)配置
// config/config.default.js
config.clusterClient = {
responseTimeout: 60000, // 超时时间(毫秒)
};
// 单独配置
agent.registryClient = agent
.cluster(RegistryClient, {
responseTimeout: 120000, // cluster-client 配置
})
.create({
serverUrl: 'xxx', // RegistryClient 参数
});
(七)调试技巧
// 查看进程 ID
console.log(`当前进程 PID: ${process.pid}`);
// 区分 Leader 和 Follower
subscribe(reg, listener) {
console.log(`[${process.pid}] subscribe called`);
}
(八)总结
核心价值
-
只建立 1 个连接(而不是 n 个)
-
Agent 和 Worker 直接通信(不经过 Master)
-
用户代码感知不到多进程
你需要做什么
-
写一个 DataClient(继承 Base)
-
实现 subscribe/publish/invoke 方法
-
在 agent.js 和 app.js 中调用 cluster()
框架帮你做什么
-
自动判断 Leader/Follower
-
自动处理 socket 通信
-
自动转发请求和结果
项目文件
egg-hello/
├── lib/registry_client.js ← DataClient
├── agent.js ← Leader 配置
├── app.js ← Follower 配置
└── app/controller/config.js ← 使用示例
二、View 插件开发
(一)核心背景与定位
框架不强制绑定特定模板引擎,支持开发者按需封装模板引擎插件(如 EJS、Nunjucks),本文以 egg-view-ejs 为例,阐述 View 插件的开发规范与核心实现逻辑。
(二)插件基础规范
1. 命名规范
-
插件 npm 包名需以 egg-view- 开头(如 egg-view-ejs、egg-view-nunjucks);
-
package.json 中 eggPlugin.name 以模板引擎名命名(如 ejs、nunjucks);
-
配置项也以模板引擎名命名(如 exports.ejs = {})。
2. 目录结构(以 egg-view-ejs 为例)
egg-view-ejs/
├── config/ # 插件配置文件
│ ├── config.default.js # 默认配置(必选)
│ └── config.local.js # 本地环境配置(可选)
├── lib/ # 核心逻辑目录
│ ├── view.js # View 基类实现(核心)
│ └── helper.js # 自定义 helper(可选)
├── app.js # 插件初始化(可选)
├── test/ # 单元测试目录
├── History.md # 版本变更记录
├── README.md # 使用文档
└── package.json # 包配置(必选)
3. package.json 核心配置
{
"name": "egg-view-ejs",
"eggPlugin": {
"name": "ejs", // 插件名(模板引擎名)
"dep": ["security"] // 依赖插件(如安全插件,可选)
},
"keywords": ["egg", "egg-plugin", "egg-view", "ejs"] // 索引关键字
}
(三)核心实现:View 基类
1. 核心要求
View 基类需在每次请求时实例化,必须提供 render(渲染文件)和 renderString(渲染模板字符串)两个方法,支持:
-
Generator 函数
-
Async 函数
-
返回 Promise 的普通函数
2. 基础实现示例(egg-view-ejs)
const ejs = require('ejs');
module.exports = class EjsView {
/**
* 渲染模板文件
* @param {string} filename 完整文件路径(框架已校验文件存在)
* @param {Object} locals 渲染数据(含 app.locals/ctx.locals/内置对象)
* @param {Object} viewOptions 自定义配置(覆盖默认配置)
* @returns {Promise<string>} 渲染后的字符串
*/
render(filename, locals, viewOptions) {
// 合并配置(默认配置 → 自定义配置 → 文件路径)
const config = Object.assign({}, this.config, viewOptions, { filename });
return new Promise((resolve, reject) => {
// 调用 ejs 原生渲染文件方法
ejs.renderFile(filename, locals, config, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
}
/**
* 渲染模板字符串
* @param {string} tpl 模板字符串(无文件路径)
* @param {Object} locals 渲染数据(同 render)
* @param {Object} viewOptions 自定义配置(同 render)
* @returns {Promise<string>} 渲染后的字符串
*/
renderString(tpl, locals, viewOptions) {
// 合并配置(关闭缓存,避免模板字符串缓存冲突)
const config = Object.assign({}, this.config, viewOptions, { cache: null });
try {
// 调用 ejs 原生渲染字符串方法
return Promise.resolve(ejs.render(tpl, locals, config));
} catch (err) {
return Promise.reject(err);
}
}
};
3. 参数说明
|
方法 |
参数 |
说明 |
|---|---|---|
|
render |
filename |
完整文件路径,框架已校验文件存在,无需额外处理 |
|
locals |
渲染数据,来源:app.locals、ctx.locals、render 入参;内置 ctx/request/helper |
|
|
viewOptions |
自定义配置,可覆盖模板引擎默认配置(如关闭缓存) |
|
|
renderString |
tpl |
模板字符串,无文件路径 |
|
locals |
同 render 的 locals |
|
|
viewOptions |
同 render 的 viewOptions |
(四)插件配置
1. 配置文件编写
配置项以模板引擎名命名,配置内容对应模板引擎自身的配置项(如 EJS 的缓存配置):
// config/config.default.js
module.exports = {
// EJS 模板引擎配置
ejs: {
cache: true, // 默认开启缓存
// 可扩展其他 EJS 配置(如 delimiter: '?' 自定义分隔符)
},
};
2. 配置优先级
自定义 viewOptions(render 入参) > 插件默认配置 > 模板引擎原生默认配置。
(五)自定义 Helper(模板渲染专用)
1. 应用场景
覆盖框架默认 ctx.helper,实现仅在模板渲染时生效的工具方法(如简化 HTML 安全处理)。
2. 实现步骤
(1)定义 Helper 子类
// lib/helper.js
module.exports = (app) => {
return class ViewHelper extends app.Helper {
/**
* 封装安全 HTML 输出方法(自动添加 safe 标识,无需模板中手动加 | safe)
* @param {string} str 待处理的 HTML 字符串
* @returns {string} 安全的 HTML 字符串
*/
shtml(str) {
// safe 由 egg-view-nunjucks 注入,标记内容无需转义
return this.safe(super.shtml(str));
}
};
};
(2)渲染时注入自定义 Helper
// lib/view.js
const ViewHelper = require('./helper');
module.exports = class EjsView {
render(filename, locals, viewOptions) {
// 替换 locals 中的 helper 为自定义 ViewHelper 实例
locals.helper = new ViewHelper(this.ctx);
// 原有渲染逻辑...
const config = Object.assign({}, this.config, viewOptions, { filename });
return new Promise((resolve, reject) => {
ejs.renderFile(filename, locals, config, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
}
// renderString 同理...
};
(六)安全相关配置
1. 依赖安全插件
在 package.json 中声明对 @eggjs/security 的依赖,确保模板安全方法可用:
{
"eggPlugin": {
"name": "nunjucks",
"dep": ["security"] // 强依赖 security 插件
}
}
2. 框架内置安全方法
-
app.injectCsrf():注入 CSRF Token 到模板;
-
app.injectNonce():注入 Nonce 随机数(用于 CSP 安全策略);
-
helper.shtml():清洗 HTML 片段,防止 XSS 攻击。
(七)插件启用方式
1. 本地插件(开发调试)
// 应用端 config/plugin.js
exports.ejs = {
enable: true,
path: require('path').join(__dirname, '../egg-view-ejs'), // 本地插件路径
};
2. npm 安装插件(生产环境)
// 应用端 config/plugin.js
exports.ejs = {
enable: true,
package: 'egg-view-ejs', // npm 包名
};
(八)总结
-
View 插件核心是实现 render/renderString 方法的 View 基类,需兼容异步/同步渲染逻辑;
-
命名规范需统一(egg-view-前缀 + 模板引擎名),配置项与模板引擎名对齐;
-
可通过自定义 Helper 简化模板使用,同时依赖 security 插件保障渲染安全;
-
插件启用支持本地路径和 npm 包两种方式,适配开发与生产环境。
三、升级生命周期事件函数
(一)核心背景
为简化应用/插件加载时机的控制逻辑,Egg.js 对 Loader 生命周期函数做了精简,废弃函数形式(仅兼容保留),推荐使用类形式,本文聚焦核心生命周期函数的替换规则与实操写法。
(二)核心生命周期函数替换规则
1. beforeStart 函数(已作废)
|
使用场景 |
旧写法(函数形式) |
新写法(类形式) |
|---|---|---|
|
插件开发 |
app.beforeStart → 迁移至 didLoad 方法 |
类中的 didLoad 方法 |
|
应用开发 |
app.beforeStart → 迁移至 willReady 方法 |
类中的 willReady 方法 |
旧写法示例
import type { Application } from 'egg';
export default (app: Application) => {
app.beforeStart(async () => {
// 原初始化逻辑
});
};
新写法示例
// app.ts 或 agent.ts
import type { Application, ILifecycleBoot } from 'egg';
export default class AppBootHook implements ILifecycleBoot {
private readonly app: Application;
constructor(app: Application) {
this.app = app;
}
// 插件:原 beforeStart 逻辑移至此
async didLoad() {}
// 应用:原 beforeStart 逻辑移至此
async willReady() {}
}
2. ready 函数(已作废)
|
旧写法(函数形式) |
新写法(类形式) |
|---|---|
|
app.ready → 迁移至 didReady 方法 |
类中的 didReady 方法 |
旧写法示例
import type { Application } from 'egg';
export default (app: Application) => {
app.ready(async () => {
// 原 ready 逻辑
});
};
新写法示例
// app.ts 或 agent.ts
import type { Application, ILifecycleBoot } from 'egg';
export default class AppBootHook implements ILifecycleBoot {
private readonly app: Application;
constructor(app: Application) {
this.app = app;
}
async didReady() {
// 原 app.ready 逻辑移至此
}
}
3. beforeClose 函数(已作废)
|
旧写法(函数形式) |
新写法(类形式) |
|---|---|
|
app.beforeClose → 迁移至 beforeClose 方法 |
类中的 beforeClose 方法 |
旧写法示例
import type { Application } from 'egg';
export default (app: Application) => {
app.beforeClose(async () => {
// 原 beforeClose 逻辑
});
};
新写法示例
// app.ts 或 agent.ts
import type { Application, ILifecycleBoot } from 'egg';
export default class AppBootHook implements ILifecycleBoot {
private readonly app: Application;
constructor(app: Application) {
this.app = app;
}
async beforeClose() {
// 原 app.beforeClose 逻辑移至此
}
}
(三)关键说明
-
类形式钩子写在 app.ts(应用进程)或 agent.ts(代理进程)中;
-
必须实现 ILifecycleBoot 接口,构造函数接收 app 实例,方法支持 async/await。
(四)插件启用(精简版)
// 本地插件
exports.hello = { enable: true, path: require('path').join(__dirname, '../egg-hello') };
// npm插件
exports.mysql = { enable: true, package: 'egg-mysql' };
(五)总结
-
生命周期函数推荐类形式,废弃函数形式(仅兼容);
-
核心替换:
-
插件 beforeStart → didLoad;
-
应用 beforeStart → willReady;
-
app.ready → didReady;
-
app.beforeClose → beforeClose;
-
-
类需实现 ILifecycleBoot,构造函数接收 app,方法支持异步;
-
插件启用分本地路径和 npm 包两种简洁写法。
更多推荐



所有评论(0)