news 2026/10/7 10:00:43

ShareDB 服务端核心 Backend 类完全解析:构造选项、中间件钩子与数据读写 API

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ShareDB 服务端核心 Backend 类完全解析:构造选项、中间件钩子与数据读写 API
  • 后端
  • 数据库

【免费下载链接】sharedb

Realtime database backend based on Operational Transformation (OT)

项目地址:https://gitcode.com/gh_mirrors/sh/sharedb
点击查看免费下载

ShareDB 是一个基于操作转换(Operational Transformation,OT)的实时数据库后端,而Backend类正是其**服务端单实例(server-side instance)**的化身:它负责与客户端建立连接、把读写请求派发给数据库适配器,同时承担构造期配置、中间件注册以及投影(projection)定义等工作。本文以官方 API 文档 docs/api/backend.md 为主线,逐项讲解Backend的构造选项、公开属性与全部常用方法,并结合 lib/backend.js 的源码实现与 test/backend.js 测试用例,说明每个 API 的真实调用链与注意事项。读完本文,你将能够独立完成 ShareDB 服务端的初始化、适配器挂载、中间件接入、投影配置,以及服务端直连模式下的文档读写与查询。

Backend() 构造函数

Backend通过sharedb包导出,直接new即可:

var Backend = require('sharedb') new Backend([options])

在源码中(lib/backend.js),构造函数会依次完成默认值注入、内部状态初始化(projections、middleware、agentsCount等)以及错误处理器的绑定。构造函数的全部可选参数如下。

构造选项总览

db—— 数据库适配器(可选)
  • 默认值:new MemoryDB()(一个全新的内存数据库实例)
  • 作用:ShareDB 的数据存储层,即数据库适配器实例,负责持久化文档内容与操作(op)日志

⚠️重要警告:默认的内存适配器是非持久化的。内存数据库把所有文档与操作都存放在进程内存中(见 lib/db/memory.js 的注释:内存占用无上限增长、无法跨 Node 进程扩展、服务重启即丢数据),绝不能用于生产环境。生产环境应显式传入如sharedb-mongo、sharedb-postgres等外部适配器。

var backend = new Backend({ db: new MemoryDB() })
pubsub—— 发布/订阅适配器(可选)
  • 默认值:new MemoryPubSub()(内存 Pub/Sub 实例)
  • 作用:用于在多个 ShareDB 实例之间广播数据变更通知的通道(见 Pub/Sub 适配器)

与数据库适配器不同,文档明确说明:内存 Pub/Sub 适配器在单机独立部署的生产环境中是允许使用的,因为 Pub/Sub 状态无需跨进程持久化。只有当需要横向扩展为多个服务器进程时,才需要换成外部的 Pub/Sub 适配器(如 Redis 类实现)。内存实现本身是完整可用的(lib/pubsub/memory.js)。

milestoneDb—— 里程碑数据库适配器(可选)
  • 默认值:null(源码中实际注入为new NoOpMilestoneDB(),见 lib/milestone-db/no-op.js)
  • 作用:里程碑快照(milestone snapshots)的数据存储,里程碑快照是按指定版本间隔保存的文档历史快照

如果省略该选项,里程碑快照将不启用,但文档历史仍可访问,只是可能带来性能损耗。

extraDbs—— 附加数据库集合(可选)
  • 默认值:{}
  • 作用:一个对象,其值可以是额外的DB适配器实例,用于查询场景。对象的键即为查询选项db字段中可传入的名字

在源码的查询路径中(lib/backend.js),queryFetch/querySubscribe会读取options.db,并到backend.extraDbs[options.db]中查找对应的适配器;找不到时会抛出ERR_DATABASE_ADAPTER_NOT_FOUND错误。

suppressPublish—— 是否抑制发布(可选)
  • 默认值:false
  • 作用:设为true时,所有已提交的变更不会通过 Pub/Sub 对外发布。适用于希望写入数据库但不通知其他订阅实例的场景
maxSubmitRetries—— 提交最大重试次数(可选)
  • 默认值:null
  • 作用:允许一次提交(submit)被重试的次数。省略时,请求将被无限次重试

这对应 OT 并发冲突处理的底层机制:当两个客户端同时提交基于同一版本的 op 时,后提交者需要基于新版本重新转换并重试提交(详见SubmitRequest的提交逻辑)。

presence—— 启用在线状态(可选)
  • 默认值:false
  • 作用:设为true时启用 Presence(在线光标、在线用户列表等实时状态)功能。源码中对应this.presenceEnabled = !!options.presence(lib/backend.js),且Agent._handleMessage中只有在presenceEnabled为真时才会处理 presence 相关的消息类型(lib/agent.js)
doNotCommitNoOps—— 是否不提交空操作(可选)
  • 默认值:false
  • 作用:设为true时,将避免把 no-op(空操作,或经转换后变成 no-op 的操作)提交到数据库。客户端提交的 no-op 会像正常提交一样被确认(ack),但文档版本不会递增。该选项对需要严格控制版本号增长的场景非常有用
errorHandler—— 非致命错误处理器(可选)
  • 默认值:ShareDB 默认将错误写入日志(源码默认实现即logger.error(error),见 lib/backend.js)
  • 作用:ShareDB 服务端运行中出现的非致命错误都会被传入该函数
function(error, context) { logger.error(error) }

在 test/backend.js 中可以看到new Backend({errorHandler: handler})的用法,测试通过自定义 handler 捕获并断言服务端错误。当需要把服务端错误接入自己的监控/报警体系时,自定义该选项是标准做法。

属性

MIDDLEWARE_ACTIONS—— 中间件动作映射

Backend暴露一个MIDDLEWARE_ACTIONS对象,其值为可用的中间件动作名映射。在源码中(lib/backend.js),它定义了 13 个动作,完整列表如下:

动作触发时机
afterWrite一个操作成功写入数据库之后
apply一个操作即将被应用到快照上、尚未提交到数据库
commit一个操作已应用到快照、即将写入数据库
connect新客户端连接到服务器
op一个操作从数据库加载出来
query一个查询即将发送到数据库
readSnapshots快照已从数据库读取、即将返回给客户端
receive收到来自客户端的消息
reply即将向客户端发送非错误回复
receivePresence服务器收到 presence 信息
sendPresence即将向客户端发送 presence 信息
submit一个操作已提交到服务器

此外,源码还定义了SNAPSHOT_TYPES(current、byVersion、byTimestamp),用于标识readSnapshots中间件中快照的读取类型,具体可参考 docs/middleware/actions.md。

中间件的注册与触发分别由use()与trigger()完成:trigger会把request.action、request.agent、request.backend挂到上下文上,并串行执行该动作下的所有中间件函数(lib/backend.js)。

方法

connect()

建立与 ShareDB 的连接,返回一个用于与 ShareDB 交互的Connection客户端实例。它是浏览器端new Connection(socket)的服务端等价物——也就是说,你可以在 Node 服务端直接创建并操作文档对象,而不必经过网络。

backend.connect([connection [, request]])

参数说明:

  • connection(可选,Connection实例):要绑定到Backend的连接。默认会新建一个Connection实例。
  • request(可选,Object,默认{}):连接上下文对象,可携带 cookie、会话数据等信息,供中间件中通过agent.custom读取。

返回值:一个Connection。

从源码看(lib/backend.js),connect()内部会创建一个StreamSocket并调用listen()生成Agent,随后把agent引用挂到connection.agent上——这为服务端代码在中间件中缓存和读取会话状态提供了便利。

listen()

将一个Stream(Node.js 流)注册到后端。当服务器收到来自客户端的新连接时应调用此方法。

backend.listen(stream [, request])
  • stream:一个Stream(或Stream-like 对象),用于新Agent与Backend之间的通信。
  • request(可选,Object,默认{}):连接上下文对象,同样会在中间件中以agent.custom形式暴露。

返回值:一个Agent,该Agent也会在中间件上下文中可用。

这是 WebSocket 接入 ShareDB 的标准入口。结合 getting-started 中的示例:先用@teamwork/websocket-json-stream把 WebSocket 转成Stream,再交给backend.listen(stream):

webSocketServer.on('connection', (webSocket) => { var stream = new WebSocketJSONStream(webSocket) backend.listen(stream) })

在源码中(lib/backend.js),listen()会new Agent(this, stream)并触发connect中间件;若中间件返回错误,则直接关闭该Agent。

close()

断开 ShareDB 及其全部底层服务(数据库、Pub/Sub 等)。

backend.close([callback])
  • callback(可选,Function):function(error) { ... },在所有服务停止后调用;若至少有一个服务无法停止,则以 error 形式回调。

源码实现(lib/backend.js)会依次关闭pubsub、db、milestoneDb,并遍历关闭extraDbs中所有附加数据库,全部完成后才触发回调。

use()

注册中间件。

backend.use(action, middleware)
  • action:string | string[]——单个动作名或动作名数组,决定中间件在何时执行。可用动作见上文MIDDLEWARE_ACTIONS。
  • middleware:function(context, next) { next(error) }——一个中间件函数。

源码支持传入动作数组,内部会递归地对每个动作注册同一中间件(lib/backend.js)。test/backend.js中可以看到典型用法,例如在afterWrite、submit等动作上挂载中间件(test/backend.js)。中间件上下文context上始终带有action、agent、backend三个属性,以及各动作特有的附加属性,详见 docs/middleware/actions.md。

addProjection()

定义一个投影(projection)。

backend.addProjection(name, collection, fields)
  • name(string):投影的名字。
  • collection(string):被投影的目标集合。
  • fields(Object):需要投影的字段集合,键为字段名,值必须为true:
share.addProjection('names', 'users', {name: true})

⚠️不支持子字段投影。也就是说,不能定义像{profile: {name: true}}这样的嵌套字段筛选。

从源码看(lib/backend.js),addProjection()会做严格校验:投影名重复会抛出Projection already exists,字段值不是true会抛出Invalid field错误。投影被保存在this.projections映射中,后续submit、getOps、fetch等所有方法都会据此把投影名解析为真实集合名(projection.target)。

submit()

向Backend提交一个操作。

backend.submit(agent, index, id, op [, options [, callback]])
  • agent:Agent实例,会传给中间件。
  • index(string):集合名或投影名。
  • id(string):文档 ID。
  • op(Object):要提交的操作。
  • options(可选,Object,默认{}):透传给数据库适配器commit方法的选项;适配器commit支持的任何选项都可在此使用。
  • callback(可选):function (error, ops) { ... },其中ops是从提交的 op 被提交到真正落库之间,其他客户端提交的 op 列表——这正是 OT 变换后需要回放给客户端的那部分操作。

在源码中(lib/backend.js),submit()会先做ot.checkOp(op)合法性检查,然后依次触发submit中间件 →SubmitRequest.submit()(内部完成 OT 变换与数据库commit)→afterWrite中间件 → 对 ops 做投影清洗(_sanitizeOps)。在客户端提交路径中,Agent._submit会在成功后将ops通过_sendOps发送给客户端,让客户端补齐错过的操作(lib/agent.js)。

getOps()

获取某个文档在指定版本区间内的操作记录,其中from包含、to不包含。

backend.getOps(agent, index, id, from, to [, options [, callback]])
  • agent:Agent实例,传给中间件。
  • index(string):集合名或投影名。
  • id(string):文档 ID。
  • from(number):要获取的起始 op 版本;设为null则从最早版本开始获取。
  • to(number):结束版本,不会被获取(即to不包含);设为null则一直获取到最新版本。
  • options(可选,Object,默认{}):
    • options.opsOptions(可选,默认{}):直接透传给数据库驱动的getOps,例如请求 op 元数据:
{ opsOptions: { metadata: true, }, }
  • callback:function (error, ops) { ... },成功时返回请求到的 ops。

从源码看(lib/backend.js),getOps()会把投影解析为目标集合,把agent.custom注入opsOptions,然后调用db.getOps(),最后对每个 op 执行_sanitizeOps(投影过滤 +op中间件)。因此返回给调用方的 ops 已经过投影与中间件处理。

getOpsBulk()

批量获取一个集合中多个文档在指定版本区间内的操作记录,语义与getOps一致(from包含、to不包含)。

backend.getOpsBulk(agent, index, fromMap, toMap [, options [, callback]])
  • agent:Agent实例。
  • index(string):集合名或投影名。
  • fromMap(Object):键为文档 ID,值为该文档请求的起始版本(包含)。例如{abc: 3}表示获取文档abc从版本3起的 ops。
  • toMap(Object):键为文档 ID,值为该文档请求的结束版本(不包含)。例如{abc: 3}表示获取文档abc截至版本3(不含)的 ops。
  • options(可选,Object,默认{}):options.opsOptions直接透传给数据库驱动的getOpsBulk,用法同getOps。
  • callback:function (error, opsMap) { ... },返回文档 ID 到 ops 的映射,如{abc: []}。

fetch()

获取一个文档的当前快照。

backend.fetch(agent, index, id, [, options [, callback]])
  • agent:Agent实例。
  • index(string):集合名或投影名。
  • id(string):文档 ID。
  • options(可选,Object,默认{}):options.opsOptions直接透传给数据库驱动的fetch,用法同上。
  • callback:function (error, snapshot) { ... },成功时返回请求的快照。

在源码中(lib/backend.js),fetch()调用db.getSnapshot()拿到快照后,会经_sanitizeSnapshots处理:若有投影则先做快照投影裁剪,再触发readSnapshots中间件(快照类型为SNAPSHOT_TYPES.current)。如果中间件通过request.rejectSnapshotRead(snapshot, error)拒绝了该快照的读取,fetch也会以错误回调(见_sanitizeSnapshots对"部分拒绝"的处理,lib/backend.js)。

fetchBulk()

从集合中批量获取多个文档快照。

backend.fetchBulk(agent, index, ids, [, options [, callback]])
  • agent:Agent实例。
  • index(string):集合名或投影名。
  • ids(string[]):文档 ID 数组。
  • options(可选,Object,默认{}):options.opsOptions直接透传给数据库驱动的fetchBulk。
  • callback:function (error, snapshots) { ... },成功时返回文档 ID 到快照的映射。

源码中(lib/backend.js),fetchBulk通过db.getSnapshotBulk()批量读取,并支持"部分拒绝":当readSnapshots中间件拒绝了其中部分文档的快照读取时,被拒绝的文档在返回的snapshotMap中会以{error: ...}对象形式标记,其余文档正常返回,整体不会因单个文档被拒而失败。

queryFetch()

获取匹配查询条件的快照。在大多数情况下,直接查询底层数据库更合适;但queryFetch可以在避免Doc实例开销的前提下应用中间件链路。

backend.queryFetch(agent, index, query, [, options [, callback]])
  • agent:Agent实例。
  • index(string):集合名或投影名。
  • query(Object):查询对象,其格式取决于所用的数据库适配器(例如sharedb-mongo使用 MongoDB 查询语法,而内置 MemoryDB 默认无查询支持,直接返回集合全部文档,见 lib/db/memory.js)。
  • options(可选,Object,默认{}):
    • options.db(可选,string):指定在哪个数据库上执行查询。这些附加数据库通过构造选项extraDbs挂载。
  • callback:function (error, snapshot) { ... },成功时返回请求的快照。

源码路径(lib/backend.js 与 lib/backend.js)显示:queryFetch先触发query中间件(中间件可以改写查询、频道甚至切换数据库),随后从options.db或extraDbs解析出目标数据库,调用其query()方法,最后对结果快照执行投影裁剪与readSnapshots中间件。

源码中的其他实用方法(补充)

除文档列出的 API 外,lib/backend.js 中还实现了一批供客户端协议路径使用的方法,了解它们有助于理解 Backend 的全貌:

  • subscribe(agent, index, id, version, ...)(lib/backend.js):订阅文档变更流,配合pubsub.subscribe(channel)使用。version为null时同时抓取当前快照,否则只抓取指定版本之后的 ops。
  • subscribeBulk(agent, index, versions, callback)(lib/backend.js):批量订阅版本映射。
  • querySubscribe(agent, index, query, options, callback)(lib/backend.js):订阅查询结果并随数据变化推送 diff,内部使用QueryEmitter轮询数据库。
  • fetchSnapshot(agent, index, id, version, callback)与fetchSnapshotByTimestamp(agent, index, id, timestamp, callback)(lib/backend.js):按版本号或时间戳获取历史快照,需要milestoneDb配合,并会结合getOps从里程碑快照重建到目标版本的文档状态。
  • getChannels(collection, id)(lib/backend.js):返回文档订阅涉及的频道(集合频道collection与文档频道collection.id),是 Pub/Sub 频道命名的核心逻辑。

一个完整的服务端使用示例

综合以上 API,一个典型的生产级 ShareDB 服务端初始化如下:

var Backend = require('sharedb') var backend = new Backend({ db: dbAdapter, // 例如 sharedb-mongo 实例,勿用默认 MemoryDB 上生产 pubsub: pubsubAdapter, // 多实例部署时可换成外部 Pub/Sub 适配器 milestoneDb: milestoneAdapter, // 可选,启用里程碑快照 presence: true, // 启用在线状态功能 doNotCommitNoOps: true, // 可选:跳过 no-op 提交 suppressPublish: false, maxSubmitRetries: 5, // 限制提交重试次数 errorHandler: function(error, context) { myMonitoring.report(error) } }) // 中间件与投影 backend.use(backend.MIDDLEWARE_ACTIONS.submit, function(context, next) { // 提交前校验或改写 next() }) backend.addProjection('names', 'users', {firstName: true, lastName: true}) // WebSocket 接入 webSocketServer.on('connection', (webSocket) => { var stream = new WebSocketJSONStream(webSocket) backend.listen(stream, {cookies: req.cookies}) // req 可在 connect 中间件中通过 agent.custom 读取 }) // 服务端直连读写(等价于服务端版本的客户端 Connection) var connection = backend.connect() var doc = connection.get('users', '123') doc.fetch(function(error) { // ... }) process.on('SIGTERM', function() { backend.close(function() { process.exit(0) }) })

小结

Backend是 ShareDB 服务端的枢纽对象,它把四类职责收敛在一个类中:构造期配置(数据库、Pub/Sub、里程碑库、附加库及各类行为开关)、连接管理(listen/connect/close)、扩展点(use注册中间件、addProjection定义投影),以及数据读写(submit、getOps、getOpsBulk、fetch、fetchBulk、queryFetch)。理解每个选项在 lib/backend.js 中的落点,以及各方法如何串联"中间件 → OT 变换 → 适配器调用 → 投影清洗"这条链路,是安全、高效地把 ShareDB 集成进生产系统的前提。更深入的中间件动作定义、数据库与 Pub/Sub 适配器接口、投影与文档历史机制,可分别查阅 docs/middleware/actions.md、docs/adapters/database.md、docs/adapters/pub-sub.md、docs/projections.md 与 docs/document-history.md。

  • 后端
  • 数据库

【免费下载链接】sharedb

Realtime database backend based on Operational Transformation (OT)

项目地址:https://gitcode.com/gh_mirrors/sh/sharedb
点击查看免费下载
上一篇:用 Turf.js 计算两点间地理方位角(Bearing):@turf/bearing 完全指南
下一篇:external-speaker外接喇叭进阶:仿照它的积木架构开发你自己的硬件扩展(完整教程)

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 9:59:04

嵌入式工程师起薪分水岭:硬件理解、代码密度与系统闭环能力

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 9:57:41

DOTS物理Raycast实战:从ECS到Job System的并行检测

1. 为什么 DOTS 里的 Raycast 值得单独搞懂很多 Unity 开发者第一次接触 DOTS 时,最先发出的疑问往往是同一个:物理检测怎么写?在传统 MonoBehaviour 开发里,一个Physics.Raycast就搞定了,API 简单直接,文档…

作者头像 李华