{"id":32512891,"url":"https://github.com/winjs-dev/winjs-renderer-react","last_synced_at":"2025-10-27T22:47:35.479Z","repository":{"id":319279812,"uuid":"1078059261","full_name":"winjs-dev/winjs-renderer-react","owner":"winjs-dev","description":"WinJS 框架的 React 渲染器，提供客户端渲染能力和 React Router 集成。","archived":false,"fork":false,"pushed_at":"2025-10-26T01:52:40.000Z","size":162,"stargazers_count":1,"open_issues_count":3,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-10-26T03:30:40.163Z","etag":null,"topics":["reactjs","renderer","winjs"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/winjs-dev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-17T06:34:23.000Z","updated_at":"2025-10-26T01:51:30.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/winjs-dev/winjs-renderer-react","commit_stats":null,"previous_names":["winjs-dev/winjs-renderer-react"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/winjs-dev/winjs-renderer-react","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/winjs-dev%2Fwinjs-renderer-react","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/winjs-dev%2Fwinjs-renderer-react/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/winjs-dev%2Fwinjs-renderer-react/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/winjs-dev%2Fwinjs-renderer-react/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/winjs-dev","download_url":"https://codeload.github.com/winjs-dev/winjs-renderer-react/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/winjs-dev%2Fwinjs-renderer-react/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":281355372,"owners_count":26486896,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-27T02:00:05.855Z","response_time":61,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["reactjs","renderer","winjs"],"created_at":"2025-10-27T22:47:26.545Z","updated_at":"2025-10-27T22:47:35.474Z","avatar_url":"https://github.com/winjs-dev.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# @winner-fed/renderer-react\n\nWinJS 框架的 React 渲染器，提供客户端渲染能力和 React Router 集成。\n\n## 特性\n\n- ✅ **React 19.x 支持** - 使用 React 19 的最新特性和并发渲染能力\n- 🚦 **React Router v7 集成** - 最新的路由管理和导航能力\n- 🎯 **多种路由模式** - 支持 Browser、Hash 和 Memory 路由\n- 🔄 **插件系统集成** - 与 WinJS 插件系统无缝集成\n- 📦 **TypeScript 支持** - 完整的类型定义\n- 🎨 **应用上下文** - 通过 Context 访问路由和应用数据\n- ⚡ **懒加载** - 内置代码分割和懒加载支持\n- 🔗 **路由预加载** - 支持程序化路由预加载\n- 🌊 **流式渲染** - 支持 React Suspense 流式渲染\n\n## 安装\n\n```bash\nnpm install @winner-fed/renderer-react\n```\n\n## 核心概念\n\n### 客户端渲染\n\n`renderClient` 是渲染 React 应用的主入口：\n\n```tsx\nimport { renderClient } from '@winner-fed/renderer-react';\n\nrenderClient({\n  rootElement: document.getElementById('root'),\n  routes: routesById,\n  routeComponents: routeComponents,\n  pluginManager: pluginManager,\n  basename: '/app',\n  history: history,\n});\n```\n\n### 路由配置\n\n路由定义采用 WinJS 路由结构，会自动转换为 React Router 格式：\n\n```tsx\nconst routesById = {\n  'home': {\n    id: 'home',\n    path: '/',\n    parentId: undefined,\n  },\n  'about': {\n    id: 'about',\n    path: '/about',\n    parentId: undefined,\n    clientLoader: async () =\u003e {\n      return { data: 'About 页面数据' };\n    },\n  },\n  'user': {\n    id: 'user',\n    path: '/user/:id',\n    parentId: 'about',\n  },\n};\n\nconst routeComponents = {\n  'home': HomePage,\n  'about': AboutPage,\n  'user': UserPage,\n};\n```\n\n### 应用上下文\n\n通过 `useAppData` Hook 访问应用数据：\n\n```tsx\nimport { useAppData } from '@winner-fed/renderer-react';\n\nfunction MyComponent() {\n  const { routes, clientRoutes, pluginManager, basename, history } = useAppData();\n  \n  return \u003cdiv\u003e当前 basename: {basename}\u003c/div\u003e;\n}\n```\n\n## API 参考\n\n### 函数\n\n#### `renderClient(options)`\n\n将 React 应用渲染到 DOM。\n\n**参数：**\n\n```tsx\ninterface RenderClientOpts {\n  // 公共路径配置\n  publicPath?: string;\n  // 是否运行时配置 publicPath\n  runtimePublicPath?: boolean;\n  // 挂载元素 ID（微前端场景可能变化）\n  mountElementId?: string;\n  // 挂载的 DOM 元素\n  rootElement?: HTMLElement;\n  // 路由配置（按 ID 索引）\n  routes: IRoutesById;\n  // 路由组件映射\n  routeComponents: IRouteComponents;\n  // 插件管理器实例\n  pluginManager: any;\n  // 路由 base 路径\n  basename?: string;\n  // 加载中显示的组件\n  loadingComponent?: React.ReactNode;\n  // History 实例（browserHistory/hashHistory/memoryHistory）\n  history: History;\n  // 是否启用流式渲染（默认 true）\n  useStream?: boolean;\n  // 是否仅返回组件（用于测试）\n  components?: boolean;\n  // 渲染完成回调\n  callback?: () =\u003e void;\n}\n```\n\n**返回值：**\n\n- 如果 `components: true`，返回 React 组件\n- 否则直接渲染到 DOM，无返回值\n\n#### `createClientRoutes({ routesById, routeComponents })`\n\n将 WinJS 路由格式转换为 React Router 格式。\n\n**参数：**\n\n```tsx\n{\n  routesById: IRoutesById;\n  routeComponents: Record\u003cstring, any\u003e;\n  parentId?: string;\n  loadingComponent?: React.ReactNode;\n  useStream?: boolean;\n}\n```\n\n#### `__getRoot()`\n\n获取当前的 React Root 实例（用于卸载等场景，如微前端）。\n\n### Hooks\n\n#### `useAppData()`\n\n获取应用上下文数据：\n\n```tsx\nconst { \n  routes,           // 路由配置\n  routeComponents,  // 路由组件映射\n  clientRoutes,     // 客户端路由树\n  pluginManager,    // 插件管理器\n  rootElement,      // 根元素\n  basename,         // base 路径\n  clientLoaderData, // 客户端加载的数据\n  preloadRoute,     // 预加载路由函数\n  history           // History 实例\n} = useAppData();\n```\n\n#### `useLoaderData()`\n\n获取当前路由的 clientLoader 加载的数据：\n\n```tsx\nfunction UserPage() {\n  const { data } = useLoaderData();\n  return \u003cdiv\u003e用户: {data.name}\u003c/div\u003e;\n}\n```\n\n#### `useClientLoaderData()`\n\n\u003e 已废弃，请使用 `useLoaderData()`\n\n#### `useRouteData()`\n\n获取当前路由的上下文数据：\n\n```tsx\nimport { useRouteData } from '@winner-fed/renderer-react';\n\nfunction MyComponent() {\n  const { route } = useRouteData();\n  return \u003cdiv\u003e当前路由 ID: {route.id}\u003c/div\u003e;\n}\n```\n\n#### `useRouteProps()`\n\n获取当前路由的属性（不包括 element）：\n\n```tsx\nfunction MyComponent() {\n  const routeProps = useRouteProps();\n  return \u003cdiv\u003e{routeProps.someCustomProp}\u003c/div\u003e;\n}\n```\n\n#### `useSelectedRoutes()`\n\n获取当前匹配的路由链（从根到当前路由）：\n\n```tsx\nfunction Breadcrumb() {\n  const routes = useSelectedRoutes();\n  return (\n    \u003cdiv\u003e\n      {routes.map(r =\u003e \u003cspan key={r.route.id}\u003e{r.route.path}\u003c/span\u003e)}\n    \u003c/div\u003e\n  );\n}\n```\n\n### 组件\n\n#### `\u003cLink\u003e`\n\n路由链接组件（从 react-router 导出）：\n\n```tsx\nimport { Link } from '@winner-fed/renderer-react';\n\n\u003cLink to=\"/about\"\u003e关于我们\u003c/Link\u003e\n\u003cLink to=\"/user/123\"\u003e用户页面\u003c/Link\u003e\n\u003cLink to=\"/settings\" state={{ from: 'home' }}\u003e设置\u003c/Link\u003e\n```\n\n#### `withRouter(Component)`\n\n为类组件注入路由相关的 props：\n\n```tsx\nimport { withRouter, RouteComponentProps } from '@winner-fed/renderer-react';\n\nclass MyComponent extends React.Component\u003cRouteComponentProps\u003e {\n  handleClick = () =\u003e {\n    this.props.history.push('/home');\n  };\n  \n  render() {\n    const { location, params, navigate } = this.props;\n    return \u003cdiv\u003e当前路径: {location.pathname}\u003c/div\u003e;\n  }\n}\n\nexport default withRouter(MyComponent);\n```\n\n注入的 props：\n\n- `history` - 包含 `back()`, `goBack()`, `push()`, `replace()` 等方法\n- `location` - 当前位置对象\n- `match` - 包含路由参数\n- `params` - 路由参数对象\n- `navigate` - 导航函数\n\n### React Router 导出\n\n直接从 `react-router` 重新导出的常用 API：\n\n```tsx\nimport {\n  // Hooks\n  useLocation,\n  useNavigate,\n  useParams,\n  useSearchParams,\n  useMatch,\n  useOutlet,\n  useOutletContext,\n  useResolvedPath,\n  useRoutes,\n  \n  // 组件\n  Link,\n  Navigate,\n  NavLink,\n  Outlet,\n  \n  // 工具函数\n  createSearchParams,\n  generatePath,\n  matchPath,\n  matchRoutes,\n  resolvePath,\n} from '@winner-fed/renderer-react';\n```\n\n### History 导出\n\n从 `history` 包重新导出：\n\n```tsx\nimport {\n  createBrowserHistory,\n  createHashHistory,\n  createMemoryHistory,\n  type History,\n} from '@winner-fed/renderer-react';\n```\n\n## 类型定义\n\n### `IRoute`\n\n路由定义接口：\n\n```tsx\ninterface IRoute {\n  id: string;                    // 路由唯一标识\n  path?: string;                 // 路由路径\n  index?: boolean;               // 是否为索引路由\n  parentId?: string;             // 父路由 ID\n  redirect?: string;             // 重定向路径\n  clientLoader?: ClientLoader;   // 客户端数据加载函数\n  routeProps?: Record\u003cstring, any\u003e; // 自定义路由属性\n}\n```\n\n### `IClientRoute`\n\n客户端路由接口（扩展自 IRoute）：\n\n```tsx\ninterface IClientRoute extends IRoute {\n  element?: React.ReactNode;       // 路由元素\n  Component?: React.ComponentType; // 路由组件\n  children?: IClientRoute[];       // 子路由\n  routes?: IClientRoute[];         // 子路由（遗留）\n}\n```\n\n### `IRoutesById`\n\n路由映射表：\n\n```tsx\ninterface IRoutesById {\n  [id: string]: IRoute;\n}\n```\n\n### `IRouteComponents`\n\n路由组件映射表：\n\n```tsx\ninterface IRouteComponents {\n  [id: string]: any; // React 组件\n}\n```\n\n### `ClientLoader`\n\n客户端数据加载器：\n\n```tsx\ntype ClientLoader = (() =\u003e Promise\u003cany\u003e) \u0026 {\n  hydrate?: boolean; // 是否需要水合\n};\n```\n\n## 插件系统集成\n\n渲染器与 WinJS 插件系统深度集成，提供多个插件钩子：\n\n### Provider 钩子（由内到外）\n\n```tsx\n// 1. innerProvider - 最内层 Provider\npluginManager.applyPlugins({\n  type: 'modify',\n  key: 'innerProvider',\n  initialValue: App,\n  args: { routes, history, plugin },\n});\n\n// 2. i18nProvider - 国际化 Provider\npluginManager.applyPlugins({\n  type: 'modify',\n  key: 'i18nProvider',\n  initialValue: App,\n  args: { routes, history, plugin },\n});\n\n// 3. accessProvider - 权限控制 Provider\npluginManager.applyPlugins({\n  type: 'modify',\n  key: 'accessProvider',\n  initialValue: App,\n  args: { routes, history, plugin },\n});\n\n// 4. dataflowProvider - 数据流 Provider\npluginManager.applyPlugins({\n  type: 'modify',\n  key: 'dataflowProvider',\n  initialValue: App,\n  args: { routes, history, plugin },\n});\n\n// 5. outerProvider - 最外层 Provider\npluginManager.applyPlugins({\n  type: 'modify',\n  key: 'outerProvider',\n  initialValue: App,\n  args: { routes, history, plugin },\n});\n\n// 6. rootContainer - 根容器\npluginManager.applyPlugins({\n  type: 'modify',\n  key: 'rootContainer',\n  initialValue: App,\n  args: { routes, history, plugin },\n});\n```\n\n### 事件钩子\n\n```tsx\n// 修改客户端路由\npluginManager.applyPlugins({\n  type: 'event',\n  key: 'patchClientRoutes',\n  args: { routes: clientRoutes },\n});\n\n// 路由变化事件\npluginManager.applyPlugins({\n  type: 'event',\n  key: 'onRouteChange',\n  args: {\n    routes,\n    clientRoutes,\n    location,\n    action,\n    basename,\n    isFirst: boolean,\n  },\n});\n```\n\n## 使用示例\n\n### 基础使用\n\n```tsx\nimport { renderClient } from '@winner-fed/renderer-react';\nimport { createBrowserHistory } from '@winner-fed/renderer-react';\n\nconst history = createBrowserHistory();\n\nrenderClient({\n  rootElement: document.getElementById('root'),\n  routes: {\n    'home': { id: 'home', path: '/' },\n    'about': { id: 'about', path: '/about' },\n  },\n  routeComponents: {\n    'home': () =\u003e \u003cdiv\u003e首页\u003c/div\u003e,\n    'about': () =\u003e \u003cdiv\u003e关于\u003c/div\u003e,\n  },\n  pluginManager: pluginManager,\n  history: history,\n  basename: '/',\n});\n```\n\n### 带数据加载\n\n```tsx\nconst routes = {\n  'user': {\n    id: 'user',\n    path: '/user/:id',\n    clientLoader: async () =\u003e {\n      const user = await fetchUser(params.id);\n      return { user };\n    },\n  },\n};\n\nfunction UserPage() {\n  const { data } = useLoaderData();\n  return \u003cdiv\u003e用户: {data.user.name}\u003c/div\u003e;\n}\n```\n\n### 路由重定向\n\n```tsx\nconst routes = {\n  'old-path': {\n    id: 'old-path',\n    path: '/old',\n    redirect: '/new',\n  },\n  'new-path': {\n    id: 'new-path',\n    path: '/new',\n  },\n};\n```\n\n### 嵌套路由\n\n```tsx\nconst routes = {\n  'layout': {\n    id: 'layout',\n    path: '/',\n  },\n  'home': {\n    id: 'home',\n    path: '/',\n    index: true,\n    parentId: 'layout',\n  },\n  'about': {\n    id: 'about',\n    path: 'about',\n    parentId: 'layout',\n  },\n};\n\nconst routeComponents = {\n  'layout': () =\u003e (\n    \u003cdiv\u003e\n      \u003cnav\u003e导航\u003c/nav\u003e\n      \u003cOutlet /\u003e\n    \u003c/div\u003e\n  ),\n  'home': () =\u003e \u003cdiv\u003e首页内容\u003c/div\u003e,\n  'about': () =\u003e \u003cdiv\u003e关于内容\u003c/div\u003e,\n};\n```\n\n### 自定义路由属性\n\n```tsx\nconst routes = {\n  'protected': {\n    id: 'protected',\n    path: '/protected',\n    routeProps: {\n      requireAuth: true,\n      keepQuery: true,\n    },\n  },\n};\n\nfunction ProtectedPage() {\n  const routeProps = useRouteProps();\n  if (routeProps.requireAuth \u0026\u0026 !isLoggedIn()) {\n    return \u003cNavigate to=\"/login\" /\u003e;\n  }\n  return \u003cdiv\u003e受保护的内容\u003c/div\u003e;\n}\n```\n\n### 预加载路由\n\n```tsx\nimport { Link, useAppData } from '@winner-fed/renderer-react';\n\nfunction Navigation() {\n  const { preloadRoute } = useAppData();\n  \n  // 程序化预加载\n  const handleHover = () =\u003e {\n    if (preloadRoute) {\n      preloadRoute('/dashboard');\n    }\n  };\n  \n  return (\n    \u003cnav\u003e\n      \u003cLink to=\"/home\"\u003e首页\u003c/Link\u003e\n      \u003cLink to=\"/products\"\u003e产品\u003c/Link\u003e\n      \n      {/* 程序化预加载 */}\n      \u003cbutton onMouseEnter={handleHover}\u003e控制台\u003c/button\u003e\n    \u003c/nav\u003e\n  );\n}\n```\n\n## 内部实现\n\n### 渲染流程\n\n1. **路由转换**：将 WinJS 的路由格式（IRoutesById）转换为 React Router 格式（IClientRoute[]）\n2. **插件集成**：应用插件系统的各种钩子（patchClientRoutes、onRouteChange 等）\n3. **数据加载**：在路由变化时自动执行 clientLoader 并缓存数据\n4. **Provider 包装**：应用多层 Provider（innerProvider → i18nProvider → accessProvider → dataflowProvider → outerProvider → rootContainer）\n5. **React 渲染**：使用 React 18+ 的 createRoot API 渲染应用\n\n### 路由转换规则\n\n- 根据 `parentId` 构建嵌套路由树\n- `index: true` 的路由转换为索引路由\n- `redirect` 字段转换为 `Navigate` 组件\n- `clientLoader` 保留在路由定义中，由渲染器管理数据加载\n\n### 性能优化\n\n- 使用 `useCallback` 避免不必要的函数重建\n- clientLoader 数据全局缓存，避免重复加载\n- 支持流式渲染（useStream），提升首屏加载速度\n- 通过 preloadRoute 支持程序化路由预加载\n\n## 依赖关系\n\n- **react** / **react-dom**：需要 React 19.0.0 或更高版本\n- **react-router**：使用 React Router v7 进行路由管理\n- **history**：使用 History v5，支持多种路由模式（Browser/Hash/Memory）\n- **@winner-fed/winjs**：与 WinJS 插件系统集成\n\n## 常见问题\n\n### 1. 如何在 clientLoader 中访问路由参数？\n\n目前 clientLoader 不直接接收参数，但可以通过 `window.location` 或组件内部的 hooks 获取：\n\n```tsx\nconst routes = {\n  'user': {\n    id: 'user',\n    path: '/user/:id',\n    clientLoader: async () =\u003e {\n      // 方案 1：从 URL 解析参数\n      const id = window.location.pathname.split('/').pop();\n      return { user: await fetchUser(id) };\n    },\n  },\n};\n```\n\n### 2. 如何在微前端场景下使用？\n\n使用 `__getRoot()` 获取 React Root 实例，在卸载时调用 `root.unmount()`：\n\n```tsx\nimport { renderClient, __getRoot } from '@winner-fed/renderer-react';\n\n// 挂载\nrenderClient({ /* ... */ });\n\n// 卸载\nconst root = __getRoot();\nif (root) root.unmount();\n```\n\n### 3. 如何处理路由守卫？\n\n通过插件系统的 `onRouteChange` 钩子实现：\n\n```tsx\nexport default {\n  onRouteChange({ location, routes }) {\n    // 路由守卫逻辑\n    if (needAuth \u0026\u0026 !isLoggedIn()) {\n      history.push('/login');\n    }\n  },\n};\n```\n\n### 4. 如何使用自定义 Loading 组件？\n\n通过 `loadingComponent` 参数传入：\n\n```tsx\nrenderClient({\n  // ...\n  loadingComponent: \u003cCustomSpinner /\u003e,\n});\n```\n\n### 5. 如何实现路由预加载？\n\n使用 `useAppData` 的 `preloadRoute` 方法：\n\n```tsx\nconst { preloadRoute } = useAppData();\n\n// 在鼠标悬停时预加载\nconst handleHover = () =\u003e {\n  if (preloadRoute) {\n    preloadRoute('/target-path');\n  }\n};\n```\n\n## 许可证\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwinjs-dev%2Fwinjs-renderer-react","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwinjs-dev%2Fwinjs-renderer-react","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwinjs-dev%2Fwinjs-renderer-react/lists"}