Skip to content

Commit

Permalink
fix(SSR): 修复多级路由嵌套时,需要合并多级路由serverLoader的数据 & feature(SSR): 支持ssr降级,客…
Browse files Browse the repository at this point in the history
…户端兜底加载serverLoader数据 (#11894)

* fix(SSR): 修复多级路由嵌套时,需要合并多级路由serverLoader的数据
feature(SSR): 支持ssr降级,客户端兜底加载serverLoader数据

* fix(SSR): 修复多级路由嵌套时,需要合并多级路由serverLoader的数据
feature(SSR): 支持ssr降级,客户端兜底加载serverLoader数据

* fix(SSR): 修复多级路由嵌套时,需要合并多级路由serverLoader的数据
feature(SSR): 支持ssr降级,客户端兜底加载serverLoader数据

* refactor: fix circular refer

* test: add test case

---------

Co-authored-by: 奇风 <[email protected]>
Co-authored-by: fz6m <[email protected]>
  • Loading branch information
3 people authored Nov 27, 2023
1 parent 8f1cf2f commit 6d45e02
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 25 deletions.
28 changes: 28 additions & 0 deletions examples/ssr-demo/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { IApi } from 'umi';

export default (api: IApi) => {
// Only for mock, this hack is incomplete, do not use it in production environment
api.onBeforeMiddleware(({ app }) => {
app.use((req, res, next) => {
if (req.query?.fallback) {
// modify response
const originWrite = res.write;
// @ts-ignore
res.write = function (chunk) {
const isHtml = ~(res.getHeader('Content-Type') as string)?.indexOf(
'text/html',
);
if (isHtml) {
chunk instanceof Buffer && (chunk = chunk.toString());
chunk = chunk.replace(
/window\.__UMI_LOADER_DATA__/,
'window.__UMI_LOADER_DATA_FALLBACK__',
);
}
originWrite.apply(this, arguments as any);
};
}
next();
});
});
};
3 changes: 1 addition & 2 deletions examples/ssr-demo/src/pages/users/user2/info.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import React from 'react';
import { useClientLoaderData, useServerLoaderData } from 'umi';

export default () => {
Expand All @@ -20,5 +19,5 @@ export async function clientLoader() {

export async function serverLoader() {
await new Promise((resolve) => setTimeout(resolve, Math.random() * 1000));
return { message: 'data from server loader of users/user2/info.tsx' };
return { messageUser2: 'data from server loader of users/user2/info.tsx' };
}
55 changes: 50 additions & 5 deletions packages/renderer-react/src/appContext.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import React from 'react';
import { matchRoutes, useLocation } from 'react-router-dom';
import { fetchServerLoader } from './dataFetcher';
import { useRouteData } from './routeContext';
import {
IClientRoute,
ILoaderData,
IRouteComponents,
IRoutesById,
ISelectedRoutes,
} from './types';

interface IAppContextType {
Expand Down Expand Up @@ -33,22 +35,65 @@ export function useSelectedRoutes() {
const location = useLocation();
const { clientRoutes } = useAppData();
// use `useLocation` get location without `basename`, not need `basename` param
const routes = matchRoutes(clientRoutes, location.pathname);
const routes = matchRoutes(clientRoutes, location.pathname) as
| ISelectedRoutes[]
| undefined;
return routes || [];
}

export function useRouteProps<T extends Record<string, any> = any>() {
const currentRoute = useSelectedRoutes().slice(-1);
const { element: _, ...props } = currentRoute[0]?.route || {};
return props as T;
return props as any as T;
}

type ServerLoaderFunc = (...args: any[]) => Promise<any> | any;
export function useServerLoaderData<T extends ServerLoaderFunc = any>() {
const route = useRouteData();
const appData = useAppData();
const routes = useSelectedRoutes();
const { serverLoaderData, basename } = useAppData();
const [data, setData] = React.useState(() => {
const ret = {} as Awaited<ReturnType<T>>;
let has = false;
routes.forEach((route) => {
// 多级路由嵌套时,需要合并多级路由 serverLoader 的数据
const routeData = serverLoaderData[route.route.id];
if (routeData) {
Object.assign(ret, routeData);
has = true;
}
});
return has ? ret : undefined;
});
React.useEffect(() => {
// @ts-ignore
if (!window.__UMI_LOADER_DATA__) {
// 支持 ssr 降级,客户端兜底加载 serverLoader 数据
Promise.all(
routes
.filter((route) => route.route.hasServerLoader)
.map(
(route) =>
new Promise((resolve) => {
fetchServerLoader({
id: route.route.id,
basename,
cb: resolve,
});
}),
),
).then((datas) => {
if (datas.length) {
const res = {} as Awaited<ReturnType<T>>;
datas.forEach((data) => {
Object.assign(res, data);
});
setData(res);
}
});
}
}, []);
return {
data: appData.serverLoaderData[route.route.id] as Awaited<ReturnType<T>>,
data,
};
}

Expand Down
25 changes: 7 additions & 18 deletions packages/renderer-react/src/browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import React, {
import ReactDOM from 'react-dom/client';
import { matchRoutes, Router, useRoutes } from 'react-router-dom';
import { AppContext, useAppData } from './appContext';
import { fetchServerLoader } from './dataFetcher';
import { createClientRoutes } from './routes';
import { ILoaderData, IRouteComponents, IRoutesById } from './types';

Expand Down Expand Up @@ -265,25 +266,17 @@ const getBrowser = (
// server loader
// use ?. since routes patched with patchClientRoutes is not exists in opts.routes
if (!isFirst && opts.routes[id]?.hasServerLoader) {
const query = new URLSearchParams({
route: id,
url: window.location.href,
}).toString();
// 在有basename的情况下__serverLoader的请求路径需要加上basename
const url = `${withEndSlash(basename)}__serverLoader?${query}`;
fetch(url, {
credentials: 'include',
})
.then((d) => d.json())
.then((data) => {
fetchServerLoader({
id,
basename,
cb: (data) => {
// setServerLoaderData when startTransition because if ssr is enabled,
// the component may being hydrated and setLoaderData will break the hydration
// @ts-ignore
React.startTransition(() => {
setServerLoaderData((d) => ({ ...d, [id]: data }));
});
})
.catch(console.error);
},
});
}
// client loader
// onPatchClientRoutes 添加的 route 在 opts.routes 里是不存在的
Expand Down Expand Up @@ -355,7 +348,3 @@ export function renderClient(opts: RenderClientOpts) {
// @ts-ignore
ReactDOM.render(<Browser />, rootElement);
}

function withEndSlash(str: string) {
return str.endsWith('/') ? str : `${str}/`;
}
26 changes: 26 additions & 0 deletions packages/renderer-react/src/dataFetcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export function fetchServerLoader({
id,
basename,
cb,
}: {
id: string;
basename?: string;
cb: (data: any) => void;
}) {
const query = new URLSearchParams({
route: id,
url: window.location.href,
}).toString();
// 在有basename的情况下__serverLoader的请求路径需要加上basename
const url = `${withEndSlash(basename)}__serverLoader?${query}`;
fetch(url, {
credentials: 'include',
})
.then((d) => d.json())
.then(cb)
.catch(console.error);
}

function withEndSlash(str: string = '') {
return str.endsWith('/') ? str : `${str}/`;
}
8 changes: 8 additions & 0 deletions packages/renderer-react/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { RouteMatch, RouteObject } from 'react-router-dom';

export interface IRouteSSRProps {
clientLoader?: () => Promise<any>;
hasServerLoader?: boolean;
Expand All @@ -22,6 +24,12 @@ export interface IClientRoute extends IRoute {
routes?: IClientRoute[];
}

export interface ISelectedRoute extends IRoute, RouteObject {}

export interface ISelectedRoutes extends RouteMatch {
route: ISelectedRoute;
}

export interface IRoutesById {
[id: string]: IRoute;
}
Expand Down

0 comments on commit 6d45e02

Please sign in to comment.