学习链接
從零開始實作 SSR — Redux 篇
從零開始實作 SSR — React-router 篇
需求:在 webpack实现ssr打包 的基础上,实现路由功能,状态管理
Router
功能
- 根据不同的 请求URL,后端生成不同的页面字符串返回
- 首次渲染后,以 SPA 应用的方式管理路由
实现
使用 react-router-dom ,服务端使用 StaticRouter 渲染 对应路由 的页面。前端在首次渲染后,再次渲染时使用 BrowserRouter
- 同构应用的路由组件
// src/client/Routes.js
import React from "react";
import { Route } from "react-router-dom";
import Home from "./components/Home";
import UserList from "./components/UserList";
const Routes = () => {
return (
<div>
<Route exact path="/" component={Home} />
<Route path="/users" component={UserList} />
</div>
);
};
export default Routes;
- 页面组件
// src/client/components/Home.js
import React from "react";
import { Link } from "react-router-dom";
const Home = () => {
return (
<div>
<div>Homepage</div>
<Link to="/users">To user page</Link>
<button onClick={() => console.log("click me")}>click me</button>
</div>
);
};
export default Home;
// src/client/components/UserList.js
import React from "react";
import { Link } from "react-router-dom";
const UserList = () => {
return (
<div>
<div>User List</div>
<Link to="/">To home page</Link>
</div>
);
};
export default UserList;
- express 根据 url 的 path,返回对应组件的 字符串
//...
app.get("*", (req, res) => {
const content = renderToString(
<StaticRouter location={req.path}>
<Routes />
</StaticRouter>
);
const html = `
<html>
<head></head>
<body>
<div id="root">${content}</div>
<script src="bundle.js"></script>
</body>
</html>
`;
res.send(html);
});
- 客户端,使用 BrowserRouter 路由的方式(SPA),接手后端渲染
// ...
ReactDOM.render(
<BrowserRouter>
<Routes />
</BrowserRouter>,
document.getElementById("root")
);
Redux
核心:类似路由的实现,在前后端分别添加 reducer, action
核心库:redux react-redux redux-thunk axios
配置及实现
babel
- 解析 jsx
- 解析 async 语法(需添加 @babel/polyfill 依赖)
{
"presets": [
"@babel/preset-react",
["@babel/preset-env", {
"useBuiltIns":"usage"
}]
]
}
- 初始化 store
import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import userReducer from "./users/users.reducer";
const store = createStore(userReducer, applyMiddleware(thunk));
export default store;
- action
import axios from "axios";
export const FETCH_USERS = "FETCH_USERS";
export const fetchUsers = () => async (dispatch) => {
const res = await axios.get("https://reqres.in/api/users?page=2");
dispatch({
type: FETCH_USERS,
payload: res.data.data,
});
};
- reducer
import { FETCH_USERS } from "./users.actions";
const reducer = (state = { users: [] }, action) => {
switch (action.type) {
case FETCH_USERS:
return {
...state,
users: action.payload,
};
default:
return state;
}
};
export default reducer;
- client 端注入 store
//...
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<Routes />
</BrowserRouter>
</Provider>,
document.getElementById("root")
);
- 服务器端 注入 store
// ...
app.get("*", (req, res) => {
const content = renderToString(
<Provider store={store}>
<StaticRouter location={req.path}>
<Routes />
</StaticRouter>
</Provider>
);
// ...
});
- UserList 页面使用 store
问题:该组件 mount 后,才请求 列表数据,导致服务端渲染没列表数据
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { Link } from "react-router-dom";
import { fetchUsers } from "../redux/users/users.actions";
const UserList = () => {
const dispatch = useDispatch();
const users = useSelector((state) => state.users);
useEffect(() => {
dispatch(fetchUsers());
}, []);
return (
<div>
<div>User List</div>
<Link to="/">To home page</Link>
<ul>
{users.map((user) => (
<li key={user.id}>{user.first_name} </li>
))}
</ul>
</div>
);
};
export default UserList;