npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@svton/service

v0.7.0

Published

React service-based state management with decorators

Readme

@svton/service

React 业务领域状态管理库,基于 Class + 装饰器模式。

特性

  • 🎯 业务内聚 - 一个 Service 管理一个业务领域
  • 🔄 双模式 - Scoped(独立实例)和 Provider(共享实例)
  • 💉 依赖注入 - Service 间自动注入
  • 📝 类型安全 - 完整的 TypeScript 支持
  • 精准更新 - 按属性订阅,避免无效渲染

安装

pnpm add @svton/service

快速开始

定义 Service

import { Service, observable, computed, action } from '@svton/service';
import { api, apiAsync } from './lib/api-client';

@Service()
class OrderService {
  @observable orders: Order[] = [];
  @observable loading = false;
  @observable filters = { status: 'all', page: 1 };

  @computed get pendingOrders() {
    return this.orders.filter(o => o.status === 'pending');
  }

  @computed get pendingCount() {
    return this.pendingOrders.length;
  }

  // Generator 函数(推荐用于复杂流程)
  // 请求失败会静默停止执行,不会抛出错误,无需 try-catch
  @action
  *fetch() {
    this.loading = true;
    
    // 如果请求失败,会静默停止,不会执行后续代码
    // 也不会抛出错误,loading 会保持为 true
    const orders = yield* api('GET:/orders', this.filters);
    this.orders = orders;
    
    this.loading = false;
  }

  // Async 函数(简单场景)
  @action
  async setFilter(key: string, value: any) {
    this.filters = { ...this.filters, [key]: value };
    await this.fetch();
  }
}

创建 Hook 和 Provider

import { createService, createServiceProvider } from '@svton/service';

// Scoped 模式(每次调用创建新实例)
export const useOrderService = createService(OrderService);

// Provider 模式(共享实例)
export const OrderProvider = createServiceProvider(OrderService);

Scoped 模式使用

每个组件独立实例,互不影响:

import { useApi } from '@svton/service';
import { apiAsync } from './lib/api-client';

function OrderPanel() {
  const service = useOrderService();

  const orders = service.useState.orders();
  const loading = service.useState.loading();
  const pendingCount = service.useDerived.pendingCount();

  const fetch = service.useAction.fetch();
  const setFilter = service.useAction.setFilter();

  useEffect(() => { fetch(); }, []);

  return (
    <div>
      {loading ? <Spinner /> : <OrderList data={orders} />}
      <span>待处理: {pendingCount}</span>
    </div>
  );
}

// 或者使用 useApi Hook(推荐)
function UserProfile({ userId }: { userId: number }) {
  const { data: user, loading, error } = useApi(
    (id: number) => apiAsync('GET:/users/:id', { id })
  );
  
  useEffect(() => {
    execute(userId);
  }, [userId]);
  
  if (loading) return <Spinner />;
  if (error) return <Error message={error.message} />;
  if (!user) return null;
  
  return <div>{user.name}</div>;
}

Provider 模式使用

子组件共享同一实例:

// 组件模式
function App() {
  return (
    <OrderProvider>
      <OrderList />
      <OrderStats />
    </OrderProvider>
  );
}

function OrderList() {
  const service = OrderProvider.useService();
  const orders = service.useState.orders();
  return <List data={orders} />;
}

function OrderStats() {
  const service = OrderProvider.useService();
  const pendingCount = service.useDerived.pendingCount();
  return <Badge count={pendingCount} />;
}

// HOC 模式
const OrderPage = OrderProvider.provide(function OrderPage() {
  const service = OrderProvider.useService();
  const orders = service.useState.orders();
  return <div>{orders.length} orders</div>;
});

全局单例

Provider 放在应用最外层即可实现全局单例:

function App() {
  return (
    <UserProvider>
      <OrderProvider>
        <Router />
      </OrderProvider>
    </UserProvider>
  );
}

Provider 内使用 Scoped

两种模式可以混合使用:

function OrderPage() {
  return (
    <OrderProvider>
      <SharedOrderList />
      {/* 独立实例,不受 Provider 影响 */}
      <IndependentPanel />
    </OrderProvider>
  );
}

function IndependentPanel() {
  const service = useOrderService(); // 创建独立实例
  // ...
}

API

装饰器

| 装饰器 | 说明 | |--------|------| | @Service() | 标记类为 Service | | @observable | 标记属性为响应式状态 | | @computed | 标记 getter 为计算属性 | | @action | 标记方法为 action,自动支持 async 和 generator 函数 | | @Inject() | 注入其他 Service |

@action 装饰器

@action 装饰器自动支持两种函数类型:

1. Async 函数(简单场景)

@action
async loadUser(id: number) {
  this.loading = true;
  const user = await apiAsync('GET:/users/:id', { id });
  this.user = user;
  this.loading = false;
}

2. Generator 函数(复杂流程,推荐)

@action
*loadUserData(id: number) {
  this.loading = true;
  
  // 请求失败会静默停止执行,不会抛出错误
  const user = yield* api('GET:/users/:id', { id });
  this.user = user;
  
  // ✅ 只有上面成功,这里才会执行
  const posts = yield* api('GET:/users/:id/posts', { id });
  this.posts = posts;
  
  this.loading = false;
}

Generator 函数的优势:

  • 请求失败静默停止后续执行(不抛出错误)
  • 代码更简洁,无需手动检查每个请求结果
  • 更好的可读性,接近同步代码风格
  • 无需 try-catch,失败时自动停止

注意:

  • Generator 函数中使用 yield* api(),不是 await apiAsync()
  • 请求失败时会静默停止,不会抛出错误
  • 如果需要在失败时执行清理代码,应该在调用 action 的地方处理
  • 非特殊场景,action 中不应该有 try-catch

函数

| 函数 | 说明 | |------|------| | createService(Class) | 创建 Scoped Hook | | createServiceProvider(Class) | 创建 Provider | | useApi(apiFn, options) | 在组件中执行 API 请求(推荐) | | useApiOnMount(apiFn, args, options) | 组件挂载时自动执行 API 请求 |

useApi Hook

在组件中使用 API 请求的推荐方式,自动管理 loading、error 状态:

import { useApi } from '@svton/service';
import { apiAsync } from './lib/api-client';

function UserProfile({ userId }: { userId: number }) {
  const { data: user, loading, error, execute } = useApi(
    (id: number) => apiAsync('GET:/users/:id', { id }),
    {
      onSuccess: (user) => console.log('User loaded:', user),
      onError: (error) => console.error('Failed:', error),
    }
  );
  
  useEffect(() => {
    execute(userId);
  }, [userId]);
  
  if (loading) return <Spinner />;
  if (error) return <Error message={error.message} />;
  if (!user) return null;
  
  return <div>{user.name}</div>;
}

useApiOnMount Hook

组件挂载时自动执行 API 请求:

function UserProfile({ userId }: { userId: number }) {
  const { data: user, loading, error, refetch } = useApiOnMount(
    (id: number) => apiAsync('GET:/users/:id', { id }),
    [userId]
  );
  
  if (loading) return <Spinner />;
  if (error) return <Error message={error.message} />;
  if (!user) return null;
  
  return (
    <div>
      <div>{user.name}</div>
      <button onClick={refetch}>Refresh</button>
    </div>
  );
}

Service 实例

const service = useXxxService();
// 或
const service = XxxProvider.useService();

// 状态
service.useState.xxx()      // 返回状态值

// 计算属性
service.useDerived.xxx()    // 返回计算值

// 方法
service.useAction.xxx()     // 返回绑定的方法

Provider

const XxxProvider = createServiceProvider(XxxService);

// 组件模式
<XxxProvider>
  <Children />
</XxxProvider>

// HOC 模式
XxxProvider.provide(Component)

// 获取实例
XxxProvider.useService()

配置

需要在 tsconfig.json 中启用装饰器:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

最佳实践

1. 使用 Generator 函数处理复杂流程

// ✅ 推荐:使用 Generator 函数
@action
*loadUserProfile(userId: number) {
  this.loading = true;
  
  // 请求失败会自动停止
  const user = yield* api('GET:/users/:id', { id: userId });
  this.user = user;
  
  const posts = yield* api('GET:/users/:id/posts', { id: userId });
  this.posts = posts;
  
  this.loading = false;
}

// ❌ 不推荐:使用 async/await 需要手动检查
@action
async loadUserProfile(userId: number) {
  this.loading = true;
  
  try {
    const user = await apiAsync('GET:/users/:id', { id: userId });
    if (!user) throw new Error('User not found');
    this.user = user;
    
    const posts = await apiAsync('GET:/users/:id/posts', { id: userId });
    if (!posts) throw new Error('Posts not found');
    this.posts = posts;
  } catch (error) {
    this.error = error;
  } finally {
    this.loading = false;
  }
}

2. 在组件中使用 useApi Hook

// ✅ 推荐:使用 useApi Hook
function UserProfile({ userId }: { userId: number }) {
  const { data: user, loading, error, execute } = useApi(
    (id: number) => apiAsync('GET:/users/:id', { id })
  );
  
  useEffect(() => {
    execute(userId);
  }, [userId]);
  
  if (loading) return <Spinner />;
  if (error) return <Error />;
  return <div>{user?.name}</div>;
}

// ❌ 不推荐:直接在组件中使用 apiAsync
function UserProfile({ userId }: { userId: number }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);
  
  useEffect(() => {
    setLoading(true);
    apiAsync('GET:/users/:id', { id: userId })
      .then(setUser)
      .finally(() => setLoading(false));
  }, [userId]);
  
  // ...
}

3. 避免在 action 中使用 try-catch

// ✅ 推荐:让错误自然抛出
@action
*loadUser(id: number) {
  this.loading = true;
  const user = yield* api('GET:/users/:id', { id });
  this.user = user;
  this.loading = false;
}

// 在调用处处理错误
async function handleLoadUser(id: number) {
  try {
    await service.loadUser(id);
  } catch (error) {
    console.error('Failed to load user:', error);
  }
}

// ❌ 不推荐:在 action 中使用 try-catch(除非有特殊需求)
@action
*loadUser(id: number) {
  this.loading = true;
  try {
    const user = yield* api('GET:/users/:id', { id });
    this.user = user;
  } catch (error) {
    this.error = error;
  } finally {
    this.loading = false;
  }
}

4. 必要时使用 await 等待请求

// ✅ 如果必须在组件中直接使用 api,使用 await
async function handleSubmit() {
  try {
    const result = await apiAsync('POST:/users', { data: formData });
    console.log('Success:', result);
  } catch (error) {
    console.error('Failed:', error);
  }
}

// ❌ 不要不等待就继续执行
function handleSubmit() {
  apiAsync('POST:/users', { data: formData }); // 没有 await
  console.log('Submitted'); // 可能在请求完成前执行
}

禁止的写法

❌ 1. 不要在 Generator 函数中使用 await

// ❌ 错误
@action
*loadUser(id: number) {
  const user = await apiAsync('GET:/users/:id', { id });
  this.user = user;
}

// ✅ 正确
@action
*loadUser(id: number) {
  const user = yield* api('GET:/users/:id', { id });
  this.user = user;
}

❌ 2. 不要在 async 函数中使用 yield*

// ❌ 错误
@action
async loadUser(id: number) {
  const user = yield* api('GET:/users/:id', { id });
  this.user = user;
}

// ✅ 正确
@action
async loadUser(id: number) {
  const user = await apiAsync('GET:/users/:id', { id });
  this.user = user;
}

❌ 3. 不要直接在组件中使用 api/apiAsync(除非必要)

// ❌ 不推荐
function UserList() {
  const [users, setUsers] = useState([]);
  
  useEffect(() => {
    apiAsync('GET:/users').then(setUsers);
  }, []);
  
  return <List data={users} />;
}

// ✅ 推荐:使用 useApi
function UserList() {
  const { data: users, loading } = useApiOnMount(
    () => apiAsync('GET:/users'),
    []
  );
  
  if (loading) return <Spinner />;
  return <List data={users} />;
}

// ✅ 或者使用 Service
function UserList() {
  const service = useUserService();
  const users = service.useState.users();
  const loading = service.useState.loading();
  const loadUsers = service.useAction.loadUsers();
  
  useEffect(() => { loadUsers(); }, []);
  
  if (loading) return <Spinner />;
  return <List data={users} />;
}

❌ 4. 不要在 action 中滥用 try-catch

// ❌ 不推荐:每个请求都 try-catch
@action
*loadUserData(id: number) {
  try {
    const user = yield* api('GET:/users/:id', { id });
    this.user = user;
  } catch (error) {
    this.error = error;
  }
  
  try {
    const posts = yield* api('GET:/users/:id/posts', { id });
    this.posts = posts;
  } catch (error) {
    this.postsError = error;
  }
}

// ✅ 推荐:让错误自然抛出,在调用处统一处理
@action
*loadUserData(id: number) {
  const user = yield* api('GET:/users/:id', { id });
  this.user = user;
  
  const posts = yield* api('GET:/users/:id/posts', { id });
  this.posts = posts;
}

❌ 5. 不要使用可选链调用 Hook

// ❌ 错误
const count = service.useState.count?.();

// ✅ 正确
const count = service.useState.count();

TypeScript 类型推断

自动类型推断

@svton/service 提供完整的 TypeScript 类型支持:

@Service()
class UserService {
  @observable user: User | null = null;
  @observable loading = false;

  @computed
  get isLoggedIn() {
    return this.user !== null;
  }

  @action
  async login(username: string, password: string) {
    // ...
  }
}

const useUserService = createService(UserService);

function MyComponent() {
  const service = useUserService();

  // ✅ 类型自动推断
  const user = service.useState.user();        // User | null
  const loading = service.useState.loading();  // boolean
  const isLoggedIn = service.useDerived.isLoggedIn(); // boolean
  const login = service.useAction.login();     // (username: string, password: string) => Promise<void>
}

Computed 属性类型

所有属性都从 Service 类直接推断,无需使用可选链:

// ✅ 直接调用,无需可选链
const doubled = service.useDerived.doubled();

// ✅ 类型完全匹配
const doubled: number = service.useDerived.doubled();

注意: 虽然 TypeScript 无法区分 @observable@computed(它们在类型层面都是非函数属性),但运行时会验证装饰器的正确使用。如果在错误的地方使用属性(如在 useState 中使用 @computed 属性),会抛出清晰的错误提示。

Go to Definition 支持

由于 TypeScript 映射类型的限制,直接在 service.useState.xxx() 上使用 Go to Definition 会跳转到类型定义,而不是原始的 Service 类属性。这是 TypeScript 的固有限制,不是我们的实现问题。

推荐的导航方式:

  1. 使用 VS Code 的 "Go To Source Definition" (TypeScript 4.7+)

    • 右键点击属性 → 选择 "Go To Source Definition"
    • 或设置键盘快捷键 (见下文)
  2. 使用 Find All References (Shift + F12)

    • 查找所有使用该属性的地方
  3. 通过 Service 实例类型跳转

    const service = useUserService();
    // 在 service 上 Cmd/Ctrl + Click 可以跳转到 UserService
  4. 添加类型注解

    const service: ServiceInstance<UserService> = useUserService();
    // 现在可以在 UserService 上跳转

设置 VS Code 快捷键:

// Command Palette → Preferences: Open Keyboard Shortcuts (JSON)
{
  "key": "cmd+click",  // 或 "ctrl+click" (Windows/Linux)
  "command": "editor.action.goToSourceDefinition",
  "when": "editorTextFocus"
}

详细说明请参考:

License

MIT