react-ssr

一、ReactSSR相关概念回顾

什么是客户端渲染

CSR:Client Side Rendering
服务器端仅返回 JSON 数据, DATA 和 HTML 在客户端进行渲染

什么是服务器端渲染

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

客户端渲染存在的问题

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

在这里插入图片描述
SPA 应用中服务器端渲染解决的问题
在这里插入图片描述

二、实现ReactSSR雏形

项目结构
react-ssr
src 源代码文件夹
client 客户端代码
server 服务器端代码
share 同构代码

创建 Node 服务器

在这里插入图片描述

实现 React SSR

  1. 引入要渲染的 React 组件
  2. 通过 renderToString 方法将 React 组件转换为 HTML 字符串
  3. 将结果HTML字符串想到到客户端
    renderToString 方法用于将 React 组件转换为 HTML 字符串, 通过 react-dom/server 导入
    在这里插入图片描述

三、服务器端程序webpack打包配置

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);

package.json

  "scripts": {
    "dev:server-build": "webpack --config webpack.server.js --watch",
  },

项目启动命令配置

  1. 配置服务器端打包命令: “dev:server-build”: “webpack --config webpack.server.js --watch”
  2. 配置服务端启动命令: “dev:server-run”: “nodemon --watch build --exec “node build/bundle.js””

四、为组件元素添加事件

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

home.js

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

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

export default Home;

client.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";
import { Provider } from "react-redux";
import store from "./createStore";

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

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);

package.json

  "scripts": {
  "dev:client-build": "webpack --config webpack.client.js --watch"
  }

五、优化:合并webpack配置

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);
  1. webpack 配置
    打包目的: 转换JSX语法, 转换浏览器不识别的高级 JavaScript 语法
    打包目标位置: public 文件夹
  2. 打包启动命令配置
    “dev:client-build”: “webpack --config webpack.client.js --watch”

在响应给客户端的 HTML 代码中添加 script 标签, 请求客户端 JavaScript 打包文件.
在这里插入图片描述
服务器端实现静态资源访问
服务器端程序实现静态资源访问功能, 客户端 JavaScript 打包文件会被作为静态资源使用
在这里插入图片描述

六、合并项目启动命令

在这里插入图片描述

七、优化:服务端打包文件体积优化

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

在这里插入图片描述

element-icons.ttf
element-icons.woff

八、代码拆分

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

优化代码组织方式, 渲染 React 组件代码是独立功能, 所以把它从服务器端入口文件中进行抽离.

server/render.js

import React from "react";
import { renderToString } from "react-dom/server";
import { StaticRouter } from "react-router-dom";
import routes from "../share/routes";
import { renderRoutes } from "react-router-config";
import { Provider } from "react-redux";
import serialize from 'serialize-javascript';

export default (req, store) => {
  const content = renderToString(
    <Provider store={store}>
      <StaticRouter location={req.path}>{renderRoutes(routes)}</StaticRouter>
    </Provider>
  );
  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';
import createStore from './createStore';
import routes from '../share/routes';
import { matchRoutes } from 'react-router-config';

app.get('*', (req, res) => {
  const store = createStore();
  // 1. 请求地址 req.path
  // 2. 获取到路由配置信息 routes
  // 3. 根据请求地址匹配出要渲染的组件的路由对象信息
  const promises = matchRoutes(routes, req.path).map(({route}) => {
    // 如何才能知道数据什么时候获取完成
    if (route.loadData) return route.loadData(store)
  })

  Promise.all(promises).then(() => {
    res.send(renderer(req, store));
  }) 
});

九、实现服务器端路由

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

home.js

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;

router.js

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

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

server/index.js

import app from './http';
import renderer from './renderer';
import createStore from './createStore';
import routes from '../share/routes';
import { matchRoutes } from 'react-router-config';

app.get('*', (req, res) => {
  const store = createStore();
  // 1. 请求地址 req.path
  // 2. 获取到路由配置信息 routes
  // 3. 根据请求地址匹配出要渲染的组件的路由对象信息
  const promises = matchRoutes(routes, req.path).map(({route}) => {
    // 如何才能知道数据什么时候获取完成
    if (route.loadData) return route.loadData(store)
  })

  Promise.all(promises).then(() => {
    res.send(renderer(req, store));
  })
  
});

server/render.js

import React from "react";
import { renderToString } from "react-dom/server";
import { StaticRouter } from "react-router-dom";
import routes from "../share/routes";
import { renderRoutes } from "react-router-config";
import { Provider } from "react-redux";
import serialize from 'serialize-javascript';

export default (req, store) => {
  const content = renderToString(
    <Provider store={store}>
      <StaticRouter location={req.path}>{renderRoutes(routes)}</StaticRouter>
    </Provider>
  );
  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>
`;
};

十、实现客户端路由

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";
import { Provider } from "react-redux";
import store from "./createStore";

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

home.js

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

client/createsStore.js

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

export default () => createStore(reducer, {}, applyMiddleware(thunk))

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";
import { Provider } from "react-redux";
import store from "./createStore";

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

store/action/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
  })
}

store/action/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;
  }
}

store/action/user.index.js

import { combineReducers } from 'redux';
import userReducer from './user.reducer';

// {user: []}
export default combineReducers({
  user: userReducer
});

遇到问题:浏览器不支持异步函数

webpack配置下
在这里插入图片描述

十二、实现服务器端redux

1、
server/createStore.js

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

export default () => createStore(reducer, {}, applyMiddleware(thunk))

server/index.js

import app from './http';
import renderer from './renderer';
import createStore from './createStore';
import routes from '../share/routes';
import { matchRoutes } from 'react-router-config';

app.get('*', (req, res) => {
  const store = createStore();
  // 1. 请求地址 req.path
  // 2. 获取到路由配置信息 routes
  // 3. 根据请求地址匹配出要渲染的组件的路由对象信息
  const promises = matchRoutes(routes, req.path).map(({route}) => {
    // 如何才能知道数据什么时候获取完成
    if (route.loadData) return route.loadData(store)
  })

  Promise.all(promises).then(() => {
    res.send(renderer(req, store));
  })
  
});

server/render.js

import React from "react";
import { renderToString } from "react-dom/server";
import { StaticRouter } from "react-router-dom";
import routes from "../share/routes";
import { renderRoutes } from "react-router-config";
import { Provider } from "react-redux";
import serialize from 'serialize-javascript';

export default (req, store) => {
  const content = renderToString(
    <Provider store={store}>
      <StaticRouter location={req.path}>{renderRoutes(routes)}</StaticRouter>
    </Provider>
  );
  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>
`;
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值