关于 workleap-squide
Squide 是用于构建模块化应用外壳的 React 框架,适合将多个模块组合成宿主应用的团队。其 API 涵盖路由、导航、全局数据和延迟注册。
主要功能
- 01初始化 Firefly 运行时
- 02按模块注册路由和导航
- 03区分公开与受保护路由
- 04获取公开和受保护的全局数据
- 05延迟注册导航项
适用场景
- 01用多个模块构建宿主应用
- 02呈现公开与受保护路由
- 03根据获取的数据显示导航
- 04在功能标志变化时更新导航
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ workleap/ wl-squide --skill workleap-squide安装后在 Claude Code 中输入/workleap-squide确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/components.mdgetting-started.mdhooks-api.mdintegrations.mdpatterns.mdruntime-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 时,请注意以下常见错误:
-
useRenderedNavigationItems函数签名:必须始终为(item, key, index, level)和(elements, key, index, level)。这些函数不接受自定义上下文参数。如果需要外部值(路由参数、位置等),请使用闭包或 React 钩子——切勿建议为这些函数添加参数。 -
活动状态样式:使用 React Router 的
NavLink,并在className/style渲染函数中使用其提供的isActive参数(例如,className={({ isActive }) => ... })。不要建议将 location/pathname 作为上下文参数传递。 -
动态路由区段:使用
resolveRouteSegments辅助函数和闭包来捕获userId等值。示例模式:创建一个返回RenderItemFunction的高阶函数。 -
延迟注册运行时参数:延迟注册回调会将
deferredRuntime作为第一个参数接收——它与外层注册函数中的runtime不同。在延迟回调中调用registerNavigationItem、getFeatureFlag等时,始终使用deferredRuntime。 -
惰性 i18next 实例:从模块的
register()函数注册惰性 i18next 实例(模块注册后,针对惰性实例调用registerInstance会抛出错误;静态实例可随时注册)。使用静态资源时,仍像之前一样调用changeLanguage,但不要使用await;仅在必须等待惰性切换完成后才能继续的情况下,才等待返回的 promise,并在 effect 中使用块体(参见references/integrations.md)。