In this paper, THE SSR part is directly started and some problems and solutions are described through actual combat. We need to have a certain basic ability of React, Node and Webpack. SKR SKR.

Server side rendering

SSR, also known as SSR, is commonly used in our SPA (single-page Application).

Why SSR?

First of all, we need to know the benefits and advantages of SSR for SPA.

  • betterSEO (Search Engine Optimization).SEOIt’s search engine optimization, in short, for baidu and other search engines, so that they can search our application. There’s a mistake here, but I could write it in index.htmlSEOWhy doesn’t it work. Because React and Vue work like thisClient-side rendering, through the browser to load JS, CSS, there is a timedelay“And the search engines don’t caredelay“, he felt that if you did not load out is not, so it is impossible to search.
  • Solve the originalWhite during renderingReact rendersSSR server renderingIt is to request data through the server, because the server Intranet request is fast, good performance so it will be faster to load all the files, and finally the page after downloading and rendering is returned to the client.

Server rendering and client rendering were mentioned above, what is the difference between them?

Client rendering route:

  1. Request an HTML
  2. The server returns an HTML
  3. Browsers download JAVASCRIPT/CSS files in HTML
  4. Wait until the JS file is downloaded
  5. Wait for js to load and initialization to complete
  6. Js code can finally run, with the JS code requesting data from the back end (Ajax/FETCH)
  7. Wait for back-end data to return
  8. The React-DOM (client) renders the data as a response page, from nothing to complete

Server rendering route:

  1. Request an HTML
  2. Server requests data (Intranet requests data quickly)
  3. Server initial rendering (server side performance is good, fast)
  4. The server returns a page that already has the correct content
  5. Client requests JS/CSS files
  6. Wait until the JS file is downloaded
  7. Wait for js to load and initialization to complete
  8. React-dom (client) renders the rest of the rendering.

The main difference is that the client renders from scratch, with the server rendering part of the server and a small part of the client rendering.

How do we do server-side rendering?

Here we use express framework, Node as the middle layer for server-side rendering. Through isomorphic to the home page, and let the service side, by calling the ReactDOMServer. RenderToNodeStream method to convert the Virtual DOM HTML string returned to the client, so as to achieve the purpose of the service side rendering.

Here, the project started with the front end and back end completed, and the React Demo was directly used

Server rendering begins

Since it is the home SSR, first we need to extract the index.js corresponding to the home page and put it into the corresponding server.js of our server, then we need to package the static CSS and JS files corresponding to the components in the index.js.

Package files into the Build folder with Webpack

Let’s run NPM run build

We can see thatTwo important folders, one is JS folder, the other is CSS folder, which is the JS and CSS of our projectStatic resource file

Will be packed afterbuildFiles can be on the server sideserver.jsAccess to the

Since it is a server, we need express

import express from 'express'
import reducers from '.. /src/reducer';

import userRouter from './routes/user'
import bodyParser from 'body-parser'
import cookieParser from 'cookie-parser'
import model from './model'
import path from 'path'
import https from 'http'
import socketIo from 'socket.io'


const Chat = model.getModel('chat')
/ / the new app
const app = express()

//work with express
const server = https.Server(app)
const io = socketIo(server)
io.on('connection'.function(socket){
  socket.on('sendmsg'.function(data){
    let {from,to,msg} = data
    let chatid = [from,to].sort().join('_')
    Chat.create({chatid,from,to,content:msg},function(e,d){
      io.emit('recvmsg'.Object.assign({},d._doc))
    })
    // console.log(data)
    // // broadcast to global
    // io.emit('recvmsg',data)
  })
})

app.use(cookieParser())
app.use(bodyParser.json())
app.use('/user',userRouter)
app.use(function(req,res,next){
  if(req.url.startsWith('/user/') || req.url.startsWith('/static/')) {return next()
  }
  // If the url root path is user or static, return to the packaged home page
  return res.sendFile(path.resolve('build/index.html'))})// Map the build file path to be used on the project
app.use('/',express.static(path.resolve('build')))


server.listen(8088.function () {
    console.log('Open successfully')})Copy the code
  • Focus on the aboveapp.use('/',express.static(path.resolve('build')))andres.sendFile(path.resolve('build/index.html'))These two pieces of code.
  • They return the packaged home page to the client in server-side code.
  • Because I used it up hereimportCode, so we need it in our development environmentbabel-cliIn thebabel-nodeTo compile.
  • The installationnpm --registry https://registry.npm.taobao.orgI Babel -cli -s’, if you want to change the source, you can nextnrm, 360 degrees without dead Angle switch various sources, easy to use!
  • We need to modifypackage.jsonTo start the servernpm scripts."server": "NODE_ENV=test nodemon --exec babel-node server/server.js"
  • cross-envA plugin to set node environment variables across platforms.
  • Nodemon and Supervisor are both watch server files. They will run again once they are changedThermal overload. nodemonA lightweight
  • Finally, let’s take a runnpm run server, you can see the server running.

ReactDOMServer.renderToString/ReactDOMServer.renderToNodeStream

  • So we’re going to start with theThe browser.React.createElementLet’s do the React classinstantiation, the instantiated component can bemountAnd finally through React.renderRender to our client browser interface.
  • On the server we can get throughrenderToStringorrenderToNodeStreamThe React method instantiates the component and generates HTML tags directly. So what’s the difference between these two?
  • renderToNodeStreamReact 16 is the latest release that supports direct rendering to node flows. Rendering to stream can reduce the first byte of your content(TTFB)To send the beginning to the end of the document to the browser before the next part of the document is generated. As the content streams from the server, the browser will begin parsing the HTML document. The speed is renderToStringThree timesSo we use it hererenderToNodeStream
import express from 'express'
import React from 'react'
import {renderToStaticMarkup,renderToNodeStream} from 'react-dom/server'

import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
import {StaticRouter} from 'react-router-dom'
import {
  createStore,
  applyMiddleware,
  // use the combination function
  compose
} from 'redux';
import App from '.. /src/App'
import reducers from '.. /src/reducer';

import userRouter from './routes/user'
import bodyParser from 'body-parser'
import cookieParser from 'cookie-parser'
import model from './model'
import path from 'path'
import https from 'http'
import socketIo from 'socket.io'

const Chat = model.getModel('chat')
/ / the new app
const app = express()

//work with express
const server = https.Server(app)
const io = socketIo(server)
io.on('connection'.function(socket){
  socket.on('sendmsg'.function(data){
    let {from,to,msg} = data
    let chatid = [from,to].sort().join('_')
    Chat.create({chatid,from,to,content:msg},function(e,d){
      io.emit('recvmsg'.Object.assign({},d._doc))
    })
    // console.log(data)
    // // broadcast to global
    // io.emit('recvmsg',data)
  })
})


app.use(cookieParser())
app.use(bodyParser.json())
app.use('/user',userRouter)
app.use(function(req,res,next){
  if(req.url.startsWith('/user/') || req.url.startsWith('/static/')) {return next()
  }
  const store = createStore(reducers,compose(
    applyMiddleware(thunk)
  ))
  // This context object contains the rendered result
  let context = {}
  const root = (<Provider store={store}>
                    <StaticRouter
                      location={req.url}
                      context={context}
                      >
                        <App></App>
                    </StaticRouter>
                </Provider>)
  const markupStream = renderToNodeStream(root)
  markupStream.pipe(res,{end:false})
  markupStream.on('end',()=>{
    res.end()
  })
})
// Map the build file path to be used on the project
app.use('/',express.static(path.resolve('build')))


server.listen(8088.function () {
    console.log('Open successfully')})Copy the code

RenderToNodeStream = renderToNodeStream = renderToNodeStream = renderToNodeStreamnpm run server, you can see the error.

css-modules-require-hook/asset-require-hook

css-modules-require-hook

  • Because the server is nowDon't knowFor our CSS file, we need to install a package that lets the server process the CSS file.
  • npm i css-modules-require-hook -SInstalled in a production environment.
  • Create one in the project root directorycrmh.conf.jsThe hook file is configured as shown in the following figure.

Write the code

// css-modules-require-hook 
module.exports = {
  generateScopedName: '[name]__[local]___[hash:base64:5]'.// The following code is not currently used in this project, but the following configuration is useful in another project of mine
  / / extensions
  //extensions: ['.scss','.css'],
  // Hooks are used to preprocess SCSS or less files
  //preprocessCss: (data, filename) =>
  // require('node-sass').renderSync({
  // data,
  // file: filename
  // }).css,
  // Whether to export the CSS class name, mainly for CSSModule
  //camelCase: true,
};
Copy the code
  • Modify ourserver.jsFile, addimport csshook from 'css-modules-require-hook/preset'.Note ⚠ ️.Be sure to place this line of code before importing the App module.
import csshook from 'css-modules-require-hook/preset'
// Our home page entry
import App from '.. /src/App'
Copy the code

It’s runningserver.js, you will find that another error has been reported.

asset-require-hook

  • This error occurs because the server does not have the images needed to process the front-end code
  • You need to installnpm i asset-require-hook -SThis plugin allows the server to process images.Note ⚠ ️.The premise is that the client code, reference images need require
  • inserver.jsWrite the code
// The client code needs to require all references to images
import assethook from 'asset-require-hook'
assethook({
  extensions: ['png'].// Images below 10000 are base64 encoded directly
  limit: 10000
})
Copy the code

This is easy because we only have the reference name of imageNo address

  • So at this time to add a shell on the outside, build before and afterStatic JS and CSS filesGo in, add HTML, head tags. Look at the full code
import 'babel-polyfill'
import express from 'express'
import React from 'react'
import {renderToString,renderToStaticMarkup,renderToNodeStream} from 'react-dom/server'

// Import CSS files and js files
import staticPath from '.. /build/asset-manifest.json'

import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
import {StaticRouter} from 'react-router-dom'
import {
  createStore,
  applyMiddleware,
  // use the combination function
  compose
} from 'redux';
// Resolve server rendered images must be placed before App
import csshook from 'css-modules-require-hook/preset'
//解决图片问题,需要require
import assethook from 'asset-require-hook'
assethook({
  extensions: ['png'].limit: 10000
})
import App from '.. /src/App'
import reducers from '.. /src/reducer';

import userRouter from './routes/user'
import bodyParser from 'body-parser'
import cookieParser from 'cookie-parser'
import model from './model'
import path from 'path'
import https from 'http'
import socketIo from 'socket.io'

const Chat = model.getModel('chat')
/ / the new app
const app = express()

//work with express
const server = https.Server(app)
const io = socketIo(server)
io.on('connection'.function(socket){
  socket.on('sendmsg'.function(data){
    let {from,to,msg} = data
    let chatid = [from,to].sort().join('_')
    Chat.create({chatid,from,to,content:msg},function(e,d){
      io.emit('recvmsg'.Object.assign({},d._doc))
    })
    // console.log(data)
    // // broadcast to global
    // io.emit('recvmsg',data)
  })
})


app.use(cookieParser())
app.use(bodyParser.json())
app.use('/user',userRouter)
app.use(function(req,res,next){
  if(req.url.startsWith('/user/') || req.url.startsWith('/static/')) {return next()
  }
  const store = createStore(reducers,compose(
    applyMiddleware(thunk)
  ))
  const obj = {
    '/msg':'Chat Message List'.'/me':'List of Personal Centres'
  }
  // This context object contains the rendered result
  let context = {}
  res.write(` <! 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"> <meta name="description" content="${obj[req.url]}"/>
      <meta name="keywords" content="SSR">
      <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
      <link rel="stylesheet" href="/${staticPath['main.css']}">
      <title>React App</title>
    </head>
    <body>
      <noscript>
        You need to enable JavaScript to run this app.
      </noscript>
      <div id="root">`)
  const root = (<Provider store={store}>
                    <StaticRouter
                      location={req.url}
                      context={context}
                      >
                        <App></App>
                    </StaticRouter>
                </Provider>)
  const markupStream = renderToNodeStream(root)
  markupStream.pipe(res,{end:false})
  markupStream.on('end',()=>{
    res.write(`</div>
          <script src="/${staticPath['main.js']}"></script>
        </body>
      </html>`)
    res.end()
  })
})
// Map the build file path to be used on the project
app.use('/',express.static(path.resolve('build')))


server.listen(8088.function () {
    console.log('Open successfully')})Copy the code
  • This time we can add SEO meta in the HTML tag<meta name="keywords" content="SSR">
  • And finally, the clientindex.jsRender mechanism in file changed tohydrate, don’trenderThe difference between them can be seen in this (portal ☞render ! == hydrate)
ReactDOM.hydrate(
    (<Provider store={store}>
        <BrowserRouter>
            <App></App>
        </BrowserRouter>
    </Provider>),
    document.getElementById('root'))Copy the code

So far, the SSR under our development mode is set up. Next, I will talk about the pit of production mode.

Production environment SSR preparation

What we are talking about above is just SSR in development mode, because we are compiling JSX and ES6 code through Babel-Node, as soon as we leave Babel-Node it will be all wrong, so we need Webpack to package the server code

We need to create onewebserver.config.js, used to package the server code

const path = require('path'),
    fs = require('fs'),
    webpack = require('webpack'),
    autoprefixer = require('autoprefixer'),
    HtmlWebpackPlugin = require('html-webpack-plugin'),
    ExtractTextPlugin = require('extract-text-webpack-plugin')
    cssFilename = 'static/css/[name].[contenthash:8].css';
    CleanWebpackPlugin = require('clean-webpack-plugin');
    nodeExternals = require('webpack-node-externals');

serverConfig = {
  context: path.resolve(__dirname, '.. '),
  entry: {server: './server/server'},
  output: {
      libraryTarget: 'commonjs2'.path: path.resolve(__dirname, '.. /build/server'),
      filename: 'static/js/[name].js'.chunkFilename: 'static/js/chunk.[name].js'
  },
  // target: 'node' indicates that the built code is to run in the Node environment.
  // Do not package node.js built-in modules, such as FS NET modules, into output files
  target: 'node'.// Specify whether these modules are required in the Node environment
  node: {
      __filename: true.__dirname: true.// module:true
  },
  module: {
      loaders: [{
          test: /\.js$/.exclude: /node_modules/.loader: 'babel-loader? cacheDirectory=true'.options: {
              presets: ['es2015'.'react-app'.'stage-0'].plugins: ['add-module-exports'["import",
                {
                  "libraryName": "antd-mobile"."style": "css"}]."transform-decorators-legacy"]}}, {test: /\.css$/.exclude: /node_modules|antd-mobile\.css/.loader: ExtractTextPlugin.extract(
          Object.assign(
            {
              fallback: {
                loader: require.resolve('style-loader'),
                options: {
                  hmr: false,}},use: [{loader: require.resolve('css-loader'),
                  options: {
                    importLoaders: 1.minimize: true.modules: false.localIdentName:"[name]-[local]-[hash:base64:8]".// sourceMap: shouldUseSourceMap,}, {},loader: require.resolve('postcss-loader'),
                  options: {
                    ident: 'postcss'.plugins: (a)= > [
                      require('postcss-flexbugs-fixes'),
                      autoprefixer({
                        browsers: [
                          '> 1%'.'last 4 versions'.'Firefox ESR'.'not ie < 9'.// React doesn't support IE8 anyway].flexbox: 'no-2009',}),],},},}, {test: /\.css$/.include: /node_modules|antd-mobile\.css/.use: ExtractTextPlugin.extract({
          fallback: require.resolve('style-loader'),
          use: [{
            loader: require.resolve('css-loader'),
            options: {
              modules:false},}]})}, {test: /\.(jpg|png|gif|webp)$/.loader: require.resolve('url-loader'),
            options: {
              limit: 10000.name: 'static/media/[name].[hash:8].[ext]',}}, {test: /\.json$/.loader: 'json-loader',}},// Instead of packing third-party modules in the node_modules directory into the output file,
  externals: [nodeExternals()],
  resolve: {extensions: [The '*'.'.js'.'.json'.'.scss']},
  plugins: [
      new CleanWebpackPlugin(['.. /build/server']),
      new webpack.optimize.OccurrenceOrderPlugin(),
      // Separate the third-party library from the js file
      new webpack.optimize.CommonsChunkPlugin({
        // Remove the chunk's common node_module
        minChunks(module) {
          return /node_modules/.test(module.context);
        },
        // Remove the same module from the sub-chunks of the chunk to be removed
        children: true./ / whether asynchronous out public module, parameter Boolean | | string
        async: false,}).new webpack.optimize.CommonsChunkPlugin({
        children:true.// If the argument is string, it is the extracted file name
        async: 'shine'.// The minimum number of file modules to be packed, i.e. the number of public modules to be removed. For example, if only one of the three chunks is used, it is not public
        // If it is Infinity, it will put the webPack Runtime code init (WebPack will no longer be automatically removed from the public module)
        minChunks:2
      }),
      / / compression
      new webpack.optimize.UglifyJsPlugin(),
      // Separate CSS files
      new ExtractTextPlugin({
        filename: cssFilename,
      }),
      new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
  ],
}

module.exports =  serverConfig
Copy the code

The key ⚠ ️

  • Specify target, where does the packaged code run
  • Specify externalsnode_modulesPackage package, because this project runs on the server side, directly using the outsidenode_modulesWill do. It’s gonna be big when you pack it up.
  • Loader uses Babel to process JS

Ok, now let’s change the package.json NPM scripts, add a packServer, and change the build scripts

  "scripts": {
    "clean": "rm -rf build/"."dev": "node scripts/start.js"."start": "cross-env NODE_ENV=development npm run server & npm run dev"."build": "npm run clean && node scripts/build.js && npm run packServer"."test": "nodemon scripts/test.js --env=jsdom"."server": "cross-env NODE_ENV=test nodemon --exec babel-node server/server.js"."gulp": "cross-env NODE_ENV=production gulp"."packServer": "cross-env NODE_ENV=production webpack --config ./config/webserver.config.js"
  },
Copy the code
  • packServerSpecifies the production environment, which will be used later.
  • buildClean the build folder first and then pack itClient codePack your bags when you’re doneServer-side code

So at this point we can almost try it out for ourselves

  • First,npm run build, will generate a packaged build folder containing ourServer and client code
  • Find the packed Node file and run it inbuild/server/static/jsDirectory, you can directly start the node file. This solves the problem in our production environment.

Pm2: Automatic server deployment

Now we will deploy our project to the server and use the PM2 daemon.

  • First we have to have a cloud server, and here I amAli cloudBuy oneUbuntu 14.04
  • Need a has been put on record after the domain name, the domain name can also be bought in Ali cloud. Of course, you can also not, you can directly access the server address.
  • Ok, let’s get started.

Server Deployment

  • There are a few things we need to change in our code before deploying to the server, the mongod connection address.
const env = process.env.NODE_ENV || 'development'
// In the production environment, you need to change the connection port of mongodb based on your server's mongodb port
const BASE_URL = env == 'development'?"mongodb://localhost:27017/chat":"Mongo: / / 127.0.0.1:19999 / chat";
Copy the code
  • Modifying a Clientsocket.ioLink address ofconst socket = io('ws://host:port')Change the server address and port number to your own
  • We need to upload our project to the code cloud. I use the code cloud here mainly because the private storage of the code cloud is free.
  • We need access to the serverSSH directoryThe copyid_rsa.pubThe public key in the code cloudSSH public keyIn, you can enterSet up theLook at the picture

  • We also need to put our own computerSSH public keyIn the code cloud Settings, I’m a MAC, in their own user directory, you can presscmd+shift+.Look at hidden files (skip this step if you have set them).
  • Git,mongodb, pM2,nginx are installed on the server(Not necessary if the server is already installed)
  • Mongodb needs to be enabled
  • Let’s create a new one in the project root directoryecosystem.jsonFile, this file is the PM2 configuration file, I will not say the specific, if you are interested in the official website to see, (portal ☞Pm2 website)
{
  "apps": [{// Application name
      "name": "chat".// The path to the execution file
      "script": "./build/server/static/js/server.js"."env": {
        "COMMON_VARIABLE": "true"
      },
      "env_production": {
        "NODE_ENV": "production"}}]."deploy": {
    "production": {
      // Server user
      "user": "xxx".// Server address
      "host": ["xxx"].// Server port
      "port": "xxx"."ref": "origin/master".// Enter your project git SSH here
      "repo": "xxx".// The project path of the server
      "path": "/www/chat/production"."ssh_options": "StrictHostKeyChecking=no"./ / hooks
      "post-deploy": "npm --registry https://registry.npm.taobao.org install && npm run build && pm2 startOrRestart ecosystem.json --env production"."env": {
        / / environment
        "NODE_ENV": "production"}}}}Copy the code
  • Create a new project in the server New project directory/www/chat/Folder.
  • Execute on the local computerpm2 deploy ecosystem.json production setup
  • There’s gonna be a mistake here, and I planted this hole on purpose becausechatFolder permission is not enough, need to enter the serverwwwFolder, executesudo chmod 777 chat.
  • Enter the server’s.bashrc file and look at a few lines of code
  • source .bashrcLet me reload it.bashrcfile
  • Enable the PM2 service pm2 deploy file. json production
  • The main reason is that the pM2 on the local computer has permission problems. You need to find the PM2 folder.chmod 666 pm2
  • If you solve all of these problems you end up with something like this

  • And finally we can go to the server,pm2 listSee success and run

  • If the application is in constantrestart, indicating onfailureThe need,pm2 logsLook at the log

  • We can accessServer ADDRESS :8088And see the application run

Domain name agent

  • ▌ We enter ali Cloud console to resolve our domain name (Portal Ali Cloud)

  • Add a record

  • Back on the server, we modify the nginx configuration file, via a reverse proxy, so that we can also access it by domain name
upstream chat {
  server 127.0.0.1:8088;
}

server {
  listen 80;
  server_name www.webman.vip;

  location / {
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forward-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_set_header X-Nginx-Proxy true;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";

    proxy_pass http://chat;
    proxy_redirect off;
  }
  Static file address
  location ~* ^.+\.(jpg|jpeg|gif|png|ico|css|js|pdf|txt){
    root/www/website/production/current/build; }}Copy the code
  • Run sudo nginx -s reload on the server to restart nginx. At this point we can access our application through our domain address.

  • Sudo vi /etc/iptables.up.rules: /etc/iptables.up. Sudo iptables-restore < /etc/iptables.up.rules

-A INPUT -s 127.0.0.1 -p tcp --destination-port 8088 -m state --state NEW,ESTABLISHED -j ACCEPT
-A OUTPUT -d 127.0.0.1 -p tcp --source-port 8088 -m state --state ESTABLISHED -j ACCEPT
Copy the code
  • Check whether the security group of Ali Cloud console has opened the corresponding port

  • Finally finally!! At last I succeeded. You can check it out by clicking the link. Go you!

  • Next time, if you want to update your project directly, you can submit it to Git in the corresponding path and then deploy it on the server using pM2 deploy file. json production.