Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 5 commits into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading