Preface
The content of this story is mainly a set of ssr frameworks built by react and koa. It adds some of its own ideas and improves its own functions on wheels made by others.
The technologies used this time are: react | rematch | react-router | koa
React server rendering advantages
Although SPA (single page application) is more friendly to interactive experience than traditional multi-page, it also has a natural flaw, which is that it is not friendly to search engines and is not conducive to crawlers crawling data (although I heard that Chrome can crawl spa page data asynchronously);
Compared with traditional SPA (Single-Page Application - Single-page Application), the main advantages of server-side rendering (SSR) are: better SEO and first-screen loading effects.
When SPA is initialized, the content is an empty div. You must wait for js to download before you start rendering the page. However, SSR can directly render the html structure, greatly optimizing the loading time of the first screen. But God is fair. This approach also increases our great development cost. Therefore, everyone must develop in a comprehensive way to the importance of the first screen time to the application, and perhaps it is better to replace the product (skeleton screen).
react server rendering process
Component Rendering
First of all, it must be the render of the root component, and this part is a little different from SPA.
Containers that use () to mix server renderings have been deprecated and will be removed in React 17. Use hydrate() instead.
hydrate is the same as render , but is used to mix containers whose HTML content is rendered by ReactDOMServer. React will attempt to attach the event listener to an existing tag.
hydrate describes the process in which ReactDOM reuses the content rendered by the ReactDOMServer server as much as possible and supplements client-specific content such as event binding.
import React from 'react'; import ReactDOM from 'react-dom'; (<App />, ('app'));
On the server side, we can use renderToString to retrieve the rendered content to replace things in the html template.
const jsx = <StaticRouter location={url} context={routerContext}> <AppRoutes context={defaultContext} initialData={data} /> </StaticRouter> const html = (jsx); let ret = ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no"> </head> <body> <div >${html}</div> </body> </html> `; return ret;
The server returns the replaced html and completes the server rendering of the component.
Router synchronous rendering
In the project, we cannot avoid using routing, but in SSR, we must achieve synchronous rendering of routes.
First, we can split the route into a component, and both the server portal and the client can be referenced separately.
function AppRoutes({ context, initialData }: any) { return ( <Switch> { ((d: any) => ( <Route<InitRoute> key={} exact={} path={} init={ || ''} component={} /> )) } <Route path='/' component={Home} /> </Switch> ); }
()
export const routes = [ { path: '/Home', component: Home, init: , exact: true, }, { path: '/Hello', component: Hello, init: , exact: true, } ];
In this way, our route is basically defined, and then client references are still the same rule, and there is no difference between them and SPA
import { BrowserRouter as Router } from 'react-router-dom'; import AppRoutes from './AppRoutes'; class App extends Component<any, Readonly<State>> { ... render() { return ( <Router> <AppRoutes/> </Router> ); } }
On the server, you need to replace BrowserRouter with StaticRouter. The difference is that BrowserRouter will keep the page and URL synchronize through the history API provided by HTML5, while StaticRouter will not change the URL. When a match is made, it will pass the context object to the component rendered as staticContext.
const jsx = <StaticRouter location={url}> <AppRoutes /> </StaticRouter> const html = (jsx);
At this point, the synchronization of the route has been completed.
redux isomorphism
Before writing this, you must first understand what water injection and dehydration are. The so-called dehydration means that the server processes some pre-requests before building HTML and injects data into the html and returns it to the browser. Water injection means that the browser initializes the components as initial data to complete the unity of the server and browser data.
Component configuration
Define a static method inside the component
class Home extends { ... public static init(store:any) { return (5); } componentDidMount() { const { incrementAsync }:any = ; incrementAsync(5); } render() { ... } } const mapStateToProps = (state:any) => { return { count: }; }; const mapDispatchToProps = (dispatch:any) => ({ incrementAsync: }); export default connect( mapStateToProps, mapDispatchToProps )(Home);
Since I am using rematch here, our methods are written in the model.
const Home: ModelConfig= { state: { count: 1 }, reducers: { increment(state, payload) { return { count: payload }; } }, effects: { async incrementAsync(payload, rootState) { await new Promise((resolve) => setTimeout(resolve, 1000)); (payload); } } }; export default Home;
Then init through the root store.
import { init } from '@rematch/core'; import models from './models'; const store = init({ models: {...models} }); export default store;
It can then be bound in our redux provider.
<Provider store = {store}> <Router> <AppRoutes context={context} initialData={} /> </Router> </Provider>
In terms of routing, we need to bind the component's init method to the route to facilitate the server to use when requesting data.
<Switch> { ((d: any) => ( <Route<InitRoute> key={} exact={} path={} init={ || ''} component={} /> )) } <Route path='/' component={Home} /> </Switch>
The above is what the client needs to do, because our server also needs to perform data operations in SSR, so in order to decouple, we create another ServiceStore to provide server use.
Before building HTML on the server, we must first execute the init method of the current component.
import { matchRoutes } from 'react-router-config'; // Use matchRoutes method to get the corresponding component array of matched routesconst matchedRoutes = matchRoutes(routes, url); const promises = []; for (const item of matchedRoutes) { if () { const promise = new Promise((resolve, reject) => { (serverStore).then(resolve).catch(resolve); }); (promise); } } return (promises);
Note that we create a new Promise array to place the init method, because a page may be composed of multiple components, we must wait for all init execution before performing the corresponding html build.
The data you can get is now hung under the window and wait for the client to read.
let ret = ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no"> </head> <body> <div >${html}</div> <script type="text/javascript">window.__INITIAL_STORE__ = ${( || {} )}</script> </body> </html> `;
Then read the initialStore data just now in our client
.... const defaultStore = window.__INITIAL_STORE__ || {}; const store = init({ models, redux: { initialState: defaultStore } }); export default store;
At this point, the isomorphism of redux has been basically completed. Because of the limitation of edges, I have not posted too much code. You can click on my repository at the bottom of the article to see the specific code. Then I will talk about a few more pitfalls in the isomorphism of redux.
1. The @loadable/component is not used asynchronous component loading because the internal methods of the component cannot be obtained. The solution is to not place the pre-request in the component, and we will split it out and write it to a file to manage it in a unified file. However, I thought it was difficult to manage, so I gave up loading the component asynchronously.
2. If the data flashes when rendering on the client, it means that the initialization data has not been successful. I was stuck here for a long time.
CSS style straight out
First of all, when parsing the css file, you cannot use style-loader when parsing it. You must use isomorphic-style-loader. When using style-loader, there will be a flash of passing because the browser needs to load css to add the style. To solve this problem, we can use isomorphic-style-loader to place css in the global context when the component is loaded, then extract it during server rendering and insert it into the style tag in the returned HTML.
Component transformation
import withStyles from 'isomorphic-style-loader/withStyles'; @withStyles(style) class Home extends { ... render() { const {count}:any = ; return ( ... ); } } const mapStateToProps = (state:any) => { return { count: }; }; const mapDispatchToProps = (dispatch:any) => ({ incrementAsync: }); export default connect( mapStateToProps, mapDispatchToProps )(Home);
withStyle is a curry function, which returns a new component and does not affect the connect function. Of course, you can also write it like connect. withStyle is mainly used to insert style into the global context.
Modification of root component
import StyleContext from 'isomorphic-style-loader/StyleContext';
const insertCss = (...styles:any) => { const removeCss = ((style:any) => style._insertCss()); return () => ((dispose:any) => dispose()); }; ( < value={{ insertCss }}> <AppError> <Component /> </AppError> </>, elRoot );
This part mainly introduces the StyleContext initialization of the root context and defines an insertCss method to trigger it in the component withStyle.
Some isomorphic-style-loader source code
... function WithStyles(props, context) { var _this; _this = _React$(this, props, context) || this; _this.removeCss = (context, styles); return _this; } var _proto = ; _proto.componentWillUnmount = function componentWillUnmount() { if () { setTimeout(, 0); } }; _proto.render = function render() { return (ComposedComponent, ); }; ...
You can see that the insert method in context is the defined insert method in the root component, and the previous style is cleared during the destruction life cycle of componentWillUnmount. The insert method is mainly to define the id and embed the current style. I will not expand it here. If you are interested, you can take a look at the source code.
Get the defined css in the server
const css = new Set(); // CSS for all rendered React components const insertCss = (...styles :any) => { return ((style:any) => (style._getCss())); }; const extractor = new ChunkExtractor({ statsFile: }); const jsx = ( < value={{ insertCss }}> <Provider store={serverStore}> <StaticRouter location={url} context={routerContext}> <AppRoutes context={defaultContext} initialData={data} /> </StaticRouter> </Provider> </> ); const html = (jsx); const cssString = (css).join(''); ...
Among them, cssString is the last css content we get. We can embed css into html like html replacement.
let ret = ` <!DOCTYPE html> <html lang="en"> <head> ... <style>${}</style> </head> <body> <div >${html}</div> ... </body> </html> `;
Then this will be done! ! ! !
Let me talk about the pitfalls I encountered while doing this
1. You cannot use the plug-in for separating css mini-css-extract-plugin, because separating css and placing css in style will conflict. This is what the github master said.
With isomorphic-style-loader the idea was to always include css into js files but render into dom only critical css and also make this solution universal (works the same on client and server side). If you want to extract css into separate files you probably need to find another way how to generate critical css rather than use isomorphic-style-loader.
2. Many articles say that css is not required to be packaged in service side packaging, because they are using style-loader. If we use isomorphic-style-loader, we also need to package css because we have to trigger withStyle on the server side.
Summarize
Because there is too much code, it only shows the idea of the entire SSR process, and the detailed code can be viewed. I hope that the big guys will guide my mistakes, thank you very much! !
The above is all the content of this article. I hope it will be helpful to everyone's study and I hope everyone will support me more.