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', 'ClientOnly',
'withSuspense', 'withSuspense',
'useSuspenseData', 'useSuspenseData',
'Await',
'defineDataLoader', 'defineDataLoader',
'defineServerDataLoader', 'defineServerDataLoader',
'defineStaticDataLoader', 'defineStaticDataLoader',

View File

@ -12,15 +12,21 @@ async function getAppData(appExport: AppExport, requestContext?: RequestContext)
return await globalLoader.getData('__app'); return await globalLoader.getData('__app');
} }
if (appExport?.dataLoader) { const appDataLoaderConfig = appExport?.dataLoader;
return await appExport.dataLoader(requestContext);
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 { 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 getRequestContext from './requestContext.js';
import type {
RequestContext, RenderMode, AppExport,
RuntimeModules, StaticRuntimePlugin, CommonJsRuntime,
Loader, DataLoaderResult, StaticDataLoader, DataLoaderConfig, DataLoaderOptions,
} from './types.js';
interface Loaders { interface Loaders {
[routeId: string]: DataLoaderConfig; [routeId: string]: DataLoaderConfig;
} }
interface CachedResult { interface CachedResult {
value: any; value: any;
status: string;
} }
interface LoaderOptions { interface Options {
fetcher: Function; fetcher: Function;
runtimeModules: RuntimeModules['statics']; runtimeModules: RuntimeModules['statics'];
appExport: AppExport; appExport: AppExport;
@ -20,16 +22,24 @@ export interface LoadRoutesDataOptions {
renderMode: RenderMode; renderMode: RenderMode;
} }
export function defineDataLoader(dataLoaderConfig: DataLoaderConfig): DataLoaderConfig { export function defineDataLoader(dataLoader: Loader, options?: DataLoaderOptions): DataLoaderConfig {
return dataLoaderConfig; return {
loader: dataLoader,
options,
};
} }
export function defineServerDataLoader(dataLoaderConfig: DataLoaderConfig): DataLoaderConfig { export function defineServerDataLoader(dataLoader: Loader, options?: DataLoaderOptions): DataLoaderConfig {
return dataLoaderConfig; return {
loader: dataLoader,
options,
};
} }
export function defineStaticDataLoader(dataLoaderConfig: DataLoaderConfig): DataLoaderConfig { export function defineStaticDataLoader(dataLoader: Loader): DataLoaderConfig {
return dataLoaderConfig; return {
loader: dataLoader,
};
} }
/** /**
@ -124,12 +134,13 @@ export function loadDataByCustomFetcher(config: StaticDataLoader) {
/** /**
* Handle for different dataLoader. * Handle for different dataLoader.
*/ */
export function callDataLoader(dataLoader: DataLoaderConfig, requestContext: RequestContext): DataLoaderResult { export function callDataLoader(dataLoader: Loader, requestContext: RequestContext): DataLoaderResult {
if (Array.isArray(dataLoader)) { if (Array.isArray(dataLoader)) {
const loaders = dataLoader.map(loader => { const loaders = dataLoader.map(loader => {
return typeof loader === 'object' ? loadDataByCustomFetcher(loader) : loader(requestContext); return typeof loader === 'object' ? loadDataByCustomFetcher(loader) : loader(requestContext);
}); });
return Promise.all(loaders);
return loaders;
} }
if (typeof dataLoader === 'object') { if (typeof dataLoader === 'object') {
@ -156,7 +167,6 @@ function loadInitialDataInClient(loaders: Loaders) {
if (dataFromSSR) { if (dataFromSSR) {
cache.set(renderMode === 'SSG' ? `${id}_ssg` : id, { cache.set(renderMode === 'SSG' ? `${id}_ssg` : id, {
value: dataFromSSR, value: dataFromSSR,
status: 'RESOLVED',
}); });
if (renderMode === 'SSR') { 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 requestContext = getRequestContext(window.location);
const loader = callDataLoader(dataLoader, requestContext); const { loader } = dataLoaderConfig;
const promise = callDataLoader(loader, requestContext);
cache.set(id, { cache.set(id, {
value: loader, value: promise,
status: 'LOADING',
}); });
} }
}); });
@ -183,7 +193,7 @@ function loadInitialDataInClient(loaders: Loaders) {
* Load initial data and register global loader. * Load initial data and register global loader.
* In order to load data, JavaScript modules, CSS and other assets in parallel. * 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 { const {
fetcher, fetcher,
runtimeModules, runtimeModules,
@ -208,57 +218,39 @@ async function init(dataloaderConfig: Loaders, options: LoaderOptions) {
} }
try { try {
loadInitialDataInClient(dataloaderConfig); loadInitialDataInClient(loaders);
} catch (error) { } catch (error) {
console.error('Load initial data error: ', error); console.error('Load initial data error: ', error);
} }
(window as any).__ICE_DATA_LOADER__ = { (window as any).__ICE_DATA_LOADER__ = {
getData: async (id, options: LoadRoutesDataOptions) => { getData: (id, options: LoadRoutesDataOptions) => {
let result; let result;
// first render for ssg use data from build time. // First render for ssg use data from build time, second render for ssg will use data from data loader.
// second render for ssg will use data from data loader.
const cacheKey = `${id}${options?.renderMode === 'SSG' ? '_ssg' : ''}`; 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); result = cache.get(cacheKey);
// Always fetch new data after cache is been used. // Always fetch new data after cache is been used.
cache.delete(cacheKey); cache.delete(cacheKey);
// Already send data request. // Already send data request.
if (result) { if (result) {
const { status, value } = result; return result.value;
if (status === 'RESOLVED') {
return result;
} }
try { const dataLoaderConfig = loaders[id];
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,
};
}
}
const dataLoader = dataloaderConfig[id];
// No data loader. // No data loader.
if (!dataLoader) { if (!dataLoaderConfig) {
return null; return null;
} }
// Call dataLoader. // 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); 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, RouteWrapper,
RenderMode, RenderMode,
DistType, DistType,
DataLoaderConfig, Loader,
RouteWrapperConfig, RouteWrapperConfig,
} from './types.js'; } from './types.js';
import Runtime from './runtime.js'; import Runtime from './runtime.js';
@ -50,7 +50,7 @@ import KeepAliveOutlet from './KeepAliveOutlet.js';
import ClientOnly from './ClientOnly.js'; import ClientOnly from './ClientOnly.js';
import useMounted from './useMounted.js'; import useMounted from './useMounted.js';
import { withSuspense, useSuspenseData } from './Suspense.js'; import { withSuspense, useSuspenseData } from './Suspense.js';
import { createRouteLoader, WrapRouteComponent, RouteErrorComponent } from './routes.js'; import { createRouteLoader, WrapRouteComponent, RouteErrorComponent, Await } from './routes.js';
export { export {
getAppConfig, getAppConfig,
@ -92,6 +92,8 @@ export {
withSuspense, withSuspense,
useSuspenseData, useSuspenseData,
Await,
createRouteLoader, createRouteLoader,
WrapRouteComponent, WrapRouteComponent,
RouteErrorComponent, RouteErrorComponent,
@ -109,7 +111,7 @@ export type {
RouteWrapper, RouteWrapper,
RenderMode, RenderMode,
DistType, DistType,
DataLoaderConfig, Loader,
RunClientAppOptions, RunClientAppOptions,
MetaType, MetaType,
TitleType, TitleType,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -49,6 +49,14 @@ describe(`start ${example}`, () => {
expect(timeStampForRouter3).not.toStrictEqual(timeStampForRouter1); 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 () => { afterAll(async () => {
await browser.close(); await browser.close();
}); });

View File

@ -71,6 +71,51 @@ export const dataLoader = defineDataLoader(async () => {
受小程序环境限制,通过 `dataLoader` 定义的应用级数据加载将在 `App``onLaunch` 生命周期中进行,页面级数据加载则会在 `Page``onLoad` 生命周期中,二者均会阻塞页面的 UI 渲染。如果这不是你想要的效果,请按照常规方式进行数据请求。(比如在组件首次 `useEffect` 时发起数据请求) 受小程序环境限制,通过 `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
当开发者希望通过统一的发送函数处理静态配置以完成 `dataLoader` 时,可以通过自定义 `fetcher` 以完成发送逻辑的统一封装,在 `dataLoader` 中只需要传递一份配置即可。 当开发者希望通过统一的发送函数处理静态配置以完成 `dataLoader` 时,可以通过自定义 `fetcher` 以完成发送逻辑的统一封装,在 `dataLoader` 中只需要传递一份配置即可。
@ -186,7 +231,7 @@ export default function Home(props) {
import { useData, defineDataLoader } from 'ice'; import { useData, defineDataLoader } from 'ice';
export default function Home() { export default function Home() {
const [useInfo, itemInfo] = useData(); const [userInfo, itemInfo] = useData();
return ( 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([ export const dataLoader = defineDataLoader([
async () => { async () => {
const useInfo = await fetch('https://example.com/api/userInfo'); 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}`); const itemInfo = await fetch(`https://example.com/api/itemInfo${ctx?.query?.itemId}`);
return itemInfo; return itemInfo;
}, },
]); ], { defer: true });
``` ```
多个数据请求的情况下,`useData` 获取的数据也对应的为数组,数组元素和 `dataLoader` 中定义的数据请求的返回值一一对应。