feat: use scripts (#6238)

* feat: build for production

* feat: use scripts

* revert: production config

* feat: use render mode

* test: use scripts

* fix: export

* fix: exports

* fix: lint

* revert: lock

* feat: export useAppContext

* fix: type

* fix: type

* refactor: hooks name

* fix: type

* refactor: remove useRenderMode

* refactor: remove useRenderMode

* refactor: function name

* fix: export app config

* refactor: app context

* chore: add changelog

* chore: remove demo code
This commit is contained in:
水澜 2023-06-05 16:01:11 +08:00 committed by GitHub
parent f839c67d7a
commit 474e19356f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 161 additions and 3 deletions

View File

@ -0,0 +1,7 @@
---
'@ice/runtime': patch
'@ice/app': patch
---
feat: support usePageAssets
feat: export useAppContext

View File

@ -61,6 +61,7 @@ export const RUNTIME_EXPORTS = [
'ClientOnly',
'withSuspense',
'useSuspenseData',
'useAppContext',
'Await',
'defineDataLoader',
'defineServerDataLoader',

View File

@ -18,6 +18,7 @@ const plugin: Plugin = () => ({
'Scripts',
'Data',
'Main',
'usePageAssets',
],
types: [
'MetaType',

View File

@ -137,6 +137,23 @@ export const Scripts: ScriptsType = (props: ScriptsProps) => {
);
};
export function usePageAssets() {
const { loaderData, matches, assetsManifest } = useAppContext();
const routeLinks = getLinks(matches, loaderData);
const routeScripts = getScripts(matches, loaderData);
const pageAssets = getPageAssets(matches, assetsManifest);
const entryAssets = getEntryAssets(assetsManifest);
// Page assets need to be load before entry assets, so when call dynamic import won't cause duplicate js chunk loaded.
const assets = [].concat(routeLinks).concat(routeScripts).concat(pageAssets).concat(entryAssets);
if (assetsManifest.dataLoader) {
assets.unshift(`${assetsManifest.publicPath}${assetsManifest.dataLoader}`);
}
return assets;
}
interface DataProps {
ScriptElement?: React.ComponentType<React.ScriptHTMLAttributes<HTMLScriptElement>> | string;
}

View File

@ -8,6 +8,7 @@ import {
import type {
RuntimePlugin,
AppContext,
PublicAppContext,
AppConfig,
RouteConfig,
RouteItem,
@ -22,7 +23,7 @@ import type {
import Runtime from './runtime.js';
import runClientApp from './runClientApp.js';
import type { RunClientAppOptions } from './runClientApp.js';
import { useAppContext, useAppData, AppContextProvider } from './AppContext.js';
import { useAppContext as useInternalAppContext, useAppData, AppContextProvider } from './AppContext.js';
import { getAppData } from './appData.js';
import { useData, useConfig } from './RouteContext.js';
import {
@ -32,6 +33,7 @@ import {
Scripts,
Main,
Data,
usePageAssets,
} from './Document.js';
import type {
MetaType,
@ -53,6 +55,26 @@ import usePageLifecycle from './usePageLifecycle.js';
import { withSuspense, useSuspenseData } from './Suspense.js';
import { createRouteLoader, WrapRouteComponent, RouteErrorComponent, Await } from './routes.js';
function useAppContext(): PublicAppContext {
const context = useInternalAppContext();
const {
appConfig,
routePath,
downgrade,
documentOnly,
renderMode,
} = context;
return {
appConfig,
routePath,
downgrade,
documentOnly,
renderMode,
};
}
export {
getAppConfig,
defineAppConfig,
@ -73,6 +95,7 @@ export {
Scripts,
Data,
Main,
usePageAssets,
// API for data-loader.
dataLoader,
callDataLoader,

View File

@ -460,6 +460,7 @@ function renderDocument(options: RenderDocumentOptions): Response {
matches,
routes,
documentOnly: true,
renderMode: 'CSR',
routePath,
basename,
downgrade,

View File

@ -111,8 +111,13 @@ export interface AppContext {
revalidate?: boolean;
}
export type PublicAppContext = Pick<
AppContext,
'appConfig' | 'routePath' | 'downgrade' | 'documentOnly' | 'renderMode'
>;
export type WindowContext = Pick<
AppContext,
AppContext,
'appData' | 'loaderData' | 'routePath' | 'downgrade' | 'matchedIds' | 'documentOnly' | 'renderMode' | 'serverData' | 'revalidate'
>;

View File

@ -4,7 +4,8 @@
import React from 'react';
import { expect, it, describe } from 'vitest';
import { renderToHTML, renderToResponse } from '../src/runServerApp';
import { Meta, Title, Links, Main, Scripts } from '../src/Document';
import { Meta, Title, Links, Main, Scripts, usePageAssets } from '../src/Document';
import { useAppContext } from '../src/';
import {
createRouteLoader,
} from '../src/routes.js';
@ -254,4 +255,54 @@ describe('run server app', () => {
expect(!!htmlContent).toBe(true);
expect(htmlContent.includes('<div>home</div')).toBe(false);
});
it('render with use scripts', async () => {
let scripts;
let renderMode;
const Document = () => {
scripts = usePageAssets();
const context = useAppContext();
renderMode = context.renderMode;
return (
<html>
<head>
<meta charSet="utf-8" />
<meta name="description" content="ICE 3.0 Demo" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Title />
<Links />
</head>
<body>
<Main />
<Scripts />
</body>
</html>
);
};
const html = await renderToHTML({
// @ts-ignore
req: {
url: '/home',
},
}, {
app: {},
assetsManifest,
runtimeModules: { commons: [] },
// @ts-ignore don't need to pass params in test case.
createRoutes: () => basicRoutes,
Document,
renderMode: 'SSR',
});
// @ts-ignore
expect(html?.value?.includes('<div>home</div>')).toBe(true);
// @ts-ignore
expect(html?.value?.includes('js/home.js')).toBe(true);
expect(scripts).toStrictEqual(['/js/home.js']);
expect(renderMode).toBe('SSR');
});
});

View File

@ -241,6 +241,33 @@ const Home = () => {
};
```
### useAppContext
获取应用级 Context, 包含字段为:
* appConfig object, 通过 defineAppConfig 定义的 appConfig
* renderMode string, 渲染模式,'CSR'|'SSR'|'SSG'
* documentOnly boolean, 是否指定为 CSR
* downgrade boolean, 是否为 SSR 降级
```ts title="src/document.ts"
import { useAppContext } from 'ice';
function Document() {
const { renderMode } = useAppContext();
console.log(renderMode);
return (
<html>
<head>
</head>
<body>
<Main />
</body>
</html>
);
}
```
### `<ClientOnly />`
:::caution

View File

@ -199,3 +199,28 @@ function Document() {
);
}
```
### usePageAssets
获取当前页面的所有 Assets 资源,包含 CSS 和 JS。
```ts title="src/document.ts"
import { usePageAssets } from 'ice';
function Document() {
const pageAssets = usePageAssets();
const pageScripts = pageAssets.filter(src => src.indexOf('.js') > -1);
console.log(pageScripts);
return (
<html>
<head>
</head>
<body>
<Main />
</body>
</html>
);
}
```