Skip to content

Commit

Permalink
feat(ssr): support useServerInsertedHTML
Browse files Browse the repository at this point in the history
  • Loading branch information
chenshuai2144 committed Jun 2, 2023
1 parent 27937cd commit d16ffcc
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 52 deletions.
12 changes: 11 additions & 1 deletion examples/ssr-demo/src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import React from 'react';
import { Link, useClientLoaderData, useServerLoaderData } from 'umi';
import {
Link,
useClientLoaderData,
useServerInsertedHTML,
useServerLoaderData,
} from 'umi';
import Button from '../components/Button';
// @ts-ignore
import bigImage from './big_image.jpg';
Expand All @@ -14,6 +19,11 @@ import umiLogo from './umi.png';
export default function HomePage() {
const clientLoaderData = useClientLoaderData();
const serverLoaderData = useServerLoaderData();

useServerInsertedHTML(() => {
return <div>inserted html</div>;
});

return (
<div>
<h1 className="title">Hello~</h1>
Expand Down
1 change: 1 addition & 0 deletions packages/preset-umi/src/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ umi build --clean
publicPath: api.config.publicPath,
});
const { vite } = api.args;

const markupArgs = await getMarkupArgs({ api });
const finalMarkUpArgs = {
...markupArgs,
Expand Down
38 changes: 2 additions & 36 deletions packages/preset-umi/src/features/exportStatic/exportStatic.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { getMarkup } from '@umijs/server';
import { lodash, logger, Mustache, winPath } from '@umijs/utils';
import { lodash, Mustache, winPath } from '@umijs/utils';
import assert from 'assert';
import { dirname, join, relative } from 'path';
import type { IApi, IRoute } from '../../types';
import { absServerBuildPath } from '../ssr/utils';
import { getPreRenderedHTML } from '../ssr/utils';

let markupRender: any;
const IS_WIN = process.platform === 'win32';

interface IExportHtmlItem {
Expand Down Expand Up @@ -54,39 +53,6 @@ function getExportHtmlData(routes: Record<string, IRoute>): IExportHtmlItem[] {
return Array.from(map.values());
}

/**
* get pre-rendered html by route path
*/
async function getPreRenderedHTML(api: IApi, htmlTpl: string, path: string) {
markupRender ??= require(absServerBuildPath(api))._markupGenerator;

try {
const markup = await markupRender(path);
const [mainTpl, extraTpl = ''] = markup.split('</html>');
// TODO: improve return type for markup generator
const helmetContent = mainTpl.match(
/<head>[^]*?(<[^>]+data-rh[^]+)<\/head>/,
)?.[1];
const bodyContent = mainTpl.match(/<body[^>]*>([^]+?)<\/body>/)?.[1];

htmlTpl = htmlTpl
// append helmet content
.replace('</head>', `${helmetContent || ''}</head>`)
// replace #root with pre-rendered body content
.replace(
new RegExp(`<div id="${api.config.mountElementId}"[^>]*>.*?</div>`),
bodyContent,
)
// append hidden templates
.replace(/$/, `${extraTpl}`);
logger.info(`Pre-render for ${path}`);
} catch (err) {
logger.error(`Pre-render ${path} error: ${err}`);
}

return htmlTpl;
}

export default (api: IApi) => {
/**
* convert user `exportStatic.extraRoutePaths` config to routes
Expand Down
49 changes: 48 additions & 1 deletion packages/preset-umi/src/features/ssr/ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import assert from 'assert';
import { existsSync, writeFileSync } from 'fs';
import { join } from 'path';
import type { IApi } from '../../types';
import { absServerBuildPath } from './utils';
import { absServerBuildPath, getPreRenderedHTML } from './utils';

export default (api: IApi) => {
const esbuildBuilder: typeof import('./builder/builder') = importLazy(
Expand All @@ -18,6 +18,29 @@ export default (api: IApi) => {
require.resolve('./webpack/webpack'),
);

// 如果 exportStatic 不存在的时候。预渲染一下 index.html
if (!api.config.exportStatic) {
// export routes to html files
api.modifyExportHTMLFiles(async (_defaultFiles) => {
const preRenderFils: typeof _defaultFiles = [];
for await (const file of _defaultFiles.filter(
(f) => !f.path.includes(':'),
)) {
// 只需要 index.html 就好了,404 copy 一下 index。但是最好还是开一下 exportStatic
if (file.path === 'index.html') {
const html = await getPreRenderedHTML(api, file.content, '/');
preRenderFils.push({
path: file.path,
content: html,
});
} else {
preRenderFils.push(file);
}
}
return preRenderFils;
});
}

api.describe({
key: 'ssr',
config: {
Expand Down Expand Up @@ -69,6 +92,30 @@ export default (api: IApi) => {
content: `
import * as React from 'react';
export { React };
`,
});

api.writeTmpFile({
noPluginDir: true,
path: 'core/serverInsertedHTMLContext.ts',
content: `
// Use React.createContext to avoid errors from the RSC checks because
// it can't be imported directly in Server Components:
import React from 'react'
export type ServerInsertedHTMLHook = (callbacks: () => React.ReactNode) => void;
// More info: https://github.com/vercel/next.js/pull/40686
export const ServerInsertedHTMLContext =
React.createContext<ServerInsertedHTMLHook | null>(null as any);
// copy form https://github.com/vercel/next.js/blob/fa076a3a69c9ccf63c9d1e53e7b681aa6dc23db7/packages/next/src/shared/lib/server-inserted-html.tsx#L13
export function useServerInsertedHTML(callback: () => React.ReactNode): void {
const addInsertedServerHTMLCallback = React.useContext(ServerInsertedHTMLContext);
// Should have no effects on client where there's no flush effects provider
if (addInsertedServerHTMLCallback) {
addInsertedServerHTMLCallback(callback);
}
}
`,
});
});
Expand Down
38 changes: 38 additions & 0 deletions packages/preset-umi/src/features/ssr/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { logger } from '@umijs/utils';
import { join } from 'path';
import { IApi } from '../../types';

Expand Down Expand Up @@ -25,3 +26,40 @@ export function absServerBuildPath(api: IApi) {
api.userConfig.ssr.serverBuildPath || 'server/umi.server.js',
);
}

/**
* get pre-rendered html by route path
*/
export async function getPreRenderedHTML(
api: IApi,
htmlTpl: string,
path: string,
) {
let markupRender;
markupRender ??= require(absServerBuildPath(api))._markupGenerator;
try {
const markup = await markupRender(path);

const [mainTpl, extraTpl = ''] = markup.split('</html>');
// TODO: improve return type for markup generator
const helmetContent =
mainTpl.match(/<head>[^]*?(<[^>]+data-rh[^]+)<\/head>/)?.[1] || '';
const bodyContent = mainTpl.match(/<body[^>]*>([^]+?)<\/body>/)?.[1];

htmlTpl = htmlTpl
// append helmet content
.replace('</head>', `${helmetContent || ''}</head>`)
// replace #root with pre-rendered body content
.replace(
new RegExp(`<div id="${api.config.mountElementId}"[^>]*>.*?</div>`),
bodyContent,
)
// append hidden templates
.replace(/$/, `${extraTpl}`);
logger.info(`Pre-render for ${path}`);
} catch (err) {
logger.error(`Pre-render ${path} error: ${err}`);
}

return htmlTpl;
}
5 changes: 5 additions & 0 deletions packages/preset-umi/src/features/tmpFiles/tmpFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,11 @@ if (process.env.NODE_ENV === 'development') {
exports.push(`export { TestBrowser } from './testBrowser';`);
}
}
if (api.config.ssr && api.appData.framework === 'react') {
exports.push(
`export { useServerInsertedHTML } from './core/serverInsertedHTMLContext';`,
);
}
// plugins
exports.push('// plugins');
const allPlugins = readdirSync(api.paths.absTmpPath).filter((file) =>
Expand Down
8 changes: 8 additions & 0 deletions packages/preset-umi/templates/server.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,18 @@ import { PluginManager } from '{{{ umiPluginPath }}}';
import createRequestHandler, { createMarkupGenerator } from '{{{ umiServerPath }}}';

let helmetContext;
let ServerInsertedHTMLContext;

try {
helmetContext = require('./core/helmetContext').context;
} catch { /* means `helmet: false`, do noting */ }


try {
ServerInsertedHTMLContext = require('./core/serverInsertedHTMLContext').ServerInsertedHTMLContext;
} catch { /* means `helmet: false`, do noting */ }


const routesWithServerLoader = {
{{#routesWithServerLoader}}
'{{{ id }}}': () => import('{{{ path }}}'),
Expand Down Expand Up @@ -50,6 +57,7 @@ const createOpts = {
getClientRootComponent,
helmetContext,
createHistory,
ServerInsertedHTMLContext,
};
const requestHandler = createRequestHandler(createOpts);

Expand Down
2 changes: 2 additions & 0 deletions packages/renderer-react/src/server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export async function getClientRootComponent(opts: {
function Html({ children, loaderData, manifest }: any) {
// TODO: 处理 head 标签,比如 favicon.ico 的一致性
// TODO: root 支持配置

return (
<html lang="en">
<head>
Expand All @@ -78,6 +79,7 @@ function Html({ children, loaderData, manifest }: any) {
__html: `<b>Enable JavaScript to run this app.</b>`,
}}
/>

<div id="root">{children}</div>
<script
dangerouslySetInnerHTML={{
Expand Down
Loading

0 comments on commit d16ffcc

Please sign in to comment.