feat: getAppData (#375)

* feat: getAppData

* fix: remove dead code

* fix: should remove default export for app entry too

* docs: app data
This commit is contained in:
水澜 2022-07-21 19:04:51 +08:00 committed by ClarkXia
parent 7661e2967f
commit 00f75371ed
15 changed files with 153 additions and 14 deletions

View File

@ -1,6 +1,7 @@
import { defineAppConfig } from 'ice';
import { defineAuthConfig } from '@ice/plugin-auth/esm/types';
import { isWeb, isNode } from '@uni/env';
import type { GetAppData } from 'ice';
if (process.env.ICE_CORE_ERROR_BOUNDARY === 'true') {
console.error('__REMOVED__');
@ -33,3 +34,14 @@ export default defineAppConfig({
rootId: 'app',
},
});
export const getAppData: GetAppData = () => {
return new Promise((resolve) => {
resolve({
title: 'gogogogo',
auth: {
admin: true,
},
});
});
};

View File

@ -1,6 +1,9 @@
import { Meta, Title, Links, Main, Scripts } from 'ice';
import { Meta, Title, Links, Main, Scripts, useAppData } from 'ice';
import type { AppData } from 'ice';
function Document() {
const appData = useAppData<AppData>();
return (
<html>
<head>
@ -10,6 +13,11 @@ function Document() {
<Meta />
<Title />
<Links />
<script
dangerouslySetInnerHTML={{
__html: `console.log('${appData?.title}')`,
}}
/>
</head>
<body>
<Main />

View File

@ -1,19 +1,22 @@
import { Suspense, lazy } from 'react';
import { Link, useData, useConfig } from 'ice';
import { Link, useData, useAppData, useConfig } from 'ice';
// not recommended but works
import { useAppContext } from '@ice/runtime';
import { useRequest } from 'ahooks';
import type { AppData } from 'ice';
import styles from './index.module.css';
const Bar = lazy(() => import('../components/bar'));
export default function Home(props) {
const appContext = useAppContext();
const appData = useAppData<AppData>();
const data = useData();
const config = useConfig();
if (typeof window !== 'undefined') {
console.log('render Home', props);
console.log('get AppData', appData);
console.log('get AppContext', appContext);
console.log('render Home', 'data', data, 'config', config);
}

View File

@ -1,4 +1,8 @@
import { dataLoader } from '@ice/runtime';
import { getAppData } from '@/app';
<%- loaders %>
loaders['__app'] = getAppData;
dataLoader.init(loaders);

View File

@ -44,6 +44,7 @@ export {
export {
defineAppConfig,
useAppData,
useData,
useConfig,
Meta,

View File

@ -1,4 +1,4 @@
import type { AppConfig as DefaultAppConfig } from '@ice/runtime';
import type { AppConfig as DefaultAppConfig, GetAppData, AppData } from '@ice/runtime';
<%- configTypes.imports -%>
@ -13,3 +13,8 @@ export type AppConfig = ExtendsAppConfig;
<% } else { -%>
export type AppConfig = DefaultAppConfig;
<% } -%>
export {
GetAppData,
AppData
}

View File

@ -0,0 +1,35 @@
import * as React from 'react';
import type { AppExport, AppData, RequestContext } from './types.js';
const Context = React.createContext<AppData | undefined>(undefined);
Context.displayName = 'AppDataContext';
function useAppData <T = AppData>(): T {
const value = React.useContext(Context);
return value;
}
const AppDataProvider = Context.Provider;
/**
* Call the getData of app config.
*/
async function getAppData(appExport: AppExport, requestContext: RequestContext): Promise<AppData> {
const hasGlobalLoader = typeof window !== 'undefined' && (window as any).__ICE_DATA_LOADER__;
if (hasGlobalLoader) {
const load = (window as any).__ICE_DATA_LOADER__;
return await load('__app');
}
if (appExport?.getAppData) {
return await appExport.getAppData(requestContext);
}
}
export {
getAppData,
useAppData,
AppDataProvider,
};

View File

@ -1,6 +1,7 @@
import * as React from 'react';
import type { ReactNode } from 'react';
import { useAppContext } from './AppContext.js';
import { useAppData } from './AppData.js';
import { getMeta, getTitle, getLinks, getScripts } from './routesConfig.js';
import type { AppContext, RouteMatch, AssetsManifest } from './types.js';
import getCurrentRoutePath from './utils/getCurrentRoutePath.js';
@ -64,6 +65,7 @@ export function Links() {
export function Scripts() {
const { routesData, routesConfig, matches, assetsManifest, documentOnly, routeModules, basename } = useAppContext();
const appData = useAppData();
const routeScripts = getScripts(matches, routesConfig);
const pageAssets = getPageAssets(matches, assetsManifest);
@ -75,6 +77,7 @@ export function Scripts() {
const routePath = getCurrentRoutePath(matches);
const appContext: AppContext = {
appData,
routesData,
routesConfig,
assetsManifest,

View File

@ -16,6 +16,7 @@ import Runtime from './runtime.js';
import App from './App.js';
import runClientApp from './runClientApp.js';
import { useAppContext } from './AppContext.js';
import { useAppData } from './AppData.js';
import { useData, useConfig } from './RouteContext.js';
import {
Meta,
@ -34,6 +35,7 @@ import type {
AppProvider,
RouteWrapper,
RenderMode,
GetAppData,
} from './types.js';
import dataLoader from './dataLoader.js';
import getAppConfig, { defineAppConfig } from './appConfig.js';
@ -45,6 +47,7 @@ export {
App,
runClientApp,
useAppContext,
useAppData,
useData,
useConfig,
Meta,
@ -77,4 +80,5 @@ export type {
AppProvider,
RouteWrapper,
RenderMode,
GetAppData,
};

View File

@ -6,6 +6,7 @@ import { createHistorySingle } from './utils/history-single.js';
import Runtime from './runtime.js';
import App from './App.js';
import { AppContextProvider } from './AppContext.js';
import { AppDataProvider, getAppData } from './AppData.js';
import type {
AppContext, AppExport, RouteItem, AppRouterProps, RoutesData, RoutesConfig,
RouteWrapperConfig, RuntimeModules, RouteMatch, RouteModules, AppConfig, DocumentComponent,
@ -38,6 +39,7 @@ export default async function runClientApp(options: RunClientAppOptions) {
} = options;
const appContextFromServer: AppContext = (window as any).__ICE_APP_CONTEXT__ || {};
let {
appData,
routesData,
routesConfig,
assetsManifest,
@ -47,6 +49,10 @@ export default async function runClientApp(options: RunClientAppOptions) {
const requestContext = getRequestContext(window.location);
if (!appData) {
appData = await getAppData(app, requestContext);
}
const appConfig = getAppConfig(app);
const basename = basenameFromServer || defaultBasename;
@ -68,6 +74,7 @@ export default async function runClientApp(options: RunClientAppOptions) {
appExport: app,
routes,
appConfig,
appData,
routesData,
routesConfig,
assetsManifest,
@ -172,6 +179,7 @@ function BrowserEntry({
routesConfig: initialRoutesConfig,
routeModules: initialRouteModules,
basename,
appData,
} = appContext;
const [historyState, setHistoryState] = useState<HistoryState>({
@ -225,12 +233,14 @@ function BrowserEntry({
return (
<AppContextProvider value={appContext}>
<App
action={action}
location={location}
navigator={history}
{...rest}
/>
<AppDataProvider value={appData}>
<App
action={action}
location={location}
navigator={history}
{...rest}
/>
</AppDataProvider>
</AppContextProvider>
);
}

View File

@ -6,6 +6,7 @@ import type { Location } from 'history';
import Runtime from './runtime.js';
import App from './App.js';
import { AppContextProvider } from './AppContext.js';
import { AppDataProvider, getAppData } from './AppData.js';
import getAppConfig from './appConfig.js';
import { DocumentContextProvider } from './Document.js';
import { loadRouteModules, loadRoutesData, getRoutesConfig } from './routes.js';
@ -14,6 +15,7 @@ import { createStaticNavigator } from './server/navigator.js';
import type { NodeWritablePiper } from './server/streamRender.js';
import type {
AppContext, RouteItem, ServerContext,
AppData,
AppExport, RuntimePlugin, CommonJsRuntime, AssetsManifest,
RouteMatch,
RequestContext,
@ -132,6 +134,13 @@ async function doRender(serverContext: ServerContext, renderOptions: RenderOptio
const location = getLocation(req.url);
const requestContext = getRequestContext(location, serverContext);
let appData;
// don't need to execute getAppData in CSR
if (!documentOnly) {
appData = await getAppData(app, requestContext);
}
const appConfig = getAppConfig(app);
const matches = matchRoutes(routes, location, serverOnlyBasename || basename);
const routePath = getCurrentRoutePath(matches);
@ -156,6 +165,7 @@ async function doRender(serverContext: ServerContext, renderOptions: RenderOptio
matches,
location,
appConfig,
appData,
routeModules,
basename,
routePath,
@ -181,6 +191,7 @@ interface renderServerEntry {
matches: RouteMatch[];
location: Location;
appConfig: AppConfig;
appData: AppData;
routeModules: RouteModules;
routePath: string;
basename?: string;
@ -196,6 +207,7 @@ async function renderServerEntry(
matches,
location,
appConfig,
appData,
renderOptions,
routeModules,
basename,
@ -217,6 +229,7 @@ async function renderServerEntry(
appExport,
assetsManifest,
appConfig,
appData,
routesData,
routesConfig,
matches,
@ -248,9 +261,11 @@ async function renderServerEntry(
const element = (
<AppContextProvider value={appContext}>
<DocumentContextProvider value={documentContext}>
<Document pagePath={routePath} />
</DocumentContextProvider>
<AppDataProvider value={appData}>
<DocumentContextProvider value={documentContext}>
<Document pagePath={routePath} />
</DocumentContextProvider>
</AppDataProvider>
</AppContextProvider>
);
@ -286,12 +301,14 @@ function renderDocument(
} = options;
const routesData = null;
const appData = null;
const appConfig = getAppConfig(app);
const routesConfig = getRoutesConfig(matches, {}, routeModules);
const appContext: AppContext = {
assetsManifest,
appConfig,
appData,
routesData,
routesConfig,
matches,

View File

@ -31,8 +31,11 @@ export interface RouteConfig {
export interface AppExport {
default?: AppConfig;
[key: string]: any;
getAppData?: GetAppData;
}
export type GetAppData = (ctx: RequestContext) => Promise<AppData> | AppData;
// app.getData & route.getData
export type GetData = (ctx: RequestContext) => Promise<RouteData> | RouteData;
export type GetServerData = (ctx: RequestContext) => Promise<RouteData> | RouteData;
@ -60,6 +63,7 @@ export interface RoutesData {
// useAppContext
export interface AppContext {
appConfig: AppConfig;
appData: any;
assetsManifest: AssetsManifest;
routesData: RoutesData;
routesConfig: RoutesConfig;

View File

@ -64,7 +64,8 @@ const compilationPlugin = (options: Options): UnpluginOptions => {
merge(programmaticOptions, compilationConfig);
}
if (removeExportExprs && /(.*)pages(.*)\.(jsx?|tsx?|mjs)$/.test(id)) {
// handle app.tsx and page entries only
if (removeExportExprs && (/(.*)pages(.*)\.(jsx?|tsx?|mjs)$/.test(id) || /(.*)src\/app/.test(id))) {
merge(programmaticOptions, {
jsc: {
experimental: {

View File

@ -64,6 +64,38 @@ export default defineAppConfig({
- 类型 `string`
- 默认值 `/`
## 应用级数据
可以在应用入口定义并导出 `getAppData` 方法,来获取应用级数据。示例:
```js
import type { GetAppData } from 'ice';
// ...
export const getAppData: GetAppData = () => {
return new Promise((resolve) => {
resolve({
success: true,
id: 34293
});
});
};
```
在页面或其他组件中,可以通过 `useAppData` 方法获取页面级数据。示例:
```js
import { useAppData } from 'ice';
import type { AppData } from 'ice';
export default function Home(props) {
const appData = useAppData<AppData>();
// ...
}
```
## 运行时拓展
应用入口除了支持定义应用配置之外,同时也承担运行时扩展的能力,比如权限配置:

View File

@ -41,7 +41,7 @@ export async function getData() {
}
// Server 端专用的数据请求
export async function getStaticData() {
export async function getServerData() {
const data = await sendRequestInServer();
return data;