Skip to content

Commit

Permalink
feat: async data loader (#6137)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
chenjun1011 and ClarkXia authored Apr 25, 2023
1 parent 7588052 commit f56497f
Show file tree
Hide file tree
Showing 15 changed files with 451 additions and 101 deletions.
35 changes: 35 additions & 0 deletions examples/with-data-loader/src/pages/with-defer-loader.tsx
Original file line number Diff line number Diff line change
@@ -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 });

52 changes: 52 additions & 0 deletions examples/with-data-loader/src/pages/with-defer-loaders.tsx
Original file line number Diff line number Diff line change
@@ -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 });

45 changes: 45 additions & 0 deletions examples/with-data-loader/src/pages/with-ssr.tsx
Original file line number Diff line number Diff line change
@@ -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;
});
1 change: 1 addition & 0 deletions packages/ice/src/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export const RUNTIME_EXPORTS = [
'ClientOnly',
'withSuspense',
'useSuspenseData',
'Await',
'defineDataLoader',
'defineServerDataLoader',
'defineStaticDataLoader',
Expand Down
16 changes: 11 additions & 5 deletions packages/runtime/src/appData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
86 changes: 39 additions & 47 deletions packages/runtime/src/dataLoader.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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,
};
}

/**
Expand Down Expand Up @@ -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') {
Expand All @@ -156,23 +167,22 @@ function loadInitialDataInClient(loaders: Loaders) {
if (dataFromSSR) {
cache.set(renderMode === 'SSG' ? `${id}_ssg` : id, {
value: dataFromSSR,
status: 'RESOLVED',
});

if (renderMode === 'SSR') {
return;
}
}

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,
});
}
});
Expand All @@ -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,
Expand All @@ -208,57 +218,39 @@ async function init(dataloaderConfig: Loaders, options: LoaderOptions) {
}

try {
loadInitialDataInClient(dataloaderConfig);
loadInitialDataInClient(loaders);
} catch (error) {
console.error('Load initial data error: ', error);
}

(window as any).__ICE_DATA_LOADER__ = {
getData: async (id, options: LoadRoutesDataOptions) => {
getData: (id, options: LoadRoutesDataOptions) => {
let result;

// first render for ssg use data from build time.
// second render for ssg will use data from data loader.
// First render for ssg use data from build time, second render for ssg will use data from data loader.
const cacheKey = `${id}${options?.renderMode === 'SSG' ? '_ssg' : ''}`;

// In CSR, all dataLoader is called by global data loader to avoid bundle dataLoader in page bundle duplicate.
result = cache.get(cacheKey);
// Always fetch new data after cache is been used.
cache.delete(cacheKey);

// Already send data request.
if (result) {
const { status, value } = result;

if (status === 'RESOLVED') {
return result;
}

try {
if (Array.isArray(value)) {
return await Promise.all(value);
}

return await value;
} catch (error) {
console.error('DataLoader: getData error.\n', error);

return {
message: 'DataLoader: getData error.',
error,
};
}
return result.value;
}

const dataLoader = dataloaderConfig[id];
const dataLoaderConfig = loaders[id];

// No data loader.
if (!dataLoader) {
if (!dataLoaderConfig) {
return null;
}

// Call dataLoader.
// In CSR, all dataLoader is called by global data loader to avoid bundle dataLoader in page bundle duplicate.
const requestContext = getRequestContext(window.location);
return await callDataLoader(dataLoader, requestContext);
const { loader } = dataLoaderConfig;
return callDataLoader(loader, requestContext);
},
};
}
Expand Down
8 changes: 5 additions & 3 deletions packages/runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import type {
RouteWrapper,
RenderMode,
DistType,
DataLoaderConfig,
Loader,
RouteWrapperConfig,
} from './types.js';
import Runtime from './runtime.js';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -92,6 +92,8 @@ export {
withSuspense,
useSuspenseData,

Await,

createRouteLoader,
WrapRouteComponent,
RouteErrorComponent,
Expand All @@ -109,7 +111,7 @@ export type {
RouteWrapper,
RenderMode,
DistType,
DataLoaderConfig,
Loader,
RunClientAppOptions,
MetaType,
TitleType,
Expand Down
Loading

0 comments on commit f56497f

Please sign in to comment.