From 259ab3a0cbe41ef35741ff1c59ba6b6ef2757c93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=87=E9=A3=8E?= Date: Thu, 23 Nov 2023 11:09:39 +0800 Subject: [PATCH 1/5] =?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=20feature(SSR)?= =?UTF-8?q?:=20=E6=94=AF=E6=8C=81ssr=E9=99=8D=E7=BA=A7=EF=BC=8C=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AB=AF=E5=85=9C=E5=BA=95=E5=8A=A0=E8=BD=BDserverLoa?= =?UTF-8?q?der=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/renderer-react/src/appContext.ts | 47 +++++++++++++++++++++-- packages/renderer-react/src/browser.tsx | 44 ++++++++++++++------- 2 files changed, 74 insertions(+), 17 deletions(-) diff --git a/packages/renderer-react/src/appContext.ts b/packages/renderer-react/src/appContext.ts index dc7ba72bff54..8cc8f7ffcca4 100644 --- a/packages/renderer-react/src/appContext.ts +++ b/packages/renderer-react/src/appContext.ts @@ -1,5 +1,6 @@ import React from 'react'; import { matchRoutes, useLocation } from 'react-router-dom'; +import { fetchServerLoader } from './browser'; import { useRouteData } from './routeContext'; import { IClientRoute, @@ -45,10 +46,50 @@ export function useRouteProps = any>() { 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(() => { + 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..51009ed78638 100644 --- a/packages/renderer-react/src/browser.tsx +++ b/packages/renderer-react/src/browser.tsx @@ -265,25 +265,18 @@ 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: any) => { // 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 里是不存在的 @@ -356,6 +349,29 @@ export function renderClient(opts: RenderClientOpts) { ReactDOM.render(, rootElement); } -function withEndSlash(str: string) { +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}/`; } From dfdad62fbf6a2b982ec473aa08a6bca1278f5367 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=87=E9=A3=8E?= Date: Thu, 23 Nov 2023 11:13:36 +0800 Subject: [PATCH 2/5] =?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=20feature(SSR)?= =?UTF-8?q?:=20=E6=94=AF=E6=8C=81ssr=E9=99=8D=E7=BA=A7=EF=BC=8C=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AB=AF=E5=85=9C=E5=BA=95=E5=8A=A0=E8=BD=BDserverLoa?= =?UTF-8?q?der=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/renderer-react/src/appContext.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/renderer-react/src/appContext.ts b/packages/renderer-react/src/appContext.ts index 8cc8f7ffcca4..3e417ca4b97a 100644 --- a/packages/renderer-react/src/appContext.ts +++ b/packages/renderer-react/src/appContext.ts @@ -62,15 +62,18 @@ export function useServerLoaderData() { return has ? ret : undefined; }); React.useEffect(() => { + // @ts-ignore if (!window.__UMI_LOADER_DATA__) { // 支持ssr降级,客户端兜底加载serverLoader数据 Promise.all( routes + // @ts-ignore .filter((route) => route.route.hasServerLoader) .map( (route) => new Promise((resolve) => { fetchServerLoader({ + // @ts-ignore id: route.route.id, basename, cb: resolve, From fbbcab31fcc0b31c84dae54a8a924ba2e5f5c236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=87=E9=A3=8E?= Date: Thu, 23 Nov 2023 11:20:09 +0800 Subject: [PATCH 3/5] =?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=20feature(SSR)?= =?UTF-8?q?:=20=E6=94=AF=E6=8C=81ssr=E9=99=8D=E7=BA=A7=EF=BC=8C=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AB=AF=E5=85=9C=E5=BA=95=E5=8A=A0=E8=BD=BDserverLoa?= =?UTF-8?q?der=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/renderer-react/src/appContext.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/renderer-react/src/appContext.ts b/packages/renderer-react/src/appContext.ts index 3e417ca4b97a..5e2180114d82 100644 --- a/packages/renderer-react/src/appContext.ts +++ b/packages/renderer-react/src/appContext.ts @@ -53,6 +53,7 @@ export function useServerLoaderData() { let has = false; routes.forEach((route) => { // 多级路由嵌套时,需要合并多级路由serverLoader的数据 + // @ts-ignore const routeData = serverLoaderData[route.route.id]; if (routeData) { Object.assign(ret, routeData); From e6608c69a19b678e4e8616500df90cb10e858408 Mon Sep 17 00:00:00 2001 From: fz6m <59400654+fz6m@users.noreply.github.com> Date: Fri, 24 Nov 2023 03:46:14 +0800 Subject: [PATCH 4/5] refactor: fix circular refer --- packages/renderer-react/src/appContext.ts | 16 +++++------ packages/renderer-react/src/browser.tsx | 31 ++-------------------- packages/renderer-react/src/dataFetcher.ts | 26 ++++++++++++++++++ packages/renderer-react/src/types.ts | 8 ++++++ 4 files changed, 44 insertions(+), 37 deletions(-) create mode 100644 packages/renderer-react/src/dataFetcher.ts diff --git a/packages/renderer-react/src/appContext.ts b/packages/renderer-react/src/appContext.ts index 5e2180114d82..973526c302ef 100644 --- a/packages/renderer-react/src/appContext.ts +++ b/packages/renderer-react/src/appContext.ts @@ -1,12 +1,13 @@ import React from 'react'; import { matchRoutes, useLocation } from 'react-router-dom'; -import { fetchServerLoader } from './browser'; +import { fetchServerLoader } from './dataFetcher'; import { useRouteData } from './routeContext'; import { IClientRoute, ILoaderData, IRouteComponents, IRoutesById, + ISelectedRoutes, } from './types'; interface IAppContextType { @@ -34,14 +35,16 @@ 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; @@ -52,8 +55,7 @@ export function useServerLoaderData() { const ret = {} as Awaited>; let has = false; routes.forEach((route) => { - // 多级路由嵌套时,需要合并多级路由serverLoader的数据 - // @ts-ignore + // 多级路由嵌套时,需要合并多级路由 serverLoader 的数据 const routeData = serverLoaderData[route.route.id]; if (routeData) { Object.assign(ret, routeData); @@ -65,16 +67,14 @@ export function useServerLoaderData() { React.useEffect(() => { // @ts-ignore if (!window.__UMI_LOADER_DATA__) { - // 支持ssr降级,客户端兜底加载serverLoader数据 + // 支持 ssr 降级,客户端兜底加载 serverLoader 数据 Promise.all( routes - // @ts-ignore .filter((route) => route.route.hasServerLoader) .map( (route) => new Promise((resolve) => { fetchServerLoader({ - // @ts-ignore id: route.route.id, basename, cb: resolve, diff --git a/packages/renderer-react/src/browser.tsx b/packages/renderer-react/src/browser.tsx index 51009ed78638..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'; @@ -268,10 +269,9 @@ const getBrowser = ( fetchServerLoader({ id, basename, - cb: (data: any) => { + 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 })); }); @@ -348,30 +348,3 @@ export function renderClient(opts: RenderClientOpts) { // @ts-ignore ReactDOM.render(, rootElement); } - -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/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; } From 69ca1dca3e3a49434f7787c8d175259bdc15a441 Mon Sep 17 00:00:00 2001 From: fz6m <59400654+fz6m@users.noreply.github.com> Date: Fri, 24 Nov 2023 04:12:43 +0800 Subject: [PATCH 5/5] test: add test case --- examples/ssr-demo/plugin.ts | 28 +++++++++++++++++++ .../ssr-demo/src/pages/users/user2/info.tsx | 3 +- 2 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 examples/ssr-demo/plugin.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' }; }