UNPKG

utquidem

Version:

The meta-framework suite designed from scratch for frontend-focused modern web development.

173 lines (146 loc) 4.37 kB
import path from 'path'; import { LOADABLE_STATS_FILE } from '@modern-js/utils/constants'; import React from 'react'; import { RuntimeContext } from '@modern-js/runtime-core'; import ReactDomServer from 'react-dom/server'; import serialize from 'serialize-javascript'; import ReactHelmet, { HelmetData } from 'react-helmet'; import { Fragment, toFragments } from './template'; import { ModernSSRReactComponent, RenderLevel, SSRServerContext, RenderResult, } from './type'; import helmetReplace from './helmet'; import { reduce } from './reduce'; import * as loadableRenderer from './loadable'; import * as styledComponentRenderer from './styledComponent'; type EntryOptions = { name: string; template: string; App: ModernSSRReactComponent; }; const buildTemplateData = ( context: SSRServerContext, data: Record<string, any>, renderLevel: RenderLevel, ) => { const { request } = context; return { data, context: { request: { params: request.params, query: request.query, pathname: request.pathname, host: request.host, url: request.url, headers: request.headers, cookieMap: request.cookieMap, }, }, renderLevel, }; }; export default class Entry { public entryName: string; public result: RenderResult; private readonly App: ModernSSRReactComponent; private readonly fragments: Fragment[]; constructor(options: EntryOptions) { this.fragments = toFragments(options.template); this.entryName = options.name; this.App = options.App; this.result = { renderLevel: RenderLevel.CLIENT_RENDER, html: '', chunksMap: { js: '', css: '', }, }; } public async renderToHtml(context: RuntimeContext): Promise<string> { const { ssrContext } = context; if (ssrContext.redirection.url) { return ''; } const prefetchData = await this.prefetch(context); if (ssrContext.redirection.url) { return ''; } if (this.result.renderLevel >= RenderLevel.SERVER_PREFETCH) { this.result.html = this.renderToString(context); } if (ssrContext.redirection.url) { return ''; } let html = ''; const templateData = buildTemplateData( ssrContext, prefetchData, this.result.renderLevel, ); const SSRData = this.getSSRDataScript(templateData); for (const fragment of this.fragments) { if (fragment.isVariable && fragment.content === 'SSRDataScript') { html += fragment.getValue(SSRData); } else { html += fragment.getValue(this.result); } } const helmetData: HelmetData = ReactHelmet.renderStatic(); return helmetData ? helmetReplace(html, helmetData) : html; } private async prefetch(context: RuntimeContext) { const { App: { prefetch }, } = this; let prefetchData; try { prefetchData = prefetch ? await prefetch(context) : null; this.result.renderLevel = RenderLevel.SERVER_PREFETCH; } catch (e) { // Todo report if render error or fetch data error. logic from prefetch.tsx this.result.renderLevel = RenderLevel.CLIENT_RENDER; console.error('SSR Error - App Prefetch error = %s', e); } return prefetchData || {}; } private renderToString(context: RuntimeContext): string { let html = ''; try { const App = React.createElement(this.App, { context: Object.assign(context, { ssr: true }), }); // Todo render Hook const renderContext = { loadableManifest: path.resolve( context.ssrContext.distDir, LOADABLE_STATS_FILE, ), result: this.result, entryName: this.entryName, }; html = reduce(App, renderContext, [ styledComponentRenderer.toHtml, loadableRenderer.toHtml, (jsx: React.ReactElement) => ReactDomServer.renderToString(jsx), ]); this.result.renderLevel = RenderLevel.SERVER_RENDER; } catch (e) { console.error('SSR Error - App Render To HTML error = %s', e); } return html; } private getSSRDataScript(templateData: ReturnType<typeof buildTemplateData>) { return { SSRDataScript: ` <script>window._SSR_DATA = ${serialize(templateData, { isJSON: true, })}</script> `, }; } }