vue-router 原理及實現

路由模式
  • hash:默認hash 模式, 使用 URL hash值來作路由
  • history:依賴 HTML5 History API 和服務器配置
  • abstract:支持所有 JavaScript 運行環境,如 Node.js 服務器端

ips hashhistory 中都會記錄瀏覽歷史,保存在瀏覽器的路由棧中

模式分配
/* other... */
if (!inBrowser) { // 非瀏覽器模式
 	mode = 'abstract'
 }
 this.mode = mode
 // 通過mode 的值來調用不同的實例
 switch (mode) { 
   case 'history':
     this.history = new HTML5History(this, options.base)
     break
   case 'hash':
     this.history = new HashHistory(this, options.base, this.fallback)
     break
   case 'abstract':
     this.history = new AbstractHistory(this, options.base)
     break
   default:
     if (process.env.NODE_ENV !== 'production') {
       assert(false, `invalid mode: ${mode}`)
     }
 }
 /* other... */
hash 模式原理

window.location對象 例如:https://www.baidu.com:443/test#/params
hash : 設置或返回從 (#) 開始的 URL(錨)。#/params
host : 設置或返回主機名和當前 URL 的端口號 www.baidu.com:443
hostname:設置或返回當前 URL 的主機名www.baidu.com
href : 設置或返回完整的 URL。https://www.baidu.com:443/test#/params
pathname: 設置或返回當前 URL 的路徑部分。/test
port:設置或返回當前 URL 的端口號。443
search : 設置或返回從問號 (?) 開始的 URL(查詢部分)。
assign() : 加載新的文檔。
reload() : 重新加載當前文檔。
replace() : 用新的文檔替換當前文檔。

  • hash即瀏覽器url中 #後面的內容
  • 通過 window.location.hash 來獲取內容
    在這裏插入圖片描述
  • www.baidu.com/這裏是什麼內容都是忽略的ssss#內容 詳細如圖

在這裏插入圖片描述

  • 通過 hashchange 事件來監聽瀏覽器的 hash值的改變, 渲染響應路由頁面
if('onhashchange' in window) { 
	window.addEventListener('hashchange',function(e){
	   console.log(window.location.hash) 
	},false)
}
history 模式原理

history 對象方法

  • go() :接受一個整數爲參數,移動到該整數指定的頁面,比如history.go(1)相當於history.forward(),history.go(-1)相當於history.back(),history.go(0)相當於刷新當前頁面

  • back() :移動到上一個訪問頁面,等同於瀏覽器的後退鍵,常見的返回上一頁就可以用back(),是從瀏覽器緩存中加載,而不是重新要求服務器發送新的網頁

  • forward() :移動到下一個訪問頁面,等同於瀏覽器的前進鍵

  • pushState()

    history.pushstate(state,title,url)
    state: 一個與指定網址相關的狀態對象,popState事件觸發時,該對象會傳入回調函數,如果不需要這個對象,此處可填null
    title: 新頁面的標題,但是所有瀏覽器目前都忽略這個值,因此這裏可以填null
    url: 新的網址,必須與當前頁面處在同一個域,瀏覽器的地址欄將顯示這個網址

  • replaceState(): history.replaceState()方法的參數和pushState()方法一摸一樣,區別是它修改瀏覽器歷史當中的記錄

  • lengthhistory.length屬性保存着歷史記錄的url數量,初始時該值爲1,如果當前窗口先後訪問了三個網址,那麼history對象就包括3項,history.length=3
    state:返回當前頁面的state對象。可以通過replaceState()pushState()改變state,可以存儲很多數據

    scrollRestoration
    history.scrollRestoration = 'manual'; 關閉瀏覽器自動滾動行爲
    history.scrollRestoration = 'auto'; 打開瀏覽器自動滾動行爲(默認)

  • window.location.href.replace(window.location.origin, '') 獲取記錄內容

在這裏插入圖片描述

  • 通過popstate事件來監聽history模式, 渲染響應路由頁面

popState 事件

每當同一個文檔的瀏覽歷史(即history)出現變化時,就會觸發popState事件

注意:僅僅調用pushState方法或replaceState方法,並不會觸發該事件,只有用戶點擊瀏覽器後退和前進按鈕時,或者使用js調用back、forward、go方法時纔會觸發。另外該事件只針對同一個文檔,如果瀏覽歷史的切換,導致加載不同的文檔,該事件不會被觸發

使用的時候,可以爲popState事件指定回調函數

window.onpopstate = function (event) {
  console.log('location: ' + document.location);
  console.log('state: ' + JSON.stringify(event.state));
};
    
// 或者

window.addEventListener('popstate', function(event) {
  console.log('location: ' + document.location);
  console.log('state: ' + JSON.stringify(event.state));
});

回調函數的參數是一個event事件對象,它的 state 屬性指向 pushStatereplaceState 方法爲當前url所提供的狀態對象(即這兩個方法的第一個參數)。上邊代碼中的event.state就是通過pushStatereplaceState方法爲當前url綁定的state對象

這個state也可以直接通過history對象讀取 history.state

注意:頁面第一次加載的時候,瀏覽器不會觸發popState事件

 window.addEventListener('popstate', e => {
    console.log(window.location.href.replace(window.location.origin, ''));
})

完整的DEMO

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>原生實現hash和browser兩種路由模式</title>
</head>
<body>
    <div class="router_box">
        <a href="/home" class="router" replace="true">主頁</a>
        <a href="/news" class="router">新聞</a>
        <a href="/team" class="router">團隊</a>
        <a href="/about" class="router">關於</a>
        <a href="/abcd" class="router">隨便什麼</a>
    </div>
    <div id="router-view"></div>
    <script>
        function Router(params){
            // 記錄routes配置
            this.routes = params.routes || [];
            // 記錄路由模式
            this.mode = params.mode || 'hash';
            console.log('this.mode', this.mode);
            // 初始化
            this.init = function(){
                // 綁定路由響應事件
                var that = this;
                document.querySelectorAll(".router").forEach((item,index)=>{
                    item.addEventListener("click",function(e){
                        // 阻止a標籤的默認行爲
                        if ( e && e.preventDefault ){
                            e.preventDefault(); 
                        }else{
                            window.event.returnValue = false;  
                        } 
                        
                        if (that.mode == 'hash'){
                            // 判斷是replace方法還是push方法
                            if (this.getAttribute("replace")){
                                var i = window.location.href.indexOf('#')
                                // 通過replace方法直接替換url
                                window.location.replace(
                                    window.location.href.slice(0, i >= 0 ? i : 0) + '#' + this.getAttribute("href")
                                )
                            }else{
                                // 通過賦值追加
                                window.location.hash = this.getAttribute("href");
                            }
                        }else{
                            if (this.getAttribute("replace")){
                                window.history.replaceState({}, '', window.location.origin+this.getAttribute("href"))
                                that.routerChange();

                            }else{
                                window.history.pushState({}, '', window.location.origin+this.getAttribute("href"))
                                that.routerChange();
                            }
                        }
                        
                    }, false);
                });
                // 監聽路由改變
                if (this.mode == 'hash'){//hash模式時監聽hashchange
                    window.addEventListener("hashchange",()=>{
                        this.routerChange();
                    });
                }else{//history模式時監聽popstate事件
                    window.addEventListener('popstate', e => {
                        console.log(123);
                        this.routerChange();
                    })
                }
                this.routerChange();
            },
            // 路由改變監聽事件
            this.routerChange = function(){
                if (this.mode == 'hash'){
                    let nowHash=window.location.hash;
                    let index=this.routes.findIndex((item,index)=>{
                        return nowHash == ('#'+item.path);
                    });
                    if(index>=0){
                        document.querySelector("#router-view").innerHTML=this.routes[index].component;
                    }else {
                        let defaultIndex=this.routes.findIndex((item,index)=>{
                            return item.path=='*';
                        });
                        if(defaultIndex>=0){
                            const i = window.location.href.indexOf('#')
                            window.location.replace(
                                window.location.href.slice(0, i >= 0 ? i : 0) + '#' + this.routes[defaultIndex].redirect
                            )
                        }
                    }
                }else{
                    let path = window.location.href.replace(window.location.origin, '');
                    let index=this.routes.findIndex((item,index)=>{
                        console.log('path...', path, 'item.path...', item.path);
                        return path == item.path;
                    });
                    if(index>=0){
                        document.querySelector("#router-view").innerHTML=this.routes[index].component;
                    }else {
                        let defaultIndex=this.routes.findIndex((item,index)=>{
                            return item.path=='*';
                        });
                        if(defaultIndex>=0){
                            console.log(window.location.origin+this.routes[defaultIndex].redirect)
                            window.history.pushState({}, '', window.location.origin+this.routes[defaultIndex].redirect)
                            this.routerChange();
                            
                        }
                    }
                }
            }
            // 調用初始化
            this.init();
        }
        new Router({
            mode: 'hash',
            routes:[
                { path: '/home', component: '<h1>主頁</h1><h4>新一代前端工程師:我們啥都會</h4>' },
                { path: '/news', component: '<h1>新聞</h1><h4>今天2020-07-01</h4>' },
                { path: '/team', component: '<h1>團隊</h1><h4>WEB前端工程師</h4>' },
                { path: '/about', component: '<h1>關於</h1><h4>我們都要加油</h4>' },
                { path:'*', redirect:'/home'}
            ]
        });
    </script>
</body>
</html>

加深理解閱讀 route-linkroute-view

route-link

官方API文檔

該組件支持用戶在具有路由功能的應用中(點擊)導航,默認渲染成帶有正確鏈接的<a>標籤,可以通過tag屬性生成別的標籤。

它本質上是通過在生成的標籤上綁定了click事件,然後執行對應的VueRouter實例的push()實現的,對於router-link組件來說,可以傳入以下props:

  • to 表示目標路由的鏈接,當被點擊後,內部會立刻把to的值傳到router.push(),所以這個值可以是一個字符串或者是描述目標位置的對象
  • tag router-link組件渲染的標籤名,默認爲a
  • exact 布爾類型,“是否激活”默認類名的依據是包含匹配
  • append 布爾類型,設置append屬性後,則在當前(相對)路勁前添加基路徑
  • replace 布爾類型,設置replace後,當點擊時會調用router.replace()而不是router.push(),這樣導航後不會留下history記錄
  • activeClass 鏈接激活時使用的CSS類名
  • exactActiveClass 配置當鏈接被精確匹配的時候應該激活的 class
  • event 聲明可以用來觸發導航的事件。可以是一個字符串或是一個包含字符串的數組
var Link = {
  name: 'RouterLink',
  props: {
    to: {
      type: toTypes,
      required: true
    },
    tag: {
      type: String,
      default: 'a'
    },
    exact: Boolean,
    append: Boolean,
    replace: Boolean,
    activeClass: String,
    exactActiveClass: String,
    event: {
      type: eventTypes,
      default: 'click'
    }
  },
  render: function render (h) {
    var this$1 = this;

    var router = this.$router;
    var current = this.$route;
    var ref = router.resolve(this.to, current, this.append);
    var location = ref.location;
    var route = ref.route;
    var href = ref.href;

    var classes = {};
    var globalActiveClass = router.options.linkActiveClass;
    var globalExactActiveClass = router.options.linkExactActiveClass;
    // 支持全局空 Active Class
    var activeClassFallback = globalActiveClass == null
      ? 'router-link-active'
      : globalActiveClass;
    var exactActiveClassFallback = globalExactActiveClass == null
      ? 'router-link-exact-active'
      : globalExactActiveClass;
    var activeClass = this.activeClass == null
      ? activeClassFallback
      : this.activeClass;
    var exactActiveClass = this.exactActiveClass == null
      ? exactActiveClassFallback
      : this.exactActiveClass;
    var compareTarget = location.path
      ? createRoute(null, location, null, router)
      : route;

    classes[exactActiveClass] = isSameRoute(current, compareTarget);
    classes[activeClass] = this.exact
      ? classes[exactActiveClass]
      : isIncludedRoute(current, compareTarget);

    var handler = function (e) {
      if (guardEvent(e)) {
        if (this$1.replace) {
          router.replace(location);
        } else {
          router.push(location);
        }
      }
    };

    var on = { click: guardEvent };
    if (Array.isArray(this.event)) {
      this.event.forEach(function (e) { on[e] = handler; });
    } else {
      on[this.event] = handler;
    }

    var data = {
      class: classes
    };

    if (this.tag === 'a') {
      data.on = on;
      data.attrs = { href: href };
    } else {
      // 找到第一個< a >子級,並應用偵聽器和href
      var a = findAnchor(this.$slots.default);
      if (a) {
        // 如果< a >是靜態節點
        a.isStatic = false;
        var aData = a.data = extend({}, a.data);
        aData.on = on;
        var aAttrs = a.data.attrs = extend({}, a.data.attrs);
        aAttrs.href = href;
      } else {
        // 沒有< a >子代,將偵聽器應用於自身
        data.on = on;
      }
    }

    return h(this.tag, data, this.$slots.default)
  }
}
route-view

router-view是一個 functional 組件,渲染路徑匹配到的視圖組件。<router-view> 渲染的組件還可以內嵌自己的 <router-view>,根據嵌套路徑,渲染嵌套組件

它只有一個名爲nameprops,這個name還有個默認值,就是default,一般情況下,我們不用傳遞name,只有在命名視圖的情況下,我們需要傳遞name,命名視圖就是在同級展示多個視圖,而不是嵌套的展示出來,

router-view組件渲染時是從VueRouter實例._route.matched屬性獲取需要渲染的組件,也就是我們在vue內部的this.$route.matched上獲取的

var View = {
  name: 'RouterView',
  functional: true,
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  render: function render (_, ref) {
    var props = ref.props;
    var children = ref.children;
    var parent = ref.parent;
    var data = ref.data;

    // 由devtools用來顯示路由器視圖標記
    data.routerView = true;

    // 直接使用父上下文的createElement()函數
    // 以便由router-view呈現的組件能夠解析命名的插槽
    var h = parent.$createElement;
    var name = props.name;
    var route = parent.$route;
    var cache = parent._routerViewCache || (parent._routerViewCache = {});

    // 確定當前視圖深度,同時檢查樹是否
    // 已被切換爲非活動但保持活動狀態
    var depth = 0;
    var inactive = false;
    while (parent && parent._routerRoot !== parent) {
      if (parent.$vnode && parent.$vnode.data.routerView) {
        depth++;
      }
      if (parent._inactive) {
        inactive = true;
      }
      parent = parent.$parent;
    }
    data.routerViewDepth = depth;

    // 如果樹處於非活動狀態並且保持活動狀態,則渲染上一視圖
    if (inactive) {
      return h(cache[name], data, children)
    }

    var matched = route.matched[depth];
    // 如果沒有匹配的路由,則呈現空節點
    if (!matched) {
      cache[name] = null;
      return h()
    }

    var component = cache[name] = matched.components[name];

    // 附加實例註冊掛鉤
    // 這將在實例的注入生命週期鉤子中調用
    data.registerRouteInstance = function (vm, val) {
      // 對於註銷,val可能未定義
      var current = matched.instances[name];
      if (
        (val && current !== vm) ||
        (!val && current === vm)
      ) {
        matched.instances[name] = val;
      }
    }

    // 同時在預緩存掛鉤中註冊實例
    // 如果同一組件實例在不同的路由上被重用
    ;(data.hook || (data.hook = {})).prepatch = function (_, vnode) {
      matched.instances[name] = vnode.componentInstance;
    };

    // 解析 props
    var propsToPass = data.props = resolveProps(route, matched.props && matched.props[name]);
    if (propsToPass) {
      // clone to prevent mutation
      propsToPass = data.props = extend({}, propsToPass);
      // 將未聲明的props具作爲屬性傳遞
      var attrs = data.attrs = data.attrs || {};
      for (var key in propsToPass) {
        if (!component.props || !(key in component.props)) {
          attrs[key] = propsToPass[key];
          delete propsToPass[key];
        }
      }
    }

    return h(component, data, children)
  }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章