From 6d45e024676b91444f152b1f760c7d55c2e09c7f Mon Sep 17 00:00:00 2001 From: gwuhaolin Date: Mon, 27 Nov 2023 16:27:27 +0800 Subject: [PATCH] =?UTF-8?q?fix(SSR):=20=E4=BF=AE=E5=A4=8D=E5=A4=9A?= =?UTF-8?q?=E7=BA=A7=E8=B7=AF=E7=94=B1=E5=B5=8C=E5=A5=97=E6=97=B6=EF=BC=8C?= =?UTF-8?q?=E9=9C=80=E8=A6=81=E5=90=88=E5=B9=B6=E5=A4=9A=E7=BA=A7=E8=B7=AF?= =?UTF-8?q?=E7=94=B1serverLoader=E7=9A=84=E6=95=B0=E6=8D=AE=20&=20feature(?= =?UTF-8?q?SSR):=20=E6=94=AF=E6=8C=81ssr=E9=99=8D=E7=BA=A7=EF=BC=8C?= =?UTF-8?q?=E5=AE=A2=E6=88=B7=E7=AB=AF=E5=85=9C=E5=BA=95=E5=8A=A0=E8=BD=BD?= =?UTF-8?q?serverLoader=E6=95=B0=E6=8D=AE=20(#11894)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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: 奇风 Co-authored-by: fz6m <59400654+fz6m@users.noreply.github.com> --- examples/ssr-demo/plugin.ts | 28 ++++++++++ .../ssr-demo/src/pages/users/user2/info.tsx | 3 +- packages/renderer-react/src/appContext.ts | 55 +++++++++++++++++-- packages/renderer-react/src/browser.tsx | 25 +++------ packages/renderer-react/src/dataFetcher.ts | 26 +++++++++ packages/renderer-react/src/types.ts | 8 +++ 6 files changed, 120 insertions(+), 25 deletions(-) create mode 100644 examples/ssr-demo/plugin.ts create mode 100644 packages/renderer-react/src/dataFetcher.ts diff --git a/examples/ssr-demo/plugin.ts b/examples/ssr-demo/plugin.ts new file mode 100644 index 000000000000..c6f5bd1b336b --- /dev/null +++ b/examples/ssr-demo/plugin.ts @@ -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(); + }); + }); +}; diff --git a/examples/ssr-demo/src/pages/users/user2/info.tsx b/examples/ssr-demo/src/pages/users/user2/info.tsx index 42300dde65e0..a78084122427 100644 --- a/examples/ssr-demo/src/pages/users/user2/info.tsx +++ b/examples/ssr-demo/src/pages/users/user2/info.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { useClientLoaderData, useServerLoaderData } from 'umi'; export default () => { @@ -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' }; } diff --git a/packages/renderer-react/src/appContext.ts b/packages/renderer-react/src/appContext.ts index dc7ba72bff54..973526c302ef 100644 --- a/packages/renderer-react/src/appContext.ts +++ b/packages/renderer-react/src/appContext.ts @@ -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 { @@ -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 = 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; export function useServerLoaderData() { - const route = useRouteData(); - const appData = useAppData(); + const routes = useSelectedRoutes(); + const { serverLoaderData, basename } = useAppData(); + const [data, setData] = React.useState(() => { + const ret = {} as Awaited>; + 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>; + datas.forEach((data) => { + Object.assign(res, data); + }); + setData(res); + } + }); + } + }, []); return { - data: appData.serverLoaderData[route.route.id] as Awaited>, + data, }; } diff --git a/packages/renderer-react/src/browser.tsx b/packages/renderer-react/src/browser.tsx index 408e576c4676..2e3adcc7a6f9 100644 --- a/packages/renderer-react/src/browser.tsx +++ b/packages/renderer-react/src/browser.tsx @@ -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'; @@ -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 里是不存在的 @@ -355,7 +348,3 @@ export function renderClient(opts: RenderClientOpts) { // @ts-ignore ReactDOM.render(, rootElement); } - -function withEndSlash(str: string) { - return str.endsWith('/') ? str : `${str}/`; -} diff --git a/packages/renderer-react/src/dataFetcher.ts b/packages/renderer-react/src/dataFetcher.ts new file mode 100644 index 000000000000..2122a46444ec --- /dev/null +++ b/packages/renderer-react/src/dataFetcher.ts @@ -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}/`; +} diff --git a/packages/renderer-react/src/types.ts b/packages/renderer-react/src/types.ts index 58f3183674b2..8e59aa6fb00c 100644 --- a/packages/renderer-react/src/types.ts +++ b/packages/renderer-react/src/types.ts @@ -1,3 +1,5 @@ +import type { RouteMatch, RouteObject } from 'react-router-dom'; + export interface IRouteSSRProps { clientLoader?: () => Promise; hasServerLoader?: boolean; @@ -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; }