WinJS 框架的 React 渲染器,提供客户端渲染能力和 React Router 集成。
- ✅ React 19.x 支持 - 使用 React 19 的最新特性和并发渲染能力
- 🚦 React Router v7 集成 - 最新的路由管理和导航能力
- 🎯 多种路由模式 - 支持 Browser、Hash 和 Memory 路由
- 🔄 插件系统集成 - 与 WinJS 插件系统无缝集成
- 📦 TypeScript 支持 - 完整的类型定义
- 🎨 应用上下文 - 通过 Context 访问路由和应用数据
- ⚡ 懒加载 - 内置代码分割和懒加载支持
- 🔗 路由预加载 - 支持程序化路由预加载
- 🌊 流式渲染 - 支持 React Suspense 流式渲染
npm install @winner-fed/renderer-react
renderClient
是渲染 React 应用的主入口:
import { renderClient } from '@winner-fed/renderer-react';
renderClient({
rootElement: document.getElementById('root'),
routes: routesById,
routeComponents: routeComponents,
pluginManager: pluginManager,
basename: '/app',
history: history,
});
路由定义采用 WinJS 路由结构,会自动转换为 React Router 格式:
const routesById = {
'home': {
id: 'home',
path: '/',
parentId: undefined,
},
'about': {
id: 'about',
path: '/about',
parentId: undefined,
clientLoader: async () => {
return { data: 'About 页面数据' };
},
},
'user': {
id: 'user',
path: '/user/:id',
parentId: 'about',
},
};
const routeComponents = {
'home': HomePage,
'about': AboutPage,
'user': UserPage,
};
通过 useAppData
Hook 访问应用数据:
import { useAppData } from '@winner-fed/renderer-react';
function MyComponent() {
const { routes, clientRoutes, pluginManager, basename, history } = useAppData();
return <div>当前 basename: {basename}</div>;
}
将 React 应用渲染到 DOM。
参数:
interface RenderClientOpts {
// 公共路径配置
publicPath?: string;
// 是否运行时配置 publicPath
runtimePublicPath?: boolean;
// 挂载元素 ID(微前端场景可能变化)
mountElementId?: string;
// 挂载的 DOM 元素
rootElement?: HTMLElement;
// 路由配置(按 ID 索引)
routes: IRoutesById;
// 路由组件映射
routeComponents: IRouteComponents;
// 插件管理器实例
pluginManager: any;
// 路由 base 路径
basename?: string;
// 加载中显示的组件
loadingComponent?: React.ReactNode;
// History 实例(browserHistory/hashHistory/memoryHistory)
history: History;
// 是否启用流式渲染(默认 true)
useStream?: boolean;
// 是否仅返回组件(用于测试)
components?: boolean;
// 渲染完成回调
callback?: () => void;
}
返回值:
- 如果
components: true
,返回 React 组件 - 否则直接渲染到 DOM,无返回值
将 WinJS 路由格式转换为 React Router 格式。
参数:
{
routesById: IRoutesById;
routeComponents: Record<string, any>;
parentId?: string;
loadingComponent?: React.ReactNode;
useStream?: boolean;
}
获取当前的 React Root 实例(用于卸载等场景,如微前端)。
获取应用上下文数据:
const {
routes, // 路由配置
routeComponents, // 路由组件映射
clientRoutes, // 客户端路由树
pluginManager, // 插件管理器
rootElement, // 根元素
basename, // base 路径
clientLoaderData, // 客户端加载的数据
preloadRoute, // 预加载路由函数
history // History 实例
} = useAppData();
获取当前路由的 clientLoader 加载的数据:
function UserPage() {
const { data } = useLoaderData();
return <div>用户: {data.name}</div>;
}
已废弃,请使用
useLoaderData()
获取当前路由的上下文数据:
import { useRouteData } from '@winner-fed/renderer-react';
function MyComponent() {
const { route } = useRouteData();
return <div>当前路由 ID: {route.id}</div>;
}
获取当前路由的属性(不包括 element):
function MyComponent() {
const routeProps = useRouteProps();
return <div>{routeProps.someCustomProp}</div>;
}
获取当前匹配的路由链(从根到当前路由):
function Breadcrumb() {
const routes = useSelectedRoutes();
return (
<div>
{routes.map(r => <span key={r.route.id}>{r.route.path}</span>)}
</div>
);
}
路由链接组件(从 react-router 导出):
import { Link } from '@winner-fed/renderer-react';
<Link to="/about">关于我们</Link>
<Link to="/user/123">用户页面</Link>
<Link to="/settings" state={{ from: 'home' }}>设置</Link>
为类组件注入路由相关的 props:
import { withRouter, RouteComponentProps } from '@winner-fed/renderer-react';
class MyComponent extends React.Component<RouteComponentProps> {
handleClick = () => {
this.props.history.push('/home');
};
render() {
const { location, params, navigate } = this.props;
return <div>当前路径: {location.pathname}</div>;
}
}
export default withRouter(MyComponent);
注入的 props:
history
- 包含back()
,goBack()
,push()
,replace()
等方法location
- 当前位置对象match
- 包含路由参数params
- 路由参数对象navigate
- 导航函数
直接从 react-router
重新导出的常用 API:
import {
// Hooks
useLocation,
useNavigate,
useParams,
useSearchParams,
useMatch,
useOutlet,
useOutletContext,
useResolvedPath,
useRoutes,
// 组件
Link,
Navigate,
NavLink,
Outlet,
// 工具函数
createSearchParams,
generatePath,
matchPath,
matchRoutes,
resolvePath,
} from '@winner-fed/renderer-react';
从 history
包重新导出:
import {
createBrowserHistory,
createHashHistory,
createMemoryHistory,
type History,
} from '@winner-fed/renderer-react';
路由定义接口:
interface IRoute {
id: string; // 路由唯一标识
path?: string; // 路由路径
index?: boolean; // 是否为索引路由
parentId?: string; // 父路由 ID
redirect?: string; // 重定向路径
clientLoader?: ClientLoader; // 客户端数据加载函数
routeProps?: Record<string, any>; // 自定义路由属性
}
客户端路由接口(扩展自 IRoute):
interface IClientRoute extends IRoute {
element?: React.ReactNode; // 路由元素
Component?: React.ComponentType; // 路由组件
children?: IClientRoute[]; // 子路由
routes?: IClientRoute[]; // 子路由(遗留)
}
路由映射表:
interface IRoutesById {
[id: string]: IRoute;
}
路由组件映射表:
interface IRouteComponents {
[id: string]: any; // React 组件
}
客户端数据加载器:
type ClientLoader = (() => Promise<any>) & {
hydrate?: boolean; // 是否需要水合
};
渲染器与 WinJS 插件系统深度集成,提供多个插件钩子:
// 1. innerProvider - 最内层 Provider
pluginManager.applyPlugins({
type: 'modify',
key: 'innerProvider',
initialValue: App,
args: { routes, history, plugin },
});
// 2. i18nProvider - 国际化 Provider
pluginManager.applyPlugins({
type: 'modify',
key: 'i18nProvider',
initialValue: App,
args: { routes, history, plugin },
});
// 3. accessProvider - 权限控制 Provider
pluginManager.applyPlugins({
type: 'modify',
key: 'accessProvider',
initialValue: App,
args: { routes, history, plugin },
});
// 4. dataflowProvider - 数据流 Provider
pluginManager.applyPlugins({
type: 'modify',
key: 'dataflowProvider',
initialValue: App,
args: { routes, history, plugin },
});
// 5. outerProvider - 最外层 Provider
pluginManager.applyPlugins({
type: 'modify',
key: 'outerProvider',
initialValue: App,
args: { routes, history, plugin },
});
// 6. rootContainer - 根容器
pluginManager.applyPlugins({
type: 'modify',
key: 'rootContainer',
initialValue: App,
args: { routes, history, plugin },
});
// 修改客户端路由
pluginManager.applyPlugins({
type: 'event',
key: 'patchClientRoutes',
args: { routes: clientRoutes },
});
// 路由变化事件
pluginManager.applyPlugins({
type: 'event',
key: 'onRouteChange',
args: {
routes,
clientRoutes,
location,
action,
basename,
isFirst: boolean,
},
});
import { renderClient } from '@winner-fed/renderer-react';
import { createBrowserHistory } from '@winner-fed/renderer-react';
const history = createBrowserHistory();
renderClient({
rootElement: document.getElementById('root'),
routes: {
'home': { id: 'home', path: '/' },
'about': { id: 'about', path: '/about' },
},
routeComponents: {
'home': () => <div>首页</div>,
'about': () => <div>关于</div>,
},
pluginManager: pluginManager,
history: history,
basename: '/',
});
const routes = {
'user': {
id: 'user',
path: '/user/:id',
clientLoader: async () => {
const user = await fetchUser(params.id);
return { user };
},
},
};
function UserPage() {
const { data } = useLoaderData();
return <div>用户: {data.user.name}</div>;
}
const routes = {
'old-path': {
id: 'old-path',
path: '/old',
redirect: '/new',
},
'new-path': {
id: 'new-path',
path: '/new',
},
};
const routes = {
'layout': {
id: 'layout',
path: '/',
},
'home': {
id: 'home',
path: '/',
index: true,
parentId: 'layout',
},
'about': {
id: 'about',
path: 'about',
parentId: 'layout',
},
};
const routeComponents = {
'layout': () => (
<div>
<nav>导航</nav>
<Outlet />
</div>
),
'home': () => <div>首页内容</div>,
'about': () => <div>关于内容</div>,
};
const routes = {
'protected': {
id: 'protected',
path: '/protected',
routeProps: {
requireAuth: true,
keepQuery: true,
},
},
};
function ProtectedPage() {
const routeProps = useRouteProps();
if (routeProps.requireAuth && !isLoggedIn()) {
return <Navigate to="/login" />;
}
return <div>受保护的内容</div>;
}
import { Link, useAppData } from '@winner-fed/renderer-react';
function Navigation() {
const { preloadRoute } = useAppData();
// 程序化预加载
const handleHover = () => {
if (preloadRoute) {
preloadRoute('/dashboard');
}
};
return (
<nav>
<Link to="/home">首页</Link>
<Link to="/products">产品</Link>
{/* 程序化预加载 */}
<button onMouseEnter={handleHover}>控制台</button>
</nav>
);
}
- 路由转换:将 WinJS 的路由格式(IRoutesById)转换为 React Router 格式(IClientRoute[])
- 插件集成:应用插件系统的各种钩子(patchClientRoutes、onRouteChange 等)
- 数据加载:在路由变化时自动执行 clientLoader 并缓存数据
- Provider 包装:应用多层 Provider(innerProvider → i18nProvider → accessProvider → dataflowProvider → outerProvider → rootContainer)
- React 渲染:使用 React 18+ 的 createRoot API 渲染应用
- 根据
parentId
构建嵌套路由树 index: true
的路由转换为索引路由redirect
字段转换为Navigate
组件clientLoader
保留在路由定义中,由渲染器管理数据加载
- 使用
useCallback
避免不必要的函数重建 - clientLoader 数据全局缓存,避免重复加载
- 支持流式渲染(useStream),提升首屏加载速度
- 通过 preloadRoute 支持程序化路由预加载
- react / react-dom:需要 React 19.0.0 或更高版本
- react-router:使用 React Router v7 进行路由管理
- history:使用 History v5,支持多种路由模式(Browser/Hash/Memory)
- @winner-fed/winjs:与 WinJS 插件系统集成
目前 clientLoader 不直接接收参数,但可以通过 window.location
或组件内部的 hooks 获取:
const routes = {
'user': {
id: 'user',
path: '/user/:id',
clientLoader: async () => {
// 方案 1:从 URL 解析参数
const id = window.location.pathname.split('/').pop();
return { user: await fetchUser(id) };
},
},
};
使用 __getRoot()
获取 React Root 实例,在卸载时调用 root.unmount()
:
import { renderClient, __getRoot } from '@winner-fed/renderer-react';
// 挂载
renderClient({ /* ... */ });
// 卸载
const root = __getRoot();
if (root) root.unmount();
通过插件系统的 onRouteChange
钩子实现:
export default {
onRouteChange({ location, routes }) {
// 路由守卫逻辑
if (needAuth && !isLoggedIn()) {
history.push('/login');
}
},
};
通过 loadingComponent
参数传入:
renderClient({
// ...
loadingComponent: <CustomSpinner />,
});
使用 useAppData
的 preloadRoute
方法:
const { preloadRoute } = useAppData();
// 在鼠标悬停时预加载
const handleHover = () => {
if (preloadRoute) {
preloadRoute('/target-path');
}
};
MIT