盒子
盒子
文章目录
  1. react, redux, react-router配合使用实例
    1. react-router
    2. Reducers
    3. Action
    4. Store
  2. Server Redering 服务端渲染
  3. 总结

react使用服务器渲染来优化SEO

seo我认为这个就是搜索引擎没有吧这个东西做好, 然后就把这个抛给了开发者(百度), 看看google, 在2015年就把这个事情给解决了, 不光分析你的html代码还分析你的cssjavascript脚本
不过没有办法谁让我们出门不用穿防弹衣呢!!!

动态页面想要做好seo优化, 在网上看了看常用的有两种

  • 使用PhantomJS爬虫来解决(改变html内容结构)
  • 使用服务端渲染(使用react的renderToString使用服务器来输入renderToString之后的html内容)

react, redux, react-router配合使用实例

react中提供了两个方法renderToStringrenderToStaticMarkup 用来将组件(Virtual DOM)输出成 HTML 字符串,这是 React 服务器端渲染的基础,它移除了服务器端对于浏览器环境的依赖,所以让服务器端渲染变成了一件有吸引力的事情。

想要达到服务器渲染除了解决浏览器环境的依赖, 还要解决两个问题

  • 前后端可以共享代码
  • 前后端路由可以统一处理

这里我们使用react-router来管理路由, 使用redux来管理stroe

这里不介绍redux的使用, 大家可以redux中文来学习redux

react-router

react-router是通过一种声明式的方式匹配不同路由来分配不同组建内容的, 通过props来变更路由然后出发re-render

假设有一个很简单的应用,只有两个页面,一个列表页 /list 和一个详情页 /item/:id,点击列表上的条目进入详情页。

可以这样定义路由,./routes.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React from 'react';
import { Route } from 'react-router';
import { List, Item } from './components';
// 无状态(stateless)组件,一个简单的容器,react-router 会根据 route
// 规则匹配到的组件作为 `props.children` 传入
const Container = (props) => {
return (
<div>{props.children}</div>
);
};
// 配置route 规则:
// - `/list` 显示 `List` 组件
// - `/item/:id` 显示 `Item` 组件
const routes = (
<Route path="/" component={Container} >
<Route path="list" component={List} />
<Route path="item/:id" component={Item} />
</Route>
);
export default routes;

上面可以看出我们指定了一个列表页 /list 和一个详情页 /item/:id,点击列表上的条目进入详情页。分别对应组建ListItem组建
接下来我们就通过这个简单的例子来实现服务器渲染的细节.

Reducers

redux中store是由reducer产出的, 所以reducer实际上反应了Store的树结构

./reducers/index.js

1
2
3
4
5
6
7
8
9
import listReducer from './list';
import itemReducer from './item';
export default function rootReducer(state = {}, action) {
return {
list: listReducer(state.list, action),
item: itemReducer(state.item, action)
};
}

rootReducerstate参数就是一个Store的状态树, 状态书下的内阁字段对应也可以有自己的reducer, 这个引入ListReduceritemReducer, 可以看到这两个reducer的state参数就是一个整个状态树对应的list和item字段.

具体看./reducers/list.js

1
2
3
4
5
6
7
const initalState = [];
export default function listReducer (state = initialState, action) {
switch(action.type) {
case 'FETCH_LIST_SUCCESS': return [...action.payload];
default: return state;
}
}

list就是一个包含items的数组, 结构类似[{id: 0, name: ‘li’}, {id: 1, name: “wang”}], 通过actions.type获取相应的store具体值

继续看./reducers/item.js

1
2
3
4
5
6
7
8
const initialState = {};
export default function listReducer (state = initialState, action) {
switch(action.type) {
case 'FETCH_ITEM_SUCCESS': return [...action.payload];
default: return state;
}
}

Action

对应的想要修改store就必须要有两个action来获取list和item, 触发reducer改变store, 然后返回新的store, 从而改变渲染内容,

./actions/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import fetch from 'isomorphic-fetch';
export function fetchList() {
return (dispatch) => {
return fetch('/api/list')
.then(res => res.json())
.then(json => dispatch({ type: 'FETCH_LIST_SUCCESS', payload: json }));
}
}
export function fetchItem(id) {
return (dispatch) => {
if (!id) return Promise.resolve();
return fetch(`/api/item/${id}`)
.then(res => res.json())
.then(json => dispatch({ type: 'FETCH_ITEM_SUCCESS', payload: json }));
}
}

通过调用dispatch出去相应的action函数来达到具体操作store中的值

isomorphic-fetch是一个后端实现Ajax的实现,
这里具体涉及到异步请求, 这里的action用到了thunk, redux通过thunk-middewar来处理异步action, 把函数当做普通的action dispatch就好了, 无非就是在action内部可以拿到dispatch函数来操作store. 通常看成成普通高阶函数就ok.

Store

这里用独立的./store.js, 配置生成的store

1
2
3
4
5
6
7
8
9
10
import { createStore } from 'redux';
import rootReducer from './reducers';
// Apply middleware here
// ...
export default function configureStore(initialState) {
const store = createStore(rootReducer, initialState);
return store;
}

react-redux

接着我们就实现<List>, <Item>组建, 然后把Redxu和react组件关联起来, 具体细节参见redux教程

./app.js入口文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React from 'react';
import { render } from 'react-dom';
import { Router } from 'react-router';
import createBrowserHistory from 'history/lib/createBrowserHistory';
import { Provider } from 'react-redux';
import routes from './routes';
import configureStore from './store';
// `__INITIAL_STATE__` 来自服务器端渲染,下一部分细说
const initialState = window.__INITIAL_STATE__;
const store = configureStore(initialState);
const Root = (props) => {
return (
<div>
<Provider store={store}>
<Router history={createBrowserHistory()}>
{routes}
</Router>
</Provider>
</div>
);
}
render(<Root />, document.getElementById('root'));

上面全部部分都是我们的客户端代码.(就是一个简单的react, 配合react-rout和redux的demo)

下面我们的重头戏来了

Server Redering 服务端渲染

对于上面能看懂的朋友, 下面就相对简单了, , , (难点就在redux的使用上面) 获取数据可以调用action,
在服务器端用一个match(react-router提供) 方法将拿到的request url匹配到我们之前定义的routes, 解析成和客户端一致的props对象传递给组件.

./server.js

我们使用express来渲染(这里是离不开node的, 别想着离开node来做这件事)

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
import express from 'express';
import React from 'react';
import { renderToString} from 'react-dom/server';
import { Provider } from 'react-redux';
import { RoutingContext, match } from 'react-router';
import routes from './routes';
import configureStore from './store';
const app express();
/**
* 装载满的页面
*/
function renderFullPage( html, initialState) {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<div id="root">
<div>
${html}
</div>
</div>
<script>
window.__INITIAL_STATE__ = ${JSON.stringify(initialState)};
</script>
<script src="/static/bundle.js"></script>
</body>
</html>
`;
}
app.use( (req, res ) => {
match( { routes, location:req.url }, ( err, redirectLocation, renderProps ) => {
if (err) {
res.state(500).end(`这里出错了!具体错误:${err}`);
}else if ( redirectLocation) {
res.redirect(redirectLocation.pathname + redirectLocation.search);
}else if (renderprops) {
const store = configureStore(); // 这里和客户端渲染使用的同一个store
const state = store.getState();
Promise.all([
store.dispatch(fetchList()),
store.dispatch(fetchItem(renderProps.params.id))
])
.then(() => {
const html = renderToString(
<Provider store={store}>
<RoutingContext {...renderProps} />
</Provider>
);
res.end(renderFullPage(html, store.getState()));
});
} else {
res.status(404).end("Not found");
}
})
})

上面已经标出了和客户端使用的同一个Store数据, 另外注意renderFullPage生成的页面html在react组件mount的部分(<div id=”root”), 前后端的html结构应该是一致的, 然后要把store的状态书写入一个全局变量(__INITIAL_STATE__), 这样客户端初始化render的时候能够校验服务器生成的html结构, 并且同步到初始化状态, 然后整个页面被客户端接管.

总结

页面内部链接跳转使用react-router提供的<Link>组建代替<a>标签就ok了, 其他交给react-router就ok

这里已经全部结束了, 总结一下, 是不是觉得很简单, 很大一部分是说了redux的基本用法, 真正用到的只是./server.js来渲染,
其中使用express中间件来监听requestresult来使用react-router提供 match 方法来拿到当前的客户端路由然后后端通过renderToString渲染成字符串返回给客户端.

还有一种优化seo的方式是通过爬虫的方法, 但是我不喜欢那种, 有点作弊的嫌疑, 如果真的被判定作弊, 那么就不是优化的问题了.