@converse/skeletor

中文文档
4.0.2发布于 2026/11/05MITTypeScriptESM + CJS近 30 天下载 3324

Models and Collections for modern web apps

原始使用文档
文档目录19 节
  1. Why Skeletor?
  2. Installation
  3. Quick Start
  4. Model
  5. Collection
  6. Local Persistence
  7. Events
  8. Integration with React
  9. Features at a Glance
  10. Design philosophy
  11. When to use it
  12. When to reach for something else
  13. Direction
  14. Migrating from Backbone
  15. Changes from Backbone
  16. Modernizations
  17. What was removed
  18. Method renames (Underscore → Lodash)
  19. Other behavioural changes

Skeletor

Skeletor

XMPP Chat CI Tests

Skeletor is a lightweight, TypeScript-first reactive data library.

It lets you define typed models and react to changes with events or subscriptions, while persisting to IndexedDB, localStorage, sessionStorage, SQLite (Node) or a REST API.

Skeletor is a modernized rewrite of Backbone's Models and Collections, without jQuery, without Underscore, and without Views or Routing. If you know Backbone, everything transfers directly. If you don't, there's nothing to unlearn.

Skeletor powers Converse.js, a full-featured open-source XMPP chat client.

Why Skeletor?

  • Reactive models — set an attribute, get a change event. Subscribe with a callback and an unsubscribe function. Works with any UI layer.
  • Direct attribute access — model.attrs.name = 'Bob' fires change events, no boilerplate needed.
  • Computed properties — declare derived values with explicit dependencies; they cache, recalculate, and fire change events automatically.
  • Store-style subscriptions — subscribe() returns an unsubscribe function, compatible with React's useSyncExternalStore and similar APIs.
  • Built-in persistence — IndexedDB, localStorage, sessionStorage, SQLite (Node), and REST out of the box.
  • TypeScript-first — full type definitions, generic model attributes, typed computed properties.
  • No jQuery, no Underscore — native browser APIs and lodash-es with individual imports for tree-shaking.
  • Works anywhere — browser, Node.js (22+), Web Workers. ESM + CJS builds included.
  • Backbone-compatible — get, set, events, collections: all there. Drop-in replacement for Backbone's data layer.

Installation

npm install @converse/skeletor

Quick Start

Model

import { Model } from '@converse/skeletor';

interface UserAttrs {
  firstName: string;
  lastName: string;
  active: boolean;
}

class User extends Model<UserAttrs> {
  get defaults() {
    return { firstName: '', lastName: '', active: false };
  }

  get computed() {
    return {
      fullName: {
        deps: ['firstName', 'lastName'],
        fn: (model: User) => `${model.get('firstName')} ${model.get('lastName')}`,
      },
    };
  }
}

const user = new User({ firstName: 'Alice', lastName: 'Smith' });

// Read — three equivalent ways
user.get('firstName'); // → 'Alice'
user.attrs.firstName; // → 'Alice'
user.get('fullName'); // → 'Alice Smith'  (computed — cached, never persisted)

// Write — fires change events
user.attrs.firstName = 'Bob'; // triggers 'change:firstName' and 'change:fullName'
user.set('active', true); // triggers 'change:active'

// React to changes
user.on('change:fullName', (model, value) => {
  console.log('Name is now', value);
});

// Store-style subscription (returns unsubscribe function)
const unsub = user.subscribe((model, changed) => {
  console.log('Changed attrs:', changed);
});
unsub(); // clean up

Collection

import { Model, Collection } from '@converse/skeletor';

class User extends Model {}

class Users extends Collection {
  get model() {
    return User;
  }
  get url() {
    return '/api/users';
  }
}

const users = new Users([
  { id: 1, name: 'Alice', active: true },
  { id: 2, name: 'Bob', active: false },
]);

users.add({ id: 3, name: 'Carol', active: true });

const active = users.filter((u) => u.get('active'));
const names = users.pluck('name'); // → ['Alice', 'Bob', 'Carol']

// Subscribe to structural changes (fires once per operation, not per model)
const unsub = users.subscribe((collection) => {
  console.log('collection changed, length:', collection.length);
});

// Load from the server
await users.fetch();

Local Persistence

Skeletor can persist models and collections to IndexedDB, localStorage, sessionStorage, or SQLite (Node.js) with no extra packages.

Manual persistence

The traditional Backbone-style API: set storage once, call save()/fetch() explicitly.

import { Model, PersistentStorage } from '@converse/skeletor';

class Settings extends Model {
  initialize() {
    this.storage = new PersistentStorage('app-settings', 'local');
  }
}

const settings = new Settings({ id: 'main' });
await settings.fetch(); // load from localStorage
settings.set('theme', 'dark');
await settings.save(); // write to localStorage

Automatic persistence (autoSync)

Opt in once, then every write persists automatically — no save() calls needed.

import { Model, PersistentStorage } from '@converse/skeletor';

class Settings extends Model {
  get autoSync() {
    return true;
  }

  initialize() {
    this.storage = new PersistentStorage('app-settings', 'local');
  }
}

const settings = new Settings({ id: 'main' });
await settings.hydrated; // resolves when prior data has been loaded

settings.attrs.theme = 'dark'; // persisted automatically (debounced)
settings.set('lang', 'en'); // also auto-saved

// Pass { noAutoSave: true } to suppress persistence for a specific set() call
settings.set('transient', true, { noAutoSave: true });

Pending writes are debounced (see autoSyncDelay, default 100ms) and flushed when the page is hidden (visibilitychange) or unloaded (pagehide).

When autoSync is off, hydrated is undefined, but await model.hydrated is still safe (awaiting undefined is a no-op), so code can await it uniformly.

On a Collection, autoSync is hydrate-only: the collection loads its stored state on construction but does not auto-save on add/remove/reset. To persist the contents, enable autoSync on the contained model class — each model then writes its own record.

Supported backends: 'local' (localStorage), 'session' (sessionStorage), 'indexed' (IndexedDB), 'memory', 'node' (SQLite, Node 22+).

[!NOTE] Durability on unload is best-effort, not guaranteed. The flush on visibilitychange/pagehide is synchronous, but the underlying write may not be:

  • With synchronous backends ('local', 'session') the flush completes before the page tears down, so the last write is safe.
  • With asynchronous backends ('indexed') the flush only starts the write; the browser can kill the page before an IndexedDB transaction commits, losing the most recent debounced change. The browser gives no primitive to await a write during unload, so this is inherent rather than a bug.

visibilitychange → hidden fires earlier and far more reliably than pagehide (especially on mobile, where backgrounded tabs are killed without pagehide), so it is the real save point. To shrink the loss window further with an async backend, lower autoSyncDelay. If a write must never be lost, call await model.save() explicitly rather than relying on the unload flush.

Events

import { EventEmitter } from '@converse/skeletor';

class Store extends EventEmitter {}

const store = new Store();
store.on('update', (data) => console.log('Updated:', data));
store.trigger('update', { key: 'value' });

// Listen to another object's events (auto-cleaned up with stopListening)
const view = new EventEmitter();
view.listenTo(store, 'update', (data) => console.log('View saw:', data));
view.stopListening(); // removes all listeners set up via listenTo

// subscribe() returns an unsubscribe function
const unsub = store.subscribe('update', (data) => console.log(data));
unsub();

Integration with React

subscribe() is directly compatible with React's useSyncExternalStore:

import { useSyncExternalStore } from 'react';

function UserName({ user }) {
  const attrs = useSyncExternalStore(
    (cb) => user.subscribe(cb),  // subscribe — returns unsub
    () => user.toJSON()          // getSnapshot
  );
  return <span>{attrs.firstName}</span>;
}

Features at a Glance

ExportWhat it provides
Modelget/set, attrs proxy, computed properties, change tracking, validation, server sync
CollectionFull array API plus where, findWhere, pluck, groupBy, keyBy, countBy, sortBy
EventEmitteron/off/trigger/once, listenTo/stopListening, subscribe() returning an unsubscribe function
PersistentStorageIndexedDB, localStorage, sessionStorage, SQLite (Node), and in-memory backends. autoSync for transparent auto-save and auto-hydrate.
syncLow-level Fetch-based HTTP function (override for custom transports)

Design philosophy

Skeletor is a headless data layer, not an application-state framework. It owns your domain model (typed models and collections, their relationships, persistence, and server sync) and stays out of your view layer. Bind it to whatever renders your UI: Lit, React (via useSyncExternalStore), or plain DOM.

That scope is deliberate. Skeletor isn't trying to compete with signal libraries, Zustand, or Redux for general UI state. What it's uniquely good at is giving a real, long-lived application a typed, persistent, promise-capable data layer it can adopt incrementally (above all an existing Backbone codebase that can't afford a rewrite).

Principles that follow from that:

  • Incremental over big-bang. Every feature is opt-in and backwards-compatible. Adopt attrs, computed, subscribe, or autoSync one model or one callsite at a time; nothing forces a migration.
  • Promise-capable, callback-compatible. Async operations return promises, while Backbone-style success/error callbacks and events keep working. New code can await, old code doesn't break.
  • Derive, don't duplicate. computed expresses derived values once, with caching and automatic change events.
  • Headless and framework-agnostic. No views, no router, no framework assumptions. The reactive surface is change events and subscribe().
  • Honest about tradeoffs. Where a guarantee can't be made (e.g. async persistence on page unload) the docs say so plainly rather than implying durability that doesn't exist (see the note under Local Persistence).

When to use it

  • You're modernizing a Backbone-era app and want a typed, promise-capable data layer without a rewrite.
  • You want a headless store with built-in persistence that binds to any UI framework.
  • Your state is genuinely domain data — entities with identity, relationships, and a persisted lifecycle.

When to reach for something else

  • You want reactivity that auto-tracks dependencies and updates individual bindings — that's what signals give you. Skeletor notifies per-attribute (change:foo), but you wire each reaction yourself, and it re-renders at component granularity.
  • Your "state" is mostly ephemeral view state (form inputs, toggles, hover) — keep that in your view layer.
  • You need time-travel, structural sharing, or a strict immutable single-store architecture — that's Redux's territory.

Direction

Skeletor is being modernized from within rather than replaced. The trajectory is a promise-first core with events and callbacks as a compatibility shim, and a reactivity model that bridges cleanly to signals. The aim is that consumers never face a migration. The library modernizes underneath them while the public surface stays stable, and the Backbone compatibility that exists today isn't sacrificed to get there.

Migrating from Backbone

If you have an existing Backbone project, you can migrate the data layer incrementally:

  1. Install Skeletor: npm install @converse/skeletor
  2. Replace import Backbone from 'backbone' with named imports: import { Model, Collection } from '@converse/skeletor'
  3. Replace Backbone.Model.extend({...}) with class MyModel extends Model { ... }
  4. Fix the handful of removed or renamed methods listed below
  5. Keep using Backbone for Views, Router, and History — they are unaffected

Your Views and Router don't need to change at all. Skeletor models and collections emit the same events Backbone does.

Changes from Backbone

Modernizations

  • Rewritten in TypeScript with full type definitions
  • Removed the dependency on jQuery
  • Replaced underscore with lodash-es, with individual imports for tree-shaking
  • Uses native browser APIs instead of lodash wherever possible
  • Drops support for older browsers (including IE); requires ES6+
  • All types (Model, Collection) are ES6 classes — use class extends instead of .extend()
  • Adds EventEmitter mixin class (replaces the old Events constructor function)
  • Async operations return Promises
  • ESM build available alongside CJS
  • attrs proxy for direct reactive attribute access
  • computed properties with caching and automatic change events
  • subscribe() returning an unsubscribe function on all reactive objects

What was removed

  • View and ElementView — manage your UI separately
  • Router and History — use the browser's History API or a dedicated router library
  • .extend() static method — use class MyModel extends Model instead
  • clone() on Model — use new MyModel(model.toJSON()) instead
  • chain() and escape() on Model
  • inject, foldl, foldr on Collection — use reduce instead
  • sample, take, tail, initial on Collection
  • without, reject, select on Collection — use filter instead
  • partition and invokeMap on Collection

Method renames (Underscore → Lodash)

Old (Underscore)New (Lodash)
restdrop
indexBykeyBy
invokeinvokeMap (then removed — use map)
containsincludes

Other behavioural changes

  • Collection.prototype.forEach no longer returns the iterated items. Use map instead.
  • Model.prototype.set returns null (not false) when validation fails.
  • Collection.prototype.create returns null (not boolean) on failure.

版本与发布记录

@converse/skeletor 共发布 23 个版本,首次发布于 2020/06/09;过去 12 个月发布了 14 个版本。

版本发布日期距今
4.0.2latest2026/11/055 天前
4.0.12026/10/2814 天前
4.0.02026/10/2517 天前
3.1.62026/08/063 个月前
3.1.52026/07/223 个月前
3.1.42026/06/234 个月前

关于 @converse/skeletor 的常见问题

@converse/skeletor 是什么?

Models and Collections for modern web apps

如何安装 @converse/skeletor?

在项目目录运行 npm install @converse/skeletor,或使用 pnpm add / yarn add / bun add。

@converse/skeletor 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@converse/skeletor 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@converse/skeletor 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@converse/skeletor 可以商用吗?

@converse/skeletor 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@converse/skeletor 的下载量有多少?

根据 npm 官方下载统计,@converse/skeletor 在 2026/10/11 – 2026/11/09 这 30 天内被下载 3,324 次,日均约 111 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@converse/skeletor 还在维护吗?

最新版本 4.0.2 发布于 2026/11/05(5 天前);至今共发布 23 个版本,其中过去 12 个月发布了 14 个;首次发布于 2020/06/09。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。