mirror of https://github.com/alibaba/ice.git
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:
parent
75880524d3
commit
f56497f694
|
@ -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 });
|
||||
|
|
@ -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 });
|
||||
|
|
@ -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;
|
||||
});
|
|
@ -61,6 +61,7 @@ export const RUNTIME_EXPORTS = [
|
|||
'ClientOnly',
|
||||
'withSuspense',
|
||||
'useSuspenseData',
|
||||
'Await',
|
||||
'defineDataLoader',
|
||||
'defineServerDataLoader',
|
||||
'defineStaticDataLoader',
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
return result.value;
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 () => {
|
||||
// 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;
|
||||
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 {
|
||||
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 loaderData = { data: routeData, pageConfig: routeConfig };
|
||||
const loaderData = {
|
||||
data: routeData,
|
||||
pageConfig: routeConfig,
|
||||
};
|
||||
|
||||
// CSR and load next route data.
|
||||
if (typeof window !== 'undefined') {
|
||||
await updateRoutesConfig(loaderData);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
|
|
|
@ -92,7 +92,9 @@ describe('run client app', () => {
|
|||
);
|
||||
},
|
||||
pageConfig: () => ({ title: 'home' }),
|
||||
dataLoader: async () => ({ data: 'test' }),
|
||||
dataLoader: {
|
||||
loader: async () => ({ data: 'test' }),
|
||||
},
|
||||
};
|
||||
const basicRoutes = [
|
||||
{
|
||||
|
@ -110,10 +112,12 @@ describe('run client app', () => {
|
|||
it('run with static runtime', async () => {
|
||||
await runClientApp({
|
||||
app: {
|
||||
dataLoader: async () => {
|
||||
dataLoader: {
|
||||
loader: async () => {
|
||||
return { msg: staticMsg };
|
||||
},
|
||||
},
|
||||
},
|
||||
// @ts-ignore don't need to pass params in test case.
|
||||
createRoutes: () => basicRoutes,
|
||||
runtimeModules: { commons: [serverRuntime], statics: [staticRuntime] },
|
||||
|
@ -261,11 +265,13 @@ describe('run client app', () => {
|
|||
let executed = false;
|
||||
await runClientApp({
|
||||
app: {
|
||||
dataLoader: async () => {
|
||||
dataLoader: {
|
||||
loader: async () => {
|
||||
executed = true;
|
||||
return { msg: '-getAppData' };
|
||||
},
|
||||
},
|
||||
},
|
||||
// @ts-ignore don't need to pass params in test case.
|
||||
createRoutes: () => basicRoutes,
|
||||
runtimeModules: { commons: [serverRuntime] },
|
||||
|
@ -290,11 +296,13 @@ describe('run client app', () => {
|
|||
|
||||
await runClientApp({
|
||||
app: {
|
||||
dataLoader: async () => {
|
||||
dataLoader: {
|
||||
loader: async () => {
|
||||
executed = true;
|
||||
return { msg: 'app' };
|
||||
},
|
||||
},
|
||||
},
|
||||
// @ts-ignore don't need to pass params in test case.
|
||||
createRoutes: () => basicRoutes,
|
||||
runtimeModules: { commons: [serverRuntime] },
|
||||
|
|
|
@ -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 = [
|
||||
{
|
||||
|
|
|
@ -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==}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
]);
|
||||
], { defer: true });
|
||||
```
|
||||
|
||||
多个数据请求的情况下,`useData` 获取的数据也对应的为数组,数组元素和 `dataLoader` 中定义的数据请求的返回值一一对应。
|
||||
|
|
Loading…
Reference in New Issue