From fe3b896a05502ad135303ee297a9fcc856c29c20 Mon Sep 17 00:00:00 2001 From: wangyiming Date: Tue, 5 Aug 2025 16:59:29 +0800 Subject: [PATCH 1/2] refactor: remove useLoader --- .../en/apis/app/runtime/core/use-loader.mdx | 89 ------- .../server-monitor/logger.mdx | 4 - .../zh/apis/app/runtime/core/use-loader.mdx | 89 ------- .../server-monitor/logger.mdx | 4 - .../src/cli/ssr/babel-plugin-ssr-loader-id.ts | 160 ------------ .../plugin-runtime/src/cli/ssr/index.ts | 3 - .../plugin-runtime/src/core/browser/index.tsx | 20 -- .../src/core/context/runtime.ts | 3 - .../runtime/plugin-runtime/src/core/index.ts | 1 - .../plugin-runtime/src/core/loader/index.ts | 1 - .../src/core/loader/loaderManager.ts | 231 ------------------ .../src/core/loader/useLoader.ts | 166 ------------- .../src/core/server/requestHandler.tsx | 13 +- .../src/core/server/string/index.ts | 18 -- .../src/core/server/string/prefetch.tsx | 101 -------- .../src/core/server/string/ssrData.ts | 4 +- .../plugin-runtime/src/core/server/tracer.ts | 2 - .../runtime/plugin-runtime/src/core/types.ts | 4 +- packages/runtime/plugin-runtime/src/index.ts | 1 - .../bff-api-app/package.json | 2 +- .../bff-client-app/modern.config.ts | 2 +- .../bff-client-app/src/base/App.tsx | 4 +- .../bff-client-app/src/custom-sdk/App.tsx | 16 +- .../bff-client-app/src/ssr/App.tsx | 2 +- .../bff-client-app/src/upload/App.tsx | 2 +- .../bff-client-app/src/useLoader.ts | 25 ++ .../bff-indep-client-app/modern.config.ts | 2 +- .../src/custom-sdk/App.tsx | 14 +- .../bff-indep-client-app/src/ssr/App.tsx | 2 +- .../bff-indep-client-app/src/useLoader.ts | 25 ++ .../bff-corss-project/tests/index.test.ts | 32 ++- .../fixtures/use-loader/modern.config.ts | 7 - .../runtime/fixtures/use-loader/package.json | 11 - .../runtime/fixtures/use-loader/src/App.jsx | 60 ----- tests/integration/runtime/package.json | 5 - tests/integration/runtime/test/index.test.js | 96 -------- 36 files changed, 110 insertions(+), 1111 deletions(-) delete mode 100644 packages/document/main-doc/docs/en/apis/app/runtime/core/use-loader.mdx delete mode 100644 packages/document/main-doc/docs/zh/apis/app/runtime/core/use-loader.mdx delete mode 100644 packages/runtime/plugin-runtime/src/cli/ssr/babel-plugin-ssr-loader-id.ts delete mode 100644 packages/runtime/plugin-runtime/src/core/loader/index.ts delete mode 100644 packages/runtime/plugin-runtime/src/core/loader/loaderManager.ts delete mode 100644 packages/runtime/plugin-runtime/src/core/loader/useLoader.ts delete mode 100644 packages/runtime/plugin-runtime/src/core/server/string/prefetch.tsx create mode 100644 tests/integration/bff-corss-project/bff-client-app/src/useLoader.ts create mode 100644 tests/integration/bff-corss-project/bff-indep-client-app/src/useLoader.ts delete mode 100644 tests/integration/runtime/fixtures/use-loader/modern.config.ts delete mode 100644 tests/integration/runtime/fixtures/use-loader/package.json delete mode 100644 tests/integration/runtime/fixtures/use-loader/src/App.jsx delete mode 100644 tests/integration/runtime/package.json delete mode 100644 tests/integration/runtime/test/index.test.js diff --git a/packages/document/main-doc/docs/en/apis/app/runtime/core/use-loader.mdx b/packages/document/main-doc/docs/en/apis/app/runtime/core/use-loader.mdx deleted file mode 100644 index c63d24c29a88..000000000000 --- a/packages/document/main-doc/docs/en/apis/app/runtime/core/use-loader.mdx +++ /dev/null @@ -1,89 +0,0 @@ ---- -title: useLoader ---- -# useLoader - -`useLoader` is an Isomorphic API, usually used to make asynchronous requests. During Server-Side Rendering (SSR), the server uses `useLoader` to prefetch the data, which is then reused on the client side. - -:::tip -The `useLoader` API is currently not supported when using Rspack as the bundler. -::: - -## Usage - -```ts -import { useLoader } from '@modern-js/runtime'; -``` - -## Function Signature - -```ts -type LoaderFn = (context: runtimeContext) => Promise; -type Options = { - onSuccess: (data: Record) => void; - onError: (error: Error) => void; - initialData: Record; - skip: boolean; - params: Record; - static: boolean; -}; -type ReturnData = { - data: Record; - loading: boolean; - error: Error; - reload: (params?: Record) => Promise | undefined; - reloading: boolean; -}; - -function useLoader(loaderFn: LoaderFn, options: Options): ReturnData; -``` - -:::info -`runtimeContext` can refer to [useRuntimeContext](/apis/app/runtime/core/use-runtime-context). - -::: - -### Input - -- `loaderFn`: function for loading data, returning a Promise. -- `options`: optional configuration. - - `onSuccess`: successful callback. - - `onError`: error callback. - - `initialData`: the initial data before the first execution,. - - `skip`: when the value is `true`, the function does not execute. - - `params`: when the result of the `params` serialization changes, the function is re-executed. `params` is also passed in as the second argument of the function. - - `static`: when the value is `true`, `useLoader` is used for [SSG](/guides/basic-features/render/ssg). - -### Return Value - -- `data`: return data on successful execution. -- `loading`: indicates whether the function is in execution. -- `error`: error message when function execution fails. -- `reload`: the function can be re-executed after the call. - - `params`: when the value is `undefined`, the last value will be reused; otherwise, the function will be re-executed with the new value. -- `reloading`: during the execution of the call to `reload`, the value of `reloading` is `true`. - -## Example - -```ts -function Container() { - const { data, error, loading } = useLoader( - async (context, params) => { - console.log(params) // nicole - return fetch(user); - }, - { - onSuccess: data => { - console.log('I did success:(', data); - }, - onError: error => { - console.log('I met error:)', error); - }, - initialData: { name: 'nicole', gender: 'female' }, - params: 'nicole' - } - ); - - return ...; -} -``` diff --git a/packages/document/main-doc/docs/en/guides/advanced-features/server-monitor/logger.mdx b/packages/document/main-doc/docs/en/guides/advanced-features/server-monitor/logger.mdx index 95e7f95f9795..a957ce260a33 100644 --- a/packages/document/main-doc/docs/en/guides/advanced-features/server-monitor/logger.mdx +++ b/packages/document/main-doc/docs/en/guides/advanced-features/server-monitor/logger.mdx @@ -17,11 +17,7 @@ Modern.js also retains SSR logs from legacy versions using `useLoader`: | Stage | Message | Level | | ------------- | ------------------------------- | ----- | | PRERENDER | App Prerender | error | -| USE_LOADER | App run useLoader | error | -:::tip -The `useLoader` API is now deprecated. We recommend migrating to convention-based routing and using Data Loaders for data fetching. Applications already using Data Loaders can enable [`ssr.disablePrerender`](/configure/app/server/ssr.html#object-type) to disable prerendering and improve SSR performance. -::: ## Built-in Monitor diff --git a/packages/document/main-doc/docs/zh/apis/app/runtime/core/use-loader.mdx b/packages/document/main-doc/docs/zh/apis/app/runtime/core/use-loader.mdx deleted file mode 100644 index 2e05f4e912b4..000000000000 --- a/packages/document/main-doc/docs/zh/apis/app/runtime/core/use-loader.mdx +++ /dev/null @@ -1,89 +0,0 @@ ---- -title: useLoader ---- -# useLoader - -一个同构的 API,通常会用来做异步请求。当 SSR 的时候,服务端使用 `useLoader` 预加载数据,同时浏览器端也会复用这部分数据。 - -:::tip -在使用 Rspack 作为打包工具时,暂不支持使用 useLoader API。 -::: - -## 使用姿势 - -```ts -import { useLoader } from '@modern-js/runtime'; -``` - -## 函数签名 - -```ts -type LoaderFn = (context: runtimeContext) => Promise; -type Options = { - onSuccess: (data: Record) => void; - onError: (error: Error) => void; - initialData: Record; - skip: boolean; - params: Record; - static: boolean; -}; -type ReturnData = { - data: Record; - loading: boolean; - error: Error; - reload: (params?: Record) => Promise | undefined; - reloading: boolean; -}; - -function useLoader(loaderFn: LoaderFn, options: Options): ReturnData; -``` - -:::info -`runtimeContext` 类型可以参考 [useRuntimeContext](/apis/app/runtime/core/use-runtime-context)。 - -::: - -### 参数 - -- `loaderFn`:用于加载数据的函数,返回 Promise。 -- `options`:可选配置项。 - - `onSuccess`:执行成功的回调。 - - `onError`:执行失败的回调。 - - `initialData`:首次执行前的初始数据,对应返回值中的 `data` 字段。 - - `skip`:当值为 `true` 时,函数不执行。 - - `params`:当 `params` 序列化结果发生改变时,函数会重新执行。同时,`params` 也会作为函数的第二个参数被传入。 - - `static`:当值为 `true` 时,`useLoader` 用于 [SSG](/guides/basic-features/render/ssg) 编译阶段数据的获取。 - -### 返回值 - -- `data`:执行成功时的返回数据。 -- `loading`:表示函数是否处于执行过程中。 -- `error`:函数执行失败时的错误信息。 -- `reload`:调用后可以重新执行函数。 - - `params`:当值为 `undefined` 时,函数执行时将复用上次的值;否则会使用新的值重新执行函数。 -- `reloading`:调用 `reload` 的执行过程中,`reloading` 值为 `true`。 - -## 示例 - -```ts -function Container() { - const { data, error, loading } = useLoader( - async (context, params) => { - console.log(params) // nicole - return fetch(user); - }, - { - onSuccess: data => { - console.log('I did success:(', data); - }, - onError: error => { - console.log('I met error:)', error); - }, - initialData: { name: 'nicole', gender: 'female' }, - params: 'nicole' - } - ); - - return ...; -} -``` diff --git a/packages/document/main-doc/docs/zh/guides/advanced-features/server-monitor/logger.mdx b/packages/document/main-doc/docs/zh/guides/advanced-features/server-monitor/logger.mdx index e80abaf8186b..7921611fffa5 100644 --- a/packages/document/main-doc/docs/zh/guides/advanced-features/server-monitor/logger.mdx +++ b/packages/document/main-doc/docs/zh/guides/advanced-features/server-monitor/logger.mdx @@ -17,11 +17,7 @@ Modern.js 也保留了旧版本中使用 `useLoader` 获取数据时的 SSR 日 | 阶段 | 消息 | Level | | ------------- | -------------------------------------------- | ----- | | PRERENDER | App Prerender | error | -| USE_LOADER | App run useLoader | error | -:::tip -`useLoader` API 目前已经废弃,建议迁移到约定式路由并使用 Data Loader 进行数据获取。已经使用 Data Loader 的应用,可以开启 [`ssr.disablePrerender`](/configure/app/server/ssr.html#object-类型),禁止预渲染,提升 SSR 性能。 -::: ## 内置 Monitor diff --git a/packages/runtime/plugin-runtime/src/cli/ssr/babel-plugin-ssr-loader-id.ts b/packages/runtime/plugin-runtime/src/cli/ssr/babel-plugin-ssr-loader-id.ts deleted file mode 100644 index de4d96ee8114..000000000000 --- a/packages/runtime/plugin-runtime/src/cli/ssr/babel-plugin-ssr-loader-id.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { Buffer } from 'buffer'; -import crypto from 'crypto'; -import * as t from '@babel/types'; -import { get } from '@modern-js/utils/lodash'; - -const RUNTIME_PACKAGE_NAMES = ['@modern-js/runtime']; -const FUNCTION_USE_LOADER_NAME = 'useLoader'; - -function getHash(filepath: string) { - const cwd = process.cwd(); - const point = filepath.indexOf(cwd); - let relativePath = filepath; - - if (point !== -1) { - relativePath = filepath.substring(point + cwd.length); - } - - const fileBuf = Buffer.from(relativePath); - const fsHash = crypto.createHash('md5'); - const md5 = fsHash - .update(fileBuf as unknown as ArrayBufferView & Uint8Array) - .digest('hex'); - return md5; -} - -function getUseLoaderPath(path: any, calleeName: string | null) { - const { node } = path; - - if (!calleeName || node.callee.name !== calleeName) { - return false; - } - - const arg1 = get(node, 'arguments.0'); - - if ( - t.isFunction(arg1) || - t.isFunctionExpression(arg1) || - t.isArrowFunctionExpression(arg1) || - t.isIdentifier(arg1) || - t.isCallExpression(arg1) || - t.isMemberExpression(arg1) - ) { - const loaderPath = path.get('arguments.0'); - if (isDuplicateInnerLoader(loaderPath)) { - return false; - } else { - return loaderPath; - } - } - - console.warn('useLoader 中 loaderId 生成失败,请检查 useLoader'); - throw path.buildCodeFrameError(` - please check the usage of ${path.node.name} - `); -} - -// fix: react-refresh 和 export default App 格式的组件写法一起使用时, useLoader 调用会被调用两次,导致生成两个innerLoader -function isDuplicateInnerLoader(path: any) { - const { node } = path; - if (t.isFunctionExpression(node.callee)) { - if (t.isBlockStatement(node.callee.body)) { - if ( - get(node.callee.body, 'body.0.declarations.0.id.name') === - 'innerLoader' && - get(node.callee.body, 'body.2.argument.name') === 'innerLoader' - ) { - return true; - } - } - } - return false; -} - -function getSelfRunLoaderExpression( - loaderExpression: t.Expression, - id: string, -) { - return t.callExpression( - t.functionExpression( - null, - [], - t.blockStatement([ - t.variableDeclaration('var', [ - t.variableDeclarator(t.identifier('innerLoader'), loaderExpression), - ]), - t.expressionStatement( - t.assignmentExpression( - '=', - t.memberExpression(t.identifier('innerLoader'), t.identifier('id')), - t.stringLiteral(id), - ), - ), - t.returnStatement(t.identifier('innerLoader')), - ]), - ), - [], - ); -} - -module.exports = function () { - let useLoader: string | null = null; - let hash = ''; - let index = 0; - - function genId() { - return `${hash}_${index++}`; - } - - return { - name: 'babel-plugin-ssr-loader-id', - pre() { - index = 0; - useLoader = null; - hash = ''; - }, - visitor: { - ImportDeclaration(path: any, state: any) { - if (useLoader) { - return false; - } - - if (!RUNTIME_PACKAGE_NAMES.includes(get(path, 'node.source.value'))) { - return false; - } - - hash = getHash(state.file.opts.filename); - - get(path, 'node.specifiers', []).forEach(({ imported, local }: any) => { - if (!imported) { - throw path.buildCodeFrameError( - `please \`import { useLoader } from ${RUNTIME_PACKAGE_NAMES[0]}\``, - ); - } - - if (!useLoader && imported.name === FUNCTION_USE_LOADER_NAME) { - useLoader = local.name; - } - }); - - return false; - }, - CallExpression(path: any) { - let loaderPath = getUseLoaderPath(path, useLoader); - if (loaderPath) { - if (!Array.isArray(loaderPath)) { - loaderPath = [loaderPath]; - } - - loaderPath.forEach((p: any) => { - p.replaceWith(getSelfRunLoaderExpression(p.node, genId())); - }); - - return false; - } - - return false; - }, - }, - }; -}; diff --git a/packages/runtime/plugin-runtime/src/cli/ssr/index.ts b/packages/runtime/plugin-runtime/src/cli/ssr/index.ts index bd08eb4db5bc..07c1b82b9952 100644 --- a/packages/runtime/plugin-runtime/src/cli/ssr/index.ts +++ b/packages/runtime/plugin-runtime/src/cli/ssr/index.ts @@ -107,9 +107,6 @@ export const ssrPlugin = (): CliPlugin => ({ return (config: any) => { const userConfig = api.useResolvedConfigContext(); if (isUseSSRBundle(userConfig) && checkUseStringSSR(userConfig)) { - config.plugins?.push( - path.join(__dirname, './babel-plugin-ssr-loader-id'), - ); config.plugins?.push(require.resolve('@loadable/babel-plugin')); } }; diff --git a/packages/runtime/plugin-runtime/src/core/browser/index.tsx b/packages/runtime/plugin-runtime/src/core/browser/index.tsx index a618500e1e53..94f36c98c860 100644 --- a/packages/runtime/plugin-runtime/src/core/browser/index.tsx +++ b/packages/runtime/plugin-runtime/src/core/browser/index.tsx @@ -2,7 +2,6 @@ import cookieTool from 'cookie'; import type React from 'react'; import { getGlobalAppInit, getGlobalInternalRuntimeContext } from '../context'; import { type RuntimeContext, getInitialContext } from '../context/runtime'; -import { createLoaderManager } from '../loader/loaderManager'; import { wrapRuntimeContextProvider } from '../react/wrapper'; import type { SSRContainer } from '../types'; import { hydrateRoot } from './hydrate'; @@ -95,32 +94,13 @@ export async function render( if (isClientArgs(id)) { // TODO: This field may suitable to be called `requestData`, because both SSR and CSR can get the context const ssrData = getSSRData(); - const loadersData = ssrData.data?.loadersData || {}; - - const initialLoadersState = Object.keys(loadersData).reduce( - (res: any, key) => { - const loaderData = loadersData[key]; - - if (loaderData?.loading !== false) { - return res; - } - - res[key] = loaderData; - return res; - }, - {}, - ); Object.assign(context, { - loaderManager: createLoaderManager(initialLoadersState, { - skipStatic: true, - }), // garfish plugin params _internalRouterBaseName: App.props.basename, ssrContext: ssrData.context, }); - context.initialData = ssrData.data?.initialData; const initialData = await runBeforeRender(context); if (initialData) { context.initialData = initialData; diff --git a/packages/runtime/plugin-runtime/src/core/context/runtime.ts b/packages/runtime/plugin-runtime/src/core/context/runtime.ts index c6aaeb9f2ac3..a009b0662ca5 100644 --- a/packages/runtime/plugin-runtime/src/core/context/runtime.ts +++ b/packages/runtime/plugin-runtime/src/core/context/runtime.ts @@ -4,12 +4,10 @@ import { ROUTE_MANIFEST } from '@modern-js/utils/universal/constants'; import { createContext, useContext, useMemo } from 'react'; import { getGlobalInternalRuntimeContext } from '.'; import type { RouteManifest } from '../../router/runtime/types'; -import { createLoaderManager } from '../loader/loaderManager'; import type { SSRServerContext, TSSRContext } from '../types'; interface BaseRuntimeContext { initialData?: Record; - loaderManager: ReturnType; isBrowser: boolean; // ssr type ssrContext?: SSRServerContext; @@ -48,7 +46,6 @@ export const getInitialContext = ( isBrowser = true, routeManifest?: RouteManifest, ): RuntimeContext => ({ - loaderManager: createLoaderManager({}), isBrowser, routeManifest: routeManifest || diff --git a/packages/runtime/plugin-runtime/src/core/index.ts b/packages/runtime/plugin-runtime/src/core/index.ts index c776b55c2576..b39d4b681a80 100644 --- a/packages/runtime/plugin-runtime/src/core/index.ts +++ b/packages/runtime/plugin-runtime/src/core/index.ts @@ -7,6 +7,5 @@ export { ServerRouterContext, useRuntimeContext, } from './context/runtime'; -export * from './loader'; export type { SSRData, SSRContainer } from './types'; diff --git a/packages/runtime/plugin-runtime/src/core/loader/index.ts b/packages/runtime/plugin-runtime/src/core/loader/index.ts deleted file mode 100644 index b0fd7280404f..000000000000 --- a/packages/runtime/plugin-runtime/src/core/loader/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as useLoader } from './useLoader'; diff --git a/packages/runtime/plugin-runtime/src/core/loader/loaderManager.ts b/packages/runtime/plugin-runtime/src/core/loader/loaderManager.ts deleted file mode 100644 index cc8e32198ba0..000000000000 --- a/packages/runtime/plugin-runtime/src/core/loader/loaderManager.ts +++ /dev/null @@ -1,231 +0,0 @@ -import invariant from 'invariant'; -import type { LoaderOptions } from './useLoader'; - -/** - * Calc id from string or object - */ -const createGetId = () => { - const idCache = new Map(); - - return (objectId: NonNullable) => { - const cachedId = idCache.get(objectId); - - if (cachedId) { - return cachedId; - } - - // WARNING: id should be unique after serialize. - const id = JSON.stringify(objectId); - - invariant(id, 'params should be not null value'); - - idCache.set(objectId, id); - - return id; - }; -}; - -export enum LoaderStatus { - idle = 0, - loading = 1, - fulfilled = 2, - rejected = 3, -} - -export type LoaderResult = { - loading: boolean; - reloading: boolean; - data: any; - error: any; - _error?: any; -}; - -const createLoader = ( - id: string, - initialData: Partial = { - loading: false, - reloading: false, - data: undefined, - error: undefined, - }, - loaderFn: () => Promise = () => Promise.resolve(), - skip = false, -) => { - let promise: Promise | null; - let status: LoaderStatus = LoaderStatus.idle; - let { data, error } = initialData; - let hasLoaded = false; - - const handlers = new Set< - (status: LoaderStatus, result: LoaderResult) => void - >(); - - const load = async () => { - if (skip) { - return promise; - } - - if (status === LoaderStatus.loading) { - return promise; - } - - status = LoaderStatus.loading; - notify(); - - promise = loaderFn() - .then(value => { - data = value; - error = null; - status = LoaderStatus.fulfilled; - }) - .catch(e => { - error = e; - data = null; - status = LoaderStatus.rejected; - }) - .finally(() => { - promise = null; - hasLoaded = true; - notify(); - }); - - return promise; - }; - - const getResult = () => ({ - loading: !hasLoaded && status === LoaderStatus.loading, - reloading: hasLoaded && status === LoaderStatus.loading, - data, - error: error instanceof Error ? `${error.message}` : error, - // redundant fields for ssr log - _error: error, - }); - - const notify = () => { - // don't iterate handlers directly, since it could be modified during iteration - [...handlers].forEach(handler => { - handler(status, getResult()); - }); - }; - - const onChange = ( - handler: (status: LoaderStatus, result: LoaderResult) => void, - ) => { - handlers.add(handler); - - return () => { - handlers.delete(handler); - }; - }; - - return { - get result() { - return getResult(); - }, - get promise() { - return promise; - }, - onChange, - load, - }; -}; - -type ManagerOption = { - /** - * whether current manage only exec static loader - */ - skipStatic?: boolean; - skipNonStatic?: boolean; -}; - -/** - * Create loaders manager. It's returned instance will add to context - * @param initialDataMap used to initialing loader data - */ -export const createLoaderManager = ( - initialDataMap: Record, - managerOptions: ManagerOption = {}, -) => { - const { skipStatic = false, skipNonStatic = false } = managerOptions; - const loadersMap = new Map(); - const getId = createGetId(); - - const add = (loaderFn: () => Promise, loaderOptions: LoaderOptions) => { - const id = getId(loaderOptions.params); - let loader = loadersMap.get(id); - - // private property for opting out loader cache, maybe change in future - const cache = (loaderOptions as any)._cache; - - if (!loader || cache === false) { - // ignore non-static loader on static phase - const ignoreNonStatic = skipNonStatic && !loaderOptions.static; - - // ignore static loader on non-static phase - const ignoreStatic = skipStatic && loaderOptions.static; - - const skipExec = ignoreNonStatic || ignoreStatic; - - loader = createLoader( - id, - typeof initialDataMap[id] !== 'undefined' - ? initialDataMap[id] - : { data: loaderOptions.initialData }, - loaderFn, - // Todo whether static loader is exec when CSR - skipExec, - ); - loadersMap.set(id, loader); - } - - return id; - }; - - const get = (id: string) => loadersMap.get(id); - - // check if there has pending loaders - const hasPendingLoaders = () => { - for (const loader of loadersMap.values()) { - const { promise } = loader; - - if (promise instanceof Promise) { - return true; - } - } - - return false; - }; - - // waiting for all pending loaders to be settled - const awaitPendingLoaders = async () => { - const pendingLoaders = []; - - for (const [id, loader] of loadersMap) { - const { promise } = loader; - - if (promise instanceof Promise) { - pendingLoaders.push([id, loader] as [string, Loader]); - } - } - - await Promise.all(pendingLoaders.map(item => item[1].promise)); - - return pendingLoaders.reduce>( - (res, [id, loader]) => { - res[id] = loader.result; - - return res; - }, - {}, - ); - }; - - return { - hasPendingLoaders, - awaitPendingLoaders, - add, - get, - }; -}; - -export type Loader = ReturnType; diff --git a/packages/runtime/plugin-runtime/src/core/loader/useLoader.ts b/packages/runtime/plugin-runtime/src/core/loader/useLoader.ts deleted file mode 100644 index e3563cb85783..000000000000 --- a/packages/runtime/plugin-runtime/src/core/loader/useLoader.ts +++ /dev/null @@ -1,166 +0,0 @@ -import invariant from 'invariant'; -import { - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; -import { RuntimeReactContext } from '../context/runtime'; -import { type Loader, LoaderStatus } from './loaderManager'; - -type LoaderFn

= (context: any, params: P) => Promise; - -export interface LoaderOptions< - Params = any, - TData = any, - TError extends Error = any, -> { - /** - * Revoke when loader excuted successfully. - */ - onSuccess?: (data: TData) => void; - - /** - * Revoke when loader ended with error - */ - onError?: (error: TError) => void; - - /** - * initialData to display once loader is ready. - */ - initialData?: TData; - - /** - * whether skip loader - * if true, the loader will not exec. - */ - skip?: boolean; - - /** - * User params, it will pass to loader's second parameter. - */ - params?: Params; - - /** - * whether loader can exec on build phase. - */ - static?: boolean; -} - -const useLoader = ( - loaderFn: LoaderFn, - options: LoaderOptions = { params: undefined } as any, -) => { - const context = useContext(RuntimeReactContext); - const isSSRRender = Boolean(context.ssr); - - const { loaderManager } = context; - const loaderRef = useRef(null); - const unlistenLoaderChangeRef = useRef<(() => void) | null>(null); - - // SSR render should ignore `_cache` prop - if (isSSRRender && Object.prototype.hasOwnProperty.call(options, '_cache')) { - delete (options as any)._cache; - } - - const load = useCallback( - (params?: Params) => { - if (typeof params === 'undefined') { - return loaderRef.current?.load(); - } - - const id = loaderManager.add( - () => { - try { - const res = loaderFn(context, params); - - if (res instanceof Promise) { - return res; - } - - return Promise.resolve(res); - } catch (e) { - return Promise.reject(e); - } - }, - { - ...options, - params, - }, - ); - - loaderRef.current = loaderManager.get(id)!; - // unsubscribe old loader onChange event - unlistenLoaderChangeRef.current?.(); - - if (isSSRRender) { - return undefined; - } - - // skip this loader, then try to unlisten loader change - if (options.skip) { - return undefined; - } - - // do not load data again in CSR hydrate stage if SSR data exists - if ( - context._hydration && - window?._SSR_DATA?.data?.loadersData?.[id]?.error === null - ) { - return undefined; - } - - const res = loaderRef.current.load(); - - unlistenLoaderChangeRef.current = loaderRef.current?.onChange( - (_status, _result) => { - setResult(_result); - - if (_status === LoaderStatus.fulfilled) { - options?.onSuccess?.(_result.data); - } - - if (_status === LoaderStatus.rejected) { - options?.onError?.(_result.error); - } - }, - ); - - return res; - }, - [options.skip], - ); - - useEffect( - () => () => { - unlistenLoaderChangeRef.current?.(); - }, - [], - ); - - useMemo(() => { - const p = options.params ?? (loaderFn as any).id; - - invariant( - typeof p !== 'undefined' && p !== null, - 'Params is required in useLoader', - ); - load(p); - }, [options.params]); - - const [result, setResult] = useState<{ - loading: boolean; - reloading: boolean; - data: TData; - error: E; - }>(loaderRef.current!.result); - - return { - ...result, - reload: load, - }; -}; - -export default useLoader; diff --git a/packages/runtime/plugin-runtime/src/core/server/requestHandler.tsx b/packages/runtime/plugin-runtime/src/core/server/requestHandler.tsx index e7992b2dfea4..ceaf5a859ffd 100644 --- a/packages/runtime/plugin-runtime/src/core/server/requestHandler.tsx +++ b/packages/runtime/plugin-runtime/src/core/server/requestHandler.tsx @@ -20,8 +20,7 @@ import { getGlobalRSCRoot, } from '../context'; import { getInitialContext } from '../context/runtime'; -import { getServerPayload } from '../context/serverPayload/index'; -import { createLoaderManager } from '../loader/loaderManager'; +import { getServerPayload } from '../context/serverPayload'; import { createRoot } from '../react'; import type { SSRServerContext } from '../types'; import { CHUNK_CSS_PLACEHOLDER } from './constants'; @@ -241,16 +240,6 @@ export const createRequestHandler: CreateRequestHandler = async ( Object.assign(context, { ssrContext, isBrowser: false, - loaderManager: createLoaderManager( - {}, - { - skipNonStatic: options.staticGenerate, - // if not static generate, only non-static loader can exec on prod env - skipStatic: - process.env.NODE_ENV === 'production' && - !options.staticGenerate, - }, - ), }); // Handle redirects from React Router with an HTTP redirect diff --git a/packages/runtime/plugin-runtime/src/core/server/string/index.ts b/packages/runtime/plugin-runtime/src/core/server/string/index.ts index 4f6a44b7ab06..44c3b3bf7520 100644 --- a/packages/runtime/plugin-runtime/src/core/server/string/index.ts +++ b/packages/runtime/plugin-runtime/src/core/server/string/index.ts @@ -16,7 +16,6 @@ import { type BuildHtmlCb, type RenderString, buildHtml } from '../shared'; import { SSRErrors, SSRTimings, type Tracer } from '../tracer'; import { getSSRConfigByEntry, safeReplace } from '../utils'; import { LoadableCollector } from './loadable'; -import { prefetch } from './prefetch'; import { SSRDataCollector } from './ssrData'; import { StyledCollector } from './styledComponent'; import type { ChunkSet, Collector } from './types'; @@ -47,22 +46,6 @@ export const renderString: RenderString = async ( cssChunk: '', }; - let prefetchData = {}; - - try { - prefetchData = await prefetch( - serverRoot, - request, - options, - ssrConfig, - tracer, - ); - chunkSet.renderLevel = RenderLevel.SERVER_PREFETCH; - } catch (e) { - chunkSet.renderLevel = RenderLevel.CLIENT_RENDER; - tracer.onError(e, SSRErrors.PRERENDER); - } - const collectors = [ new StyledCollector(chunkSet), new LoadableCollector({ @@ -76,7 +59,6 @@ export const renderString: RenderString = async ( }), new SSRDataCollector({ request, - prefetchData, ssrConfig, ssrContext: runtimeContext.ssrContext!, chunkSet, diff --git a/packages/runtime/plugin-runtime/src/core/server/string/prefetch.tsx b/packages/runtime/plugin-runtime/src/core/server/string/prefetch.tsx deleted file mode 100644 index 795d15631a1e..000000000000 --- a/packages/runtime/plugin-runtime/src/core/server/string/prefetch.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { ChunkExtractor } from '@loadable/server'; -import { time } from '@modern-js/runtime-utils/time'; -import { parseHeaders } from '@modern-js/runtime-utils/universal/request'; -import type React from 'react'; -import { renderToStaticMarkup } from 'react-dom/server'; -import type { LoaderResult } from '../../loader/loaderManager'; -import { wrapRuntimeContextProvider } from '../../react/wrapper'; -import type { HandleRequestOptions } from '../requestHandler'; -import type { SSRConfig } from '../shared'; -import { SSRErrors, SSRTimings, type Tracer } from '../tracer'; - -export const prefetch = async ( - App: React.ReactElement, - request: Request, - options: HandleRequestOptions, - ssrConfig: SSRConfig, - { onError, onTiming }: Tracer, -) => { - const { runtimeContext: context, resource } = options; - - const { entryName, loadableStats } = resource; - - if (typeof ssrConfig === 'boolean' || !ssrConfig.disablePrerender) { - try { - const end = time(); - // disable renderToStaticMarkup when user configures disablePrerender - if (loadableStats) { - const extractor = new ChunkExtractor({ - stats: loadableStats, - entrypoints: [entryName].filter(Boolean), - }); - renderToStaticMarkup( - extractor.collectChunks( - wrapRuntimeContextProvider( - App, - Object.assign(context, { ssr: false }), - ), - ), - ); - } else { - renderToStaticMarkup( - wrapRuntimeContextProvider( - App, - Object.assign(context, { ssr: false }), - ), - ); - } - - const cost = end(); - - onTiming(SSRTimings.PRERENDER, cost); - - // tracker.trackTiming(SSRTimings.PRERENDER, cost); - } catch (e) { - const error = e as Error; - onError(error, SSRErrors.PRERENDER); - - // re-throw the error - throw e; - } - } - - if (!context.loaderManager.hasPendingLoaders()) { - return { - initialData: context.initialData, - i18nData: context.__i18nData__, - }; - } - - let loadersData: Record = {}; - try { - const end = time(); - - loadersData = await context.loaderManager.awaitPendingLoaders(); - - const cost = end(); - - onTiming(SSRTimings.USE_LOADER, cost); - } catch (e) { - onError(e, SSRErrors.USE_LOADER); - - // re-throw the error - throw e; - } - - Object.keys(loadersData).forEach(id => { - const data = loadersData[id]; - if (data._error) { - onError(data._error, SSRErrors.USE_LOADER); - delete data._error; - } - }); - - return { - loadersData, - initialData: context.initialData, - i18nData: context.__i18nData__, - // todo: move to plugin state - storeState: context?.store?.getState(), - }; -}; diff --git a/packages/runtime/plugin-runtime/src/core/server/string/ssrData.ts b/packages/runtime/plugin-runtime/src/core/server/string/ssrData.ts index e0fd63369e30..41e2e16b3018 100644 --- a/packages/runtime/plugin-runtime/src/core/server/string/ssrData.ts +++ b/packages/runtime/plugin-runtime/src/core/server/string/ssrData.ts @@ -10,7 +10,6 @@ import type { ChunkSet, Collector } from './types'; export interface SSRDataCreatorOptions { request: Request; - prefetchData: Record; chunkSet: ChunkSet; ssrContext: SSRServerContext; ssrConfig?: SSRConfig; @@ -43,7 +42,7 @@ export class SSRDataCollector implements Collector { } #getSSRData(): SSRContainer { - const { prefetchData, chunkSet, ssrConfig, ssrContext } = this.#options; + const { chunkSet, ssrConfig, ssrContext } = this.#options; const { reporter, request } = ssrContext; @@ -61,7 +60,6 @@ export class SSRDataCollector implements Collector { : undefined; return { - data: prefetchData, context: { request: { params: request.params, diff --git a/packages/runtime/plugin-runtime/src/core/server/tracer.ts b/packages/runtime/plugin-runtime/src/core/server/tracer.ts index d799934c3488..495c3143ad78 100644 --- a/packages/runtime/plugin-runtime/src/core/server/tracer.ts +++ b/packages/runtime/plugin-runtime/src/core/server/tracer.ts @@ -4,12 +4,10 @@ export enum SSRTimings { PRERENDER = 'ssr-prerender', RENDER_HTML = 'ssr-render-html', RENDER_SHELL = 'ssr-render-shell', - USE_LOADER = 'use-loader', } export enum SSRErrors { PRERENDER = 'App Prerender', - USE_LOADER = 'App run useLoader', RENDER_HTML = 'App Render To HTML', RENDER_STREAM = 'An error occurs during streaming SSR', RENDER_SHELL = 'An error occurs during streaming render shell', diff --git a/packages/runtime/plugin-runtime/src/core/types.ts b/packages/runtime/plugin-runtime/src/core/types.ts index f59ee074eeef..4cfddc762387 100644 --- a/packages/runtime/plugin-runtime/src/core/types.ts +++ b/packages/runtime/plugin-runtime/src/core/types.ts @@ -1,7 +1,6 @@ import type { OnError, OnTiming } from '@modern-js/app-tools'; import type { BaseSSRServerContext } from '@modern-js/types'; import type { RenderLevel } from './constants'; -import type { LoaderResult } from './loader/loaderManager'; declare global { interface Window { @@ -11,11 +10,10 @@ declare global { } export interface SSRData { - loadersData?: Record; initialData?: Record; - storeState?: any; [props: string]: any; } + export interface RouteData { [routeId: string]: any; } diff --git a/packages/runtime/plugin-runtime/src/index.ts b/packages/runtime/plugin-runtime/src/index.ts index bbe1c742778c..27115d115219 100644 --- a/packages/runtime/plugin-runtime/src/index.ts +++ b/packages/runtime/plugin-runtime/src/index.ts @@ -12,7 +12,6 @@ export { getRequest } from './core/context/request'; export { setHeaders, setStatus, redirect } from './core/context/response'; export { - useLoader, RuntimeReactContext, defineRuntimeConfig, useRuntimeContext, diff --git a/tests/integration/bff-corss-project/bff-api-app/package.json b/tests/integration/bff-corss-project/bff-api-app/package.json index 32f89be53edf..ffe28f8678bb 100644 --- a/tests/integration/bff-corss-project/bff-api-app/package.json +++ b/tests/integration/bff-corss-project/bff-api-app/package.json @@ -93,4 +93,4 @@ "dist-1/runtime/**/*", "dist-1/plugin/**/*" ] -} +} \ No newline at end of file diff --git a/tests/integration/bff-corss-project/bff-client-app/modern.config.ts b/tests/integration/bff-corss-project/bff-client-app/modern.config.ts index 5241f48020e4..7e4594608753 100644 --- a/tests/integration/bff-corss-project/bff-client-app/modern.config.ts +++ b/tests/integration/bff-corss-project/bff-client-app/modern.config.ts @@ -8,7 +8,7 @@ export default applyBaseConfig({ prefix: '/web-app', }, server: { - ssr: true, + ssr: false, port: 3401, }, plugins: [bffPlugin(), expressPlugin(), crossProjectApiPlugin()], diff --git a/tests/integration/bff-corss-project/bff-client-app/src/base/App.tsx b/tests/integration/bff-corss-project/bff-client-app/src/base/App.tsx index 40347aa375a7..0040a87795fd 100644 --- a/tests/integration/bff-corss-project/bff-client-app/src/base/App.tsx +++ b/tests/integration/bff-corss-project/bff-client-app/src/base/App.tsx @@ -7,7 +7,9 @@ import { useEffect, useState } from 'react'; configure({ interceptor(request) { return async (url, params) => { - const res = await request(url, params); + const urlString = typeof url === 'string' ? url : url.toString(); + const path = new URL(urlString, window.location.href); + const res = await request(path, params); return res.json(); }; }, diff --git a/tests/integration/bff-corss-project/bff-client-app/src/custom-sdk/App.tsx b/tests/integration/bff-corss-project/bff-client-app/src/custom-sdk/App.tsx index 0207021a8c61..47e3a263359d 100644 --- a/tests/integration/bff-corss-project/bff-client-app/src/custom-sdk/App.tsx +++ b/tests/integration/bff-corss-project/bff-client-app/src/custom-sdk/App.tsx @@ -1,14 +1,15 @@ -import { useLoader } from '@modern-js/runtime'; import hello from 'bff-api-app/api/index'; import { configure } from 'bff-api-app/runtime'; +import { useLoader } from '../useLoader'; configure({ interceptor(request) { return async (url, params) => { - let path = new URL(url); - path = path.toString().replace('3399', '3401'); + const urlString = typeof url === 'string' ? url : url.toString(); + const path = new URL(urlString, window.location.href); + const pathString = path.toString().replace('3399', '3401'); - const res = await request(path, params); + const res = await request(pathString, params); const data = await res.json(); data.message = 'Hello Custom SDK'; return data; @@ -17,10 +18,15 @@ configure({ }); const App = () => { - const { data } = useLoader(async () => { + const { data, loading } = useLoader(async () => { const res = await hello(); return res; }); + + if (loading) { + return

Loading...
; + } + const { message = 'bff-express' } = data || {}; return
{message}
; }; diff --git a/tests/integration/bff-corss-project/bff-client-app/src/ssr/App.tsx b/tests/integration/bff-corss-project/bff-client-app/src/ssr/App.tsx index a1cc1066f30a..a21ff10a47d2 100644 --- a/tests/integration/bff-corss-project/bff-client-app/src/ssr/App.tsx +++ b/tests/integration/bff-corss-project/bff-client-app/src/ssr/App.tsx @@ -1,6 +1,6 @@ -import { useLoader } from '@modern-js/runtime'; import hello from 'bff-api-app/api/index'; import { configure } from 'bff-api-app/runtime'; +import { useLoader } from '../useLoader'; configure({ setDomain() { diff --git a/tests/integration/bff-corss-project/bff-client-app/src/upload/App.tsx b/tests/integration/bff-corss-project/bff-client-app/src/upload/App.tsx index d045408aa77a..f78759589ad8 100644 --- a/tests/integration/bff-corss-project/bff-client-app/src/upload/App.tsx +++ b/tests/integration/bff-corss-project/bff-client-app/src/upload/App.tsx @@ -12,7 +12,7 @@ const getMockImage = () => { return new File([blob], 'mock_image.png', { type: 'image/png' }); }; -const Index = (): JSX.Element => { +const Index = (): React.ReactElement => { const [file, setFile] = React.useState(); const [fileName, setFileName] = React.useState(''); diff --git a/tests/integration/bff-corss-project/bff-client-app/src/useLoader.ts b/tests/integration/bff-corss-project/bff-client-app/src/useLoader.ts new file mode 100644 index 000000000000..9d28cc06193d --- /dev/null +++ b/tests/integration/bff-corss-project/bff-client-app/src/useLoader.ts @@ -0,0 +1,25 @@ +import { useEffect, useState } from 'react'; + +export function useLoader(loader: () => Promise): { + data: T | null; + loading: boolean; +} { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const loadData = async () => { + setLoading(true); + try { + const result = await loader(); + setData(result); + } finally { + setLoading(false); + } + }; + + loadData(); + }, []); + + return { data, loading }; +} diff --git a/tests/integration/bff-corss-project/bff-indep-client-app/modern.config.ts b/tests/integration/bff-corss-project/bff-indep-client-app/modern.config.ts index 8820b1be0ddf..2f2b3758dcdc 100644 --- a/tests/integration/bff-corss-project/bff-indep-client-app/modern.config.ts +++ b/tests/integration/bff-corss-project/bff-indep-client-app/modern.config.ts @@ -7,7 +7,7 @@ export default applyBaseConfig({ prefix: '/indep-web-app', }, server: { - ssr: true, + ssr: false, }, plugins: [bffPlugin(), expressPlugin()], }); diff --git a/tests/integration/bff-corss-project/bff-indep-client-app/src/custom-sdk/App.tsx b/tests/integration/bff-corss-project/bff-indep-client-app/src/custom-sdk/App.tsx index 9ca4c2a73488..d5c17f06d9f8 100644 --- a/tests/integration/bff-corss-project/bff-indep-client-app/src/custom-sdk/App.tsx +++ b/tests/integration/bff-corss-project/bff-indep-client-app/src/custom-sdk/App.tsx @@ -1,11 +1,15 @@ -import { useLoader } from '@modern-js/runtime'; import hello from 'bff-api-app/api/index'; import { configure } from 'bff-api-app/runtime'; +import { useLoader } from '../useLoader'; configure({ interceptor(request) { return async (url, params) => { - const res = await request(url, params); + const urlString = typeof url === 'string' ? url : url.toString(); + const path = new URL(urlString, window.location.href); + const pathString = path.toString().replace(/\:[0-9]+/, ':3399'); + + const res = await request(pathString, params); const data = await res.json(); data.message = 'Hello Custom SDK'; return data; @@ -14,10 +18,14 @@ configure({ }); const App = () => { - const { data } = useLoader(async () => { + const { data, loading } = useLoader(async () => { const res = await hello(); return res; }); + + if (loading) { + return
Loading...
; + } const { message = 'bff-express' } = data || {}; return
interceptor return:{message}
; }; diff --git a/tests/integration/bff-corss-project/bff-indep-client-app/src/ssr/App.tsx b/tests/integration/bff-corss-project/bff-indep-client-app/src/ssr/App.tsx index 33b54a12e586..3f98766bf91f 100644 --- a/tests/integration/bff-corss-project/bff-indep-client-app/src/ssr/App.tsx +++ b/tests/integration/bff-corss-project/bff-indep-client-app/src/ssr/App.tsx @@ -1,8 +1,8 @@ -import { useLoader } from '@modern-js/runtime'; import hello from 'bff-api-app/api/index'; import user from 'bff-api-app/api/user/index'; import { configure } from 'bff-api-app/runtime'; import { useEffect, useState } from 'react'; +import { useLoader } from '../useLoader'; configure({ setDomain() { diff --git a/tests/integration/bff-corss-project/bff-indep-client-app/src/useLoader.ts b/tests/integration/bff-corss-project/bff-indep-client-app/src/useLoader.ts new file mode 100644 index 000000000000..9d28cc06193d --- /dev/null +++ b/tests/integration/bff-corss-project/bff-indep-client-app/src/useLoader.ts @@ -0,0 +1,25 @@ +import { useEffect, useState } from 'react'; + +export function useLoader(loader: () => Promise): { + data: T | null; + loading: boolean; +} { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const loadData = async () => { + setLoading(true); + try { + const result = await loader(); + setData(result); + } finally { + setLoading(false); + } + }; + + loadData(); + }, []); + + return { data, loading }; +} diff --git a/tests/integration/bff-corss-project/tests/index.test.ts b/tests/integration/bff-corss-project/tests/index.test.ts index 11ff22028905..3607a0108d00 100644 --- a/tests/integration/bff-corss-project/tests/index.test.ts +++ b/tests/integration/bff-corss-project/tests/index.test.ts @@ -75,8 +75,13 @@ describe('corss project bff', () => { expect(text).toBe(expectedText); }); - test('basic usage with ssr', async () => { + test('basic usage with csr', async () => { await page.goto(`${host}:${port}/${SSR_PAGE}`); + await page.waitForFunction(() => { + const loadingEl = document.querySelector('.loading'); + const helloEl = document.querySelector('.hello'); + return !loadingEl && helloEl; + }); await new Promise(resolve => setTimeout(resolve, 3000)); const text1 = await page.$eval('.hello', el => el?.textContent); expect(text1).toBe(expectedText); @@ -119,7 +124,7 @@ describe('corss project bff', () => { const BASE_PAGE = 'base'; const CUSTOM_PAGE = 'custom-sdk'; const UPLOAD_PAGE = 'upload'; - const host = `http://localhost`; + const host = `http://127.0.0.1`; const prefix = '/api-app'; let app: any; let apiApp: any; @@ -135,6 +140,11 @@ describe('corss project bff', () => { browser = await puppeteer.launch(launchOptions as any); page = await browser.newPage(); + + page.on('console', msg => { + // 打印所有类型的日志 + console.log('[browser]', msg.type(), msg.text()); + }); }); test('api-app should works', async () => { @@ -154,8 +164,13 @@ describe('corss project bff', () => { expect(text).toBe(expectedText); }); - test('basic usage with ssr', async () => { + test('basic usage with csr', async () => { await page.goto(`${host}:${port}/${SSR_PAGE}`); + await page.waitForFunction(() => { + const loadingEl = document.querySelector('.loading'); + const helloEl = document.querySelector('.hello'); + return !loadingEl && helloEl; + }); const text1 = await page.$eval('.hello', el => el?.textContent); expect(text1).toBe(expectedText); }); @@ -211,6 +226,11 @@ describe('corss project bff', () => { indepClientApp = await launchApp(indepAppDir, port, {}); browser = await puppeteer.launch(launchOptions as any); page = await browser.newPage(); + + page.on('console', msg => { + // 打印所有类型的日志 + console.log('[browser]', msg.type(), msg.text()); + }); }); test('basic usage', async () => { @@ -222,7 +242,7 @@ describe('corss project bff', () => { expect(text).toBe('hello:Hello get bff-api-app'); }); - test('basic usage with ssr', async () => { + test('basic usage with csr', async () => { await page.goto(`${host}:${port}/${SSR_PAGE}`); await new Promise(resolve => setTimeout(resolve, 2000)); const text1 = await page.$eval('.hello', el => el?.textContent); @@ -231,7 +251,7 @@ describe('corss project bff', () => { test('support custom sdk', async () => { await page.goto(`${host}:${port}/${CUSTOM_PAGE}`); - await new Promise(resolve => setTimeout(resolve, 1000)); + await new Promise(resolve => setTimeout(resolve, 3000)); const text = await page.$eval('.hello', el => el?.textContent); expect(text).toBe('interceptor return:Hello Custom SDK'); }); @@ -285,7 +305,7 @@ describe('corss project bff', () => { expect(text).toBe('hello:Hello get bff-api-app'); }); - test('basic usage with ssr', async () => { + test('basic usage with csr', async () => { await page.goto(`${host}:${port}/${SSR_PAGE}`); const text1 = await page.$eval('.hello', el => el?.textContent); expect(text1).toBe('node-fetch:Hello get bff-api-app'); diff --git a/tests/integration/runtime/fixtures/use-loader/modern.config.ts b/tests/integration/runtime/fixtures/use-loader/modern.config.ts deleted file mode 100644 index cb644f6aba50..000000000000 --- a/tests/integration/runtime/fixtures/use-loader/modern.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { applyBaseConfig } from '../../../../utils/applyBaseConfig'; - -export default applyBaseConfig({ - server: { - ssr: true, - }, -}); diff --git a/tests/integration/runtime/fixtures/use-loader/package.json b/tests/integration/runtime/fixtures/use-loader/package.json deleted file mode 100644 index a4a583c7ff9d..000000000000 --- a/tests/integration/runtime/fixtures/use-loader/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "private": true, - "name": "use-loader", - "version": "2.66.0", - "dependencies": { - "@modern-js/app-tools": "workspace:*", - "@modern-js/runtime": "workspace:*", - "react": "^19.1.1", - "react-dom": "^19.1.1" - } -} diff --git a/tests/integration/runtime/fixtures/use-loader/src/App.jsx b/tests/integration/runtime/fixtures/use-loader/src/App.jsx deleted file mode 100644 index 95153fe3af83..000000000000 --- a/tests/integration/runtime/fixtures/use-loader/src/App.jsx +++ /dev/null @@ -1,60 +0,0 @@ -import { useLoader } from '@modern-js/runtime'; -import { useState } from 'react'; - -function App() { - const [count, setCount] = useState(0); - - const { data, loading, reload } = useLoader( - async (context, params) => { - console.log(`useLoader exec with params: ${params}`); - return new Promise(resolve => { - setTimeout(() => { - resolve(params); - }, 10); - }); - }, - { - onSuccess: _data => { - console.log(`useLoader success ${_data}`); - }, - onError: _error => { - console.log(`useLoader error ${_error}`); - }, - initialData: 'nicole', - params: count, - }, - ); - - if (loading) { - return 'Loading...'; - } - - const handleAdd = () => { - setCount(pre => pre + 1); - }; - - const handleReload = () => { - reload(); - }; - - const handleLoadUpdate = () => { - reload(100); - }; - - return ( -
-
{data}
- - - -
- ); -} - -export default App; diff --git a/tests/integration/runtime/package.json b/tests/integration/runtime/package.json deleted file mode 100644 index 114613259edc..000000000000 --- a/tests/integration/runtime/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "private": true, - "name": "runtime", - "version": "2.66.0" -} diff --git a/tests/integration/runtime/test/index.test.js b/tests/integration/runtime/test/index.test.js deleted file mode 100644 index e88483634f75..000000000000 --- a/tests/integration/runtime/test/index.test.js +++ /dev/null @@ -1,96 +0,0 @@ -const { join } = require('path'); -const path = require('path'); -const puppeteer = require('puppeteer'); -const { - launchApp, - getPort, - killApp, - sleep, - launchOptions, -} = require('../../../utils/modernTestUtils'); - -const fixtureDir = path.resolve(__dirname, '../fixtures'); - -describe('useLoader with SSR', () => { - let $data; - let logs = []; - let errors = []; - let app; - /** @type {puppeteer.Page} */ - let page; - /** @type {puppeteer.Browser} */ - let browser; - - beforeAll(async () => { - const appDir = join(fixtureDir, 'use-loader'); - const appPort = await getPort(); - app = await launchApp(appDir, appPort); - - browser = await puppeteer.launch(launchOptions); - page = await browser.newPage(); - - page.on('console', msg => logs.push(msg.text)); - page.on('pageerror', error => errors.push(error.message)); - await page.goto(`http://localhost:${appPort}`, { - waitUntil: ['networkidle0'], - }); - $data = await page.$('#data'); - }); - - afterAll(async () => { - if (browser) { - browser.close(); - } - if (app) { - await killApp(app); - } - }); - - it(`use ssr data without load request`, async () => { - logs = []; - errors = []; - const targetText = await page.evaluate(el => el?.textContent, $data); - expect(targetText).toEqual('0'); - expect(logs.join('\n')).not.toMatch('useLoader exec with params: '); - expect(errors.length).toEqual(0); - }); - - it(`update data when loadId changes`, async () => { - logs = []; - errors = []; - await page.click('#add'); - await sleep(2000); - const targetText = await page.evaluate(el => el?.textContent, $data); - expect(targetText).toEqual('1'); - - const logMsg = logs.join('\n'); - expect(logMsg).not.toMatch('useLoader exec with params: 1'); - expect(logMsg).not.toMatch('useLoader success: 1'); - }); - - it(`useLoader reload without params`, async () => { - logs = []; - errors = []; - await page.click('#reload'); - await sleep(2000); - const targetText = await page.evaluate(el => el?.textContent, $data); - expect(targetText).toEqual('1'); - - const logMsg = logs.join('\n'); - expect(logMsg).not.toMatch('useLoader exec with params: 1'); - expect(logMsg).not.toMatch('useLoader success: 1'); - }); - - it(`useLoader reload with params`, async () => { - logs = []; - errors = []; - await page.click('#update'); - await sleep(2000); - const targetText = await page.evaluate(el => el?.textContent, $data); - expect(targetText).toEqual('100'); - - const logMsg = logs.join('\n'); - expect(logMsg).not.toMatch('useLoader exec with params: 100'); - expect(logMsg).not.toMatch('useLoader success: 100'); - }); -}); From 6f10ce2bebf37e769ce131cbc1dbd56999a95420 Mon Sep 17 00:00:00 2001 From: wangyiming Date: Tue, 5 Aug 2025 17:10:19 +0800 Subject: [PATCH 2/2] chore: remove PreRender --- .../en/apis/app/runtime/ssr/pre-render.mdx | 96 -------------- .../zh/apis/app/runtime/ssr/pre-render.mdx | 96 -------------- .../plugin-runtime/src/core/server/index.ts | 2 +- .../src/core/server/react/index.ts | 1 - .../src/core/server/react/prerender/index.ts | 116 ----------------- .../src/core/server/react/prerender/type.ts | 33 ----- .../src/core/server/react/prerender/util.ts | 121 ------------------ 7 files changed, 1 insertion(+), 464 deletions(-) delete mode 100644 packages/document/main-doc/docs/en/apis/app/runtime/ssr/pre-render.mdx delete mode 100644 packages/document/main-doc/docs/zh/apis/app/runtime/ssr/pre-render.mdx delete mode 100644 packages/runtime/plugin-runtime/src/core/server/react/prerender/index.ts delete mode 100644 packages/runtime/plugin-runtime/src/core/server/react/prerender/type.ts delete mode 100644 packages/runtime/plugin-runtime/src/core/server/react/prerender/util.ts diff --git a/packages/document/main-doc/docs/en/apis/app/runtime/ssr/pre-render.mdx b/packages/document/main-doc/docs/en/apis/app/runtime/ssr/pre-render.mdx deleted file mode 100644 index 6ca9b93cce20..000000000000 --- a/packages/document/main-doc/docs/en/apis/app/runtime/ssr/pre-render.mdx +++ /dev/null @@ -1,96 +0,0 @@ ---- -title: PreRender ---- -# PreRender - -A Helmet-like HOC without content implements SPA routing-level caching, manner without additional configuration. - -## Usage - -```tsx -import { PreRender } from '@modern-js/runtime/ssr'; - -export default () => ( - <> - - -); -``` - -## Function Signature - -The `PreRender` provides a set of configuration for controlling caching rules, expiration times, caching algorithms, and more. - -```tsx -type Props { - interval: number; - staleLimit: number; - level: number; - include: { header?: string[], query?: string[] }; - matches: { header?: Record, query?: Record } -} - -function PreRender(props: Props): React.Component -``` - -### Input - -- `interval`: set the time the cache keep fresh, seconds. During this time, the cache will be used directly and not invoke asynchronous rendering. -- `staleLimit`: sets the time when the cache is completely expired, seconds.During this time, The cache can be returned and asynchronous rendering will be invoke, otherwise must wait for the re-rendered result. -- `level`: sets the calculation rule level for the cache identity, usually used with `includes` and `matches`. The default value is `0`. - -```bash -0: pathname -1: pathname + querystring -2: pathname + headers -3: pathname + querystring + headers -``` - -- `includes`: sets the content that needs to be included in the cache identifier, used when the `level` is not `0`. The default value is `null`. -- `matches`: sets the rewriting rule for the value of query or header in cache identity, usually used in cache category, supports regular expressions. The default value is `null`. - -## Example - -```tsx -import { PreRender } from '@modern-js/runtime/ssr'; - -export default function App() { - return ( - <> - -
Hello Modern
- - ); -} -``` - -The following example shows how to add the parameters in the query and header into the cache identifier calculation: - -```tsx -/* calculate cache identifier using channel in query and language in header */ - -``` - -The following example shows how not to let the test channel affect the online cache: - -```tsx -/* rewrite the channel value starting with test_ in the query as "testChannel", otherwise rewrite it as "otherChannel" */ - -``` diff --git a/packages/document/main-doc/docs/zh/apis/app/runtime/ssr/pre-render.mdx b/packages/document/main-doc/docs/zh/apis/app/runtime/ssr/pre-render.mdx deleted file mode 100644 index f2c1c64812a9..000000000000 --- a/packages/document/main-doc/docs/zh/apis/app/runtime/ssr/pre-render.mdx +++ /dev/null @@ -1,96 +0,0 @@ ---- -title: PreRender ---- -# PreRender - -无展示内容的高阶组件,通过类似 Helmet 的方式实现 SPA 路由级的缓存,无需额外配置。 - -## 使用姿势 - -```tsx -import { PreRender } from '@modern-js/runtime/ssr'; - -export default () => ( - <> - - -); -``` - -## 函数签名 - -`PreRender` 组件提供了一套常用的配置参数,用于控制缓存的规则、过期时间、缓存算法等。 - -```tsx -type Props { - interval: number; - staleLimit: number; - level: number; - include: { header?: string[], query?: string[] }; - matches: { header?: Record, query?: Record } -} - -function PreRender(props: Props): React.Component -``` - -### 参数 - -- `interval`:设置缓存保持新鲜的时间,单位秒。在该时间内,将直接使用缓存,并且不做异步渲染。 -- `staleLimit`:设置缓存完全过期的时间,单位秒。在该时间内,缓存可以被返回,并且会做一步渲染,否则必须使用重新渲染的结果。 -- `level`:设置缓存标识的计算规则等级,通常配合 `includes` 与 `matches` 使用。默认值为 `0`。 - -```bash -0:路由路径 -1:路由路径 + 查询字符串 -2:路由路径 + 请求头 -3:路由路径 + 查询字符串 + 请求头 -``` - -- `includes`:设置需要被纳入缓存标识的内容,在 level 非 0 时使用。默认值为 `null`。 -- `matches`:设置 query 或 header 的值在缓存标识计算中的重写规则,通常用在缓存分类时,支持正则表达式。默认值为 `null`。 - -## 示例 - -```tsx -import { PreRender } from '@modern-js/runtime/ssr'; - -export default function App() { - return ( - <> - -
Hello Modern
- - ); -} -``` - -下面例子展示了如何将 query、header 中指定的参数纳入缓存计算中: - -```tsx -/* 使用 query 中的 channel 和 header 中的 language 计算缓存标识 */ - -``` - -下面例子展示了如何不让测试频道影响线上缓存: - -```tsx -/* 将 query 中 channel 值为 test_ 开头的重写为 testChannel,否则重写为 otherChannel */ - -``` diff --git a/packages/runtime/plugin-runtime/src/core/server/index.ts b/packages/runtime/plugin-runtime/src/core/server/index.ts index bcaa65bbbf09..5271ed1646c0 100644 --- a/packages/runtime/plugin-runtime/src/core/server/index.ts +++ b/packages/runtime/plugin-runtime/src/core/server/index.ts @@ -1,7 +1,7 @@ import type { RuntimePlugin } from '../plugin'; // react component -export { PreRender, NoSSR, NoSSRCache } from './react'; +export { NoSSR, NoSSRCache } from './react'; export const ssr = (_config: any): RuntimePlugin => ({ name: '@modern-js/plugin-ssr', diff --git a/packages/runtime/plugin-runtime/src/core/server/react/index.ts b/packages/runtime/plugin-runtime/src/core/server/react/index.ts index 92e34d30095a..5970f4f0f058 100644 --- a/packages/runtime/plugin-runtime/src/core/server/react/index.ts +++ b/packages/runtime/plugin-runtime/src/core/server/react/index.ts @@ -1,3 +1,2 @@ -export { PreRender } from './prerender'; export { NoSSR } from './nossr'; export { NoSSRCache } from './no-ssr-cache'; diff --git a/packages/runtime/plugin-runtime/src/core/server/react/prerender/index.ts b/packages/runtime/plugin-runtime/src/core/server/react/prerender/index.ts deleted file mode 100644 index ca629319ce7d..000000000000 --- a/packages/runtime/plugin-runtime/src/core/server/react/prerender/index.ts +++ /dev/null @@ -1,116 +0,0 @@ -import React, { createElement } from 'react'; -import withSideEffect from 'react-side-effect'; -import type { GeneralizedProps, SprProps } from './type'; -import { - aggKeysFromPropsList, - aggMatchesFromPropsList, - exist, - getOutermostProperty, -} from './util'; - -const PROP_NAMES = { - INTERVAL: 'interval', - STALE_LIMIT: 'staleLimit', - LEVEL: 'level', - INCLUDES: 'includes', - EXCLUDES: 'excludes', - FALLBACK: 'fallback', - MATCHES: 'matches', -}; - -const handleClientStateChange = () => { - // not used -}; - -const mapStateOnServer = (reduceProps: GeneralizedProps) => { - const defaultProps: SprProps = { - interval: 10, - staleLimit: false, - level: 0, - includes: null, - excludes: null, - fallback: false, - matches: null, - }; - - return Object.keys(defaultProps).reduce((props: SprProps, key: string) => { - const propKey = key as keyof SprProps; - const reduceProp = reduceProps[propKey]; - let nextProps = props; - if (exist(reduceProp)) { - nextProps = { ...props, [propKey]: reduceProp }; - } - return nextProps; - }, defaultProps); -}; - -const reducePropsToState = (propsList: GeneralizedProps[]) => { - const reduceProps = { - interval: getOutermostProperty(propsList, PROP_NAMES.INTERVAL), - staleLimit: getOutermostProperty(propsList, PROP_NAMES.STALE_LIMIT), - level: getOutermostProperty(propsList, PROP_NAMES.LEVEL), - includes: aggKeysFromPropsList(propsList, PROP_NAMES.INCLUDES), - excludes: aggKeysFromPropsList(propsList, PROP_NAMES.EXCLUDES), - fallback: getOutermostProperty(propsList, PROP_NAMES.FALLBACK), - matches: aggMatchesFromPropsList(propsList, PROP_NAMES.MATCHES), - }; - - return reduceProps; -}; - -function factory(Component: React.ComponentType) { - class Spr extends React.Component { - static set canUseDOM(canUseDOM: boolean) { - (Component as any).canUseDOM = canUseDOM; - } - - static get canUseDOM() { - return (Component as any).canUseDOM; - } - - static peek: any = (Component as any).peek; - - static rewind: () => SprProps = (Component as any).rewind; - - static config: () => SprProps = () => { - const mappedState: SprProps = (Component as any).rewind(); - return mappedState; - }; - - private verify() { - return true; - } - - public render() { - const newProps = { ...this.props }; - - const validate = this.verify(); - if (!validate) { - throw new Error('invalid props, check usage'); - } - - if (process.env.NODE_ENV === 'development') { - console.error( - '[Warn] PreRender has been deprecated, please use SSR Cache instead. reference to docs: https://modernjs.dev/guides/advanced-features/ssr.html', - ); - } - - return createElement(Component, { ...newProps }); - } - } - - return Spr; -} - -const NullComponent = () => null; -const SprSideEffects = withSideEffect( - reducePropsToState, - handleClientStateChange, - mapStateOnServer, -)(NullComponent); - -/** - * @deprecated - * The Prerender already has been deprecated, please use [SSR Cache](https://modernjs.dev/guides/advanced-features/ssr.html) instead. - */ -export const PreRender: any = factory(SprSideEffects); diff --git a/packages/runtime/plugin-runtime/src/core/server/react/prerender/type.ts b/packages/runtime/plugin-runtime/src/core/server/react/prerender/type.ts deleted file mode 100644 index f6fbbad80cda..000000000000 --- a/packages/runtime/plugin-runtime/src/core/server/react/prerender/type.ts +++ /dev/null @@ -1,33 +0,0 @@ -export type SprProps = { - interval: number; - staleLimit: number | boolean; - level: number; - includes: MetaKeyMap | null; - excludes: MetaKeyMap | null; - fallback: boolean; - matches: MetaKeyMatch | null; -}; - -export type GeneralizedProps = SprProps & { - [propName: string]: any; -}; - -export type SprConstructor = { - config: () => SprProps; -}; - -export type MetaKeyMap = { - header?: string[]; - query?: string[]; -}; - -type MatchMap = { - [propName: string]: { - [propName: string]: string; - }; -}; - -export type MetaKeyMatch = { - header?: MatchMap; - query?: MatchMap; -}; diff --git a/packages/runtime/plugin-runtime/src/core/server/react/prerender/util.ts b/packages/runtime/plugin-runtime/src/core/server/react/prerender/util.ts deleted file mode 100644 index e35a2ee20ff0..000000000000 --- a/packages/runtime/plugin-runtime/src/core/server/react/prerender/util.ts +++ /dev/null @@ -1,121 +0,0 @@ -import type { GeneralizedProps, MetaKeyMap, MetaKeyMatch } from './type'; - -const REQUEST_META = ['header', 'query']; - -export const getInnermostProperty = function getInnermostProperty( - propsList: GeneralizedProps[], - propName: string, -) { - for (let i = propsList.length - 1; i >= 0; i--) { - const props = propsList[i]; - - if (props.hasOwnProperty(propName)) { - return props[propName]; - } - } - - return null; -}; - -export const getOutermostProperty = function getOutermostProperty( - propsList: GeneralizedProps[], - propName: string, -) { - for (const props of propsList) { - if (props.hasOwnProperty(propName)) { - return props[propName]; - } - } - - return null; -}; - -export const aggKeysFromPropsList = function aggKeysFromPropsList( - propsList: GeneralizedProps[], - propName: string, -) { - const initResult: MetaKeyMap = REQUEST_META.reduce( - (result: MetaKeyMap, next: string) => { - const key = next as keyof MetaKeyMap; - result[key] = []; - return result; - }, - {}, - ); - - const res = propsList - .filter(props => usefulObject(props[propName])) - .reduce((result: any, next: GeneralizedProps) => { - REQUEST_META.forEach(key => { - const prop = next[propName]; - if (prop?.hasOwnProperty(key) && usefulArray(prop[key])) { - result[key] = unique(result[key].concat(prop[key])); - } - }); - return result; - }, initResult); - - return REQUEST_META.reduce((result: MetaKeyMap, next: string) => { - const key = next as keyof MetaKeyMap; - if (result[key] && result[key]?.length === 0) { - delete result[key]; - } - return result; - }, res); -}; - -export const aggMatchesFromPropsList = function aggMatchesFromPropsList( - propsList: GeneralizedProps[], - propName: string, -) { - const initResult: MetaKeyMatch = REQUEST_META.reduce( - (result: MetaKeyMatch, next: string) => { - const key = next as keyof MetaKeyMap; - result[key] = {}; - return result; - }, - {}, - ); - const res = propsList - .filter(props => usefulObject(props[propName])) - .reduce((result: any, next: GeneralizedProps) => { - REQUEST_META.forEach(key => { - const prop = next[propName]; - // 这边目前是浅拷贝,越后渲染优先级越高 - if (prop?.hasOwnProperty(key) && usefulObject(prop[key])) { - result[key] = Object.assign(result[key], prop[key]); - } - }); - return result; - }, initResult); - - return REQUEST_META.reduce((result: MetaKeyMatch, next: string) => { - const key = next as keyof MetaKeyMatch; - if (result[key] && Object.keys(result[key]!).length === 0) { - delete result[key]; - } - return result; - }, res); -}; - -function unique(arr: any[]) { - return Array.from(new Set(arr)); -} - -function usefulObject(target: any) { - if (!exist(target)) { - return false; - } - return target.constructor === Object && Object.keys(target).length > 0; -} - -function usefulArray(target: any) { - if (!exist(target)) { - return false; - } - return Array.isArray(target) && target.length > 0; -} - -export function exist(target: any) { - return target != null; -}