天天看点

前端路由与历史

在做全栈支持的过程中发现很多后端同学对hashHistory和browserHistory的区别不清楚不理解,也不知道如何正确地配置browserHistory,在本文中我们希望回答大家的这些疑惑:

  1. React应用url中的?_k=xxx 是什么?
  2. history是什么?为什么我们需要和它打交道?
  3. hashHistory和browserHistory有什么区别? 什么情况下使用hashHistory,什么情况下使用browserHistory?
  4. 如何在SOFA系统中配置browserHistory?

前言

刚学习React那会一直很疑惑为什么React应用的url后面非得有个奇怪的?_k=xxx,比方说现在linke的链接就是这样的:

https://linke.alipay.com/index.htm#/alipay?_k=9v1xhg      

而通常我们使用的链接都是类似语雀链接的形式,比较干净清爽:

https://lark.alipay.com/chenglin.mcl/forget/laub8f      

仔细看这个链接会发现链接中还有个#字符号,而井号前面是index.htm 似乎已经把网页加载完了?那#后面的是什么?_k字段又有什么意义?

一、React-router

这个_k字段是我们在使用前端路由库的时候产生的,#后面的内容叫做hash。在用React全家桶的时候我们会接触到​​React-router​​这个库,我这是React官方出品的前端路由解决方案,在使用的时候会让我们传入history对象到路由组件中:

import { browserHistory } from 'react-router'

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

这个history的取值通常有如下几种:

● browserHistory

● hashHistory

● createMemoryHistory

browserHistory是官方推荐的history,但需要进行一定的配置才可以使用,在开发阶段我们通常都是使用的第二种hashHistory,用了hashHistory就会发现链接中出现了#号,而且有奇怪的字段_k。而createMemoryHistory用的比较少这里不多描述。

所以history是什么?hash是什么意思?hashHistory为什么会有上面的效果?

二、路由

要讲history我们首先得聊聊路由。简单来说路由就是用来跟后端服务器进行交互的一种方式,通过不同的路径,来请求不同的资源,请求不同的页面是路由的其中一种功能。路由这个概念一开始是来自于后端,比方说访问www.example.com/test/index.html,服务器会根据路由配置返回相应的页面信息。

前端路由与历史

这仍然是目前大多数网站所采取的方式,比如在SOFA MVC当中的VM模板也是同样吐整个页面出来到浏览器,这样路由变化的时候整个页面都会刷新,导致资源的浪费,用户需要花费额外的时间等待页面刷新,造成体验不佳。

三、异步交互方式

而打破这个局面的是ajax的诞生,ajax允许页面局部刷新,能够快速地将渐步更新呈现在用户界面上,不需要重载整个页面,这使得程序能够更快地回应用户的操作。首先让Ajax技术发扬光大的是​​Google的Gmail​​,这种异步获取数据局部刷新的能力让Gmail获得很大的成功,网页应用获得了近似桌面应用的使用体验,这奠定了后面异步交互方式繁荣的基础,开发人员首次意识到我去还可以这样玩。

这种异步交互的集大成者就是SPA,在单页应用中,不仅页面交互不需要刷新页面,连页面跳转也不需要刷新页面,而实现页面跳转不需要刷新页面的技术方案就是前端路由。

前端路由与历史

前端路由的实现方式本质上是检测 url 的变化,截获 url 地址,然后解析匹配路由规则。那么有没有什么方式可以让我们改变url而不导致页面向后台发送请求呢?

一种解决方式是通过hash,另一种方式是利用浏览器自带的history对象。

四、Hash

那么什么是hash呢?hash是URL中的锚点,是​

​#​

​后面的部分,例如www.example.com/index.html#footer中这个#footer就是hash串,你可以在浏览器的开发者工具中通过window.location.hash获取页面url的hash值。

这个锚点的作用是在不请求服务器的情况下定位到页面的某个位置,如用id标识的位置,比如上面的footer可以让我们在访问页面时自动滚动到页面底部。而这个hash有个特性是在在第一个#后面出现的字符串都会被认为是位置字符,而不会被发送到服务器端,所以用改变#后面的值不会触发页面的重载,比如在浏览器控制台中输入location.hash=‘123’ 会让url带上#123但页面没有重新刷新。而且每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,这对ajax应用非常有用。

利用hash的特性可以在不触发页面刷新的情况下,解析当前hash的值和前端设定的路由进行匹配。比如#introduction,匹配上之后获取介绍页的内容挂载到预设的节点上。

在hashHistory的实现方式中_k存储的是一个hash的key,用于快速索引记录在内存中的history对象。

五、认识history对象

MDN: History 接口允许操作浏览器的曾经在标签页或者框架里访问的会话历史记录

在使用浏览器浏览网页的过程中,你很自然地想要回到上一页或者到下一页去。当我们在不同的地址之间跳转的时候就形成了我们的浏览历史,为了实现前进后退的功能,浏览器产商定义了history对象,最初的history对象实现了go,forward,back等api来在历史记录见进行跳转。比如你可以右击打开浏览器的console,输入history.back() 看是否浏览器的链接向后退了一步并刷新了页面。

在HTML5规范中,W3C对history对象进行了一波升级,新增了history.pushState()和history.replaceState()两个方法,利用这2个方法,可以实现通过脚本修改浏览器中的URL地址,而不触发页面重载。而实际上React router的browserHistory的命名就是类似于“使用浏览器(browser)的api实现的history”

六、react-router 中 hashHistory 和 browserHistory 模式区别

hashHistory: 不需要服务器配置,在 URL 生成一个 hash 来跟踪状态,通常在测试环境使用,也可以作为发布环境使用。如果设为hashHistory,路由将通过URL的hash部分(#)切换,URL的形式类似example.com/#/some/path。

browserHistory: 需要服务器端做配置,路径是真实的URL,是 react-router 官方推荐首选。如果设为browserHistory,浏览器的路由就不再通过Hash完成了,而显示正常的路径example.com/some/path,背后调用的是浏览器的History API。

大多数情况下,browserHistory 模式明显是优于 hashHistory 模式的,但 browserHistory模式 需要在服务器上进行一定的配置,否则用户直接向服务器请求某个子路由,会显示网页找不到的404错误。

七、browserHistory模式为什么需要配置服务器?

在browserHistory 模式下,URL 是指向真实 URL 的资源路径,当通过真实 URL 访问网站的时候(首页),这个时候可以正常加载我们的网站资源,而用户在非首页下手动刷新网页时,由于路径是指向服务器的真实路径,但该路径下并没有相关资源,用户访问的资源不存在,返回给用户的是 404 错误。

所以要让页面正常运行,我们需要让页面在访问这些url的时候返回单页应用的index.html,然后再从前端解析到正确的路由进行加载。

八、browserHistory模式如何配置服务器?

其本质是让所有期望能正常访问的 url 都返回 index.html,浏览器上的path,会自动被React-router处理,进行无刷新跳转
  1. 比如如果开发服务器使用的是webpack-dev-server,加上–history-api-fallback参数就可以了。
$ webpack-dev-server --inline --content-base . --history-api-fallback      
  1. 当服务器为nginx时,*.conf配置类似如下:
server {
  ...
  location / {
    try_files $uri /index.html
  }
}      
  1. 针对于其他服务器配置,原理都是一样的,即把路由都指向index.html,静态资源指向真实的文件夹。比方说在SOFA应用中,我们可以配置 Controller 使得请求能够被指向 构建出来的 HTML 页面:
// IndexController.java

@RequestMapping(value = {"/*"}, method = RequestMethod.GET)
public String index(ModelMap modelMap) {
  return "index.html";  
}      

总结