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',
|
'ClientOnly',
|
||||||
'withSuspense',
|
'withSuspense',
|
||||||
'useSuspenseData',
|
'useSuspenseData',
|
||||||
|
'Await',
|
||||||
'defineDataLoader',
|
'defineDataLoader',
|
||||||
'defineServerDataLoader',
|
'defineServerDataLoader',
|
||||||
'defineStaticDataLoader',
|
'defineStaticDataLoader',
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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',
|
||||||
},
|
},
|
||||||
|
|
|
@ -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] },
|
||||||
|
|
|
@ -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 = [
|
||||||
{
|
{
|
||||||
|
|
|
@ -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==}
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
|
@ -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` 中定义的数据请求的返回值一一对应。
|
|
||||||
|
|
Loading…
Reference in New Issue