Skip to content

数据通信

添加 store Action

js
export const actionTypes = {
  setDatas: "movies/setDatas",
  fetchMovies: "movies/fetchMovies",
};

export function setDatas(datas) {
  return { type: actionTypes.setDatas, payload: datas };
}

export function fetchMovies(page = 1, limit = 10) {
  return async function (dispatch) {
    const resp = await fetch("/api/getArticleList").then((res) => res.json());
    dispatch(setDatas(resp.data));
  };
}

添加 store Reducer

js
import { actionTypes } from "../actions/movies";

export default function (state = [], { type, payload }) {
  switch (type) {
    case actionTypes.setDatas:
      return payload;
    default:
      return state;
  }
}

添加 store

js
import { createStore, compose, applyMiddleware } from "redux";
import reducer from "./reducers";
import thunk from "redux-thunk";

let store;

store = createStore(reducer, compose(applyMiddleware(thunk)));

export default store;

页面组件

jsx
import React, { useEffect } from "react";
import { connect } from "react-redux";
import { fetchMovies } from "../../store/actions/movies";

//类组件:componentWillMount 服务端运行
//类组件:componentDidMount  服务端不运行

function Page({ movies = [], loadMovies }) {
  useEffect(() => {
    loadMovies && loadMovies();
  }, []);
  return (
    <div>
      <h1>电影列表</h1>
      <ul>
        {movies.map((m) => (
          <li key={m._id}>{m.name}</li>
        ))}
      </ul>
    </div>
  );
}

function mapStateToProps(state) {
  return {
    movies: state.movies,
  };
}

function mapDispatchToProps(dispatch) {
  return {
    loadMovies() {
      dispatch(fetchMovies());
    },
  };
}

export default connect(mapStateToProps, mapDispatchToProps)(Page);

服务器端加载数据

render.js 渲染前加载数据

js
// src/server/render.js
import loadData from "./loadData";
export default async (req, res) => {
  const context = {};
  //加载数据到仓库
  //调用对应组件(根据路由匹配到的组件)的loadData
  await loadData(req.path);

  //渲染组件
  const componentHTML = ReactDom.renderToString(
    <App location={req.path} context={context} />
  );
  const html = getHTML(componentHTML, req.path);
  res.send(html);
};

loadData.js 加载数据

js
// src/server/loadData.js
import { matchRoutes } from "react-router-config";
import routeConfig from "../routes/routeConfig";
import store from "../store";

/**
 * 负责服务端渲染前的加载
 */
export default function (pathname) {
  const matches = matchRoutes(routeConfig, pathname);
  const proms = [];
  for (const match of matches) {
    if (match.route.component.loadData) {
      proms.push(Promise.resolve(match.route.component.loadData(store)));
    }
  }
  return Promise.all(proms);
}

对应页面组件添加loadData方法

jsx
// src/pages/Movies/index.js
function Page({ movies = [], loadMovies }) {
  useEffect(() => {
    // 如果服务器处理了数据,则什么也不做
    // 如果服务器没有处理数据,则需要加载数据
    if (window.requestPath === "/movies") {
      //不需要加载数据
      console.log("不需要加载数据");
      return;
    } else {
      console.log("加载数据");
      loadMovies && loadMovies();
    }
  }, []);
  return (
    <div>
      <h1>电影列表</h1>
      <ul>
        {movies.map((m) => (
          <li key={m._id}>{m.name}</li>
        ))}
      </ul>
    </div>
  );
}
// 在组件服务端渲染之前需要运行的函数
Page.loadData = async function (store) {
  await store.dispatch(fetchMovies());
};

store.js 注入window.pageDatas

js
import { createStore, compose, applyMiddleware } from "redux";
import reducer from "./reducers";
import thunk from "redux-thunk";

let store;
if (globalThis.document) {
  store = createStore(
    reducer,
    window.pageDatas,
    compose(applyMiddleware(thunk))
  );
} else {
  store = createStore(reducer, compose(applyMiddleware(thunk)));
}
export default store;

页面模板文件添加

js
import store from "../store";

export default (componentHTML, path) => {
  const html = `<!DOCTYPE html>
  <html lang="en">
  <head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>SSR</title>
    ${getLinks()}
  </head>
  <body>
    <div id="root">${componentHTML}</div>
    <script>
      window.pageDatas = ${JSON.stringify(store.getState())}
      window.requestPath = "${path}";
    </script>
    ${getScripts()}
  </body>
  </html>
  `;
  return html;
};

模拟请求接口

js
const express = require("express");

const app = express();

app.get("/getArticleList", (req, res) => {
  console.log("getArticleList");
  res.send({
    code: 0,
    data: [
      {
        _id: 1,
        name: "react",
      },
      {
        _id: 2,
        name: "vue",
      },
    ],
    msg: "success",
  });
});

app.listen(8081, () => {
  console.log("service start on 8081");
});

``;

服务端数据共享

服务端数据共享,即在服务端渲染时,将数据传递给客户端,客户端再进行渲染。

store.js 调整

js
import { createStore, compose, applyMiddleware } from "redux";
import reducer from "./reducers";
import thunk from "redux-thunk";

function makeStore() {
  let store;
  if (globalThis.document) {
    store = createStore(
      reducer,
      window.pageDatas,
      compose(applyMiddleware(thunk))
    );
  } else {
    store = createStore(reducer, compose(applyMiddleware(thunk)));
  }
  return store;
}
export default makeStore;

App.jsx 调整

jsx
// server/App.jsx
export default ({ location, context, store }) => {
  return (
    <Provider store={store}>
      <StaticRouter location={location} context={context}>
        <RouteApp />
      </StaticRouter>
    </Provider>
  );
};
// client/App.jsx
import makeStore from "@/store";
const store = makeStore();

export default () => {
  return (
    <Provider store={store}>
      <BrowserRouter>
        <RouteApp />
      </BrowserRouter>
    </Provider>
  );
};

render.js

js
// server/render.js
import makeStore from "../store";

export default async (req, res) => {
  const store = makeStore();
  // 加载数据到仓库
  const context = {};
  //加载数据到仓库
  //调用对应组件(根据路由匹配到的组件)的loadData
  await loadData(req.path, store);
  const componentHTML = ReactDom.renderToString(
    <App location={req.path} context={context} store={store} />
  );
  res.send(getHtmlTemplate(componentHTML, req.path, store));
};

Released under the MIT License.