什么是服务端渲染
- renderToString 将react组件转换为字符串随浏览器返回
- 事件(onclick)处理,需要客户端实现同样的代码 单独打包 然后服务端直接引入 同时也要主要静态资源的跟目录
- 路由控制 服务端使用react-router-dom/StaticRouter 来实现路由跳转
- 请求数据 需要给组件添加一个静态方法 配置路由的时候添加一个属性 标志是当前组件需要请求数据
- 使用react-router-dom/matchPath来进行路由匹配
- 多级路由匹配使用react-router-config/renderRoutes
- matchRoutes来匹配多级路由下当前匹配的路由
- 匹配的路由遍历 然后将存在loadData标识的函数执行,即route.loadData(store),并将这个结果存在一个promises数组里面。loadData函数执行返回store.dispatch()(返回一个promise) promise.all 全部执行成功代表数据返回 开始渲染页面。
- 404的实现可以在 static-router 上传递的context值添加一个404的标识,然后服务端判断这个标识来显示状态码404
- 301 会根据context.action == 'REPLACE' 来进行页面跳转 res.redirect(301,context.url);
- 渲染CSS 服务端使用isomorphic-style-loader的loader
- 引入样式的页面this.props.staticContext.csses.push(styles._getCss());
- 服务端判断if (context.csses.length>0) { cssStr=context.csses.join('\r\n');}
服务端渲染
浏览器直接返回HTML字符串到页面中
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);
复制代码
客户端渲染
页面上的内容由于浏览器运行JS脚本而渲染到页面上的
let express=require('express');
let app=express();
app.get('/',(req,res) => {
res.send(`
<html>
<body>
<div id="root"></div>
<script>
document.getElementById('root').innerHTML = 'hello';
</script>
</body>
</html>
`);
});
app.listen(9090);
复制代码
服务器端打包React组件
- webpack.config.js 配置
//服务器端
let path = require('path');
<!--排除node本身自带的模块打包,因为服务端环境就是node-->
let nodeExternals=require('webpack-node-externals');
module.exports = {
target: 'node',//打包的是服务器端node文件
mode:'development',//开发模式
output:{
path:path.resolve(__dirname,'build'),
filename:'bundle.js'
},
externals:[nodeExternals()],
module:{
rules:[
{
test:/\.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
options: {
presets: [
[
"@babel/preset-env",{
targets: {
browsers:['last 2 versions']
}
}
],"@babel/preset-react"
]
}
}
]
}
}
复制代码
- Home/index.js
import React,{Component} from 'react';
export default class Home extends Component{
render() {
return <div>Home</div>
}
}
复制代码
- server/src/index.js
import React from 'react';
import Home from './containers/Home';
<!--react 服务端渲染 将组建转换为字符串-->
import {renderToString} from 'react-dom/server';
import express from 'express';
let app=express();
const html=renderToString(<Home />);
app.get('/',(req,res) => {
res.send(`
<html>
<body>
<div id="root">
${html}
</div>
</body>
</html>
`);
});
app.listen(9090);
复制代码
- package.json
"scripts": {
"start": "node ./server/build/bundle.js",
"build":"webpack --config server/webpack.config.js"
}
// 优化后的package.json
// npm-run-all 执行多条命令 nodemon 监控文件变化重启
"scripts": {
"dev":"npm-run-all --parallel dev:**",
"dev:start":"nodemon './server/build/bundle.js'",
"dev:build":"webpack --config server/webpack.config.js --watch"
}
复制代码
计数器组件(服务端处理事件)
- src/client/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import Counter from '../containers/Counter';
ReactDOM.hydrate(<Counter/>,document.querySelector('#root'));
复制代码
- src/containers/Counter/index.js
import React,{Component} from 'react';
export default class Counter extends Component{
state={number:0}
render() {
return (
<div>
<p>{this.state.number}</p>
<button onClick={()=>this.setState({number:this.state.number+1})}>+</button>
</div>
)
}
}
复制代码
- webpack.client.js
//浏览器端
let path = require('path');
module.exports = {
mode: 'development',//开发模式
entry:path.resolve(__dirname,'./src/client/index.js'),
output:{
path:path.resolve(__dirname,'public'),
filename:'index.js'
},
module:{
rules:[
{
test:/\.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
options: {
presets: [
"@babel/preset-env",
"@babel/preset-react"
],
plugins: [
"@babel/plugin-proposal-class-properties"
]
}
}
]
}
}
复制代码
- server/index.js
import React from 'react';
import {renderToString} from 'react-dom/server';
import Home from '../containers/Home';
import express from 'express';
let app=express();
<!--静态资源目录以public为准-->
app.use(express.static('public'));
const content=renderToString(<Home />);
app.get('/',(req,res) => {
res.send(`
<!DOCTYPE html>
<html lang="en">
<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></title>
</head>
<body>
<div id="root">${content}</div>
<!--引入客户端打包的index.js文件 来处理浏览器端事件-->
<script src="/index.js"></script>
</body>
</html>
`);
});
app.listen(9090);
复制代码
使用路由
- src/client/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import {BrowserRouter} from 'react-router-dom';
import routes from '../routes';
<!--注意ReactDOM,render 和ReactDOM.hydrate的区别-->
ReactDOM.hydrate(
<BrowserRouter>
{routes}
</BrowserRouter>
,document.querySelector('#root'));
复制代码
- src/server/index.js
import React from 'react';
import {renderToString} from 'react-dom/server';
import Home from '../containers/Home';
import express from 'express';
<!--服务端路由StaticRouter-->
import {StaticRouter} from 'react-router-dom';
import routes from '../routes';
let app=express();
app.use(express.static('public'));
//context数据的传递 StaticRouter需要知道当前路径
app.get('*',(req,res) => {
const content=renderToString(
<StaticRouter context={{}} location={req.path}>
{routes}
</StaticRouter>
);
res.send(`
<!DOCTYPE html>
<html lang="en">
<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></title>
</head>
<body>
<div id="root">${content}</div>
<script src="/index.js"></script>
</body>
</html>
`);
});
app.listen(9090);
复制代码
- src/routes.js
import React,{Fragment} from 'react';
import {Route} from 'react-router-dom';
import Home from './containers/Home';
import Counter from './containers/Counter';
export default (
<Fragment>
<Route path="/" exact component={Home}></Route>
<Route path="/counter" exact component={Counter}></Route>
</Fragment>
)
复制代码
跳转路由
- src/client/index.js
import React,{Fragment} from 'react';
import ReactDOM from 'react-dom';
import {BrowserRouter} from 'react-router-dom';
import Header from '../components/Header';
import routes from '../routes';
ReactDOM.hydrate(
<BrowserRouter>
<Fragment>
<Header/>
<div className="container" style={{marginTop:50}}>
{routes}
</div>
</Fragment>
</BrowserRouter>
,document.querySelector('#root'));
复制代码
- /Header/index.js
import React,{Component} from 'react';
import {Link} from 'react-router-dom';
export default class Home extends Component{
render() {
return (
<nav className="navbar navbar-inverse navbar-fixed-top">
<div className="container">
<div className="navbar-header">
<a className="navbar-brand" href="#">珠峰SSR</a>
</div>
<div id="navbar" className="collapse navbar-collapse">
<ul className="nav navbar-nav">
<li><Link to="/">Home</Link></li>
<li><Link to="/counter">Counter</Link></li>
</ul>
</div>
</div>
</nav>
)
}
}
复制代码
- src/server/index.js
import express from 'express';
import render from './render';
let app=express();
app.use(express.static('public'));
//context数据的传递 StaticRouter需要知道当前路径
app.get('*',(req,res) => {
render(req,res);
});
app.listen(9090);
复制代码
- src/server/render.js
import React,{Component,Fragment} from 'react';
import {StaticRouter} from 'react-router-dom';
import Header from '../components/Header';
import routes from '../routes';
import {renderToString} from 'react-dom/server';
export default function (req,res) {
const content=renderToString(
<StaticRouter context={{}} location={req.path}>
<Fragment>
<Header/>
<div className="container" style={{marginTop:50}}>
{routes}
</div>
</Fragment>
</StaticRouter>
);
res.send(`
<!DOCTYPE html>
<html lang="en">
<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">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" >
<title>珠峰SSR</title>
</head>
<body>
<div id="root">${content}</div>
<script src="/index.js"></script>
</body>
</html>
`);
}
复制代码
Redux(引入状态管理)
- src/client/index.js
import React,{Fragment} from 'react';
import ReactDOM from 'react-dom';
import {BrowserRouter} from 'react-router-dom';
import Header from '../components/Header';
import routes from '../routes';
import {Provider} from 'react-redux';
import getStore from '../store';
ReactDOM.hydrate(
<Provider store={getStore()}>
<BrowserRouter>
<Fragment>
<Header/>
<div className="container" style={{marginTop:50}}>
{routes}
</div>
</Fragment>
</BrowserRouter>
</Provider>
,document.querySelector('#root'));
复制代码
- src/containers/Counter/index.js
import React,{Component} from 'react';
import {connect} from 'react-redux';
import actions from '../../store/actions';
class Counter extends Component{
render() {
return (
<div>
<p>{this.props.number}</p>
<button className="btn btn-primary" onClick={this.props.increment}>+</button>
</div>
)
}
}
export default connect(
state => state,
actions
)(Counter);
复制代码
- src/server/render.js
import React,{Component,Fragment} from 'react';
import {StaticRouter} from 'react-router-dom';
import Header from '../components/Header';
import routes from '../routes';
import {renderToString} from 'react-dom/server';
import getStore from '../store';
import {Provider} from 'react-redux';
export default function (req,res) {
const content=renderToString(
<Provider store={getStore()}>
<StaticRouter context={{}} location={req.path}>
<Fragment>
<Header/>
<div className="container" style={{marginTop:50}}>
{routes}
</div>
</Fragment>
</StaticRouter>
</Provider>
);
res.send(`
<!DOCTYPE html>
<html lang="en">
<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">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" >
<title>珠峰SSR</title>
</head>
<body>
<div id="root">${content}</div>
<script src="/index.js"></script>
</body>
</html>
`);
}
复制代码
- src/store/index.js
import reducer from './reducer';
import {createStore,applyMiddleware} from 'redux';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
function getStore() {
return createStore(reducer,applyMiddleware(thunk,logger));
}
export default getStore;
复制代码
客户端加载数据
- src/containers/Home/index.js
import React,{Component} from 'react';
import {connect} from 'react-redux';
import actions from '../../store/actions/home';
class Home extends Component{
componentDidMount() {
this.props.getHomeList();
}
render() {
return (
<div className="row">
<div className="col-md-12">
<ul className="list-group">
{
this.props.list.map(item => (
<li className="list-group-item" key={item.id}>{item.name}</li>
))
}
</ul>
</div>
</div>
)
}
}
export default connect(
state => state.home,
actions
)(Home);
复制代码
- src/store/reducers/home.js
import * as types from '../action-types';
import axios from 'axios';
export default {
getHomeList() {
return function (dispatch,getState) {
axios.get('http://localhost:4000/api/users').then(result => {
let list=result.data;
dispatch({
type: types.SET_HOME_LIST,
payload:list
});
});
}
}
}
复制代码
- api/server.js
let express=require('express');
let cors=require('cors');
let app=express();
var corsOptions = {
origin: 'http://localhost:9090',
optionsSuccessStatus: 200
}
app.use(cors(corsOptions));
let users=[{id:1,name:'zfpx1'},{id:2,name:'zfpx2'}];
app.get('/api/users',function (req,res) {
res.json(users);
});
app.listen(4000);
复制代码
服务器端路由
- src/client/index.js
import React,{Fragment} from 'react';
import ReactDOM from 'react-dom';
import {BrowserRouter} from 'react-router-dom';
import Header from '../components/Header';
import routes from '../routes';
import {Provider} from 'react-redux';
import {Route} from 'react-router-dom';
import getStore from '../store';
ReactDOM.hydrate(
<Provider store={getStore()}>
<BrowserRouter>
<Fragment>
<Header/>
<div className="container" style={{marginTop: 70}}>
<Fragment>
{routes.map(route => (
<Route {...route}/>
))}
</Fragment>
</div>
</Fragment>
</BrowserRouter>
</Provider>
,document.querySelector('#root'));
复制代码
- src/containers/Home/index.js
Home组件添加一个静态方法loadData,标志这个组件需要动态获取数据。
import React,{Component} from 'react';
import {connect} from 'react-redux';
import actions from '../../store/actions/home';
class Home extends Component{
static loadData=() => {
console.log('加载数据');
}
//componentDidMount在服务器端是不执行的
componentDidMount() {
this.props.getHomeList();
}
render() {
return (
<div className="row">
<div className="col-md-12">
<ul className="list-group">
{
this.props.list.map(item => (
<li className="list-group-item" key={item.id}>{item.name}</li>
))
}
</ul>
</div>
</div>
)
}
}
export default connect(
state => state.home,
actions
)(Home);
复制代码
- src/routes.js
import React,{Fragment} from 'react';
import {Route} from 'react-router-dom';
import Home from './containers/Home';
import Counter from './containers/Counter';
export default [
{
path: '/',
component: Home,
exact: true,
key:'home',
loadData:Home.loadData
},
{
path: '/counter',
component: Counter,
key:'login',
exact: true
}
]
复制代码
- src/server/render.js
服务端使用matchPath来找出当前匹配的路由
import React,{Component,Fragment} from 'react';
import {StaticRouter} from 'react-router-dom';
import Header from '../components/Header';
import routes from '../routes';
import {renderToString} from 'react-dom/server';
import {Route,matchPath} from 'react-router-dom';
import getStore from '../store';
import {Provider} from 'react-redux';
export default function (req,res) {
let store=getStore();
<!--找出当前匹配的路由-->
let matchedRoutes=routes.filter(route => {
return matchPath(req.path,route);
});
console.log(matchedRoutes);
const content=renderToString(
<Provider store={store}>
<StaticRouter context={{}} location={req.path}>
<Fragment>
<Header/>
<div className="container" style={{marginTop:70}}>
{routes.map(route => (
<Route {...route}/>
))}
</div>
</Fragment>
</StaticRouter>
</Provider>
);
res.send(`
<!DOCTYPE html>
<html lang="en">
<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">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" >
<title>珠峰SSR</title>
</head>
<body>
<div id="root">${content}</div>
<script src="/index.js"></script>
</body>
</html>
`);
}
复制代码
多级路由
- src/client/index.js
使用react-router-config的renderRoutes来匹配多级路由
import React,{Fragment} from 'react';
import ReactDOM from 'react-dom';
import {BrowserRouter} from 'react-router-dom';
import Header from '../components/Header';
import routes from '../routes';
import {Provider} from 'react-redux';
import {Route,matchPath} from 'react-router-dom';
import {matchRoutes,renderRoutes} from 'react-router-config';
import getStore from '../store';
ReactDOM.hydrate(
<Provider store={getStore()}>
<BrowserRouter>
<Fragment>
<Header/>
<div className="container" style={{marginTop: 70}}>
<Fragment>
{renderRoutes(routes)}
</Fragment>
</div>
</Fragment>
</BrowserRouter>
</Provider>
,document.querySelector('#root'));
复制代码
- src/components/Header/index.js
import React,{Component} from 'react';
import {Link} from 'react-router-dom';
export default class Home extends Component{
render() {
return (
<nav className="navbar navbar-inverse navbar-fixed-top">
<div className="container">
<div className="navbar-header">
<a className="navbar-brand" href="#">珠峰SSR</a>
</div>
<div id="navbar" className="collapse navbar-collapse">
<ul className="nav navbar-nav">
<li><Link to="/">Home</Link></li>
<li><Link to="/user/list">用户列表</Link></li>
<li><Link to="/counter">Counter</Link></li>
</ul>
</div>
</div>
</nav>
)
}
}
复制代码
- src/routes.js
import React,{Fragment} from 'react';
import Home from './containers/Home';
import User from './containers/User';
import UserList from './containers/User/components/UserList';
import Counter from './containers/Counter';
export default [
{
path: '/',
component: Home,
exact: true,
key:'/home',
loadData:Home.loadData
},
{
path: '/user',
component: User,
key: '/user',
routes: [
{
path: '/user/list',
component: UserList,
key:'/user/list'
}
]
},
{
path: '/counter',
component: Counter,
key:'login',
exact: true
}
]
复制代码
- src/server/render.js
使用matchRoutes来匹配多级路由的情况下的当前匹配路由
import React,{Component,Fragment} from 'react';
import {StaticRouter} from 'react-router-dom';
import Header from '../components/Header';
import routes from '../routes';
import {renderToString} from 'react-dom/server';
import {Route,matchPath} from 'react-router-dom';
import {matchRoutes,renderRoutes} from 'react-router-config';
import getStore from '../store';
import {Provider} from 'react-redux';
export default function (req,res) {
let store=getStore();
/**
let matchedRoutes=routes.filter(route => {
return matchPath(req.path,route);
});
*/
let matchedRoutes= matchRoutes(routes,req.path);
console.log(matchedRoutes);
const content=renderToString(
<Provider store={store}>
<StaticRouter context={{}} location={req.path}>
<Fragment>
<Header/>
<div className="container" style={{marginTop:70}}>
{renderRoutes(routes)}
</div>
</Fragment>
</StaticRouter>
</Provider>
);
res.send(`
<!DOCTYPE html>
<html lang="en">
<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">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" >
<title>珠峰SSR</title>
</head>
<body>
<div id="root">${content}</div>
<script src="/index.js"></script>
</body>
</html>
`);
}
复制代码
后台获取数据
- src/client/index.js
import React,{Fragment} from 'react';
import ReactDOM from 'react-dom';
import {BrowserRouter} from 'react-router-dom';
import Header from '../components/Header';
import routes from '../routes';
import {Provider} from 'react-redux';
import {renderRoutes} from 'react-router-config';
import {getClientStore} from '../store';
ReactDOM.hydrate(
<Provider store={getClientStore()}>
<BrowserRouter>
<Fragment>
<Header/>
<div className="container" style={{marginTop: 70}}>
<Fragment>
{renderRoutes(routes)}
</Fragment>
</div>
</Fragment>
</BrowserRouter>
</Provider>
,document.querySelector('#root'));
复制代码
- src/containers/Home/index.js
import React,{Component} from 'react';
import {connect} from 'react-redux';
import actions from '../../store/actions/home';
class Home extends Component{
static loadData=(store) => {
//dispatch方法的返回值是action,也就是返回一个promise
//https://github.com/reduxjs/redux/blob/master/src/createStore.js
return store.dispatch(actions.getHomeList());
}
//componentDidMount在服务器端是不执行的
componentDidMount() {
if(this.props.list.length==0)
this.props.getHomeList();
}
render() {
return (
<div className="row">
<div className="col-md-12">
<ul className="list-group">
{
this.props.list.map(item => (
<li className="list-group-item" key={item.id}>{item.name}</li>
))
}
</ul>
</div>
</div>
)
}
}
export default connect(
state => state.home,
actions
)(Home);
复制代码
- src/server/render.js
import React,{Component,Fragment} from 'react';
import {StaticRouter} from 'react-router-dom';
import Header from '../components/Header';
import routes from '../routes';
import {renderToString} from 'react-dom/server';
import {Route,matchPath} from 'react-router-dom';
import {matchRoutes,renderRoutes} from 'react-router-config';
import {getStore} from '../store';
import {Provider} from 'react-redux';
export default function (req,res) {
let store=getStore();
/**
let matchedRoutes=routes.filter(route => {
return matchPath(req.path,route);
});
*/
let matchedRoutes=matchRoutes(routes,req.path);
let promises=[];
matchedRoutes.forEach(item => {
if (item.route.loadData)
// 说明加载此组件需要请求数据,promises数组里面放入一个promise
promises.push(item.route.loadData(store));
});
<!--promise执行成功,数据正确返回,开始渲染页面-->
Promise.all(promises).then(result => {
const content=renderToString(
<Provider store={store}>
<StaticRouter context={{}} location={req.path}>
<Fragment>
<Header/>
<div className="container" style={{marginTop:70}}>
{renderRoutes(routes)}
</div>
</Fragment>
</StaticRouter>
</Provider>
);
res.send(`
<!DOCTYPE html>
<html lang="en">
<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">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" >
<title>珠峰SSR</title>
</head>
<body>
<div id="root">${content}</div>
<script>
// 在window上挂在状态值
window.context = {
state:${JSON.stringify(store.getState())}
}
</script>
<script src="/index.js"></script>
</body>
</html>
`);
});
}
复制代码
- src/store/index.js
将客户端store和服务端store拆离开,并且通过window.context.state进行关联
import reducers from './reducers';
import {createStore,applyMiddleware} from 'redux';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
export function getStore() {
return createStore(reducers,applyMiddleware(thunk,logger));
}
export function getClientStore() {
let initState=window.context.state;
return createStore(reducers,initState,applyMiddleware(thunk,logger));
}
复制代码
Node代理服务器
- src/server/index.js
import express from 'express';
import proxy from 'express-http-proxy';
import render from './render';
let app=express();
app.use(express.static('public'));
app.use('/api',proxy('http://127.0.0.1:4000',{
//修改请求路径
proxyReqPathResolver: function (req) {
return `/api/${req.url}`;
}
}));
//context数据的传递 StaticRouter需要知道当前路径
app.get('*',(req,res) => {
render(req,res);
});
app.listen(9090);
复制代码
- src/store/actions/home.js
import * as types from '../action-types';
import axios from 'axios';
export default {
getHomeList() {
//https://github.com/reduxjs/redux-thunk/blob/master/src/index.js
return function (dispatch,getState,request) {
//http://localhost:4000/api/users
return request.get('/api/users').then(result => {
let list=result.data;
dispatch({
type: types.SET_HOME_LIST,
payload:list
});
});
}
}
}
复制代码
- src/store/index.js
客户端和服务端分离请求
import reducers from './reducers';
import {createStore,applyMiddleware} from 'redux';
import clientRequest from '../client/request';
import serverRequest from '../server/request';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
export function getStore() {
return createStore(reducers,applyMiddleware(thunk.withExtraArgument(serverRequest),logger));
}
export function getClientStore() {
let initState=window.context.state;
return createStore(reducers,initState,applyMiddleware(thunk.withExtraArgument(clientRequest),logger));
}
复制代码
- src/client/request.js
import axios from 'axios';
export default axios.create({
baseURL:'/'
});
复制代码
- src/server/request.js
import axios from 'axios';
export default axios.create({
baseURL:'http://localhost:4000/'
});
复制代码
404
- src/routes.js
import Home from './containers/Home';
import User from './containers/User';
import UserList from './containers/User/components/UserList';
import Counter from './containers/Counter';
import Login from './containers/Login';
import Logout from './containers/Logout';
import Profile from './containers/Profile';
import NotFound from './containers/NotFound';
import App from './containers/App';
export default [
{
path: '/',
component: App,
loadData:App.loadData,
routes: [
{
path: '/',
component: Home,
exact: true,
key:'/home',
loadData:Home.loadData
},
{
path: '/user',
component: User,
key: '/user',
routes: [
{
path: '/user/list',
component: UserList,
key:'/user/list'
}
]
},
{
path: '/counter',
component: Counter,
key:'counter',
exact: true
},
{
path: '/login',
component: Login,
key:'/login',
exact: true
},
{
path: '/logout',
component: Logout,
key:'/logout',
exact: true
},
{
path: '/profile',
component: Profile,
key:'/profile',
exact: true
},
{
component: NotFound
}
]
}
]
复制代码
- src/server/render.js
import React,{Component,Fragment} from 'react';
import {StaticRouter} from 'react-router-dom';
import Header from '../components/Header';
import routes from '../routes';
import {renderToString} from 'react-dom/server';
import {matchRoutes,renderRoutes} from 'react-router-config';
import {getStore} from '../store';
import {Provider} from 'react-redux';
import App from '../containers/App';
export default function (req,res) {
let store=getStore(req);
/**
let matchedRoutes=routes.filter(route => {
return matchPath(req.path,route);
});
*/
let matchedRoutes=matchRoutes(routes,req.path);
let promises=[];
matchedRoutes.forEach(item => {
if (item.route.loadData)
promises.push(item.route.loadData(store));
});
Promise.all(promises).then(result => {
let context={};
const content=renderToString(
<Provider store={store}>
<StaticRouter context={context} location={req.path}>
{renderRoutes(routes)}
</StaticRouter>
</Provider>
);
if (context.notFound) {
res.status(404);
}
res.send(`
<!DOCTYPE html>
<html lang="en">
<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">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" >
<title>珠峰SSR</title>
</head>
<body>
<div id="root">${content}</div>
<script>
window.context = {
state:${JSON.stringify(store.getState())}
}
</script>
<script src="/index.js"></script>
</body>
</html>
`);
});
}
复制代码
- src/containers/NotFound/index.js
import React,{Component} from 'react';
export default class NotFound extends Component{
componentWillMount() {
if (this.props.staticContext) {
this.props.staticContext.notFound=true;
}
}
render() {
return (
<div>404</div>
)
}
}
复制代码
301
- server/render.js
const content=renderToString(
<Provider store={store}>
<StaticRouter context={context} location={req.path}>
{renderRoutes(routes)}
</StaticRouter>
</Provider>
);
<!--这个是返回的标识 证明是重定向-->
if (context.action == 'REPLACE') {
return res.redirect(301,context.url);
} else if (context.notFound) {
res.status(404);
}
复制代码
Promise.all
这个处理是为了如果多个请求,其中一个请求失败了,页面将不会渲染。所以需要处理一下
let promises=[];
matchedRoutes.forEach(item => {
if (item.route.loadData) {
let promise=new Promise(function (resolve,reject) {
return item.route.loadData(store).then(resolve,resolve);
});
promises.push(promise);
}
});
复制代码
使用CSS
- src/containers/App.js
import React,{Component,Fragment} from 'react';
import {renderRoutes} from 'react-router-config';
import Header from '../components/Header';
import actions from '../store/actions/session';
import styles from './App.css';
export default class App extends Component{
static loadData=(store) => {
return store.dispatch(actions.getUser());
}
render() {
return (
<Fragment>
<Header/>
<div className="container" className={styles.app}>
<Fragment>
{renderRoutes(this.props.route.routes)}
</Fragment>
</div>
</Fragment>
)
}
}
复制代码
- webpack.client.js
module:{
rules:[
{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true,
localIdentName:'[name]_[local]_[hash:base64:5]'
}
}
]
}
]
}
复制代码
- webpack.server.js
服务端需要用isomorphic-style-loader插件
module:{
rules:[
{
test: /\.css$/,
use: [
'isomorphic-style-loader',
{
loader: 'css-loader',
options: {
modules: true,
localIdentName:'[name]_[local]_[hash:base64:5]'
}
}
]
}
]
}
复制代码
- src/containers/App.css
.app{
margin-top:70px;
}
复制代码
CSS服务器端渲染
- src/containers/App.js
componentWillMount() {
if (this.props.staticContext) {
this.props.staticContext.csses.push(styles._getCss());
}
}
复制代码
- src/server/render.js
let context={csses:[]};
const content=renderToString(
<Provider store={store}>
<StaticRouter context={context} location={req.path}>
{renderRoutes(routes)}
</StaticRouter>
</Provider>
);
+ let cssStr='';
+ if (context.csses.length>0) {
+ cssStr=context.csses.join('\r\n');
+ }
if (context.action == 'REPLACE') {
return res.redirect(301,context.url);
} else if (context.notFound) {
res.status(404);
}
res.send(`
<!DOCTYPE html>
<html lang="en">
<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">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" >
+ <style>${cssStr}</style>
<title>珠峰SSR</title>
</head>
<body>
<div id="root">${content}</div>
<script>
window.context = {
state:${JSON.stringify(store.getState())}
}
</script>
<script src="/index.js"></script>
</body>
</html>
`);
复制代码