Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

浅谈前端路由 #137

Open
lzf0402 opened this issue Oct 24, 2017 · 1 comment
Open

浅谈前端路由 #137

lzf0402 opened this issue Oct 24, 2017 · 1 comment

Comments

@lzf0402
Copy link

lzf0402 commented Oct 24, 2017


title: 浅谈前端路由
date: 2017-10-24

传统的web开发模式里,路由都是由服务端控制并管理,根据不同的url映射不同的页面。而Ajax的出现,使得前端路由成为可能。本文整理了前端路由的实现方式,以及目前主流框架里配套的路由体系,旨在对前端路由有一个清晰的认识。

什么是前端路由

对于用户来说,路由就是浏览器地址栏中的url与所见网页的对应关系。而对于web开发人员来说,路由更像是url与处理函数的对应关系。

传统的服务端路由,根据客户端请求的不同网址,返回不同的网页内容,这种情况一是会造成服务器压力增加,二是每次都重新请求,响应较慢、用户体验下降。于是,单页应用(spa,single page application)应运而生。在url地址改变的过程中,通过js来实现不同UI之间的切换(js对DOM的操作),而不再向服务器重新请求页面,只通过ajax向服务端请求数据,对用户来说这种无刷新的、即时响应是更好的体验。其中,根据url地址栏的变化而展示不同的UI,就是通过前端路由来实现的。目前主流的支持单页应用的前端框架,基本都有配套的或第三方的路由系统。

前端路由的实现方式

在HTML5的 history API出现之前,前端路由主要是通过 hash 来实现的,hash能兼容低版本的浏览器。下面分别来介绍这2种方式。

  • 方法一:基于hash(location.hash+hashchange事件)

我们知道location.hash的值是url中#后面的内容,如http://www.163.com#netease此网址中,location.hash='#netease'。hash满足以下几个特性,才使得其可以实现前端路由:

  1. url中hash值的变化并不会重新加载页面,因为hash是用来指导浏览器行为的,对服务端是无用的,所以不会包括在http请求中。
  2. hash值的改变,都会在浏览器的访问历史中增加一个记录,也就是能通过浏览器的回退、前进按钮控制hash的切换
  3. 我们可以通过hashchange事件,监听到hash值的变化,从而响应不同路径的逻辑处理。
window.addEventListener("hashchange", funcRef, false)

如此一来,我们就可以在hashchange事件里,根据hash值来更新对应的视图,但不会去重新请求页面,同时呢,也在history里增加了一条访问记录,用户也仍然可以通过前进后退键实现UI的切换。

触发hash值的变化有2种方法:

一种是通过a标签,设置href属性,当标签点击之后,地址栏会改变,同时会触发hashchange事件

<a href="#kaola">to KAOLA</a>

另一种是通过js直接赋值给location.hash,也会改变url,触发hashchange事件。

location.hash="#kaola"

下面展示一个通过hash实现的简易Router:

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) || '/';
    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.route('/', function() {
    // 设置响应内容
});
Router.route('/blue', function() {
    // 设置响应内容
});

【route vs routes】

route是一条路由,是将一个URL路径和一个处理函数相关联,是一条url和函数的映射规则,如上面代码中通过原型上的route可以设置一条路由规则,将一个path和其callback关联起来。

而router则更像是一个容器,或者说一种机制,它管理了一组route。简单来说,route只是进行了URL和函数的映射,而在当接收到一个URL之后,去路由映射表中查找相应的函数,这个过程是由router来处理的,如上面代码,Router管理传入的route,并且在hash改变的时候,根据当前的url响应其对应的函数。

  • 方法二:基于History新API(history.pushState()+popState事件)

HTML5中history对象上新的API,同样能实现前端的路由。通过pushState()方法或replaceState()方法可以修改url的地址,并在popstate事件中能监听地址的改变,不同的是,手动的进行pushState()并不会触发popstate事件。

先认识下两个新增的API:history.pushState和 history.replaceState,这两个API都接收三个参数:

window.history.pushState(null, null, "http://www.163.com");
  • 状态对象(state object),一个JavaScript对象,与用pushState()方法创建的新历史记录条目关联。无论何时用户导航到新创建的状态,会触发popstate事件,并能在事件中使用该对象。
  • 标题(title) :传入一个短标题给当前state。现在大多数浏览器不支持或者会忽略此参数,最好传入null代替;
  • 地址(URL):新的历史记录条目的地址。浏览器不会在调用pushState()方法后加载该地址,但之后,可能会试图加载,例如用户重启浏览器。新的URL不一定是绝对路径;如果是相对路径,它将以当前URL为基准;传入的URL与当前URL应该是同源的,否则,pushState()会抛出异常。该参数是可选的;不指定的话则为文档当前URL。

这两个API的相同之处是都会操作浏览器的历史记录,而不会引起页面的刷新。不同之处在于,pushState会增加一条新的历史记录,而replaceState则会替换当前的历史记录。这两个api,加上state改变触发的popstate事件,提供了单页应该的另一种路由方式。

下面展示一个使用这种方式的简单demo:

<p id="menu">
 <a href="/profile" title="profile">profile</a>
 <a href="/account" title="account">account</a>?
</p>
<div class="main" id="main"></div>
<script>
;(function(){
    var menubox = document.getElementById('menu');
    var mainbox = document.getElementById('main');
    
    menubox.addEventListener('click',function(e){
        e.preventDefault();
        var elm = e.target;
        var uri = elm.href;
        var tlt = elm.title;
        history.pushState({path:uri,title:tlt},null,uri);
        mainbox.innerHTML = 'current page is '+tlt;
    })
    window.addEventListener('popstate',function(e){
        var state = e.state;
        console.log(state);
        mainbox.innerHTML = 'current page is '+state.title; // 还原UI
    })
})()
</script>

当我们在历史记录中切换时就会触发 popstate 事件,可以在事件中还原当前state对应的UI。对于触发popstate 事件的方式,各浏览器实现也有差异,我们可以根据不同浏览器做兼容处理。

两种方式对比,基于Hash的路由,兼容性更好;基于History API的路由,则更正式,可以设置与当前URL同源的任意URL,路径更直观。另外,基于Hash的路由不需要对服务器做改动,基于History API的路由需要对服务器做一些改造,配置不同的路由都返回相同的页面。

vue-router

vue-router是Vue.js框架的路由插件,支持以上2中方式的路由设置。
vue-router基本的路由配置如下:

import Router from 'vue-router'
import Index from 'pages/index'
import Error from 'pages/error'

Vue.use(Router)

export default new Router({
 mode: 'history', // 设置路由方式
 routes: [
   {
     path: '/',
     name: 'index',
     component: Index
   },
   {
     path: '*',
     name: '404',
     component: Error
   }
 ]
})

// 在组件中使用router视图组件
<router-view></router-view>

可以通过vue-router实例来配置路由规则列表,指定路径path与组件component的对应关系。可以通过mode这一参数控制路由的实现模式,默认值是hash,基于hash的实现方式,如果显示设置为history,则会设为基于history API的实现方式,如果浏览器不支持,可以设置fallback来控制是否需要回滚为'hash'模式。另外,如果是非浏览器端运行(如nodejs中),会将mode强制设为'abstract'模式。
vue-router支持路由嵌套、动态路由的配置、重定向及别名等,可参看官方文档

react-router

react-router 是基于 history 模块提供的 api 进行开发的。其路由配置是通过react component方式进行的,如下:

render((
  <Router history={browserHistory}>
    <Route path="/" component={App}>
      <Route path="about" component={About}/>
      <Route path="users" component={Users}>
        <Route path="/user/:userId" component={User}/>
      </Route>
      <Route path="*" component={NoMatch}/>
    </Route>
  </Router>
), document.body)

// link 触发路由
<Link to={`/user/89757`}>'joey'</Link>

以上声明了一份含有 path to component 的各个映射的路由表。Router 在react 组件生命周期的钩子 componentWillMount (组件被挂载前)中,使用 this.history.listen 去注册了 url 更新的回调函数。回调函数将在 url 更新时触发,回调中的 setState 会去 render 新的 component。

router in NEJ

NEJ也是支持单页应用的前端框架,其模块调度系统可以用于支持单页富应用的系统架构、模块拆分和重组、模块调度管理 等功能。如下所示,通过dispatcher来配置模块与路由的映射关系,实现单页应用。

NEJ.define([
    'util/dispatcher/dispatcher'
],function(_e,_p){
    // 取调度器实例
    var dispatcher = _p._$$Dispatcher._$getInstance();
    
    // start up dispatcher
    dispatch._$startup({
        rules: {
            rewrite: {
                404: '/m/overview/'
            },
            title: {
                '/m/overview/': '概览'
            },
            alias: {
                tab: '/?/tab/',
                layout: '/m',
                overview: '/m/overview/'
            }
        },
        modules: {
            '/?/tab/': 'module/tab/index.html',// 私有模块
            '/m/overview/': 'module/overview/index.html',
            '/m': {
                module: 'module/layout/index.html',
                composite: {
                    tab: '/?/tab/'
                }
            }
        },
        onbeforechange: function(options) {
            var umi = options.path || '';
            if (!!umi && umi.indexOf('/?') < 0 && umi.indexOf('/m') < 0) {
                options.path = '/m' + umi;
            }
        }
    });
})

其定义的模块是指从系统中拆分出来的可与用户进行交互完成一部分完整功能的独立单元,包含了模块的样式、结构和功能逻辑。为标识模块,采用了一套UMI(Uniform Module Identifier)统一模块标识,如/m/m0,每个UMI均可唯一标识一个模块及模块在系统中的依赖关系。并将模块分成两种类型:提供容器的模块和使用容器的模块,实现了模块之间的任意组合。其路由配置是path与模块的映射关系,底层也是基于hash实现的。

最后

随着浏览器性能的日益提升,在越来越重视用户体验、注重快速响应的今天,单页应用也越来越多。无论是借助流行前端框架的router插件,或是自己实现,对前端路由及其实现机制有个全面的了解,还是很有必要的。

参考文献


add by lzf

@fengzilong
Copy link
Member

@kaola-blog-bot 更新下

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants