(多进程/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 拿到结果

对用户来说:就像本地调用,感知不到多进程!

(五)关键知识点

(我在示例代码里不懂的地方)

  1. constructor 和 super 是固定写法constructor(options) { super({ initMethod: 'init' }); }

  2. agent.cluster() 创建 Leader,app.cluster() 创建 Follower

  3. 事件机制跨进程

    1. Agent 调用 `emit()` → Worker 的 `on()` 回调被触发

  4. 只支持异步方法(因为要通过 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)

  • 用户代码感知不到多进程

你需要做什么

  1. 写一个 DataClient(继承 Base)

  2. 实现 subscribe/publish/invoke 方法

  3. 在 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 包名
};

(八)总结

  1. View 插件核心是实现 render/renderString 方法的 View 基类,需兼容异步/同步渲染逻辑;

  2. 命名规范需统一(egg-view-前缀 + 模板引擎名),配置项与模板引擎名对齐;

  3. 可通过自定义 Helper 简化模板使用,同时依赖 security 插件保障渲染安全;

  4. 插件启用支持本地路径和 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 逻辑移至此
  }
}

(三)关键说明

  1. 类形式钩子写在 app.ts(应用进程)或 agent.ts(代理进程)中;

  2. 必须实现 ILifecycleBoot 接口,构造函数接收 app 实例,方法支持 async/await。

(四)插件启用(精简版)

// 本地插件
exports.hello = { enable: true, path: require('path').join(__dirname, '../egg-hello') };
// npm插件
exports.mysql = { enable: true, package: 'egg-mysql' };

(五)总结

  1. 生命周期函数推荐类形式,废弃函数形式(仅兼容);

  2. 核心替换:

    1. 插件 beforeStart → didLoad;

    2. 应用 beforeStart → willReady;

    3. app.ready → didReady;

    4. app.beforeClose → beforeClose;

  3. 类需实现 ILifecycleBoot,构造函数接收 app,方法支持异步;

  4. 插件启用分本地路径和 npm 包两种简洁写法。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐