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

View File

@ -1,5 +1,4 @@
import { defineAppConfig } from 'ice'; import { defineAppConfig, defineDataLoader } from 'ice';
import type { GetAppData } from 'ice';
export default defineAppConfig(() => ({ export default defineAppConfig(() => ({
app: { app: {
@ -7,7 +6,7 @@ export default defineAppConfig(() => ({
}, },
})); }));
export const getAppData: GetAppData = () => { export const dataLoader = defineDataLoader(() => {
return new Promise((resolve) => { return new Promise((resolve) => {
resolve({ resolve({
title: 'gogogogo', 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) => { return new Promise((resolve) => {
resolve({ resolve({
success: true, success: true,
id: 34293, id: 34293,
}); });
}); });
}; });
export const miniappManifest = { export const miniappManifest = {
title: 'miniapp test', title: 'miniapp test',

View File

@ -1,5 +1,4 @@
import { defineAppConfig, Link } from 'ice'; import { defineAppConfig, defineDataLoader, Link } from 'ice';
import type { GetAppData } from 'ice';
import { defineAuthConfig } from '@ice/plugin-auth/esm/types'; import { defineAuthConfig } from '@ice/plugin-auth/esm/types';
export default defineAppConfig(() => ({})); export default defineAppConfig(() => ({}));
@ -21,7 +20,7 @@ export const authConfig = defineAuthConfig((data) => {
}; };
}); });
export const getAppData: GetAppData = () => { export const dataLoader = defineDataLoader(() => {
return new Promise((resolve) => { return new Promise((resolve) => {
resolve({ resolve({
auth: { 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'; import { defineRequestConfig } from '@ice/plugin-request/esm/types';
export async function getAppData() { export const dataLader = defineDataLoader(async () => {
try { try {
return await requestAPI('/user'); return await requestAPI('/user');
} catch (err) { } catch (err) {
console.log('request error', err); console.log('request error', err);
} }
} });
export default { export default {
app: { app: {

View File

@ -1,5 +1,4 @@
import type { GetAppData } from 'ice'; import { defineAppConfig, defineDataLoader } from 'ice';
import { defineAppConfig } from 'ice';
import { defineStoreConfig } from '@ice/plugin-store/esm/types'; import { defineStoreConfig } from '@ice/plugin-store/esm/types';
export const store = defineStoreConfig(async (appData) => { 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) => { return new Promise((resolve) => {
resolve({ resolve({
user: { user: {
@ -18,6 +17,6 @@ export const getAppData: GetAppData = () => {
}, },
}); });
}); });
}; });
export default defineAppConfig(() => ({})); 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 coreEnvKeys = getCoreEnvKeys();
const routesInfo = await generateRoutesInfo(rootDir, routesConfig); 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 csr = !userConfig.ssr && !userConfig.ssg;
const disableRouter = userConfig?.optimization?.router && routesInfo.routesCount <= 1; 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)/, /src\/app.(js|jsx|ts|tsx)/,
async (event: string) => { async (event: string) => {
if (event === 'change') { 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')) { if (hasExportAppData !== !!cache.get('hasExportAppData')) {
cache.set('hasExportAppData', hasExportAppData ? 'true' : ''); cache.set('hasExportAppData', hasExportAppData ? 'true' : '');
renderExportsTemplate({ renderExportsTemplate({

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import type { AppExport, AppData, RequestContext } from './types.js'; import type { AppExport, AppData, RequestContext } from './types.js';
import { callDataLoader } from './dataLoader.js';
const Context = React.createContext<AppData | undefined>(undefined); const Context = React.createContext<AppData | undefined>(undefined);
@ -23,9 +24,15 @@ async function getAppData(appExport: AppExport, requestContext?: RequestContext)
return await globalLoader.getData('__app'); return await globalLoader.getData('__app');
} }
if (appExport?.getAppData) { if (appExport?.dataLoader) {
return await appExport.getAppData(requestContext); return await appExport.dataLoader(requestContext);
} }
const loader = appExport?.dataLoader;
if (!loader) return null;
await callDataLoader(loader, requestContext);
} }
export { 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 getRequestContext from './requestContext.js';
import { setFetcher, loadDataByCustomFetcher } from './dataLoaderFetcher.js';
interface Loaders { interface Loaders {
[routeId: string]: DataLoaderConfig; [routeId: string]: DataLoaderConfig;
} }
interface Result { interface CachedResult {
value: any; value: any;
status: string; status: string;
} }
interface LoaderOptions {
fetcher: Function;
runtimeModules: RuntimeModules['statics'];
appExport: AppExport;
}
export function defineDataLoader(dataLoaderConfig: DataLoaderConfig): DataLoaderConfig { export function defineDataLoader(dataLoaderConfig: DataLoaderConfig): DataLoaderConfig {
return dataLoaderConfig; return dataLoaderConfig;
} }
@ -23,12 +28,43 @@ export function defineStaticDataLoader(dataLoaderConfig: DataLoaderConfig): Data
return dataLoaderConfig; 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 context = (window as any).__ICE_APP_CONTEXT__ || {};
const matchedIds = context.matchedIds || []; const matchedIds = context.matchedIds || [];
const routesData = context.routesData || {}; const routesData = context.routesData || {};
@ -48,22 +84,7 @@ function loadInitialData(loaders: Loaders) {
if (dataLoader) { if (dataLoader) {
const requestContext = getRequestContext(window.location); const requestContext = getRequestContext(window.location);
const loader = callDataLoader(dataLoader, requestContext);
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);
}
cache.set(id, { cache.set(id, {
value: loader, 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. * 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(loadersConfig: Loaders, options: Options) { async function init(loadersConfig: Loaders, options: LoaderOptions) {
const { const {
fetcher, fetcher,
runtimeModules, runtimeModules,
@ -108,7 +124,7 @@ async function init(loadersConfig: Loaders, options: Options) {
} }
try { try {
loadInitialData(loadersConfig); loadInitialDataInClient(loadersConfig);
} catch (error) { } catch (error) {
console.error('Load initial data error: ', 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, AppProvider,
RouteWrapper, RouteWrapper,
RenderMode, RenderMode,
GetAppData,
DataLoaderConfig, DataLoaderConfig,
RouteWrapperConfig, RouteWrapperConfig,
} from './types.js'; } from './types.js';
@ -94,7 +93,6 @@ export type {
AppProvider, AppProvider,
RouteWrapper, RouteWrapper,
RenderMode, RenderMode,
GetAppData,
DataLoaderConfig, DataLoaderConfig,
RunClientAppOptions, 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 type { RouteItem, RouteModules, RouteWrapperConfig, RouteMatch, RequestContext, RoutesConfig, RoutesData, RenderMode } from './types.js';
import RouteWrapper from './RouteWrapper.js'; import RouteWrapper from './RouteWrapper.js';
import { useAppContext } from './AppContext.js'; import { useAppContext } from './AppContext.js';
import { loadDataByCustomFetcher } from './dataLoaderFetcher.js'; import { callDataLoader } from './dataLoader.js';
type RouteModule = Pick<RouteItem, 'id' | 'load'>; type RouteModule = Pick<RouteItem, 'id' | 'load'>;
@ -79,16 +79,8 @@ export async function loadRoutesData(
loader = dataLoader; loader = dataLoader;
} }
if (Array.isArray(loader)) { if (loader) {
routesData[id] = await Promise.all(loader.map(load => { routesData[id] = await callDataLoader(loader, requestContext);
if (typeof load === 'object') {
return loadDataByCustomFetcher(load);
}
return load(requestContext);
}));
} else if (loader) {
routesData[id] = await loader(requestContext);
} }
}), }),
); );

View File

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

View File

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

View File

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

View File

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