# React-SSR
# 1、简介
# 1、csr与ssr
- SSR:在服务端,把页面所有的内容都初始化好后,将整个页面的HTML丢到浏览器,给浏览器渲染,浏览器渲染的是整个页面的内容,页面的任何操作都会引起页面的刷新,服务端重新生成页面然后返回整个html文件给客户端
- 优点
- 更好的首屏性能,不需要提前下载一堆css和js后才可看到页面
- 更利于SEO,蜘蛛可以直接抓取已渲染的内容
- 缺点
- 每次页面的改变都需要服务端把所有的代码都初始化,页面需刷新后重新展示
- 优点
- CSR:服务器只返回必要的html(只有一个框架和占位节点)和js资源,js在客户端解析、执行然后渲染生成页面,同时js执行会完成页面事件的绑定,优点是前后端分离,页面交互较好
- SSR + SPA:服务端渲染返回首屏,客户端渲染进行aqs88saa交互。
- 既能满足首屏的性能和SEO,又提高了后续页面操作性能和体验
- 前后端分离开发:前端注重动画等渲染,后端注重数据及性能
# 2、同构
一般是指服务端和客户端同构,意思是服务端和客户端运行同一套代码程序
服务端执行:让 React 代码在服务器端先执行一次,使得用户下载的 HTML 已经包含了所有的页面展示内容
客户端执行:React代码在客户端再次执行,为 HTML 网页中的内容添加数据及事件的绑定,页面就具备了 React 的各种交互能力
# 3、使用
分布式,负载均衡,java较成熟,node暂无成熟方案
内部系统搭建使用nodejs,eg:淘宝广告页等
# 2、代码解析
# 1、核心代码
ReactDOMServer.renderToString(element)
服务端渲染,将React元素渲染为初始HTML,并在首次请求时将标记下发,以加快页面加载速度,并允许搜索引擎爬取你的页面以达到 SEO 优化的目的。
ReactDOM.hydrate(element, container[, callback])
与render()相同,但它用于在ReactDOMServer渲染的容器中对HTML的内容进行hydrate操作,react会尝试在已有标记上绑定事件监听器
ReactDOM.render(element, container[, callback])
在提供的container里渲染一个React元素,并返回对该组件的引用(或者对无状态组件返回null),但避免使用该引用(ref替代)
- 如果React元素之前在container里渲染过,这将会对其执行更新操作,并仅会在必要时改变DOM以映射最新的React元素,会使用React的DOM差分算法进行高效的更新
- 如果提供了可选的回调函数,该回调将在组件被渲染或更新之后被执行
# 2、打包
# 1、相关包
- npm-run-all:并行运行
- nodemon:node服务器,支持热更新
# 2、打包命令
"scripts": {
"start": "node ./build/bundle.js", //运行服务器打包代码
"dev": "npm-run-all --parallel dev:**", //并行运行dev: xx
"dev:start": "nodemon --watch build --exec node \"./build/bundle.js\"",
//--watch选项,配置文件修改,则重新打包
"dev:build:server": "webpack --config webpack.server.js --watch",
"dev:build:client": "webpack --config webpack.client.js --watch"
},
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
# 3、打包配置
- webpack.base.js
- webpack.client.js
- webpack.server.js
//1. webpack.base.js: babel-loader
//2. webpack.client.js
entry: './src/client/index.js',
output: {
filename: 'index.js',
path: path.resolve(__dirname, 'public')
},
loader: style-loader\css-loader\url-loader
module.exports = merge(config, clientConfig);
//3. webpack.server.js
entry: './src/server/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'build')
},
target: 'node', //不必把node内置模块代码进行打包
//webpack-node-externals,这个库会扫描node_modules文件夹中的所有node_modules名称,并构建一个外部函数,告诉Webpack不要捆绑这些模块或其任何子模块
externals: [nodeExternals()],
loader: isomorphic-style-loader/url-loader
//只在对应的DOM元素上生成class类名,然后返回生成的CSS样式代码,将css文件可以转化为style标签插入到html的header头中,实现server render的critical css的插入,提升页面的体验
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 2、服务端代码
# 1、相关库
- react-router-config
- renderRoutes
- matchRoutes
- express-http-proxy
- react-helmet:head管理工具
const routes = [
{
component: Root,
routes: [
{
path: "/",
exact: true,
component: Home
},
{
path: "/child/:id",
component: Child,
routes: [
{
path: "/child/:id/grand-child",
component: GrandChild
}
]
}
]
}
];
//1、matchRoutes(routes, pathname)
import { matchRoutes } from "react-router-config";
const branch = matchRoutes(routes, "/child/23");
[
routes[0],
routes[0].routes[1]
]
branch[0].match.url;
branch[0].match.isExact;
// 2、renderRoutes(routes, extraProps = {}, switchProps = {})
import { renderRoutes } from "react-router-config";
import routes from '../Routes';
const App = () => {
return (
<BrowserRouter>
<div>
{renderRoutes(routes)}
</div>
</BrowserRouter>
)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 2、代码分析
- web服务器放置静态资源
- api接口代理
- ssr路由处理:路由匹配、数据获取、ssr
import express from 'express';
const app = express();
var server = app.listen(9002);
// 1. 静态服务器:客户端打包代码index.js放置在public中
app.use(express.static('public'));
// 2. api接口代理
import proxy from 'express-http-proxy';
app.use('/api', proxy('http://127.0.0.1', {
proxyReqPathResolver: function (req) {
console.log('111api', req.path, req.url);
return '/api/' + req.url;
}
}));
// 3. 路由处理:路由匹配、数据获取、ssr
app.get('*', function (req, res) { /* */}
// 3.1 路由匹配:req.path在路由配置中匹配
import routes from '../Routes';
import { matchRoutes } from 'react-router-config'
const matchedRoutes = matchRoutes(routes, req.path);
// 3.2 数据获取
const store = getStore(req);
// 让matchRoutes里面所有的组件,对应的loadData方法执行一次
const promises = [];
matchedRoutes.forEach(item => {
if (item.route.loadData) {
const promise = new Promise((resolve, reject) => {
item.route.loadData(store).then(resolve).catch((e) => {
console.log(`1111${item.route.path}error`, e);
});
})
promises.push(promise);
}
})
// 3.3 数据获取结束后进行ssr渲染
// 使用Promise.all保证所有数据加载完成
Promise.all(promises).then(() => {
const context = { css: [] };
const html = render(store, routes, req, context);
if (context.action === 'REPLACE') {
res.redirect(301, context.url)
} else if (context.NOT_FOUND) {
res.status(404);
res.send(html);
} else {
res.send(html);
}
}).catch((e) => {
console.log('1111error', e);
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# 1、路由匹配
//react-router-config
//配置对象等价于:<Route>
export default [{
path: '/',
//接收参数component:no render or children
//key:避免重复渲染组件,性能优化
component: App,
loadData: App.loadData,
routes: [
{
path: '/',
component: Home,
exact: true,
loadData: Home.loadData,
key: 'home'
}, {
path: '/translation',
component: Translation,
loadData: Translation.loadData,
exact: true,
key: 'translation'
},
{
component: NotFound
}
]
}];
const matchedRoutes = matchRoutes(routes, req.path);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 2、数据获取
//2.1、getStore
const reducer = combineReducers({
home: homeReducer,
header: headerReducer,
translation: translationReducer
});
export const getStore = (req) => {
// 改变服务器端store的内容,那么就一定要使用serverAxios
return createStore(reducer, applyMiddleware(thunk.withExtraArgument(serverAxios(req))));
}
//2.2、serverAxios读取文件内容
const createInstance = (req) => {
return {
get: (url) => {
const dataPath = path.join(process.cwd(), 'public', url);
const data = fs.readFileSync(`${dataPath}`, 'utf8');
return Promise.resolve({
data: JSON.parse(data)
});
}
}
};
//2.3、使用promise封装数据获取
matchedRoutes.forEach(item => {
if (item.route.loadData) {
const promise = new Promise((resolve, reject) => {
item.route.loadData(store).then(resolve).catch((e) => {
console.log(`${item.route.path}error`, e);
});
})
promises.push(promise);
}
})
//Home组件代码
ExportHome.loadData = (store) => {
return store.dispatch(getHomeList())
}
//actions.js
export const getHomeList = () => {
return (dispatch, getState, axiosInstance) => {
return axiosInstance.get('/api/news.json')
.then((res) => {
const list = res.data.data;
dispatch(changeList(list))
});
}
}
const changeList = (list) => ({
type: CHANGE_LIST,
list
})
//reducer.js
export default (state = defaultState, action) => {
switch(action.type) {
case CHANGE_LIST:
return {
...state,
newsList: action.list
}
default:
return state;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# 3、渲染
拼接字符串返回:body、数据、样式
- 使用StaticRouter匹配,使用renderToString渲染字符串
- 数据挂载在window.context下
- 样式放置在style标签中
const context = { css: [] };
const html = render(store, routes, req, context);
//util.js中render
export const render = (store, routes, req, context) => {
const insertCss = (...styles) => {
styles.forEach(style => context.css.push(style._getCss()))
};
//使用StaticRouter路由匹配
const content = renderToString((
<Provider store={store} >
<StaticRouter location={req.path} context={context}>
<Switch>
{renderRoutes(routes)}
</Switch>
</StaticRouter>
</Provider>
));
const helmet = Helmet.renderStatic();
const cssStr = context.css.length ? context.css.join('\n') : '';
return `
<html>
<head>
${helmet.title.toString()}
${helmet.meta.toString()}
<style>${cssStr}</style>
</head>
<body>
<div id="root">${content}</div>
<script>
window.context = {
state: ${JSON.stringify(store.getState())}
}
</script>
<script src='/index.js'></script>
</body>
</html>
`;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 3、客户端代码
- 获取数据,进行挂载
- 使用BrowserRouter路由
- 使用ReactDom.hydrate渲染,复用模版
const store = getClientStore();
const App = () => {
return (
<Provider store={store}>
<BrowserRouter>
<div>{renderRoutes(routes)}</div>
</BrowserRouter>
</Provider>
)
}
ReactDom.hydrate(<App />, document.getElementById('root'))
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
# 1、数据获取
export const getClientStore = () => {
// 使用window.context上的数据设置初始化
const defaultState = window.context.state;
// 改变客户端store的内容,一定要使用clientAxios
return createStore(reducer, defaultState, applyMiddleware(thunk.withExtraArgument(clientAxios)));
}
//request.js中clientAxios
const instance = axios.create({
baseURL: '/',
params: {
secret: config.secret
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
# 2、组件交互
- 初始数据通过服务端获取直接加载
- 后续交互数据通过客户端发起请求获取
/* 代码结构
-Home
-store
-action.js
-constants.js
-index.js
-reducer.js
-index.js
-style.css
*/
class Home extends Component {
onClickItem(title) {
console.log('111home', title);
}
//该生命周期不会在服务端执行,服务端加载数据使用loadData
componentDidMount() {
//服务端有值,客户端直接使用不会重新获取数据
if (!this.props.list.length) {
this.props.getHomeList();
}
}
getList() {
const { list } = this.props;
return list.map(item => <div className={styles.item} key={item.id} onClick={() => { this.onClickItem(item.title) }} >{item.title}</div>)
}
render() {
return (
<Fragment>
<Helmet>
<title>首页新闻</title>
<meta name="description" content="新闻页面 - 丰富多彩的资讯" />
</Helmet>
<div className={styles.container}>
{this.getList()}
</div>
</Fragment>
)
}
}
const mapStateToProps = state => ({
list: state.home.newsList
});
const mapDispatchToProps = dispatch => ({
getHomeList() {
dispatch(getHomeList());
}
});
const ExportHome = connect(mapStateToProps, mapDispatchToProps)(withStyle(Home, styles));
ExportHome.loadData = (store) => {
return store.dispatch(getHomeList())
}
export default ExportHome;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52