React SSR

React SSR

源码地址:https://github.com/qifutian/learngit/tree/main/react-ssr/react-ssr
React服务端渲染

什么是服务端渲染

SSR: Server Side Rendering
服务器返回html,DATA和html在服务端进行渲染

什么是客户端渲染
CSR:Client Side Rendering
服务器返回html,DATA和html在客户端进行渲染

客户端渲染的问题

  1. 首屏等待时间藏,体验差
  2. 页面结构为空,不利于SEO

SPA应用在服务端渲染解决的问题

在这里插入图片描述

React SSR同构

同构指的是代码复用,即实现客户端和服务端最大程度的代码复用

服务端渲染

搭建项目

在这里插入图片描述

  1. 新建文件夹react-ssr
  2. 新建src,及下的client,server,share
  3. 创建package.json
  4. 安装依赖 npm install

package.json

{
  "name": "ssr-guide",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "npm-run-all --parallel dev:*",
    "dev:server-run": "nodemon --watch build --exec \"node build/bundle.js\"",
    "dev:server-build": "webpack --config webpack.server.js --watch",
    "dev:client-build": "webpack --config webpack.client.js --watch"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@babel/cli": "^7.10.3",
    "@babel/core": "^7.10.3",
    "@babel/polyfill": "^7.10.4",
    "@babel/preset-env": "^7.10.3",
    "@babel/preset-react": "^7.10.1",
    "axios": "^0.19.2",
    "babel-loader": "^8.1.0",
    "express": "^4.17.1",
    "nodemon": "^2.0.4",
    "npm-run-all": "^4.1.5",
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "react-helmet": "^6.1.0",
    "react-redux": "^7.2.0",
    "react-router-config": "^5.1.1",
    "react-router-dom": "^5.2.0",
    "redux": "^4.0.5",
    "redux-thunk": "^2.3.0",
    "serialize-javascript": "^4.0.0",
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.12",
    "webpack-dev-server": "^3.11.0",
    "webpack-merge": "^4.2.2",
    "webpack-node-externals": "^1.7.2"
  }
}

创建node服务器

server下创建http.js 和index.js
http.js

import express from 'express';

const app = express();

app.use(express.static('public'));

app.listen(3000, () => console.log('app is running on 3000 port'));

export default app;

index.js 用来处理客户端的请求

import app from './http';
app.get('/',(req,res)=>{


})

实现React SSR

  1. 引入要渲染react组件
  2. 通过renderToString方法将react组件转为html字符串
  3. 将结果html字符串想到客户端

renderToString方法用来将React组件换HTML字符串,通过render-dom/sever导入

在share下创建pages/Home.js,获取首页组件换为html返回客户端

import React from 'react'
function Home (){
  return <div>Home works</div>
}
export default Home;

在server的index.js引用

import app from './http';
import React from 'react'
import Home from '../share/pages/Home'
import { renderToString } from 'react-dom/server'
app.get('/',(req,res)=>{

   const content = renderToString(<Home />)
   res.send(`
        <html>
    <head>
      <title>React SSR</title>
    </head>
    <body>
      <div id="root">${content}</div>
      <script src="bundle.js"></script>
     
    </body>
  </html>
    `)
})

webpack打包配置

问题:在Node环境下不支持ESModule模块系统,不支持JSX语法

根目录新建webpack.server.js

const path = require('path');
const merge = require('webpack-merge');
const baseConfig = require('./webpack.base');
const nodeExternals = require('webpack-node-externals');

const config = {
  target: 'node',
  entry: './src/server/index.js',
  output: {
    path: path.join(__dirname, 'build'),
    filename: 'bundle.js'
  },
  externals: [nodeExternals()]
}

module.exports = merge(baseConfig, config);

公共的webpack配置webpack.base.js

module.exports = {
  mode: "development",
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: [
              [
                "@babel/preset-env",
                {
                  useBuiltIns: "usage"
                }
              ],
              "@babel/preset-react"
            ]
          }
        }
      }
    ]
  }
};

项目启动命令配置

  1. 配置服务器端打包命令: “dev:server-build”: “webpack --config webpack.server.js --watch” 在修改文件之后自动打包
  2. 配置服务端启动命令: “dev:server-run”: “nodemon --watch build --exec “node build/bundle.js”” 自动从新运行
    运行时: npm run dev:server-build
客户端 React 附加事件
实现思路分析:在客户端对组件进行二次"渲染", 为组件元素附加事件.

客户端二次 “渲染” hydrate
使用 hydrate 方法对组件进行渲染, 为组件元素附加事件.
hydrate 方法在实现渲染的时候, 会复用原本已经存在的 DOM 节点, 减少重新生成节点以及删除原本 DOM 节点的开销.
通过 react-dom 导入 hydrate.

在这里插入图片描述

在client下index.js,是专门处理客户端的方式

import React from 'react';
import ReactDOM from 'react-dom';
import Home from '../share/pages/Home'

ReactDOM.HYDRATE(<Home />,document.getElementById("root"))

在Home上增加点击事件

import React from 'react'
function Home (){
  return <div onClick={()=>console.log('hello')}>Home works</div>
}
export default Home;

在node下不支持,需要引入react支持

客户端React附加事件

客户端react打包配置

  1. webapck配置
    打包目的:转换jsx雨大,转换浏览器不是吧的高阶JS语法
    打包目标位置:public文件夹
  2. 打包启动命令配置
    “dev:client-build”:“webpack --config webpack.client.js --watch”

客户端打包配置
webpack.client.js

const path = require("path");
const merge = require('webpack-merge');
const baseConfig = require('./webpack.base');

const config = {
  entry: "./src/client/index.js",
  output: {
    path: path.join(__dirname, "public"),
    filename: "bundle.js"
  }
};

module.exports = merge(baseConfig, config);

引入打包后的hundle.js,在server下的index.js

在服务端实现访问静态资源功能,客户端的js打包文件会作为静态资源

app.use(express.static("public"))

合并公共的配置

进行webpack的合并,分为webpack.client.js,webpack.server.js,weebpack.base.js存放公共资源

合并项目启动命令

目的:使用一个明亮启动项目,解决多个命令启动繁琐问题,通过npm-run-all实现

"dev":"npm-run-all --parallel dev:*"

直接npm run dev就可以

打包体积的优化

拆分一些node的隐藏模块,去除一些node的无用模块

问题:在服务器端打包文件中, 包含了 Node 系统模块. 导致打包文件本身体积庞大.
解决:通过 webpack 配置剔除打包文件中的 Node 模块.

在这里插入图片描述

const path = require('path');
const merge = require('webpack-merge');
const baseConfig = require('./webpack.base');
const nodeExternals = require('webpack-node-externals');

const config = {
  target: 'node',
  entry: './src/server/index.js',
  output: {
    path: path.join(__dirname, 'build'),
    filename: 'bundle.js'
  },
  externals: [nodeExternals()]
}

module.exports = merge(baseConfig, config);

代码优化

将启动服务器代码和渲染代码进行模块拆分

优化代码的组织方式,渲染React组件代码是独立功能,所以把他从服务器入口文件抽离

server下新建renderer.js,放置模板渲染的代码

import React from "react";
import Home from '../share/pages/Home'
import { renderToString } from "react-dom/server";

export default (req, store) => {
  const content = renderToString(
    <Home />
  );
  const initalState = serialize(store.getState());
  return `
  <html>
    <head>
      <title>React SSR</title>
    </head>
    <body>
      <div id="root">${content}</div>
      <script>window.INITIAL_STATE = ${initalState} </script>
      <script src="bundle.js"></script>
    </body>
  </html>
`;
};

server/index.js

import app from './http'
import renderer from './renderer'

app.get("/",(req,res)=>{
  res.send(renderer())
}) 

实现服务端路由

实现思路分析

在 React SSR 项目中需要实现两端路由.
客户端路由是用于支持用户通过点击链接的形式跳转页面.
服务器端路由是用于支持用户直接从浏览器地址栏中访问页面.
客户端和服务器端公用一套路由规则.

编写路由规则

在这里插入图片描述

  1. Express 路由接收任何请求
    Express 路由接收所有 GET 请求, 服务器端 React 路由通过请求路径匹配要进行渲染的组件.
    在这里插入图片描述
  2. 服务器端路由配置
    renderRoutes将数组形式的路由转为组件形式
    调用StaticRouter组件在这里插入图片描述

在share下的pages新建List.js,share新建routes.js

import Home from '../share/pages/Home';
import List from '../share/pages/List';

export default [{
  path: '/',
  component: Home,
  exact: true
}, {
  path: '/list',
  ...List
}]

在renderer.js中增加路由引用

在server/index.js支持所有的路由,/改为*

客户端路由

添加客户端路由配置

在这里插入图片描述
在client/index.js

import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import { renderRoutes } from "react-router-config";
import routes from "../share/routes";

ReactDOM.hydrate(
  
    <BrowserRouter>{renderRoutes(routes)}</BrowserRouter>,
  document.getElementById("root")
);

在Home中添加a链接

import React from "react";
import { Link } from 'react-router-dom';

function Home() {
  return <div onClick={() => console.log("Hello")}>
    Home works
    <Link to="/list">jump to list</Link>
  </div>;
}

export default Home;

Redux支持

思路分析

在实现了 React SSR 的项目中需要实现两端 Redux.
客户端 Redux 就是通过客户端 JavaScript 管理 Store 中的数据.
服务器端 Redux 就是在服务器端搭建一套 Redux 代码, 用于管理组件中的数据.
客户端和服务器端共用一套 Reducer 代码.
创建 Store 的代码由于参数传递不同所以不可以共用.

实现客户端 Redux

  1. 创建 Store
  2. 配置 Store
  3. 创建 Action 和 Reducer
  4. 配置 polyfill
    由于浏览器不能识别异步函数代码, 所以需要 polyfill 进行填充.

在client下新建createStore.js

import { applyMiddleware, createStore } from 'redux';
import thunk from 'redux-thunk';
import reducer from '../share/store/reducers';

const store = createStore(reducer, window.INITIAL_STATE, applyMiddleware(thunk));

export default store;

在client/index.js下使用Provider包裹根组件,在renderer方法创建store
在share下新建store放置redux代码,新建actions,reducers
user.reducer.js

import { SAVE_USER } from "../actions/user.action";

export default (state = [], action) => {
  switch(action.type) {
    case SAVE_USER:
      return action.payload.data;
    default:
      return state;
  }
}

user.action.js

import axios from 'axios';

export const SAVE_USER = 'save_user';

// 发送请求 获取用户列表数据
export const fetchUser = () => async dispatch => {
  // let response = await axios.get('https://jsonplaceholder.typicode.com/users');
  let response = {
    data: [{id: 1, name: "</script><script>alert(1)</script>"}]
  }
  dispatch({
    type: SAVE_USER,
    payload: response
  })
}

实现服务端redux

  1. 创建store
    在这里插入图片描述

  2. 配置 Store
    在这里插入图片描述
    服务端store数据填充

问题:服务器端创建的 store 是空的, 组件并不能从Store中获取到任何数据.
解决:服务器端在渲染组件之前获取到组件所需要的数据.

  1. 在组件中添加 loadData 方法, 此方法用于获取组件所需数据,方法被服务器端调用
  2. 将 loadData 方法保存在当前组件的路由信息对象中.
  3. 服务器端在接收到请求后,根据请求地址匹配出要渲染的组件的路由信息
  4. 从路由信息中获取组件中的 loadData 方法并调用方法获取组件所需数据
  5. 当数据获取完成以后再渲染组件并将结果响应到客户端

服务器端 store 数据填充

  1. 组件 loadData 方法
    服务器端通过调用组件的 loadData 方法获取组件所需数据并将数据存储在服务器端的 Store 中

在这里插入图片描述

  1. 服务器端获取组件所需数据
    服务器端在接收到请求以后,先根据请求路径分析出要渲染的路由信息,再从路由信息中得到 loadData方法.

在这里插入图片描述

React 警告消除

警告原因: 客户端 Store 在初始状态下是没有数据的, 在渲染组件的时候生成的是空 ul, 但是服务器端是先获取数据再进行的组件渲染, 所以生成的是有子元素的ul, hydrate 方法在对比的时候发现两者不一致, 所以报了个警告.
解决思路: 将服务器端获取到的数据回填给客户端, 让客户端拥有初始数据.

  1. 服务器响应 Store 初始状态
    在这里插入图片描述
  2. 客户端设置 Store 初始状态
    在这里插入图片描述

预防XSS攻击

防范 XSS 攻击

转义状态中的恶意代码.

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值