mirror of https://github.com/alibaba/ice.git
Feat: ssg (#4542)
This commit is contained in:
parent
8071445e80
commit
575f9b19ba
|
|
@ -1,21 +1,4 @@
|
|||
{
|
||||
"mpa": true,
|
||||
"plugins": [
|
||||
[
|
||||
"build-plugin-prerender",
|
||||
{
|
||||
"routes": [
|
||||
"/home",
|
||||
"/dashboard"
|
||||
],
|
||||
"minify": {
|
||||
"collapseBooleanAttributes": false,
|
||||
"collapseWhitespace": false
|
||||
},
|
||||
"renderer": {
|
||||
"maxConcurrentRoutes": 4
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
"ssr": "static"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,19 @@
|
|||
{
|
||||
"name": "exmaple-with-prerender-mpa",
|
||||
"name": "with-prerender-mpa-exmaple",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"ice.js": "^1.0.11",
|
||||
"react": "^16.4.1",
|
||||
"react-dom": "^16.4.1",
|
||||
"build-plugin-prerender": "^1.5.1"
|
||||
"react-dom": "^16.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^16.9.13",
|
||||
"@types/react-dom": "^16.9.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "icejs start",
|
||||
"build": "icejs build"
|
||||
"start": "../../packages/icejs/bin/ice-cli.js start --mode dev",
|
||||
"build": "../../packages/icejs/bin/ice-cli.js build --mode prod"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,3 @@
|
|||
{
|
||||
"plugins": [
|
||||
[
|
||||
"build-plugin-prerender",
|
||||
{
|
||||
"routes": [
|
||||
"/",
|
||||
"/about",
|
||||
"/dashboard"
|
||||
],
|
||||
"renderer": {
|
||||
"headless": false
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
"ssr": "static"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
{
|
||||
"name": "exmaple-basic-prerender",
|
||||
"name": "with-prerender-spa-example",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"ice.js": "^1.4.0",
|
||||
"react": "^16.8.1",
|
||||
"react-dom": "^16.8.1"
|
||||
},
|
||||
|
|
@ -11,10 +10,10 @@
|
|||
"@types/react-dom": "^16.9.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "icejs start --mode dev",
|
||||
"build": "icejs build --mode prod"
|
||||
"start": "../../packages/icejs/bin/ice-cli.js start --mode dev",
|
||||
"build": "../../packages/icejs/bin/ice-cli.js build --mode prod"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const appConfig: IAppConfig = {
|
|||
rootId: 'ice-container'
|
||||
},
|
||||
router: {
|
||||
// not support hash
|
||||
type: 'browser',
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,16 @@
|
|||
import React from 'react';
|
||||
import { useParams } from 'ice';
|
||||
|
||||
export default () => (
|
||||
<h2>About</h2>
|
||||
);
|
||||
function About() {
|
||||
const { id } = useParams();
|
||||
|
||||
return (
|
||||
<h2>About {id}</h2>
|
||||
);
|
||||
}
|
||||
|
||||
About.getStaticPaths = async () => {
|
||||
return await Promise.resolve(['/about/a', '/about/b', '/about/c']);
|
||||
};
|
||||
|
||||
export default About;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from 'react';
|
||||
import { Link } from 'ice';
|
||||
|
||||
const Home = () => {
|
||||
const Home = ({ data = [] }) => {
|
||||
return (
|
||||
<>
|
||||
<div id="menu">
|
||||
|
|
@ -14,8 +14,11 @@ const Home = () => {
|
|||
<Link to="/Dashboard">Dashboard</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/about">About</Link>
|
||||
<Link to="/about/1">About</Link>
|
||||
</li>
|
||||
{
|
||||
data.map((item: number) => (<li key={item}><strong>{item}</strong></li>))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<div>Home</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
import React from 'react';
|
||||
|
||||
export default () => {
|
||||
return (<div>NotFound</div>);
|
||||
};
|
||||
|
|
@ -1,21 +1,30 @@
|
|||
import Home from '@/pages/Home';
|
||||
import About from '@/pages/About';
|
||||
import Dashboard from '@/pages/Dashboard';
|
||||
import NotFound from '@/pages/NotFound';
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '/',
|
||||
exact: true,
|
||||
component: Home
|
||||
component: Home,
|
||||
getInitialProps: () => {
|
||||
return Promise.resolve({ data: [Math.random(), Math.random(), Math.random()] });
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
path: '/about/:id',
|
||||
exact: true,
|
||||
component: About
|
||||
component: About,
|
||||
},
|
||||
{
|
||||
path: '/dashboard',
|
||||
exact: true,
|
||||
ssr: true, // this route will use ssr in production
|
||||
component: Dashboard
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
component: NotFound,
|
||||
}
|
||||
];
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
"rollback": "ts-node ./scripts/rollback.ts",
|
||||
"owner": "ts-node ./scripts/owner.ts",
|
||||
"dependency:check": "ts-node ./scripts/dependency-check.ts",
|
||||
"clean": "lerna clean --yes && rimraf packages/*/lib",
|
||||
"clean": "rimraf packages/*/lib",
|
||||
"lint": "eslint --cache --ext .js,.jsx,.ts,.tsx ./",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"test": "jest --forceExit",
|
||||
|
|
@ -69,8 +69,7 @@
|
|||
"typescript": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "^3.6.4",
|
||||
"path-to-regexp": "6.1.0"
|
||||
"core-js": "^3.6.4"
|
||||
},
|
||||
"resolutions": {
|
||||
"@typescript-eslint/eslint-plugin": "^4.0.0",
|
||||
|
|
|
|||
|
|
@ -63,4 +63,5 @@ export function runApp(appConfig?: IAppConfig) {
|
|||
|
||||
export default {
|
||||
createBaseApp: frameworkAppBase,
|
||||
initAppLifeCycles,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export const USER_CONFIG = [
|
|||
},
|
||||
{
|
||||
name: 'ssr',
|
||||
validation: 'boolean'
|
||||
validation: 'boolean|string'
|
||||
},
|
||||
{
|
||||
name: 'auth',
|
||||
|
|
|
|||
|
|
@ -24,14 +24,15 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@builder/app-helpers": "^2.3.4",
|
||||
"@builder/webpack-config": "^1.0.0",
|
||||
"@loadable/babel-plugin": "^5.13.2",
|
||||
"@loadable/webpack-plugin": "^5.14.0",
|
||||
"@builder/webpack-config": "^1.0.0",
|
||||
"chalk": "^4.0.0",
|
||||
"cheerio": "^1.0.0-rc.3",
|
||||
"fs-extra": "^8.1.0",
|
||||
"html-minifier": "^4.0.0",
|
||||
"parseurl": "^1.3.3",
|
||||
"path-to-regexp": "^1.7.0",
|
||||
"query-string": "^6.13.7"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
import { join } from 'path';
|
||||
import * as fse from 'fs-extra';
|
||||
|
||||
interface PageData {
|
||||
path: string;
|
||||
html: string;
|
||||
strict?: boolean;
|
||||
sensitive?: boolean;
|
||||
exact?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* generate pages html and pages-data.json file
|
||||
*/
|
||||
async function generateStaticPages(buildDir: string, renderPagesBundlePath: string) {
|
||||
const pagesDataPath = join(buildDir, 'server', 'pages-data.json');
|
||||
|
||||
// eslint-disable-next-line
|
||||
const renderPages = require(renderPagesBundlePath);
|
||||
const htmlTemplate = fse.readFileSync(join(buildDir, 'index.html'), 'utf8');
|
||||
const pagesData: PageData[] = await renderPages.default({ htmlTemplate });
|
||||
|
||||
// write pages html
|
||||
pagesData.forEach((pageData) => {
|
||||
const { html, path } = pageData;
|
||||
const htmlPath = join(buildDir, path, 'index.html');
|
||||
fse.ensureFileSync(htmlPath);
|
||||
fse.writeFileSync(htmlPath, html);
|
||||
});
|
||||
|
||||
// write pages data to json
|
||||
fse.ensureFileSync(pagesDataPath);
|
||||
fse.writeJSONSync(pagesDataPath, pagesData, { spaces: 2 });
|
||||
}
|
||||
|
||||
export default generateStaticPages;
|
||||
|
|
@ -4,13 +4,14 @@ import { minify } from 'html-minifier';
|
|||
import LoadablePlugin from '@loadable/webpack-plugin';
|
||||
import getWebpackConfig from '@builder/webpack-config';
|
||||
import { formatPath } from '@builder/app-helpers';
|
||||
import generateStaticPages from './generateStaticPages';
|
||||
|
||||
const plugin = async (api): Promise<void> => {
|
||||
const { context, registerTask, getValue, onGetWebpackConfig, onHook, log, applyMethod, modifyUserConfig } = api;
|
||||
const { rootDir, command, webpack, commandArgs, userConfig } = context;
|
||||
const { outputDir } = userConfig;
|
||||
const TEMP_PATH = getValue('TEMP_PATH');
|
||||
const { outputDir, ssr, publicPath = '/', devPublicPath = '/' } = userConfig;
|
||||
const PROJECT_TYPE = getValue('PROJECT_TYPE');
|
||||
const TEMP_PATH = getValue('TEMP_PATH');
|
||||
// Note: Compatible plugins to modify configuration
|
||||
const buildDir = path.join(rootDir, outputDir);
|
||||
const serverDir = path.join(buildDir, 'server');
|
||||
|
|
@ -19,11 +20,20 @@ const plugin = async (api): Promise<void> => {
|
|||
|
||||
// render server entry
|
||||
const templatePath = path.join(__dirname, '../src/server.ts.ejs');
|
||||
const ssrEntry = path.join(TEMP_PATH, 'server.ts');
|
||||
const ssrEntry = path.join(TEMP_PATH, 'plugins/ssr/server.ts');
|
||||
const routesFileExists = fse.existsSync(path.join(rootDir, 'src', `routes.${PROJECT_TYPE}`));
|
||||
applyMethod('addRenderFile', templatePath, ssrEntry, { outputDir, routesPath: routesFileExists ? '@' : '.' });
|
||||
applyMethod(
|
||||
'addRenderFile',
|
||||
templatePath,
|
||||
ssrEntry,
|
||||
{
|
||||
outputDir,
|
||||
routesPath: routesFileExists ? '@' : '.',
|
||||
publicPath: command === 'build' ? publicPath : devPublicPath
|
||||
});
|
||||
|
||||
const mode = command === 'start' ? 'development' : 'production';
|
||||
|
||||
const webpackConfig = getWebpackConfig(mode);
|
||||
// config DefinePlugin out of onGetWebpackConfig, so it can be modified by user config
|
||||
webpackConfig
|
||||
|
|
@ -40,6 +50,7 @@ const plugin = async (api): Promise<void> => {
|
|||
.tap(([args]) => {
|
||||
return [{
|
||||
...args,
|
||||
// will add assets by @loadable/component
|
||||
inject: false,
|
||||
}];
|
||||
});
|
||||
|
|
@ -59,7 +70,7 @@ const plugin = async (api): Promise<void> => {
|
|||
onGetWebpackConfig('ssr', (config) => {
|
||||
config.entryPoints.clear();
|
||||
|
||||
config.entry('server').add(ssrEntry);
|
||||
config.entry('index').add(ssrEntry);
|
||||
|
||||
config.target('node');
|
||||
|
||||
|
|
@ -92,10 +103,13 @@ const plugin = async (api): Promise<void> => {
|
|||
|
||||
config.output
|
||||
.path(serverDir)
|
||||
.filename(serverFilename)
|
||||
.filename('[name].js')
|
||||
.publicPath('/')
|
||||
.libraryTarget('commonjs2');
|
||||
|
||||
// not generate vendor
|
||||
config.optimization.splitChunks({ cacheGroups: {} });
|
||||
|
||||
// in case of app with client and server code, webpack-node-externals is helpful to reduce server bundle size
|
||||
// while by bundle all dependencies, developers do not need to concern about the dependencies of server-side
|
||||
// TODO: support options to enable nodeExternals
|
||||
|
|
@ -127,6 +141,7 @@ const plugin = async (api): Promise<void> => {
|
|||
res.send(html);
|
||||
}
|
||||
}
|
||||
|
||||
if (command === 'start') {
|
||||
const originalDevMiddleware = config.devServer.get('devMiddleware');
|
||||
config.devServer.set('devMiddleware', {
|
||||
|
|
@ -171,6 +186,30 @@ const plugin = async (api): Promise<void> => {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (command === 'build' && ssr === 'static') {
|
||||
// SSG, pre-render page in production
|
||||
const ssgTemplatePath = path.join(__dirname, './renderPages.ts.ejs');
|
||||
const ssgEntryPath = path.join(TEMP_PATH, 'plugins/ssr/renderPages.ts');
|
||||
const ssgBundlePath = path.join(serverDir, 'renderPages.js');
|
||||
applyMethod(
|
||||
'addRenderFile',
|
||||
ssgTemplatePath,
|
||||
ssgEntryPath,
|
||||
{
|
||||
outputDir,
|
||||
routesPath: routesFileExists ? '@' : '.',
|
||||
publicPath: command === 'build' ? publicPath : devPublicPath
|
||||
}
|
||||
);
|
||||
|
||||
config.entry('renderPages').add(ssgEntryPath);
|
||||
|
||||
onHook('after.build.compile', async () => {
|
||||
await generateStaticPages(buildDir, ssgBundlePath);
|
||||
await fse.remove(ssgBundlePath);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onHook(`after.${command}.compile`, () => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
import '@/app';
|
||||
import { join } from 'path';
|
||||
import routes from '<%- routesPath %>/routes';
|
||||
import loadable from '@loadable/component';
|
||||
import * as pathToRegexp from 'path-to-regexp';
|
||||
|
||||
const { renderStatic } = require('./server');
|
||||
|
||||
export default async function ssgRender(options) {
|
||||
const { htmlTemplate } = options;
|
||||
const loadableStatsPath = join(process.cwd(), '<%- outputDir %>', 'loadable-stats.json');
|
||||
const buildConfig = { loadableStatsPath, publicPath: '<%- publicPath %>' };
|
||||
const htmlTemplateContent = htmlTemplate || `__ICE_SERVER_HTML_TEMPLATE__`;
|
||||
const pagesData = [];
|
||||
const flatRoutes = await getFlatRoutes(routes || []);
|
||||
|
||||
for (const flatRoute of flatRoutes) {
|
||||
const { path = '', getInitialProps, ...rest } = flatRoute;
|
||||
|
||||
const keys = [];
|
||||
pathToRegexp(path, keys);
|
||||
if (keys.length > 0) {
|
||||
// don't render and export static page when the route is dynamic, e.g.: /news/:id, /*
|
||||
continue;
|
||||
}
|
||||
|
||||
const initialContext = { pathname: path, location: { pathname: path } };
|
||||
const { html } = await renderStatic({ htmlTemplateContent, buildConfig, initialContext });
|
||||
|
||||
delete rest.component;
|
||||
delete rest.routeWrappers;
|
||||
pagesData.push({ html, path, ...rest });
|
||||
}
|
||||
|
||||
return pagesData;
|
||||
};
|
||||
|
||||
async function getFlatRoutes(routes, parentPath = '') {
|
||||
return await routes.reduce(async (asyncPrev, route) => {
|
||||
let prev = await asyncPrev;
|
||||
const { children, path: currentPath, redirect } = route;
|
||||
if (children) {
|
||||
prev = prev.concat(await getFlatRoutes(children, currentPath));
|
||||
} else if (!redirect) {
|
||||
route.path = join(parentPath, currentPath);
|
||||
if (route?.component?.__LOADABLE__) {
|
||||
route.component = loadable(route.component.dynamicImport);
|
||||
}
|
||||
prev.push(route);
|
||||
const getStaticPaths = route.getStaticPaths || route.component.getStaticPaths;
|
||||
if (typeof getStaticPaths === 'function') {
|
||||
const staticPaths = await getStaticPaths();
|
||||
if (Array.isArray(staticPaths)) {
|
||||
// add static paths to render
|
||||
staticPaths.forEach((staticPath) => {
|
||||
prev.push({
|
||||
...route,
|
||||
path: staticPath
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return prev;
|
||||
}, Promise.resolve([]));
|
||||
}
|
||||
|
|
@ -1,27 +1,40 @@
|
|||
import '@/app';
|
||||
import { join } from 'path';
|
||||
import * as pathToRegexp from 'path-to-regexp';
|
||||
import * as cheerio from 'cheerio';
|
||||
import * as queryString from 'query-string';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { matchPath } from 'ice';
|
||||
import { setInitialData } from 'react-app-renderer';
|
||||
import reactAppRendererWithSSR from 'react-app-renderer/lib/server';
|
||||
import { getAppConfig } from './core/appConfig';
|
||||
import loadStaticModules from './core/loadStaticModules';
|
||||
import { emitLifeCycles } from './core/publicAPI';
|
||||
import app from './core/runApp';
|
||||
import { InitialContext, ServerContext } from 'react-app-renderer/lib/types';
|
||||
import { getAppConfig } from '../../core/appConfig';
|
||||
import loadStaticModules from '../../core/loadStaticModules';
|
||||
import { emitLifeCycles } from '../../core/publicAPI';
|
||||
import app from '../../core/runApp';
|
||||
import routes from '<%- routesPath %>/routes';
|
||||
import loadable from '@loadable/component';
|
||||
|
||||
const chalk = require('chalk');
|
||||
const parseurl = require('parseurl');
|
||||
|
||||
const { createBaseApp } = app;
|
||||
const { createBaseApp, initAppLifeCycles } = app;
|
||||
|
||||
// appConfig set by: import '@/app'
|
||||
const appConfig = getAppConfig();
|
||||
|
||||
const serverRender = async (context, opts = {}) => {
|
||||
interface GenerateHtml {
|
||||
htmlTemplateContent: string;
|
||||
loadableComponentExtractor?: any;
|
||||
helmet?: any;
|
||||
bundleContent?: string;
|
||||
pageInitialProps?: any;
|
||||
initialData?: any;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
|
||||
const serverRender = async (context: ServerContext, opts = {}) => {
|
||||
let options;
|
||||
if (context.req) {
|
||||
options = { ctx: context, ...opts }
|
||||
|
|
@ -30,44 +43,64 @@ const serverRender = async (context, opts = {}) => {
|
|||
}
|
||||
let {
|
||||
ctx = {},
|
||||
pathname: originPathname,
|
||||
pathname,
|
||||
initialData,
|
||||
htmlTemplate,
|
||||
loadableStatsPath,
|
||||
publicPath = '/'
|
||||
pagesData = [],
|
||||
} = options;
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// loadable-stats.json will be genreated to the build dir in development
|
||||
// loadable-stats.json will be generated to the build dir in development
|
||||
loadableStatsPath = join(process.cwd(), '<%- outputDir %>', 'loadable-stats.json');
|
||||
}
|
||||
const buildConfig = { loadableStatsPath, publicPath };
|
||||
const buildConfig = { loadableStatsPath, publicPath: '<%- publicPath %>' };
|
||||
|
||||
const htmlTemplateContent = htmlTemplate || `__ICE_SERVER_HTML_TEMPLATE__`;
|
||||
// get html template
|
||||
const $ = cheerio.load(htmlTemplateContent, { decodeEntities: false });
|
||||
|
||||
// load module to run before createApp ready
|
||||
loadStaticModules(appConfig);
|
||||
|
||||
let pageInitialProps;
|
||||
let error;
|
||||
|
||||
const { req, res } = ctx as any;
|
||||
|
||||
const { search, hash, path, pathname: parsedPathname } = parseurl(req);
|
||||
const pathname = originPathname || parsedPathname;
|
||||
|
||||
const parsedQuery = queryString.parse(search);
|
||||
const initialContext = {
|
||||
req,
|
||||
res,
|
||||
pathname,
|
||||
query: parsedQuery,
|
||||
path,
|
||||
location: { pathname, search, hash, state: null },
|
||||
let initialContext: InitialContext = { pathname, location: { pathname } };
|
||||
if (req) {
|
||||
const { search, hash, path, pathname } = parseurl(req);
|
||||
const parsedQuery = queryString.parse(search);
|
||||
initialContext = {
|
||||
req,
|
||||
res,
|
||||
pathname,
|
||||
query: parsedQuery,
|
||||
path,
|
||||
location: { pathname, search, hash, state: null },
|
||||
};
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return renderStatic({ htmlTemplateContent, initialContext, initialData, buildConfig });
|
||||
}
|
||||
const pageData = pagesData.find(({ path, exact: end = false, strict = false, sensitive = false }) => {
|
||||
try {
|
||||
const regexp = pathToRegexp(path, [], { end, strict, sensitive });
|
||||
return regexp.test(initialContext.pathname);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if (pageData) {
|
||||
// enable pre-render pages
|
||||
const { html, ssr } = pageData;
|
||||
if (ssr) {
|
||||
return renderStatic({ htmlTemplateContent, initialContext, initialData, buildConfig });
|
||||
}
|
||||
return { html };
|
||||
}
|
||||
return renderStatic({ htmlTemplateContent, initialContext, initialData, buildConfig });
|
||||
}
|
||||
|
||||
export async function renderStatic({ htmlTemplateContent, initialContext, initialData, buildConfig }) {
|
||||
let error;
|
||||
let html = htmlTemplateContent;
|
||||
try {
|
||||
// get initial data
|
||||
if (!initialData) {
|
||||
|
|
@ -82,11 +115,12 @@ const serverRender = async (context, opts = {}) => {
|
|||
setInitialData(initialData);
|
||||
|
||||
// get page initial props
|
||||
let { component, getInitialProps } = await getComponentByPath(routes, pathname);
|
||||
let { component, getInitialProps } = await getComponentByPath(routes, initialContext.pathname);
|
||||
if (component && component.load) {
|
||||
const loadedPageComponent = await component.load();
|
||||
component = loadedPageComponent.default;
|
||||
}
|
||||
let pageInitialProps = {};
|
||||
if (getInitialProps) {
|
||||
console.log('[SSR]', 'getting initial props of page component');
|
||||
pageInitialProps = await getInitialProps(initialContext);
|
||||
|
|
@ -94,50 +128,79 @@ const serverRender = async (context, opts = {}) => {
|
|||
|
||||
// generate bundle content and register global variables in html
|
||||
console.log('[SSR]', 'generating html content');
|
||||
const { bundleContent, loadableComponentExtractor } = reactAppRendererWithSSR({
|
||||
initialContext,
|
||||
initialData,
|
||||
pageInitialProps
|
||||
}, {
|
||||
appConfig,
|
||||
buildConfig,
|
||||
appLifecycle: {
|
||||
createBaseApp,
|
||||
emitLifeCycles,
|
||||
}
|
||||
});
|
||||
const { bundleContent, loadableComponentExtractor } =
|
||||
reactAppRendererWithSSR(
|
||||
{
|
||||
initialContext,
|
||||
initialData,
|
||||
pageInitialProps,
|
||||
},
|
||||
{
|
||||
appConfig,
|
||||
buildConfig,
|
||||
appLifecycle: { createBaseApp, emitLifeCycles, initAppLifeCycles },
|
||||
}
|
||||
);
|
||||
|
||||
const helmet = Helmet.renderStatic();
|
||||
|
||||
$(`#${appConfig.app.rootId || 'ice-container'}`).append(bundleContent);
|
||||
$('head').append(`<script>
|
||||
window.__ICE_SSR_ENABLED__=true;
|
||||
window.__ICE_APP_DATA__=${JSON.stringify(initialData)};
|
||||
window.__ICE_PAGE_PROPS__=${JSON.stringify(pageInitialProps)};
|
||||
</script>`);
|
||||
// lazy load bundle
|
||||
$('head').append(`${loadableComponentExtractor.getLinkTags()}`);
|
||||
$('head').append(`${loadableComponentExtractor.getStyleTags()}`);
|
||||
$('body').append(`${loadableComponentExtractor.getScriptTags()}`);
|
||||
// react-helmet
|
||||
$('head').append(`${helmet.title.toString()}`);
|
||||
$('head').append(`${helmet.meta.toString()}`);
|
||||
$('head').append(`${helmet.link.toString()}`);
|
||||
$('head').append(`${helmet.script.toString()}`);
|
||||
html = generateHtml({
|
||||
htmlTemplateContent,
|
||||
loadableComponentExtractor,
|
||||
helmet,
|
||||
bundleContent,
|
||||
initialData,
|
||||
pageInitialProps,
|
||||
});
|
||||
} catch (e) {
|
||||
error = e;
|
||||
$('head').append(`
|
||||
<script>
|
||||
window.__ICE_SSR_ERROR__ = "${(error instanceof Error ? error.message : error).replace(/\"/g, '\'')}";
|
||||
</script>`)
|
||||
logError('[SSR] generate html template error');
|
||||
html = generateHtml({
|
||||
htmlTemplateContent,
|
||||
error,
|
||||
});
|
||||
logError('[SSR] generate html template error' + error.message);
|
||||
}
|
||||
|
||||
const html = $.html();
|
||||
return { html, error, redirectUrl: initialContext.url };
|
||||
return { html, error, redirectUrl: initialContext.redirectUrl };
|
||||
}
|
||||
|
||||
async function getComponentByPath(routes, currPath) {
|
||||
export function generateHtml({
|
||||
htmlTemplateContent,
|
||||
loadableComponentExtractor,
|
||||
helmet,
|
||||
bundleContent,
|
||||
initialData,
|
||||
pageInitialProps,
|
||||
error
|
||||
}: GenerateHtml) {
|
||||
const $ = cheerio.load(htmlTemplateContent, { decodeEntities: false });
|
||||
if (error) {
|
||||
$('head').append(`
|
||||
<script>
|
||||
window.__ICE_SSR_ERROR__ = "${(error instanceof Error ? error.message : error).replace(/\"/g, '\'')}";
|
||||
</script>`)
|
||||
return $.html();
|
||||
}
|
||||
$(`#${appConfig?.app?.rootId || 'ice-container'}`).append(bundleContent);
|
||||
$('head').append(`<script>
|
||||
window.__ICE_SSR_ENABLED__=true;
|
||||
window.__ICE_APP_DATA__=${JSON.stringify(initialData)};
|
||||
window.__ICE_PAGE_PROPS__=${JSON.stringify(pageInitialProps)};
|
||||
</script>`);
|
||||
// inject assets
|
||||
$('head').append(`${loadableComponentExtractor.getLinkTags()}`);
|
||||
$('head').append(`${loadableComponentExtractor.getStyleTags()}`);
|
||||
$('body').append(`${loadableComponentExtractor.getScriptTags()}`);
|
||||
// inject tags to header
|
||||
$('head').append(`${helmet.title.toString()}`);
|
||||
$('head').append(`${helmet.meta.toString()}`);
|
||||
$('head').append(`${helmet.link.toString()}`);
|
||||
$('head').append(`${helmet.script.toString()}`);
|
||||
|
||||
return $.html();
|
||||
}
|
||||
|
||||
export async function getComponentByPath(routes, currPath) {
|
||||
async function findMatchRoute(routeList) {
|
||||
let matchedRoute = routeList.find(route => {
|
||||
return matchPath(currPath, route);
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ const module = ({ appConfig, addDOMRender, buildConfig, setRenderApp, wrapperRou
|
|||
|
||||
const [apps, setApps] = useState(null);
|
||||
const BasicLayout = Layout || DefaultLayout;
|
||||
const RenderAppRoute = CustomAppRoute || AppRoute;
|
||||
const RenderAppRoute = (CustomAppRoute || AppRoute) as typeof AppRoute;
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
"@types/glob": "^7.1.1",
|
||||
"@types/history": "^4.7.5",
|
||||
"@types/node": "^12.12.12",
|
||||
"path-to-regexp": "^6.2.0",
|
||||
"path-to-regexp": "^1.7.0",
|
||||
"typescript": "^4.0.0",
|
||||
"vite": "^2.3.4"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { pathToRegexp } from 'path-to-regexp';
|
||||
import * as pathToRegexp from 'path-to-regexp';
|
||||
import joinPath from '../joinPath';
|
||||
|
||||
const joinTests: [string[], string][] = [
|
||||
|
|
@ -54,8 +54,6 @@ const regexpPathTests: [string[], string, string[] | null][] = [
|
|||
[['/login', '/:path(abc|xyz)*'], '/login/abc/abc', ['/login/abc/abc', 'abc/abc']],
|
||||
[['/login', '/:path(abc|xyz)*'], '/login/xxx', null],
|
||||
[['/login', '/(.*)'], '/login/xxx', ['/login/xxx', 'xxx']],
|
||||
[['/abc', '/:test((?!login)[^/]+)'], '/abc/xxx', ['/abc/xxx', 'xxx']],
|
||||
[['/abc', '/:test((?!login)[^/]+)'], '/abc/login', null],
|
||||
[['/abc', '/user(s)?/:user'], '/abc/user/123', ['/abc/user/123', undefined, '123']],
|
||||
[['/abc', '/user(s)?/:user'], '/abc/users/123', ['/abc/users/123', 's', '123']],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@
|
|||
"@loadable/component": "^5.14.1",
|
||||
"@loadable/server": "^5.14.0",
|
||||
"create-app-container": "^0.1.2",
|
||||
"create-use-router": "^0.1.1",
|
||||
"query-string": "^6.13.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -2,12 +2,32 @@ import * as queryString from 'query-string';
|
|||
import type { RuntimeModule } from 'create-app-shared';
|
||||
|
||||
export type OnError = (err: Error, componentStack: string) => void
|
||||
|
||||
export interface Context {
|
||||
pathname: string;
|
||||
path: string;
|
||||
query: queryString.ParsedQuery<string>;
|
||||
ssrError: any;
|
||||
initialContext: InitialContext,
|
||||
initialData: { [k: string]: any },
|
||||
pageInitialProps: { [k: string]: any }
|
||||
}
|
||||
|
||||
export interface ServerContext {
|
||||
req?: Request;
|
||||
res?: Response;
|
||||
}
|
||||
|
||||
export interface Location {
|
||||
pathname: string;
|
||||
search?: string;
|
||||
hash?: string;
|
||||
state?: string | null;
|
||||
}
|
||||
|
||||
export interface InitialContext extends ServerContext {
|
||||
pathname: string;
|
||||
location?: Location;
|
||||
path?: string;
|
||||
query?: queryString.ParsedQuery<string>;
|
||||
}
|
||||
|
||||
export type RenderAppConfig = {
|
||||
app?: {
|
||||
rootId?: string;
|
||||
|
|
@ -15,7 +35,7 @@ export type RenderAppConfig = {
|
|||
onErrorBoundaryHandler?: OnError;
|
||||
ErrorBoundaryFallback?: React.ComponentType;
|
||||
errorBoundary?: boolean;
|
||||
getInitialData?: (context: Context) => Promise<any>;
|
||||
getInitialData?: (context: InitialContext) => Promise<any>;
|
||||
},
|
||||
renderComponent?: React.ComponentType
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ type BuildResult = void | ITaskConfig[];
|
|||
|
||||
export async function viteBuild(context: any): Promise<BuildResult> {
|
||||
const { applyHook, command, commandArgs } = context;
|
||||
|
||||
|
||||
const configArr = context.getWebpackConfig();
|
||||
await applyHook(`before.${command}.load`, { args: commandArgs, webpackConfig: configArr });
|
||||
|
||||
|
|
@ -25,4 +25,4 @@ export async function viteBuild(context: any): Promise<BuildResult> {
|
|||
console.error('CONFIG', chalk.red('Failed to load vite config.'));
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
"fs-extra": "^9.0.1",
|
||||
"glob": "^7.1.4",
|
||||
"multer": "^1.4.2",
|
||||
"path-to-regexp": "^6.0.0",
|
||||
"path-to-regexp": "^1.7.0",
|
||||
"lodash.debounce": "^4.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/* eslint @typescript-eslint/no-var-requires:0 */
|
||||
import { pathToRegexp } from 'path-to-regexp';
|
||||
import * as pathToRegexp from 'path-to-regexp';
|
||||
|
||||
function decodeParam(val) {
|
||||
if (typeof val !== 'string' || val.length === 0) {
|
||||
|
|
@ -44,4 +44,4 @@ function matchPath(req, mockConfig) {
|
|||
}
|
||||
}
|
||||
|
||||
export default matchPath;
|
||||
export default matchPath;
|
||||
|
|
|
|||
Loading…
Reference in New Issue