ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Feathers 与 Express 集成:从应用绑定到 REST 传输的完整实践指南

2026/9/21 16:35:25 拓冰建站 浏览量
Feathers 与 Express 集成:从应用绑定到 REST 传输的完整实践指南 Feathers 与 Express 集成从应用绑定到 REST 传输的完整实践指南【免费下载链接】feathersThe API and real-time application framework项目地址: https://gitcode.com/gh_mirrors/fe/feathersfeathersjs/express是 Feathers 框架的 Express 集成模块它将 Express 的中间件生态、路由能力与 Feathers 的服务抽象、Hook 机制无缝融合让开发者既能用 Feathers 声明式地组织业务逻辑又能直接复用 Express 海量的中间件资源。本文基于本仓库 docs/api/express.md 展开结合packages/express的源码实现系统讲解如何把一个 Feathers 应用变为 Express 兼容应用、如何通过 REST 传输暴露服务、如何定制中间件与错误处理帮助读者掌握这套集成的底层机制与全部配置项。安装与模块概览安装feathersjs/express当前仓库版本为5.0.49见 package.jsonnpm install feathersjs/express --save该模块主要提供三部分能力对应 模块入口 的导出Express 框架绑定express(app)把 Feathers 应用转换为 Express 4 兼容应用在保留 Feathers 全部功能的同时获得完整的 Express API基于 Express 的 REST 传输rest()通过GET、POST、PUT、PATCH、DELETE等 HTTP 方法暴露和消费服务面向 Feathers 错误 的 Express 错误处理器errorHandler以及notFound、authenticate、parseAuthentication等配套中间件。最小集成方式import { feathers } from feathersjs/feathers import express from feathersjs/express const app express(feathers())⚠️重要提示自 Feathers v5 起Koa 成为官方推荐的框架集成因为它更现代、更快、更易用。如果你显式选择 Express 集成应已具备 Express 路由 的使用经验。express(app)将 Feathers 应用绑定到 Expressexpress(app) - app接受一个 Feathers 应用 作为参数返回一个同时具备 Feathers 与 Express 双重能力的应用对象。绑定后你既可以使用app.service()、app.hooks()等 Feathers API也可以使用app.get/set、app.use、app.listen等 Express API。从源码看绑定过程做了几件关键事情见 packages/express/src/index.ts版本与合法性校验如果传入的对象没有setup方法会抛出feathersjs/express requires a valid Feathers application instance如果传入的是旧版本 Feathers 应用会抛出包含版本号的错误。对应测试见 index.test.ts。重写use与listenuse被替换为能够自动判别服务对象与Express 中间件的统一注册入口下文详解listen被包装为异步函数先调用 Express 原生listen启动 HTTP 服务再内部调用app.setup(server)初始化服务。属性合并把 Feathers 应用原型上尚未存在于 Express 应用上的属性包括不可枚举属性逐一复制到 Express 应用上保证service、hooks等方法可用。注入req.feathers.provider rest绑定后自动挂载一个中间件为每个请求设置provider标记后续 REST 调用都会带有该标记。挂载teardown重写后的teardown在调用 Feathers 的teardown之后会一并关闭已启动的 HTTP 服务见 index.test.ts 的验证。import { feathers } from feathersjs/feathers import express from feathersjs/express const app express(feathers())express(app, expressApp)扩展既有 Express 应用express(app, expressApp) - app允许把一个 Feathers 应用app注入到一个已经存在的 Express 应用expressApp中返回的仍是原来的expressApp源码中直接Object.assign到该实例上测试见 index.test.ts。这在需要渐进式迁移、或希望多个模块共享一个 Express 实例的场景下非常有用。express()返回纯 Express 应用如果不传入任何参数express() - app行为与直接调用 Express 完全相同返回一个普通 Express 应用此时不具备service、services等 Feathers 方法见 index.test.ts。app.use(path, service|mw|[mw])统一注册服务与中间件app.use(path, service|mw|[mw]) - app是绑定后应用的核心注册入口它根据参数类型自动分流传入服务对象拥有至少一个服务方法走 Feathers 的服务注册机制传入中间件函数或函数数组走 Express 的原生注册机制。// 注册一个服务 app.use(todos, { async get(id) { return { id } } }) // 注册一个 Express 中间件 app.use(/test, (req, res) { res.json({ message: Hello world from Express middleware }) }) // 注册多个 Express 中间件函数 app.use( /test, (req, res, next) { res.data Step 1 worked next() }, (req, res) { res.json({ message: Hello world from Express middleware ${res.data} }) } )从源码看packages/express/src/index.tsuse的判别逻辑是遍历所有参数函数或数组被归类为中间件其余对象中第一个非选项对象被视为服务随后通过hasMethod判断——如果对象含handle/set方法Express 应用或 Router 的特征或者不包含任何默认服务方法就交给 Express 原生use否则作为服务调用 Feathers 的use并把中间件按位置拆分为express.before与express.after选项传入。因此把中间件放在服务之前或之后会被分别注入到服务调用链的前后对应测试见 index.test.ts。app.listen(port) 与 app.setup(server)app.listen(port) - PromiseHttpServer首先调用 Express 的 app.listen 启动服务器然后内部调用app.setup(server)初始化全部服务例如触发服务的setup钩子、初始化 Socket.io 等// 在 3030 端口监听 const server await app.listen(3030)app.setup(server) - app通常由app.listen内部调用但在以下三种场景中需要手动显式调用子应用Sub-Apps把一个 Feathers 应用作为子应用挂载到另一个应用时子应用的setup不会被自动触发必须手动调用以初始化其服务import { feathers } from feathersjs/feathers import express from feathersjs/express const api feathers() api.use(service, myService) const mainApp express(feathers()).use(/api/v1, api) const server await mainApp.listen(3030) // 现在用 server 手动初始化 Feathers 子应用 await api.setup(server)HTTPSHTTPS 需要自行创建https.createServer这种情况下app.setup(server)也必须显式调用。在生成的应用中src/index.js大致如下import https from https import { app } from ./app const port app.get(port) const server https .createServer( { key: fs.readFileSync(privatekey.pem), cert: fs.readFileSync(certificate.pem) }, app ) .listen(443) // 调用 app.setup 初始化所有服务和 SocketIO app.setup(server) server.on(listening, () logger.info(Feathers application started))仓库的测试 index.test.ts 以相同方式验证了 HTTPS 场景先https.createServer({ key, cert }, app)再app.setup(httpsServer)最后通过https://localhost:7889/secureTodos/dishes成功获取服务数据。虚拟主机Virtual Hosts借助 vhost 中间件可以把 Feathers 应用挂到虚拟主机名下但此时应用的.listen永远不会被调用因此也需要手动setupimport vhost from vhost import { feathers } from feathersjs/feathers import express from feathersjs/express const app express(feathers()) app.use(/todos, todoService) const host express().use(vhost(foo.com, app)) const server await host.listen(8080) // 虚拟主机应用从未调用 .listen这里必须手动 setup app.setup(server)rest()通过 RESTful API 暴露服务rest()注册一个 Feathers 传输机制允许通过 RESTful API 暴露和消费 服务。服务方法到 HTTP 方法的映射如下Service methodHTTP methodPath.find()GET/messages.get()GET/messages/1.create()POST/messages.update()PUT/messages/1.patch()PATCH/messages/1.remove()DELETE/messages/1这套映射在 packages/transport-commons/src/http.ts 中定义post → create、put → update、patch → patch、delete → remove而GET在带 id 时映射为get、不带 id 时映射为find。每个 HTTP 方法调用服务时的参数顺序也由argumentsFor决定如get: [id, params]、create: [data, params]并由getStatusCode计算响应状态码create返回201、空结果返回204、设置了http.location返回303。app.configure(rest())使用默认配置启用 REST 传输默认响应格式化为 JSON通过res.json发送。注意json、urlencoded等请求体解析中间件以及参数注入中间件必须在任何服务注册之前挂载import { feathers } from feathersjs/feathers import express, { json, urlencoded, rest } from feathersjs/express // 创建 Express 兼容的 Feathers 应用 const app express(feathers()) // 为 REST 服务启用 JSON 解析 app.use(json()) // 为 REST 服务启用 URL 编码表单解析 app.use(urlencoded({ extended: true })) // 启用 REST 传输 app.configure(rest())重要json、urlencoded请求体解析器与参数中间件params必须注册在任何服务之前。从源码看packages/express/src/rest.tsrest()是一个配置函数其内部做了三件事注册parseAuthentication中间件、注册servicesMiddleware负责把请求路由到匹配的服务、并通过app.mixins.push为每个服务装配before serviceMiddleware after formatter的中间件链由Router().use(...)组合。若在非 Express 兼容应用上调用会抛出feathersjs/express/rest needs an Express compatible app.测试见 rest.test.ts。serviceMiddleware是 REST 调用的核心rest.ts它从req.lookup取得命中的服务通过getServiceMethod解析出要调用的服务方法如果该方法不在服务的methods选项内或使用了默认方法做覆盖会抛出MethodNotAllowed405错误随后构建params { query, headers, route, ...req.feathers }调用服务方法并把执行结果写入res.data、把 Hook 上下文写入res.hook。相关测试覆盖了 405 错误、404 路由、204 空响应等场景rest.test.ts。app.configure(rest(formatter))自定义响应格式化器默认的 REST 响应格式化器是一个把服务返回数据格式化为 JSON 的中间件。如需自定义可传入一个formatter(req, res)函数该中间件可以访问res.data服务返回的数据并可借助res.format做内容协商import { feathers } from feathersjs/feathers import express, { json, urlencoded, rest } from feathersjs/express const app express(feathers()) // 启用 JSON 解析 app.use(json()) // 启用 URL 编码解析 app.use(urlencoded({ extended: true })) // 自定义 REST 格式化器 app.configure( rest(function (req, res) { // 以 text/plain 格式返回消息 res.format({ text/plain: function () { res.end(The Message is: ${res.data.text}) } }) }) )传入rest(...)的可以是选项对象含formatter与authentication字段也可以是单个函数等价于{ formatter }见 rest.ts。默认formatter的实现rest.ts仅在res.data未定义时跳过否则通过res.format以application/json输出——这也是formatter单独导出、可在纯 Express 应用里复用的原因测试见 rest.test.ts。自定义服务中间件只希望在某个特定服务调用前后执行的 Express 中间件可以按执行顺序直接传给app.useconst todoService { async get(id: Id) { return { id, description: You have to do ${id}! } } } app.use(todos, logRequest, todoService, updateData)服务之后的中间件可以从响应对象上取得本次服务调用的信息res.data—— 将要发送给客户端的数据res.hook—— 本次服务方法调用的 Hook 上下文例如updateData可以这样实现function updateData(req, res, next) { res.data.updateData true next() }如果在服务之后的中间件里直接调用res.send且不调用next其后的中间件包括 REST 格式化器都会被跳过。这可用于针对特定服务方法渲染不同视图例如把结果导出为 CSVimport json2csv from json2csv const fields [done, description] app.use(todos, todoService, function (req, res) { const result res.data const data result.data // 分页时为 result.data否则为 result 本身 const csv json2csv({ data, fields }) res.type(csv) res.end(csv) })注意自定义服务中间件只会在 REST 请求中生效不会作用于其他传输方式如 Socket.io。如果可能应尽量使用 Hooks 或定制服务来替代自定义中间件这样对所有传输方式都有效。仓库测试 rest.test.ts 详细验证了服务前后中间件与中间件数组的执行顺序——请求体先被两个before中间件追加字段服务返回后又被两个after中间件追加字段最终响应包含全部四个字段证明before/after链按声明顺序精确执行。paramsreq.feathers 与服务参数注入所有 Express 中间件都可以通过req.feathers对象为服务方法调用设置params属性import { feathers } from feathersjs/feathers import type { Id, Params } from feathersjs/feathers import express, { json, urlencoded, rest } from feathersjs/express const app express(feathers()) app.use(json()) app.use(urlencoded({ extended: true })) app.use(function (req, res, next) { req.feathers.fromMiddleware Hello world next() }) app.configure(rest()) app.use(todos, { async get(id: Id, params: Params) { console.log(params.provider) // - rest console.log(params.fromMiddleware) // - Hello world return { id, params, description: You have to do ${id}! } } }) app.listen(3030)应避免直接整体赋值req.feathers something因为它可能已经包含其他 Feathers 插件依赖的信息更稳妥的做法是逐个添加属性或使用{ ...req.feathers, something }展开合并。⚠️重要Express 中间件的执行顺序至关重要任何设置服务参数的中间件都必须注册在app.configure(rest())之前或作为 自定义服务中间件 注册。提示虽然可以为了方便而在服务中访问req.feathers.req req以拿到请求对象但更推荐让服务尽可能与传输层解耦。通常可以在中间件里预处理数据使服务无需感知 HTTP 请求与响应对象。params.queryURL 查询参数params.query包含客户端发来的 URL 查询参数。REST 传输使用 qs。⚠️注意只有params.query会在服务端与客户端之间传递params的其他部分不会。这是出于安全考虑——防止客户端篡改params.user或数据库选项等敏感信息。你始终可以在 Hook 中把params.query映射到其他params属性。例如请求GET /messages?readtrue$sort[createdAt]-1会将params.query设置为{ read: true, $sort: { createdAt: -1 } }提示URL 是字符串因此可能需要进行类型转换通常借助 查询 Schema 与 Resolver 完成。注意当请求中的数组元素超过 20 个时qs 解析器会隐式地把它转换为以索引为键的对象。如需扩大限制可设置自定义查询解析器app.set(query parser, str qs.parse(str, {arrayLimit: 1000}))。params.provider传输来源标记通过 REST 发起的任何 服务方法调用其params.provider都会被设置为rest。在 Hook 中可据此阻止外部用户执行特定调用import { HookContext } from declarations app.service(users).hooks({ before: { remove: [ async (context: HookContext) { // 检查 if(context.params.provider) 可阻止一切外部调用 if (context.params.provider rest) { throw new Error(You can not delete a user via REST) } } ] } })该标记由绑定层统一注入feathersExpress在初始化时就挂载了req.feathers { ...req.feathers, provider: rest }见 packages/express/src/index.ts测试 rest.test.ts 也断言了provider: rest与中间件注入的test: Happy会一并出现在服务收到的params中。params.headers请求头params.headers包含原始服务请求的 HTTP 头serviceMiddleware中通过const params { query, headers, route, ...req.feathers }合并见 rest.ts。params.route路由占位符服务 URL 中的 Express 路由占位符会被添加到服务的params.route中。何时该用、何时不该用嵌套路由详见 FAQ 中的嵌套/自定义路由条目。import { feathers } from feathersjs/feathers import express, { rest } from feathersjs/express const app express(feathers()) app.configure(rest()) app.use(function (req, res, next) { req.feathers.fromMiddleware Hello world next() }) app.use(users/:userId/messages, { async get(id, params) { console.log(params.query) // - ?query console.log(params.provider) // - rest console.log(params.fromMiddleware) // - Hello world console.log(params.route.userId) // 请求 GET /users/1/messages 时为 1 return { id, params, read: false, text: Feathers is great!, createdAt: new Date().getTime() } } }) app.listen(3030)仓库测试 rest.test.ts 验证了嵌套路由参数的正确性注册/:appId/:id/todo后请求/theApp/myId/todo/dishesparams.route得到{ appId: theApp, id: myId }同时dishes作为服务get的id参数。内置中间件feathersjs/express自带以下中间件全部可从模块导出中按需引用。notFound(options)notFound()返回一个中间件抛出NotFound404Feathers 错误。它应被用作错误处理器之前的最后一个中间件。可用选项verbose设为true时错误消息中包含请求的 URL默认falseimport { notFound, errorHandler } from feathersjs/express // 返回包含 URL 的错误 app.use(notFound({ verbose: true })) app.use(errorHandler())源码实现见 packages/express/src/handlers.ts构造Page not found: url消息并附带{ url }作为错误的额外数据。errorHandler()errorHandler是一个 Express 错误处理中间件会把 REST 调用产生的任意错误响应格式化为 JSON当有人直接在浏览器中访问 API 时则输出 HTML并设置正确的错误码。提示你仍然可以在 Feathers 中使用任何其他 Express 兼容的错误中间件。重要与 Express 一样错误处理器必须注册在所有中间件和服务之后。app.use(errorHandler())使用默认配置启用错误处理器import { feathers } from feathersjs/feathers import express from feathersjs/express const app express(feathers()) // 在应用启动前注册 app.use(express.errorHandler())app.use(errorHandler(options))import { feathers } from feathersjs/feathers import express from feathersjs/express const app express(feathers()) // 与 Express 一样错误中间件必须放在中间件链的最后 app.use( express.errorHandler({ html: function (error, req, res, next) { // 用 error 对象渲染你的错误视图 res.render(error, error) } }) ) app.use( errorHandler({ html: { 404: path/to/notFound.html, 500: there/will/be/robots.html } }) )⚠️重要如果你希望响应为 JSON 格式务必在请求中把Accept头设置为application/json否则默认错误处理器会返回 HTML。创建错误处理器时可传入以下选项htmlFunction | Object可选自定义格式化函数或包含自定义 HTML 错误页面路径的对象也可设为false完全禁用 HTML 错误页面只返回 JSON。loggerFunction | false默认console设置用于记录错误的日志对象将以logger.error(error)方式记录。源码还支持jsonFunction | Object可选与public静态资源目录默认指向包的public目录见 packages/express/src/handlers.ts。默认的 HTML 错误页面位于packages/express/public下的401.html、404.html、default.html。错误处理器的内部行为handlers.ts值得说明非 Feathers 错误会被包装为GeneralError保留原始errors与stack根据content-type或Accept头选择 JSON 或 HTML 格式输出JSON 输出在生产环境NODE_ENV production会剔除stack字段404 错误则不附带堆栈5xx 错误走logger.error4xx 走logger.info。仓库测试 error-handler.test.ts 覆盖了自定义 HTML/JSON 处理器、按错误码401/402/406分发、默认回退、非 Feathers 错误转换GeneralError、以及text/html与application/json两种格式的完整输出。authenticate()express.authenticate(...strategies)允许用一个已注册了可解析 HTTP 头的 认证服务及其 策略来保护 Express 中间件。认证信息会被写入req.feathers对象如req.feathers.user。下面的例子用 JWT 策略保护/hello端点因此请求需要携带Authorization: Bearer JWT头并用用户邮箱渲染消息import { authenticate } from feathersjs/express app.use(/hello, authenticate(jwt), (req, res) { const { user } req.feathers res.render(Hello ${user.email}) }) // 配合非默认的认证服务使用 app.use( /hello, authenticate({ service: v2/auth, strategies: [jwt, api-key] }), (req, res) { const { user } req.feathers res.render(Hello ${user.email}) } )当用户点击普通链接时浏览器不会发送对应的认证头。为了让浏览器链接也能发起已认证的请求有两个方案一是使用会话详见 服务端渲染指南二是把 JWT 访问令牌放到查询字符串中再映射为认证请求import { authenticate } from feathersjs/express const setQueryAuthentication (req, res, next) { const { access_token } req.query if (access_token) { req.authentication { strategy: jwt, accessToken: access_token } } next() } // 通过 hello?access_tokenyour jwt 访问 app.use(/hello, setQueryAuthentication, authenticate(jwt), (req, res) { const { user } req.feathers res.render(Hello ${user.email}) })如何从认证客户端获取访问令牌参见 认证客户端文档。⚠️注意使用 HTTPS 时 URL 是加密的但采用这种方式必须确保访问令牌不会被任何日志机制记录下来。从源码看packages/express/src/authentication.tsauthenticate本质上是对feathersjs/authentication的authenticateHook 的封装它把req.feathers作为params构造 HookContext 执行认证再把认证后的context.params回写为req.feathers。认证测试 authentication.test.ts 验证了无 JWT 时返回NotAuthenticated、携带Authorization: Bearer token时可访问受保护端点。parseAuthenticationparseAuthentication中间件会被rest()自动注册使用默认 认证服务 的策略解析请求头中的认证信息。如果你还希望用另一个认证服务解析认证可以带着该服务的配置再次注册此中间件import { parseAuthentication } from feathersjs/express app.use( parseAuthentication({ service: api/v1/authentication, strategies: [jwt, local] }) )源码实现authentication.ts会调用app.defaultAuthentication(service)解析出认证服务策略取settings.strategies或配置中的parseStrategies/authStrategies解析成功后把authentication合并进req.feathers。测试 authentication.test.ts 验证了当认证服务没有配置任何解析策略时请求将无法通过认证。cors 与 compressioncors对 cors 模块的引用用于跨域配置compression对 compression 模块的引用用于响应压缩。两者均由 模块入口 直接导入并原样导出测试中可见app.use(express.cors())的用法rest.test.ts。内置 Express 中间件Built insfeathersjs/express同样重新导出了标准的 Express 中间件json—— JSON 请求体解析器urlencoded—— URL 编码表单请求体解析器serveStatic别名static—— 静态托管目录中的文件Router—— 创建 Express 路由对象此外还导出了raw、text、query以及原生express模块引用original见 packages/express/src/index.ts。小结Express 集成的最佳实践要点综合文档与源码使用feathersjs/express时有几个关键顺序与约定需要牢记先绑定后配置express(feathers())必须在注册任何服务之前完成绑定层会重写use/listen并注入provider标记。请求体解析器先行json()、urlencoded()与参数注入中间件必须注册在所有服务之前。错误处理器殿后errorHandler必须放在中间件链的最后notFound则紧邻其前。优先使用 Hook 而非自定义服务中间件自定义中间件只对 REST 生效Hooks 对所有传输方式通用。善用params约定params.query是客户端唯一可传入的数据通道params.provider用于区分传输来源params.route承载嵌套路由参数服务应尽量保持传输无关。特殊部署场景手动setup子应用、HTTPS、虚拟主机三种场景下需要显式调用app.setup(server)。通过本文的配置示例与源码佐证你可以直接在自己的 Feathers 项目中完成 Express 绑定、REST 服务暴露、中间件编排、认证保护与错误处理的全套搭建若需进一步深入可继续阅读 应用配置、服务与 Hook、数据库查询 及 Schema 校验 等章节。【免费下载链接】feathersThe API and real-time application framework项目地址: https://gitcode.com/gh_mirrors/fe/feathers创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考