如何定义路由(router)

电讯网络设备路由器(router)

路由器(英语:Router,又称路径器)是一种电讯网络设备,提供路由与转送两种重要机制,可以决定数据包从来源端到目的端所经过的路由路径(host到host之间的传输路径),这个过程称为路由;将路由器输入端的数据包移送至适当的路由器输出端(在路由器内部进行),这称为转送。路由工作在OSI模型的第三层——即网络层,例如网际协议(IP)。

http(s) request router(应用层)


URI vs URL

网址/链接

URI(Uniform Resource Identifier/统一资源标识符),URI用字符串标识某一互关网资源。支持30多种协议方案(http,https,ftp,file,mailto 等等 )。

URL(Uniform Resource Locator/统一资源定位符),URL标识资源的地点。

URL是URI的子集。

uri example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ftp://linghit.com/shunli/lunar.json
http://zxcs.linghit.com/indexpage/index.html
mailto:sunyi@linghit.com
tel:+1-388-888-8888
telnet://192.168.1.123
https://www.eather.com/get?code=1001

//绝对URI
https://zxcs.linghit.com/query(cgi)

//绝对URL
https://zxcs.linghit.com/forecast.images/banner.png

//相对URI
location.href = '/api/v2/order/query'

URI格式

格式

协议名://用户名:密码@服务器地址:端口/带层次的文件路径?查询字符串#文件片段标识符

如果类比vue-router,react-router的路由配置,带层次的文件路径可以翻译成嵌套关系:

1
2
3
4
5
6
7
8
9
10
React.render((
<Router>
<Route path="/" component={App}>
<Route path="about" component={About} />
<Route path="inbox" component={Inbox}>
<Route path="messages/:id" component={Message} />
</Route>
</Route>
</Router>
), document.body)

通过上面的配置,这个应用知道如何渲染下面四个 URL:

URL 组件
/ App
/about App -> About
/inbox App -> Inbox
/inbox/messages/:id App -> Inbox -> Message

案例

https://root:123456@www.zxcs.linghit.com:80/dir1/dir2/index.html?querya=1&queryb=2#name

登录认证信息可选,端口号可先,无就用默认。


静态网站

定义

在网站设计中,纯粹HTML格式的网页通常被称为“静态网页”,早期的网站一般都是由静态网页制作的。

静态网页的网址形式通常为:

也就是以.htm、.html、.shtml、.xml等为后后缀的。在HTML格式的网页上,也可以出现各种动态的效果,如.GIF格式的动画、FLASH、滚动字母等,这些“动态效果”只是视觉上的。

静态网页的特点

  • 静态网页每个网页都有一个固定的URL,且网页URL以.htm、.html、.shtml等常见形式为后缀,而不含有“?”;
  • 网页内容一经发布到网站服务器上,无论是否有用户访问,每个静态网页的内容都是保存在网站服务器上的,也就是说,静态网页是实实在在保存在服务器上的文件,每个网页都是一个独立的文件;
  • 静态网页的内容相对稳定,因此容易被搜索引擎检索;
  • 静态网页没有数据库的支持,在网站制作和维护方面工作量较大,因此当网站信息量很大时完全依靠静态网页制作方式比较困难;
  • 静态网页的交互性较差,在功能方面有较大的限制。

demo:


动态网站

定义

动态网站并不是指具有动画功能的网站,而是指网站内容可根据不同情况动态变更的网站,一般情况下动态网站通过数据库进行架构。 动态网站除了要设计网页外,还要通过数据库和编程序来使网站具有更多自动的和高级的功能。动态网站体现在网页一般是以asp,jsp,php,aspx等结束,而静态网页一般是HTML(标准通用标记语言的子集)结尾,动态网站服务器空间配置要比静态的网页要求高,费用也相应的高,不过动态网页利于网站内容的更新,适合企业建站。动态是相对于静态网站而言。

动态网站的特点

  • 动态网站可以实现交互功能,如用户注册、信息发布、产品展示、订单管理等等;
  • 动态网页并不是独立存在于服务器的网页文件,而是浏览器发出请求时才反馈网页;
  • 动态网页中包含有服务器端脚本,所以页面文件名常以asp、jsp、php等为后缀。但也可以使用URL静态化技术,使网页后缀显示为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
39
40
41
42
43
const http = require('http')
const fs = require('fs')
const httpPort = 3000

function simpleRouter(ctx, url) {
let res;
if (/query/i.test(url)) {
res = `
<h1>query</h1>
<h2>${url}</h2>
`;
} else if (/date/i.test(url)) {
res = `
<h1>date</h1>
<h2>${new Date().toString()}</h2>
`;
} else if ('/test.html' === url) {
res = fs.readFileSync('test.html', 'utf-8', (err, content) => {
if (err) {
console.log(err);
}
})
} else {
res = '404';
}

return res;

}

http.createServer((req, res) => {
let url = req.url,
response;

res.writeHead(200, {
'Content-Type': 'text/html; charset=utf-8'
})

response = simpleRouter(res, url);
res.end(response);
}).listen(httpPort, () => {
console.log('Server listening on: http://localhost:%s', httpPort)
})

在线nodejs代理代码

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
const Koa = require('koa');
const serve = require('koa-static-server');
const router = require('koa-router')();
const app = new Koa();
const path = require('path');
const koaBody = require('koa-body');
const request = require('request');
const rp = require('request-promise');

app.use( router.routes() ).use( router.allowedMethods() );

router.all(
'*',
koaBody(),
async function(ctx, next) {
let url = ctx.request.url,
proxy = 'http://sandbox.zxcs.linghit.com';
method = ctx.request.method,
referer = ctx.request.header.referer,
api = `${proxy}${url}`;
if( /api/i.test(url) ) {

if( 'GET' === method ) {
let result = await new Promise( (resolve, reject) => {
request.get(api, (err, res, body) => {
if( err ) {
reject(err);
} else {
resolve(body);
}
});
} );
ctx.body = result;
} else if('POST' === method) {
let result = await new Promise( (resolve, reject) => {
request.post({
url: api,
form: ctx.request.body
}, (err, res, body) => {
if( err ) {
reject(err);
} else {
resolve(body);
}
});
} );
ctx.body = result;
}
} else {
if( /payment/i.test(url) ) {
let result = await new Promise( (resolve, reject) => {
request.get(api, (err, res, body) => {
if( err ) {
reject(err);
} else {
resolve(body);
}
});
} );
if(/page-result/img.test(result) ) {
let new_referer = referer.substring(0, (referer.lastIndexOf('/') + 1 ) ),
query = url.substring( url.indexOf('?') ),
result = /BZJP/i.test(url) ? 'resultQt.html' : result.html;
ctx.redirect(`${new_referer}${result}${query}`);
}
ctx.body = result;
} else {
await next();
}
}
}
);

app.use(
serve({
rootDir: path.join(__dirname, '../public/')
})
);

let port = 80;

app.listen(port, () => {
console.log('');
console.log('--------------------------------');
console.log('');
console.log(`proxy start on prot ${port}`);
console.log('');
console.log('--------------------------------');
console.log('');
});

#文件片段标识符(hash)

了解http协议就会知道,url的组成部分有很多,譬如协议、主机名、资源路径、查询字段等等,其中包含一个称之为片段的部分,以“#”为标识。

例如: http://www.gmail.com/text/#123,123便是url中的hash部分。

打开控制台,输入 location.hash,你可以得到当前url的hash部分(如果当前url不存在hash则返回空字符串)。接下来,输入 location.hash = ‘123’,会发现浏览器地址栏的url变了,末尾增加了’#123’字段,并且,页面没有被重新刷新。很显然,这很符合我们的要求。


ajax的出现可以异步无刷新改变页面内容

(太熟悉了, 略过)


history

1
2
3
4
5
6
7
//向前和向后跳转
window.history.forward();
window.history.back();

//跳转到 history 中指定的一个点
window.history.go(-1); //back()
window.history.go(1); //forward()

history HTML5 api

  • history.pushState()//添加历史记录条目
  • history.replaceState()//修改历史记录条目
  • window.onpopstate//上面两方法与其配合使用

使用 history.pushState() 可以改变referrer,它在用户发送 XMLHttpRequest 请求时在HTTP头部使用,改变state后创建的 XMLHttpRequest 对象的referrer都会被改变。因为referrer是标识 /创建XMLHttpRequest对象时/this 所代表的/window对象中document的/URL。

pushState() 方法

pushState() 需要三个参数: 一个状态对象, 一个标题 (目前被忽略), 和 (可选的) 一个URL. 让我们来解释下这三个参数详细内容:

  • 状态对象 -

    • 状态对象state是一个JavaScript对象,通过pushState () 创建新的历史记录条目。无论什么时候用户导航到新的状态,popstate事件就会被触发,且该事件的state属性包含该历史记录条目状态对象的副本。
    • 状态对象可以是能被序列化的任何东西。原因在于Firefox将状态对象保存在用户的磁盘上,以便在用户重启浏览器时使用,我们规定了状态对象在序列化表示后有640k的大小限制。如果你给 pushState() 方法传了一个序列化后大于640k的状态对象,该方法会抛出异常。如果你需要更大的空间,建议使用 sessionStorage 以及 localStorage.
  • 标题 — Firefox 目前忽略这个参数,但未来可能会用到。传递一个空字符串在这里是安全的,而在将来这是不安全的。二选一的话,你可以为跳转的state传递一个短标题。

  • URL — 该参数定义了新的历史URL记录。注意,调用 pushState() 后浏览器并不会立即加载这个URL,但可能会在稍后某些情况下加载这个URL,比如在用户重新打开浏览器时。新URL不必须为绝对路径。如果新URL是相对路径,那么它将被作为相对于当前URL处理。新URL必须与当前URL同源,否则 pushState() 会抛出一个异常。该参数是可选的,缺省为当前URL。

在某种意义上,调用 pushState() 与 设置 window.location = “#foo” 类似,二者都会在当前页面创建并激活新的历史记录。但 pushState() 具有如下几条优点:

  • 新的 URL 可以是与当前URL同源的任意URL 。而设置 window.location 仅当你只修改了哈希值时才保持同一个 document。
  • 如果需要,你可以不必改变URL。而设置 window.location = “#foo”;在当前哈希不是 #foo 的情况下, 仅仅是新建了一个新的历史记录项。
  • 你可以为新的历史记录项关联任意数据。而基于哈希值的方式,则必须将所有相关数据编码到一个短字符串里。
  • 假如 标题 在之后会被浏览器用到,那么这个数据是可以被使用的(哈希则不然)。

注意 pushState() 绝对不会触发 hashchange 事件,即使新的URL与旧的URL仅哈希不同也是如此。

pushState() 方法的例子

假设在 http://mozilla.org/foo.html 中执行了以下 JavaScript 代码:

1
2
var stateObj = { foo: "bar" };
history.pushState(stateObj, "page 2", "bar.html");

这将使浏览器地址栏显示为 http://mozilla.org/bar.html,但并不会导致浏览器加载 bar.html ,甚至不会检查bar.html 是否存在。

假设现在用户又访问了 http://google.com,然后点击了返回按钮。此时,地址栏将显示 http://mozilla.org/bar.html,同时页面会触发 popstate 事件,事件对象state中包含了 stateObj 的一份拷贝。页面本身与 foo.html 一样,尽管其在 popstate 事件中可能会修改自身的内容。

如果我们再次点击返回按钮,页面URL会变为http://mozilla.org/foo.html,文档对象document会触发另外一个 popstate 事件,这一次的事件对象state object为null。 这里也一样,返回并不改变文档的内容,尽管文档在接收 popstate 事件时可能会改变自己的内容,其内容仍与之前的展现一致。

replaceState() 方法

history.replaceState() 的使用与 history.pushState() 非常相似,区别在于 replaceState() 是修改了当前的历史记录项而不是新建一个。 注意这并不会阻止其在全局浏览器历史记录中创建一个新的历史记录项。

replaceState()的使用场景在于为了响应用户操作,你想要更新状态对象state或者当前历史记录的URL。

replaceState() 方法示例

假设 http://mozilla.org/foo.html 执行了如下JavaScript代码:

1
2
var stateObj = { foo: "bar" };
history.pushState(stateObj, "page 2", "bar.html");

然后,假设http://mozilla.org/bar.html执行了如下 JavaScript:

1
history.replaceState(stateObj, "page 3", "bar2.html");

这将会导致地址栏显示http://mozilla.org/bar2.html,,但是浏览器并不会去加载bar2.html 甚至都不需要检查 bar2.html 是否存在。

假设现在用户重新导向到了http://www.microsoft.com,然后点击了回退按按钮。这里,地址栏会显示http://mozilla.org/bar2.html。加入用户再次点击回退按钮,地址栏会显示http://mozilla.org/foo.html,完全跳过了abar.html。

window.onpopstate

window.onpopstate是popstate事件在window对象上的事件处理程序.

每当处于激活状态的历史记录条目发生变化时,popstate事件就会在对应window对象上触发. 如果当前处于激活状态的历史记录条目是由history.pushState()方法创建,或者由history.replaceState()方法修改过的, 则popstate事件对象的state属性包含了这个历史记录条目的state对象的一个拷贝.

调用history.pushState()或者history.replaceState()不会触发popstate事件. popstate事件只会在浏览器某些行为下触发, 比如点击后退、前进按钮(或者在JavaScript中调用history.back()、history.forward()、history.go()方法).

当网页加载时,各浏览器对popstate事件是否触发有不同的表现,Chrome 和 Safari会触发popstate事件, 而Firefox不会.

popstate事件示例

1
2
3
4
5
6
7
8
9
10
window.onpopstate = function(event) {
alert("location: " + document.location + ", state: " + JSON.stringify(event.state));
};
//绑定事件处理函数.
history.pushState({page: 1}, "title 1", "?page=1"); //添加并激活一个历史记录条目 http://example.com/example.html?page=1,条目索引为1
history.pushState({page: 2}, "title 2", "?page=2"); //添加并激活一个历史记录条目 http://example.com/example.html?page=2,条目索引为2
history.replaceState({page: 3}, "title 3", "?page=3"); //修改当前激活的历史记录条目 http://ex..?page=2 变为 http://ex..?page=3,条目索引为3
history.back(); // 弹出 "location: http://example.com/example.html?page=1, state: {"page":1}"
history.back(); // 弹出 "location: http://example.com/example.html, state: null
history.go(2); // 弹出 "location: http://example.com/example.html?page=3, state: {"page":3}

history HTML5 api 实际应用

将首次打开的网页返回到我们预期的网页

易起问的入口为:yd.linghit.com

易起问的老师页面地址:yd.linghit.com/home/user/index?answer_id=250

这两个链接都可以直接作为外链让用户点击后进入对应的页面,正常情况下以微信为列,进入页面不跳转至其他页面点击手机的返回键,页面会关闭直接返回到进行页面的地方。老板觉得这样不太好,应该点击返回键后调到首页再退出。。。。。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// document.referrer 表示当前文档的来源,即当前文档是从那个文档打开的url,
// 抓包发现从微信直接进来的链接来源referrer是null,从网页内部跳转的是正常的,所以通过判断referrer来判断是否从微信直接跳进来进行处理,对于正常页面跳转不做处理
if(!document.referrer){
if( window.history && window.history.pushState ){
// 压入历史记录状态

history.pushState({page: 1}, "title 1", "");

// 只有使用pushState和replaceState才能监听popstate后退事件

window.addEventListener("popstate", function () {

// 后退时把当前的历史状态改为你要跳转的页面,防止多次回退又回来的问题
history.replaceState({page: 1}, "title 1", "/home/index/index");

// 这里就可以处理自己的事情了
location.href = "/home/index/index";
});
}
}

当用户点击返回的时候作一些挽留操作

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
//popstate自动触发的bug(目前暂时发现在iPhone6可以还原这个bug)
(function() {
if(window.history.pushState){
//初始时如果没有state,先加一个进去,防止popstate不触发
if(!window.history.state){
window.history.pushState('init', '', '');
}
//为当前页面history压入一个state
window.history.replaceState({hasState: true}, '', '');
console.log('push state over');

window.addEventListener('load', function() {
setTimeout(function() {

//监听页面的popstate事件。此事件一般在用户点击浏览器后退或前进按钮时触发。
window.addEventListener('popstate', function(e) {
console.log('popstate trigger.');
//侦测是用户触发的操作, 处理自定义逻辑,弹窗或其他
alert('浏览器后退!');
}, false);

}, 0);
});
}
})();

pajx(pushState + ajax) 开始接近前端路由了

http://demo.static.web.com/pjax.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
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
<!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>pjax</title>
</head>

<body>
<div>
<h1>pjax page!</h1>
</div>
<nav>
<ul id="nav-box">
<li data-index="part1.html" class="todo" class="active">part1</li>
<li data-index="part2.html">part2</li>
<li data-index="part3.html">part3</li>
</ul>
</nav>
<section id="page-containter">

</section>
<script>
(function() {

var setContent = function(content) {
var $container = document.getElementById('page-containter');
$container.innerHTML = content;
};

var makeRequest = function makeRequest(options) {
httpRequest = new XMLHttpRequest();

if (!httpRequest) {
console.error('Giving up :( Cannot create an XMLHTTP instance');
return false;
}
httpRequest.onreadystatechange = function() {
if (httpRequest.readyState === XMLHttpRequest.DONE) {
if (httpRequest.status === 200) {
options.cb(httpRequest.responseText);
} else {
console.error('There was a problem with the request.');
}
}

};
httpRequest.open('GET', options.url || 'part1.html');
httpRequest.send();
}

//页面初始化
var init = function() {
makeRequest({
url: location.search.replace('?', '') || 'part1.html',
cb: function(res) {
setContent(res);
}
});
};

init();

//导航事件
(function() {
var $box = document.getElementById('nav-box');
$box.addEventListener('click', function clickCb(e) {
var urlIndex = e.target.getAttribute('data-index');
makeRequest({
url: urlIndex,
cb: function(res) {
window.history.pushState({
page: urlIndex
}, '', '?' + urlIndex);
setContent(res);
}
});

}, false);
})();

if (window.history.pushState) {

//初始时如果没有state,先加一个进去,防止popstate不触发
if (!window.history.state) {
window.history.pushState({
page: 'part1.html'
}, '', '?part1.html');
}

window.addEventListener('load', function() {
setTimeout(function() {

//监听页面的popstate事件。此事件一般在用户点击浏览器后退或前进按钮时触发。
window.addEventListener('popstate', function(e) {
e.state && e.state.page && makeRequest({
url: e.state.page,
cb: function(res) {
setContent(res);
}
});
}, false);
}, 0);
});
}
})();
</script>
</body>

</html>

优点:

  • 减轻服务端压力

按需请求,每次只需加载页面的部分内容,而不用重复加载一些公共的资源文件和不变的页面结构,大大减小了数据请求量,以减轻对服务器的带宽和性能压力,还大大提升了页面的加载速度。

  • 优化页面跳转体验

常规页面跳转需要重新加载画面上的内容,会有明显的闪烁,而且往往和跳转前的页面没有连贯性,用户体验不是很好。如果再遇上页面比较庞大、网速又不是很好的情况,用户体验就更加雪上加霜了。使用pjax后,由于只刷新部分页面,切换效果更加流畅,而且可以定制过度动画,在等待页面加载的时候体验就比较舒服了。

缺点:

  • 不支持一些低版本的浏览器(如IE系列)

    pjax使用了pushState来改变地址栏的url,这是html5中history的新特性,在某些旧版浏览器中可能不支持。不过pjax会进行判断,功能不适用的时候会执行默认的页面跳转操作。

  • 使服务端处理变得复杂

    要做到普通请求返回完整页面,而pjax请求只返回部分页面,服务端就需要做一些特殊处理,当然这对于设计良好的后端框架来说,添加一些统一处理还是比较容易的,自然也没太大问题。另外,即使后台不做处理,设置pjax的fragment参数来达到同样的效果。

综合来看,pajx的优点很强势,缺点也几乎可以忽略,还是非常值得推荐的,尤其是类似博客这种大部分情况下只有主体内容变化的网站。关键它使用简单、学习成本小,即时全站只有极个别页面能用得到,尝试下没什么损失。pjax的github主页介绍的已经很详细了,想了解更多可以看下源码。

https://github.com/welefen/pjax

前端路由

什么是前端路由

前端路由,拥有这样一种能力:客户端浏览器可以不依赖服务端,根据不同的URL渲染不同的视图页面。

前端路由的存在合理性

在Ajax之剑还未亮出,前端仍处于襁褓之中的时候,路由的工作交给了后端。在进行页面切换的时候,浏览器发送不同的url请求;服务器接收到浏览器的请求时,通过解析不同的url去拼接需要的html或者模板,然后将结果返回给浏览器端进行渲染。

服务器端路由也是不落俗套的有利亦有弊。它的好处是安全性更高,更严格得控制页面的展现。这在某些场景中是很有用的,譬如下单支付流程,每一步只有在上一步成功执行之后才能抵达。这在服务器端可以为每一步流程添加验证机制,只有验证通过才返回正确的页面。那么前端路由不能实现每一步的验证?自然不是,姑且相信你的代码可以写的很严谨,保证正常情况下流程不会错,但是另一个不得不面对的事实是:前端是毫无安全性可言的。用户可以肆意修改代码来进入不同的流程,你可能会为此添加不少的处理逻辑。相较之下,当然是后端控制页面的进入权限更为安全和简便。

另一方面,后端路由无疑增加了服务器端的负荷,并且需要reload页面,用户体验其实不佳。

这样,前端路由就有用武之地了。首先,它的出现无疑减轻了服务器端的压力。特别是对于一个比较复杂的应用来讲,或者更确切的说,对于拥有一个复杂路由系统的应用来说,服务器端需要为每一个不同的url执行一段处理逻辑在高并发的情况下实在有点不堪重负;其次,页面的切换可以不需要刷新整个页面了,没有网络延迟,没有闪烁刷新,提升了用户体验。

前端路由实现方式

  • 在页面不刷新的前提下实现url变化
  • 捕捉到url的变化,以便执行页面替换逻辑

如何实现更新url并且页面不刷新

  1. 利用url中的hash字段;
  2. 使用html5提供的history API。

如何跟踪url的变化:

  1. hash + window.onhashchange
  2. history HTML5 api + window.onpopState

低版本不支持hashchange

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
(function(window) {

// 如果浏览器原生支持该事件,则退出
if ( "onhashchange" in window.document.body ) { return; }

var location = window.location,
oldURL = location.href,
oldHash = location.hash;

// 每隔100ms检测一下location.hash是否发生变化
setInterval(function() {
var newURL = location.href,
newHash = location.hash;

// 如果hash发生了变化,且绑定了处理函数...
if ( newHash != oldHash && typeof window.onhashchange === "function" ) {
// execute the handler
window.onhashchange({
type: "hashchange",
oldURL: oldURL,
newURL: newURL
});

oldURL = newURL;
oldHash = newHash;
}
}, 100);

})(window);

低版本不支持hisotry HTML5 api

https://github.com/browserstate/history.js/

react-router

Histories

React Router 是建立在 history 之上的。 简而言之,一个 history 知道如何去监听浏览器地址栏的变化, 并解析这个 URL 转化为 location 对象, 然后 router 使用它匹配到路由,最后正确地渲染对应的组件。

常用的 history 有三种形式, 但是你也可以使用 React Router 实现自定义的 history。

  • browserHistory
  • hashHistory
  • createMemoryHistory
    你可以从 React Router 中引入它们:
1
2
// JavaScript 模块导入(译者注:ES6 形式)
import { browserHistory } from 'react-router'

然后将它们传递给:

1
2
3
4
render(
<Router history={browserHistory} routes={routes} />,
document.getElementById('app')
)
browserHistory

Browser history 是使用 React Router 的应用推荐的 history。它使用浏览器中的 History API 用于处理 URL,创建一个像example.com/some/path这样真实的 URL 。

服务器配置

服务器需要做好处理 URL 的准备。处理应用启动最初的 / 这样的请求应该没问题,但当用户来回跳转并在 /accounts/123 刷新时,服务器就会收到来自 /accounts/123 的请求,这时你需要处理这个 URL 并在响应中包含 JavaScript 应用代码。

一个 express 的应用可能看起来像这样的:

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 port = process.env.PORT || 8080
const app = express()

// 通常用于加载静态资源
app.use(express.static(__dirname + '/public'))

// 在你应用 JavaScript 文件中包含了一个 script 标签
// 的 index.html 中处理任何一个 route
app.get('*', function (request, response){
response.sendFile(path.resolve(__dirname, 'public', 'index.html'))
})

app.listen(port)
console.log("server started on port " + port)

如果你的服务器是 nginx,请使用 try_files 指令:

1
2
3
4
5
6
server {
...
location / {
try_files $uri /index.html
}
}

当在服务器上找不到其他文件时,这可以让 nginx 服务器提供静态文件服务并指向index.html 文件。

对于Apache服务器也有类似的方式,创建一个.htaccess文件在你的文件根目录下:

1
2
3
4
5
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
IE8, IE9 支持情况

如果我们能使用浏览器自带的 window.history API,那么我们的特性就可以被浏览器所检测到。如果不能,那么任何调用跳转的应用就会导致 全页面刷新,它允许在构建应用和更新浏览器时会有一个更好的用户体验,但仍然支持的是旧版的。

你可能会想为什么我们不后退到 hash history,问题是这些 URL 是不确定的。如果一个访客在 hash history 和 browser history 上共享一个 URL,然后他们也共享同一个后退功能,最后我们会以产生笛卡尔积数量级的、无限多的 URL 而崩溃。

hashHistory

Hash history 使用 URL 中的 hash(#)部分去创建形如 example.com/#/some/path 的路由。

我应该使用 createHashHistory吗?

Hash history不需要服务器任何配置就可以运行,如果你刚刚入门,那就使用它吧。但是我们不推荐在实际线上环境中用到它,因为每一个 web 应用都应该渴望使用 browserHistory。

像这样 ?_k=ckuvup 没用的在 URL 中是什么?

当一个 history 通过应用程序的 push 或 replace 跳转时,它可以在新的 location 中存储 “location state” 而不显示在 URL 中,这就像是在一个 HTML 中 post 的表单数据。

在 DOM API 中,这些 hash history 通过 window.location.hash = newHash 很简单地被用于跳转,且不用存储它们的location state。但我们想全部的 history 都能够使用location state,因此我们要为每一个 location 创建一个唯一的 key,并把它们的状态存储在 session storage 中。当访客点击“后退”和“前进”时,我们就会有一个机制去恢复这些 location state。

createMemoryHistory

Memory history不会在地址栏被操作或读取。这就解释了我们是如何实现服务器渲染的。同时它也非常适合测试和其他的渲染环境(像 React Native )。

和另外两种history的一点不同是你必须创建它,这种方式便于测试。

1
const history = createMemoryHistory(location)

实现示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React from 'react'
import { render } from 'react-dom'
import { browserHistory, Router, Route, IndexRoute } from 'react-router'

import App from '../components/App'
import Home from '../components/Home'
import About from '../components/About'
import Features from '../components/Features'

render(
<Router history={browserHistory}>
<Route path='/' component={App}>
<IndexRoute component={Home} />
<Route path='about' component={About} />
<Route path='features' component={Features} />
</Route>
</Router>,
document.getElementById('app')
)

vue-router

HTML5 History 模式

vue-router 默认 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。

如果不想要很丑的 hash,我们可以用路由的 history 模式,这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面。

1
2
3
4
const router = new VueRouter({
mode: 'history',
routes: [...]
})

当你使用 history 模式时,URL 就像正常的 url,例如 http://yoursite.com/user/id,也好看!

不过这种模式要玩好,还需要后台配置支持。因为我们的应用是个单页客户端应用,如果后台没有正确的配置,当用户在浏览器直接访问 http://oursite.com/user/id 就会返回 404,这就不好看了。

所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。

后端配置例子

Apache
1
2
3
4
5
6
7
8
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
</IfModule>
nginx
1
2
3
location / {
try_files $uri $uri/ /index.html;
}
原生 Node.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const http = require('http')
const fs = require('fs')
const httpPort = 80

http.createServer((req, res) => {
fs.readFile('index.htm', 'utf-8', (err, content) => {
if (err) {
console.log('We cannot open 'index.htm' file.')
}

res.writeHead(200, {
'Content-Type': 'text/html; charset=utf-8'
})

res.end(content)
})
}).listen(httpPort, () => {
console.log('Server listening on: http://localhost:%s', httpPort)
})
基于 Node.js 的 Express
1
对于 Node.js/Express,请考虑使用 connect-history-api-fallback 中间件。

警告

给个警告,因为这么做以后,你的服务器就不再返回 404 错误页面,因为对于所有路径都会返回 index.html 文件。为了避免这种情况,你应该在 Vue 应用里面覆盖所有的路由情况,然后在给出一个 404 页面。

1
2
3
4
5
6
const router = new VueRouter({
mode: 'history',
routes: [
{ path: '*', component: NotFoundComponent }
]
})

或者,如果你使用 Node.js 服务器,你可以用服务端路由匹配到来的 URL,并在没有匹配到路由的时候返回 404,以实现回退。更多详情请查阅 Vue 服务端渲染文档。