导航菜单

  • 0.api
  • 0.Async
  • 0.module
  • 1.ES2015
  • 2.Promise
  • 3.Node
  • 4.NodeInstall
  • 5.REPL
  • 6.NodeCore
  • 7.module&NPM
  • 8.Encoding
  • 9.Buffer
  • 10.fs
  • 11.Stream-1
  • 11.Stream-2
  • 11.Stream-3
  • 11.Stream-4
  • 12-Network-2
  • 12.NetWork-3
  • 12.Network-1
  • 13.tcp
  • 14.http-1
  • 14.http-2
  • 15.compress
  • 16.crypto
  • 17.process
  • 18.yargs
  • 19.cache
  • 20.action
  • 21.https
  • 22.cookie
  • 23.session
  • 24.express-1
  • 24.express-2
  • 24.express-3
  • 24.express-4
  • 25.koa-1
  • 26.webpack-1-basic
  • 26.webpack-2-optimize
  • 26.webpack-3-file
  • 26.webpack-4.tapable
  • 26.webpack-5-AST
  • 26.webpack-6-sources
  • 26.webpack-7-loader
  • 26.webpack-8-plugin
  • 26.webpack-9-hand
  • 26.webpack-10-prepare
  • 28.redux
  • 28.redux-jwt-back
  • 28.redux-jwt-front
  • 29.mongodb-1
  • 29.mongodb-2
  • 29.mongodb-3
  • 29.mongodb-4
  • 29.mongodb-5
  • 29.mongodb-6
  • 30.cms-1-mysql
  • 30.cms-2-mysql
  • 30.cms-3-mysql
  • 30.cms-4-nunjucks
  • 30.cms-5-mock
  • 30.cms-6-egg
  • 30.cms-7-api
  • 30.cms-8-roadhog
  • 30.cms-9-yaml
  • 30.cms-10-umi
  • 30.cms-12-dva
  • 30.cms-13-dva-ant
  • 30.cms-14-front
  • 30.cms-15-deploy
  • 31.dva
  • 31.cms-13-dva-antdesign
  • 33.redis
  • 34.unittest
  • 35.jwt
  • 36.websocket-1
  • 36.websocket-2
  • 38.chat-api-1
  • 38.chat-api-2
  • 38.chat-3
  • 38.chat-api-3
  • 38.chat
  • 38.chat2
  • 38.chat2
  • 39.crawl-0
  • 39.crawl-1
  • 39.crawl-2
  • 40.deploy
  • 41.safe
  • 42.test
  • 43.nginx
  • 44.enzyme
  • 45.docker
  • 46.elastic
  • 47.oauth
  • 48.wxpay
  • index
  • 52.UML
  • 53.design
  • index
  • 54.linux
  • 57.ts
  • 56.react-ssr
  • 58.ts_react
  • 59.ketang
  • 59.ketang2
  • 61.1.devops-linux
  • 61.2.devops-vi
  • 61.3.devops-user
  • 61.4.devops-auth
  • 61.5.devops-shell
  • 61.6.devops-install
  • 61.7.devops-system
  • 61.8.devops-service
  • 61.9.devops-network
  • 61.10.devops-nginx
  • 61.11.devops-docker
  • 61.12.devops-jekins
  • 61.13.devops-groovy
  • 61.14.devops-php
  • 61.15.devops-java
  • 61.16.devops-node
  • 61.17.devops-k8s
  • 62.1.react-basic
  • 62.2.react-state
  • 62.3.react-high
  • 62.4.react-optimize
  • 62.5.react-hooks
  • 62.6.react-immutable
  • 62.7.react-mobx
  • 62.8.react-source
  • 63.1.redux
  • 63.2.redux-middleware
  • 63.3.redux-hooks
  • 63.4.redux-saga
  • 63.5.redux-saga-hand
  • 64.1.router
  • 64.2.router-connected
  • 65.1.typescript
  • 65.2.typescript
  • 65.3.typescript
  • 65.4.antd
  • 65.4.definition
  • 66-1.vue-base
  • 66-2.vue-component
  • 66-3.vue-cli3.0
  • 66-4.$message组件
  • 66-5.Form组件
  • 66-6.tree
  • 66-7.vue-router-apply
  • 66-8.axios-apply
  • 66-9.vuex-apply
  • 66-10.jwt-vue
  • 66-11.vue-ssr
  • 66-12.nuxt-apply
  • 66-13.pwa
  • 66-14.vue单元测试
  • 66-15.权限校验
  • 67-1-network
  • 68-2-wireshark
  • 7.npm2
  • 69-hooks
  • 70-deploy
  • 71-hmr
  • 72.deploy
  • 73.import
  • 74.mobile
  • 75.webpack-1.文件分析
  • 75.webpack-2.loader
  • 75.webpack-3.源码流程
  • 75.webpack-4.tapable
  • 75.webpack-5.prepare
  • 75.webpack-6.resolve
  • 75.webpack-7.loader
  • 75.webpack-8.module
  • 75.webpack-9.chunk
  • 75.webpack-10.asset
  • 75.webpack-11.实现
  • 76.react_optimize
  • 77.ts_ketang_back
  • 77.ts_ketang_front
  • 78.vue-domdiff
  • 79.grammar
  • 80.tree
  • 81.axios
  • 82.1.react
  • 82.2.react-high
  • 82.3.react-router
  • 82.4.redux
  • 82.5.redux_middleware
  • 82.6.connected
  • 82.7.saga
  • 82.8.dva
  • 82.8.dva-source
  • 82.9.roadhog
  • 82.10.umi
  • 82.11.antdesign
  • 82.12.ketang-front
  • 82.12.ketang-back
  • 83.upload
  • 84.graphql
  • 85.antpro
  • 86.1.uml
  • 86.2.design
  • 87.postcss
  • 88.react16-1
  • 89.nextjs
  • 90.react-test
  • 91.react-ts
  • 92.rbac
  • 93.tsnode
  • 94.1.JavaScript
  • 94.2.JavaScript
  • 94.3.MODULE
  • 94.4.EventLoop
  • 94.5.文件上传
  • 94.6.https
  • 94.7. nginx
  • 95.1. react
  • 95.2.react
  • 96.1.react16
  • 96.2.fiber
  • 96.3.fiber
  • 97.serverless
  • 98.websocket
  • 100.1.react-basic
  • 101.1.monitor
  • 101.2.monitor
  • 102.java
  • 103.1.webpack-usage
  • 103.2.webpack-bundle
  • 103.3.webpack-ast
  • 103.4.webpack-flow
  • 103.5.webpack-loader
  • 103.6.webpack-tapable
  • 103.7.webpack-plugin
  • 103.8.webpack-optimize1
  • 103.9.webpack-optimize2
  • 103.10.webpack-hand
  • 103.11.webpack-hmr
  • 103.11.webpack5
  • 103.13.splitChunks
  • 103.14.webpack-sourcemap
  • 103.15.webpack-compiler1
  • 103.15.webpack-compiler2
  • 103.16.rollup.1
  • 103.16.rollup.2
  • 103.16.rollup.3
  • 103.16.vite.basic
  • 103.16.vite.source
  • 103.16.vite.plugin
  • 103.16.vite.1
  • 103.16.vite.2
  • 103.17.polyfill
  • 104.1.binary
  • 104.2.binary
  • 105.skeleton
  • 106.1.react
  • 106.2.react_hooks
  • 106.3.react_router
  • 106.4.redux
  • 106.5.redux_middleware
  • 106.6.connected-react-router
  • 106.6.redux-first-history
  • 106.7.redux-saga
  • 106.8.dva
  • 106.9.umi
  • 106.10.ketang
  • 106.11.antdesign
  • 106.12.antpro
  • 106.13.router-6
  • 106.14.ssr
  • 106.15.nextjs
  • 106.16.1.cms
  • 106.16.2.cms
  • 106.16.3.cms
  • 106.16.4.cms
  • 106.16.mobx
  • 106.17.fomily
  • 107.fiber
  • 108.http
  • 109.1.webpack_usage
  • 109.2.webpack_source
  • 109.3.dll
  • 110.nest.js
  • 111.xstate
  • 112.Form
  • 113.redux-saga
  • 114.react+typescript
  • 115.immer
  • 116.pro5
  • 117.css-loader
  • 118.1.umi-core
  • 119.2.module-federation
  • 119.1.module-federation
  • 120.create-react-app
  • 121.react-scripts
  • 122.react-optimize
  • 123.jsx-runtime
  • 124.next.js
  • 125.1.linux
  • 125.2.linux-vi
  • 125.3.linux-user
  • 125.4.linux-auth
  • 125.5.linux-shell
  • 125.6.linux-install
  • 125.7.linux-system
  • 125.8.linux-service
  • 125.9.linux-network
  • 125.10.nginx
  • 125.11.docker
  • 125.12.ci
  • 125.13.k8s
  • 125.14.k8s
  • 125.15.k8s
  • 125.16.k8s
  • 126.11.react-1
  • 126.12.react-2
  • 126.12.react-3
  • 126.12.react-4
  • 126.12.react-5
  • 126.12.react-6
  • 126.12.react-7
  • 126.12.react-8
  • 127.frontend
  • 128.rollup
  • 129.px2rem-loader
  • 130.health
  • 131.hooks
  • 132.keepalive
  • 133.vue-cli
  • 134.react18
  • 134.2.react18
  • 134.3.react18
  • 135.function
  • 136.toolkit
  • 137.lerna
  • 138.create-vite
  • 139.cli
  • 140.antd
  • 141.react-dnd
  • 142.1.link
  • 143.1.gulp
  • 143.2.stream
  • 143.3.gulp
  • 144.1.closure
  • 144.2.v8
  • 144.3.gc
  • 145.react-router-v6
  • 146.browser
  • 147.lighthouse
  • 148.1.basic
  • 148.2.basic
  • 148.3.basic
  • 148.4.basic
  • 148.5.basic
  • 149.1.vite
  • 149.2.vite
  • 149.3.vite
  • 149.4.vite
  • 150.react-window
  • 151.react-query
  • 152.useRequest
  • 153.transition
  • 154.emotion
  • 155.1.formily
  • 155.2.formily
  • 155.3.formily
  • 155.3.1.mobx.usage
  • 155.3.2.mobx.source
  • 156.vue-loader
  • 103.11.mf
  • 157.1.react18
  • 158.umi4
  • 159.rxjs
  • 159.rxjs2
  • 160.bff
  • 161.zustand
  • 162.vscode
  • 163.emp
  • 164.cors
  • 1.渲染模式
    • 1.1 服务器渲染
    • 1.2 客户端渲染
    • 1.3 为什么SSR
  • 2.SSR+SPA同构
    • 2.1 安装
    • 2.2 webpack.config.base.js
    • 2.3 webpack.config.client.js
    • 2.4 webpack.config.server.js
    • 2.5 Counter.js
    • 2.6 src\server\index.js
    • 2.7 src\client\index.js
    • 2.8 package.json
  • 3.使用路由
    • 3.1 安装
    • 3.2 客户端路由
    • 3.3 routesConfig.js
    • 3.4 App.js
    • 3.5 server\index.js
    • 3.6 src\client\index.js
    • 3.7 src\routes\Home.js
  • 4. 头部导航
    • 4.1 Header\index.js
    • 4.2 App.js
  • 5. 集成redux
    • 5.1 安装
    • 5.2 store\index.js
    • 5.3 action-types.js
    • 5.4 counter.js
    • 5.5 counter.js
    • 5.6 src\routes\Counter.js
    • 5.7 src\App.js
  • 6. 子路由并调用接口
    • 6.1 安装
    • 6.2 api.js
    • 6.3 action-types.js
    • 6.4 user.js
    • 6.5 user.js
    • 6.6 src\store\index.js
    • 6.7 src\routesConfig.js
    • 6.8 User.js
    • 6.9 UserAdd.js
    • 6.10 src\routes\UserList.js
    • 6.11 Header\index.js
  • 7. 代理接口和服务器加载数据
    • 7.1 安装
    • 7.2 src\server\index.js
    • 7.3 src\client\index.js
    • 7.4 request.js
    • 7.5 request.js
    • 7.6 UserList.js
    • 7.7 src\store\actionCreators\user.js
    • 7.8 src\App.js
    • 7.9 src\store\index.js
  • 8. 登录和权限
    • 8.1 安装
    • 8.2 api.js
    • 8.3 src\routesConfig.js
    • 8.4 Login.js
    • 8.5 Logout.js
    • 8.6 Profile.js
    • 8.7 action-types.js
    • 8.8 auth.js
    • 8.9 auth.js
    • 8.10 src\store\index.js
    • 8.11 src\App.js
    • 8.12 server\index.js
    • 8.13 request.js
    • 8.14 src\components\Header\index.js
    • 8.15 client\index.js
  • 9. 状态码301和404
    • 9.1 NotFound.js
    • 9.2 src\routesConfig.js
    • 9.3 src\server\index.js
  • 10. 支持CSS
    • 10.1 安装
    • 10.2 src\App.css
    • 10.3 src\App.js
    • 10.4 webpack.config.base.js
    • 10.5 src\server\index.js
    • 10.6 src\client\index.js
  • 11. SEO
    • 11.1 安装
    • 11.2 src\routes\Home.js
    • 11.3 src\server\index.js
  • 12. 流式SSR
    • 12.1 user.js
    • 12.2 server\index.js
    • 12.3 src\routes\UserList.js
    • 12.4 api.js
  • 13.参考
    • 13.1 源码参考
    • 13.2 水合

1.渲染模式 #

1.1 服务器渲染 #

  • 页面上的内容是由服务器生产的
npm install express --save

render\client.js

let express = require('express');
let app = express();
app.get('/', (req, res) => {
  res.send(`
        <html>
          <body>
            <div id="root">hello</div>
          </body>
        </html>
    `);
});
app.listen(8080);

1.2 客户端渲染 #

  • 页面上的内容由于浏览器运行JS脚本而渲染到页面上的
    • 浏览器访问服务器
    • 服务器返回一个空的HTML页面,里面有一个JS资源链接,比如client
    • 浏览器下载JS代码并在浏览器中运行
    • 内容呈现在页面上
let express = require('express');
let app = express();
app.get('/', (req, res) => {
  res.send(`
        <html>
          <body>
            <div id="root"></div>
            <script>root.innerHTML = 'hello'</script>
          </body>
        </html>
    `);
});
app.listen(8090);

1.3 为什么SSR #

  • 首屏等待 在客户端渲染的模式下,所有的数据请求和DOM渲染都在浏览器端完成,所以第一次访问页面时,可能会出现白屏,而服务器端渲染会在服务器端进行数据请求和DOM渲染,浏览器收到的完整的内容,可以渲染页面
  • SEO SPA对搜索引擎不够友好

2.SSR+SPA同构 #

  • 第一次访问页面是SSR,后面的访问是SPA,而且支持SEO
  • 客户端和服务器端同构可以实现(尽可能复用代码)
  • 工作流程
    • 服务器端运行React代码渲染出HTML字符串
    • 服务器把渲染出的HTML页面发送给了浏览器
    • 浏览器接受到HTML会渲染到页面上
    • 浏览器发现页面引用的client.js文件会去下载
    • 浏览器下载得到的client.js文件并在浏览器端执行
    • 浏览器中的代码接管了页面的所有内容,后面和客户端渲染是一样的

2.1 安装 #

npm install react react-dom --save
npm install webpack webpack-cli source-map-loader babel-loader @babel/preset-env @babel/preset-react webpack-merge webpack-node-externals npm-run-all nodemon  --save-dev

2.2 webpack.config.base.js #

webpack.config.base.js

module.exports = {
    mode: 'development',
    resolve: {
        alias: {
            '@': path.resolve(__dirname, 'src')
        }
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                enforce: 'pre',
                use: ['source-map-loader']
            },
            {
                test: /\.js/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: [
                            "@babel/preset-env",
                            "@babel/preset-react"
                        ]
                    }
                },
                exclude: /node_modules/,

            }
        ]
    }
}

2.3 webpack.config.client.js #

webpack.config.client.js

const path = require('path');
const { merge } = require('webpack-merge');
const base = require('./webpack.config.base');
const config = merge(base, {
    target: 'web',
    entry: './src/client/index.js',
    output: {
        path: path.resolve('public'),
        filename: 'client.js'
    }
});
module.exports = config;

2.4 webpack.config.server.js #

webpack.config.server.js

const path = require('path');
const { merge } = require('webpack-merge');
const webpackNodeExternals = require('webpack-node-externals');
const base = require('./webpack.config.base');
module.exports = merge(base, {
    target: 'node',
    entry: './src/server/index.js',
    output: {
        path: path.resolve('build'),
        filename: 'server.js'
    },
    externals: [webpackNodeExternals()]
});

2.5 Counter.js #

src\routes\Counter.js

import React, { useState } from 'react';
function Counter() {
    const [number, setNumber] = useState(0);
    return (
        <div>
            <p>{number}</p>
            <button onClick={() => setNumber(number + 1)}>+</button>
        </div>
    )
}
export default Counter;

2.6 src\server\index.js #

src\server\index.js

import React from 'react';
import { renderToString } from 'react-dom/server';
import Counter from '../routes/Counter';
const express = require('express');
const app = express();
app.use(express.static('public'));
app.get('*', (req, res) => {
  const html = renderToString(
    <Counter />
  );
  res.send(`
      <html>
        <head>
          <meta charset="UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <meta http-equiv="X-UA-Compatible" content="ie=edge">
          <title>ssr</title>
        </head>
        <body>
        <div id="root">${html}</div>
        <script src="/client.js"></script>
      </body>
      </html>
  `);
});
app.listen(3000, () => console.log("server started on 3000"));

2.7 src\client\index.js #

src\client\index.js

import React from 'react';
import { hydrateRoot } from 'react-dom/client';
import Counter from '../routes/Counter';
const root = document.getElementById('root');
hydrateRoot(root, <Counter />);

2.8 package.json #

package.json

{
  "scripts": {
    "start": "nodemon build/server.js",
    "build": "npm-run-all --parallel build:**",
    "build:server": "webpack --config webpack.config.server.js --watch",
    "build:client": "webpack --config webpack.config.client.js --watch"
  },
}

3.使用路由 #

3.1 安装 #

npm install react-router-dom --save

3.2 客户端路由 #

  • 客户端请求服务器
  • 服务器返回HTML给浏览器,浏览器渲染显示页面
  • 浏览器发现需要外链JS资源,加载JS资源
  • 加载好的JS资源在浏览器端执行
  • JS中的React代码开始实现路由功能
  • 路由代码首先获取地址栏中的地址,然后根据不同的地址根据路由配置渲染对应内容

3.3 routesConfig.js #

src\routesConfig.js

import React from 'react';
import Home from './routes/Home';
import Counter from './routes/Counter';
export default [
  {
    path: '/',
    element: <Home />,
    index: true
  },
  {
    path: '/counter',
    element: <Counter />
  }
]

3.4 App.js #

src\App.js

import React from 'react';
import { useRoutes } from 'react-router-dom';
import routesConfig from './routesConfig';
function App() {
  return (
    useRoutes(routesConfig)
  )
}
export default App;

3.5 server\index.js #

src\server\index.js

import React from 'react';
import { renderToString } from 'react-dom/server';
+import routesConfig from '../routesConfig';
+import { StaticRouter } from "react-router-dom/server";
+import { matchRoutes } from 'react-router-dom';
+import App from '../App';
const express = require('express');
const app = express();
app.use(express.static('public'));
app.get('*', (req, res) => {
   const html = renderToString(
+     <StaticRouter location={req.url}>
+       <App />
     </StaticRouter>
   );
    res.send(`
      <html>
        <head>
          <meta charset="UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <meta http-equiv="X-UA-Compatible" content="ie=edge">
          <title>ssr</title>
        </head>
        <body>
        <div id="root">${html}</div>
        <script src="/client.js"></script>
      </body>
      </html>
  `);
});
app.listen(3000, () => console.log("server started on 3000"));

3.6 src\client\index.js #

src\client\index.js

import React from 'react';
import { hydrateRoot } from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
+import App from '../App';
const root = document.getElementById('root');
hydrateRoot(root,
+  <BrowserRouter>
+    <App />
+  </BrowserRouter>);

3.7 src\routes\Home.js #

src\routes\Home.js

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

4. 头部导航 #

4.1 Header\index.js #

src\components\Header\index.js

import React from 'react';
import { Link } from 'react-router-dom';
function Header() {
  return (
    <ul>
      <li><Link to="/">Home</Link></li>
      <li><Link to="/counter">Counter</Link></li>
    </ul>
  )
}
export default Header

4.2 App.js #

src\App.js

import React from 'react';
import { useRoutes } from 'react-router-dom';
import routesConfig from './routesConfig';
+import Header from './components/Header';
function App() {
  return (
+   <>
+    <Header />
     {useRoutes(routesConfig)}
+   </>
  )
}
export default App;

5. 集成redux #

5.1 安装 #

npm install redux react-redux redux-thunk redux-promise redux-logger --save

5.2 store\index.js #

src\store\index.js

import { createStore, combineReducers, applyMiddleware } from 'redux'
import thunk from 'redux-thunk';
import promise from 'redux-promise';
import logger from 'redux-logger';
import counter from './reducers/counter';
export function getStore() {
    const reducers = { counter }
    const combinedReducer = combineReducers(reducers);
    const store = applyMiddleware(thunk, promise,logger)(createStore)(combinedReducer);
    return store
}

5.3 action-types.js #

src\store\action-types.js

export const ADD = 'ADD';

5.4 counter.js #

src\store\reducers\counter.js

import { ADD } from '../action-types';
const initialState = { number: 0 };
function counter(state = initialState, action) {
  switch (action.type) {
    case ADD:
      return { number: state.number + 1 }
    default:
      return state;
  }
}
export default counter;

5.5 counter.js #

src\store\actionCreators\counter.js

import { ADD } from '@/store/action-types';
const actionCreators = {
  add() {
    return { type: ADD };
  }
}
export default actionCreators;

5.6 src\routes\Counter.js #

src\routes\Counter.js

import React from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import actionCreators from '@/store/actionCreators/counter';
function Counter() {
+    const number = useSelector(state => state.counter.number);
+    const dispatch = useDispatch();
    return (
        <div>
            <p>{number}</p>
+           <button onClick={() => dispatch(actionCreators.add())}>+</button>
        </div>
    )
}
export default Counter;

5.7 src\App.js #

src\App.js

import React from 'react';
import { useRoutes } from 'react-router-dom';
import routesConfig from './routesConfig';
import Header from './components/Header';
+import { Provider } from 'react-redux';
+import { getStore } from './store';
const store = getStore();
function App() {
  return (
+    <Provider store={store}>
      <Header />
      {useRoutes(routesConfig)}
+    </Provider>
  )
}
export default App;

6. 子路由并调用接口 #

6.1 安装 #

npm install cors axios --save-dev

6.2 api.js #

api.js

const express = require('express')
const cors = require('cors');
const app = express();
app.use(cors());
const users = [{ id: 1, name: 'zhufeng1' }, { id: 2, name: 'zhufeng2' }, { id: 3, name: 'zhufeng3' }];
app.get('/api/users', (req, res) => {
  res.json({
    success: true,
    data: users
  });
});
app.listen(5000, () => console.log('api server started on port 5000'));

6.3 action-types.js #

src\store\action-types.js

export const ADD = 'ADD';
+export const SET_USER_LIST = 'SET_USER_LIST';
+export const ADD_USER = 'ADD_USER';

6.4 user.js #

src\store\reducers\user.js

import { ADD_USER, SET_USER_LIST } from '../action-types';
const initialState = { list: [] };
function counter(state = initialState, action) {
  switch (action.type) {
    case SET_USER_LIST:
      return { list: action.payload }
    case ADD_USER:
      return { list: [...state.list, action.payload] }
    default:
      return state;
  }
}
export default counter;

6.5 user.js #

src\store\actionCreators\user.js

import { SET_USER_LIST, ADD_USER } from '../action-types';
import axios from 'axios';
const actions = {
  getUserList() {
    return function (dispatch, getState) {
      return axios.get('http://localhost:5000/api/users').then((response) => {
        const { data } = response.data;
        dispatch({
          type: SET_USER_LIST,
          payload: data
        });
      });
    }
  },
  addUser(user) {
    return { type: ADD_USER, payload: user }
  }
}
export default actions;

6.6 src\store\index.js #

src\store\index.js

import { createStore, combineReducers, applyMiddleware } from 'redux'
import thunk from 'redux-thunk';
import promise from 'redux-promise';
import logger from 'redux-logger';
import counter from './reducers/counter';
+import user from './reducers/user';
export function getStore() {
+   const reducers = { counter, user }
    const combinedReducer = combineReducers(reducers);
    const store = applyMiddleware(thunk, promise, logger)(createStore)(combinedReducer);
    return store
}

6.7 src\routesConfig.js #

src\routesConfig.js

import React from 'react';
import Home from './routes/Home';
import Counter from './routes/Counter';
+import User from './routes/User';
+import UserAdd from './routes/UserAdd';
+import UserList from './routes/UserList';
export default [
  {
    path: '/',
    element: <Home />,
    index: true
  },
  {
    path: '/counter',
    element: <Counter />
  },
+ {
+   path: '/user',
+   element: <User />,
+   children: [
+     {
+       path: '/user/List',
+       element: <UserList />,
+       index: true
+     },
+     {
+       path: '/user/Add',
+       element: <UserAdd />
+     }
+   ]
+ }
]

6.8 User.js #

src\routes\User.js

import React from 'react';
import { Link, Outlet } from 'react-router-dom';
function User() {
    return (
        <>
            <ul>
                <li><Link to="/user/add">UserAdd</Link></li>
                <li><Link to="/user/list">UserList</Link></li>
            </ul>
            <Outlet />
        </>
    )
}
export default User;

6.9 UserAdd.js #

src\routes\UserAdd.js

import React, { useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import actionCreators from '@/store/actionCreators/user';
function UserAdd() {
    const list = useSelector(state => state.user.list);
    const nameRef = useRef();
    const navigate = useNavigate();
    const dispatch = useDispatch();
    const handleSubmit = (event) => {
        event.preventDefault();
        const name = nameRef.current.value;
        dispatch(actionCreators.addUser({ id: Date.now(), name }));
        navigate('/User/List');
    }
    return (
        <form onSubmit={handleSubmit}>
            用户名 <input ref={nameRef} />
            <input type="submit"></input>
        </form>
    )
}
export default UserAdd;

6.10 src\routes\UserList.js #

src\routes\UserList.js

import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import actionCreators from '@/store/actionCreators/user';
function UserList() {
    const list = useSelector(state => state.user.list);
    const dispatch = useDispatch();
    useEffect(() => {
        if (list.length === 0) {
            dispatch(actionCreators.getUserList());
        }
    }, [])
    return (
        <ul>
            {
                list.map(user => <li key={user.id}>{user.name}</li>)
            }
        </ul>
    )
}
export default UserList;

6.11 Header\index.js #

src\components\Header\index.js

import React from 'react';
import { Link } from 'react-router-dom';
function Header() {
  return (
    <ul>
      <li><Link to="/">Home</Link></li>
      <li><Link to="/counter">Counter</Link></li>
+     <li><Link to="/user/list">User</Link></li>
    </ul>
  )
}
export default Header

7. 代理接口和服务器加载数据 #

  • createStore.ts
  • redux-thunk

7.1 安装 #

npm install express-http-proxy --save

7.2 src\server\index.js #

src\server\index.js

import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter } from "react-router-dom/server";
+import proxy from 'express-http-proxy';
import App from '../App';
+import { getServerStore } from '../store';
+import { matchRoutes } from 'react-router-dom';
+import routesConfig from '../routesConfig';
const express = require('express');
const app = express();
app.use(express.static('public'));
+app.use('/api', proxy('http://localhost:5000', {
+  proxyReqPathResolver(req) {
+    return `/api${req.url}`;
+  }
+}));
app.get('*', (req, res) => {
+ const routeMatches = matchRoutes(routesConfig, { pathname: req.url });
+  if (routeMatches) {
+   const store = getServerStore();
+   const promises = routeMatches
+     .map(({ route }) => route.element.type.loadData && route.element.type.loadData(store).then(data => data, error => error))
+     .concat(App.loadData && App.loadData(store))
+     .filter(Boolean)
+   Promise.all(promises).then(() => {
+     const html = renderToString(
+       <StaticRouter location={req.url}>
+         <App store={store} />
+       </StaticRouter>
+     );
      res.send(`
      <html>
        <head>
          <meta charset="UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <meta http-equiv="X-UA-Compatible" content="ie=edge">
          <title>ssr</title>
        </head>
        <body>
        <div id="root">${html}</div>
+       <script>
+         var context = {
+          state:${JSON.stringify(store.getState())}
+         }
+       </script>
        <script src="/client.js"></script>
      </body>
      </html>
  `);
    })
+ } else {
+   res.sendStatus(404);
+ }
});
app.listen(3000, () => console.log("server started on 3000"));

7.3 src\client\index.js #

src\client\index.js

import React from 'react';
import { hydrateRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { getClientStore } from '../store';
import App from '../App';
const root = document.getElementById('root');
+const store = getClientStore();
hydrateRoot(root,
  <BrowserRouter>
+   <App store={store} />
  </BrowserRouter>);

7.4 request.js #

src\server\request.js

import axios from 'axios'
const request = axios.create({
  baseURL: 'http://localhost:5000/'
});
export default request

7.5 request.js #

src\client\request.js

import axios from 'axios'
const request = axios.create({
  baseURI: '/'
});
export default request

7.6 UserList.js #

src\routes\UserList.js

import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import actionCreators from '@/store/actionCreators/user';
function UserList() {
    const list = useSelector(state => state.user.list);
    const dispatch = useDispatch();
    useEffect(() => {
        if (list.length === 0) {
            dispatch(actionCreators.getUserList());
        }
    }, [])
    return (
        <ul>
            {
                list.map(user => <li key={user.id}>{user.name}</li>)
            }
        </ul>
    )
}
+UserList.loadData = (store) => {
+    return store.dispatch(actionCreators.getUserList());
+}
export default UserList;

7.7 src\store\actionCreators\user.js #

src\store\actionCreators\user.js

import { SET_USER_LIST, ADD_USER } from '../action-types';
const actions = {
  getUserList() {
+   return function (dispatch, getState, request) {
+      return request.get('/api/users').then((response) => {
        const { data } = response.data;
        dispatch({
          type: SET_USER_LIST,
          payload: data
        });
      });
    }
  },
  addUser(user) {
    return { type: ADD_USER, payload: user }
  }
}
export default actions;

7.8 src\App.js #

src\App.js

import React from 'react';
import { useRoutes } from 'react-router-dom';
import routesConfig from './routesConfig';
import Header from './components/Header';
import { Provider } from 'react-redux';
+function App({ store }) {
  return (
    <Provider store={store}>
      <Header />
      {useRoutes(routesConfig)}
    </Provider>
  )
}
export default App;

7.9 src\store\index.js #

src\store\index.js

import { createStore, combineReducers, applyMiddleware } from 'redux'
import thunk from 'redux-thunk';
import promise from 'redux-promise';
import logger from 'redux-logger';
import counter from './reducers/counter';
import user from './reducers/user';
+import clientRequest from '@/client/request';
+import serverRequest from '@/server/request';
+const clientThunk = thunk.withExtraArgument(clientRequest);
+const serverThunk = thunk.withExtraArgument(serverRequest);
+const reducers = { counter, user }
+const combinedReducer = combineReducers(reducers);
+export function getClientStore() {
+    const initialState = window.context.state;
+    return applyMiddleware(clientThunk, promise, logger)(createStore)(combinedReducer, initialState);
+}
+export function getServerStore() {
+    return applyMiddleware(serverThunk, promise, logger)(createStore)(combinedReducer);
+}

8. 登录和权限 #

8.1 安装 #

npm install express-session --save

8.2 api.js #

const express = require('express')
const cors = require('cors');
+const session = require('express-session');
+const app = express();
+app.use(cors());
+app.use(session({
+  saveUninitialized: true,
+  resave: true,
+  secret: 'zhufeng'
+}))
+app.use(express.json());
+app.use(express.urlencoded({ extended: true }));
const users = [{ id: 1, name: 'zhufeng1' }, { id: 2, name: 'zhufeng2' }, { id: 3, name: 'zhufeng3' }];
app.get('/api/users', (req, res) => {
  res.json({
    success: true,
    data: users
  });
});
+app.post('/api/login', (req, res) => {
+  const user = req.body;
+  req.session.user = user;
+  res.json({
+    success: true,
+    data: user
+  });
+});
+app.get('/api/logout', (req, res) => {
+  req.session.user = null;
+  res.json({
+    success: true
+  });
+});
+app.get('/api/user', (req, res) => {
+  const user = req.session.user;
+  if (user) {
+    res.json({
+      success: true,
+      data: user
+    });
+  } else {
+    res.json({
+      success: false,
+      error: '用户未登录'
+    });
+  }
+});
app.listen(5000, () => console.log('api server started on port 5000'));

8.3 src\routesConfig.js #

src\routesConfig.js

import React from 'react';
import Home from './routes/Home';
import Counter from './routes/Counter';
import User from './routes/User';
import UserAdd from './routes/UserAdd';
import UserList from './routes/UserList';
+import Login from './routes/Login';
+import Logout from './routes/Logout';
+import Profile from './routes/Profile';
export default [
  {
    path: '/',
    element: <Home />,
    index: true
  },
  {
    path: '/counter',
    element: <Counter />
  },
  {
    path: '/user',
    element: <User />,
    children: [
      {
        path: '/user/List',
        element: <UserList />,
        index: true
      },
      {
        path: '/user/Add',
        element: <UserAdd />
      }
    ]
  },
+ {
+   path: '/login',
+   element: <Login />
+ },
+ {
+   path: '/logout',
+   element: <Logout />
+ },
+ {
+   path: '/profile',
+   element: <Profile />
+ },
]

8.4 Login.js #

src\routes\Login.js

import React, { useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import actionCreators from '@/store/actionCreators/auth';
function Login() {
  const list = useSelector(state => state.user.list);
  const dispatch = useDispatch();
  const nameRef = useRef();
  const handleSubmit = (event) => {
    event.preventDefault();
    const name = nameRef.current.value;
    dispatch(actionCreators.login({ name }));
  }
  return (
    <form onSubmit={handleSubmit}>
      用户名 <input ref={nameRef} />
      <input type="submit"></input>
    </form>
  )
}
export default Login;

8.5 Logout.js #

src\routes\Logout.js

import React from 'react';
import { useDispatch } from 'react-redux';
import actionCreators from '@/store/actionCreators/auth';
function Logout() {
  const dispatch = useDispatch();
  return (
    <button onClick={() => dispatch(actionCreators.logout())}>退出</button>
  )
}
export default Logout;

8.6 Profile.js #

src\routes\Profile.js

import React, { useEffect } from 'react';
import { useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
function Profile() {
  const user = useSelector(state => state.auth.user);
  const navigate = useNavigate();
  useEffect(() => {
    if (!user) {
      navigate('/login');
    }
  },[]);
  return <div>用户名:{user && user.name}</div>
}
export default Profile;

8.7 action-types.js #

src\store\action-types.js

export const ADD = 'ADD';

export const SET_USER_LIST = 'SET_USER_LIST';
export const ADD_USER = 'ADD_USER';

+export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
+export const LOGIN_ERROR = 'LOGIN_ERROR';
+export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS';

8.8 auth.js #

src\store\reducers\auth.js

import { LOGIN_ERROR, LOGIN_SUCCESS, LOGOUT_SUCCESS } from '../action-types';
const initialState = { user: null, error: null }
function auth(state = initialState, action) {
  switch (action.type) {
    case LOGIN_SUCCESS:
      return { user: action.payload, error: null };
    case LOGIN_ERROR:
      return { user: null, error: action.payload };
    case LOGOUT_SUCCESS:
      return { user: null, error: null };
    default:
      return state;
  }
}
export default auth;

8.9 auth.js #

src\store\actionCreators\auth.js



import { LOGIN_ERROR, LOGIN_SUCCESS, LOGOUT_SUCCESS } from '../action-types';
import { push } from 'redux-first-history';
const actionCreators = {
  login(user) {
    return function (dispatch, getState, request) {
      return request.post('/api/login', user).then(res => {
        const { success, data, error } = res.data;
        if (success) {
          dispatch({ type: LOGIN_SUCCESS, payload: data });
          dispatch(push('/profile'));
        } else {
          dispatch({ type: LOGIN_ERROR, payload: error });
        }
      });
    }
  },
  logout() {
    return function (dispatch, getState, request) {
      return request.get('/api/logout').then(res => {
        const { success } = res.data;
        if (success) {
          dispatch({ type: LOGOUT_SUCCESS });
          dispatch(push('/login'));
        }
      });
    }
  },
  validate() {
    return function (dispatch, getState, request) {
      return request.get('/api/validate').then(res => {
        const { success, data } = res.data;
        if (success) {
          dispatch({ type: LOGIN_SUCCESS, payload: data });
        }
      });
    }
  }
}

export default actionCreators;

8.10 src\store\index.js #

src\store\index.js

import { createStore, combineReducers, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import promise from 'redux-promise';
import logger from 'redux-logger'
import counter from './reducers/counter';
import user from './reducers/user';
+import auth from './reducers/auth';
import clientRequest from '@/client/request';
import serverRequest from '@/server/request';
+import { createBrowserHistory, createMemoryHistory } from 'history'
+import { createReduxHistoryContext } from 'redux-first-history';
export function getClientStore() {
  const initialState = window.context.state;
+  const { createReduxHistory, routerMiddleware, routerReducer } = createReduxHistoryContext({
+    history: createBrowserHistory()
+  });
+  const reducers = { counter, user, auth, router: routerReducer };
+  const combinedReducer = combineReducers(reducers);
+  const store = applyMiddleware(thunk.withExtraArgument(clientRequest), promise, routerMiddleware, logger)
+    (createStore)
+    (combinedReducer, initialState);
+  const history = createReduxHistory(store);
+  return { store, history }
}
export function getServerStore(req) {
+ const { createReduxHistory, routerMiddleware, routerReducer } = createReduxHistoryContext({
+   history: createMemoryHistory()
+ });
+ const reducers = { counter, user, auth, router: routerReducer };
+ const combinedReducer = combineReducers(reducers);
+ const store = applyMiddleware(thunk.withExtraArgument(serverRequest(req)), promise, routerMiddleware, logger)(createStore)(combinedReducer);
+  const history = createReduxHistory(store);
+  return { store, history }
}

8.11 src\App.js #

src\App.js

import React from 'react';
import { useRoutes } from 'react-router-dom';
import routesConfig from './routesConfig';
import Header from './components/Header';
import { Provider } from 'react-redux';
+import actionCreators from './store/actionCreators/auth';
function App({ store }) {
  return (
    <Provider store={store}>
      <Header />
      {useRoutes(routesConfig)}
    </Provider>
  )
}
+App.loadData = (store) => {
+  return store.dispatch(actionCreators.validate())
+}
export default App;

8.12 server\index.js #

src\server\index.js

app.get('*', (req, res) => {
  const routeMatches = matchRoutes(routesConfig, { pathname: req.url });
  if (routeMatches) {
+   const store = getServerStore(req);
    const promises = routeMatches
      .map(({ route }) => route.element.type.loadData && route.element.type.loadData(store).then(data => data, error => error))
      .concat(App.loadData && App.loadData(store))
      .filter(Boolean)
  }
}

8.13 request.js #

src\server\request.js

import axios from 'axios'
const request = (req) => axios.create({
  baseURL: 'http://localhost:5000/',
+ headers: {
+   cookie: req.get('cookie') || ''
+ }
});
export default request

8.14 src\components\Header\index.js #

src\components\Header\index.js

import React from 'react';
import { Link } from 'react-router-dom';
+import { useSelector } from 'react-redux';
function Header() {
+ const { user } = useSelector(state => state.auth)
  return (
    <ul>
      <li><Link to="/">Home</Link></li>
      <li><Link to="/counter">Counter</Link></li>
      <li><Link to="/user/list">User</Link></li>
+     {
+       user ? (
+         <>
+           <li><Link to="/profile">个人中心</Link></li>
+           <li><Link to="/logout">退出</Link></li>
+         </>
+       ) : <li><Link to="/login">登录</Link></li>
+     }
    </ul>
  )
}
export default Header

8.15 client\index.js #

src\client\index.js

import React from 'react';
import { hydrateRoot } from 'react-dom/client';
+import { HistoryRouter as Router } from "redux-first-history/rr6";
import App from '@/App';
import { getClientStore } from '../store';
const root = document.getElementById('root');
const { store, history } = getClientStore();
hydrateRoot(root,
+ <Router history={history}>
    <App store={store} />
+ </Router>
);

9. 状态码301和404 #

9.1 NotFound.js #

src\routes\NotFound.js

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

9.2 src\routesConfig.js #

src\routesConfig.js

import React from 'react';
import Home from './routes/Home';
import Counter from './routes/Counter';
import User from './routes/User';
import UserAdd from './routes/UserAdd';
import UserList from './routes/UserList';
import Login from './routes/Login';
import Logout from './routes/Logout';
import Profile from './routes/Profile';
+import NotFound from './routes/NotFound';
export default [
  {
    path: '/',
    element: <Home />,
    index: true
  },
  {
    path: '/counter',
    element: <Counter />
  },
  {
    path: '/user',
    element: <User />,
    children: [
      {
        path: '/user/List',
        element: <UserList />,
        index: true
      },
      {
        path: '/user/Add',
        element: <UserAdd />
      }
    ]
  },
  {
    path: '/login',
    element: <Login />
  },
  {
    path: '/logout',
    element: <Logout />
  },
  {
    path: '/profile',
    element: <Profile />
  },
+ {
+   path: '*',
+   element: <NotFound />
+ }
]

9.3 src\server\index.js #

src\server\index.js

import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter } from "react-router-dom/server";
import proxy from 'express-http-proxy';
import App from '../App';
import { getServerStore } from '../store';
import { matchRoutes } from 'react-router-dom';
import routesConfig from '../routesConfig';
const express = require('express');
const app = express();
app.use(express.static('public'));
app.use('/api', proxy('http://localhost:5000', {
  proxyReqPathResolver(req) {
    return `/api${req.url}`;
  }
}));
app.get('*', (req, res) => {
  const routeMatches = matchRoutes(routesConfig, { pathname: req.url });
  if (routeMatches) {
    const store = getServerStore(req);
    const promises = routeMatches
      .map(({ route }) => route.element.type.loadData && route.element.type.loadData(store).then(data => data, error => error))
      .concat(App.loadData && App.loadData(store))
      .filter(Boolean)
    Promise.all(promises).then(() => {
+     if (req.url === '/profile' && (!(store.getState().auth.user))) {
+       return res.redirect('/login');
+     } else if (routeMatches[routeMatches.length - 1].route.path === '*') {
+       res.statusCode = 404;
+     }
      const html = renderToString(
        <StaticRouter location={req.url}>
          <App store={store} />
        </StaticRouter>
      );
      res.send(`
      <html>
        <head>
          <meta charset="UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <meta http-equiv="X-UA-Compatible" content="ie=edge">
          <title>ssr</title>
        </head>
        <body>
        <div id="root">${html}</div>
        <script>
          var context = {
           state:${JSON.stringify(store.getState())}
          }
        </script>
        <script src="/client.js"></script>
      </body>
      </html>
  `);
    })
  } else {
    res.sendStatus(404);
  }
});
app.listen(3000, () => console.log("server started on 3000"));

10. 支持CSS #

10.1 安装 #

npm install css-loader isomorphic-style-loader-react18 --save

10.2 src\App.css #

src\App.css

.color {
  color: red
}

10.3 src\App.js #

src\App.js

import React from 'react';
import { useRoutes } from 'react-router-dom';
import routesConfig from './routesConfig';
import Header from './components/Header';
import { Provider } from 'react-redux';
import actionCreators from './store/actionCreators/auth';
+import useStyles from 'isomorphic-style-loader-react18/useStyles'
+import styles from './App.css'
function App({ store }) {
+ useStyles(styles);
  return (
    <Provider store={store}>
      <Header />
      {useRoutes(routesConfig)}
+     <div className={styles.color}>red</div>
    </Provider>
  )
}
App.loadData = (store) => {
  return store.dispatch(actionCreators.validate())
}
export default App;

10.4 webpack.config.base.js #

webpack.config.base.js

const path = require('path');
module.exports = {
    mode: 'development',
    resolve: {
        alias: {
            '@': path.resolve(__dirname, 'src')
        }
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                enforce: 'pre',
                use: ['source-map-loader']
            },
            {
                test: /\.js/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: [
                            "@babel/preset-env",
                            "@babel/preset-react"
                        ]
                    }
                },
                exclude: /node_modules/
            },
+           {
+               test: /\.css$/,
+               use: [
+                   {
+                       loader: 'isomorphic-style-loader-react18'
+                   },
+                   {
+                       loader: 'css-loader',
+                       options: {
+                           modules: true
+                       }
+                   }
+               ]
+           }
+       ]
    }
}

10.5 src\server\index.js #

src\server\index.js

import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter } from "react-router-dom/server";
import proxy from 'express-http-proxy';
+import StyleContext from 'isomorphic-style-loader-react18/StyleContext'
import App from '../App';
import { getServerStore } from '../store';
import { matchRoutes } from 'react-router-dom';
import routesConfig from '../routesConfig';

const express = require('express');
const app = express();
app.use(express.static('public'));
app.use('/api', proxy('http://localhost:5000', {
  proxyReqPathResolver(req) {
    return `/api${req.url}`;
  }
}));
app.get('*', (req, res) => {
  const routeMatches = matchRoutes(routesConfig, { pathname: req.url });
  if (routeMatches) {
    const store = getServerStore(req);
    const promises = routeMatches
      .map(({ route }) => route.element.type.loadData && route.element.type.loadData(store).then(data => data, error => error))
      .concat(App.loadData && App.loadData(store))
      .filter(Boolean)
    Promise.all(promises).then(() => {
      if (req.url === '/profile' && (!(store.getState().auth.user))) {
        return res.redirect('/login');
      } else if (routeMatches[routeMatches.length - 1].route.path === '*') {
        res.statusCode = 404;
      }
+     const css = new Set()
+     const insertCss = (...styles) => styles.forEach(style => {
+       css.add(style._getCss())
+     })
      const html = renderToString(
        <StaticRouter location={req.url}>
+         <StyleContext.Provider value={{ insertCss }}>
            <App store={store} />
+         </StyleContext.Provider>
        </StaticRouter>
      );
      res.send(`
      <html>
        <head>
          <meta charset="UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <meta http-equiv="X-UA-Compatible" content="ie=edge">
          <title>ssr</title>
+         <style>${[...css].join('')}</style>
        </head>
        <body>
        <div id="root">${html}</div>
        <script>
          var context = {
           state:${JSON.stringify(store.getState())}
          }
        </script>
        <script src="/client.js"></script>
      </body>
      </html>
  `);
    })
  } else {
    res.sendStatus(404);
  }
});
app.listen(3000, () => console.log("server started on 3000"));

10.6 src\client\index.js #

src\client\index.js

import React from 'react';
import { hydrateRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
+import StyleContext from 'isomorphic-style-loader-react18/StyleContext'
import { getClientStore } from '../store';
import App from '../App';
const root = document.getElementById('root');
const store = getClientStore();
+const insertCss = (...styles) => {
+  const removeCss = styles.map(style => style._insertCss())
+  return () => removeCss.forEach(dispose => dispose())
+}
hydrateRoot(root,
  <BrowserRouter>
+   <StyleContext.Provider value={{ insertCss }}>
+     <App store={store} />
+   </StyleContext.Provider>
  </BrowserRouter>);

11. SEO #

11.1 安装 #

npm install react-helmet --save

11.2 src\routes\Home.js #

src\routes\Home.js

import React from 'react';
import { Helmet } from 'react-helmet';
function Home() {
  return (
    <>
      <Helmet>
        <title>首页标题</title>
        <meta name="description" content="首页描述"></meta>
      </Helmet>
      <div>
        Home
      </div>
    </>
  )
}
export default Home;

11.3 src\server\index.js #

src\server\index.js

import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter } from "react-router-dom/server";
import proxy from 'express-http-proxy';
import StyleContext from 'isomorphic-style-loader-react18/StyleContext'
+import { Helmet } from 'react-helmet';
import App from '../App';
import { getServerStore } from '../store';
import { matchRoutes } from 'react-router-dom';
import routesConfig from '../routesConfig';
const express = require('express');
const app = express();
app.use(express.static('public'));
app.use('/api', proxy('http://localhost:5000', {
  proxyReqPathResolver(req) {
    return `/api${req.url}`;
  }
}));
app.get('*', (req, res) => {
  const routeMatches = matchRoutes(routesConfig, { pathname: req.url });
  if (routeMatches) {
    const store = getServerStore(req);
    const promises = routeMatches
      .map(({ route }) => route.element.type.loadData && route.element.type.loadData(store).then(data => data, error => error))
      .concat(App.loadData && App.loadData(store))
      .filter(Boolean)
    Promise.all(promises).then(() => {
      if (req.url === '/profile' && (!(store.getState().auth.user))) {
        return res.redirect('/login');
      } else if (routeMatches[routeMatches.length - 1].route.path === '*') {
        res.statusCode = 404;
      }
      const css = new Set()
      const insertCss = (...styles) => styles.forEach(style => {
        css.add(style._getCss())
      })
+     let helmet = Helmet.renderStatic();
      const html = renderToString(
        <StaticRouter location={req.url}>
          <StyleContext.Provider value={{ insertCss }}>
            <App store={store} />
          </StyleContext.Provider>
        </StaticRouter>
      );

      res.send(`
      <html>
        <head>
          <meta charset="UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <meta http-equiv="X-UA-Compatible" content="ie=edge">
+         ${helmet.title.toString()}
+         ${helmet.meta.toString()}
          <style>${[...css].join('')}</style>
        </head>
        <body>
        <div id="root">${html}</div>
        <script>
          var context = {
           state:${JSON.stringify(store.getState())}
          }
        </script>
        <script src="/client.js"></script>
      </body>
      </html>
  `);
    })
  } else {
    res.sendStatus(404);
  }
});
app.listen(3000, () => console.log("server started on 3000"));

12. 流式SSR #

12.1 user.js #

src\store\actionCreators\user.js

import { SET_USER_LIST, ADD_USER } from '../action-types';
const actions = {
  getUserList() {
    return function (dispatch, getState, request) {
      return request.get('/api/users').then((response) => {
        const { data } = response.data;
        dispatch({
          type: SET_USER_LIST,
          payload: data
        });
+       return getState().user.list;
      });
    }
  },
  addUser(user) {
    return { type: ADD_USER, payload: user }
  }
}
export default actions;

12.2 server\index.js #

src\server\index.js

import React from 'react';
+import { renderToPipeableStream } from 'react-dom/server';
import { StaticRouter } from "react-router-dom/server";
import proxy from 'express-http-proxy';
import StyleContext from 'isomorphic-style-loader-react18/StyleContext'
import { Helmet } from 'react-helmet';
import App from '../App';
import { getServerStore } from '../store';
import { matchRoutes } from 'react-router-dom';
import routesConfig from '../routesConfig';

const express = require('express');
const app = express();
app.use(express.static('public'));
app.use('/api', proxy('http://localhost:5000', {
  proxyReqPathResolver(req) {
    return `/api${req.url}`;
  }
}));
app.get('*', (req, res) => {
  const routeMatches = matchRoutes(routesConfig, { pathname: req.url });
  if (routeMatches) {
    const store = getServerStore(req);
    const promises = routeMatches
      .map(({ route }) => route.element.type.loadData && route.element.type.loadData(store).then(data => data, error => error))
      .concat(App.loadData && App.loadData(store))
      .filter(Boolean)
    Promise.all(promises).then(() => {
      if (req.url === '/profile' && (!(store.getState().auth.user))) {
        return res.redirect('/login');
      } else if (routeMatches[routeMatches.length - 1].route.path === '*') {
        res.statusCode = 404;
      }
      const css = new Set()
      const insertCss = (...styles) => styles.forEach(style => {
        css.add(style._getCss())
      })
      let helmet = Helmet.renderStatic();
+     const { pipe } = renderToPipeableStream(
        <StaticRouter location={req.url}>
          <StyleContext.Provider value={{ insertCss }}>
            <App store={store} />
          </StyleContext.Provider>
        </StaticRouter>,
+        {
+          onShellReady() {
+            res.statusCode = 200;
+            res.setHeader('Content-type', 'text/html;charset=utf8');
+            res.write(`
+            <html>
+            <head>
+              <title>ssr</title/>
+                ${helmet.title.toString()}
+                ${helmet.meta.toString()}
+              <style>${[...css].join('')}</style>
+            </head>
+            <body>
+            <div id="root">`);
+            pipe(res);
+            res.write(`</div>
+            <script>
+              var context = {
+              state:${JSON.stringify(store.getState())}
+              }
+            </script>
+            <script src="/client.js"></script>
+          </body>
+          </html>`);
          }
        }
      );
    })
  } else {
    res.sendStatus(404);
  }
});
app.listen(3000, () => console.log("server started on 3000"));

12.3 src\routes\UserList.js #

src\routes\UserList.js

+import React, { Suspense, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import actionCreators from '@/store/actionCreators/user';
function UserList() {
+   const dispatch = useDispatch();
+   const resourceRef = useRef();
+   if (!resourceRef.current) {
+       const promise = dispatch(actionCreators.getUserList());
+       const resource = wrapPromise(promise);
+       resourceRef.current = resource;
+   }
+   return (
+       <Suspense fallback={<div>loading...</div>}>
+           <LazyList resource={resourceRef.current} />
+       </Suspense>
+   )
}
+function LazyList({ resource }) {
+    const userList = resource.read();
+    return (
+        <ul>
+            {
+                userList.map(item => <li key={item.id}>{item.name}</li>)
+            }
+        </ul>
+    )
+}
+/* 
+const promise = getUserList()
+const resource = wrapPromise(promise);
+function getUserList() {
+    return new Promise((resolve) => {
+        setTimeout(() => {
+            resolve([
+                { id: 1, name: 'zhufeng1' },
+                { id: 2, name: 'zhufeng2' },
+                { id: 3, name: 'zhufeng3' }
+            ])
+        }, 5000)
+    });
+} 
+*/
+function wrapPromise(promise) {
+    let status = "pending";
+    let result;
+    let suspender = promise.then(
+        (r) => {
+            status = "success";
+            result = r;
+        },
+        (e) => {
+            status = "error";
+            result = e;
+        }
+    );
+    return {
+        read() {
+            if (status === "pending") {
+                throw suspender;
+            } else if (status === "error") {
+                throw result;
+            } else if (status === "success") {
+                return result;
+            }
+        }
+    };
+}
export default UserList;

12.4 api.js #

api.js

const express = require('express')
const cors = require('cors');
const session = require('express-session');
const app = express();
app.use(cors());
app.use(session({
  saveUninitialized: true,
  resave: true,
  secret: 'zhufeng'
}))
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
const users = [{ id: 1, name: 'zhufeng1' }, { id: 2, name: 'zhufeng2' }, { id: 3, name: 'zhufeng3' }];
app.get('/api/users', (req, res) => {
+ setTimeout(() => {
+   res.json({
+     success: true,
+     data: users
+   });
+ }, 5000);
});
app.post('/api/login', (req, res) => {
  const user = req.body;
  req.session.user = user;
  res.json({
    success: true,
    data: user
  });
});
app.get('/api/logout', (req, res) => {
  req.session.user = null;
  res.json({
    success: true
  });
});
app.get('/api/user', (req, res) => {
  const user = req.session.user;
  if (user) {
    res.json({
      success: true,
      data: user
    });
  } else {
    res.json({
      success: false,
      error: '用户未登录'
    });
  }
});
app.listen(5000, () => console.log('api server started on port 5000'));

13.参考 #

13.1 源码参考 #

  • react-dom-server
  • createStore.ts
  • redux-thunk

13.2 水合 #

  • 水合反应( hydrated reaction),也叫作水化
  • 是指物质溶解在水里时,与水发生的化学作用,水合分子的过程
  • 组件在服务器端拉取数据(水),并在服务器端首次渲染
  • 脱水: 对组件进行脱水,变成HTML字符串,脱去动态数据,成为风干标本快照
  • 注水:发送到客户端后,重新注入数据(水),重新变成可交互组件




访问验证

请输入访问令牌

Token不正确,请重新输入