LoopBack 4 不再规划的三项 LoopBack 3 功能:存储组件、推送通知与离线同步的迁移替代方案
LoopBack 4 不再规划的三项 LoopBack 3 功能:存储组件、推送通知与离线同步的迁移替代方案
本文围绕 LoopBack 迁移指南中的 not-planned.md 文档展开,系统梳理 LoopBack 团队在从 LoopBack 3 迁移到 LoopBack 4 时明确"不再规划"(not planned)的三类功能——存储组件(Storage component)、推送通知(Push notifications)以及离线数据访问与同步(Synchronization),并说明官方推荐的在 LoopBack 4 中重建等价能力的技术路线。读完本文,你将掌握如何用 x-parser: stream 扩展 + multer + Service/Controller 组合替代旧的文件上传下载组件、如何用 Model/Repository/Service 结构重建推送能力,以及离线同步场景下应研究哪些专门解决方案。
背景:为什么这些 LoopBack 3 功能被"停用"
在 LoopBack 早期(2013—2015 年前后),团队曾广泛探索各种功能领域,试图让框架对更广泛的受众具有吸引力。其中一些功能流行起来,另一些则反响平平;与此同时,技术世界不断演进,当年的许多范式已被更优的思路取代。
更重要的是,LoopBack 维护者社区的增长速度不足以支撑 LoopBack 所提供的那张宽广的功能谱系。结果是:大部分扩展之间缺乏良好集成,且许多已经过时。为了保证项目可持续、并给框架用户提供良好的开发者体验,团队决定停用(sunset)若干 LoopBack 3 扩展与功能,并鼓励社区接手这些扩展的后续开发。
具体到迁移文档体系,overview.md 中明确提到:借着这次大版本升级的机会,LoopBack 团队放弃了一批没有精力继续维护和演进的功能与组件,相关说明详见 not-planned.md。被列入"不再规划"清单的共三类:
| 功能 | 对应 LoopBack 3 组件 | LoopBack 4 中的态度 |
|---|---|---|
| 文件存储(上传/下载) | loopback-component-storage |
不提供一等公民支持,用 x-parser: stream + multer + Service/Controller 自行实现 |
| 移动推送通知 | loopback-component-push |
不内置,用 Model/Repository/Service + 第三方推送 SDK(apn、node-gcm)重建 |
| 离线数据访问与同步 | Synchronization 组件 | 不迁移(与内置持久层深度耦合),推荐研究数据库配套的专门同步方案 |
Storage component:文件上传下载的 LoopBack 4 替代方案
LoopBack 3 中的组件构成
loopback-component-storage 提供了一套统一的 REST API,用于存储和获取任意文件,底层可对接第三方存储服务(如 Amazon S3)。从架构上看,该组件由以下四部分组成:
- 处理文件上传的自定义层;
- 处理文件下载的自定义层;
- 对接不同存储服务的连接器(这部分由
pkgcloud包承担); - 提供 File 与 Container 抽象并暴露 REST API 的集成层。
在 LoopBack 4 中逐层重建
LoopBack 4 中,官方给出的替代路线与上述四个部分一一对应:
- 上传:使用
x-parser: stream扩展让请求体以流(stream)形式接收,再用multer包处理multipart/form-data文件上传。 - 下载:LoopBack 4 尚未对文件下载提供一等公民支持(相关讨论见 loopback-next#2230)。作为变通方案,可以在 Controller 中注入完整的 Express response 对象,直接使用 Express API 将响应体以流式输出。
- 云存储对接:可继续使用
pkgcloud,或直接使用云服务商提供的 Node.js 客户端库;社区另有multer-storage-pkgcloud可将 multer 与 pkgcloud 打通。 - REST API 封装:用 TypeScript 创建提供 File/Container API 的 Service,再以一个或多个 Controller 实现 REST 接口。
源码级实例:examples/file-transfer
仓库中的 file-transfer 示例 是上述方案的一个完整可运行实现,它暴露了三个端点:POST /files(接受 multipart/form-data 上传)、GET /files(列出已上传文件)、GET /files/{filename}(下载单个文件)。下面沿着源码走一遍关键链路。
第一步:配置 multer 并绑定服务。 在 application.ts 中,应用通过 configureFileUpload() 方法把 multer 的 diskStorage 配置绑定到 FILE_UPLOAD_SERVICE 键上:
// examples/file-transfer/src/application.ts(节选)
protected configureFileUpload(destination?: string) {
// 默认上传到 dist/.sandbox
destination = destination ?? path.join(__dirname, '../.sandbox');
this.bind(STORAGE_DIRECTORY).to(destination);
const multerOptions: multer.Options = {
storage: multer.diskStorage({
destination,
// 保留原始文件名
filename: (req, file, cb) => {
cb(null, file.originalname);
},
}),
};
// 配置 file upload service
this.configure(FILE_UPLOAD_SERVICE).to(multerOptions);
}
其中 FILE_UPLOAD_SERVICE 与 STORAGE_DIRECTORY 两个绑定键定义在 keys.ts:
export const FILE_UPLOAD_SERVICE = BindingKey.create<FileUploadHandler>(
'services.FileUpload',
);
export const STORAGE_DIRECTORY = BindingKey.create<string>('storage.directory');
第二步:用 Provider 暴露 multer 请求处理器。 上传服务本身是一个 @injectable 的 Provider,它接收 @config() 注入的 multer 配置,返回 multer(options).any() 生成的 Express 请求处理器,见 file-upload.service.ts:
@injectable({
scope: BindingScope.TRANSIENT,
tags: {[ContextTags.KEY]: FILE_UPLOAD_SERVICE},
})
export class FileUploadProvider implements Provider<FileUploadHandler> {
constructor(@config() private options: multer.Options = {}) {
if (!this.options.storage) {
// 默认使用内存存储
this.options.storage = multer.memoryStorage();
}
}
value(): FileUploadHandler {
return multer(this.options).any();
}
}
这种"Provider + Binding"的方式正是 LoopBack 4 依赖注入的典型用法:FileUploadHandler 类型即 express-serve-static-core 的 RequestHandler(见 types.ts),意味着 multer 中间件可以像任何 Express 中间件一样在 Sequence 中执行。
第三步:Controller 接收流式请求体。 上传控制器在 file-upload.controller.ts 中,借助 @requestBody.file() 快捷装饰器(其底层正是 x-parser: 'stream' 扩展)把整个请求以流形式交给 multer handler:
@post('/files', {responses: {...}})
async fileUpload(
@requestBody.file()
request: Request,
@inject(RestBindings.Http.RESPONSE) response: Response,
): Promise<object> {
return new Promise<object>((resolve, reject) => {
this.handler(request, response, (err: unknown) => {
if (err) reject(err);
else resolve(FileUploadController.getFilesAndFields(request));
});
});
}
从底层看,x-parser: 'stream' 由 body-parser.stream.ts 中的 StreamBodyParser 实现。该解析器在 supports(mediaType) 中恒返回 false,注释明确说明"它只能通过请求体规范中的 {x-parser: 'stream'} 扩展被触发",parse() 则直接把原始 Request 对象作为值返回——即不做任何解析、保留请求流原样。正是这一机制让 multer 可以接管后续的 multipart 解析。
第四步:文件下载——注入 Express response。 下载控制器在 file-download.controller.ts 中演示了"注入完整 Express response 对象并调用 Express API"的变通方案:
@get('/files/{filename}')
@oas.response.file()
downloadFile(
@param.path.string('filename') fileName: string,
@inject(RestBindings.Http.RESPONSE) response: Response,
) {
const file = this.validateFileName(fileName);
response.download(file, fileName);
return response;
}
@oas.response.file() 装饰器(来自 @loopback/openapi-v3,源码见 response.decorator.ts)用于在 OpenAPI 规范中声明响应为二进制文件类型。注意 validateFileName() 对文件名做了路径沙箱校验:用 path.resolve 解析后必须仍落在存储目录内,否则抛出 HttpErrors.BadRequest——这正是文件下载场景必须防住的路径穿越(path traversal)攻击。
第五步:Sequence。 整个示例的 Sequence 只是简单继承 MiddlewareSequence(见 sequence.ts),说明这套方案不要求自定义请求处理流程,标准中间件序列即可承载 multer 处理器。
测试验证
仓库为 file-transfer 示例提供了两组验收测试,可作为实现正确性的参照:
- file-upload.acceptance.ts:通过
client.post('/files').field(...).attach(...)模拟 multipart 上传,断言响应体中的files数组包含fieldname、originalname、mimetype等元数据; - file-download.acceptance.ts:覆盖了列文件、下载文件、文件不存在返回 404,以及使用
%2Fbad-file.txt、%2E%2E%2Fbad-file.txt(即/与../变体)绕过沙箱时返回 400 的路径穿越防护场景。
实际运行效果可参考示例自带的截图 upload-download.png。
启动示例的方式为在 examples/file-transfer 目录执行 npm start,应用默认监听 3000 端口,上传文件默认存放在应用根目录下的 .sandbox 目录,可通过应用配置中的 fileStorageDirectory 自定义存储位置。
把成果打包成 LoopBack Component
如果你对上述实现满意,官方鼓励将代码打包为 LoopBack Component 并分享给整个社区。组件通过 app.component() 方法挂载到应用(详见 Component.md 中的 "Using components" 一节),例如:
const app = new RestApplication();
app.component(YourFileStorageComponent);
若希望官方在文档中收录你的组件,只需提交一个 pull request,把组件加入 "Using components" 列表即可。
Push notifications:推送通知的 LoopBack 4 重建路线
loopback-component-push 让服务端应用即使在移动 App 未被使用时也能向 App 发送信息。在 LoopBack 3 中,该组件由以下部分组成:
- Device 模型及管理"设备—应用—用户"关系的 API;
- Application 模型,为 iOS、Android 等设备类型提供推送设置;
- Notification 模型,捕获通知消息并持久化定时通知;
- 可选的后台任务,消费定时通知请求;
- Push 连接器,与设备注册记录及推送服务商交互——iOS 走 APNS、Android 走 GCM;
- Push 模型,提供与设备无关的高层推送 API。
LoopBack 4 中官方不打算内置该组件,但给出了清晰的"重建配方":
- 实现 Device 与 Application 模型:按 LoopBack 4 的标准做法创建 Model、Repository 和 Controller,用 REST API 暴露设备注册与管理能力;
- 实现推送 Service:创建一个 Service 提供 TypeScript 级别的"发送通知"与"持久化待发送定时通知"API。与推送服务商的交互可直接使用
apn(对接 Apple 的 APNS)和node-gcm(对接 Google 的推送服务)等包。
值得说明的是,LoopBack 4 的 Service 是从依赖注入容器(Context 中的 Binding)解析出来的对象(详见 Service.md 的 Overview),支持 proxy(远程 REST/SOAP/gRPC 代理)、class、provider 三种形态;推送 Service 通常实现为 class 或 provider 并注册到应用上下文,然后被 Controller 注入使用。这样既隔离了第三方 SDK 的细节,也让定时发送逻辑可以挂在 Life-cycle 或后台任务上。
Offline data access and synchronization:离线同步的替代方案
LoopBack 3 的 Synchronization 组件允许浏览器客户端在离线状态下访问和修改数据,并在客户端重新上线后,把客户端与服务器两侧的变更进行同步(复制)。LoopBack 团队明确表示:该功能在 LoopBack 3 中的实现复杂且与内置持久化层深度耦合,几乎无法迁移到 LoopBack 4 的编程模型,因此不在规划内。
官方给出的建议是:如果确实需要离线数据访问能力,应当去研究针对你所使用的数据库专门设计的同步方案。文档提供了三个出发点:
- MongoDB Realm:为 MongoDB Atlas 提供双向、离线优先的同步能力;
- PouchDB:让应用离线时在本地存储数据,恢复在线后与 CouchDB 及兼容服务器同步;
- GUN:一个轻量、易用且快速的实时数据同步与存储系统,提供离线优先(offline-first)能力。
这些方案的共同点是"离线优先"(offline-first):客户端先保证本地可用性,再以增量同步的方式与服务器收敛数据,这与 LoopBack 3 中"框架内建复制引擎"的思路完全不同,也是当下更符合生态现实的选择。
总结与迁移建议
"不再规划"不等于"功能缺失":它只是说明这些能力不再由 LoopBack 框架内建维护,而是通过组合生态中的现成库 + LoopBack 4 的 Service/Controller 编程模型来重建。对照表如下:
| 旧能力 | LoopBack 4 重建路径 | 关键依赖/机制 |
|---|---|---|
| 文件存储组件 | x-parser: stream + multer + Service/Controller |
@requestBody.file()、@oas.response.file()、RestBindings.Http.RESPONSE、pkgcloud |
| 推送通知 | Device/Application Model + Repository + Controller + 推送 Service | apn、node-gcm |
| 离线同步 | 不迁移,改用数据库配套同步方案 | MongoDB Realm、PouchDB、GUN |
在动手前,建议先通读 Understanding-the-differences.md 建立对两代 LoopBack 概念映射的整体认知,再结合 migration/overview.md 的增量迁移节奏,逐个评估这三类功能在你现有应用中的真实占比。对于文件上传下载这类高频需求,直接基于 file-transfer 示例 改造即可获得生产可用的最小实现;对于推送与离线同步,则应把精力放在第三方生态选型与 Service 抽象设计上。
