前端路由&vue-router部分解读

前端路由

由于SPA单页应用以及前后端分离的思想的出现,前端路由在当今的前端开发中是必不可少的。一些流行框架vue、react都有对应的实现vue-router、react-router等。

框架路由的实现都是对原生路由进行了封装。原生路由我们通常采用hash实现或者H5的history实现。

hash路由

hash路由:一个明显的标志就是带有#,我们主要是通过监听url中的hash变化来进行路由跳转。
hash的优势就是具备好的兼容性,唯一的不好就是url中会一直存在#不够美观,看起来更像是hack实现。

下面基于hash实现一个简单的路由。(采用ES6的语法)

class Routers {
    constructor() {
        // 以键值对形式储存路由
        this.routes = {};
        // 当前路由的URL
        this.currentUrl = '';
        // 记录出现过的hash
        this.history = [];
        // 作为指针,默认指向this.history的末尾,根据后退前进指向history中不同的hash
        this.currentIndex = this.history.length - 1;
        this.refresh = this.refresh.bind(this);
        this.backOff = this.backOff.bind(this); 

        // 默认不是后退操作
        this.isBack = false;
        window.addEventListener('load', this.refresh, false);
        window.addEventListener('hashchange', this.refresh, false);
    }
    // 将path路径与对应的callback函数存储
    route(path, callback) {
        this.routes[path] = callback || function() {};
    }
    // 刷新
    refresh() {
        // 获取当前URL中的hash路径
        this.currentUrl = location.hash.slice(1) || '/';
        if(!this.isBack) {
            if (this.currentIndex < this.history.length - 1) {
                this.history = this.history.slice(0, this.currentIndex + 1);
            }
            this.history.push(this.currentUrl);
            this.currentIndex++;
        }
        // 执行当前hash路径的callback函数
        this.routes[this.currentUrl]();
        this.isBack = false;
    }
    // 后退
    backOff() {
        // 后退操作设置为true
        this.isBack = true;
        // 如果指针小于0的话就不存在对应的hash路由
        this.currentIndex <= 0 ? (this.currentIndex = 0) : (this.currentIndex = this.currentIndex - 1);
        // 随着后退,location.hash页应该随之变化
        location.hash = `#${this.history[this.currentIndex]}`;
        // 执行指针目前指向hash路由对应的callback
        this.routes[this.history[this.currentIndex]]();
    }
}
    // 调用
    Router.route('/',function() {

    });

HTML5 history API路由

部分API介绍

window.history.back(); // 后退
window.history.forward(); // 前进
window.history.go(-3); // 后退3个页面


// history.pushState用于在浏览历史中添加历史记录,但是不触发跳转

// 三个参数

/*
state: 一个与指定网址相关的状态对象,popstate事件触发时,该对象会传入回调函数。如果不需要这个对象,此处可以填null。

title: 新页面的标题,但是所有浏览器目前都忽略这个值,因此可以填null。

url:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。
*/


// history.replaceState方法的参数与pushState方法一模一样,区别是它修改浏览历史中当前记录,而非添加记录,同样不触发跳转。

/*
popstate事件,每当同一个文档的浏览历史(即history对象)出现变化时,就会触发popstate事件。仅仅调用pushState和replaceState方法,并不会触发该事件,只有用户点击浏览器倒退按钮和前进按钮,或者使用JavaScript调用back、forward、go方法时才会触发。另外该事件只针对同一个文档,如果浏览历史切换导致加载不同的文档,该事件也不会触发。
*/

下面基于新的标准实现一个简单路由

    class Routers {
        constructor() {
            this.routes = {};
            // 在初始化时监听popstate事件
            this._bindPopState();
        }

        // 初始化路由
        init(path) {
            history.replaceState({path: path}, null, path);
            this.routes[path] && this.routes[path]();
        }
        // 将路径和对应的回调函数加入hashMap存储
        route(path, callback) {
            this.routes[path] = callback || function() {};
        }
        // 触发路由对应回调
        go(path) {
            history.pushState({path:path}, null, path);
            this.routes[path] && this.routes[path]();
        }
        // 监听popstate事件
        _bindPopState() {
            window.addEventListener('popstate', e => {
                const path = e.state && e.state.path;
                this.routes[path] && this.routes[path]();
            });
        }
    }

vue-router部分实现


    // 创建路由
    const routes = [
        {// 首页
            path: '/',
            component: () => import('src/pages/home/index.vue')
        },
        {// 首页更多功能
            path: '/functionManage',
            component: () => import('src/pages/home/functionManage.vue')
        }
    ]
    // router路由管理
    const router = new VueRouter({
        routes,
    })

    // router实例注入根实例
    window.vm = new Vue({
        router
    })

通过mode参数控制路由的实现模式。

    const router = new VueRouter({
    // HTML5 history 模式
    mode: 'history',
    base: process.env.NODE_ENV === 'production' ? process.env.PROXY_PATH : '',
    routes,
})

在入口文件中需要实例化一个 VueRouter 的实例对象 ,然后将其传入 Vue 实例的 options 中。

    var VueRouter = function VueRouter(options) {
        // match匹配函数
        this.matcher = createMatcher(options.routes || [], this);
        // 根据mode实例化具体的History,默认为'hash'模式
        var mode = options.mode || 'hash';
        // 通过supportsPushState判断浏览器是否支持'history'模式
        // 如果设置的是'history'但是如果浏览器不支持的话,'history'模式会退回到'hash'模式
        // fallback 是当浏览器不支持 history.pushState 控制路由是否应该回退到 hash 模式。默认值为 true。
        this.fallback = mode === 'history' && !supportsPushState &&   options.fallback !== false;
        if (this.fallback) {
            mode = 'hash';
        }
        this.mode = mode;
        // 根据不同模式选择实例化对应的History类
        switch (mode) {
            case 'history':
                this.history = new HTML5History(this, options.base);
                break;
            case 'hash': 
                this.history = new HashHistory(this, options.base, this.fallback);
                break;
        }
    }


    //作为参数传入的字符串属性mode只是一个标记,用来指示实际起作用的对象属性history的实现类,两者对应关系:
    modehistory: 
        'history': HTML5History;
        'hash': HashHistory;
        'abstract': AbstractHistory;

在初始化对应的history之前,会对mode做一些校验:若浏览器不支持HTML5History方式(通过supportsPushState变量判断),则mode设为hash;若不是在浏览器环境下运行,则mode设为abstract;
VueRouter类中的onReady(),push()等方法只是一个代理,实际是调用的具体history对象的对应方法,在init()方法中初始化时,也是根据history对象具体的类别执行不同操作

HashHistory

1、hash虽然出现在url中,但不会被包括在http请求中,它是用来指导浏览器动作的,对服务器没有影响,改变hash不会重新加载页面。
2、监听hash
window.addEventListener(‘hashchagne’, function, false)
3、每一次改变hash(window.location.hash),都会在浏览器访问历史中增加一个记录。
Vue作为渐进式的前端框架,本身的组件定义中应该是没有有关路由内置属性_route,如果组件中要有这个属性,应该是在插件加载的地方,即VueRouter的install()方法中混入Vue对象的,install.js的源码:

function install (Vue) {

  ...

  Vue.mixin({
    beforeCreate: function beforeCreate () {
      if (isDef(this.$options.router)) {
        this._routerRoot = this;
        this._router = this.$options.router;
        this._router.init(this);
        Vue.util.defineReactive(this, '_route', this._router.history.current);
      } else {
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
      }
      registerInstance(this, this);
    },
    destroyed: function destroyed () {
      registerInstance(this);
    }
  });
}

通过Vue.mixin()方法,全局注册一个混合,影响注册之后所有创建的每个Vue实例,该混合在beforeCreate钩子中通过Vue.util.defineReactive()定义了响应式的_route属性。所谓响应式属性,即当_route值改变时,会自动调用Vue实例的render()方法,更新视图。

$router.push()–>HashHistory.push()–>History.transitionTo()–>History.updateRoute()–>{app._route=route}–>vm.render()

HashHistory.push()方法:将路由添加到浏览器访问历史栈顶(直接对window.location.hash赋值)
HashHistory.replace()方法:替换掉当前路由(调用window.location.replace)

监听地址栏:
上面的VueRouter.push()和VueRouter.replace()是可以在vue组件的逻辑代码中直接调用的,除此之外在浏览器中,用户还可以直接在浏览器地址栏中输入改变路由,因此还需要监听浏览器地址栏中路由的变化 ,并具有与通过代码调用相同的响应行为,在HashHistory中这一功能通过setupListeners监听hashchange实现:相当于直接调用了replace方法。

HTML5History

History interface是浏览器历史记录栈提供的接口,通过back(),forward(),go()等方法,我们可以读取浏览器历史记录栈的信息,进行各种跳转操作。
HTML5引入了history.pushState()和history.replaceState()方法,他们分别可以添加和修改历史记录条目。这些方法通常与window.onpopstate配合使用。
window.history.pushState(stateObject,title,url)
window.history,replaceState(stateObject,title,url)
复制代码
stateObject:当浏览器跳转到新的状态时,将触发popState事件,该事件将携带这个stateObject参数的副本
title:所添加记录的标题
url:所添加记录的url(可选的)

pushState和replaceState两种方法的共同特点:当调用他们修改浏览器历史栈后,虽然当前url改变了,但浏览器不会立即发送请求该url,这就为单页应用前端路由,更新视图但不重新请求页面提供了基础。

export function pushState (url?: string, replace?: boolean) {
    saveScrollPosition()
    // 加了 try...catch 是因为 Safari 有调用 pushState 100 次限制
    // 一旦达到就会抛出 DOM Exception 18 错误
    const history = window.history
    try {
        if (replace) {
            // replace 的话 key 还是当前的 key 没必要生成新的
            history.replaceState({ key: _key }, '', url)
        } else {
            // 重新生成 key
            _key = genKey()
            // 带入新的 key 值
            history.pushState({ key: _key }, '', url)
        }
    } catch (e) {
        // 达到限制了 则重新指定新的地址
        window.location[replace ? 'replace' : 'assign'](url)
    }
}

// 直接调用 pushState 传入 replace 为 true
export function replaceState (url?: string) {
    pushState(url, true)
}

复制代码代码结构以及更新视图的逻辑与hash模式基本类似,只不过将对window.location.hash()直接进行赋值window.location.replace()改为了调用history.pushState()和history.replaceState()方法。
在HTML5History中添加对修改浏览器地址栏URL的监听popstate是直接在构造函数中执行的:

constructor (router: Router, base: ?string) {

  window.addEventListener('popstate', e => {
    const current = this.current
    this.transitionTo(getLocation(this.base), route => {
      if (expectScroll) {
        handleScroll(router, route, current, true)
      }
    })
  })
}

复制代码以上就是’hash’和’history’两种模式,都是通过浏览器接口实现的。

两种模式比较

一般的需求场景中,hash模式与history模式是差不多的,根据MDN的介绍,调用history.pushState()相比于直接修改hash主要有以下优势:

1、pushState设置的新url可以是与当前url同源的任意url,而hash只可修改#后面的部分,故只可设置与当前同文档的url
2、pushState设置的新url可以与当前url一模一样,这样也会把记录添加到栈中,而hash设置的新值必须与原来不一样才会触发记录添加到栈中
3、pushState通过stateObject可以添加任意类型的数据记录中,而hash只可添加短字符串
4、pushState可额外设置title属性供后续使用

history模式的问题

hash模式仅改变hash部分的内容,而hash部分是不会包含在http请求中的(hash带#):
http://oursite.com/#/user/id //如请求,只会发送http://oursite.com/
所以hash模式下遇到根据url请求页面不会有问题
而history模式则将url修改的就和正常请求后端的url一样(history不带#)
http://oursite.com/user/id
如果这种向后端发送请求的话,后端没有配置对应/user/id的get路由处理,会返回404错误。
官方推荐的解决办法是在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。同时这么做以后,服务器就不再返回 404 错误页面,因为对于所有路径都会返回 index.html 文件。为了避免这种情况,在 Vue 应用里面覆盖所有的路由情况,然后在给出一个 404 页面。或者,如果是用 Node.js 作后台,可以使用服务端的路由来匹配 URL,当没有匹配到路由的时候返回 404,从而实现 fallback。

参考链接:https://juejin.im/post/5b08c9ccf265da0dd527d98d

你可能感兴趣的:(JavaScript)