stephen's blog

[object Object] object(s)
 

Hack!如何让Github Pages支持browser history

问题提出

之前我在写一个日记生成器的时候,大致思路是抓取github issues上的文章,然后使用react + react-router搭建SPA,最后部署到github pages上。其中遇到一个很麻烦的问题就是,github pages原生是不支持SPA的,举个例子:比如我们前端路由是xxx.github.io/diary/post,一旦刷新页面,github pages就会返回404,因为它并不知道这个路由/post,所以有什么解决办法呢?

解决思路

github上已经有人提供了一种hack的解决办法,具体源码在这里。接下来我们一步一步说明这个hack的方法是如何解决这个问题的:

404.html

首先我们需要提供一个404.html,当你刷新页面后,github pages是返回404的,所以这个404.html是为了覆盖原有的404页面。最关键的是,这个404页面需要提供一段脚本代码:

1
2
3
4
5
6
7
8
9
var pathPrefix = false;
var l = window.location;
l.replace(
l.protocol + '//' + l.hostname + (l.port ? ':' + l.port : '') +
l.pathname.split('/').slice(0, 2 * pathPrefix).join('/') + '/?p=/' +
l.pathname.slice(1).split('/').slice(pathPrefix).join('/').replace(/&/g, '~and~') +
(l.search ? '&q=' + l.search.slice(1).replace(/&/g, '~and~') : '') +
l.hash
);

这段代码啥意思呢?第一个pathPrefix我们先不管,看下面l.replace(),这个API是重载当前文档,并且不会产生新的历史纪录,即覆盖History对象中的当前纪录,后面的一段代码就是将当前的url转换成查询字符串以及hash片段,比如:

1
2
3
$ http://www.foo.tld/one/two?a=b&c=d#qwe
->
$ http://www.foo.tld/?p=/one/two&q=a=b~and~c=d#qwe

再说说pathPrefix,如果你的页面不是使用自定义域名,而是xxx.github.io/xxx,则应该设置为true,不然就为false,举个例子:

1
2
3
$ https://username.github.io/repo-name/one/two?a=b&c=d#qwe
->
$ https://username.github.io/repo-name/?p=/one/two&q=a=b~and~c=d#qwe

这是因为需要保留repo的名称。说了这么多,这个到底有什么用呢?不急,继续看下面。

index.html

Github pages在收到404.html新请求后(l.replace()重载文档会发送一个新请求,比如/diary/post/2015转换成了 /diary/?p=/post/2015),这时候会忽略查询参数和hash片段,返回index.html,相当于/diary/index.html,这时候魔法出现了,我们在index.html的头部加入以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(function(l) {
if (l.search) {
var q = {};
l.search.slice(1).split('&').forEach(function(v) {
var a = v.split('=');
q[a[0]] = a.slice(1).join('=').replace(/~and~/g, '&');
});
if (q.p !== undefined) {
window.history.replaceState(null, null,
l.pathname.slice(0, -1) + (q.p || '') +
(q.q ? ('?' + q.q) : '') +
l.hash
);
}
}
}(window.location))

注意这段代码必须在SPA加载之前执行,就是放在打包出来的bundle.js之前

这段代码做了什么呢,第一是将url还原,即/diary/?p=/post/2015 还原为/diary/post/2015,然后使用history.replaceState()这个API添加到history对象中,不过注意这时候浏览器不会刷新页面,简单说把history想象成盒子,把url放到history盒子里去,但是不会加载这个url,只有等页面刷新,也就是SPA的js文件加载执行后,才会使用这个url,于是等SPA页面加载后,就会根据之前的url加载相应页面,也就解决了咱们的问题。

总结

总体来说整个hack的过程是: 提供404.html -> 加载脚本并请求index.html -> 还原url并且加载应用。