- 后端
- 微服务
【免费下载链接】loopback-next
LoopBack makes it easy to build modern API applications that require complex integrations.
导读
本文基于 LoopBack 官方示例@loopback/example-lb3-application(examples/lb3-application),系统讲解如何把已有的 LoopBack 3(LB3)应用挂载到全新的 LoopBack 4(LB4)项目上,并进一步把 Express 中间件迁移到公共位置、让 LB3 与 LB4 共享,最后实现在 LB4 项目中直接运行 LB3 风格的验收测试与集成测试。读完本文,你将掌握一条"LB3 应用原地复用、LB4 能力渐进接管"的平滑迁移路径:无需重写旧应用,即可让新老两套代码在同一进程中协同对外提供服务。
一、为什么需要"挂载"而非"重写"?
LoopBack 3 与 LoopBack 4 是两代架构完全不同的框架:LB3 以 JavaScript 编写、依赖loopback-boot运行时引导,而 LB4 是 TypeScript 优先、基于依赖注入与装饰器的新架构。对于已经拥有大量 LB3 模型、boot 脚本和 REST 接口的存量应用,一次性重写成本高、风险大。
@loopback/booter-lb3app包提供了一条渐进式路径:在 LB4 应用启动时,通过 Booter 加载并引导 LB3 应用,把它的 Swagger 规范转换为 OpenAPI v3 规范,再把整个 LB3 应用(连同规范)挂载到 LB4 应用的 Express 路由器上,从而让旧接口"原样照常运行",同时新功能可以逐步用 LB4 的方式开发。该包的定位在其 README 中有明确说明:
Boot a LoopBack 3 application and expose its REST API via LoopBack 4.
官方示例@loopback/example-lb3-application正是这条路径的完整可运行演示:它包含一个真实的 LB3 应用(CoffeeShop 咖啡馆模型 + 用户认证),一个 LB4 应用(CoffeeShopApplication),以及一个将它们组装起来的 Express 服务器。
二、第一步:把 LB3 应用挂载到 LB4 项目
这一节对应 README 的 "Mounting LB3 app on LB4 app" 章节,是整条迁移路径的起点。以下是完整操作步骤。
2.1 创建 LB4 项目并放入 LB3 应用
使用 CLI 创建新的 LoopBack 4 项目:
$ lb4 app按提示填写项目信息,保留所有 feature 默认启用。
从 LB4 项目根目录新建
lb3app/目录,把现有 LB3 应用整体复制进去。最终目录布局应如下:lb3app/ # LoopBack 3 application in JavaScript common/ models/ # LB3 model files server/ boot/ # LB3 boot scripts public/ # front-end assets (LB4 way) src/ # LoopBack 4 application in TypeScript示例仓库中,lb3app/server/server.js 是标准的 LB3 入口:创建
loopback()实例后调用boot(app, __dirname, ...)引导应用、装配模型、数据源与中间件。
2.2 把 LB3 依赖并入主 package.json
删除lb3app/package.json、lb3app/node_modules/以及(若存在)lb3app/package-lock.json,将 LB3 运行时依赖移动到 LB4 项目的package.json中。典型的最小依赖集如下,此外还需按实际使用情况补上各类 connector 与 component:
{ "compression": "^1.7.4", "cors": "^2.8.5", "helmet": "^3.16.0", "loopback": "^3.25.1", "loopback-boot": "^3.3.0" }注意:
loopback-boot必须使用 3.2.1 或更高版本。这一点在Lb3AppBooter的源码中也有对应的运行时提示(packages/booter-lb3app/src/lb3app.booter.ts):如果 LB3 应用未能正确引导,首先检查它是否使用了 3.2.1 以上的loopback-boot。版本过低时,boot()回调的时序问题会导致挂载失败。
随后在 LB4 项目根目录执行npm install安装这些依赖。当前仓库示例实际使用的版本更高(见 examples/lb3-application/package.json):loopback@^3.28.0、loopback-boot@^3.3.1、helmet@^4.6.0、compression@^1.8.2、cors@^2.8.6,可作为升级参考。
2.3 禁用 LB3 的错误处理,交给 LB4
错误处理统一由新架构负责,避免新旧两层重复拦截:
- 删除
lb3app/server/middleware.development.json; - 编辑
lb3app/server/middleware.json,移除以下两个条目:final下的loopback#urlNotFoundfinal:after下的strong-error-handler
- 从
package.json依赖中移除strong-error-handler; - 在
lb3app/server/config.json中,若remoting里已有"handleErrors": false,把它移动到rest属性下(remoting.rest)。
2.4 静态资源交由 LB4 提供
- 把
lb3app/client下的前端文件移动到 LB4 项目的public/目录; - 在
lb3app/server/middleware.json中移除静态资源中间件条目:files下的loopback#static; - 删除
lb3app/server/boot/root.js,因为主页将由 LB4 项目负责提供。
示例仓库的 public/index.html 即同时包含 LB4 首页与保留的lb3-index.html旧首页。
2.5 移除 LB3 的 API Explorer
删除lb3app/server/component-config.json,禁用 LB3 自带的 explorer,统一改用 LB4 的 explorer(示例中通过RestExplorerComponent提供,见 src/application.ts)。
2.6 安装并注册 @loopback/booter-lb3app
安装依赖:
npm install --save @loopback/booter-lb3app在
src/application.ts顶部引入组件:import {Lb3AppBooterComponent} from '@loopback/booter-lb3app';在 Application 构造函数中注册组件:
this.component(Lb3AppBooterComponent);
组件本身极其轻量,其实现就是把Lb3AppBooter注册进 booters 列表(见 lb3app.booter.component.ts):
export class Lb3AppBooterComponent implements Component { booters = [Lb3AppBooter]; }2.7 启动验证
$ npm start Server is running at http://127.0.0.1:3000打开控制台输出的地址查看public/提供的前端页面,或访问http://127.0.0.1:3000/explorer浏览 REST API。至此,LB3 应用已成功挂载在 LB4 应用之上——但你还可以进一步优化:把几乎全部中间件迁移到公共位置,让新旧两代应用共享。
三、挂载的底层原理:Lb3AppBooter 做了什么
在进入中间件迁移之前,先看挂载动作在源码层面究竟如何发生。Lb3AppBooter的load()方法(packages/booter-lb3app/src/lb3app.booter.ts)依次完成四件事:
- 加载并引导 LB3 应用:
loadAndBootTheApp()通过require()载入lb3app/server/server.js,若lb3App.booting为真则等待'booted'事件,确保引导完成(lb3app.booter.ts); - 生成并转换 OpenAPI 规范:
buildOpenApiSpec()用loopback-swagger从 LB3 应用生成 Swagger 2.0 规范,再经swagger2openapi转换为 OpenAPI v3 规范,并支持通过specTransformer选项自定义转换(lb3app.booter.ts); - 挂载 LB3 应用:按
mode选择挂载方式(见下); - 把 LB3 的数据源与模型绑定到 LB4 上下文:
lb3-datasources.{ds name}与lb3-models.{model name}(lb3app.booter.ts),这正是后续"从 LB4 运行 LB3 集成测试"时能通过get()取到 LB3 模型的前提。
挂载分为两种模式(Lb3AppBooterOptions.mode,默认选项见 lb3app.booter.ts):
| 模式 | 行为 | 关键代码 |
|---|---|---|
fullApp(默认) | 把整个 LB3 应用作为 Express 应用挂载到其restApiRoot(来自 LB3config.json),并使用rebaseOpenApiSpec按根路径重定位规范 | mountFullApp(lb3app.booter.ts) |
restRouter | 仅挂载 LB3 的 REST 处理器到restApiRoot选项指定的路径(默认/api) | mountRoutesOnly(lb3app.booter.ts) |
最终挂载调用的是 LB4RestApplication.mountExpressRouter(basePath, router, spec)API(packages/rest/src/rest.application.ts),该 API 会把 Express 路由注册进 LB4 REST 服务器并同步进 OpenAPI 规范。
四、第二步:把 Express 中间件迁移到公共位置
挂载成功后,LB3 应用内仍残留着大量 Express 中间件(favicon、compression、cors、helmet 等)。本节按照 README 的 "Migrating Express middleware from LB3 app" 章节,把它们从 LB3 的middleware.json迁移到公共的 Express 应用中,让 LB3 与 LB4 共享同一份中间件配置。
4.1 更新 LB3 的 config.json
编辑lb3app/server/config.json:
移除已不再需要的
host与port(监听交由 Express 应用负责):"host": "0.0.0.0", "port": 3000,把
restApiRoot改为/或你希望挂载 LB3 应用的任意路径——该路径相对 LB4 REST API 路径(默认/api)而言;为
rest属性新增"handleUnknownPaths": false,防止 LB3 REST API 对无法处理的请求返回 404。
迁移后的config.json应与示例仓库中的 lb3app/server/config.json 一致:
{ "restApiRoot": "/", "remoting": { "context": false, "rest": { "handleErrors": false, "handleUnknownPaths": false, "normalizeHttpPath": false, "xml": false }, "json": { "strict": false, "limit": "100kb" }, "urlencoded": { "extended": true, "limit": "100kb" }, "cors": false } }各参数作用:context: false关闭 remoting context;handleErrors: false与handleUnknownPaths: false把错误处理与未知路径处理全部交给外层;normalizeHttpPath: false保持路径原样;xml: false禁用 XML 响应;json/urlencoded下的strict、limit、extended控制请求体解析行为;cors: false说明 CORS 已由外层公共中间件负责。
4.2 配置基础 Express 应用(src/server.ts)
思路是创建一个基础 Express 应用作为总入口,把 LB4 应用挂载为子路由,同时承载从 LB3 迁移过来的全部中间件。仓库内对应的完整教程参见 docs/site/express-with-lb4-rest-tutorial.md。
middleware.json中每个根属性代表一个中间件阶段,按顺序在 Express 应用中加载。例如"compression": {}对应compression(),loopback#favicon对应loopback.favicon()。迁移后,LB3 的middleware.json只剩下 REST 路由本身(lb3app/server/middleware.json):
{ "routes": { "loopback#rest": { "paths": [ "${restApiRoot}" ] } } }注意:
routes阶段的中间件暂不迁移(README 中说明这是待完成事项),仅保留loopback#rest路由条目。
LB3 应用中的静态文件移动到 Express 应用的public/目录,任何非 REST 路由也移动到 Express 应用。最终src/server.ts与示例仓库的 src/server.ts 一致:
import {ApplicationConfig} from '@loopback/core'; import {once} from 'events'; import express, {Request, Response} from 'express'; import * as http from 'http'; import {AddressInfo} from 'net'; import * as path from 'path'; // Replace CoffeeShopApplication with the name of your application import {CoffeeShopApplication} from './application'; const loopback = require('loopback'); const compression = require('compression'); const cors = require('cors'); const helmet = require('helmet'); export class ExpressServer { private app: express.Application; public readonly lbApp: CoffeeShopApplication; public server?: http.Server; public url: String; constructor(options: ApplicationConfig = {}) { this.app = express(); this.lbApp = new CoffeeShopApplication(options); // Middleware migrated from LoopBack 3 this.app.use(loopback.favicon()); this.app.use(compression()); this.app.use(cors()); this.app.use(helmet()); // Mount the LB4 REST API this.app.use('/api', this.lbApp.requestHandler); // Custom Express routes this.app.get('/ping', function (_req: Request, res: Response) { res.send('pong'); }); // Serve static files in the public folder this.app.use(express.static(path.join(__dirname, '../public'))); } public async boot() { await this.lbApp.boot(); } public async start() { await this.lbApp.start(); const port = this.lbApp.restServer.config.port || 3000; const host = this.lbApp.restServer.config.host || '127.0.0.1'; this.server = this.app.listen(port, host); await once(this.server, 'listening'); const add = <AddressInfo>this.server.address(); this.url = `http://${add.address}:${add.port}`; } public async stop() { if (!this.server) return; await this.lbApp.stop(); this.server.close(); await once(this.server, 'close'); this.server = undefined; } }代码要点:
- 中间件顺序:
favicon → compression → cors → helmet依次挂载在this.app上,随后才挂载 LB4 REST API(/api前缀)、自定义/ping路由与静态目录,符合 Express 中间件"先通用后业务"的执行顺序; - LB4 以路由形式接入:通过
this.app.use('/api', this.lbApp.requestHandler)把 LB4 应用暴露为/api下的子路由; - 端口与主机:读取
lbApp.restServer.config的port/host,缺省回退到3000与127.0.0.1; - 生命周期:
boot()/start()/stop()均委托给内部的lbApp,并用once()等待listening/close事件保证异步时序。
这些从 LB3middleware.json迁移而来的中间件,现在被 LB3 与 LB4 应用共享。
4.3 更新 src/index.ts 入口
Express 应用取代CoffeeShopApplication成为程序入口,修改src/index.ts(与示例仓库的 src/index.ts 一致):
import {ApplicationConfig} from '@loopback/core'; import {ExpressServer} from './server'; export {ApplicationConfig, ExpressServer}; export async function main(options: ApplicationConfig = {}) { const server = new ExpressServer(options); await server.boot(); await server.start(); console.log(`Server is running at ${server.url}`); }4.4 阻止 LB4 应用自行监听
由于现在由 Express 应用统一监听端口,LB4 应用必须停止自行监听。在src/index.ts的启动配置中给rest加上listenOnStart: false:
const config = { rest: { port: +(process.env.PORT ?? 3000), host: process.env.HOST ?? 'localhost', openApiSpec: { // useful when used with OpenAPI-to-GraphQL to locate your application setServersFromRequest: true, }, listenOnStart: false, }, };openApiSpec.setServersFromRequest: true表示 OpenAPI 规范中的 server 地址取自实际请求,便于与 OpenAPI-to-GraphQL 等工具配合。
随后在CoffeeShopApplication类的bootOptions中配置lb3app的挂载路径与模式。bootOptions最终形态(与示例仓库的 src/application.ts 一致):
this.bootOptions = { controllers: { // Customize ControllerBooter Conventions here dirs: ['controllers'], extensions: ['.controller.js'], nested: true, }, lb3app: { mode: 'fullApp', }, };mode: 'fullApp'告诉 Booter 把整个 LB3 应用(而非仅 REST 路由)挂载上来,与第三节中mountFullApp的行为对应。
4.5 启动并验证
$ npm start浏览器访问http://localhost:3000/,即可看到同时挂载了 LB3 与 LB4 应用的 Express 应用。示例中/返回 LB4 首页,/api下是 LB4 REST API(其中包含来自 LB3 的CoffeeShops等接口),/ping返回pong,/lb3-index.html保留旧 LB3 首页——这些行为均有对应验收测试(home-page.acceptance.ts、lb3app.acceptance.ts)逐一断言。
五、第三步:从 LB4 项目运行 LB3 测试
挂载与中间件迁移完成后,还可以让旧的 LB3 测试继续在 LB4 项目中运行。目标是让 LB3 测试使用 LB4 服务器而非 LB3 应用本身,覆盖两类测试:
- 验收测试(acceptance-level):通过 HTTP 调用应用逻辑,例如
POST /users/login; - 集成测试(integration-level):通过 JS API 调用应用逻辑,例如
MyModel.create()。
5.1 把 LB3 测试路径加入 test 命令
在package.json的test条目中追加 LB3 测试目录。示例仓库实际使用的命令是(见 examples/lb3-application/package.json):
"test": "lb-mocha \"dist/__tests__/**/*.js\" \"lb3app/test/*.js\""该命令先用lb-mocha运行编译后的 LB4 测试(dist/__tests__/),再运行lb3app/test/下的 LB3 测试。示例仓库的 LB3 测试包括 acceptance.js、authentication.js 与 integration.js。
5.2 运行验收测试
首先把 LB3 测试依赖移动到package.json的devDependencies并执行npm install。然后在测试文件中按启动方式二选一改造。
方式一:启动 Express 服务器
- 请求辅助函数改为直接针对 Express 服务器,注意 LB3 API 挂在基路径
/api下:
// can use lb4's testlab's supertest as the dependency is already installed const {supertest} = require('@loopback/testlab'); const assert = require('assert'); const should = require('should'); const {ExpressServer} = require('../../dist/server'); let app; function jsonForExpressApp(verb, url) { // use the express server, it mounts LoopBack 3 apis to // base path '/api' return supertest(app.server) verb .set('Content-Type', 'application/json') .set('Accept', 'application/json') .expect('Content-Type', /json/); }- 在
before钩子中 boot + start,在after钩子中 stop:
describe('LoopBack 3 style tests - Launch Express server', function () { before(async function () { app = new ExpressServer(); await app.boot(); await app.start(); }); after(async () => { await app.stop(); }); // your tests here runTests(); });方式二:直接启动 LB4 应用
- 请求辅助函数改用 LB4 应用的 REST 服务器,路径无需
/api前缀:
// can use lb4's testlab's supertest as the dependency is already installed const {supertest} = require('@loopback/testlab'); const assert = require('assert'); const should = require('should'); const {CoffeeShopApplication} = require('../../dist/application'); let app; function jsonForLB4(verb, url) { // use the lb4 app's rest server return supertest(app.restServer.url) verb .set('Content-Type', 'application/json') .set('Accept', 'application/json') .expect('Content-Type', /json/); }- 生命周期钩子:
describe('LoopBack 3 style tests - launch LB4 app', function () { before(async function () { app = new CoffeeShopApplication(); await app.boot(); await app.start(); }); after(async () => { await app.stop(); }); // your tests here runTests(); });这两种写法的完整示例就是 lb3app/test/acceptance.js——它同时包含"从 Express 服务器引导"与"从 LB4 应用引导"两个 describe 块,且测试用例与 LB4 风格的 src/tests/acceptance/lb3app.acceptance.ts 完全对应,便于对照学习。现在运行npm test,LB3 测试就会与 LB4 测试一同执行。
可选方案:也可以把旧测试逐步改写成 LB4 风格的测试(参考 lb3app.acceptance.ts),仓库内亦有测试方法论文档 docs/site/Testing-your-application.md 可参考。
5.3 运行集成测试
集成测试需要直接调用 LB3 模型的方法(如CoffeeShop.create())。得益于第三节中提到的绑定机制,LB3 模型已通过lb3-models.{model name}键绑定到 LB4 应用上下文,用get()即可取出:
describe('LoopBack 3 style integration tests', function () { let app; let CoffeeShop; before(async function () { // If launch the LoopBack 4 application // app = new CoffeeShopApplication(); app = new ExpressServer(); await app.boot(); await app.start(); }); before(() => { // follow the syntax: lb3-models.{ModelName} // If launch the LoopBack 4 application // CoffeeShop = await app.get('lb3-models.CoffeeShop'); CoffeeShop = await app.lbApp.get('lb3-models.CoffeeShop'); }); after(async () => { await app.stop(); }); // your tests here runTests(); });- LB3 模型的绑定键语法:
lb3-models.{model name}; - LB3 数据源同样绑定到 LB4 上下文,键语法:
lb3-datasources.{ds name}。
模型与数据源的绑定实现位于 packages/booter-lb3app/src/lb3app.booter.ts,注意其中使用visitedDs/visitedModels集合去重,避免共享实例被重复绑定。
完整的集成测试示例见 lb3app/test/integration.js,它展示了CoffeeShop.find、CoffeeShop.count、CoffeeShop.create等典型用例;认证相关的 LB3 测试见 lb3app/test/authentication.js。
六、示例项目中的可复现细节
为帮助读者把上文步骤映射到真实代码,这里汇总示例仓库的关键文件及其作用:
| 文件 | 作用 |
|---|---|
| lb3app/server/server.js | LB3 入口,创建 app 并调用loopback-boot引导 |
| lb3app/server/config.json | 迁移后的 LB3 配置(restApiRoot、remoting 各项) |
| lb3app/server/middleware.json | 迁移后仅保留loopback#rest路由 |
| lb3app/server/boot/create-sample-models.js | LB3 boot 脚本,自动建表并写入示例数据 |
| lb3app/server/boot/authentication.js | LB3 boot 脚本,调用server.enableAuth()开启认证 |
| lb3app/common/models/coffee-shop.js | LB3 模型,含status/greet两个 remote method |
| src/application.ts | LB4 应用,注册Lb3AppBooterComponent并配置lb3app.bootOptions |
| src/server.ts | Express 服务器,承载共享中间件并挂载 LB4/LB3 |
| src/index.ts | 入口,listenOnStart: false配置 |
| lb3app/test/acceptance.js | LB3 风格验收测试(两种启动方式) |
| lb3app/test/integration.js | LB3 风格集成测试(模型方法调用) |
示例的 CoffeeShop 模型同时演示了认证与 ACL 的迁移效果:coffee-shop.json中为greet方法配置了 ACL,拒绝$unauthenticated角色访问(coffee-shop.json),因此 lb3app.acceptance.ts 中断言匿名访问/api/CoffeeShops/greet返回 401,而携带access_token的请求返回 200。
此外,示例项目还提供了两个辅助脚本:
npm run migrate:执行 migrate.ts,通过--rebuild参数决定 drop 还是 alter 现有 schema;npm run openapi-spec:执行 openapi-spec.ts,把合并后的 OpenAPI v3 规范导出到文件。
关于 OpenAPI 规范的合并,lb3app.acceptance.ts 中有完整断言:LB3 的 Swagger 规范经转换后以 OpenAPI 3.0.0 形式并入 LB4 规范,CoffeeShop、User等 schema、requestBodies、paths(如/Users/login、/CoffeeShops)以及basePath均被正确迁移。
七、常见问题与注意事项
- LB3 应用没有正确引导:确认
loopback-boot版本 ≥ 3.2.1(Booter 源码 lb3app.booter.ts 有相应提示),并检查lb3app/server/server.js是否正确导出 app 实例。 - 端口冲突或双重监听:迁移到 Express 服务器后,务必在
config.rest中设置listenOnStart: false,否则 LB4 应用会与 Express 服务器争抢端口。 - 404 由谁返回:为 LB3 的
rest配置设置handleUnknownPaths: false,并移除final/final:after中的loopback#urlNotFound与strong-error-handler,让错误处理统一由 LB4/Express 层负责。 - 中间件顺序:从
middleware.json迁移中间件时,必须按原阶段顺序(favicon → compression → cors → helmet → 业务路由 → 静态资源)在 Express 应用中排列。 routes阶段中间件暂不支持迁移:README 明确指出这是待办事项(对应 upstream issue #4181),目前仅保留loopback#rest路由。- 获取 LB3 模型/数据源:集成测试中统一通过
lb3-models.{name}、lb3-datasources.{name}绑定键从 LB4 上下文取用,不要直接requireLB3 内部文件。
八、总结
本文完整呈现了"LB3 应用挂载到 LB4 项目"的三步走路径:
- 挂载:通过
@loopback/booter-lb3app在 LB4 应用启动时引导 LB3 应用、转换 OpenAPI 规范并挂载其 REST API,同时把模型与数据源绑定到 LB4 上下文; - 中间件迁移:借助基础 Express 应用承载
favicon、compression、cors、helmet等公共中间件,让新旧两代应用共享,LB3 的middleware.json退化为仅剩loopback#rest; - 测试复用:修改
test命令把lb3app/test纳入执行范围,验收测试与集成测试分别通过 Express 服务器/LB4 应用两种方式运行,旧测试资产得以完整保留。
这条路径让团队可以"新功能用 LB4 写、旧接口零改动继续跑",是 LoopBack 3 存量应用向 LoopBack 4 渐进迁移的官方推荐做法。完整可运行示例就在仓库的 examples/lb3-application 中,核心 Booter 实现位于 packages/booter-lb3app/src/lb3app.booter.ts,读者可以在此基础上对照源码继续深入。
- 后端
- 微服务
【免费下载链接】loopback-next
LoopBack makes it easy to build modern API applications that require complex integrations.
相关推荐
在 LoopBack 4 中挂载 LoopBack 3 应用:@loopback/booter-lb3app 实战指南
在 LoopBack 4 中挂载 LoopBack 3 应用:@loopback/booter lb3app 实战指南 导读 LoopBack 3(LB3)与
后端微服务将 LoopBack 3 组件中的 Model Mixin 迁移到 LoopBack 4:三步迁移指南
将 LoopBack 3 组件中的 Model Mixin 迁移到 LoopBack 4:三步迁移指南 本文聚焦于一个具体迁移场景:如何把"以提供 Model
后端微服务LoopBack 3 到 LoopBack 4 数据源(Datasource)迁移完整指南
LoopBack 3 到 LoopBack 4 数据源(Datasource)迁移完整指南 本篇指南以官方迁移文档 datasources.md https:/
后端微服务
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考