refactor: app data loader (#689)

* refactor: getAppData to dataLoader

* refactor: merge data loader logic in one file

* fix: comments
This commit is contained in:
水澜 2022-11-14 18:09:04 +08:00 committed by ClarkXia
parent e21fb48509
commit 6ce0835d9b
21 changed files with 92 additions and 147 deletions

View File

@ -1,6 +1,5 @@
import { defineAppConfig } from 'ice';
import { defineAppConfig, defineDataLoader } from 'ice';
import { isWeb, isNode } from '@uni/env';
import type { GetAppData } from 'ice';
if (process.env.ICE_CORE_ERROR_BOUNDARY === 'true') {
console.error('__REMOVED__');
@ -26,10 +25,10 @@ export default defineAppConfig(() => ({
},
}));
export const getAppData: GetAppData = () => {
export const dataLoader = defineDataLoader(() => {
return new Promise((resolve) => {
resolve({
title: 'gogogogo',
});
});
};
});

View File

@ -1,5 +1,4 @@
import { defineAppConfig } from 'ice';
import type { GetAppData } from 'ice';
import { defineAppConfig, defineDataLoader } from 'ice';
export default defineAppConfig(() => ({
app: {
@ -7,7 +6,7 @@ export default defineAppConfig(() => ({
},
}));
export const getAppData: GetAppData = () => {
export const dataLoader = defineDataLoader(() => {
return new Promise((resolve) => {
resolve({
title: 'gogogogo',
@ -16,4 +15,4 @@ export const getAppData: GetAppData = () => {
},
});
});
};
});

View File

@ -1,13 +1,13 @@
import { defineAppConfig, type GetAppData } from 'ice';
import { defineAppConfig, defineDataLoader } from 'ice';
export const getAppData: GetAppData = () => {
export const dataLoader = defineDataLoader(() => {
return new Promise((resolve) => {
resolve({
success: true,
id: 34293,
});
});
};
});
export const miniappManifest = {
title: 'miniapp test',

View File

@ -1,5 +1,4 @@
import { defineAppConfig, Link } from 'ice';
import type { GetAppData } from 'ice';
import { defineAppConfig, defineDataLoader, Link } from 'ice';
import { defineAuthConfig } from '@ice/plugin-auth/esm/types';
export default defineAppConfig(() => ({}));
@ -21,7 +20,7 @@ export const authConfig = defineAuthConfig((data) => {
};
});
export const getAppData: GetAppData = () => {
export const dataLoader = defineDataLoader(() => {
return new Promise((resolve) => {
resolve({
auth: {
@ -29,4 +28,4 @@ export const getAppData: GetAppData = () => {
},
});
});
};
});

View File

@ -1,13 +1,13 @@
import { request as requestAPI } from 'ice';
import { request as requestAPI, defineDataLoader } from 'ice';
import { defineRequestConfig } from '@ice/plugin-request/esm/types';
export async function getAppData() {
export const dataLader = defineDataLoader(async () => {
try {
return await requestAPI('/user');
} catch (err) {
console.log('request error', err);
}
}
});
export default {
app: {

View File

@ -1,5 +1,4 @@
import type { GetAppData } from 'ice';
import { defineAppConfig } from 'ice';
import { defineAppConfig, defineDataLoader } from 'ice';
import { defineStoreConfig } from '@ice/plugin-store/esm/types';
export const store = defineStoreConfig(async (appData) => {
@ -10,7 +9,7 @@ export const store = defineStoreConfig(async (appData) => {
};
});
export const getAppData: GetAppData = () => {
export const dataLoader = defineDataLoader(() => {
return new Promise((resolve) => {
resolve({
user: {
@ -18,6 +17,6 @@ export const getAppData: GetAppData = () => {
},
});
});
};
});
export default defineAppConfig(() => ({}));

View File

@ -1,19 +0,0 @@
import { defineAppConfig } from '../disable-data-loader/.ice';
import type { GetAppData } from '../disable-data-loader/.ice';
export default defineAppConfig(() => ({
app: {
rootId: 'app',
},
}));
export const getAppData: GetAppData = () => {
return new Promise((resolve) => {
resolve({
title: 'gogogogo',
auth: {
admin: true,
},
});
});
};

View File

@ -1,32 +0,0 @@
{
"compileOnSave": false,
"buildOnSave": false,
"compilerOptions": {
"baseUrl": "../disable-data-loader",
"outDir": "../disable-data-loader/build",
"module": "esnext",
"target": "es6",
"jsx": "react-jsx",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"lib": ["es6", "dom"],
"sourceMap": true,
"allowJs": true,
"rootDir": "../disable-data-loader",
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitAny": false,
"importHelpers": true,
"strictNullChecks": true,
"suppressImplicitAnyIndexErrors": true,
"noUnusedLocals": true,
"skipLibCheck": true,
"paths": {
"@/*": ["../disable-data-loader/src/*"],
"ice": ["../disable-data-loader/.ice"]
}
},
"include": ["../disable-data-loader/src", "../disable-data-loader/.ice", "../disable-data-loader/ice.config.*"],
"exclude": ["../disable-data-loader/node_modules", "../disable-data-loader/build", "../disable-data-loader/public"]
}

View File

@ -141,7 +141,7 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt
const coreEnvKeys = getCoreEnvKeys();
const routesInfo = await generateRoutesInfo(rootDir, routesConfig);
const hasExportAppData = (await getFileExports({ rootDir, file: 'src/app' })).includes('getAppData');
const hasExportAppData = (await getFileExports({ rootDir, file: 'src/app' })).includes('dataLoader');
const csr = !userConfig.ssr && !userConfig.ssg;
const disableRouter = userConfig?.optimization?.router && routesInfo.routesCount <= 1;

View File

@ -92,7 +92,7 @@ const getWatchEvents = (options: Options): WatchEvent[] => {
/src\/app.(js|jsx|ts|tsx)/,
async (event: string) => {
if (event === 'change') {
const hasExportAppData = (await getFileExports({ rootDir, file: 'src/app' })).includes('getAppData');
const hasExportAppData = (await getFileExports({ rootDir, file: 'src/app' })).includes('dataLoader');
if (hasExportAppData !== !!cache.get('hasExportAppData')) {
cache.set('hasExportAppData', hasExportAppData ? 'true' : '');
renderExportsTemplate({

View File

@ -33,7 +33,7 @@ export default class DataLoaderPlugin {
public apply(compiler: Compiler) {
const plugins = this.getAllPlugin(['keepExports']) as PluginData[];
let keepExports = ['dataLoader', 'getAppData'];
let keepExports = ['dataLoader'];
plugins.forEach(plugin => {
if (plugin.keepExports) {
keepExports = keepExports.concat(plugin.keepExports);

View File

@ -17,7 +17,7 @@ import * as app from '@/app';
const loaders = {};
<% } -%>
<% if(hasExportAppData) {-%>loaders['__app'] = app.getAppData;<% } -%>
<% if(hasExportAppData) {-%>loaders['__app'] = app.dataLoader;<% } -%>
<% if(!dataLoaderImport.imports) {-%>
let fetcher = (options) => {

View File

@ -1,5 +1,6 @@
import * as React from 'react';
import type { AppExport, AppData, RequestContext } from './types.js';
import { callDataLoader } from './dataLoader.js';
const Context = React.createContext<AppData | undefined>(undefined);
@ -23,9 +24,15 @@ async function getAppData(appExport: AppExport, requestContext?: RequestContext)
return await globalLoader.getData('__app');
}
if (appExport?.getAppData) {
return await appExport.getAppData(requestContext);
if (appExport?.dataLoader) {
return await appExport.dataLoader(requestContext);
}
const loader = appExport?.dataLoader;
if (!loader) return null;
await callDataLoader(loader, requestContext);
}
export {

View File

@ -1,16 +1,21 @@
import type { DataLoaderConfig, RuntimeModules, AppExport, RuntimePlugin, CommonJsRuntime } from './types.js';
import type { DataLoaderConfig, DataLoaderResult, RuntimeModules, AppExport, RuntimePlugin, CommonJsRuntime } from './types.js';
import getRequestContext from './requestContext.js';
import { setFetcher, loadDataByCustomFetcher } from './dataLoaderFetcher.js';
interface Loaders {
[routeId: string]: DataLoaderConfig;
}
interface Result {
interface CachedResult {
value: any;
status: string;
}
interface LoaderOptions {
fetcher: Function;
runtimeModules: RuntimeModules['statics'];
appExport: AppExport;
}
export function defineDataLoader(dataLoaderConfig: DataLoaderConfig): DataLoaderConfig {
return dataLoaderConfig;
}
@ -23,12 +28,43 @@ export function defineStaticDataLoader(dataLoaderConfig: DataLoaderConfig): Data
return dataLoaderConfig;
}
const cache = new Map<string, Result>();
/**
* Custom fetcher for load static data loader config.
* Set globally to avoid passing this fetcher too deep.
*/
let dataLoaderFetcher;
export function setFetcher(customFetcher) {
dataLoaderFetcher = customFetcher;
}
export function loadDataByCustomFetcher(config) {
return dataLoaderFetcher(config);
}
/**
* Start getData once loader is ready, and set to cache.
* Handle for different dataLoader.
*/
function loadInitialData(loaders: Loaders) {
export function callDataLoader(dataLoader: DataLoaderConfig, requestContext): DataLoaderResult {
if (Array.isArray(dataLoader)) {
return dataLoader.map(loader => {
return typeof loader === 'object' ? loadDataByCustomFetcher(loader) : loader(requestContext);
});
}
if (typeof dataLoader === 'object') {
return loadDataByCustomFetcher(dataLoader);
}
return dataLoader(requestContext);
}
const cache = new Map<string, CachedResult>();
/**
* Start getData once data-loader.js is ready in client, and set to cache.
*/
function loadInitialDataInClient(loaders: Loaders) {
const context = (window as any).__ICE_APP_CONTEXT__ || {};
const matchedIds = context.matchedIds || [];
const routesData = context.routesData || {};
@ -48,22 +84,7 @@ function loadInitialData(loaders: Loaders) {
if (dataLoader) {
const requestContext = getRequestContext(window.location);
let loader;
if (Array.isArray(dataLoader)) {
loader = dataLoader.map(loader => {
if (typeof loader === 'object') {
return loadDataByCustomFetcher(loader);
}
return loader(requestContext);
});
} else if (typeof dataLoader === 'object') {
return loadDataByCustomFetcher(loader);
} else {
loader = dataLoader(requestContext);
}
const loader = callDataLoader(dataLoader, requestContext);
cache.set(id, {
value: loader,
@ -73,17 +94,12 @@ function loadInitialData(loaders: Loaders) {
});
}
interface Options {
fetcher: Function;
runtimeModules: RuntimeModules['statics'];
appExport: AppExport;
}
/**
* Init data loader in client side.
* Load initial data and register global loader.
* In order to load data, JavaScript modules, CSS and other assets in parallel.
*/
async function init(loadersConfig: Loaders, options: Options) {
async function init(loadersConfig: Loaders, options: LoaderOptions) {
const {
fetcher,
runtimeModules,
@ -108,7 +124,7 @@ async function init(loadersConfig: Loaders, options: Options) {
}
try {
loadInitialData(loadersConfig);
loadInitialDataInClient(loadersConfig);
} catch (error) {
console.error('Load initial data error: ', error);
}

View File

@ -1,14 +0,0 @@
/**
* custom fetcher for load static data loader config
* set globally to avoid passing this fetcher too deep
*/
let fetcher;
export function setFetcher(customFetcher) {
fetcher = customFetcher;
}
export function loadDataByCustomFetcher(config) {
return fetcher(config);
}

View File

@ -15,7 +15,6 @@ import type {
AppProvider,
RouteWrapper,
RenderMode,
GetAppData,
DataLoaderConfig,
RouteWrapperConfig,
} from './types.js';
@ -94,7 +93,6 @@ export type {
AppProvider,
RouteWrapper,
RenderMode,
GetAppData,
DataLoaderConfig,
RunClientAppOptions,
};

View File

@ -3,7 +3,7 @@ import { RouteComponent } from './types.js';
import type { RouteItem, RouteModules, RouteWrapperConfig, RouteMatch, RequestContext, RoutesConfig, RoutesData, RenderMode } from './types.js';
import RouteWrapper from './RouteWrapper.js';
import { useAppContext } from './AppContext.js';
import { loadDataByCustomFetcher } from './dataLoaderFetcher.js';
import { callDataLoader } from './dataLoader.js';
type RouteModule = Pick<RouteItem, 'id' | 'load'>;
@ -79,16 +79,8 @@ export async function loadRoutesData(
loader = dataLoader;
}
if (Array.isArray(loader)) {
routesData[id] = await Promise.all(loader.map(load => {
if (typeof load === 'object') {
return loadDataByCustomFetcher(load);
}
return load(requestContext);
}));
} else if (loader) {
routesData[id] = await loader(requestContext);
if (loader) {
routesData[id] = await callDataLoader(loader, requestContext);
}
}),
);

View File

@ -18,7 +18,7 @@ import getRequestContext from './requestContext.js';
import getAppConfig from './appConfig.js';
import matchRoutes from './matchRoutes.js';
import DefaultAppRouter from './AppRouter.js';
import { setFetcher } from './dataLoaderFetcher.js';
import { setFetcher } from './dataLoader.js';
export interface RunClientAppOptions {
app: AppExport;

View File

@ -31,12 +31,11 @@ export type RouteConfig<T = {}> = T & {
export interface AppExport {
default?: AppConfig;
[key: string]: any;
getAppData?: GetAppData;
dataLoader?: DataLoader;
}
export type GetAppData = (ctx: RequestContext) => (Promise<AppData> | AppData);
export type DataLoader = (ctx: RequestContext) => (Promise<RouteData> | RouteData) | RouteData;
export type DataLoaderResult = (Promise<RouteData> | RouteData) | RouteData;
export type DataLoader = (ctx: RequestContext) => DataLoaderResult;
interface StaticDataLoader {
key?: string;

View File

@ -101,7 +101,7 @@ describe('run client app', () => {
it('run with static runtime', async () => {
await runClientApp({
app: {
getAppData: async () => {
dataLoader: async () => {
return { msg: staticMsg };
},
},
@ -258,7 +258,7 @@ describe('run client app', () => {
let executed = false;
await runClientApp({
app: {
getAppData: async () => {
dataLoader: async () => {
executed = true;
return { msg: '-getAppData' };
},
@ -287,7 +287,7 @@ describe('run client app', () => {
await runClientApp({
app: {
getAppData: async () => {
dataLoader: async () => {
executed = true;
return { msg: 'app' };
},

View File

@ -140,12 +140,14 @@ function Home() {
### useAppData
useAppData 返回应用全局数据,需要搭配 `src/app.ts` 中导出的 getAppData 使用:
useAppData 返回应用全局数据,需要搭配 `src/app.ts` 中导出的 `dataLoader` 使用:
```ts title="src/app.ts"
export async function getAppData() {
import { defineDataLoader } from 'ice';
export const dataLoader = defineDataLoader(() => {
return await fetch('/api/user');
}
})
```
在任意组件内进行消费: