mcpField
W
workleap-squide

workleap-squide

github.com/workleap开发842 次安装6 天前更新
代码仓库

关于 workleap-squide

Squide 是用于构建模块化应用外壳的 React 框架,适合将多个模块组合成宿主应用的团队。其 API 涵盖路由、导航、全局数据和延迟注册。

主要功能

  1. 01初始化 Firefly 运行时
  2. 02按模块注册路由和导航
  3. 03区分公开与受保护路由
  4. 04获取公开和受保护的全局数据
  5. 05延迟注册导航项

适用场景

  1. 01用多个模块构建宿主应用
  2. 02呈现公开与受保护路由
  3. 03根据获取的数据显示导航
  4. 04在功能标志变化时更新导航

安装

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

$npx skills add https://github.com/workleap/wl-squide --skill workleap-squide

安装后在 Claude Code 中输入/workleap-squide确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
components.md
getting-started.md
hooks-api.md
integrations.md
patterns.md
runtime-api.md

详细介绍

Squide 框架

Squide 是一个 React 模块化应用外壳。仅使用文档中说明的 API。

核心概念

  • 运行时:FireflyRuntime 实例是 Squide 应用的核心。切勿直接实例化,应使用 initializeFirefly(),由它负责接入插件、日志记录和模块生命周期。
  • 模块化注册:模块通过注册函数注册路由、导航项和 MSW 处理程序;主机在启动时汇总这些注册。
  • 公开路由与受保护路由:路由默认是 protected(在 ProtectedRoutes 下渲染)。公开路由请使用 registerPublicRoute()。受保护路由会同时获取公开和受保护的全局数据。
  • 延迟注册:依赖远程数据或功能标志的导航项使用两阶段注册:从注册函数返回一个函数,将这些导航项的注册延后到第二阶段。

关键模式

主机应用设置
// host/src/index.tsx
import { createRoot } from "react-dom/client";
import { FireflyProvider, initializeFirefly } from "@squide/firefly";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { App } from "./App.tsx";
import { registerHost } from "./register.tsx";

const runtime = initializeFirefly({
    localModules: [registerHost]
});

const queryClient = new QueryClient();
const root = createRoot(document.getElementById("root")!);

root.render(
    <FireflyProvider runtime={runtime}>
        <QueryClientProvider client={queryClient}>
            <App />
        </QueryClientProvider>
    </FireflyProvider>
);
// host/src/App.tsx
import { AppRouter, useIsBootstrapping } from "@squide/firefly";
import { createBrowserRouter, Outlet } from "react-router";
import { RouterProvider } from "react-router/dom";

function BootstrappingRoute() {
    if (useIsBootstrapping()) {
        return <div>Loading...</div>;
    }
    return <Outlet />;
}

export function App() {
    return (
        <AppRouter>
            {({ rootRoute, registeredRoutes, routerProps, routerProviderProps }) => (
                <RouterProvider
                    router={createBrowserRouter([{
                        element: rootRoute,
                        children: [{
                            element: <BootstrappingRoute />,
                            children: registeredRoutes
                        }]
                    }], routerProps)}
                    {...routerProviderProps}
                />
            )}
        </AppRouter>
    );
}
// host/src/register.tsx
import { PublicRoutes, ProtectedRoutes, type ModuleRegisterFunction, type FireflyRuntime } from "@squide/firefly";
import { RootLayout } from "./RootLayout.tsx";

export const registerHost: ModuleRegisterFunction<FireflyRuntime> = runtime => {
    runtime.registerRoute({
        element: <RootLayout />,
        children: [PublicRoutes, ProtectedRoutes]
    }, { hoist: true });

    // HomePage and NotFoundPage are local page components
    runtime.registerRoute({ index: true, element: <HomePage /> });
    runtime.registerPublicRoute({ path: "*", element: <NotFoundPage /> });
};
导航渲染

重要: RenderItemFunction 的签名为 (item, key, index, level) => ReactNode,RenderSectionFunction 的签名为 (elements, key, index, level) => ReactNode。这些签名是固定的,不接受自定义上下文参数,但可以少传一些参数。使用闭包访问外部值。

重要: 始终整体展开 additionalProps,绝不要逐个键展开。渲染器必须读取而非转发的值应放在 $context 中,并通过 context 提供。不要从 additionalProps 中解构出某个键,以免它出现在元素上——这正是 $context 的用途。这个 $context 是布局的每项数据;它不是模块注册上下文,也不是 React 上下文。在 @squide/firefly 18.2.0-19.0.0(@squide/react-router 9.1.0-10.0.0)中,它名为 $meta。

import { Link, Outlet } from "react-router";
import {
    useNavigationItems, useRenderedNavigationItems, isNavigationLink,
    type RenderItemFunction, type RenderSectionFunction
} from "@squide/firefly";

// Signature: (item, key, index, level) => ReactNode
const renderItem: RenderItemFunction = (item, key, index, level) => {
    if (!isNavigationLink(item)) return null;
    const { label, linkProps, additionalProps } = item;
    return (
        <li key={key}>
            <Link {...linkProps} {...additionalProps}>{label}</Link>
        </li>
    );
};

// Signature: (elements, key, index, level) => ReactNode
const renderSection: RenderSectionFunction = (elements, key, index, level) => (
    <ul key={key}>{elements}</ul>
);

export function RootLayout() {
    const navigationItems = useNavigationItems();
    const navigationElements = useRenderedNavigationItems(navigationItems, renderItem, renderSection);
    return (
        <>
            <nav>{navigationElements}</nav>
            <Outlet />
        </>
    );
}
全局数据获取
// Protected data
import { useProtectedDataQueries, useIsBootstrapping, AppRouter } from "@squide/firefly";

// ApiError and isApiError are app-specific; define them to match your API's error shape
function BootstrappingRoute() {
    const [session] = useProtectedDataQueries([{
        queryKey: ["/api/session"],
        queryFn: async () => {
            const response = await fetch("/api/session");
            if (!response.ok) throw new ApiError(response.status);
            return response.json();
        }
    }], error => isApiError(error) && error.status === 401);

    if (useIsBootstrapping()) return <div>Loading...</div>;

    return (
        <SessionContext.Provider value={session}>
            <Outlet />
        </SessionContext.Provider>
    );
}

// In App component, set waitForProtectedData
<AppRouter waitForProtectedData>...</AppRouter>
// Public data
const [data] = usePublicDataQueries([{ queryKey: [...], queryFn: ... }]);
<AppRouter waitForPublicData>...</AppRouter>
延迟注册导航项
export const register: ModuleRegisterFunction<FireflyRuntime, unknown, DeferredRegistrationData> = runtime => {
    // Always register routes
    runtime.registerRoute({ path: "/feature", element: <FeaturePage /> });

    // Return function for deferred navigation items
    return (deferredRuntime, { userData }) => {
        if (userData.isAdmin && deferredRuntime.getFeatureFlag("enable-feature")) {
            deferredRuntime.registerNavigationItem({
                $id: "feature",
                $label: "Feature",
                to: "/feature"
            });
        }
    };
};
// Execute deferred registrations in BootstrappingRoute.
// Wrap in useMemo — without it, a new object reference each render re-triggers all deferred registrations.
const data = useMemo(() => ({ userData }), [userData]);
useDeferredRegistrations(data);

重要: 每次更新(功能标志变更或新增数据)时,都会再次运行延迟注册函数。Squide 会丢弃上一次运行所注册的所有内容,然后重新执行,因此每次运行都必须注册希望渲染的完整集合,而不能只注册相较于上次运行的差异。Squide 只会丢弃由其管理的内容:若插件向模块公开了自己的注册表,必须通过可选的 Plugin.onDeferredRegistrationScopeStarted 钩子清空该注册表;若注册表不由插件管理,则使用 runtime.registerDeferredRegistrationScopeStartedListener——两者使用相同的钩子、选项和保证(参见 references/runtime-api.md)。

另请参阅: 有关错误边界、身份验证、测试模式和高级导航(多层级、模块化标签页、动态片段、活动状态),请参阅 references/patterns.md。有关 MSW 设置、LaunchDarkly、Honeycomb、i18next 和 Storybook 集成,请参阅 references/integrations.md。有关插件编写和完整的运行时 API,请参阅 references/runtime-api.md。

参考指南

如需查阅上述模式之外的详细 API 文档,请参阅以下参考文件:

  • references/getting-started.md — Squide 是什么以及它解决的问题、模块化设计原则、从零搭建宿主应用和本地模块(软件包、文件、Rsbuild 配置、模板)
  • references/runtime-api.md — initializeFirefly 选项、路由注册选项(hoist、parentPath、parentId)、路由属性、导航项属性、导航注册选项(menuId、sectionId)、延迟注册作用域启动监听器,以及请求处理程序注册
  • references/hooks-api.md — Squide 的所有钩子:数据获取(usePublicDataQueries、useProtectedDataQueries)、导航(包括渲染属性类型和 canRender())、事件总线、环境变量、功能标志、日志记录、路由,以及 i18next 钩子
  • references/components.md — AppRouter 属性(包括 strictMode 和注册验证)、FireflyProvider、辅助函数(isNavigationLink、resolveRouteSegments、mergeDeferredRegistrations)
  • references/patterns.md — 本地模块设置、延迟注册更新运行和待处理区段、错误边界、身份验证、模块化标签页、MSW 请求处理程序、测试(包括 createDeferredRegistrationsRunner),以及其他常见模式
  • references/integrations.md — LaunchDarkly(插件、实用工具、测试客户端)、Honeycomb、i18next 和 Storybook 集成详情

常见陷阱

技能维护者: 更新此技能前,请阅读 ODR-0008。正文必须保持在约 250 行以内;新的 API 内容应放入相应的 references/ 文件。

使用 Squide API 时,请注意以下常见错误:

  1. useRenderedNavigationItems 函数签名:必须始终为 (item, key, index, level) 和 (elements, key, index, level)。这些函数不接受自定义上下文参数。如果需要外部值(路由参数、位置等),请使用闭包或 React 钩子——切勿建议为这些函数添加参数。

  2. 活动状态样式:使用 React Router 的 NavLink,并在 className/style 渲染函数中使用其提供的 isActive 参数(例如,className={({ isActive }) => ... })。不要建议将 location/pathname 作为上下文参数传递。

  3. 动态路由区段:使用 resolveRouteSegments 辅助函数和闭包来捕获 userId 等值。示例模式:创建一个返回 RenderItemFunction 的高阶函数。

  4. 延迟注册运行时参数:延迟注册回调会将 deferredRuntime 作为第一个参数接收——它与外层注册函数中的 runtime 不同。在延迟回调中调用 registerNavigationItem、getFeatureFlag 等时,始终使用 deferredRuntime。

  5. 惰性 i18next 实例:从模块的 register() 函数注册惰性 i18next 实例(模块注册后,针对惰性实例调用 registerInstance 会抛出错误;静态实例可随时注册)。使用静态资源时,仍像之前一样调用 changeLanguage,但不要使用 await;仅在必须等待惰性切换完成后才能继续的情况下,才等待返回的 promise,并在 effect 中使用块体(参见 references/integrations.md)。

评论

支持 Markdown