Early SSR(Server Side Rendering) : Server Side Rendering. In the earliest era of webpage development, this form was adopted. The Server rendered the page structure and returned it directly to the client.
** As the idea of separating the front and back ends to improve development efficiency becomes popular, react, Vue and other front-end frameworks are supported by default, and the front end routing without refreshing switch page has gradually become the mainstream form of front-end development. The server only returns an empty page. The client loads JS and fills the entire page to present it to the client, reducing the pressure on the server. However, the first screen wait time is long, and the server returns an empty page, which is not SEO friendly.
** SSR in the new era: ** In order to solve the pain point of CSR, developers re-focus on SSR. Combining WITH CSR, the isomorphic mode is adopted to refresh SSR to directly display the page structure. After that, the client takes over the page, and the front end route cuts the page without refreshing, which combines the advantages of SSR and CSR. At present, the react and Vue combined with the corresponding SSR framework, next. Js and nuxt.js.
This article uses a simple demo to understand the React SSR server rendering process.
Isomorphism: Isomorphism is a concept found in newer front-end frameworks such as Vue and React. Isomorphism is actually a combination of client-side and server-side rendering. We put together the presentation of the page and the interaction, and let the code execute twice. Once on the server side for server-side rendering and once again on the client side to take over the page interaction.
In essence, SSR can be implemented because of the existence of virtual DOM. DOM operations cannot be implemented on the server, while virtual DOM is a JavaScript object mapping of real DOM. React does not directly manipulate DOM when performing page operations. Instead, you manipulate the virtual DOM, that is, normal JavaScript objects, which makes SSR possible. On the server side, the virtual DOM is mapped as a string and returned, and on the client side, the virtual DOM is mapped as a real DOM and mounted to the page.
SSR typically requires a Node server as the middle layer, which handles server rendering and forwards client requests to the data server.
1. Configuration webpack
Since you need the Node middle tier, you must have an entry point to the Node service code and client code, and configure two webPack configurations
Client webpack.client.js:
const path = require('path'); module.exports = { mode: 'development', entry: './src/client/index.js', output: { filename: 'index.js', path: path.resolve(__dirname, 'public') }, module: { rules: [ { test: /\.jsx?$/, loader: 'babel-loader', exclude: /node_modules/,}} resolve: {extensions: [". Js ", ". Path. The resolve (__dirname, ".. / SRC "), / / reference file can use the "@" representative "SRC" absolute path, style file for "~ @}}}"Copy the code
The service side webpack. Server. Js
const path = require('path'); const nodeExternals = require('webpack-node-externals'); module.exports = { target: 'node', mode: 'development', entry: './src/server/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'build') }, externals: [nodeExternals()], module: { rules: [ { test: /\.jsx?$/, loader: 'babel-loader', exclude: /node_modules/, } } resolve: { extensions: [" js ", "JSX"], / / introduction file support omit suffix, configure the more the more performance overhead alias: {" @ ": Path. The resolve (__dirname, ".. / SRC "), / / reference file can use the "@" representative "SRC" absolute path, style file for "~ @}}}"Copy the code
The webpack-node-externals plugin is used to keep third-party modules from being packaged into the final source code in the Node environment because the NPM in the Node environment already has these dependencies installed. Target: Node is used to keep the core module of Node unpacked by Webpack.
2. Configure routes with the react-router-config configuration.
We use the same thing for the page code, just different routes for the front and back ends, The client uses BrowserRouter, while react-router-dom provides StaticRouter for client rendering. It is recommended to use react-router-config for route rendering management
Routing configuration file:
import App from "./containers/App" import Home from "./containers/Home"; import Login from "./containers/Login"; import Personal from "./containers/Personal"; import NotFound from "./containers/NotFound"; const routes = [ { path: "/", component: App, loadData: App.loadData, routes:[ { path: "/", component: Home, exact: LoadData: home. loadData,}, {path: "/login", exact: true, component: Login, }, { path: "/personal", exact: true, component: Personal }, { component: NotFound, } ] } ] export default routes;Copy the code
Client entry route:
import { renderRoutes } from "react-router-config"; import routes from '.. /Router'; const App = () => { return <Provider store={getClientStore()}> <BrowserRouter> {renderRoutes(routes)} </BrowserRouter> </Provider>} // mount to page reactdom.render (<App/>, document.querySelector('#root'))Copy the code
Server side entry route:
import { renderRoutes } from "react-router-config"; import routes from '.. /Router'; const App = () => { return <Provider store={getClientStore()}> <StaticRouter location={url} context={{}}> {routes (routes)} </StaticRouter> </Provider>} return reactdom.renderToString (<App/>)Copy the code
The StaticRouter match requires manually passing in the matching route address location={url}.
3. Combined with Redux, the data of the home page can be directly displayed
Node forwards requests. On the node side, I use KOA and use koA-server-HTTP-proxy to make proxy requests
import proxy from 'koa-server-http-proxy'; . app.use(proxy('/api', { target: 'http://xxx.com', changeOrigin: true })) ...Copy the code
Store creation:
// The server store is used by all users. When each user accesses the server store, this function is re-executed to provide a separate store for each user, instead of creating a singleton in the first place: export const getServerStore = (ctx) => createStore(reducer, applyMiddleware(logger, thunk.withExtraArgument(serverHttp))); // client store export const getClientStore = () => {const initState = window._content.state; return createStore(reducer, initState, applyMiddleware(logger, thunk.withExtraArgument(clientHttp))); }Copy the code
The server does not need a proxy for the initial page data request, while the client needs a proxy, the solution is:
Axios builds two instances of clientHttp and serverHttp, sets different baseURL, and thunk. WithExtraArgument (API) is passed in when redux-Thunk middleware is applied to createStore. An AXIOS instance is obtained in the third parameter of asynchronous Dispatch, through which the request is dispatched.
The first-screen data can be obtained through Redux and Dispatch
. Import routes from '.. /Router'; Const matchedRoutes = matchRoutes(routes, ctx.url); // Get the matched route const matchedRoutes = matchRoutes(routes, ctx.url); // Get an array of data requests -- a set of promise const promiseDatas = []; matchedRoutes.forEach(({route}) => { if(route.loadData) { promiseDatas.push(route.loadData(store)); }}) / / perform data request for store into the initial data Promise. All (promises). Then (() = > {} / / generates to return page)... . Import {getNewsList} from './store/actions'; import {useSelector, useDispatch} from 'react-redux'; import styles from './index.css'; const Home = () => { const name = useSelector(({root}) => root.name); const list = useSelector(({home}) => home.list); const dispatch = useDispatch(); useEffect(() => { if(! list.length) { dispatch(getNewsList()); } }, []) return <div> <h1 className={styles.title}>Home Page !!! </h1> <h2>name: {name}</h2> <ul> { list.map(({title, content}) => <li key={title}> <h4>{title}</h4> <p>{content}</p> </li>) } </ul> <button onClick={() => console.log('click Button ')}>click</button> </div>} home. loadData = (store) => {return store.dispatch(getNewsList()); } export default Home;Copy the code
Data for dehydration and water injection
After the server renders, it gets the home page data, but the client renders again, and the Store is empty. Solution: Assign a global variable (waterflood) to the data obtained during server rendering, and the store created by the client takes the value of this variable as the initial value (dehydration), so that the first screen of data can be displayed directly.
// Inject data '<! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, <meta http-equiv=" x-UA-compatible "content=" IE =edge"> <link rel="stylesheet" Href = "https://cdnjs.cloudflare.com/ajax/libs/antd/4.8.2/antd.min.css" integrity="sha512-CPolmBEaYWn1PClN5taQQ0ucEhAt+9j7+Tiog/SblkFjZ5k6M3TioqmlpcHKwUhIcsu1s7lgnX4Plsb6T8Kq5A==" crossorigin="anonymous" /> <title>React-SSR</title> </head> <body> <div id="root">${contents}</div> <script> window._content = { state: ${json.stringify (store.getState())}} </script> <script SRC ="/index.js"></script> </body> </ HTML > '// client dewater export const getClientStore = () => { const initState = window._content.state; return createStore(reducer, initState, applyMiddleware(logger, thunk.withExtraArgument(clientHttp))); }Copy the code
4. Straight out of the front screen style
Webpack configures CSS parsing
// webpack.client.js -- CsS-loader and style-loader are normally configured on the client..... module: { rules: [ { test: /\.css$/i, use: [ 'style-loader', { loader: 'css-loader', options: { importLoaders: 1, esModule: false, modules: { compileType: 'module', localIdentName: '[name]_[local]_[hash:base64:5]' }, } } ] }, ] } ..... // webpack.server.js -- use isomorphic-style-loader instead of style-loader on the server, because style-loader generates the style tag and mount it to the page. Module: {rules: [{test: /\. CSS $/, use: ['isomorphic-style-loader', {loader: 'css-loader', options: { esModule: false, importLoaders: 1, modules: { compileType: 'module', localIdentName: '[name]_[local]_[hash:base64:5]' }, } }] }, ] }Copy the code
Server Transformation
import React from 'React'; import {renderToString} from 'react-dom/server'; import { renderRoutes } from "react-router-config"; import StyleContext from 'isomorphic-style-loader/StyleContext'; StaticRouter import {StaticRouter} from 'react-router-dom'; import {Provider} from 'react-redux'; export const render = (store, routes, url, context) => { const css = new Set(); const insertCss = (... styles) => { styles.forEach(style => { css.add(style._getCss()); })}; const contents = renderToString( <StyleContext.Provider value={{ insertCss }}> <Provider store={store}> // The context can be obtained in the props. StaticContext of the component when rendered on the server, <StaticRouter location={url} context={{}}> {renderRoutes(routes)} </StaticRouter> </Provider> </StyleContext.Provider> ); return `<! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, "> <meta http-equiv=" x-UA-compatible "content="ie=edge"> <style =" box-sizing: border-box id="ssr-style">${[...css].join('\n')}</style> <title>React-SSR</title> </head> <body> <div id="root">${contents}</div> <script> window._content = { state: ${JSON.stringify(store.getState())} } </script> <script src="/index.js"></script> </body> </html>`; }Copy the code
Client use
import useStyles from 'isomorphic-style-loader/useStyles'; const Home = () => { ... If (props. StaticContext) {useStyles(styles); } return <div> .... </div> }Copy the code
Finally, post the dependent version
< span style = "max-width: 100%; box-sizing: border-box; "^ 7.12.1 @", "Babel/plugin - transform - runtime" : "^ 7.12.1", "@ Babel/preset - env" : "^ 7.12.1", "@ Babel/preset - react" : "^ 7.12.1 @", "Babel/preset - stage - 0" : "^ 7.8.3", "@ Babel/runtime" : "^ 7.12.1", "axios" : "^ 0.21.0", "Babel - loader" : "^ 8.1.0 CSS -", "loader" : "^ 5.0.1", "isomorphic - style - loader" : "^ 5.1.0", "koa" : "^ 2.13.0", "koa - the router" : "^ 9.4.0 koa", "- - the proxy server - HTTP" : "^ 0.1.0 from", "koa - static" : "^ 5.0.0", "react" : "16.14.0", "the react - dom" : "16.14.0", "the react - redux" : "^ 7.2.2", "the react - the router - config" : "^ 5.1.1", "the react - the router - dom" : "^ 5.2.0", "story" : "^ 4.0.5 redux -", "thunk" : "^ 2.3.0", "style - loader" : "^ 2.0.0", "webpack" : "5.4.0", "webpack - cli" : "^ 4.1.0 webpack - node -", "externals" : "^ 2.5.2"}, "devDependencies" : {" redux - logger ":" ^ 3.0.6 ", "webpack - merge" : "^ 5.3.0}"Copy the code