Technology stack: WebPack 3.9.1+ Webpack-dev-server 2.9.5+ react16.x + Express4.x

preface

(good panic! Maybe IT’s because I’m lazy and… And then, well, I’m lazy, and then… Let’s do it!!

There are also a lot of SSR about React on the Internet, but they are not detailed enough, and some even make beginners confused. In this article, I will introduce you step by step to configure React SSR from 0, so that everyone who reads this article can use it.

The concept of SSR

Server Slide Rendering, abbreviated as SSR, refers to server-side Rendering. As A Java background, I understand how it works. In fact, SSR is mainly aimed at SPA applications, with the following purposes:

  1. Solve the single page SEO single page of the application of most major HTML is not returned from the server, the server just return a string of script, see most of the content on the page is generated by the script, for general website, but for some sites that rely on search engine traffic is fatal, Search engines can not grab the relevant content of the page, that is, the user can not search the relevant information of this website, naturally there is no traffic at all.
  2. Solve the rendering hang Because HTML page generated by the script returned by the server, in general the volume of the script will not be too small, need time to download the client, the browser parses to generate the page elements also need time, this will inevitably lead to the page display speed than traditional server side rendering to slowly, hang home page is easy to happen, Even if JS is disabled in the browser, basic elements of the page cannot be seen.

How to use server-side rendering in React

React – DOM is a rendering tool developed by React specifically for the Web. On the client side, we can render the react component using the Render method of the React-DOM. On the server side, the React-dom /server provides the method to render the React component into HTML.

The browser rendering is compared to the server rendering as follows :(the red box is the server rendering, which is obviously much faster than the browser rendering)

Project structures,

The project structure diagram is as follows:

The Build folder is used to configure the WebPack environment

  • Webpack.config.base.js is the base configuration
  • Webpack.config.client. js is the client packaging configuration
  • Webpack.config.server.js is used to package the server rendering configuration

package.json:

{
 "name": "juejin-reactssr"."version": "1.0.0"."description": ""."main": "index.js"."scripts": {
   "build:client": "webpack --config build/webpack.config.client.js"."build:server": "webpack --config build/webpack.config.server.js"."clear": "rimraf dist"."build": "npm run clear && npm run build:client && npm run build:server"."start":"node server/server.js"
 },
 "author": "Jerry"."license": "ISC"."dependencies": {
   "express": "^ 4.16.3"."react": "^ 16.2.0"."react-dom": "^ 16.2.0"."react-router": "^ 4.2.0"."react-router-dom": "^ 4.2.2." "
 },
 "devDependencies": {
   "babel-core": "^ 6.26.0"."babel-loader": "^ 7.1.2." "."babel-plugin-transform-decorators-legacy": "^" 1.3.4."babel-preset-es2015": "^ 6.24.1"."babel-preset-es2015-loose": "^ 8.0.0." "."babel-preset-react": "^ 6.24.1"."babel-preset-stage-1": "^ 6.24.1"."cross-env": "^ 5.1.1." "."file-loader": "^ 1.1.5." "."html-webpack-plugin": "^ 2.30.1"."http-proxy-middleware": "^ 0.17.4"."memory-fs": "^ 0.4.1"."react-hot-loader": "^ 3.1.3"."rimraf": "^ 2.6.2." "."uglifyjs-webpack-plugin": "^ 1.1.2." "."webpack": "^ 3.9.1." "."webpack-dev-server": "^ 2.9.5"."webpack-merge": "^ 4.1.2." "
 }
}

webpack.config.base.js:

` ``javascript const path = require('path') module.exports = { output: { path: path.join(__dirname, '.. /dist'), publicPath: '/public/', }, devtool:"source-map", module: { rules: [ { test: /.(js|jsx)$/, loader: 'babel-loader', exclude: [ path.resolve(__dirname, '../node_modules') ] } ] }, }Copy the code
Webpack. Config. Server. Js: ` ` ` javascript / / the js to client/server - entry. Js packaged into the node can perform file const path = require ('path')
const webpackMerge = require('webpack-merge')
const baseConfig = require('./webpack.config.base')

const config=webpackMerge(baseConfig,{
 target: 'node'Entry: {app: path.join(__dirname,'.. /client/server-entry.js'),
 },
 output: {
   filename: 'server-entry.js',
   libraryTarget: 'commonjs2'Commonjs2},}) module.exports = configCopy the code

Client folder Is used to package online clients

app.js:

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App.jsx'

ReactDOM.render(<App/>.document.getElementById('root'))

Copy the code

App.jsx:

import React from 'react'
export default class App extends React.Component{
 render(){
   return (
     <div>
       App
     </div>)}}Copy the code

Server-entry. js: This file is used to generate the template required for server rendering

// The template used by the server for rendering
import React from 'react'
import App from './App.jsx'
export default <App/>
Copy the code

template.html:

<! DOCTYPE html><html lang="en">
<head>
   <meta charset="UTF-8">
   <title>Title</title>
</head>
<body>
<div id="root"><! -- app --></div>
</body>
</html>
Copy the code

The server folder corresponds to the server

const express = require('express')
const ReactSSR = require('react-dom/server')
const serverEntry = require('.. /dist/server-entry')
const app = express()

app.get(The '*'.function (req, res) {
 / / ReactDOMServer renderToString is to render the React instance as HTML tags
 let appString = ReactSSR.renderToString(serverEntry.default);
 // return to the client
 res.send(appString);
})
app.listen(3000.function () {
 console.log('server is listening on 3000 port');
})
Copy the code

The following

We run NPM start, open the browser and type http://localhost:3000/ and we find that the server returns the rendered template. So far we have achieved the simplest SSR goal (but this is not our final goal, because only the rendered template is returned. We need to return to the entire page, which may also reference other js files.

We will continue to improve

Let’s go back to the server side and improve our server.js, where the + line indicates the additions

const express = require('express')
const ReactSSR = require('react-dom/server')
const serverEntry = require('.. /dist/server-entry')
+ const fs=require('fs')
+ const path=require('path')
const app = express()

// Import the index.html file generated by NPM run build
+ const template=fs.readFileSync(path.join(__dirname,'.. /dist/index.html'),'utf8')
app.get(The '*'.function (req, res) {
  / / ReactDOMServer renderToString is to render the React instance as HTML tags
  let appString = ReactSSR.renderToString(serverEntry.default);
  / / <! --App--> Position is where we insert the render result
  + appString=template.replace('<! --App-->',appString);
  // return to the client
  res.send(appString);
})
app.listen(3000.function () {
  console.log('server is listening on 3000 port');
})
Copy the code

Console NPM start, open the browser and type http://localhost:3000/ to find that the app.js file referenced by the page also returns the entire page, which is obviously not what we want

App.get (‘*’, function (req, res) {})

const express = require('express')
const ReactSSR = require('react-dom/server')
const serverEntry = require('.. /dist/server-entry')
const fs=require('fs')
const path=require('path')
const app = express()
All static files accessed through /public are static files
+ app.use('/public',express.static(path.join(__dirname,".. /dist")))
const template=fs.readFileSync(path.join(__dirname,'.. /dist/index.html'),'utf8')
app.get(The '*'.function (req, res) {
  / / ReactDOMServer renderToString is to render the React instance as HTML tags
  let appString = ReactSSR.renderToString(serverEntry.default);
  / / <! --App--> Position is where we insert the render result
  appString=template.replace('<! -- app -->',appString);
  // return to the client
  res.send(appString);
})
app.listen(3000.function () {
  console.log('server is listening on 3000 port');
})
Copy the code

So app.js returns the corresponding JS content instead of the entire page

The above is the whole process of our server SSR (PS: Of course, there is also a disadvantage at present is that we directly command line to start webpack for packaging, which can meet our needs. After all, plans don’t keep up with changes, and sometimes you’ll find it inconvenient to start WebPack from the command line. For example, we react in debugging the rendering of a service, we could not have a file each time update, waiting for the webpack packaged the output to a file on your hard disk, and then you restart the service to load the new file, because this is a waste of time, development time, after all, you could change the code at any time, and changes may be small.

So what can be done to solve this problem? We can start the WebPack service when we start the NodeJS service, so we can get the webPack context in the NodeJS execution environment, so we can not restart the service but can get the latest bundle every time the file is updated.

We’ll leave that question here (Todo…

Next, let’s start by looking at wepack-dev-Server and how Hot Module Replacement (or HMR) is one of the most useful features webPack provides. It allows various modules to be updated at run time without a complete refresh.)

Wepack-dev-server and HMR are not suitable for production environments, which means they should only be used in development environments, which we will configure next

Webpack dev – server configuration

First of all, the package. The json

"scripts": {
    "build:client": "webpack --config build/webpack.config.client.js"."build:server": "webpack --config build/webpack.config.server.js",
    + "dev:client":"cross-env NODE_ENV=development webpack-dev-server --config build/webpack.config.client.js"."clear": "rimraf dist"."build": "npm run clear && npm run build:client && npm run build:server"."start":"node server/server.js"
  }
Copy the code

webpack.config.client.js

const path = require('path')
const webpackMerge = require('webpack-merge')
const baseConfig = require('./webpack.config.base')
+ const webpack=require('webpack')
const HTMLWebpackPlugin = require('html-webpack-plugin')

// Determine if the current is a development environment
+ const isDev = process.env.NODE_ENV === 'development'

const config=webpackMerge(baseConfig,{
  entry: {
    app: path.join(__dirname, '.. /client/app.js'),},output: {
    filename: '[name].[hash].js',},plugins: [
    new HTMLWebpackPlugin({
      template: path.join(__dirname, '.. /client/template.html')})]})// localhost:8888/filename
+ if (isDev) {
  config.entry = {
    app: [
      'react-hot-loader/patch',
      path.join(__dirname, '.. /client/app.js')
    ]
  }
  config.devServer = {
    host: '0.0.0.0'.// Indicates that the local IP address localhost can be accessed in any way
    compress: true.port: '8888'.contentBase: path.join(__dirname, '.. /dist'),// Tell the server where to supply the content. Only if you want to provide static files
    hot: true.// Enable HMR mode
    overlay: {
      errors: true // Whether to display an error
    },
    publicPath: '/public'.historyApiFallback: {//404 Indicates the path configuration
      index: '/public/index.html'
    }
  }
  config.plugins.push(new webpack.NamedModulesPlugin(),
    new webpack.HotModuleReplacementPlugin())
}

module.exports = config
Copy the code

app.js:

import React from 'react'
import ReactDOM from 'react-dom'
+ import {AppContainer} from 'react-hot-loader'
import App from "./App.jsx";
+ const root=document.getElementById('root');
+ const render=Component= >{
  ReactDOM.render(<AppContainer><Component/></AppContainer>,root)

}
+ render(App);
+ if(module.hot){
  module.hot.accept('./App.jsx', () = > {const NextApp =require('./App.jsx').default; render(NextApp); })}Copy the code

Above, devServer and HMR have been configured

Modify the app.jsx content to see the content changed without refreshing the page

Back to where we left off (finish the server-side rendering at development time)

In server.js we distinguish between environment variables

const express = require('express')
const ReactSSR = require('react-dom/server')

const fs = require('fs')
const path = require('path')
const app = express()

+ const isDev = process.env.NODE_ENV === 'development'
+ if(! isDev) {The production environment reads the files directly from the generated dist directory
 const serverEntry = require('.. /dist/server-entry')
 All static files accessed through /public are static files
 app.use('/public', express.static(path.join(__dirname, ".. /dist")))
 const template = fs.readFileSync(path.join(__dirname, '.. /dist/index.html'), 'utf8')
 app.get(The '*'.function (req, res) {
   / / ReactDOMServer renderToString is to render the React instance as HTML tags
   let appString = ReactSSR.renderToString(serverEntry.default);
   / / <! --App--> Position is where we insert the render result
   appString = template.replace('<! -- app -->', appString);
   // return to the clientres.send(appString); })}else {// Development environment we read directly from memory minus the time written to hard disk
 const devStatic = require('./util/dev-static')
 devStatic(app);
}


app.listen(3000.function () {
 console.log('server is listening on 3000 port');
})
Copy the code

Create dev-static.js in the server directory to handle development-time server rendering

const axios = require('axios')
const webpack = require('webpack')
const path = require('path')
const serverConfig = require('.. /.. /build/webpack.config.server')
const ReactSSR = require('react-dom/server')
const MemoryFs = require('memory-fs')
const proxy = require('http-proxy-middleware')

//getTemplate to get the packed template (in memory)
const getTemplate = (a)= > {
 return new Promise((resolve, reject) = > {
   // HTTP to get the index. HTML in dev-server
   axios.get('http://localhost:8888/public/index.html')
     .then(res= > {
       resolve(res.data)
     }).catch(reject)
 })
}

const Module = module.constructor;

// Start a webpack in the Node environment to get the packaged server-entry.js
const mfs = new MemoryFs

// The server uses Webpack
const serverCompiler = webpack(serverConfig);
serverCompiler.outputFileSystem = mfs
let serverBundle
serverCompiler.watch({}, (err, stats) => {
 if (err) throw err
 stats = stats.toJSON()
 stats.errors.forEach(err= > console.error(err))
 stats.warnings.forEach(warn= > console.warn(warn))

 // Get the bundle path
 const bundlePath = path.join(
   serverConfig.output.path,
   serverConfig.output.filename
 )
 const bundle = mfs.readFileSync(bundlePath, 'utf8')
 const m = new Module()
 m._compile(bundle, 'server-entry.js')
 serverBundle = m.exports.default
})

module.exports = function (app) {
// HTTP proxy: everything accessed through /public is proxy to http://localhost:8888
 app.use('/public', proxy({
   target: 'http://localhost:8888'
 }))
 app.get(The '*'.function (req, res) {
   getTemplate().then(template= > {
     let content = ReactSSR.renderToString(serverBundle);
     res.send(template.replace('<! -- app -->', content)); })})}Copy the code

Meanwhile, NPM scripts is configured as follows:

"scripts": {
   "build:client": "webpack --config build/webpack.config.client.js"."build:server": "webpack --config build/webpack.config.server.js"."dev:client": "cross-env NODE_ENV=development webpack-dev-server --config build/webpack.config.client.js"."dev:server": "cross-env NODE_ENV=development node server/server.js"."clear": "rimraf dist"."build": "npm run clear && npm run build:client && npm run build:server"
 },
Copy the code

Run NPM run dev:client and NPM run dev:server to modify the contents of app. JSX browser

The above is the most basic configuration of React SSR and HMR, but data and routing are not involved. Next, I will introduce the configuration and deployment of mobx and React-Router based on this.githubWelcome to follow