preface
Recently, DUE to the need of work, I practiced the Server Side Render (SSR for short) technology. During this process, I encountered a lot of problems and referred to the solutions of many open source frameworks, feeling a lot of benefit, so I have this article, the purpose is to start from scratch. Teach you how to build your own React based SSR framework and thoroughly understand the principle of SSR.
What is SSR (Server-side rendering)
First we need to figure out what is SSR?
SSR is relative to CSR (Client rendering). Generally, when we develop based on Vue or React projects, pages are rendered by the client. The general process is as follows (here uses React as an example) :
When the user enters the URL in the address bar of the browser, the browser first requests the corresponding HTML resource from the server. The server returns an HTML page containing resource paths such as JS, CSS, and images. The browser then requests the corresponding RESOURCES such as JS and CSS images based on the resource paths. The browser starts to execute JS and then calls the Render method provided by ReactDOM to render the virtual Dom to the page, completing the rendering process of the page.
The process of SSR is slightly more complicated, and the general process is as follows:
Enter the URL in the browser address bar and send the request to the server. The server finds the route component to render according to the pathname of the request and calls renderToString or renderToStaticMarkup provided by React. The React Component is converted to a string and returned to the browser for rendering. After the browser retrieves the HTML, it executes the JS code again to perform operations such as event binding.
The advantage of SSR
From the above process analysis of CSR and SSR, we can see that SSR has obvious advantages.
- Avoid the blank screen and improve the loading speed of the first screen
The string returned by the server already contains the entire Dom structure of the page, so the page loads quickly and you don’t need to wait for the browser to finish executing JS to see the page effect. This is especially advantageous for large single-page applications, because the js volume of packaged single-page applications is usually large, and it takes a certain amount of time to load and execute JS, which will lead to a temporary blank screen in page loading. SSR can avoid this phenomenon very well.
- Better SEO
At present, many search engines do not support single page applications very well, because a lot of data in web pages need to be obtained through the implementation of JS, which is very unfavorable to crawler analysis and index. SSR is a good solution to this pain point, because the pages generated by SSR already contain complete data, combined with HTML meta tags, title and description, etc., can greatly improve the ranking of search.
How do I tell if a page is server-side or client-side rendered?
When you visit a page, you must have a question: how do you know if the page you are visiting is rendered by the client or the server?
There are many ways to judge. For example, you can choose to place the mouse pointer anywhere on the page, right click, and select Show page source code. The rendered page will not contain the page content.
For server rendering, right click and select Show source code to see the full page content. Again, use the example shown above to see what the server rendering looks like.
Solution idea
In order to support the use of SSR as conveniently as possible, I want to achieve SSR should have the following characteristics:
- Low coupling to the server, both
Nodejs
orServerless
Mode, can be very convenient to achieve deployment integration - Support page-level server loading data
- Support the use of
css-modules
和less
Realize the principle of
The client side and the server side compile separately. After the server side compiles, a server.js file will be generated, which is equivalent to the server side entry file. By referring to this file, nodeJS executes the render method after loading the file, and generates the corresponding HTML fragment according to the passed pathname. Back to the front end.
The node layer can be used as follows:
router.get('/ *'.async (ctx) => {
const render = require('dist/server.js');
const html = render({
// Path of the current request (mandatory)
pathname: ctx.req.pathname,
/ / is optional
initialData: {},}); ctx.res.body = html; });Copy the code
Client-side implementation
In order to better understand the implementation process, I do not use scaffolding to implement THE SSR process.
Since SSR and CSR code are isomorphic, we first create a React project and use Webpack to compile the client code.
Initialization engineering
Create a new project and execute NPM init to create the following directory:
├ ─ ─ public │ └ ─ ─ index. The HTML ├ ─ ─ the SRC │ ├ ─ ─ index. The js │ └ ─ ─ pages ├ ─ ─ package. The json └ ─ ─ yarn. The lockCopy the code
The index.js file in the SRC directory is used as the entry file:
src/index.js
import React, { Component } from 'react'
import ReactDOM from 'react-dom';
class App extends Component {
render() {
return (
<div>Welcome</div>
);
}
}
ReactDOM.render(<App />.document.getElementById('root'));
Copy the code
Webpack compilation
In order for the browser to execute the JS code, you also need Webpack to compile the above code.
Create a webpack.cli.js file in the project root directory to compile the client code:
webpack.cli.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const DIST_PATH = path.resolve(__dirname, 'dist');
module.exports = {
mode: 'development'.entry: './src/index.js'.output: {
filename: '[name].js'.chunkFilename: '[name].bundle.js'.path: path.resolve(__dirname, 'dist'),},resolve: {
extensions: ['.jsx'.'.js'.'.json'].alias: {
components: path.resolve(__dirname, 'src/components/'),
pages: path.resolve(__dirname, 'src/pages/'),}},module: {
rules: [{test: /\.(js|jsx)$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',}}, {test: /\.css$/,
use: ['style-loader'.'css-loader'],}, {test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'url-loader'.options: {
limit: 8192,},},],}, {test: /\.(eot|woff2? |ttf|svg)$/,
use: [
{
loader: 'url-loader'.options: {
name: '[name]-[hash:5].min.[ext]'.limit: 5000.// fonts file size <= 5KB, use 'base64'; else, output svg file
publicPath: 'fonts/'.outputPath: 'fonts/',},},],},plugins: [
new HtmlWebpackPlugin({
title: 'webpack-start'.filename: 'index.html'.template: './public/index.html'.inject: true.favicon: ' '.minify: false.hash: true,})],devServer: {
contentBase: DIST_PATH,
hot: true.historyApiFallback: true.compress: false.port: 9000.open: true,}};Copy the code
Installation-related dependencies:
$ yarn add react react-dom $ yarn add webpack webpack-cli webpack-dev-server style-loader url-loader html-webpack-plugin babel-loader @babel/core @babel/preset-env @babel/preset-react @babel/plugin-proposal-class-properties -DCopy the code
Add a line to the scripts of package.json:
"scripts": {+"start": "webpack-dev-server --config webpack.cli.js"
}
Copy the code
$NPM run start, open a browser and type localhost:9000 to see the client render.
routing
Now that we have implemented a React project, a single page application is missing an important part — routing.
$yarn add react-router-dom, install the react-router dependency, create a renderroutes. js file, and define a renderRoutes method that generates routes from the route configuration file. And support nested routines by.
src/components/renderRoutes.js
import React from 'react';
import { Route, Switch, Redirect } from 'react-router-dom';
const RouteWithProps = ({ path, exact, strict, render, location, sensitive, ... rest }) = > (
<Route
path={path}
exact={exact}
strict={strict}
location={location}
sensitive={sensitive}
render={(props)= >render({ ... props, ... rest })} />
);
export default function renderRoutes(routes = [], extraProps = {}, switchProps = {}) {
return routes ? (
<Switch {. switchProps} >
{routes.map((route, i) => {
if (route.redirect) {
return (
<Redirect
key={route.key || i}
from={route.path}
to={route.redirect}
exact={route.exact}
strict={route.strict}
/>
);
}
return (
<RouteWithProps
key={route.key || i}
path={route.path}
exact={route.exact}
strict={route.strict}
sensitive={route.sensitive}
render={(props)= >{ const childRoutes = renderRoutes(route.routes, extraProps, { location: props.location, }); if (route.component) { const newProps = { route, ... props, ... extraProps, } let { component: Component } = route; return (<Component {. newProps} route={route}>
{childRoutes}
</Component>); } else { return childRoutes; }}} / >); })}<Route component={require('@ /pages/404').default} / >
</Switch>
) : null;
}
Copy the code
/ / react-router/react-router
src/index.js
import React, { Component } from 'react'
import ReactDOM from 'react-dom';
import { Router } from 'react-router-dom';
import { createBrowserHistory } from 'history'
import renderRouters from '@/components/renderRoutes';
import routersConfig from '@/config/routes'
const history = createBrowserHistory();
class App extends Component {
render() {
return (
<Router history={history}>{renderRouters(routersConfig)}</Router>
);
}
}
ReactDOM.render(<App />.document.getElementById('root'));
Copy the code
Routing configuration file:
config/routes.js
const routersConfig = [
{
path: '/'.name: 'Home'.exact: true.component: require('pages/Home').default,
}, {
path: '/login'.name: 'Login'.exact: true.component: require('pages/Login').default,
},
];
export default routersConfig;
Copy the code
Here we have built a minimal React application, including Webpack, ES6 syntax support, front-end routing implementation, and more.
Server-side implementation
The idea of server side implementation has been introduced above. First, we need a portal for server side compilation to be provided to Webpack for server side compilation.
Before getting into the specifics of the code implementation, it’s important to understand the following concepts.
The concept of isomorphism
Isomorphism refers to the fact that clients and servers share the same set of code, which is the core idea behind the React server rendering implementation.
renderToString
React’s virtual Dom is an in-memory form of Dom, which makes it possible to run React on a server.
React provides two methods for rendering components on the server side: renderToString and renderToStaticMarkup. The purpose of both methods is to convert the virtual Dom to an HTML string for output.
RenderToString adds two attributes prefixed with data- to the returned HTML fragment. Data-reactid is used by React to distinguish Dom nodes. When the props or state of the component changes, React recognizes this property and updates the Dom quickly. Data-react-checksum is a checksum for creating the Dom. This allows the React client to reuse the same code structure as the server.
The client calls the reactdom.hydrate () method and React will retain this node and only do the event handling binding, giving you a very high performance first time loading experience.
renderToStaticMarkup
The renderToStaticMarkup method is similar to the renderToString method, but it does not create properties starting with data- inside React.
As mentioned earlier, React uses the data-react-checksum attribute to check whether the client and server render the same page structure. If a data-react-checksum discrepancy is detected, React will discard the Dom structure provided by the server, then re-render the component and mount it to the page, in which case it will no longer have the performance benefits of server-side rendering. So, here we choose the renderToString method.
StaticRouter
The React-Router provides a component StaticRouter for the scenario rendered by the server. Because the server rendering is stateless, the server passes it to the StaticRouter based on the requested URL so that it can match the route.
Code implementation
Create a server.js file in the SRC directory as the entry file for server compilation.
Define a serverRender method that takes a parameter pathName, which is passed to the StaticRouter component when the serverRender method is called, RenderRouters are used to generate corresponding components, and renderToString provided by React-DOM/Server is used to render the components into strings. These components will be embedded into HTML fragments and returned.
The code is as follows:
src/server.js
import React from 'react';
import { StaticRouter } from 'react-router-dom';
import { renderToString } from 'react-dom/server';
import renderRouters from '@/components/renderRoutes';
import routersConfig from '@/config/routes';
const serverRender = ({ pathname }) = > {
const RootComponent = () = > (
<StaticRouter location={pathname} static={{}}>
{renderRouters(routersConfig)}
</StaticRouter>
);
const bundleContent = renderToString(RootComponent());
console.log('html content:', bundleContent)
const html = ` <! DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> <meta name="theme-color" content="#000000" /> <title>react ssr</title> </head> <body> <div id="root">${bundleContent}</div>
</body>
</html>
`
return {
html,
}
}
export default serverRender;
Copy the code
Then configure the Webpack configuration file for server-side packaging:
webpack.server.js
const path = require('path');
module.exports = {
mode: 'production'.entry: './src/server.js'.output: {
filename: 'index.js'.path: path.resolve(__dirname, 'dist/server'),
publicPath: '/'.libraryTarget: 'commonjs2',},target: 'node'.resolve: {
extensions: ['.ts'.'.tsx'.'.js'.'.jsx'].alias: {
The '@': path.resolve(__dirname, 'src/'),
components: path.resolve(__dirname, 'src/components/'),
pages: path.resolve(__dirname, 'src/pages/'),}},module: {
rules: [{test: /\.(js|jsx)$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',}}, {test: /\.(png|svg|jpg|gif)$/,
use: [
{
loader: 'file-loader'.options: {
name: 'static/media/[name].[hash:8].[ext]',},},],},},};Copy the code
As you can see, the Webpack configuration file packaged on the server is very similar to the Webpack configuration file packaged on the client, but there are a few things to note:
- Profile based
target
Property set tonode
, because the compiled code runs innodejs
. output
的libraryTarget
Property needs to be set tocommonjs2
To make packaged code compatiblecommonjs
Specification.- Specify the entry for packing as
entry: './src/server.js'
.
Add a command to the srcipts field in package.json:
"scripts": {+"build:ssr": "webpack --config webpack.server.js"
}
Copy the code
Create a server.js file in the project root directory with the following contents:
const express = require('express');
const app = express();
app.use(express.static('dist', { index: false }));
app.get('/ *'.async (req, res) => {
const render = require('./dist/server/index.js').default; // eslint-disable-line
const html = await render({
// Path of the current request (mandatory)
pathname: req.url,
});
res.send(html.html);
});
app.listen(3000);
console.log('the app is listening at port 3000');
Copy the code
To perform:
$ npm run build:ssr
Copy the code
To perform:
$ node server.js
Copy the code
Open a browser and visit localhost:3000 to see how the server rendered.
Consider further:
One problem with this is that now we can actually return the HTML based on the requested URL, but there are no JS and CSS resources in the returned HTML.
React requires javascript code to be executed on the client while rendering on the server to perform actions such as binding events. So here we want to use the HTML file compiled by the client.
When webpack is compiled on the client side, with the help of htML-webpack-plugin, the js and CSS resource addresses can be directly embedded into THE HTML file for output, similar to the following:
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<title>SSR</title>
</head>
<body>
<div id="root"></div>
<script src="main.js? 44c0eed6a8d4ddce3c64"></script></body>
</html>
Copy the code
It contains the js and CSS resource addresses packaged by the client.
So the next idea of our transformation is to use the HTML file generated after client compilation as the output HTML template compiled by our server.
To make it easy to manipulate the DOM with NodeJS, I chose cheerio, whose API is similar to jquery. Now modify server.js:
const serverRender = ({ pathname }) = > {
const RootComponent = () = > (
<StaticRouter location={pathname} static={{}}>
{renderRouters(routersConfig)}
</StaticRouter>
);
const bundleContent = renderToString(RootComponent());
const $ = cheerio.load('__SERVER_HTML_TEMPLATE__');
$('#root').append(bundleContent)
const html = $.html();
return {
html,
}
}
Copy the code
As you can see here, I used a variable __SERVER_HTML_TEMPLATE__ for the placeholder, and the server.js code looks like this after the server-side compilation (the following is a snippet of server.js code after the server-side compilation).
So after the server-side compilation, we need to write a Webpack plug-in that replaces the __SERVER_HTML_TEMPLATE__ global variable with the HTML string generated by the client-side compilation.
Plug-ins are as follows:
replaceHtmlTemplateWebpackPlugin
const minify = require('html-minifier').minify
const path = require('path');
const fs = require('fs');
class SSRCompileDonePlugin {
apply(compiler) {
compiler.hooks.afterEmit.tap('SSRCompileDone'.(stats) = > {
const htmlFilePath = path.join(__dirname, 'dist'.'index.html');
const ssrBuildFile = path.join(__dirname, 'dist'.'server'.'index.js');
const bundle = fs.readFileSync(ssrBuildFile, 'utf-8');
const html = fs.readFileSync(htmlFilePath, 'utf-8');
const minifedHtml = minify(html, { collapseWhitespace: true.quoteCharacter: '\' ' });
const newBundle = bundle.replace(/__SERVER_HTML_TEMPLATE__/, minifedHtml);
fs.writeFileSync(ssrBuildFile, newBundle, 'utf-8'); }); }}module.exports = SSRCompileDonePlugin;
Copy the code
Note that string replacement cannot be performed until the server compiled file is output, so the string replacement is done in compiler.hooks. AfterEmit.
After introducing the plug-in in the webpack.server.js configuration file, the server side compilation is re-executed, and the __SERVER_HTML_TEMPLATE__ string in the output of the server side compilation has been replaced with the HTML template of the output of the client side compilation, This will cover our basic needs.
$NPM run build: SSR: $NPM run build: SSR Finally, execute Node server.js to see the basic effects of SSR. As you can see, the js resource compiled by the client is also successfully referenced, very perfect!
It is important to note that a client-side compilation must be performed before performing server-side compilation. There are two main purposes for this compilation:
- The server compilation relies on the template output from the client compilation
html
Files. I’ve talked about this in detail SSR
Will also be executedjs
Code, so need to use the client package after outputjs
Resources, at the same timejs
After the code is executed, it loads images, font files, styles, and other resources that depend on client-side compilation
The styling
So far, our SSR has the basic functionality of being able to direct HTML fragments, but we still have a tricky problem to solve, and that is styling.
During client compilation, CSS-loader + style-loader is generally used to process styles. Css-loader parses CSS-type files, and style-loader inserts styles into HTML in the form of style tags.
For server rendering, this is not possible. If the server compiles using the above method, ReferenceError: Window is not defined will be reported. Obviously, the Window object does not exist at the time of server rendering.
We searched a lot of information, and the library isomorphic-style-loader was mostly used to deal with the server rendering style. This library is similar to style-loader, but can be tedious to use. Is there a better way to handle server rendering styles?
The answer is yes.
Note that csS-loader provides onlyLocals (which is provided in csS-loader 3.x and changed to exportOnlyLocals in the latest 4.x version). Here’s what the document says:
Useful when you use css modules for pre-rendering (for example SSR). For pre-rendering with mini-css-extract-plugin you should use this option instead of style-loader! css-loader in the pre-rendering bundle. It doesn’t embed CSS but only exports the identifier mappings.
This property is provided for pre-rendering (such as SSR) and is used in conjunction with the mini-CSS-extract-plugin, which does not embed CSS and only exports identifier maps.
Our server-side rendering style scheme relies on this option.
In client rendering, CSS-Loader is still used for packaging, and the MINI-CSS-extract-plugin is used to extract CSS styles from JS files into separate CSS files and output them to dist directory. Only the following configuration is required for server packaging:
module.exports = {
module: {
rules: [{test: /\.css$/i,
loader: 'css-loader'.options: {
onlyLocals: true,},},],},};Copy the code
This configuration will package the className and embed it in the HTML. As mentioned earlier, the HTML string returned by the server already contains the path of the CSS resource wrapped by the client. So, when the server returns the HTML, it has the className style. A web request is made to obtain the client’s packaged style file, and the style takes effect.
Now we follow this idea, first transform the client Webpack configuration file, in order to achieve better style isolation, here I choose to open CSS-Module, and support the use of less.
Installation-related dependencies:
Yarn add [email protected] [email protected] [email protected] [email protected] [email protected] [email protected] [email protected] [email protected]Copy the code
Take a look at the Webpack configuration handled by the client compilation style (the full Webpack configuration file is available in the source code) :
webpack.cli.prod.js
module.exports = {
module: {
rules: [{test: /\.less/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader'.options: {
modules: {
localIdentName: '[name]-[local]--[hash:base64:5]',},importLoaders: 1,}}, {loader: 'postcss-loader'.options: {
ident: 'postcss'.plugins: () = > [
require('postcss-flexbugs-fixes'),
require('postcss-preset-env') ({autoprefixer: {
flexbox: 'no-2009',},stage: 3,}),],}}, {loader: 'less-loader'.options: {
javascriptEnabled: true,},},],},plugins: [
new MiniCssExtractPlugin(),
],
};
Copy the code
Take a look at the Webpack configuration handled by the server-side compile style (the full Webpack configuration file is available in the source code) :
webpack.server.js
module.exports = {
module: {
rules: [{test: /\.less/,
use: [
{
loader: 'css-loader'.options: {
modules: {
localIdentName: '[name]-[local]--[hash:base64:5]',},onlyLocals: true.importLoaders: 1,}}, {loader: 'postcss-loader'.options: {
ident: 'postcss'.plugins: () = > [
require('postcss-flexbugs-fixes'),
require('postcss-preset-env') ({autoprefixer: {
flexbox: 'no-2009',},stage: 3,}),],}}, {loader: 'less-loader'.options: {
javascriptEnabled: true,},},],},},};Copy the code
Use:
Home/index.js
import React from 'react';
import styles from './index.less';
export default() = > {return (
<div className={styles.container}>
<h2>Home page</h2>
</div>
);
}
Copy the code
Home/index.less
.container {
background: red;
}
Copy the code
After the Webpack configuration file is successfully configured, we recompile the client and server. After starting the NodeJS service, we can see that the HTML fragment directly generated by the SSR we want already contains the corresponding className identifier, and also loads the CSS resources compiled by the client. So the server rendering style problem is perfectly solved here!
Data isomorphism
Another issue that the server rendering has to consider is how to use the same set of code to request data.
When the server sends out the HTML, it needs to complete the request for the data on the server side and carry it back. When the browser takes over the page, it needs to be able to determine that if the data is already there, it doesn’t need to ask the backend service.
React, while rendering on the client, data requests are usually made in componentDidMount, but server rendering doesn’t go through this lifecycle, so we need to consider other ways to get data. After comparing several options, we decided to define a static method, getInitialProps, for the routing components that need to request data, to get data from both the client rendering and the server.
Something like this:
Home/index.js
class Home extends React.Component {
render() {
return (
<div className={styles.container}>
<h2>Home page</h2>
</div>
);
}
}
Home.getInitialProps = async() = > {const entry = 'http:localhost:3000/api/list'
const { list = [] } = await axios(entry);
return {
home: {
list,
},
};
};
export default Home;
Copy the code
The server needs to find the current React component based on the pathName passed in by the foreground and then invoke static methods defined on that component.
The first choice is to encapsulate a getComponentByPath method, which uses the matchPath method provided by the React-Router to match the component corresponding to the route according to the pathName.
function getComponentByPath(routes, currPath) {
function findMatchRoute(routeList) {
const matchedRoute = routeList.find(route= > {
return matchPath(currPath, route)
})
if (matchedRoute) {
return matchedRoute.children
? findMatchRoute(matchedRoute.children)
: matchedRoute
}
return null
}
const matchedRoute = findMatchRoute(routes)
return matchedRoute && matchedRoute.component
}
Copy the code
Modify the serverRender method in the server.js file. To make it easier to manipulate asynchronous data, we change the serverRender function to async function.
const serverRender = async ({ pathname }) => {
let pageInitialProps = {}
try {
const PageComponent = getComponentByPath(routersConfig, pathname);
const getInitialProps = PageComponent && PageComponent.getInitialProps;
if (getInitialProps) {
console.log('[SSR]'.'getting initial props of page component')
pageInitialProps = awaitgetInitialProps(); }}catch (error) {
console.log('[SSR] generate html template error')}const RootComponent = () = > (
<StaticRouter location={pathname} static={{}}>
{renderRouters(routersConfig, pageInitialProps)}
</StaticRouter>
);
const bundleContent = renderToString(RootComponent());
}
Copy the code
According to the pathName passed by the foreground, the component corresponding to the current route can be matched by the encapsulated getComponentByPath method. If the static getInitialProps method exists on the component, it can be directly called. In this way, the component initialization data can be easily retrieved on the server side. The data captured will be passed to the renderRouters method and injected into the window.__GLOBAL_PAGE_PROPS__ global variable.
$('head').append(
`<script>window.__GLOBAL_PAGE_PROPS__ = The ${JSON.stringify(
pageInitialProps,
)}; </script>`);
Copy the code
Revamp the renderRouters approach.
const RouteWithProps = ({ path, exact, strict, render, location, sensitive, ... rest }) = > (
<Route
path={path}
exact={exact}
strict={strict}
location={location}
sensitive={sensitive}
render={(props)= >render({ ... props, ... rest })} />
);
function withRoutes(route) {
const { component } = route;
let Component = (args) = > {
const{ render, ... props } = args;return render(props);
};
if (component) {
const OldComponent = Component;
Component = props= > {
const [data, setData] = useState(typeof window! = ='undefined' ? window.__GLOBAL_PAGE_PROPS__ : {});
useEffect(() = > {
// When enter the page for the first time, need to use window.__ICE_PAGE_PROPS__ as props
// And don't need to re-request to switch routes
// Set the data to null after use, otherwise other pages will use
async function fetchData() {
if (typeof window! = ='undefined') {
if (window.__GLOBAL_PAGE_PROPS__) {
window.__GLOBAL_PAGE_PROPS__ = null;
} else if (component.getInitialProps) {
// When the server does not return data, the client calls getinitialprops
(async() = > {const pathname = window && window.location && window.location.pathname
const ctx = { }
const result = await component.getInitialProps({ pathname, ctx });
setData(result);
})();
}
}
}
fetchData();
}, []);
return (
<OldComponent {. Object.assign({}, props.data)} / >); }}const ret = (args) = > {
const{ render, ... rest } = args;return (
<RouteWithProps
{. rest}
render={(props)= > {
return <Component {. props} route={route} render={render} />; }} / >
);
};
return ret;
}
export default function renderRoutes(routes = [], extraProps = {}, switchProps = {}) {
return routes ? (
<Switch {. switchProps} >
{routes.map((route, i) => {
if (route.redirect) {
return (
<Redirect
key={route.key || i}
from={route.path}
to={route.redirect}
exact={route.exact}
strict={route.strict}
/>
);
}
const RouteRoute = withRoutes(route);
return (
<RouteRoute
key={route.key || i}
path={route.path}
exact={route.exact}
strict={route.strict}
sensitive={route.sensitive}
render={(props)= >{ const childRoutes = renderRoutes(route.routes, extraProps, { location: props.location, }); if (route.component) { const newProps = { route, ... props, ... extraProps, } let { component: Component } = route; return (<Component {. newProps} route={route}>
{childRoutes}
</Component>); } else { return childRoutes; }}} / >); })}<Route component={require('@ /pages/404').default} / >
</Switch>
) : null;
}
Copy the code
The renderRoutes method is executed once on the server and once on the client. The server passes the data obtained from the static method getInitialProps to the props of the component, so that the server can directly obtain the data from the props of the component to complete the rendering of the component.
There are two types of client execution: __GLOBAL_PAGE_PROPS__ is used to determine if there is any data requested by the server in the window.__global_page_props__ rendering. If so, pass the data directly to the component props. If not according to the pathname to invoke the component. GetInitialProps method, to request data, request to the data will also be passed on to the components of props.
In this way, whether the server rendering or the client rendering, as long as the logic of the request data is written on the component’s getInitialProps static method, the same logic can be used to satisfy both the server and client requests.
Finally, let’s take a look at the effect of data isomorphism transformation:
conclusion
The react-SSR core is now complete, but there are still some features that are not covered, such as how to integrate data flow management (such as Redux), how to support internationalization, and how to make SEO better with React-Helmet. These links can be easily integrated on the basis of understanding the SSR principle mentioned above. I will continue to update the demo later when I have time.
Finally, attach the code warehouse address, welcome everyone star 😊!
The resources
Umi micro front end solution Flying ice micro front end solution
More exciting content welcome to pay attention to my public number!