路由模式
hash
:默認hash
模式, 使用URL hash
值來作路由history
:依賴HTML5 History API
和服務器配置abstract
:支持所有JavaScript
運行環境,如Node.js
服務器端
ips hash
和 history
中都會記錄瀏覽歷史,保存在瀏覽器的路由棧中
模式分配
/* 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()
方法一摸一樣,區別是它修改瀏覽器歷史當中的記錄 -
length
:history.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
屬性指向pushState
和replaceState
方法爲當前url所提供的狀態對象(即這兩個方法的第一個參數)。上邊代碼中的event.state
就是通過pushState
和replaceState
方法爲當前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-link
和 route-view
route-link
該組件支持用戶在具有路由功能的應用中(點擊)導航,默認渲染成帶有正確鏈接的
<a>
標籤,可以通過tag屬性生成別的標籤。
它本質上是通過在生成的標籤上綁定了click事件,然後執行對應的VueRouter實例的push()實現的,對於
router-link
組件來說,可以傳入以下props:
to
表示目標路由的鏈接,當被點擊後,內部會立刻把to的值傳到router.push()
,所以這個值可以是一個字符串或者是描述目標位置的對象tag
router-link
組件渲染的標籤名,默認爲aexact
布爾類型,“是否激活”默認類名的依據是包含匹配append
布爾類型,設置append
屬性後,則在當前(相對)路勁前添加基路徑replace
布爾類型,設置replace
後,當點擊時會調用router.replace()
而不是router.push()
,這樣導航後不會留下history記錄activeClass
鏈接激活時使用的CSS類名exactActiveClass
配置當鏈接被精確匹配的時候應該激活的 classevent
聲明可以用來觸發導航的事件。可以是一個字符串或是一個包含字符串的數組
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>
,根據嵌套路徑,渲染嵌套組件
它只有一個名爲
name
的props
,這個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)
}
}