React与SSR - 原始版

我们知道React等前端框架默认都是SPA+CSR (单页应用+客户端渲染),所谓客户端渲染就是服务端返回:空页面 + JS bundle文件,在客户端浏览器再通过JS bundle对指定的DOM节点进行页面渲染(生成html、css)。
这种CSR模式有两个主要问题
1:在加载完js bundle文件前,页面是空白的
2:搜索引擎不友好
所以,电商应用都会选择SSR模式(服务端渲染)。

那么如何实现React下的SSR呢? 大致分为两类:使用第三方框架(如NextJS)、使用React API自己实现SSR。
本文讨论如何用原生React API实现SSR。

//建立项目文件夹
mkdir my-react-ssr

//建立子文件夹
cd my-react-ssr
mkdir public src build server
cd src
mkdir server

//建立代码文件
touch publc/index.html
touch src/App.js
touch server/index

安装依赖:
npm init -y
npm install express --save
npm install react react-dom --save
npm install @babel/core @babel/preset-env @babel/preset-react babel-loader --save-dev
npm install webpack webpack-cli webpack-node-externals --save-dev
npm install clean-webpack-plugin --save-dev

npm install npm-run-all nodemon --save-dev

//以下部分为typescript准备
npm install @types/react @types/react-dom @babel/preset-typescript --save-dev

配置webpack
touch webpack.config.js
touch webpack.server.js
touch .babelrc.json

至此我们完成了配置和依赖,最后是代码部分
public/index.html 该文件相当于页面模板,其中的css、js目前都是hardcode,后面的文章再讨论如何动态生成该模板。

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>React SSR Demo</title>
    <link rel="stylesheet" href="https://res.ebdcdn.com/static/css/global.1622171660.css"></link>
    <link rel="stylesheet" href="https://res.ebdcdn.com/static/css/_ed3f20,collections.css,collections-studio.css"></link>
  </head>
  <body>
    <div id="root"></div>
    <script src="app.bundle.js" async></script>  
  </body>
</html>

src/server/index.js 该处使用express作为webserver,根据html模板和react组件生成html,注意:这里使用的renderToStaticMarkup,而非renderToString

import path from 'path';
import fs from 'fs';

import React from 'react';
import express from 'express';
import ReactDOMServer from 'react-dom/server';

//import Home from '../components/Home';
import ProdList from '../components/ProdList';

const PORT = 3006; //process.env.PORT || 3006;
const app = express();

app.get('/', (req, res) => {
    //const app = ReactDOMServer.renderToString(<Home />);
    const app = ReactDOMServer.renderToStaticMarkup(<ProdList />);
    
    const indexFile = path.resolve('./public/index.html');
    fs.readFile(indexFile, 'utf8', (err, data) => {
      
      if (err) {
        console.error('Something went wrong:', err);
        return res.status(500).send('Oops, better luck next time!');
      }
  
      return res.send(
        data.replace('<div id="root"></div>', `<div id="root" name="root123" attr1='001'>${app}</div>`)
      );
    });
  });

  app.use(express.static('./build'));
  
  app.listen(PORT, () => {
    console.log(`Server is listening on port ${PORT}`);
  });

src/App.js 该处代码中的hydrate意在当js bundle下载完成后,需要与页面现有的html(服务端renderToStaticMarkup生成的)进行调和

import React from 'react';
import ReactDOM from 'react-dom';
//import Home from './components/Home';
import ProdList from "./components/ProdList";

//ReactDOM.hydrate(<Home />, document.getElementById('root'));
ReactDOM.hydrate(<ProdList />, document.getElementById('root'));

.bablerc.json

{
    "presets": [
      "@babel/preset-env",
      "@babel/preset-react"
    ]
  }

package.json 中的scripts部分

  "scripts": {
    "build:client": "webpack --config webpack.config.js --mode=production",
    "build:server": "webpack --config webpack.server.js --mode=development",
    "start": "nodemon ./server/index.js",
    "all": "npm-run-all --parallel build:* start"
  },

webpack.config.js

const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const path = require('path');
module.exports = {
  entry: {app: './src/app.js'},
  module: {
    rules: [
      {
        test: /\.(ts|js)x?$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: [
              "@babel/preset-env",
              "@babel/preset-react",
              "@babel/preset-typescript",
            ],
          },
        }
      }
      // {
      //   test: /\.tsx?$/,
      //   use: 'ts-loader',
      //   exclude: [/node_modules/, /tests/],
      // },
    ],
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
  },
  output: {
    path: path.join(__dirname, 'build'),
    filename: '[name].bundle.js'
  },
  plugins: [
    new CleanWebpackPlugin()  
  ]
};

webpack.server.js

const path = require('path');
const nodeExternals = require('webpack-node-externals');

module.exports = {
  entry: {
    index: './src/server/index.js',
  },

  target: 'node',

  externals: [nodeExternals()],

  output: {
    path: path.resolve('server'),
    filename: '[name].js'
  },

  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'babel-loader'
      }
    ]
  }
};

源码地址

虽然这样也能实现SSR,但有几个明显的缺陷
1:express对路由支持太差
2:webconfig等配置、模板文件中的bundle文件需要手动修改
3:还没有实现css的加载
上述问题会在后续文章中逐一解决。

上一篇:vue-cli(vue2)按需引入mint-ui


下一篇:认识前端工具链(三)之测试工具