Vue_001路由

前端路由实现思路

  • 路由是什么
  • hash模式?history模式?memory模式
  • Vue-Router源码
  • 正则表达式的使用
  • VueRouter的一些API

什么是路由

先看什么是上网路由

  • 分发请求的对象

你在家里如何访问百度/腾讯/淘宝的呢?

  • 你先得有一个路由器
  • 输入一个地址 如 www.baidu.com
  • 假设你家里是电信的宽带,路由访问电信
  • 电信找到 百度的首页给你返回

停止学习框架,为什么

  • angular
  • vue
  • react

都有各自的路由,你学框架要学三遍

  • 我们应该学好基础

前端路由

需求:实现如下图所示的路由功能(hash路由)

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html>
<head>
<title>Parcel Sandbox</title>
<meta charset="UTF-8" />
</head>

<body>
<a href="#1">go to 1</a>
<a href="#2">go to 2</a>
<a href="#3">go to 3</a>
<a href="#4">go to 4</a>

<div id="app"></div>

<div id="div404" style="display: none;">你要找的内容被狗吃了</div>

<script src="src/index.js"></script>
</body>
</html>

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const app = document.querySelector("#app");
const div1 = document.createElement("div");
div1.innerHTML = "1";
const div2 = document.createElement("div");
div2.innerHTML = "2";
const div3 = document.createElement("div");
div3.innerHTML = "3";
const div4 = document.createElement("div");
div4.innerHTML = "4";
const routeTable = {
"1": div1,
"2": div2,
"3": div3,
"4": div4
};

function route(container) {
let number = window.location.hash.substr(1);
// 默认路由
number = number || 1;

// 获取界面
let div = routeTable[number.toString()];
// 兜底路由,当你访问的路由不存在时候
if (!div) {
div = document.querySelector("#div404");
}
// 展示界面
container.innerHTML = "";
container.appendChild(div);
}

route(app);

window.addEventListener("hashchange", () => {
console.log("hash 变了");
route(app);
});
  • 重点是 监听 hash 改变的事件 hashchange
  • 在线代码

嵌套路由实现(只说原理不写代码)

继续以 hash 模式来说

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#1
#2
#3
#4

如何嵌套呢?
只能这样
#1/1.1
#1/1.2
#1/1.3

// 思路
// 假如 当前路由是 #1/1.1
let routerPath = window.location.hash.substr(1); // 1/1.1
let arr = routerPath.split('/') // ['1','1.1']

再去进一步的匹配路由

hash / history / memory 路由的区别

  • hash 任何情况下都可以使用的前端路由

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    缺点 SEO 不友好(根源是因为服务器收不到 hash)

    打开 chrome 访问 https://www.baidu.com#1
    点击 network
    点击 network
    点击 network
    发现,url 为 https://www.baidu.com hash被吃了

    结论:浏览器不会把 # 号之后的内容发给浏览器的
    这就导致:
    https://www.baidu.com#1
    https://www.baidu.com#abc
    https://www.baidu.com#hjx

    三个页面有自己的内容

    最后实际请求的都是 https://www.baidu.com

    这样 google收录后永远都会展示默认页面https://www.baidu.com 的内容(默认路由)
  • history 后端把所有前端路由都渲染同一页面

    1
    唯一缺点 IE8以下不支持

history 模式

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
const app = document.querySelector("#app");
const div1 = document.createElement("div");
div1.innerHTML = "1";
const div2 = document.createElement("div");
div2.innerHTML = "2";
const div3 = document.createElement("div");
div3.innerHTML = "3";
const div4 = document.createElement("div");
div4.innerHTML = "4";
const routeTable = {
"/1": div1,
"/2": div2,
"/3": div3,
"/4": div4
};

function route(container) {
let number = window.location.pathname;
console.log("number: " + number);

if (number === "/") {
number = "/1";
}

// 获取界面
let div = routeTable[number.toString()];
if (!div) {
div = document.querySelector("#div404");
}
div.style.display = "block";

// 展示界面
container.innerHTML = "";
container.appendChild(div);
}

const allA = document.querySelectorAll("a.link");

for (let a of allA) {
a.addEventListener("click", e => {
e.preventDefault();
const href = a.getAttribute("href");
window.history.pushState(null, `page ${href}`, href);
// 通知
onStateChange(href);
});
}

route(app);

function onStateChange() {
console.log("state 变了");
route(app);
}

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html>
<head>
<title>Parcel Sandbox</title>
<meta charset="UTF-8" />
</head>

<body>
<a class="link" href="/1">go to 1</a>
<a class="link" href="/2">go to 2</a>
<a class="link" href="/3">go to 3</a>
<a class="link" href="/4">go to 4</a>
<div id="app"></div>

<div id="div404" style="display: none;">你要找的内容被狗吃了</div>

<script src="src/index.js"></script>
</body>
</html>

注意

  • 如果单纯改变 pathname 每次改变路由到会造成页面刷新

    1
    window.location.pathname // 默认会带一个 "/" 开头
  • 必须结合 history API

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    如果你用a标签触发路由改变,要阻止a它的默认事件(不阻止就刷新页面)
    // 劫持点击事件
    aLink.addEventListener('click',(e)=>{
    // 阻止默认事件
    e.preventDefault();
    // 触发 history API
    const path = aLink.getAttribute('href');
    window.history.pushState(null,null,path);
    // 通知 路由事件
    onStateChange();
    })

memory 模式(前端一般不用,因为没有任何url) 单机版路由

原理就是把 路由存在 localStorage /或者本地里

1
2
3
4
此模式适合非浏览器 
- app
- react native
- weex

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
const app = document.querySelector("#app");
const div1 = document.createElement("div");
div1.innerHTML = "1";
const div2 = document.createElement("div");
div2.innerHTML = "2";
const div3 = document.createElement("div");
div3.innerHTML = "3";
const div4 = document.createElement("div");
div4.innerHTML = "4";
const routeTable = {
"/1": div1,
"/2": div2,
"/3": div3,
"/4": div4
};

function route(container) {
let number = window.localStorage.getItem("xxx");

if (!number) {
number = "/1";
}

// 获取界面
let div = routeTable[number.toString()];
if (!div) {
div = document.querySelector("#div404");
}
div.style.display = "block";

// 展示界面
container.innerHTML = "";
container.appendChild(div);
}

const allA = document.querySelectorAll("a.link");

for (let a of allA) {
a.addEventListener("click", e => {
e.preventDefault();
const href = a.getAttribute("href");
window.localStorage.setItem("xxx", href);
// 通知
onStateChange(href);
});
}

route(app);

function onStateChange() {
console.log("state 变了");
route(app);
}

Vue-Router

如何看源码

  • 从你最常用的地方看 router-link
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    1.打开代码链接 
    2.搜 link 发现有个 Link 对象(vue的组件) 往下看 return 返回信息 里面有个
    return h(this.tag, data, this.$slots.default)
    1. 这个 h 是啥?
    2. vue 除了提供模版渲染的方式,还提供了 一个 render函数
    可以让你用 js 的方式创建任意元素
    3. 理解完 h函数 我们继续看
    return h(this.tag, data, this.$slots.default)
    - this.tag 代表你要渲染的标签
    - data ? 又不知道了
    - this.$slots.default 插槽 标签里的内容 不重要
    4. 看data是啥
    var data = {
    class: classes
    };

    if (this.tag === 'a') {
    data.on = on;
    data.attrs = { href: href };
    } else {
    var a = findAnchor(this.$slots.default);
    if (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 {
    data.on = on;
    }
    }
    5. data.on ? 应该是个事件继续检索信息
    var on = { click: guardEvent };
    if (Array.isArray(this.event)) {
    this.event.forEach(function (e) { on[e] = handler; });
    } else {
    on[this.event] = handler;
    }
    6. handler 是啥?
    var handler = function (e) {
    if (guardEvent(e)) {
    if (this$1.replace) {
    router.replace(location);
    } else {
    router.push(location);
    }
    }
    };
    7. 看 router.push(location); 熟悉吗?
    router 是那里定义的?
    继续看
    var router = this.$router;

    $router是啥!这时就要脑补了

    // 参考vue-router文档里
    const routes = [
    { path: '/foo', component: Foo },
    { path: '/bar', component: Bar }
    ]

    // 3. 创建 router 实例,然后传 `routes` 配置
    // 你还可以传别的配置参数, 不过先这么简单着吧。
    const router = new VueRouter({
    routes // (缩写) 相当于 routes: routes
    })

    // 4. 创建和挂载根实例。
    // 记得要通过 router 配置参数注入路由,
    // 从而让整个应用都有路由功能
    const app = new Vue({
    router
    }).$mount('#app')

    这里的router 就是 VueRouter 继续找

    10 找到了 push 定义的地方
    VueRouter.prototype.push = function push (location, onComplete, onAbort) {
    this.history.push(location, onComplete, onAbort);
    };

    11 发现了 里面路由的各种模式定义
    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:
    {
    assert(false, ("invalid mode: " + mode));
    }
    }

    1. 就看 HTML5History 模式
    HTML5History.prototype.push = function push (location, onComplete, onAbort) {
    var this$1 = this;
    var ref = this;
    var fromRoute = ref.current;
    this.transitionTo(location, function (route) {
    pushState(cleanPath(this$1.base + route.fullPath));
    handleScroll(this$1.router, route, fromRoute, false);
    onComplete && onComplete(route);
    }, onAbort);
    }

    1. 继续看 transitionTo 是啥

    History.prototype.transitionTo = function transitionTo (location, onComplete, onAbort) {
    var this$1 = this;
    var route = this.router.match(location, this.current);
    this.confirmTransition(route, function () {
    this$1.updateRoute(route);
    onComplete && onComplete(route);
    this$1.ensureURL();

    // fire ready cbs once
    if (!this$1.ready) {
    this$1.ready = true;
    this$1.readyCbs.forEach(function (cb) { cb(route); });
    }
    }, function (err) {
    if (onAbort) {
    onAbort(err);
    }
    if (err && !this$1.ready) {
    this$1.ready = true;
    this$1.readyErrorCbs.forEach(function (cb) { cb(err); });
    }
    });
    };

    1. 看出 onComplete 是成功的逻辑处理
    this.transitionTo(location, function (route) {
    pushState(cleanPath(this$1.base + route.fullPath));
    handleScroll(this$1.router, route, fromRoute, false);
    onComplete && onComplete(route);
    }, onAbort);

    15 看 pushState 又熟悉了
    function pushState (url, replace) {
    saveScrollPosition();
    var history = window.history;
    try {
    if (replace) {
    history.replaceState({ key: _key }, '', url);
    } else {
    _key = genKey();
    history.pushState({ key: _key }, '', url);
    }
    } catch (e) {
    window.location[replace ? 'replace' : 'assign'](url);
    }
    }

h函数是啥?

我们想实现一个 传递 level参数 就渲染 h1~h6的 自定义组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<ah :level="1">Hello world!</ah>
当我们开始写一个只能通过 level prop 动态生成 heading 标签的组件时,你可能很快想到这样实现:

<script type="text/x-template" id="anchored-heading-template">
<h1 v-if="level === 1">
<slot></slot>
</h1>
<h2 v-else-if="level === 2">
<slot></slot>
</h2>
<h3 v-else-if="level === 3">
<slot></slot>
</h3>
<h4 v-else-if="level === 4">
<slot></slot>
</h4>
<h5 v-else-if="level === 5">
<slot></slot>
</h5>
<h6 v-else-if="level === 6">
<slot></slot>
</h6>
</script>

是不是很傻

  • 用 render 函数 重写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Vue.component('anchored-heading', {
render: function (createElement) {
return createElement(
'h' + this.level, // 标签名称
this.$slots.default // 子元素数组
)
},
props: {
level: {
type: Number,
required: true
}
}
})

h 就是 createElement

h 就是 createElement

h 就是 createElement