feat: async data loader (#6137)

* chore: refactor unfinished

* feat: support create router

* refactor: render mode

* fix: code splitting false

* feat: add location for icestark

* chore: remove console

* test: examples

* fix: dataloader is undefined

* fix: test

* fix: test case

* fix: test case

* fix: types

* fix: test case

* fix: lock

* feat: async data loader

* fix: update lock

* fix: hydration

* fix: router

* fix: router

* chore: log

* fix: hmr

* fix: test

* fix: test

* feat: await

* fix: await component

* fix: lint

* refactor: type

* fix: type

* fix: app data loader

* fix: test

* fix: test

* test: async data

* test: async data

* docs: async data loader

* fix: lint

* refactor: loader config

* fix: test

* fix: compat with old useage

---------

Co-authored-by: ClarkXia <xiawenwu41@gmail.com>
This commit is contained in:
水澜 2023-04-25 10:47:40 +08:00 committed by GitHub
parent 75880524d3
commit f56497f694
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 455 additions and 105 deletions

View File

@ -0,0 +1,35 @@
import { useData, defineDataLoader, Await } from 'ice';
import styles from './index.module.css';
export default function Home() {
const data = useData();
return (
<>
<h2 className={styles.title}>With dataLoader</h2>
<Await resolve={data} fallback={<p>Loading item info...</p>} errorElement={<p>Error loading!</p>}>
{(itemInfo) => {
return <p>Item id is <span id="itemId">{itemInfo.id}</span></p>;
}}
</Await>
</>
);
}
export function pageConfig() {
return {
title: 'Home',
};
}
export const dataLoader = defineDataLoader(async () => {
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve({
id: 1233,
});
}, 100);
});
return await promise;
}, { defer: true });

View File

@ -0,0 +1,52 @@
import { useData, defineDataLoader, Await } from 'ice';
import styles from './index.module.css';
export default function Home() {
const data = useData();
return (
<>
<h2 className={styles.title}>With dataLoader</h2>
<Await resolve={data[0]} fallback={<p>Loading item info...</p>} errorElement={<p>Error loading!</p>}>
{(itemInfo) => {
return <p>Item id is {itemInfo.id}</p>;
}}
</Await>
<Await resolve={data[1]} fallback={<p>Loading item info...</p>} errorElement={<p>Error loading!</p>}>
{(itemInfo) => {
return <p>Item price is {itemInfo.price}</p>;
}}
</Await>
</>
);
}
export function pageConfig() {
return {
title: 'Home',
};
}
export const dataLoader = defineDataLoader([
async () => {
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve({
id: 1233,
});
}, 100);
});
return await promise;
},
async () => {
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve({
price: 9.99,
});
}, 2000);
});
return await promise;
},
], { defer: true });

View File

@ -0,0 +1,45 @@
import { useData, defineDataLoader, defineServerDataLoader, Await } from 'ice';
import styles from './index.module.css';
export default function Home() {
const data = useData();
return (
<>
<h2 className={styles.title}>With dataLoader</h2>
<Await resolve={data} fallback={<p>Loading item info...</p>} errorElement={<p>Error loading!</p>}>
{(itemInfo) => {
return <p>Item id is {itemInfo.id}</p>;
}}
</Await>
</>
);
}
export function pageConfig() {
return {
title: 'Home',
};
}
export const dataLoader = defineDataLoader(async () => {
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve({
id: 1233,
});
}, 100);
});
return await promise;
}, { defer: true });
export const serverDataLoader = defineServerDataLoader(async () => {
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve({
id: 1233,
});
}, 100);
});
return await promise;
});

View File

@ -61,6 +61,7 @@ export const RUNTIME_EXPORTS = [
'ClientOnly',
'withSuspense',
'useSuspenseData',
'Await',
'defineDataLoader',
'defineServerDataLoader',
'defineStaticDataLoader',

View File

@ -12,15 +12,21 @@ async function getAppData(appExport: AppExport, requestContext?: RequestContext)
return await globalLoader.getData('__app');
}
if (appExport?.dataLoader) {
return await appExport.dataLoader(requestContext);
const appDataLoaderConfig = appExport?.dataLoader;
if (!appDataLoaderConfig) {
return null;
}
const loader = appExport?.dataLoader;
let loader;
if (!loader) return null;
if (typeof appDataLoaderConfig === 'function' || Array.isArray(appDataLoaderConfig)) {
loader = appDataLoaderConfig;
} else {
loader = appDataLoaderConfig.loader;
}
await callDataLoader(loader, requestContext);
return await callDataLoader(loader, requestContext);
}
export {

View File

@ -1,16 +1,18 @@
import type { RequestContext, RenderMode, DataLoaderConfig, DataLoaderResult, RuntimeModules, AppExport, StaticRuntimePlugin, CommonJsRuntime, StaticDataLoader } from './types.js';
import getRequestContext from './requestContext.js';
import type {
RequestContext, RenderMode, AppExport,
RuntimeModules, StaticRuntimePlugin, CommonJsRuntime,
Loader, DataLoaderResult, StaticDataLoader, DataLoaderConfig, DataLoaderOptions,
} from './types.js';
interface Loaders {
[routeId: string]: DataLoaderConfig;
}
interface CachedResult {
value: any;
status: string;
}
interface LoaderOptions {
interface Options {
fetcher: Function;
runtimeModules: RuntimeModules['statics'];
appExport: AppExport;
@ -20,16 +22,24 @@ export interface LoadRoutesDataOptions {
renderMode: RenderMode;
}
export function defineDataLoader(dataLoaderConfig: DataLoaderConfig): DataLoaderConfig {
return dataLoaderConfig;
export function defineDataLoader(dataLoader: Loader, options?: DataLoaderOptions): DataLoaderConfig {
return {
loader: dataLoader,
options,
};
}
export function defineServerDataLoader(dataLoaderConfig: DataLoaderConfig): DataLoaderConfig {
return dataLoaderConfig;
export function defineServerDataLoader(dataLoader: Loader, options?: DataLoaderOptions): DataLoaderConfig {
return {
loader: dataLoader,
options,
};
}
export function defineStaticDataLoader(dataLoaderConfig: DataLoaderConfig): DataLoaderConfig {
return dataLoaderConfig;
export function defineStaticDataLoader(dataLoader: Loader): DataLoaderConfig {
return {
loader: dataLoader,
};
}
/**
@ -124,12 +134,13 @@ export function loadDataByCustomFetcher(config: StaticDataLoader) {
/**
* Handle for different dataLoader.
*/
export function callDataLoader(dataLoader: DataLoaderConfig, requestContext: RequestContext): DataLoaderResult {
export function callDataLoader(dataLoader: Loader, requestContext: RequestContext): DataLoaderResult {
if (Array.isArray(dataLoader)) {
const loaders = dataLoader.map(loader => {
return typeof loader === 'object' ? loadDataByCustomFetcher(loader) : loader(requestContext);
});
return Promise.all(loaders);
return loaders;
}
if (typeof dataLoader === 'object') {
@ -156,7 +167,6 @@ function loadInitialDataInClient(loaders: Loaders) {
if (dataFromSSR) {
cache.set(renderMode === 'SSG' ? `${id}_ssg` : id, {
value: dataFromSSR,
status: 'RESOLVED',
});
if (renderMode === 'SSR') {
@ -164,15 +174,15 @@ function loadInitialDataInClient(loaders: Loaders) {
}
}
const dataLoader = loaders[id];
const dataLoaderConfig = loaders[id];
if (dataLoader) {
if (dataLoaderConfig) {
const requestContext = getRequestContext(window.location);
const loader = callDataLoader(dataLoader, requestContext);
const { loader } = dataLoaderConfig;
const promise = callDataLoader(loader, requestContext);
cache.set(id, {
value: loader,
status: 'LOADING',
value: promise,
});
}
});
@ -183,7 +193,7 @@ function loadInitialDataInClient(loaders: Loaders) {
* Load initial data and register global loader.
* In order to load data, JavaScript modules, CSS and other assets in parallel.
*/
async function init(dataloaderConfig: Loaders, options: LoaderOptions) {
async function init(loaders: Loaders, options: Options) {
const {
fetcher,
runtimeModules,
@ -208,57 +218,39 @@ async function init(dataloaderConfig: Loaders, options: LoaderOptions) {
}
try {
loadInitialDataInClient(dataloaderConfig);
loadInitialDataInClient(loaders);
} catch (error) {
console.error('Load initial data error: ', error);
}
(window as any).__ICE_DATA_LOADER__ = {
getData: async (id, options: LoadRoutesDataOptions) => {
getData: (id, options: LoadRoutesDataOptions) => {
let result;
// first render for ssg use data from build time.
// second render for ssg will use data from data loader.
// First render for ssg use data from build time, second render for ssg will use data from data loader.
const cacheKey = `${id}${options?.renderMode === 'SSG' ? '_ssg' : ''}`;
// In CSR, all dataLoader is called by global data loader to avoid bundle dataLoader in page bundle duplicate.
result = cache.get(cacheKey);
// Always fetch new data after cache is been used.
cache.delete(cacheKey);
// Already send data request.
if (result) {
const { status, value } = result;
if (status === 'RESOLVED') {
return result;
}
try {
if (Array.isArray(value)) {
return await Promise.all(value);
}
return await value;
} catch (error) {
console.error('DataLoader: getData error.\n', error);
return {
message: 'DataLoader: getData error.',
error,
};
}
return result.value;
}
const dataLoader = dataloaderConfig[id];
const dataLoaderConfig = loaders[id];
// No data loader.
if (!dataLoader) {
if (!dataLoaderConfig) {
return null;
}
// Call dataLoader.
// In CSR, all dataLoader is called by global data loader to avoid bundle dataLoader in page bundle duplicate.
const requestContext = getRequestContext(window.location);
return await callDataLoader(dataLoader, requestContext);
const { loader } = dataLoaderConfig;
return callDataLoader(loader, requestContext);
},
};
}

View File

@ -16,7 +16,7 @@ import type {
RouteWrapper,
RenderMode,
DistType,
DataLoaderConfig,
Loader,
RouteWrapperConfig,
} from './types.js';
import Runtime from './runtime.js';
@ -50,7 +50,7 @@ import KeepAliveOutlet from './KeepAliveOutlet.js';
import ClientOnly from './ClientOnly.js';
import useMounted from './useMounted.js';
import { withSuspense, useSuspenseData } from './Suspense.js';
import { createRouteLoader, WrapRouteComponent, RouteErrorComponent } from './routes.js';
import { createRouteLoader, WrapRouteComponent, RouteErrorComponent, Await } from './routes.js';
export {
getAppConfig,
@ -92,6 +92,8 @@ export {
withSuspense,
useSuspenseData,
Await,
createRouteLoader,
WrapRouteComponent,
RouteErrorComponent,
@ -109,7 +111,7 @@ export type {
RouteWrapper,
RenderMode,
DistType,
DataLoaderConfig,
Loader,
RunClientAppOptions,
MetaType,
TitleType,

View File

@ -1,6 +1,8 @@
import React from 'react';
import { useRouteError } from 'react-router-dom';
import type { RouteItem, RouteModules, RenderMode, DataLoaderConfig, RequestContext, ComponentModule } from './types.js';
import React, { Suspense } from 'react';
import { useRouteError, defer, Await as ReactRouterAwait } from 'react-router-dom';
// eslint-disable-next-line camelcase
import type { UNSAFE_DeferredData } from '@remix-run/router';
import type { RouteItem, RouteModules, RenderMode, RequestContext, ComponentModule, DataLoaderConfig } from './types.js';
import RouteWrapper from './RouteWrapper.js';
import { useAppContext } from './AppContext.js';
import { callDataLoader } from './dataLoader.js';
@ -89,12 +91,25 @@ export function RouteErrorComponent() {
return <></>;
}
export function Await(props) {
return (
<Suspense fallback={props.fallback}>
<ReactRouterAwait
resolve={props.resolve}
errorElement={props.errorElement}
>
{props.children}
</ReactRouterAwait>
</Suspense>
);
}
/**
* Create loader function for route module.
*/
interface LoaderData {
data: any;
pageConfig: any;
data?: any;
pageConfig?: any;
}
export interface RouteLoaderOptions {
@ -104,28 +119,94 @@ export interface RouteLoaderOptions {
renderMode: RenderMode;
}
export function createRouteLoader(options: RouteLoaderOptions): () => Promise<LoaderData> {
return async () => {
const { dataLoader, pageConfig, staticDataLoader, serverDataLoader } = options.module;
const { requestContext, renderMode, routeId } = options;
// eslint-disable-next-line camelcase
type LoaderFunction = () => LoaderData | UNSAFE_DeferredData | Promise<LoaderData>;
export function createRouteLoader(options: RouteLoaderOptions): LoaderFunction {
const { dataLoader, pageConfig, staticDataLoader, serverDataLoader } = options.module;
const { requestContext, renderMode, routeId } = options;
let dataLoaderConfig: DataLoaderConfig;
if (renderMode === 'SSG') {
dataLoaderConfig = staticDataLoader;
} else if (renderMode === 'SSR') {
dataLoaderConfig = serverDataLoader || dataLoader;
} else {
dataLoaderConfig = dataLoader;
}
if (!dataLoaderConfig) {
return () => {
return {
pageConfig: pageConfig ? pageConfig({}) : {},
};
};
}
let loader;
let loaderOptions;
// Compat dataLoaderConfig not return by defineDataLoader.
if (typeof dataLoaderConfig === 'function' || Array.isArray(dataLoaderConfig)) {
loader = dataLoaderConfig;
} else {
loader = dataLoaderConfig.loader;
loaderOptions = dataLoaderConfig.options;
}
const getData = () => {
const hasGlobalLoader = typeof window !== 'undefined' && (window as any).__ICE_DATA_LOADER__;
const globalLoader = hasGlobalLoader ? (window as any).__ICE_DATA_LOADER__ : null;
let routeData: any;
if (globalLoader) {
routeData = await globalLoader.getData(routeId, { renderMode });
routeData = globalLoader.getData(routeId, { renderMode });
} else {
let loader: DataLoaderConfig;
if (renderMode === 'SSG') {
loader = staticDataLoader;
} else if (renderMode === 'SSR') {
loader = serverDataLoader || dataLoader;
} else {
loader = dataLoader;
}
routeData = loader && await callDataLoader(loader, requestContext);
routeData = callDataLoader(loader, requestContext);
}
return routeData;
};
// Async dataLoader.
if (loaderOptions?.defer) {
return async () => {
const promise = getData();
return defer({
data: promise,
// Call pageConfig without data.
pageConfig: pageConfig ? pageConfig({}) : {},
});
};
}
// Await dataLoader before render.
return async () => {
const result = getData();
let routeData;
try {
if (Array.isArray(result)) {
routeData = await Promise.all(result);
} else if (result instanceof Promise) {
routeData = await result;
} else {
routeData = result;
}
} catch (error) {
console.error('DataLoader: getData error.\n', error);
routeData = {
message: 'DataLoader: getData error.',
error,
};
}
const routeConfig = pageConfig ? pageConfig({ data: routeData }) : {};
const loaderData = { data: routeData, pageConfig: routeConfig };
const loaderData = {
data: routeData,
pageConfig: routeConfig,
};
// CSR and load next route data.
if (typeof window !== 'undefined') {
await updateRoutesConfig(loaderData);

View File

@ -31,7 +31,7 @@ export type RouteConfig<T = {}> = T & {
export interface AppExport {
default?: AppConfig;
[key: string]: any;
dataLoader?: DataLoader;
dataLoader?: DataLoaderConfig;
}
export type DataLoaderResult = (Promise<RouteData> | RouteData) | RouteData;
@ -49,7 +49,7 @@ export interface StaticDataLoader {
// route.defineDataLoader
// route.defineServerDataLoader
// route.defineStaticDataLoader
export type DataLoaderConfig = DataLoader | StaticDataLoader | Array<DataLoader | StaticDataLoader>;
export type Loader = DataLoader | StaticDataLoader | Array<DataLoader | StaticDataLoader>;
// route.pageConfig
export type PageConfig = (args: { data?: RouteData }) => RouteConfig;
@ -71,6 +71,15 @@ export interface RoutesData {
[routeId: string]: RouteData;
}
export interface DataLoaderOptions {
defer?: boolean;
}
export interface DataLoaderConfig {
loader: Loader;
options?: DataLoaderOptions;
}
export interface LoadersData {
[routeId: string]: LoaderData;
}

View File

@ -28,14 +28,20 @@ describe('routes', () => {
const homeItem = {
default: () => <></>,
pageConfig: () => ({ title: 'home' }),
dataLoader: async () => ({ type: 'getData' }),
serverDataLoader: async () => ({ type: 'getServerData' }),
staticDataLoader: async () => ({ type: 'getStaticData' }),
dataLoader: { loader: async () => ({ type: 'getData' }) },
serverDataLoader: { loader: async () => ({ type: 'getServerData' }) },
staticDataLoader: { loader: async () => ({ type: 'getStaticData' }) },
};
const aboutItem = {
default: () => <></>,
pageConfig: () => ({ title: 'about' }),
};
const InfoItem = {
default: () => <></>,
pageConfig: () => ({ title: 'home' }),
dataLoader: { loader: async () => ({ type: 'getAsyncData' }), options: { defer: true } },
};
const homeLazyItem = {
Component: homeItem.default,
loader: createRouteLoader({
@ -145,6 +151,19 @@ describe('routes', () => {
});
});
it('load async route', async () => {
const { data: deferredResult } = await createRouteLoader({
routeId: 'home',
module: InfoItem,
})();
const data = await deferredResult.data;
expect(data).toStrictEqual({
type: 'getAsyncData',
});
});
it('load route data for SSG', async () => {
const routesDataSSG = await createRouteLoader({
routeId: 'home',
@ -226,7 +245,6 @@ describe('routes', () => {
})();
expect(routesDataCSR).toStrictEqual({
data: undefined,
pageConfig: {
title: 'about',
},

View File

@ -92,7 +92,9 @@ describe('run client app', () => {
);
},
pageConfig: () => ({ title: 'home' }),
dataLoader: async () => ({ data: 'test' }),
dataLoader: {
loader: async () => ({ data: 'test' }),
},
};
const basicRoutes = [
{
@ -110,8 +112,10 @@ describe('run client app', () => {
it('run with static runtime', async () => {
await runClientApp({
app: {
dataLoader: async () => {
return { msg: staticMsg };
dataLoader: {
loader: async () => {
return { msg: staticMsg };
},
},
},
// @ts-ignore don't need to pass params in test case.
@ -261,9 +265,11 @@ describe('run client app', () => {
let executed = false;
await runClientApp({
app: {
dataLoader: async () => {
executed = true;
return { msg: '-getAppData' };
dataLoader: {
loader: async () => {
executed = true;
return { msg: '-getAppData' };
},
},
},
// @ts-ignore don't need to pass params in test case.
@ -290,9 +296,11 @@ describe('run client app', () => {
await runClientApp({
app: {
dataLoader: async () => {
executed = true;
return { msg: 'app' };
dataLoader: {
loader: async () => {
executed = true;
return { msg: 'app' };
},
},
},
// @ts-ignore don't need to pass params in test case.

View File

@ -14,7 +14,9 @@ describe('run server app', () => {
const homeItem = {
default: () => <div>home</div>,
pageConfig: () => ({ title: 'home' }),
dataLoader: async () => ({ data: 'test' }),
dataLoader: {
loader: async () => ({ data: 'test' }),
},
};
const basicRoutes = [
{

View File

@ -7457,6 +7457,7 @@ packages:
'@types/prop-types': 15.7.5
'@types/scheduler': 0.16.2
csstype: 3.1.1
dev: true
/@types/react/18.0.28:
resolution: {integrity: sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==}
@ -9622,8 +9623,8 @@ packages:
engines: {node: '>=10'}
hasBin: true
dependencies:
JSONStream: 1.3.5
is-text-path: 1.0.1
JSONStream: 1.3.5
lodash: 4.17.21
meow: 8.1.2
split2: 3.2.2
@ -9653,6 +9654,7 @@ packages:
resolution: {integrity: sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==}
dependencies:
is-what: 3.14.1
dev: false
/copy-text-to-clipboard/3.0.1:
resolution: {integrity: sha512-rvVsHrpFcL4F2P8ihsoLdFHmd404+CMg71S756oRSeQgqk51U3kicGdnvfkrxva0xXH92SjGS62B0XIJsbh+9Q==}
@ -10752,6 +10754,7 @@ packages:
requiresBuild: true
dependencies:
prr: 1.0.1
dev: false
optional: true
/error-ex/1.3.2:
@ -13065,6 +13068,7 @@ packages:
engines: {node: '>=0.10.0'}
hasBin: true
requiresBuild: true
dev: false
optional: true
/image-size/1.0.2:
@ -13589,6 +13593,7 @@ packages:
/is-what/3.14.1:
resolution: {integrity: sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==}
dev: false
/is-whitespace-character/1.0.4:
resolution: {integrity: sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==}
@ -14870,6 +14875,7 @@ packages:
source-map: 0.6.1
transitivePeerDependencies:
- supports-color
dev: false
/leven/3.1.0:
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
@ -15168,6 +15174,7 @@ packages:
dependencies:
pify: 4.0.1
semver: 5.7.1
dev: false
optional: true
/make-dir/3.1.0:
@ -15554,6 +15561,7 @@ packages:
sax: 1.2.4
transitivePeerDependencies:
- supports-color
dev: false
optional: true
/negotiator/0.6.3:
@ -16006,6 +16014,7 @@ packages:
/parse-node-version/1.0.1:
resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==}
engines: {node: '>= 0.10'}
dev: false
/parse-numeric-range/1.3.0:
resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==}
@ -17337,6 +17346,7 @@ packages:
nanoid: 3.3.4
picocolors: 1.0.0
source-map-js: 1.0.2
dev: false
/postcss/8.4.21:
resolution: {integrity: sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==}
@ -17490,6 +17500,7 @@ packages:
/prr/1.0.1:
resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==}
dev: false
optional: true
/pseudomap/1.0.2:
@ -18624,12 +18635,6 @@ packages:
/react-dev-utils/12.0.1_a37q6j7dwawz22saey2vgkpwqm:
resolution: {integrity: sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==}
engines: {node: '>=14'}
peerDependencies:
typescript: '>=2.7'
webpack: '>=4'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
'@babel/code-frame': 7.18.6
address: 1.2.2
@ -18660,7 +18665,9 @@ packages:
transitivePeerDependencies:
- eslint
- supports-color
- typescript
- vue-template-compiler
- webpack
/react-dom/17.0.2_react@17.0.2:
resolution: {integrity: sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==}
@ -18671,6 +18678,7 @@ packages:
object-assign: 4.1.1
react: 17.0.2
scheduler: 0.20.2
dev: false
/react-dom/18.2.0_react@18.2.0:
resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
@ -18806,6 +18814,7 @@ packages:
/react-refresh/0.14.0:
resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==}
engines: {node: '>=0.10.0'}
dev: false
/react-router-config/5.1.1_2dl5roaqnyqqppnjni7uetnb3a:
resolution: {integrity: sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg==}
@ -18903,6 +18912,7 @@ packages:
dependencies:
loose-envify: 1.4.0
object-assign: 4.1.1
dev: false
/react/18.2.0:
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
@ -19518,6 +19528,7 @@ packages:
chokidar: 3.5.3
immutable: 4.2.4
source-map-js: 1.0.2
dev: false
/sax/1.2.4:
resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
@ -19534,6 +19545,7 @@ packages:
dependencies:
loose-envify: 1.4.0
object-assign: 4.1.1
dev: false
/scheduler/0.21.0:
resolution: {integrity: sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==}
@ -20563,6 +20575,7 @@ packages:
serialize-javascript: 6.0.1
terser: 5.14.2
webpack: 5.76.2_w34or7orauknzckzea4nxxqrru
dev: true
/terser-webpack-plugin/5.3.5_webpack@5.76.2:
resolution: {integrity: sha512-AOEDLDxD2zylUGf/wxHxklEkOe2/r+seuyOWujejFrIxHf11brA1/dWQNIgXa1c6/Wkxgu7zvv0JhOWfc2ELEA==}
@ -21810,7 +21823,7 @@ packages:
mime-types: 2.1.35
range-parser: 1.2.1
schema-utils: 4.0.0
webpack: 5.76.2_w34or7orauknzckzea4nxxqrru
webpack: 5.76.2_esbuild@0.17.16
/webpack-dev-server/4.11.1_webpack@5.76.2:
resolution: {integrity: sha512-lILVz9tAUy1zGFwieuaQtYiadImb5M3d+H+L1zDYalYoDl0cksAB1UNyuE5MMWJrG6zR1tXkCP2fitl7yoUJiw==}
@ -21850,7 +21863,7 @@ packages:
serve-index: 1.9.1
sockjs: 0.3.24
spdy: 4.0.2
webpack: 5.76.2_w34or7orauknzckzea4nxxqrru
webpack: 5.76.2_esbuild@0.17.16
webpack-dev-middleware: 5.3.3_webpack@5.76.2
ws: 8.12.1
transitivePeerDependencies:
@ -22151,6 +22164,7 @@ packages:
- '@swc/core'
- esbuild
- uglify-js
dev: true
/webpackbar/5.0.2_webpack@5.76.2:
resolution: {integrity: sha512-BmFJo7veBDgQzfWXl/wwYXr/VFus0614qZ8i9znqcl9fnEdiVkdbi0TedLQ6xAK92HZHDJ0QmyQ0fmuZPAgCYQ==}

View File

@ -49,6 +49,14 @@ describe(`start ${example}`, () => {
expect(timeStampForRouter3).not.toStrictEqual(timeStampForRouter1);
});
test('should work with deferred data loader', async () => {
await page.push('/with-defer-loader');
await page.waitForNetworkIdle();
const data = (await page.$$text('#itemId'))[0];
expect(data).toEqual('1233');
});
afterAll(async () => {
await browser.close();
});

View File

@ -71,6 +71,51 @@ export const dataLoader = defineDataLoader(async () => {
受小程序环境限制,通过 `dataLoader` 定义的应用级数据加载将在 `App``onLaunch` 生命周期中进行,页面级数据加载则会在 `Page``onLoad` 生命周期中,二者均会阻塞页面的 UI 渲染。如果这不是你想要的效果,请按照常规方式进行数据请求。(比如在组件首次 `useEffect` 时发起数据请求)
:::
## 异步消费数据
默认情况下,页面会等待数据请求完成后,再开始渲染,在数据接口比较快的情况下,这可以避免页面的二次渲染。
如果数据接口较慢,也可以选择先渲染不依赖于动态数据的部分,待数据回来后,再重新渲染依赖数据的页面内容。
具体做法如下:
- 1. 在定义 dataLoader 时标记 defer: true
- 2. 在消费数据时,使用 Await 组件包裹依赖于数据的页面内容
```tsx title="src/pages/index.tsx"
import { useData, defineDataLoader, Await } from 'ice';
// 页面组件的 UI 实现
export default function Home() {
const data = useData();
return (
<>
<div>Hello ICE</div>
<Await resolve={data} fallback={<div>loading...</div>} errorElement={<div>Error!</div>} />
{
(data) => <div>{JSON.stringify(data)}</div>
}
</Await>
</>
);
};
// 在定义 dataLoader 时标记 defer: true
export const dataLoader = defineDataLoader(async () => {
const data = await fetch('https://example.com/api/xxx');
return data;
}, { defer: true });
```
注意:
- 当 dataLoader 被声明为异步时useData 返回的内容不可直接消费,需由 Await 组件处理
Await 组件接收三个参数
* resolve 数据请求对象
* fallback 数据加载过程中展示的 UI
* errorElement 请求失败时展示的 UI
## 静态 dataLoader
当开发者希望通过统一的发送函数处理静态配置以完成 `dataLoader` 时,可以通过自定义 `fetcher` 以完成发送逻辑的统一封装,在 `dataLoader` 中只需要传递一份配置即可。
@ -186,7 +231,7 @@ export default function Home(props) {
import { useData, defineDataLoader } from 'ice';
export default function Home() {
const [useInfo, itemInfo] = useData();
const [userInfo, itemInfo] = useData();
return (
<>
@ -196,6 +241,40 @@ export default function Home() {
);
};
export const dataLoader = defineDataLoader([
async () => {
const userInfo = await fetch('https://example.com/api/userInfo');
return userInfo;
},
async (ctx) => {
const itemInfo = await fetch(`https://example.com/api/itemInfo${ctx?.query?.itemId}`);
return itemInfo;
},
]);
```
多个数据请求的情况下,`useData` 获取的数据也对应的为数组,数组元素和 `dataLoader` 中定义的数据请求的返回值一一对应。
如果 dataLoader 被声明为异步,消费时可以分别 Await 不同的数据,这样可以做到先返回的数据,先渲染。
```tsx
import { useData, defineDataLoader } from 'ice';
export default function Home() {
const [userInfo, itemInfo] = useData();
return (
<>
<Await resolve={userInfo}>
{ (data) => <div>Hello {data?.name}</div> }
</Await>
<Await resolve={itemInfo}>
{ (data) => <div>{JSON.stringify(data)}</div> }
</Await>
</Await>
);
};
export const dataLoader = defineDataLoader([
async () => {
const useInfo = await fetch('https://example.com/api/userInfo');
@ -205,7 +284,5 @@ export const dataLoader = defineDataLoader([
const itemInfo = await fetch(`https://example.com/api/itemInfo${ctx?.query?.itemId}`);
return itemInfo;
},
]);
```
多个数据请求的情况下,`useData` 获取的数据也对应的为数组,数组元素和 `dataLoader` 中定义的数据请求的返回值一一对应。
], { defer: true });
```