Sails 应用热重载:sails.reloadActions() 动作注册机制深度解析
【免费下载链接】sailsRealtime MVC Framework for Node.js项目地址: https://gitcode.com/gh_mirrors/sa/sails
本文聚焦 Sails 框架(Realtime MVC Framework for Node.js)中用于开发期热重载动作(actions)的核心 API ——
sails.reloadActions()。它允许应用在运行时清空并重新加载全部动作:既包括各 hook(如 blueprints、security)通过registerActions()注册的动作,也包括api/controllers目录下从磁盘加载的传统控制器与独立动作文件。读完本文,你将掌握该 API 的调用签名、hooksToSkip选项的用法、底层实现调用链(源码 lib/app/reload-actions.js)、它与registerAction()的分工,以及开发场景中的安全边界。
一、为什么需要 reloadActions:从静态加载到开发期热重载
Sails 应用在启动(lift/load)过程中会经历一次完整的动作注册流程:
- hook 阶段:各 hook 在初始化时调用自身的
registerActions()方法注册动作。例如 blueprints hook 会为每个模型注册find、create、update、destroy、populate、add、remove、replace、findOne等 CRUD 动作(见 lib/hooks/blueprints/index.js),security hook 也会注册自己的动作(见 lib/hooks/security/index.js)。 - 磁盘加载阶段:从
api/controllers目录加载传统控制器(PascalCase 且以Controller结尾的字典式文件)和 kebab-case 命名的独立动作文件(见 lib/app/private/controller/load-action-modules.js)。 - 注册入
sails._actions:所有动作最终被写入应用实例的私有动作字典sails._actions。
问题在于:应用一旦启动,这一套动作注册流程默认不会再次执行。当开发者修改了控制器或动作文件,又不想重启整个 Sails 进程(重启会丢失内存状态、拖慢开发循环)时,就需要一个在运行期重新执行注册流程的入口——这就是sails.reloadActions()的设计初衷。
文档明确标注该 API仍处于实验阶段(experimental),其接口与行为在后续版本中可能发生变化,使用时需留意版本兼容性。
二、API 签名与参数说明
reloadActions()有两种调用形式(见 docs/reference/application/advanced-usage/sails.reloadActions.md):
sails.reloadActions(cb);或者:
sails.reloadActions(options, cb);参数表如下:
| 序号 | 参数 | 类型 | 说明 |
|---|---|---|---|
| 1 | options | ((dictionary?)) | 可选。目前仅接受一个键hooksToSkip,值为一个数组,列出不应该调用其registerActions()方法的 hook 名称 |
| 2 | cb | ((function)) | 回调函数,在动作重载完成后被调用(错误优先,即function(err)形式) |
2.1 无 options 的简写形式
直接传回调即可,此时等价于sails.reloadActions({}, cb)。源码中通过_.isFunction(options)判断是否省略了第一个参数(见 lib/app/reload-actions.js)。
2.2 options.hooksToSkip 的作用
hooksToSkip用于在重载时排除指定 hook的动作注册逻辑:
- 未提供时,
hooksToSkip默认为空数组,所有已加载 hook 都会被遍历; - 提供时,
_.difference(_.keys(sails.hooks), hooksToSkip)会计算「全部已加载 hook 减去待跳过 hook」的差集,只有差集中的 hook 会执行其registerActions()(见 lib/app/reload-actions.js)。
典型的应用场景:某个自定义 hook(如向sails._actions注册custom-action)持有特殊的内存状态,开发者希望在重载动作时保留其注册结果,避免被重新注册或覆盖。
三、底层实现调用链逐段拆解
reloadActions方法被挂载到Sails.prototype上,其定义位于 lib/app/reload-actions.js,并在 lib/app/Sails.js 中以Sails.prototype.reloadActions = require('./reload-actions');方式绑定。整个执行流程分四步:
步骤 1:参数归一化与差集计算
// Allow for options to be left out. if (_.isFunction(options)) { cb = options; options = {}; } // Default options to an empty dictionary. else if (!_.isObject(options)) { options = {}; } // Default `hooksToSkip` to an empty array. var hooksToSkip = options.hooksToSkip || []; // The list of hooks we want to reload is the list of all hooks minus the hooks to skip. var hooksToReload = _.difference(_.keys(sails.hooks), hooksToSkip);(摘自 lib/app/reload-actions.js)
步骤 2:清空动作字典
// Clear the actions dictionary. sails._actions = {};sails._actions是应用的全部动作注册表,这一步意味着重载是全量重建而非增量合并——所有既有动作(含 hooks 注册的和磁盘加载的)都会被清空后重新注册。
步骤 3:并行执行各 hook 的 registerActions()
async.each(hooksToReload, function(hookIdentity, next) { if (_.isFunction(sails.hooks[hookIdentity].registerActions)) { sails.hooks[hookIdentity].registerActions(next); } else { return next(); } }, function doneReloadingActions(err) { if (err) {return cb(err);} // Reload the controller actions. loadActionModules(sails, cb); });(摘自 lib/app/reload-actions.js)
值得注意的实现细节:
- 只有暴露了
registerActions方法的 hook才会被重新调用;没有该方法(如被禁用的 grunt、views 等 hook)的 hook 直接跳过; - 多个 hook 之间使用
async.each并行执行registerActions(),各 hook 注册动作完成后统一回到doneReloadingActions; - 若任一 hook 注册出错,直接以错误调用回调
cb(err),不再继续磁盘加载; - blueprints hook 的
registerActions()会遍历sails.models,为每个模型重新注册整套 CRUD 动作(lib/hooks/blueprints/index.js),这也是「重载后蓝图动作仍可用」的机制保证。
步骤 4:重新加载磁盘控制器与动作文件
hook 阶段完成后,调用loadActionModules(sails, cb)重新扫描api/controllers目录(默认路径可经sails.config.paths.controllers覆盖,见 lib/app/private/controller/load-action-modules.js)。该函数内部:
- 使用
includeAll.optional递归读取控制器目录下的所有文件; - 通过两个正则区分文件类型:
- 传统控制器:
^((?:(?:.*)/)*([0-9A-Z][0-9a-zA-Z_]*))Controller\..+$(PascalCase 且以Controller结尾); - 独立动作文件:
^((?:(?:.*)/)*([a-z][a-z0-9-]*))\..+$(kebab-case);
- 传统控制器:
- 对传统控制器字典中的每个方法、每个独立动作文件,调用
helpRegisterAction()完成注册; - 若发现同名动作冲突(如磁盘文件与已注册动作 identity 相同),会抛出
E_CONFLICT错误; - 对既不像控制器又不像动作的「垃圾」文件,记录
sails.log.warn警告并忽略; - 最后将
sails.config.controllers.moduleDefinitions中定义的动作以force=true覆盖式注册(见 lib/app/private/controller/load-action-modules.js)。
而helpRegisterAction()(lib/app/private/controller/help-register-action.js)负责最终写入sails._actions:函数直接存入,actions2(machine)定义则通过machine-as-action编译成可调用函数,并统一打上_middlewareType标记便于日志诊断。
四、与 sails.registerAction() 的分工协作
理解reloadActions()之前,需要先分清它与 sails.registerAction() 的关系:
| 能力 | sails.registerAction() | sails.reloadActions() |
|---|---|---|
| 作用对象 | 单个动作 | 全部动作(hooks + 磁盘) |
| 是否清空已有动作 | 否,可叠加 | 是,先清空sails._actions再重建 |
| 是否读取磁盘 | 否,直接注册传入的函数/定义 | 是,重新扫描api/controllers |
| 主要用途 | 运行时按需注册/覆盖单个动作 | 开发期整体热重载 |
从源码看,blueprints hook 的registerActions()内部正是通过多次调用sails.registerAction(BlueprintController.create, modelIdentity + '/create')等语句逐个注册(lib/hooks/blueprints/index.js),可见二者是「批量重载入口」与「单点注册原语」的关系。
五、典型使用场景与代码示例
场景 A:修改控制器后整体热重载
开发时修改了api/controllers/TopLevelController.js,希望不重启进程就让改动生效:
// 某个开发工具/路由中触发 sails.reloadActions(function(err) { if (err) { sails.log.error('Failed to reload actions:', err); return; } sails.log.info('All actions reloaded successfully.'); });场景 B:重载时保留某个自定义 hook 的动作
自定义 hookmyHook向sails._actions注册了custom-action,重载时不想让它被清掉再重建(或该 hook 需要保留内存状态):
sails.reloadActions({ hooksToSkip: ['myHook'] }, function(err) { if (err) { /* 处理错误 */ } // 此时 custom-action 仍保留,其余动作已全部重建 });场景 C:配合 registerAction 使用
// 先注册一个自定义动作 sails.registerAction(function(req, res) { return res.json({ ok: true }); }, 'ping'); // 再整体重载:注意 reloadActions 会清空 _actions, // 因此自定义动作若由 hook 注册,应通过 hooksToSkip 保留 sails.reloadActions({ hooksToSkip: ['myHook'] }, function(err) { // ... });六、测试用例对行为的印证
仓库测试 test/unit/app.reloadActions.test.js 完整覆盖了上述行为,可直接作为行为契约参考:
- 无
hooksToSkip时全量重载:测试先写入api/controllers/TopLevelController.js(含fnAction),启动时断言toplevel/fnaction与 hook 注册的custom-action存在;随后修改控制器文件并新增api/controllers/nested/standalone-action.js,调用sailsApp.reloadActions(cb)后断言新动作toplevel/machineaction、nested/standalone-action均被加载,旧动作仍存在(test/unit/app.reloadActions.test.js); hooksToSkip生效:传入{hooksToSkip: ['myHook']}后断言custom-action不再存在,证明被跳过的 hook 没有重新执行registerActions()(test/unit/app.reloadActions.test.js);- 与蓝图路由的联动:集成测试 test/integration/hook.blueprints.restful.routes.test.js 与 test/integration/hook.blueprints.shortcut.routes.test.js 均在重载动作后验证 RESTful 与 shortcut 路由仍能正确工作,说明
reloadActions()重载后的动作可以继续被路由系统正常解析。
测试中的自定义 hook 定义方式也值得参考——一个实现了registerActions方法的 hook 形如:
hooks: { myHook: function() { return { initialize: function(cb) { this.registerActions(cb); }, registerActions: function(cb) { sails.registerAction(function(){}, 'custom-action'); return cb(); } }; } }(摘自 test/unit/app.reloadActions.test.js)
七、安全边界与注意事项
文档特别强调了一条安全警告,即使不使用.reloadActions()也适用:
切勿在运行时用不可信的代码动态替换磁盘上的 Sails 控制器或动作文件。因为
reloadActions()会真实执行应用文件中的代码,如果这些文件被替换为不安全的内容,调用reloadActions()就构成安全风险。此风险仅在应用主动覆写自身文件、将其替换为不安全代码时存在(相关讨论见 Sails 仓库 issue #7209)。
结合源码可进一步理解此风险的本质:
reloadActions()会重新执行api/controllers下所有文件的模块代码(includeAll.optional会require这些文件,见 lib/app/private/controller/load-action-modules.js);- 若文件被恶意替换,
require阶段即会执行恶意逻辑,随后注册的动作也会被注入路由。
因此务必保证控制器目录下文件的来源可信,同时遵循常规安全实践(如不将不可信的写入操作开放到对外路由)。
此外还有几点使用提醒:
- 实验性 API:接口与行为可能随版本变化,升级 Sails 前请查阅 升级指南 与 CHANGELOG.md;
- 全量重建开销:
reloadActions()清空并重建全部动作,涉及 hook 遍历与磁盘扫描,属于重量级操作,仅建议在开发场景使用,生产环境应直接重启进程; - 错误传播:hook 注册失败或磁盘加载冲突(
E_CONFLICT)都会通过回调错误暴露,调用方应妥善处理err; - 配合相关 API:与 sails.registerAction()、sails.reloadActions() 的应用生命周期上下文 结合理解,可完整掌握 Sails 动作注册体系。
八、小结
reloadActions()是 Sails 面向开发体验的关键 API:它以「清空 → hook 重注册 → 磁盘重扫描」三步流程实现了动作注册表的热重建,弥补了 Sails 启动后动作静态化的短板。理解其内部对sails.hooks、sails._actions与loadActionModules的协作关系,开发者就能在自定义 hook、开发辅助工具中正确使用hooksToSkip,并规避动态覆写文件带来的安全风险。
【免费下载链接】sailsRealtime MVC Framework for Node.js项目地址: https://gitcode.com/gh_mirrors/sa/sails
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考