mcpField
A
async-preact-signals

async-preact-signals

github.com/rodydavis开发6060 次安装2 周前更新
代码仓库

关于 async-preact-signals

为使用 Preact Signals 的开发者提供一种处理 Promise 异步数据的模式。它通过 asyncSignal 和状态类表示加载、数据与错误,并支持取消先前请求及映射状态。

主要功能

  1. 01以状态类表示加载、数据和错误
  2. 02通过 asyncSignal 创建异步信号
  3. 03依赖变化时取消先前请求
  4. 04将状态映射为其他值

适用场景

  1. 01在 Signals UI 中展示请求状态
  2. 02在依赖变化时更新异步数据
  3. 03复用多个请求的加载与错误处理

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/rodydavis/skills --skill async-preact-signals

安装后在 Claude Code 中输入/async-preact-signals确认已加载。

详细介绍

异步 Preact Signals

在 JavaScript 中使用 signals 时,经常需要处理来自 Promises 的异步数据。

异步与同步

但与其他状态管理库不同,signals 没有_异步_状态图,所有值都必须_同步_计算。

刚开始使用 signals 时,人们往往只想在函数回调中添加 async,但这会破坏其底层工作方式,并导致行为未定义。☹️

异步函数是一种有泄漏的抽象,会迫使你在整个状态图中处理异步。异步也并非总是更好,还可能带来性能影响。😬

使用 Promises

我们仍然可以用同步操作完成很多事情,并让常见异步模式更易于使用。

例如,使用 fetch 发起 http 请求时,你会希望返回 Promise 中的数据并更新一些 UI。

const el = document.querySelector('#output');
let postId = '123';
fetch(`/posts/${postId}`).then(res => res.json()).then(post => {
    el.innerText = post.title;
})

现在添加 signals 后,每当 post id 发生变化,我们都可以重新运行 fetch。

import { effect, signal } from "@preact/signals-core";

const el = document.querySelector('#output');
const postId =  signal( '123');

effect(() => {
    fetch(`/posts/${postId.value}`).then(res => res.json()).then(post => {
        el.innerText = post.title;
    });
});

这样更好了,但现在我们需要处理一种情况:如果前一个 fetch 尚未完成,post id 就发生了变化,我们需要停止前一个请求。

import { effect, signal } from "@preact/signals-core";

const el = document.querySelector('#output');
const postId =  signal( '123');
let controller;

effect(() => {
    if (controller) {
         controller.abort();
    }
    controller = new AbortController();
    const signal = controller.signal;
    try {
       fetch(`/posts/${postId.value}`, { signal }).then(res => res.json()).then(post => {
            el.innerText = post.title;
        }); 
    } catch (err) {
       // todo: show error message
    }
});

不过,这仍然跳过了许多我们通常希望展示的内容,例如加载状态和错误状态。

import { effect, signal, batch } from "@preact/signals-core";

const el = document.querySelector('#output');
const postId =  signal( '123');
const postData = signal({});
const errorMessage = signal('');
const loading = signal(false);
let controller;

effect(() => {
    if (controller) {
         controller.abort();
    }
    controller = new AbortController();
    const signal = controller.signal;
    batch(() => {
       loading.value = true;
       errorMessage.value = '';
       postData.value = {};
    });
    try {
       fetch(`/posts/${postId.value}`, { signal }).then(res => res.json()).then(post => {
            batch(() => {
                 postData.value = post;
                 loading.value = false;
             });
        }); 
    } catch (err) {
        errorMessage.value = err.message;
    }
});
effect(() =>  {
    if (loading.value) {
        el.innerText = 'Loading...';
    } else if (errorMessage.value) {
        el.innerText = `Error: ${errorMessage.value}`;
    } else {
        el.innerText = postData.value.title;
    }
});

现在我们可以展示恰当的状态了,但这只适用于一个请求……

我们可以将这些逻辑封装到一个类中以便复用,或者创建一种能够处理异步数据的新 signal 类型。

AsyncState

我们想要一个基类,让加载状态可以轻松继承它:

export class AsyncState<T> {
  constructor() {}

  get value(): T | null {
    return null;
  }

  get requireValue(): T {
    throw new Error("Value not set");
  }

  get error(): any {
    return null;
  }

  get isLoading(): boolean {
    return false;
  }

  get hasValue(): boolean {
    return false;
  }

  get hasError(): boolean {
    return false;
  }

  map<R>(builders: {
    onLoading: () => R;
    onError: (error: any) => R;
    onData: (data: T) => R;
  }): R {
    if (this.hasError) {
      return builders.onError(this.error);
    }
    if (this.hasValue) {
      return builders.onData(this.requireValue);
    }
    return builders.onLoading();
  }
}

这个类实际上来自我创建的 preact signals 的 Dart 移植版。

这样我们就可以轻松检查是否有实际值、错误或是否正在加载。它还提供了一个简单的构建器方法,可以将状态映射为另一个值。🤩

AsyncData

加载状态继承 AsyncState,并在构造函数中传入值,以供重写的方法使用。

export class AsyncData<T> extends AsyncState<T> {
  private _value: T;

  constructor(value: T) {
    super();
    this._value = value;
  }

  get requireValue(): T {
    return this._value;
  }

  get hasValue(): boolean {
    return true;
  }

  toString() {
    return `AsyncData{${this._value}}`;
  }
}
AsyncLoading

对于加载状态,我们像 AsyncData 一样重写这些方法。

export class AsyncLoading<T> extends AsyncState<T> {
  get value(): T | null {
    return null;
  }

  get isLoading(): boolean {
    return true;
  }

  toString() {
    return `AsyncLoading{}`;
  }
}
AsyncError

对于错误状态,我们可以传入任意类型的对象,将错误作为值返回,而不是抛出异常(类似 Go)。

export class AsyncError<T> extends AsyncState<T> {
  private _error: any;

  constructor(error: any) {
    super();
    this._error = error;
  }

  get error(): any {
    return this._error;
  }

  get hasError(): boolean {
    return true;
  }

  toString() {
    return `AsyncError{${this._error}}`;
  }
}

asyncSignal

现在我们创建好了这些状态类,就可以创建一个函数来创建异步 signal,并包含前面讨论的所有逻辑。

我们需要随时展示同步值,并且能够中止之前的请求。

export function asyncSignal<T>(
  cb: () => Promise<T>
): ReadonlySignal<AsyncState<T>> {
  const loading = new AsyncLoading<T>();
  const reset = Symbol("reset");
  const s = signal<AsyncState<T>>(loading);
  const c = computed<Promise<T>>(cb);
  let controller: AbortController | null;
  let abortSignal: AbortSignal | null;

  function execute(cb: Promise<T>, cancel: AbortSignal) {
    (async () => {
      s.value = loading;
      try {
        const result = await new Promise<T>(async (resolve, reject) => {
          if (cancel.aborted) {
            reject(cancel.reason);
          }
          cancel.addEventListener("abort", () => {
            reject(cancel.reason);
          });
          try {
            const result = await cb;
            if (cancel.aborted) {
              reject(cancel.reason);
              return;
            }
            resolve(result);
          } catch (error) {
            reject(error);
          }
        });
        s.value = new AsyncData<T>(result);
      } catch (error) {
        if (error === reset) {
          s.value = loading;
        } else {
          s.value = new AsyncError<T>(error);
        }
      }
    })();
  }

  effect(() => {
    if (controller != null) {
      controller.abort(reset);
    }
    controller = new AbortController();
    abortSignal = controller.signal;
    execute(c.value, abortSignal);
  });

  return s;
}

这样就可以非常轻松地创建多个异步 signals,也可以在应用中其他使用 signals 的地方使用,例如 effects 和 computeds。

const el = document.querySelector('#output');
const postId =  signal('123');

const result = asyncSignal(() => fetch(`/posts/${postId.value}`).then(res => res.json()));

effect(() => {
   el.innerText = result.value.map({
      onLoading: () => 'Loading...',
      onError: (err) => `Error: ${err}`,
      onData: (post) => post.title, 
   });
});

postId.value = '456';

总结

我已经在 这里发起了 Preact Signals 的 GitHub 讨论,你也可以在这里找到最终的源代码 gist。🎉

这让处理异步数据变得轻松了许多,也很期待听到你对如何改进它的想法 👀

如果你还想了解 Angular 如何处理异步 signals,可以查看 resource signal 和 computedFrom/Async signal。

评论

支持 Markdown