浅谈React Router中的browserHistory和hashHistory

React Router简述

React router,即路由,是react生态系统中一个重要的组成部分。它可以使单页应用具有类似于多页应用的
路由系统,即前端路由。绝对多数教程中对于Router History中的browserHisotry和hashHistory都是
一笔带过:不要用hashHistory,用brwoserHistory,其实在开发中,并没有那么简单,hashhistory有着
一定的应用场景,而browserHistory也会有几个坑。

1. hashHistory vs browserHistory

现在,有这么一个简单的小例子:

index.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import { Router, Route, browserHistory, hashHistory} from 'react-router';

import App from './components/app';
import reducers from './reducers';
import User from './components/user';

const createStoreWithMiddleware = applyMiddleware()(createStore);

ReactDOM.render(
<Provider store={createStoreWithMiddleware(reducers)}>
<Router history={hashHistory}>
<Route path="/" component={App} >
<Route path="user" component={User} />
</Route>
</Router>
</Provider>
, document.querySelector('.container'));

app.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { Component } from 'react';
import { Link } from 'react-router';


export default class App extends Component {
render() {
return (
<div>
<Link to="user"><h2>User</h2></Link>
{this.props.children}
</div>
);
}
}

user.js:

1
2
3
4
5
6
7
8
9
import React, { Component } from 'react';

export default class User extends Component {
render(){
return (
<h3>User Component</h3>
)
}
}

在webpack-dev server下的运行效果大概是这样:

/
home_hash.png

/user
user_hash.png

注意到地址栏里那一串奇怪的字符串了么(?_k=jgh8bl),这是hashHistory为我们自动添加的,
而且,当我们访问/user时,路径也很奇怪。

接下来我们来试着把history = {hashHistory}换成history = {browserHistory}试试,

/
home_browser.png

/user
user_browser.png

注意,这里有个小坑,在webpack.config.js中,我们需要在devServer里面加一个
historyApiFallback: true的配置项,这一点大家应该都知道,所以就不赘述了。

嗯,这次地址栏清爽多了,没有奇怪的字符串,一切都正常了。似乎browserHistory这种
方式明显比hashHistory好很多。但是,这里要说一下,如果我们有意不希望用户直接通过
地址栏访问user路由的话,就可以使用hashHistory。

2.生产环境

假如现在项目写完了,需要部署到生产环境下,那么webpack-devserver就不能用了。现在
我们需要做两件事:1.写个简单的node服务器,2.用webpack对项目进行打包,生成bundle.js

1. node服务器

因为整个项目只需要index.html和bundle.js这两个文件,所以我们可以写个简单的静态
服务器,这里,我们用express来写,几行代码就能搞定。

server.js:

1
2
3
4
5
6
7
8
const express = require('express');
const path = require('path');

const app = express();

app.use(express.static(__dirname));

app.listen(8080);

当浏览器访问/的时候,服务器就会默认发送index.js给浏览器,由于这个机制的存在,我们不用
配置任何路由,注意,这里的路由说的不是前端路由。

2. webpack打包

webpack.config.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
25
26
module.exports = {
entry: [
'./src/index.js'
],
output: {
path: __dirname,
publicPath: '/',
filename: 'bundle.js'
},
module: {
loaders: [{
exclude: /node_modules/,
loader: 'babel',
query: {
presets: ['react', 'es2015', 'stage-1']
}
}]
},
resolve: {
extensions: ['', '.js', '.jsx']
},
devServer: {
historyApiFallback: true,
contentBase: './'
}
};

在跟路径下打开命令行里输入webpack -p就会生成一个打包好的文件,我们给它起名为bundle.js。
现在把webpack-devserver停掉,然后运行server.js,node server.js,之后访问server
指定的端口(8080)应该就会看到正常的页面了。

/
home_browser.png

/user
user_browser.png

貌似一切都OK了,但是,当我们在/user下刷新一下时就不太OK了。。。
/user
user_error.png

页面中显示的”Cannot GET user”是express给我们响应回来的内容,并不是我们刚才写的user.js,
也就是说,浏览器并没有使用我们react-router中给出的路由。

其实原因很简单,因为react-router中的路由实际上是前端路由,并不是真正意义上的路由。说白了,
不管你请求什么地址,前端路由都不会让浏览器向服务器发送请求,而是在前端直接做处理,然后给用户
看相应的页面,也就是说,无论用户请求什么路径,其实用户一直都是在index页面上。

这一点我们同样可以在浏览器的console里的Network中得到证实,理论上,当我们点击user链接或者在
浏览器中手动切换到/user时,浏览器并没有向服务器请求新的页面。但是,当我们真的部署到生产环境时,
在浏览器手动切换/user或者刷新时,浏览器并不知道我们其实是想让前端路由来做处理并且不发送请求给
服务器的,所以依然会发请求给服务器,请求/user,然后服务器没有相关的配置,所以就返回一个默认的
404页面给浏览器。

如果我们这里使用的是hashHistory,就不会有这个问题,因为hashHistory在路径后面加了一个#符号,
这就相当于告诉浏览器不要向服务器发请求。

3. 改进server.js

这里我们如果仍然想使用browserHistory的话,就要对服务器端进行改进,其实做法很简单,就是添加一个
匹配所有路径的配置,然后让这个配置返回index.html,也就是说无论浏览器请求什么路径,都会返回index.html
然后浏览器的前端路由(react-router)就能开始工作了。

server.js:

1
2
3
4
5
6
7
8
9
10
11
12
const express = require('express');
const path = require('path');

const app = express();

app.use(express.static(__dirname));

app.get('*', (req, res)=>{
res.sendFile(path.resolve(__dirname, 'index.html'));
});

app.listen(8080);

然后重启服务器,一切就正常了,而且没有向在切换到/user时向服务器发送新的请求,这说明前端路由也能正常
工作了。而且,当我们访问一个不存在的路径时,比如(/abcde),服务器也没有显示默认的Cannot GET页面
而是在控制台里报了个warning,说不能找到相应的路径。

4. 关于其他路径

也许会有同学问,如果我的服务器还配置了一套API在/api路径下,那么这套API是不是就不能用了。答案是不会。
但是我们要把这些配置写在app.get('*')之前。因为express是按照配置的顺序来对请求进行匹配的,如果写
app.get('*')之后,就会先匹配到/*,因为成功匹配上了,就会返回index.html。

server.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const express = require('express');
const path = require('path');

const app = express();

app.use(express.static(__dirname));

app.get('/api', (req, res)=>{
res.json({username: 'abc'});
});

app.get('*', (req, res)=>{
res.sendFile(path.resolve(__dirname, 'index.html'));
});

app.listen(8080);

这时我们访问/和/user时,一切正常,并且当我们访问/api时,也可以得到相应的内容。

/api
api_img.png

3. 总结

我们简略的分析了一下browserHistory和hashHistory的一些区别,并且做了个简单的
生产环境部署,以及踩了browserHistory在生产环境下的一个小坑。总的来说,一般情况
下我们都会选用在地址栏显示清爽的browserHistory,但是在一些情况下,hashHistory
也有者它的好处。