• react简单的服务器渲染示例(含redux, redux-thunk的使用)


    1. package.json

    1. {
    2. "name": "react-server",
    3. "version": "1.0.0",
    4. "description": "",
    5. "main": "index.js",
    6. "scripts": {
    7. "dev": "npm-run-all --parallel dev:**",
    8. "dev:build:client": "webpack --config config/webpack.client.js --watch",
    9. "dev:build:server": "webpack --config config/webpack.server.js --watch",
    10. "dev:start": "nodemon --watch dist --exec node \"./dist/bundle.js\""
    11. },
    12. "keywords": [],
    13. "author": "",
    14. "license": "ISC",
    15. "devDependencies": {
    16. "@babel/core": "^7.0.0",
    17. "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
    18. "@babel/preset-env": "^7.22.20",
    19. "@babel/preset-react": "^7.22.15",
    20. "@babel/preset-stage-0": "^7.8.3",
    21. "@babel/preset-typescript": "^7.23.0",
    22. "@reduxjs/toolkit": "^1.9.7",
    23. "@types/react": "^18.2.27",
    24. "@types/react-dom": "^18.2.12",
    25. "antd": "^5.10.0",
    26. "autoprefixer": "^9.7.3",
    27. "axios": "^1.5.1",
    28. "babel-core": "^7.0.0-bridge.0",
    29. "babel-loader": "7",
    30. "clean-webpack-plugin": "^3.0.0",
    31. "cross-env": "^7.0.3",
    32. "css-loader": "5.0.0",
    33. "eslint-loader": "^4.0.2",
    34. "express-http-proxy": "^2.0.0",
    35. "file-loader": "^5.0.2",
    36. "happypack": "^5.0.1",
    37. "html-webpack-plugin": "^3.2.0",
    38. "less": "^3.10.3",
    39. "less-loader": "5.0.0",
    40. "lodash": "^4.17.15",
    41. "mini-css-extract-plugin": "^0.8.0",
    42. "moment": "^2.24.0",
    43. "node-sass": "^9.0.0",
    44. "nodemon": "^3.0.1",
    45. "npm-run-all": "^4.1.5",
    46. "optimize-css-assets-webpack-plugin": "^5.0.3",
    47. "postcss": "^8.4.31",
    48. "postcss-loader": "^3.0.0",
    49. "postcss-pxtorem": "5.0.0",
    50. "react-activation": "^0.12.4",
    51. "react-redux": "^8.1.3",
    52. "recoil": "^0.7.7",
    53. "redux": "^4.2.1",
    54. "redux-persist": "^6.0.0",
    55. "sass": "^1.69.3",
    56. "sass-loader": "5.0.0",
    57. "style-loader": "^1.0.1",
    58. "terser-webpack-plugin": "^2.2.2",
    59. "thread-loader": "^4.0.2",
    60. "typescript": "^5.2.2",
    61. "url-loader": "^3.0.0",
    62. "webpack": "^4.41.2",
    63. "webpack-cli": "^3.3.10",
    64. "webpack-dev-server": "^3.9.0",
    65. "webpack-merge": "^4.2.2",
    66. "webpack-node-externals": "^3.0.0",
    67. "webpack-parallel-uglify-plugin": "^1.1.2",
    68. "yarn": "^1.22.19"
    69. },
    70. "dependencies": {
    71. "express": "^4.18.2",
    72. "path": "^0.12.7",
    73. "react": "^18.2.0",
    74. "react-dom": "^18.2.0",
    75. "react-router-config": "1.0.0-beta.4",
    76. "react-router-dom": "4.3.1"
    77. }
    78. }

    2. 新建.babelrc文件

    1. {
    2. "presets": [
    3. "@babel/preset-react",
    4. "@babel/preset-typescript"
    5. ],
    6. "plugins": []
    7. }

    3. 新建tsconfig.json文件

    1. {
    2. "compilerOptions": {
    3. "target": "es5",
    4. "lib": [
    5. "dom",
    6. "dom.iterable",
    7. "esnext"
    8. ],
    9. "allowJs": true,
    10. "skipLibCheck": true,
    11. "esModuleInterop": true,
    12. "allowSyntheticDefaultImports": true,
    13. "strict": true,
    14. "forceConsistentCasingInFileNames": true,
    15. "module": "esnext",
    16. "moduleResolution": "node",
    17. "resolveJsonModule": true,
    18. "isolatedModules": true,
    19. "noEmit": true,
    20. "jsx": "react-jsx",
    21. "noImplicitAny": false
    22. },
    23. "include": [
    24. "src"
    25. ]
    26. }

    4. config目录

    paths.js

    1. const path = require('path');
    2. const srcPath = path.resolve(__dirname, '..', 'src');
    3. const distPath = path.resolve(__dirname, '..', 'dist');
    4. module.exports = {
    5. srcPath,
    6. distPath
    7. }

    webpack配置文件

    (1) webpack.base.js提取公共配置代码

    1. module.exports = {
    2. // 打包的规则
    3. module: {
    4. rules: [
    5. {
    6. test: /\.[jt]sx?$/, // 检测文件类型
    7. loader: 'babel-loader', // 注意下载babel-loader babel-core
    8. exclude: /node_modules/, // node_modules目录文件不编译
    9. }
    10. ]
    11. }
    12. }

    (2) webpack.server.js服务器配置

    使用webpack-merge合并公共配置代码

    1. const nodeExternals = require('webpack-node-externals');
    2. const { distPath, srcPath } = require('./paths');
    3. const path = require('path');
    4. const merge = require('webpack-merge');
    5. const config = require('./webpack.base.js');
    6. const serverConfig = {
    7. mode: 'production', // 也可以写development
    8. target: 'node', // 告诉webpack打包的代码是服务器端文件
    9. entry: './src/server/index.js',
    10. output: { // 打包生成的文件应该放到哪儿去
    11. filename: 'bundle.js',
    12. path: distPath,
    13. },
    14. resolve: {
    15. extensions: ['.tsx', '.ts', '.jsx', '.js'],
    16. // 针对npm中的第三方模块优先采用jsnext中指向的es6模块语法的文件
    17. mainFields: ['jsnext:main', 'brower', 'main'],
    18. alias: {
    19. "components": path.resolve(srcPath, "containers/components"), //配置样式简单的路径
    20. "@": srcPath // 把 src 这个常用目录修改为 @
    21. },
    22. },
    23. externals: [
    24. /**
    25. * 1、如何让webpackexternals不影响测试环境?
    26. 由于webpackexternals将部分库文件排除在打包范围之外,这样在某些情况下可能会影响单元测试的运行,可以使用webpack-node-externals来排除node_modules目录下的所有依赖项。
    27. */
    28. nodeExternals(),
    29. ],
    30. }
    31. module.exports = merge(config, serverConfig);

    (3) webpack.client.js客户端配置

    使用webpack-merge合并公共配置代码

    1. const { distPath, srcPath, publicPath } = require('./paths');
    2. const path = require('path');
    3. const merge = require('webpack-merge');
    4. const config = require('./webpack.base.js');
    5. const clientConfig = {
    6. mode: 'production', // 也可以写development
    7. entry: './src/client/index.js',
    8. output: { // 打包生成的文件应该放到哪儿去
    9. filename: 'index.js',
    10. path: publicPath,
    11. },
    12. resolve: {
    13. extensions: ['.tsx', '.ts', '.jsx', '.js'],
    14. // 针对npm中的第三方模块优先采用jsnext中指向的es6模块语法的文件
    15. mainFields: ['jsnext:main', 'brower', 'main'],
    16. alias: {
    17. "components": path.resolve(srcPath, "containers/components"), //配置样式简单的路径
    18. "@": srcPath // 把 src 这个常用目录修改为 @
    19. },
    20. },
    21. }
    22. module.exports = merge(config, clientConfig);

    5. src/App.tsx

    1. import React from "react";
    2. import Header from "components/header";
    3. import { renderRoutes } from 'react-router-config'
    4. import _ from 'lodash'
    5. const App = (props) => {
    6. return (
    7. <section>
    8. <Header />
    9. {/* 显示页面对应的内容 */}
    10. {renderRoutes(props.route.routes)}
    11. </section>
    12. );
    13. };
    14. export default App;

    6. src/routes.tsx

    嵌套路由, 让head组件一直存在, head之下的页面作为子路由

    1. import React from 'react'
    2. import Home from './containers/home'
    3. import Login from './containers/login'
    4. import App from './App';
    5. const routes = [
    6. {
    7. path: '/',
    8. component: Home,
    9. loadData: Home.loadData,
    10. exact: true,
    11. key: 'home',
    12. },
    13. {
    14. path: '/login',
    15. component: Login,
    16. loadData: Login.loadData,
    17. exact: true,
    18. key: 'login',
    19. }
    20. ]
    21. export default [{
    22. path: '/',
    23. component: App,
    24. routes,
    25. }]

    7. src/store/index.ts

    redux-thunk和redux的使用

    1) 引入需要的reducer1, reducer2...

    2) 使用combineReducers组合引入的reducer1, reducer2..., 构成新的reducer

    3) 使用createStore创建store实例, 传入reducer和中间件!!!

    createstore语法:

    1. const store = createStore(reducer, [preloadedState], enhancer);
    2. /**
    3. *createStore接受3个参数:
    4. 第一个是reducer,
    5. 第二个是初始的state,
    6. 第三个enhancer是store的增强器。
    7. 执行本方法返回一个对象,
    8. 包含dispatch,subsribe,getState,replaceReducer,observable五个方法。
    9. */

    a. 服务端获取store实例

    const store = createStore(
          reducer, 
          applyMiddleware(thunk.withExtraArgument(serverAxios)); // 注意传服务端的axios实例
        );

    b. 客户端获取store实例

            // 拿服务端的数据作为客户端的数据
        const defaultState = window.context.state
        const store = createStore(
          reducer, 
          defaultState, 
          // 改变客户端store的内容, 那么一定要使用clientAxios
          applyMiddleware(thunk.withExtraArgument(clientAxios)) // 注意传客户端的axios实例
        );

    1. import { legacy_createStore as createStore, applyMiddleware, combineReducers } from "redux";
    2. import thunk from 'redux-thunk';
    3. import { reducer as homeReducer } from '../containers/home/store'
    4. import clientAxios from '@/client/request'
    5. import serverAxios from '@/server/request'
    6. // 将各个模块的reducer组合一下
    7. const reducer = combineReducers({
    8. home: homeReducer,
    9. });
    10. // 导出getStore是因为想要每个用户的store都是独享的!!!
    11. export const getStore = () => {
    12. const store = createStore(
    13. reducer,
    14. // 改变服务器store的内容, 那么一定要使用serverAxios
    15. applyMiddleware(thunk.withExtraArgument(serverAxios))
    16. );
    17. return store
    18. }
    19. export const getClientStore = () => {
    20. // 拿服务端的数据作为客户端的数据
    21. const defaultState = window.context.state
    22. const store = createStore(
    23. reducer,
    24. defaultState,
    25. // 改变客户端store的内容, 那么一定要使用clientAxios
    26. applyMiddleware(thunk.withExtraArgument(clientAxios))
    27. );
    28. return store
    29. }

    8. src/containers(组件页面目录)/

    home目录

    home/index.tsx

    1. import React, {useEffect} from "react";
    2. import { connect } from "react-redux";
    3. import { getHomeList } from './store/actions'
    4. import _ from 'lodash'
    5. // 同构--同一套react代码, 在服务器执行一次, 在客户端再执行一次
    6. const Home = (props) => {
    7. // 如果是class组件使用componentDidMount生命周期
    8. useEffect(()=> {
    9. // 避免首屏时服务端和客户端都各自调用了一次!!!
    10. if(!props.list.length) props.getHomeList()
    11. }, [])
    12. const getList = () => {
    13. return <ul>
    14. {_.map(props.list, (item) => {
    15. return <li key={item.uid}><span>{item.code}: </span>{item.type}</li>
    16. })}
    17. </ul>
    18. }
    19. return (
    20. <section>
    21. <div>this is {props.name}</div>
    22. {getList()}
    23. <button onClick={() => alert(11)}>click</button>
    24. </section>
    25. );
    26. };
    27. Home.loadData = (store) => {
    28. // 这个函数, 负责在服务器渲染之前, 把这个路由需要的数据提前加载好
    29. return store.dispatch(getHomeList())
    30. }
    31. const mapStateToProps = (state) => {
    32. return ({
    33. name: state.home.name,
    34. list: state.home.newsList,
    35. });
    36. }
    37. const mapDispatchToProps = (dispatch) => ({
    38. getHomeList() {
    39. console.log('test')
    40. dispatch(getHomeList())
    41. }
    42. });
    43. export default connect(mapStateToProps, mapDispatchToProps)(Home);

    home/store目录

    home/store/index.ts

    1. import reducer from './reducer'
    2. export { reducer }

    home/store/actions.ts

    1. import { CHANGE_LIST } from './constants'
    2. const changeList = (list) => ({
    3. type: CHANGE_LIST,
    4. list,
    5. })
    6. export const getHomeList = (server) => {
    7. let url1 = `/app/mock/22915/api/code`
    8. // const url1 = 'http://rap2api.taobao.org/app/mock/22915/api/code'
    9. return (dispatch, getState, axiosInstance) => {
    10. return axiosInstance.post(url1, {
    11. rows: 10,
    12. page: 1
    13. })
    14. .then(res => {
    15. const list = res.data.rows;
    16. dispatch(changeList(list.slice(0, 10)))
    17. })
    18. }
    19. }

    home/store/constants.ts

    1. // 定义常量
    2. export const CHANGE_LIST = 'HOME/CHANGE_LIST'

    home/store/reducer.ts

    1. import { CHANGE_LIST } from './constants'
    2. const defaultState = {
    3. newsList: [],
    4. name: 'Tom Li-1'
    5. }
    6. const reducer = (state:any = defaultState, action) => {
    7. switch (action.type) {
    8. case CHANGE_LIST:
    9. return {
    10. ...state,
    11. newsList: action.list
    12. };
    13. default:
    14. return state;
    15. }
    16. };
    17. export default reducer

    login/index.tsx

    1. import React from 'react'
    2. import Header from '../components/header';
    3. const Login = () => {
    4. return <div>
    5. <Header />
    6. <div>login</div>
    7. </div>
    8. }
    9. export default Login;

    components/公共组件目录

    header/index.tsx

    1. import React from 'react'
    2. import {Link} from 'react-router-dom'
    3. // 同构--同一套react代码, 在服务器执行一次, 在客户端再执行一次
    4. const Header = () => {
    5. return <div>
    6. <Link to="/">HOME</Link>
    7. &nbsp;&nbsp;&nbsp;&nbsp;
    8. <Link to="/login">LOGIN</Link>
    9. </div>
    10. }
    11. export default Header

    9. src/server(服务端代码目录)/

    index.tsx

    // 注意store要使用服务端定义的store

    1. import express from 'express';
    2. import React from 'react';
    3. import { render } from './utils'
    4. import routes from '../routes'
    5. import { getStore } from '../store'
    6. import { matchPath } from 'react-router-dom'
    7. import _ from 'lodash'
    8. import proxy from 'express-http-proxy'
    9. var app = express();
    10. /**
    11. * 客户端渲染
    12. * react代码在浏览器上执行, 消耗的是用户浏览器的性能
    13. *
    14. * 服务器渲染
    15. * react代码在服务器上执行, 消耗的是服务器端的性能(或者资源)
    16. * 报错信息查询网站: stackoverflow.com
    17. */
    18. // 服务器请求/api的时候, 做代理
    19. app.use('/api', proxy('http://rap2api.taobao.org', {
    20. proxyReqPathResolver(req) {
    21. console.log(req.url,'req.url=======')
    22. // const parts = req.url.split('?')
    23. // const queryString = parts[1]
    24. // const updatedPath = parts[0].replace(/test/, 'tent');
    25. return req.url
    26. }
    27. }));
    28. // 只要是静态文件, 都到public目录找
    29. app.use(express.static('public'));
    30. // * => 任意路径都能走到下列的方法
    31. app.get('*', function (req, res) {
    32. const store = getStore()
    33. // 如果在这里, 能够拿到异步数据并填充到store之中
    34. // store里面到底填充什么, 我们不知道, 我们需要结合当前用户请求地址, 和路由, 做判断
    35. // 如果用户访问根/路径, 我们就拿home组件的异步数据
    36. // 如果用户访问/login路径, 我们就拿login组件的异步数据
    37. // 根据路由的路径, 来往store里面加数据
    38. const matchedRoutes = [];
    39. function getRoutes(routes) {
    40. routes.some(route => {
    41. const match = matchPath(req.path, route);
    42. if (match) {
    43. matchedRoutes.push(route)
    44. }
    45. if (Array.isArray(_.get(route, 'routes')) && _.get(route, 'routes')) {
    46. getRoutes(_.get(route, 'routes'))
    47. }
    48. return match
    49. })
    50. }
    51. getRoutes(routes)
    52. // 让matchRoutes里面所有的组件, 对应的loadData执行一次
    53. const promises = []
    54. matchedRoutes.forEach(route => {
    55. try {
    56. promises.push(route.loadData(store))
    57. } catch (error) {
    58. }
    59. })
    60. console.log(matchedRoutes, 'matchedRoutes==')
    61. Promise.all(promises)
    62. .then(() => {
    63. res.send(render({store, routes, req}))
    64. })
    65. })
    66. var server = app.listen(2000);

    utils.tsx

    添加script标签是因为模板字符串渲染成dom, onClick等事件没有反应, 所以script标签再同构一下

    1. import React from 'react';
    2. import { renderToString } from "react-dom/server"
    3. import { StaticRouter, Route } from 'react-router-dom'
    4. import { Provider } from 'react-redux'
    5. import _ from 'lodash'
    6. import { renderRoutes } from 'react-router-config'
    7. export const render = ({store, routes, req}) => {
    8. // 虚拟dom是真实dom的一个JavaScript对象的映射
    9. const content = renderToString((
    10. <Provider store={store}>
    11. <StaticRouter location={req.path} context={{}}>
    12. <div>
    13. {renderRoutes(routes)}
    14. </div>
    15. </StaticRouter>
    16. </Provider>
    17. ))
    18. // 当store数据更新完成再渲染dom
    19. return (
    20. `<html>
    21. <head>
    22. <title>ssr</title>
    23. <link rel="icon" href="/flower.jpg" />
    24. </head>
    25. <body>
    26. <div id="root">${content}</div>
    27. <script>
    28. window.context = ${JSON.stringify({state:store.getState()})}
    29. </script>
    30. <script src="/index.js"></script>
    31. </body>
    32. </html>`
    33. )
    34. }

    request/index.ts

    导出服务端axios实例

    1. import axios from "axios";
    2. const instance = axios.create({
    3. baseURL: 'http://rap2api.taobao.org'
    4. })
    5. export default instance

    10. src/client(客户端代码目录)/

    index.tsx

    // 注意store要使用客户端定义的store

    1. import React from "react";
    2. import ReactDOM from "react-dom";
    3. import { BrowserRouter, Route } from "react-router-dom";
    4. import routes from "../routes";
    5. import { Provider } from "react-redux";
    6. import { getClientStore } from '../store';
    7. import { renderRoutes } from 'react-router-config'
    8. const App = () => {
    9. return (
    10. <Provider store={getClientStore()}>
    11. <BrowserRouter>
    12. <div>
    13. {renderRoutes(routes)}
    14. </div>
    15. </BrowserRouter>
    16. </Provider>
    17. );
    18. };
    19. ReactDOM.hydrate(<App />, document.getElementById("root"));

    request/index.ts

    导出客户端axios实例

    1. import axios from "axios";
    2. const instance = axios.create({
    3. baseURL: '/api'
    4. })
    5. export default instance

    11. 因为npm-run-all, 所以执行yarn dev就能运行代码并监听组件是否修改了

    修改home组件, 刷新浏览器就行了

  • 相关阅读:
    java每日一题:static与final的区别
    GIT高级使用技巧
    Docker系列--在容器中安装JDK的方法(有示例)
    微信小程序_22,全局数据共享
    Lumiprobe染料 NHS 酯丨BDP FL NHS 酯研究
    快鲸scrm推出教育培训行业私域运营解决方案
    sqlalchemy从入门到熟悉(一)
    章节二:Rasa创建一个简单助手
    C#基础--委托、lambda表达式和事件
    1.两数之和(哈希)
  • 原文地址:https://blog.csdn.net/qq_42750608/article/details/134256263