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);
client
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);
client.js
文件会去下载client.js
文件并在浏览器端执行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
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/,
}
]
}
}
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;
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()]
});
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;
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"));
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 />);
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"
},
}
npm install react-router-dom --save
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 />
}
]
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;
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"));
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>);
src\routes\Home.js
import React from 'react';
function Home() {
return (
<div>
Home
</div>
)
}
export default Home;
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
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;
npm install redux react-redux redux-thunk redux-promise redux-logger --save
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
}
src\store\action-types.js
export const ADD = 'ADD';
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;
src\store\actionCreators\counter.js
import { ADD } from '@/store/action-types';
const actionCreators = {
add() {
return { type: ADD };
}
}
export default actionCreators;
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;
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;
npm install cors axios --save-dev
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'));
src\store\action-types.js
export const ADD = 'ADD';
+export const SET_USER_LIST = 'SET_USER_LIST';
+export const ADD_USER = 'ADD_USER';
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;
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;
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
}
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 />
+ }
+ ]
+ }
]
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;
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;
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;
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
npm install express-http-proxy --save
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"));
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>);
src\server\request.js
import axios from 'axios'
const request = axios.create({
baseURL: 'http://localhost:5000/'
});
export default request
src\client\request.js
import axios from 'axios'
const request = axios.create({
baseURI: '/'
});
export default request
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;
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;
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;
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);
+}
npm install express-session --save
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'));
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 />
+ },
]
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;
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;
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;
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';
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;
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;
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 }
}
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;
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)
}
}
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
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
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>
);
src\routes\NotFound.js
import React from 'react';
function NotFound(props) {
return (
<div>NotFound</div>
)
}
export default NotFound;
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 />
+ }
]
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"));
npm install css-loader isomorphic-style-loader-react18 --save
src\App.css
.color {
color: red
}
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;
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
+ }
+ }
+ ]
+ }
+ ]
}
}
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"));
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>);
npm install react-helmet --save
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;
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"));
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;
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"));
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;
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'));