Skip to content

Echo 高级用法

本文档介绍 Echo 状态管理库的高级功能和用法。

等待初始化完成

当使用持久化存储(特别是 IndexedDB)时,初始化过程是异步的。为确保在使用状态前已完成初始化,应使用 ready() 方法:

typescript
// 创建并配置存储
const settingsStore = new Echo({ theme: "light" }).indexed({
  name: "settings",
  database: "app-settings",
  sync: true,
});

// 等待初始化完成后再使用
settingsStore.ready().then(() => {
  // 现在可以安全地使用store了
  console.log(settingsStore.current);
});

// 或使用 async/await
async function initSettings() {
  await settingsStore.ready();
  // 初始化完成,可以安全使用
  console.log(settingsStore.current);
}

资源清理

Echo 实例使用了一些需要手动清理的资源,如数据库连接和跨窗口通信通道。在不再需要 Echo 实例时,应该清理这些资源:

typescript
// 清理资源(持久化数据不会消失)
userStore.destroy();

这在以下情况特别重要:

  • 组件卸载时
  • 用户登出时
  • 应用关闭时

切换存储键名

Echo 提供了 switch 方法,允许您在当前数据库和对象仓库下切换到不同的键名。注意:此方法仅限于 IndexedDB 方案使用。

typescript
// 创建 IndexedDB 存储
const projectStore = new Echo({ title: "项目1" }).indexed({
  name: "project-1",
  database: "projects-db",
  object: "projects-store",
});

// 切换到另一个项目的数据(在同一个数据库和对象仓库下)
projectStore.switch("project-2");

// 等待切换完成后再使用
projectStore
  .switch("project-3")
  .ready()
  .then(() => {
    console.log("已切换到项目3的数据");
    console.log(projectStore.current);
  });

这个功能在需要管理多个项目数据的应用中特别有用。例如,您可以在同一个数据库中存储多个项目的数据,每个项目使用不同的键名。

当使用 switch 方法时,它会保持在同一个数据库和对象仓库下,只切换键名。这意味着您可以在同一个数据库结构中管理多个相关的数据集,而不需要创建多个数据库或对象仓库。

需要注意的是,当切换到一个新的键名时:

  • 如果该键名下已经有持久化的数据,Echo 会加载这些数据
  • 如果该键名下没有持久化的数据,Echo 会使用默认状态(构造函数中提供的状态)初始化,而不是使用当前状态
typescript
// 示例:管理多个用户的设置
const settingsStore = new Echo({ theme: "light" }).indexed({
  name: "user-123", // 当前用户ID
  database: "app-settings",
  object: "user-settings",
});

// 切换到另一个用户的设置
function switchToUser(userId: string) {
  settingsStore
    .switch(userId)
    .ready()
    .then(() => {
      console.log(`已切换到用户 ${userId} 的设置`);
      // 如果 userId 下没有数据,此时状态为默认值 { theme: "light" }
    });
}

// 使用
switchToUser("user-456");

如果尝试在 LocalStorage 或临时存储模式下使用 switch 方法,将会抛出异常。

异步操作处理

Echo 本身不提供异步操作中间件,但可以轻松地在异步函数中使用 Echo:

typescript
async function fetchUserData(userId: string) {
  try {
    // 显示加载状态
    userStore.set({ loading: true, error: null });

    // 发起API请求
    const response = await fetch(`/api/users/${userId}`);

    if (!response.ok) {
      throw new Error(`请求失败: ${response.status}`);
    }

    const userData = await response.json();

    // 更新状态
    userStore.set({
      loading: false,
      userData,
      error: null,
    });

    return userData;
  } catch (error) {
    // 处理错误
    userStore.set({
      loading: false,
      error: error.message,
    });
    throw error;
  }
}

自定义状态管理类

对于复杂应用,可以通过继承 Echo 类来创建自定义状态管理类,封装业务逻辑:

typescript
interface AuthState {
  user: {
    id: string;
    name: string;
    email: string;
  } | null;
  token: string | null;
  isAuthenticated: boolean;
  loading: boolean;
  error: string | null;
}

class AuthStore extends Echo<AuthState> {
  constructor() {
    super({
      user: null,
      token: null,
      isAuthenticated: false,
      loading: false,
      error: null,
    });

    // 使用 localStorage 存储,并启用跨窗口同步
    this.localStorage({
      name: "auth-store",
      sync: true,
    });

    // 初始化时检查本地存储的令牌
    this.checkAuth();
  }

  private checkAuth() {
    // 检查令牌是否有效
    const { token } = this.current;
    if (token) {
      this.validateToken(token);
    }
  }

  async login(email: string, password: string) {
    try {
      this.set({ loading: true, error: null });

      const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password }),
      });

      if (!response.ok) {
        throw new Error('登录失败');
      }

      const { user, token } = await response.json();

      this.set({
        user,
        token,
        isAuthenticated: true,
        loading: false,
        error: null,
      });

      return user;
    } catch (error) {
      this.set({ loading: false, error: error.message });
      throw error;
    }
  }

  async logout() {
    try {
      const { token } = this.current;

      if (token) {
        // 可选:通知服务器使令牌失效
        await fetch('/api/logout', {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${token}`,
            'Content-Type': 'application/json',
          },
        });
      }
    } catch (error) {
      console.error('注销时出错:', error);
    } finally {
      // 无论服务器请求成功与否,都清除本地状态
      this.set({
        user: null,
        token: null,
        isAuthenticated: false,
        loading: false,
        error: null,
      }, { replace: true });
    }
  }

  private async validateToken(token: string) {
    try {
      this.set({ loading: true });

      const response = await fetch('/api/validate-token', {
        headers: { 'Authorization': `Bearer ${token}` },
      });

      if (!response.ok) {
        throw new Error('令牌无效');
      }

      const { user } = await response.json();

      this.set({
        user,
        isAuthenticated: true,
        loading: false,
      });
    } catch (error) {
      // 令牌无效,清除状态
      this.set({
        user: null,
        token: null,
        isAuthenticated: false,
        loading: false,
        error: '会话已过期,请重新登录',
      });
    }
  }
}

// 创建单例实例
export const authStore = new AuthStore();

// 在组件中使用
function LoginStatus() {
  const { user, isAuthenticated, loading, error } = authStore.use();

  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误: {error}</div>;

  return (
    <div>
      {isAuthenticated ? (
        <>
          <p>欢迎, {user.name}!</p>
          <button onClick={() => authStore.logout()}>退出登录</button>
        </>
      ) : (
        <button onClick={() => /* 显示登录表单 */}>登录</button>
      )}
    </div>
  );
}

性能优化

使用选择器减少重渲染

选择器可以帮助组件只订阅它们需要的状态部分,避免不必要的重渲染:

tsx
// 不好的做法:订阅整个状态
function UserProfile() {
  // 每当状态中的任何字段变化时都会重渲染
  const state = userStore.use();

  return <div>{state.user.name}</div>;
}

// 好的做法:使用选择器
function UserProfile() {
  // 只有当用户名变化时才会重渲染
  const userName = userStore.use((state) => state.user.name);

  return <div>{userName}</div>;
}

避免频繁更新

对于频繁更新的状态(如表单输入),可以考虑使用本地状态,只在必要时更新 Echo 状态:

tsx
function SearchForm() {
  // 使用本地状态管理输入
  const [query, setQuery] = useState("");
  // 只获取搜索结果
  const results = searchStore.use((state) => state.results);

  const handleSearch = () => {
    // 只在用户提交时更新全局状态
    searchStore.set({ query });
    performSearch(query);
  };

  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <button onClick={handleSearch}>搜索</button>
      <div>
        {results.map((result) => (
          <div key={result.id}>{result.title}</div>
        ))}
      </div>
    </div>
  );
}

跨窗口状态同步

Echo 支持跨窗口状态同步,这对于多标签页应用特别有用:

typescript
// 启用跨窗口同步
const userPrefs = new Echo({
  theme: "light",
  fontSize: "medium",
  notifications: true,
}).localStorage({
  name: "user-preferences",
  sync: true, // 启用跨窗口同步
});

// 在一个窗口中更改主题
userPrefs.set({ theme: "dark" });

// 在另一个窗口中,状态会自动更新
// 并且监听器会被触发

这个功能在以下场景特别有用:

  • 用户在多个标签页打开了同一个应用
  • 需要在所有标签页中保持一致的用户设置
  • 一个标签页中的登录/登出操作需要影响所有标签页

与其他库集成

与 React Context 集成

可以将 Echo 与 React Context 结合使用,提供更好的依赖注入:

tsx
// 创建上下文
const TodoContext = React.createContext<Echo<TodoState> | null>(null);

// 提供者组件
function TodoProvider({ children }) {
  // 创建 Echo 实例
  const todoStore = useMemo(() => {
    return new Echo<TodoState>({ todos: [] }).localStorage({
      name: "todos",
      sync: true,
    });
  }, []);

  // 确保在组件卸载时清理资源
  useEffect(() => {
    return () => todoStore.destroy();
  }, [todoStore]);

  return (
    <TodoContext.Provider value={todoStore}>{children}</TodoContext.Provider>
  );
}

// 自定义 Hook 简化使用
function useTodos<Selected = TodoState>(
  selector?: (state: TodoState) => Selected
) {
  const store = useContext(TodoContext);
  if (!store) {
    throw new Error("useTodos 必须在 TodoProvider 内部使用");
  }
  return store.use(selector);
}

// 在组件中使用
function TodoList() {
  const todos = useTodos((state) => state.todos);
  // ...
}

调试技巧

添加日志监听器

可以添加一个监听器来记录所有状态变化:

typescript
if (process.env.NODE_ENV === "development") {
  userStore.subscribe((state) => {
    console.log("[Echo 状态更新]", state);
  });
}

创建调试工具

可以创建一个简单的调试组件来显示当前状态:

tsx
function EchoDebugger({ store, name = "Store" }) {
  const state = store.use();

  if (process.env.NODE_ENV !== "development") {
    return null;
  }

  return (
    <div
      style={{
        position: "fixed",
        bottom: 10,
        right: 10,
        background: "#f0f0f0",
        padding: 10,
        borderRadius: 4,
        maxWidth: 300,
        maxHeight: 400,
        overflow: "auto",
      }}
    >
      <h3>{name} 状态</h3>
      <pre>{JSON.stringify(state, null, 2)}</pre>
    </div>
  );
}

// 使用
function App() {
  return (
    <div>
      {/* 应用组件 */}
      <EchoDebugger store={userStore} name="用户" />
    </div>
  );
}