Detailed description of the server rendering Transformation (koa2 + webpack3.11) and koa2webpack3.11 of the React Project
Because of the need for Web SEO, we need to transform the previous React project into server rendering. After some investigation and research, we have consulted a lot of Internet information. Step into the trap.
Selection idea: Implement server rendering. If you want to use the latest React version and do not make major changes to the existing writing method, if you plan to use server rendering at the beginning, we recommend that you use the NEXT framework to write it directly.
Project address: https://github.com/wlx200510/react_koa_ssr
Scaffolding Selection: webpack3.11.0 + react Router4 + Redux + koa2 + React16 + Node8.x
Experience: I am more familiar with React knowledge, have successfully expanded my technical fields, and have accumulated experience in practical projects.
Note: before using the framework, make sure that the current webpack version is 3.x Node 8. x or above. You are advised to use React for more than 3 months and have actual React project experience.
Project directory Introduction
── Assets │ ── index.css // some global resource files can be js images and other ├ ── config │ ├ ── webpack. config. dev. js Development Environment webpack packaging settings │ ── webpack. config. prod. set ├ ── package for webpack packaging in js production environment. json ├ ── README. md ├ ── server rendering file, if not very familiar with, it is recommended to refer to [koa tutorial] (http://wlxadyl.cn/2018/02/11/koa-learn/) │ ── app. js │ ── clientRouter. js // This file contains the logic of matching the server route to the react route │ ── ignore. js │ ── index. js └ ── src ├ ── app this folder is mainly used to place the general logic of browsers and servers │ ── conf IgureStore. js // redux-thunk setting │ ── createApp. js // set different router modes based on different rendering environments │ ── index. js │ ── router │ ├ index. js │ ── routes. js // route configuration file! Important ├ ── assets │ ├ ── css places some public style files │ ── _ base. scss // initialize css │ ── index for many projects. scss │ ── my. scss │ ── img ├ ── components place some public components │ ├ ── FloatDownloadBtn public component sample code │ ── FloatDownloadBtn. js │ ├ ── FloatDownloadBtn. scss │ ── index. js │ ── Loading. js │ ── Model. js function component writing │ ├ ── favicon. ico ── index. ejs // If the rendering template is required by the project, you can put some public files in ── index. js // contains the hot update logic ├ ── pages page component folder │ ── home │ ├ ── components // used to place page components, main logic │ ── homePage. js │ ── containers // use connect to encapsulate high-level components and inject global state data │ ── homeContainer. js │ ── index. js // page routing configuration file note thunk attributes │ CER │ └ ── index. js // the CER of the page is exposed here to store for unified processing. Note the Writing Method │ ── user │ ├ ── components │ └ ── userPage. js │ ── iners │ ── userContainer. js │ ── index. js └ ── store ├ ── actions // location where each action is stored │ ├ ── home. js │ ── thunk. js ├ ── constants. js // The collection of action names prevents duplicate-names ── ers CERs ── index. js // reference all reducers on each page for unified combine processing here
Project Construction ideas
- Local development uses webpack-dev-server for hot updates. The basic process is similar to previous react development and is still browser-side rendering. Therefore, you must consider a set of logic when writing code, two rendering environments.
- After the front-end page is rendered, its Router jump will not send requests to the server, thus reducing the pressure on the server. Therefore, there are two ways to access the page, we also need to consider the routing homogeneous problem in the two rendering environments.
- In the production environment, koa is used as the backend server to load data on demand, obtain data on the server, render the entire HTML, and merge the entire status tree with the latest React16 capabilities to implement server rendering.
Local Development
Check that the main file involved in local development is the index under the src directory. js file to determine the current runtime environment. The module is used only in the development environment. hot APIs are used to implement page rendering update notifications when CER changes. Pay attention to the hydrate method. This is an API method added specifically for server rendering in v16, based on the render method, it achieves the maximum possible reuse of the content rendered on the server, and implements the process from static DOM to dynamic NODES. In essence, the process of determining the checksum mark in v15 is replaced, making the reuse process more efficient and elegant.
const renderApp=()=>{ let application=createApp({store,history}); hydrate(application,document.getElementById('root'));}window.main = () => { Loadable.preloadReady().then(() => { renderApp() });};if(process.env.NODE_ENV==='development'){ if(module.hot){ module.hot.accept('./store/reducers/index.js',()=>{ let newReducer=require('./store/reducers/index.js'); store.replaceReducer(newReducer) }) module.hot.accept('./app/index.js',()=>{ let {createApp}=require('./app/index.js'); let newReducer=require('./store/reducers/index.js'); store.replaceReducer(newReducer) let application=createApp({store,history}); hydrate(application,document.getElementById('root')); }) }}
Note window. main Function Definition, combined with index. ejs can know that this function is triggered only after all the scripts are loaded. The react-loadable method is used for lazy page loading, the method for packaging pages should be explained in combination with route settings. Here is a general impression. It should be noted that the three methods exposed under the app file are common on the browser side and the server side. The next step is the idea of this part.
Route Processing
Next, let's take a look at the file in the src/app directory, index. js exposes three methods. The three methods involved are used in server and browser development. This section mainly describes the code ideas and createApp In the router file. the processing of routes in js files is the key point to achieve interconnection between routes at both ends.
The routes. js in the router folder is a routing configuration file, which leads the routing configurations on each page into a configuration array. You can use this configuration to flexibly control the online and offline pages. Index in the same directory. js is the standard method of RouterV4. It passes in the route configuration by traversing the configuration array. ConnectRouter is a component used to merge the Router. It is noted that history needs to be passed in as a parameter in createApp. js files for separate processing. First, let's take a look at several configuration items in the Route component. It is worth noting that the thunk attribute is a key step to implement rendering after the backend obtains data, it is precisely this attribute that implements a lifecycle hook similar to the component in Next that obtains data in advance. Other attributes can be found in the relevant React-router document, which is not described here.
import routesConfig from './routes';const Routers=({history})=>( <ConnectedRouter history={history}> <div> { routesConfig.map(route=>( <Route key={route.path} exact={route.exact} path={route.path} component={route.component} thunk={route.thunk} /> )) } </div> </ConnectedRouter>)export default Routers;
View the createApp under the app directory. the code in js can be found that this framework is different for different working environments, and Loadable is used only in the production environment. the Capture method implements lazy loading and dynamically introduces the packaged js files corresponding to different pages. Here, let's take a look at the writing of the routing configuration file in the component. Take index. js on the home page as an example. Note/* webpackChunkName: 'home' */specifies the js file name corresponding to the page after packaging. Therefore, you need to modify this comment for different pages, avoid packaging them together. The loading configuration item only takes effect in the development environment. It is displayed before the page is loaded. You can delete this component if you do not need it for actual project development.
Import {homeThunk} from '.. /.. /store/actions/thunk '; const LoadableHome = Loadable ({loader: () => import (/* webpackChunkName: 'home '*/'. /containers/homeContainer. js '), loading: Loading,}); const HomeRouter = {path:'/', exact: true, component: LoadableHome, thunk: homeThunk // server rendering will turn on and execute this action to get the data required for page rendering} export default HomeRouter
Here, we can say that sometimes the page files of the project we want to transform are from window. the code for getting parameters in location should be removed when transformed to server rendering, or used in the lifecycle after render. The page-level components have already been injected with related routing information. You can use this. props. location to obtain parameters in the URL. BrowserRouter is used in this project. If HashRouter is used, the parameters may be slightly different and can be used as needed.
According to React16 server rendering API introduction:
- The history in the ConnectedRouter injection used by the browser is: import createHistory from 'History/createBrowserHistory'
- The history used by the server is import createHistory from 'History/createMemoryHistory'
Server Rendering
This does not involve some basic knowledge of koa2. If you are not familiar with the koa2 framework, refer to another blog. Here we can see that all the server code is under the server folder. First, simple app. js is used to ensure that a new server instance is returned for each connection. This is a key idea for the single-threaded js language. The clientRouter. js file should be introduced in detail, and the/src/app/configureStore. js file should be used to understand the data acquisition process of server rendering and the React rendering mechanism.
/* ConfigureStore. js */import {createStore, applyMiddleware, compose} from "redux"; import thunkMiddleware from "redux-thunk"; import createHistory from 'History/createmoryhistory '; import {routerReducer, routerMiddleware} from 'react-router-redux 'import rootReducer from '.. /store/export CERs/index. js'; const routerjavascers = routerMiddleware (createHistory (); // route const composeEnhancers = process. env. NODE_E NV = 'development '? Window. _ cmdx_devtools_extension_compose _: compose; const middleware = [thunkMiddleware, router‑cers]; // inject the route to Cer. You can obtain the route information from reducer. let configureStore = (initialState) => createStore (rootReducer, initialState, composeEnhancers (applyMiddleware (... middleware); export default configureStore;
Window. _ javasx_devtools_extension_compose _ this variable is the Redux developer tool in the browser. We recommend that you install it when developing a React-redux application. Otherwise, an error message is displayed. Most of the code here is the sample code of redux-thunk. If you cannot understand this part, we suggest you read the official redux-thunk documentation. Note that the initialState parameter to be passed in the configureStore method, the specific idea of this rendering is: Determine the thunk method of the route on the server. If there is a route, execute the data retrieval logic. This is a blocking process and can be used as synchronization, put the obtained information in the global State and inject window into the HTML output at the front end. _ INITIAL_STATE _: global variable. After html is loaded, this variable assigns a value to the global State of existing data as the initState provided to the react application, after the browser js load is complete, the existing dom and the initial initState on the page will be reused and merged to the life cycle after render, so that the componentDidMount can be used from this. obtain the data required for rendering in props.
However, you must also consider that page switching may also be performed on the frontend. At this time, the React application will not trigger requests to the backend. Therefore, no data is obtained during the componentDidMount lifecycle, to solve this problem, we recommend that you call the action trigger function in props in this lifecycle, but perform a logical judgment within the action to avoid repeated requests, in actual projects, the request data usually has a identified ID, which can be stored in the store, and then a comparison validation can be performed to return in advance to avoid repeated ajax requests, for details, see store/actions/home. js.
Import {ADD, GET_HOME_INFO} from '.. /constants 'export const add = (count) => ({type: ADD, count,}) export const getHomeInfo = (sendId = 1) => async (dispatch, getState) ==>{ let {name, age, id} = getState (). homeReducer. homeInfo; if (id = sendId) {return // compare and verify the identity id of the Request id and existing data to avoid repeated data acquisition. } Console. log ('footer '. includes ('foo') await new Promise (resolve => {let homeInfo = {name: 'wd2010', age: '25', id: sendId} console. log ('----------- request gethomeinfo') setTimeout () => resolve (homeInfo), 1000 )}). then (homeInfo =>{ dispatch ({type: GET_HOME_INFO, data: {homeInfo }})})}
Note that the async/await method here involves the use of the server koa2 for data requests. Therefore, you need to return the async function in a unified manner. If you are not familiar with this method, you are advised to read ES7 knowledge, the main reason is how async works with Promise to Implement Asynchronous process transformation. If koa2 is involved in server work, more async functions are used. This is also the requirement of Node version 8 in this project. the two keywords can be used directly from 8.
However, some server parameter injection problems are often involved in specific projects. However, this issue varies greatly depending on different projects and is not part of the React server transformation and cannot be shared in a unified manner, if you really want to use a company project, if you have any need to consult this project, you can join me in the discussion.
Rendering process using the Home page as an Example
For your convenience, I used a page as an example to sort out the overall process of data streams. Let's take a look at the ideas:
- The server receives the request and finds the corresponding route configuration through/home.
- Determine whether a route exists in the thunk method, and then execute the function exposed in store/actions/thunk. js.
- Data obtained asynchronously is injected into the global state. The dispatch distribution does not take effect.
- In the HTML code to be output, the global state obtained from the data is put in the global variable window. _ INITIAL_STATE _, which serves as the initState.
- Window. _ INITIAL_STATE _ will merge into the global state before the react life cycle takes effect. At this time, react finds that the dom has been generated and does not trigger render again, and the data status is synchronized.
Server directly output HTML
The basic process has ended. Some of the function writing methods of the Reducer, and the locations of actions, are organized by referring to some analysis on the Internet. For details, see your own understanding, it also helps the team develop. If you meet the reader background I set at the beginning of this article, I believe this article is sufficient for you to light up your server rendering technology. It doesn't matter if you do not know much about React. You can refer to here to add some basic React knowledge.
The above is all the content of this article. I hope it will be helpful for your learning and support for helping customers.