专栏首页云前端前端路由的原理及应用

前端路由的原理及应用

前端路由的起源

传统的web开发中,并没有前端路由这个概念。那么前端路由是如何出现的呢?

早期的路由都是后端来实现的,根据用户访问的地址的不同,浏览器从服务器请求对应的资源或页面展示给用户。当页面数据量大,结构复杂的时候,随之造成服务器的压力也比较大,而且用户访问速度也比较慢。

ajax,全称Asynchronous Javascript And XML,是浏览器实现异步加载的一种方案。ajax的出现,实现了局部刷新页面,极大地提升了用户交互体验,也为前端路由的出现奠定了一定的基础。

随着SPA单页面应用的发展,便出现了前端路由一说。单页面顾名思义就是一个网站只有一个html页面,但是点击不同的导航显示不同的内容,对应的url也会发生变化。也就是通过JS实时检测url的变化,从而改变显示的内容。SPA可以说是ajax的进阶版了。而SPA实现的核心,就是前端路由。

前端路由的实现原理

前端路由,简单粗暴的理解就是把不同路由对应不同的内容或者页面的任务交给前端来做。

前端路由主要有两种实现方式: - location.hash + hashchange事件 - H5 history API + popState事件

基于hash

hash即URL中"#"字符后面的部分。

  • 使用浏览器访问网页时,如果网址URL中带有hash,页面就会定位到id(或者name)与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);

h5 history API

这里是MDN文档:https://developer.mozilla.org/en-US/docs/Web/API/History

DOM window 对象通过 history 对象提供了对浏览器历史的访问。它暴露了很多有用的方法和属性,允许你在用户浏览历史中向前和向后跳转,同时——从HTML5开始——提供了对history栈中内容的操作方法。

在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 对象的几种方法来实现简单的跳转,比如backgoforward等等。然而,HTML新增加了 history.pushState()history.replaceState() 方法,这两个方法允许开发者在浏览历史中添加和修改记录。

history.pushState(state, title, url) //向浏览器历史栈中增加一条记录。
history.replaceState(state, title, url) //替换历史栈中的当前记录。

history.pushState()history.replaceState() 方法都需要三个参数:

  • state--状态对象state,是一个JavaScript对象;
  • title--标题,可以理解为document.title,可以传空字符串或者null;
  • url--该参数定义了新的历史URL记录。传入的url可以为绝对路径或相对路径,若为相对路径,那么它将被作为相对于当前URL处理。新URL必须与当前URL同源,否则 pushState() 会抛出一个异常。该参数是可选的,缺省为当前URL。

并且,这两个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>

popstate事件

页面不刷新已经办到了,那么如何追踪URL的变化,并根据URL的变化来呈现我们的页面呢?这时 popstate 就要登场了。

window.onpopstatepopstate 事件在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,我们可以总结出:通过 pushStatereplaceState 这两个 API 可以改变 url 地址且不会发送请求,浏览器的历史记录条目的变化还会触发 'popstate' 事件。结合这些就能用另一种方式来实现前端路由了,但原理跟用 hash 实现大同小异。不过用了 history API 的实现,单页路由的 url 就不会多出一个#,变得更加美观。

前端路由的应用——react-router

了解到上面提到的两种方式之后,再结合目前前端路由的实际应用,像 react-routervue-routerui.router 这些与前端框架配合使用的路由库,也都是基于hash和history API的原理实现的,下面主要来讲一讲 react-router

history.js

要想了解react-router,那么应该先了解history 。因为 historyReact 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(path, [state]) push方法能够使用户跳转到新的location。默认情况下,点击时,会调用history.push方法 history.push({ pathname: '/new-place' })
  • history.replace(path, [state]) replace方法与push相似,但它并非添加location,而是替换当前索引上的位置。重定向时要使用replace。这也是React Router的组件中使用的方法。 history.replace({ pathname: '/go-here-instead' })
  • history.goBack() 返回上一层页面。实际上是将history的索引值减1
  • history.goForward() goForward与goBack相对,前进一层页面
  • history.go(n) go是一个强大的方法,并包含了goForward与goBack的功能。传入负数则退后,传入正数则向前。

改变location当然需要监听事件:

  • history.listen()history.listen((location, action) => { console.log(`The current URL is
    • history.createHref(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

本文分享自微信公众号 - 云前端(fewelife),作者:小米

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2018-02-02

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • [译] 无容器下的云计算

    Cloudflare 有一个云计算平台称为 Workers。不像据我所知道的其它云计算平台所必须的那样,它无需容器或虚拟机。我们相信这将是无服务器和云计算的未来...

    江米小枣
  • [译] 代码审查之最佳实践

    原文:https://medium.com/palantir/code-review-best-practices-19e02780015f 作者:Robert...

    江米小枣
  • [译] 不用祖传秘方 - 写好代码的几个小技巧

    原文:【The Non-Secret Formula for Writing Better Code】https://hackernoon.com/the-no...

    江米小枣
  • BOM相关知识

    history对象 history对象保存着用户上网的历史记录,从窗口被打开那一刻起。属于window对象的属性。 go()方法可以在用户的历史记录中任意跳转...

    wangxl
  • javascript入门笔记8-window对象

    History 对象 history对象记录了用户曾经浏览过的页面(URL),并可以实现浏览器前进与后退相似导航的功能。 注意:从窗口被打开的那一刻开始记录,...

    方志朋
  • 1.初识backbone.js

    backbone,英文意思是:勇气, 脊骨,但是在程序里面,尤其是在backbone后面加上后缀js之后,它就变成了一个框架,一个js库。

    the5fire
  • ASP.NET MVC5高级编程 ——(6)过滤器

    1、过滤器(Filters)就是向请求处理管道中注入额外的逻辑。提供了一个简单而优雅的方式来实现横切关注点。

    浩Coding
  • 行业内参 | 5个行业,120条新闻,一文全面了解过去1周大事件

    苏宁金融「5+1」核心业务策略正式成型。「5」指的是聚焦供应链金融、消费金融、微商金融、支付和产品销售五大核心业务,业务将加快平台化发展;而「1」指的是输出金融...

    机器之心
  • 【译】感谢你的Code Review

    这意味着我需要发出大量的代码审查。在一次修改中通常会涉及到从UI到数据库的所有部分。

    Jackeyzhe
  • python学习推荐:anaconda

    前言 Python因轻简易用,并且擅长计算数据,渐渐走入了生物信息的大圈子,但用好却不易学,其中比较头疼的就是包管理和Python不同版本的问题,特别是当你使用...

    企鹅号小编

扫码关注云+社区

领取腾讯云代金券