前端路由的起源
传统的web开发中,并没有前端路由这个概念。那么前端路由是如何出现的呢?
早期的路由都是后端来实现的,根据用户访问的地址的不同,浏览器从服务器请求对应的资源或页面展示给用户。当页面数据量大,结构复杂的时候,随之造成服务器的压力也比较大,而且用户访问速度也比较慢。
ajax
,全称Asynchronous Javascript And XML
,是浏览器实现异步加载的一种方案。ajax
的出现,实现了局部刷新页面,极大地提升了用户交互体验,也为前端路由的出现奠定了一定的基础。
随着SPA
单页面应用的发展,便出现了前端路由一说。单页面顾名思义就是一个网站只有一个html页面,但是点击不同的导航显示不同的内容,对应的url也会发生变化。也就是通过JS实时检测url的变化,从而改变显示的内容。SPA
可以说是ajax
的进阶版了。而SPA实现的核心,就是前端路由。
前端路由,简单粗暴的理解就是把不同路由对应不同的内容或者页面的任务交给前端来做。
前端路由主要有两种实现方式: - location.hash + hashchange事件 - H5 history API + popState事件
hash
即URL中"#"字符后面的部分。
hash
值一样的元素的位置;hash
还有一个另一个特点,hash
的改变不会使页面重新加载;hash
值随请求发送到服务器端;window.loaction.hash
属性可以设置和获取hash
值。我们用window.location
处理hash
的改变不会重新加载页面,而是当做新页面,放入历史栈中。并且,当页面发生跳转触发hashchange
事件时,我们可以在对应的事件处理函数中注册ajax等操作从而改变页面内容。那么如何改变hash
呢?主要有两种方法:
1.设置a标签的href属性为一个hash值,当点击a标签时会在当前的url后面增加上hash值,同时触发'hashchange'事件;2.直接在js中对location.hash进行更改,此时url改变,并触发hashchange事件。
下面是通过改变hash来模拟前端路由的一个demo:
function Router(){
this.routes={};
this.currentURL='';
}
Router.prototype.route = function(path,callback){
this.routes[path] = callback || function(){};
}
Router.prototype.refresh = function(){
this.currentURL = location.hash.slice(1) || '/index';
this.routes[this.currentURL]();
}
Router.prototype.init = function () {
window.addEventListener('load',this.refresh.bind(this),false);
window.addEventListener('hashchange',this.refresh.bind(this),false);
}
window.Router = new Router();
window.Router.init();
上面的代码中,我们定义了一个Router对象,对象的属性routes是一个路由映射对象,curreURL表示当前的URL,route表示为对应的url指定的视图函数,refresh函数为刷新页面的函数。我们给window绑定监听事件,监听hashchange事件,当url中的hash值改变时,刷新页面展示对应的内容。
当我们点击a标签时,window监听到url的hash改变,触发refresh方法,根据获取到的currentURl,执行routes对象中对应的route视图函数:
<div id="index-page" class="content">
<ul>
<li><a href="#/index">index</a></li>
<li><a href="#/news">news</a></li>
<li><a href="#/about">about</a></li>
</ul>
</div><div id="news-page" class="content">
<h1>this is new page</h1>
<a href="#/index">back</a>
</div><div id="about-page" class="content">
<h1>this is about page</h1>
<a href="#/index">back</a>
</div>
function display_page(id){
$(".content").eq(id).show().siblings().hide();
}
Router.route('/index',function(){
display_page(0);
})
Router.route('/news',function(){
display_page(1);
})
Router.route('/about',function(){
display_page(2);
})
运行效果如图:
但是在低版本浏览器中并不兼容hashchange
事件,需要通过轮询监听url的变化,来检测hash的变化,下面是一段魔力的代码:
(function(window) {
// 如果浏览器不支持原生实现的事件,则开始模拟,否则退出。
if ( "onhashchange" in window.document.body ) { return; }
var location = window.location,
oldURL = location.href,
oldHash = location.hash;
// 每隔100ms检查hash是否发生变化
setInterval(function() {
var newURL = location.href,
newHash = location.hash;
// hash发生变化且全局注册有onhashchange方法(这个名字是为了和模拟的事件名保持统一);
if ( newHash != oldHash && typeof window.onhashchange === "function" ) {
// 执行方法
window.onhashchange({
type: "hashchange",
oldURL: oldURL,
newURL: newURL
});
oldURL = newURL;
oldHash = newHash;
}
}, 100);
})(window);
这里是MDN文档:https://developer.mozilla.org/en-US/docs/Web/API/History
DOM window 对象通过 history 对象提供了对浏览器历史的访问。它暴露了很多有用的方法和属性,允许你在用户浏览历史中向前和向后跳转,同时——从HTML5开始——提供了对history
栈中内容的操作方法。
// 在history中向后跳转,与用户点击浏览器的回退按钮效果相同
window.history.back();
// 在history中向前跳转,与用户点击浏览器的前进按钮效果相同
window.history.forward();
// 跳转到history中指定的一个点
windiw.history.go();
用go()
方法载入到会话历史中的某一个特定页面,通过与当前页面相对位置来标记(当前页面的相对位置为0)。
// 向前移动一个页面
window.history.go(-1);
// 向后移动一个页面
window.history.go(1);
由此,向go()
传递数值,浏览器页面就会向前(负数)或向后(正数)移动相应数值的页面。
pushState()
和replaceState()
在html5之前,浏览器的历史记录是不能被操作的,开发者只能调用 history
对象的几种方法来实现简单的跳转,比如back
、go
、forward
等等。然而,HTML新增加了 history.pushState()
和 history.replaceState()
方法,这两个方法允许开发者在浏览历史中添加和修改记录。
history.pushState(state, title, url) //向浏览器历史栈中增加一条记录。
history.replaceState(state, title, url) //替换历史栈中的当前记录。
history.pushState()
和 history.replaceState()
方法都需要三个参数:
并且,这两个API都会操作浏览器的历史栈,而不会引起页面的刷新。
不同的是,pushState 将指定的url直接压入历史记录栈顶,而 replaceState 则是将当前历史记录栈换成传入的数据。
来看一个Mozilla应用pushState和replaceState的demo:
<!DOCTYPE HTML>
<!-- this starts off as http://example.com/line?x=5 -->
<title>Line Game - 5</title>
<p>You are at coordinate <span id="coord">5</span> on the line.</p>
<p>
<a href="?x=6" onclick="go(1); return false;">Advance to 6</a> or
<a href="?x=4" onclick="go(-1); return false;">retreat to 4</a>?
</p>
<script>
var currentPage = 5; // prefilled by server!!!!
function go(d) {
setupPage(currentPage + d);
history.pushState(currentPage, document.title, '?x=' + currentPage);
}
onpopstate = function(event) {
setupPage(event.state);
}
function setupPage(page) {
currentPage = page;
document.title = 'Line Game - ' + currentPage;
document.getElementById('coord').textContent = currentPage;
document.links[0].href = '?x=' + (currentPage+1);
document.links[0].textContent = 'Advance to ' + (currentPage+1);
document.links[1].href = '?x=' + (currentPage-1);
document.links[1].textContent = 'retreat to ' + (currentPage-1);
}
</script>
页面不刷新已经办到了,那么如何追踪URL的变化,并根据URL的变化来呈现我们的页面呢?这时 popstate
就要登场了。
window.onpopstate
是 popstate
事件在window对象上的事件处理程序.
每当处于激活状态的历史记录条目发生变化时,popstate
事件就会在对应window对象上触发。但是调用history.pushState()
或者 history.replaceState()
不会触发 popstate
事件。 popstate
事件只会在浏览器某些行为下触发,比如点击后退、前进按钮(或者在JavaScript中调用history.back()
、history.forward()
、history.go()
方法)。
当网页加载时,各浏览器对popstate事件是否触发有不同的表现,Chrome 和 Safari会触发popstate事件, 而Firefox不会.
再来看一下mozilla官方的一个小demo:
假如当前网页地址为http://example.com/example.html,则运行下述代码后:
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}
看了上面的demo,我们可以总结出:通过 pushState
和 replaceState
这两个 API 可以改变 url 地址且不会发送请求,浏览器的历史记录条目的变化还会触发 'popstate' 事件。结合这些就能用另一种方式来实现前端路由了,但原理跟用 hash
实现大同小异。不过用了 history API
的实现,单页路由的 url 就不会多出一个#,变得更加美观。
了解到上面提到的两种方式之后,再结合目前前端路由的实际应用,像 react-router
, vue-router
,ui.router
这些与前端框架配合使用的路由库,也都是基于hash和history API的原理实现的,下面主要来讲一讲 react-router
。
要想了解react-router
,那么应该先了解history
。因为 history
为 React Router
提供了其核心功能。
history
是一个独立的第三方js库(https://github.com/ReactTraining/history) ,根据不同的浏览器和环境,history提供了以下三种方式来创建history对象:
createBrowserHistory
:是React Router推荐使用的history。它使用浏览器中的 History API 处理 URL,创建一个像example.com/some/path这样真实的 URL createHashHistory
:使用 URL 中的 hash(#)部分去创建形如 example.com/#/some/path 的路由,支持大部分的浏览器包括IE8+createMemoryHistory
:不会在地址栏被操作或读取。这就解释了react-router是如何实现服务器渲染的。同时它也非常适合测试和其他的渲染环境(像 React Native )。基本用法如下:
import createHistory from 'history/createBrowserHistory'const history = createHistory()// 获取当前的location.
const location = history.location// 监听当前 location的变化
const unlisten = history.listen((location, action) => {
console.log(action, location.pathname, location.state)
})history.push('/home', { some: 'state' })unlisten()
这些History对象有一些共同的属性:
history.length
—— 历史堆栈中的条目数history.loaction
—— 当前位置history.action
—— 当前的导航操作也可以使用 history
对象的方法来改变当前的location
:
history.push({ pathname: '/new-place' })
history.replace({ pathname: '/go-here-instead' })
改变location当然需要监听事件:
值得注意的是,history.location对象实现了window.location对象的一些方法,但是跟原生location不同的是多了key属性。每一个location都拥有一个与之关联且独一无二的key,'key'用于特定location的识别,向特定location存储数据。以下是location的属性:
location.pathname
—— url的基本路径location.search
—— 查询字段location.hash
—— url中的hash值location.state
—— url对应的state字段location.key
—— 生成的方法:Math.random().toString(36).substr(2,length)location.action
—— 分为push、replace、pop三种{
pathname: '/here',
search: '?key=value',
hash: '#extra-information',
state: { modal: true },
key: 'abc123'
}
以上就是history的基础API,虽然使用React Router,它会为你自动创建history对象,所以你并不需要与history进行直接的交互,但是了解history对我们理解react-router会非常有帮助。这里我就不介绍react-router的使用方法了,可以去这里看看:https://github.com/reactjs/react-router ,也可以阅读下源码,深入理解react-router是如何结合history对象,实现点击'link'跳转页面并更新视图的。
下面来个例子,看一下 react-router
的使用:
import React from "react";
import {render} from "react-dom";
import {Router, Route, Link } from 'react-router-dom';
import createBrowserHistory from 'history/createBrowserHistory';const Home = () => (
<h2>Home</h2>
)
const About = () => (
<h2>About</h2>
)const Topic = ({ topicId }) => (
<h3>{topicId}</h3>
)const Topics = ({ match }) => {
const items = [
{ name: 'Rendering with React', slug: 'rendering' },
{ name: 'Components', slug: 'components' },
{ name: 'Props v. State', slug: 'props-v-state' },
] return (
<div>
<h2>Topics</h2>
<ul>
{items.map(({ name, slug }) => (
<li key={name}>
<Link to={`${match.url}/${slug}`}>{name}</Link>
</li>
))}
</ul>
{items.map(({ name, slug }) => (
<Route key={name} path={`${match.path}/${slug}`} render={() => (
<Topic topicId={name} />
)} />
))}
<Route exact path={match.url} render={() => (
<h3>Please select a topic.</h3>
)}/>
</div>
)
}
const history = createBrowserHistory();
const App = () => (
<Router history={history}>
<div>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/about">About</Link></li>
<li><Link to="/topics">Topics</Link></li>
</ul> <hr/>
<Route exact path="/" component={Home}/>
<Route path="/about" component={About}/>
<Route path="/topics" component={Topics} />
</div>
</Router>
)render(
<App/>,
document.querySelector("#root")
);
至此,前端路由的实现原理已经讲的差不多了,不知道大家有没有理解这两种方式呢?下面来总结一下:
hash
方式:js通过hashChange事件来监听url的改变,浏览器兼容性较好,但是IE7及以下需要使用轮询方式;history API
:url看起来像普通网站那样,以"/"分割,没有#,但页面并没有跳转,不过使用这种模式需要服务端支持,服务端在接收到所有的请求后,都指向同一个html文件,不然会出现404。https://segmentfault.com/a/1190000007238999 http://blog.csdn.net/xllily_11/article/details/51820909 https://www.cnblogs.com/wozien/p/6597306.html https://segmentfault.com/a/1190000010251949 https://zhuanlan.zhihu.com/p/20799258?refer=jscss https://segmentfault.com/a/1190000005160459 https://segmentfault.com/a/1190000004527878