手把手教你实现React SSR服务端渲染【含demo实现】

最近遇到的新需求,网上的资料啥都有,就是不太满足自己所需,捣鼓很久终于实现了,本文绝对详细【含实现】!

本文支持:react-ssr实现、react路由、ts语法、material-ui材料库

浏览器解析过程

从输入页面URL到页面渲染完成大致流程为:

  • 解析URL

  • 浏览器本地缓存

  • DNS解析

  • 建立TCP/IP连接

  • 发送HTTP请求

  • 服务器处理请求并返回HTTP报文

  • 浏览器根据深度遍历的方式把html节点遍历构建DOM树

  • 遇到CSS外链,异步加载解析CSS,构建CSS规则树

  • 遇到script标签,如果是普通JS标签则同步加载并执行,阻塞页面渲染,如果标签上有defer / async属性则异步加载JS资源

  • 将dom树和CSS DOM树构造成render树

  • 渲染render树

一.主流渲染方式

要实现服务端渲染,先要了解一下几个概念

1.1 CSR客户端渲染

1.1.1 概念

客户端渲染,指页面上的内容由浏览器执行js脚本而生成。

  • React SPA 单页面应用项目就是客户端渲染

    • 原理:webpack将所有代码打包成想要脚本,然后插入到HTML界面中,而浏览器会解析script脚本,再通过动态插入DOM方式展示页面

    • 浏览器中的index.html < div id="root"></ div> 和 一些 script 脚本

1.1.2 流程

浏览器请求HTML,React项目中利用webpack将页面打包成bundle.js脚本,插入index.html,然后再把index.html和打包好的bundle.js都发给浏览器,浏览器解析index.html获取静态页面,再解析其中的bundle.js脚本,获取页面的动态数据和交互事件,向静态页面插入数据,最终形成一个完成页面。

即在浏览器中输出组件,通过执行js脚本,渲染页面到浏览器,生成DOM和操作DOM来实现用户交互

html 仅仅作为静态文件,客户端在请求时,服务端不做任何处理,直接以原文件的形式返回给客户端客户端,然后根据 html 上的 JavaScript,生成 DOM 插入 html。

1.1.3 优缺点

优点
  • 前端负责渲染页面,后端负责实现接口,两者各司其职

  • 前端跳转时,无需请求后台,加速页面跳转,提高体验

缺点
  • 内容通过JS加载,搜索引擎无法爬取分析网页内容,不利于SEO

    • SEO指搜索引擎优化,即搜索引擎可准确理解并反映网站内容)

  • 需要等待整个应用的js加载以及通过接口数据请求获取页面动态数据,因此首屏加载时间长

1.2 SSR服务端渲染

1.2.1 概念

SSR服务端渲染(Server-side Rendering),指在浏览器发起页面请求后由服务端完成页面的HTML结构拼接,返回至浏览器解析后能直接构建出有内容的页面

  • 实现:

    • 利用Node的express框架,当浏览器请求时,通过服务端直接将完整页面返回给浏览器,而不通过js

      【怎么给浏览器?】

      • 方法一:并利用express.send()方法直接将页面发送给浏览器。

      • 方法二:结合ejs模板引擎并利用express.render根据模板引擎渲染页面,再发送给浏览器

1.2.2 流程

浏览器请求HTML,Nodejs利用express框架,get()获取请求,send()将组件转化后的HTML字符串直接返回给页面

即将组件在服务端转化为HTML字符串,直接将其发给浏览器,然后将其中的静态标记“激活“为客户端上完全可交互的应用程序。

当用户第一次请求页面时,由服务器把需要的组件或页面渲染成 HTML 字符串,然后把它返回给客户端。客户端拿到手的,是可以直接渲染然后呈现给用户的 HTML 内容,不需要为了生成 DOM 内容自己再去跑一遍 JS 代码。使用服务端渲染的网站,可以说是“所见即所得”,页面上呈现的内容,我们在 html 源文件里也能找到。

1.2.3 优缺点

优点
  • 服务端直接输出HTML,SEO友好

  • 加载首页无需加载整个应用的JS,因此首页加载速度快

缺点
  • 页面每次跳转都需要访问服务器,体验比客户端渲染差

1.3 同构渲染

本人查阅资料,发现通常实现的SSR渲染,并非单纯的服务端渲染,而是两者都有,所以如果听到要实现SSR渲染,多半都是同构渲染!!

  • 通过了解CSR和SSR,我们发现两者的优缺互补

  • 因此就出现了将传统的纯服务端直出的首屏优势和客户端渲染站内跳转优势结合的同构渲染

  • 也是常说的Server Side Rendering(SSR渲染)

1.3.1 概念

同构渲染,就是一套React代码在服务器上运行一次,在浏览器上再运行一次。而服务器端完成页面结构,客户端渲染绑定事件。

  • 代码在前端服务器上运行,生成HTML结构,并设置静态资源插入js文件,发给浏览器

  • 浏览器通过加载js文件,绑定DOM事件

  • 客户端渲染此时再接管页面,完成路由跳转

1.3.2 流程

二.服务端渲染实现

根据以上渲染方式的说明,我们可以知道:要实现一个完整的React-SSR渲染,要做的事如下:

  • 第一步:服务端执行React代码,生成HTML页面,并将打包好的客户端.js文件插入HTML页面中

  • 发送HTML页面至浏览器展示

  • 浏览器加载HTML页面中的客户端.js文件

  • 客户端js代码执行,并绑定DOM事件,最后接管页面操作

【负责】

  • 客户端:事件绑定、路由跳转

  • 服务端:请求数据、绑定动态数据、渲染生成HTML

2.1 Node服务端渲染

下面演示利用Nodejs、express实现一个单纯的服务端渲染:

1. 安装 Node.js 和 Express

首先,确保你的计算机已安装 Node.js。然后,安装 Express:

npm install express
2. 创建 Express 应用

接下来,我们创建一个名为 ssr-demo 的文件夹,并在其中创建一个名为 app.js 的文件:

mkdir ssr-demo
cd ssr-demo
touch app.js
3. 编写服务端代码

服务端需要拼接HTML字符串,然后发送给浏览器解析

发送HTML字符串,有以下两种方式:

  • 第一种:直接进行字符串拼接,再利用res.send(HTML字符串),发给浏览器

  • 第二种:利用ejs模板引擎生成,再利用res.render("ejs存文件名",{xxx,xxx}),渲染模板并发给浏览器

【ejs是什么?】

ejs就是一个模板引擎,用于更加简便的书写在服务端渲染的html代码

其中通过res.render传入所需的动态数据/url

不使用ejs

打开 app.js 文件,编写以下代码:

const express = require('express');
const app = express();
const port = 3000;
​
app.get('/', (req, res) => {
  const html = `
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>SSR Demo</title>
    </head>
    <body>
        <h1>Welcome to Server-Side Rendering!</h1>
        <p>This page is rendered on the server.</p>
    </body>
    </html>
  `;
  res.send(html);
});
​
app.listen(port, () => {
  console.log(`Server is running at http://localhost:${port}`);
});

这段代码会创建一个简单的 Express 服务器,并在根目录(/)下返回一个 HTML 字符串,这就是一个简单的服务端渲染示例。

使用ejs

ejs语法见文档:EJS -- 嵌入式 JavaScript 模板引擎 | EJS 中文文档

首先在当前目录下,创建一个views文件,将index.ejs放入

server.tsx

import express from "express";
import path from "path";
​
const server = express();
//设置视图引擎为ejs
server.set("view engine", "ejs");
//设置视图文件位置,__dirname表示当前目录,即当前目录下的views文件夹
//渲染页面时,就会从该目录下寻找res.render()中设置的ejs文件名
server.set("views", path.join(__dirname, "views"));
​
const html = <div>首页</div>
server.get("/", (req, res) => {
  //参数一:渲染的ejs文件名
  //参数二:传给ejs文件的参数值
  res.render("index", {  html });
});
​
server.listen(3000, () => {
  console.log("Server running on http://localhost:3000");
});
​

index.ejs

<!DOCTYPE html>
<html lang="CN">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SSR Demo</title>
  </head>
  <body>
    <div id="root"><%- html %></div>
  </body>
</html>
4. 运行应用

在终端中,运行以下命令启动服务器:

node app.js

然后,打开浏览器访问 http://localhost:3000,你将看到服务端渲染的页面。

2.2 React项目服务端渲染

express服务返回的只是普通字符串,我们可是要返回react的组件!

下面开始演示react项目的服务端渲染↓

1.同Node服务端渲染
  • 安装Node.js和Express

2. 创建React组件
import React from 'react';
const Home = () => {
  return (
    <div>
      <div>用于测试React服务端渲染的组件</div>
    </div>
  )
}
export default Home
3.编写服务端代码

renderToString 是react-dom/server提供的服务端渲染方法

renderToString、renderToStaticMarkup、renderToNodeStream、renderToStaticNodeStream四个方法能够将 React 组件渲染成静态的(HTML)标签,前两者能在客户端和服务端运行,后两者只能在服务端运行。

不使用ejs
import express from 'express';
//改动1:导入react-dom18,此版本中提供了一系列react组件用于服务端渲染的方法
import { renderToString } from 'react-dom/server';
import App from './client/App';//导入创建的react组件
​
const app = express();
//改动2:利用renderToString,将 React 树渲染为一个 HTML 字符串。
const content = renderToString(<Home />);
//改动3:将渲染后的React树,插入HTML结构中,并发给浏览器                               
app.get('/', function (req, res) {
   res.send(
   `
    <html>
      <head>
        <title>ssr</title>
      </head>
      <body>
        <div id="root">${content}</div>
      </body>
    </html>
   `
   );
})
app.listen(3000, () => {
  console.log('listen:3000')
})
使用ejs

server.tsx

import express from "express";
import path from "path";
import App from "../client/App"
const server = express();
//设置视图引擎为ejs
server.set("view engine", "ejs");
//设置视图文件位置,__dirname表示当前目录,即当前目录下的views文件夹
//渲染页面时,就会从该目录下寻找res.render()中设置的ejs文件名
server.set("views", path.join(__dirname, "views"));

const html = <App/>
server.get("/", (req, res) => {
  //参数一:渲染的ejs文件名
  //参数二:传给ejs文件的参数值
  res.render("index", {  html });
});

server.listen(3000, () => {
  console.log("Server running on http://localhost:3000");
});

views/index.ejs

<!DOCTYPE html>
<html lang="CN">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SSR Demo</title>
  </head>
  <body>
    <div id="root"><%- html %></div>
  </body>
</html>

到此就实现了一个React组件最普通的服务端渲染!

2.3 实现同构渲染

【核心】

实现以下两步:

  • 服务端可运行React代码

  • 浏览器同样可运行React代码

  • 两者公用一套React代码进行运行

①第一步:创建项目结构
├── src
│ ├── client       //1.客户端
| │└── components // 存放公共组件文件夹
| │└── pages      // 存放页面文件夹
│ │└── App.tsx    // 路由配置文件夹
│ │└── index.tsx // 客户端业务入口文件
│ │
│ ├── public      //2.公共静态资源文件夹
│ │
│ ├── server     //3.服务端
│ │ └── views 
│ │ | └── client.ejs // 相当于index.html 
│ │ └── index.js // 服务端业务入口文件
│ │
├── .babelrc // babel 配置文件
├── package.json  //项目依赖文件
├── webpack.config.client.json  //客户端打包编译文件
├── webpack.config.server.json  //服务端打包编译文件

②第二步:安装对应依赖

package.json

可提前安装或写到哪安哪

{
  "name": "ts-react-webpack-ssr",
  "version": "1.0.0",
  "scripts": {
    "build:server": "webpack --config webpack.config.server.js",
    "build:client": "webpack --config webpack.config.client.js",
    "start": "node ./dist/server.js",
    "start:dev": "node dev.js"
  },
  "devDependencies": {
    "@types/cross-spawn": "^6.0.2",
    "@types/express": "^4.17.11",
    "@types/node": "^14.14.35",
    "@types/react": "^17.0.3",
    "@types/react-dom": "^17.0.3",
    "clean-webpack-plugin": "^3.0.0",
    "copy-webpack-plugin": "^8.1.0",
    "cross-spawn": "^7.0.3",
    "ts-loader": "^8.0.18",
    "typescript": "^4.2.3",
    "webpack": "^5.26.3",
    "webpack-cli": "^4.5.0",
    "webpack-manifest-plugin": "^3.1.0",
    "webpack-node-externals": "^2.5.2"
  },
  "dependencies": {
    "ejs": "^3.1.6",
    "express": "^4.17.1",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-router-dom": "^6.16.0"
  }
}
③配置TS
  • 安装ts-loader、typescript

  • 添加tsconfig配置文件

    • ①方式一:通过webpack编译配置ts-loader,并指定对应的tsconfig.xxx.json配置文件

      • 需要添加两个tsconfig.xxx.json文件

      • 再在webpack.config.xxx.js文件中添加对应编译配置

    • tsconfig.client.json

      • 以es6语法为准

    {
      "include": ["client"],
      "compilerOptions": {
        "module": "es6",
        "target": "es5",
        "moduleResolution": "node",
        "jsx": "react",
        "allowSyntheticDefaultImports": true,
        "forceConsistentCasingInFileNames": true,
        "skipLibCheck": true,
        "resolveJsonModule": true,
        "strict": true,
        "strictNullChecks": true,
        "noImplicitAny": true
      }
    }
    
    
    • tsconfig.server.json

      • 以commonjs语法为准

    {
      "include": ["server"],
      "compilerOptions": {
        "module": "commonjs", // classic format that Node.js understands
        "esModuleInterop": true, // allow imports of modules in ES format
        "skipLibCheck": true, // only check types we refer to from our code
        "forceConsistentCasingInFileNames": true, // prevents cross-OS problems
        "resolveJsonModule": true, // enable import of JSON files
        "lib": ["es6", "dom"], // use JavaScript ES6 & DOM API
        "target": "es6", // compile to ES6
        "jsx": "react", // compile JSX to React.createElement statements for SSR
        "allowJs": true, // allow import of JS modules
         // enable strict type checking
        "strict": true,
        "strictNullChecks": true,
        "noImplicitAny": true,
      }
    }
    

    注意:客户端和服务端所添加的语法支持,表示只能使用该语法,否则会报错!!

  • 在webpack.config中添加对应的配置文件

    • webpack.config.client.js

        resolve: {
          extensions: [".ts", ".tsx"],
        }, 
       module: {
          rules: [
            {
              test: /\.tsx?$/,              //指定的文件后缀
              loader: "ts-loader",          //使用的编译加载器
              options: {
                configFile: "tsconfig.client.json",//指定的tsconfig.client.json配置文件
              }
            }
          ]
        }
      
    • webpack.config.server.js

        resolve: {
          extensions: [".ts", ".tsx",".js"],//会自行添加以上后缀,再去寻找文件
        }, 
       module: {
          rules: [
            {
              test: /\.tsx?$/,      //指定的文件后缀
              loader: "ts-loader",   //使用的编译加载器
              options: {
                configFile: "tsconfig.server.json",//指定的tsconfig.server.json配置文件
              }
            }
          ]
        }
      
    • ②方式2:利用babel编译配置babel-loader,再添加.babelrc配置文件

      • 需要添加.babelrc文件

      {
        "presets": [
          [
            "@babel/preset-env",
            {
              "targets": {
                "browsers": "defaults"
              },
              "useBuiltIns": "usage",
              "corejs": 3
            }
          ],
          [
            "@babel/preset-react",
            {
              "runtime": "automatic"
            }
          ],
          "@babel/preset-typescript"
        ]
      }
      

      如果项目中使用了react-native,需要再添加以下配置

        "plugins": [
          "@babel/plugin-proposal-class-properties",
          [
            "module-resolver",
            {
              "alias": {
                "^react-native$": "react-native-web"
              }
            }
          ]
        ]
      
      • 再在两个webpack.config.xxx.js中,添加编译配置

        resolve: {
          extensions: [".ts", ".tsx", ".js"],
          alias: { //使用react-native报错时,添加以下配置
            "react-native$": "react-native-web"
          }
        },  
        module: {
          rules: [
            {
              test: /\.jsx?$|\.tsx?$/,
              exclude: /node_modules/,
              use: "babel-loader"
            }
          ]
        }
      
④服务端渲染页面

client/app.tsx

  • 后续用于配置路由

import {Routes,Route} from "react-router-dom";
import React from "react";
import {Home} from "./components/Home";//导出并非default,需要加{}
export const App: React.FC = () => {
    return(<Home/>)
};

client/components/Home/index.tsx

  • 组件文件

import React from "react";

export  const Home: React.FC = () => {
    const handleClick = () =>{
        alert("点击事件")
    }
    return(
        <div>
        <p>这是首页</p>
        <button onClick={handleClick}>点击</button>
        </div>
    )   
};

server/server.tsx

  • 服务端渲染页面文件

import express from "express";
import fs from "fs";
import path from "path";
import React from "react";
import ReactDOMServer from "react-dom/server";
import { App } from "../client/app";
import {StaticRouter} from "react-router-dom/server";
const server = express();

server.set("view engine", "ejs");//设置视图引擎为ejs
server.set("views", path.join(__dirname, "views"));//设置视图文件位置,__dirname表示当前目录

server.use("/", express.static(path.join(__dirname, "static")));//挂载静态目录,便于后续访问

const manifest = fs.readFileSync(
  path.join(__dirname, "static/manifest.json"),
  //读取打包后生成的mainfest.json文件,便于后续挂载client打包后的js文件
  "utf-8"
);
const assets = JSON.parse(manifest);//获取mainfest.json的配置

server.get("/", (req, res) => {
   //renderToString渲染组件结构为HTML字符串
  const component = ReactDOMServer.renderToString(<App/>); 
  //根据设置的模板引擎位置,找到client.ejs文件进行渲染
    //同时传入assets、component两个参数
  res.render("client", { assets, component });
});
//监听端口运行
server.listen(3000, () => {
  console.log(`Server running on http://localhost:3000`);
});

server/views/client.ejs

  • 模板引擎文件,从server.tsx传入参数,插入html页面的主体,同时引入客户端的client.js,便于后续绑定事件

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>React + Node.js App</title>
    <script defer="defer" src="<%= assets["client.js"] %>"></script>
  </head>
  <body>
    <div id="root"><%- component %></div>
  </body>
</html>

⑤客户端hydrate渲染绑定事件

client/client.tsx

import React from "react";
import ReactDOM from "react-dom";
import { App } from "./app";
import {HashRouter as Router} from "react-router-dom";
function Main() {
    return (<App />);
  }
ReactDOM.hydrate(<Main />, document.getElementById("root"));
  • 此时用的并非是ReactDOM.render()方法了,而是hydrate(),区别如下:

    • hydrate()与 render() 相同,但调用该方式时 React 将会保留该节点且只进行事件处理绑定,而不会二次渲染。

    • 因此看起来是服务端渲染一次,然后客户端渲染一次,其实并非这样

    • 客户端的hydrate渲染会复用服务端返回的节点,进行一次类似于 render 的 hydrate 渲染过程,把交互事件绑上去(此时页面可交互),并接管页面。!

⑥编译打包并运行

完成客户端和服务端的代码后,我们还需要将其打包发送给浏览器,最后才能运行查看效果!

因此就需要配置webpack.config.server.js和webpacl.config.client.js

webpack.config.server.js

const nodeExternals = require("webpack-node-externals");
const path = require("path");
const CopyPlugin = require("copy-webpack-plugin");

module.exports = {
  name: "server",
  entry: {
    server: path.resolve(__dirname, "server", "server.tsx"),
  },
  mode: "production",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "[name].js",
  },
  externals: [nodeExternals()],
  resolve: {
    extensions: [".ts", ".tsx"],
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        loader: "ts-loader",
        options: {
          configFile: "tsconfig.server.json",
        },
      },
    ],
  },
  target: "node",
  node: {
    __dirname: false,
  },
  plugins: [
    new CopyPlugin({
      patterns: [{ context: "server", from: "views", to: "views" }],
    }),
  ],
};

webpacl.config.client.js

const path = require("path");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");

module.exports = {
  name: "client",
  entry: {
    client: path.resolve(__dirname, "client/client.tsx"),
  },
  mode: "production",
  output: {
    path: path.resolve(__dirname + "/dist/static"),
    filename: "[name].[contenthash].js",
    publicPath: "",
  },
  resolve: {
    extensions: [".ts", ".tsx", ".js"],
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        loader: "ts-loader",
        options: {
          configFile: "tsconfig.client.json",
        },
      },
    ],
  },
  target: "web",
  plugins: [new CleanWebpackPlugin(), new WebpackManifestPlugin()],
};

修改package.json文件

  "scripts": {
    "build:server": "webpack --config webpack.config.server.js",
    "build:client": "webpack --config webpack.config.client.js",
    "start": "node ./dist/server.js",
    "start:dev": "node dev.js"
  },
  • 执行npm build:client / yarn build:client 编译打包客户端文件

  • 执行npm build:server / yarn build:server 编译打包服务端文件

  • 执行npm start / yarn start 运行打包后的server.js文件

    • 如果项目是利用yarn命令,注意安装、运行都利用yarn命令

生成的dis文件夹如下:

运行结果如下:

至此就完成了一个简单地ts+react+node+express的同构案例

2.4 兼容路由

React路由作为项目必不可少的一部分,当然需要配置,以下是具体步骤:

再执行客户端js文件,让页面绑定事件

  • 注意1:利用HashRouter/BrowserRouter包裹的组件,不可以运行在Nodejs中,会报错:document is not defined

  • 注意2:在服务端时需要使用react-router-dom/server的StaticRouter组件,否则报错:useRoutes() may be used only in the context of a <Router> component.

  • 注意3:使用StaticRouter组件,需要传入location={req.url},否则报错:

  • 注意4:一定要使用HashRouter组件,否则刷新本页面时,会出现页面请求失败的情况

①第一步:修改路由配置文件

client/app.tsx

  • 添加react路由的配置,我这是直接使用了组件嵌套,版本react-router-dom6

  • 注意在此不要使用HashRouter包裹在最外层,因为app.tsx还复用在了服务端中,而服务端使用的是StaticRouter

import {Routes,Route} from "react-router-dom";
import React from "react";
import {Home} from "./components/Home";
import { Detail } from "./components/Detail/inde";
export const App: React.FC = () => {
    return(
            <Routes>
                <Route path="/" element={<Home/>}/>
                <Route path="/detail" element={<Detail/>}/>
            </Routes>
    )
};
  • 还可以利用react-router-config实现路由表的写法

route.js

const routes = [
  { 
     path: "/", 
     component: Home, 
     exact: true 
  },
  {
    path: "/detail",
    component: Detail,
    exact: true,
  }
 ]
export default routes;

app.tsx

// client.js
import { BrowserRouter } from "react-router-dom";
import { renderRoutes } from "react-router-config";
import Routes from "./routes";
 
const App = () => {
  return (
    <BrowserRouter>
      <div>{renderRoutes(Routes)}</div>
    </BrowserRouter>
  );
};

②第二步:修改客户端渲染文件

client/clinet.tsx

  • 在此再引入客户端的HashRouter

  • 注意:一定要使用HashRouter,否则会出现刷新页面404的问题

import React from "react";
import ReactDOM from "react-dom";
import { App } from "./app";
import {HashRouter as Router} from "react-router-dom";
function Main() {
    return (
        <Router>
          <App />
        </Router>
    );
  }
ReactDOM.hydrate(<Main />, document.getElementById("root"));

③第三步:修改服务端渲染文件

server/server.tsx

  • 使用StaticRouter包裹路由组件

    • 为什么使用StaticRouter而非HashRouter?

      • 主要是因为 BrowserRouter 使用的是 History API 记录位置,而 History API 是属于浏览器的 API ,在 SSR 的环境下,服务端不能使用浏览器 API 。

import express from "express";
import fs from "fs";
import path from "path";
import React from "react";
import ReactDOMServer from "react-dom/server";
import { App } from "../client/app";
import {StaticRouter} from "react-router-dom/server";
const server = express();

server.set("view engine", "ejs");
server.set("views", path.join(__dirname, "views"));

server.use("/", express.static(path.join(__dirname, "static")));

const manifest = fs.readFileSync(
  path.join(__dirname, "static/manifest.json"),
  "utf-8"
);
const assets = JSON.parse(manifest);

server.get("/", (req, res) => {
    //利用React-router-dom提供的服务端组件StaticRouter
    //同时显式地向location传path。
  const component = ReactDOMServer.renderToString(
    <StaticRouter location={req.url}>
      <App/>
    </StaticRouter>
); 
  res.render("client", { assets, component });
});

server.listen(3000, () => {
  console.log(`Server running on http://localhost:3000`);
});

  • 再次build后运行

    • 效果图

    • 页面浏览器接收

2.5 兼容CSS

  • 安装style-loade、css-loader、isomorphic-style-loader

    • style-loader 它的作用是把生成出来的 css 样式动态插入到 HTML 中,然而在服务端渲染是没有办法使用 DOM 的,因此服务端渲染不能使用它。

    • isomorphic-style-loader 主要是导出了3个函数, getCss 、 _insertCss 与getContent ,供使用者调用,而不再是简单粗暴的插入 DOM 中。

第一步:设置客户端样式

在此使用的是react-router-dom6

看到很多博客说服务端需要利用staticRouter中的context,但v6中根本没有这个属性呀!

后面直接尝试添加webpack编译配置,直接import+设置className,发现也能正常显示css

【这个点属实没搞明白,不知道有没有大佬可以帮忙解答下,但是不影响本文,以下操作样式仍好用哦!】

  • 为保证css能被进行编译,在webpack文件中添加相应的配置

webpack.config.client.js

module: {
  rules: [
     {
        test: /\.css$/,
        exclude: /node_modules/,
        use: ["style-loader", "css-loader"]
      },
   ],
 }
第二步:设置服务端样式

webpack.config.server.js

module: {
   rules: [
      {
        test: /\.css?$/,
        use: [
          "isomorphic-style-loader",
          {
            loader: "css-loader",
            options: {
              modules: true
            }
          }
        ]
      }
    ]
}
  • 创建css文件,并import至设置样式的组件中

import  "./style.css";
  • 此时页面能看到相应的样式效果,同时源代码中也提示相应的样式代码

2.6 兼容logo图标设置

本demo中,如果想要设置项目的logo图标,如下图:

①第一步:将要设置的logo图片,在client端的style.css中引入图片

import "./logo.png"

②第二步:执行yarn build:client后,可以看到打包后的dis生成了一个img文件夹,其中包含logo

③第三步:设置server端,读取打包后的mainfest.json,并获取其中img/logo对应的打包后的logo.png名称

此时的manifest.json内容如下:

{

  "client.js": "client.de603a184343d3620362.js",

  "img/logo.png": "img/logo.f0eaac.png"

}

server.tsx代码:

import express from "express";
import fs from "fs";
import path from "path";
import React from "react";
import ReactDOMServer from "react-dom/server";
import { App } from "../client/app";
import {StaticRouter} from "react-router-dom/server";
const server = express();

server.set("view engine", "ejs");
server.set("views", path.join(__dirname, "views"));

server.use("/", express.static(path.join(__dirname, "static")));

//读取dis中的manifest.json文件

const manifest = fs.readFileSync(
  path.join(__dirname, "static/manifest.json"),
  "utf-8"
);

//获取其中的json值
const assets = JSON.parse(manifest);

server.get("/", (req, res) => {
  const component = ReactDOMServer.renderToString(
    <StaticRouter location={req.url} >
      <App/>
    </StaticRouter>
); 

//作为参数传给ejs模板
  res.render("client", { assets, component });
});

server.listen(3000, () => {
  console.log(`Server running on http://localhost:3000`);
});
 

④第四步:将打包后的logo路径,传给ejs模板引擎,设置图标路径

 <link rel="icon" href="<%= assets['img/logo.png'] %>" />

⑤最后

yarn build:client

yarn build:server

yarn start

就可以看到图标被成功切换!

2.7 兼容Material-ui库

要实现material-ui的ssr渲染

详细步骤见官方文档:Server rendering - Material UI

①第一步:下载相关包 yarn add

yarn add @mui/material @emotion/react @emotion/styled

②第二步:

  • 创建theme用于在client端和server端中共享

    import { createTheme } from '@mui/material/styles';
    import { red } from '@mui/material/colors';
    ​
    // Create a theme instance.
    const theme = createTheme({
      palette: {
        primary: {
          main: '#556cd6',
        },
        secondary: {
          main: '#19857b',
        },
        error: {
          main: red.A400,
        },
      },
    });
    ​
    export default theme;
  • 创建createEmotionCache.js定义创建cache的函数

    import createCache from '@emotion/cache';
    ​
    export default function createEmotionCache() {
      return createCache({ key: 'css' });
    }
  • 修改client.tsx和server.tsx,利用ThemeProvider和CacheProvider包裹

    server.tsx

    server.get("/", (req, res) => {
      const cache = createEmotionCache();
      const { extractCriticalToChunks, constructStyleTagsFromChunks } = createEmotionServer(cache);
         // Grab the CSS from emotion
    ​
      const html = ReactDOMServer.renderToString(
        <CacheProvider value={cache}>
          <ThemeProvider theme={theme}>
            <CssBaseline />
            <StaticRouter location={req.url} >
              <App/>
            </StaticRouter>
          </ThemeProvider>
        </CacheProvider>
    ); 
    const emotionChunks = extractCriticalToChunks(html);
    const emotionCss = constructStyleTagsFromChunks(emotionChunks);
      res.render("client", { assets,emotionCss, html });
    });

    client.tsx

    import React from "react";
    import ReactDOM from "react-dom";
    import { App } from "./app";
    import { ThemeProvider } from '@mui/material/styles';
    import { CacheProvider } from '@emotion/react';
    import {HashRouter as Router} from "react-router-dom";
    import createEmotionCache from "../server/createEmotionCache";
    import theme from "./style/theme";
    const cache = createEmotionCache();
    function Main() {
        return (
        <CacheProvider value={cache}>
          <ThemeProvider theme={theme}>
            <Router>
              <App />
            </Router>
           </ThemeProvider>
        </CacheProvider>
        );
      }
    ReactDOM.hydrate(<Main />, document.getElementById("root"));
    ​
  • 在client.ejs中添加导出的材料库样式

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8" />
        <title>React + Node.js App</title>
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel="icon" href="<%= assets['img/logo.png'] %>" />
        <script defer="defer" src="<%= assets["client.js"] %>"></script>
      </head>
      <%-emotionCss%>
      <body>
        <div id="root"><%- html %></div>
      </body>
    </html>
    
    

【本文demo地址】

可根据自身需求自取!!

Liuri/ssr服务端渲染demo

【技术资料】:

【参考文章,侵删】:

三、SSR脚手架

目前前端流行的三种技术栈 React, Vue 和 Angula ,已经孵化出对应的服务端渲染框架,开箱即用,需要的可以自行搜索

  • React: Next.js

  • Vue: Nuxt.js

  • Angula: Nest.js

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

邓六日

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值