前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【路由】:history——ReactRouter vs VueRouter

【路由】:history——ReactRouter vs VueRouter

作者头像
WEBJ2EE
发布2021-04-07 00:32:18
1.4K0
发布2021-04-07 00:32:18
举报
文章被收录于专栏:WebJ2EEWebJ2EE

1. 前言

  • 基于 React、Vue 研发的单页应用(SPA),是目前主流。
  • 前端路由(Router),又是单页应用(SPA)中非常重要一环。
  • 无刷新(reload)修改、监听浏览器URL变化,又是前端路由的核心。即要在浏览器不 reload 的情况下,把 “UI 的变化” 同“浏览器地址栏中 URL的变化”,双向映射起来。
  • 浏览器历史管理(history),又是实现“无刷新修改、监听浏览器 URL 变化”技术的基础。
  • 基于Hash、基于H5 History API、基于内存,又是“浏览器历史管理”课题中的三个技术流派。

上面谈到的浏览器历史管理,是浏览器层面的原生技术,现实不仅如此:

  • React 的官方路由库 react-router,它基于浏览器history 的 API,设计了自己的 history 管理库(我把它叫做 react-router's history)。而且 react-router 的能力、特性、使用模式,都取决于 react-router's history 库。
  • 同理,Vue 的官方路由库 vue-router,它也有自己的一套 history 管理库(为了与 react-router's history 区分,我把它叫做 vue-router's history),同样,vue-router's history 也决定了 vue-router 接口、能力、特性。例如:命名路由、重定向、嵌套路由、路由别名、导航守卫,这些技能都由 vue-router's history 提供底层支持。

站在业务开发者角度,vue-router 用起来更舒服一些,因为 vue-router 提供的导航守卫、命名路由、路由传参等特性,基本上不需要再去二次封装,拿来就能用,实用性比较高。react-router 则更自由灵活一些,很多场景、模式,需要根据官方文档的建议,再结合实际业务场景,进行二次封装,才能应用到生产项目中,复杂度高一些。

这篇文章分析一下浏览器原生的历史管理、react-router 中的历史管理,以及vue-router 中的历史管理。给大家直观展示一下两大主流框架(React、Vue)在路由管理方面的异同。

2. HTML5 History API(浏览器原生 history)

The DOM Window object provides access to the browser's session history (not to be confused for WebExtensions history) through the history object. It exposes useful methods and properties that let you navigate back and forth through the user's history, and manipulate the contents of the history stack.

HTML5 introduced the pushState() and replaceState() methods for add and modifying history entries, respectively. These methods work in conjunction with the onpopstate event.

2.1. Adding and modifying history entries

2.1.1. pushState()

Suppose https://mozilla.org/foo.html executes the following JavaScript:

代码语言:javascript
复制
let stateObj = {
  foo: "bar",
}
history.pushState(stateObj, "page 2", "bar.html")

This will cause the URL bar to display https://mozilla.org/bar.html, but won't cause the browser to load bar.html or even check that bar.html exists.

Suppose now that the user navigates to https://google.com, then clicks the Back button. At this point, the URL bar will display https://mozilla.org/bar.html and history.state will contain the stateObj. The popstate event won't be fired because the page has been reloaded. The page itself will look like bar.html.

If the user clicks Back once again, the URL will change to https://mozilla.org/foo.html, and the document will get a popstate event, this time with a null state object. Here too, going back doesn't change the document's contents from what they were in the previous step, although the document might update its contents manually upon receiving the popstate event.

语法:

注意:pushState 与 window.location

In a sense, calling pushState() is similar to setting window.location = "#foo"(备注:因为只改地址,不刷新页面), in that both will also create and activate another history entry associated with the current document. But pushState() has a few advantages:

  • The new URL can be any URL in the same origin as the current URL. In contrast, setting window.location keeps you at the same document only if you modify only the hash.
  • You don't have to change the URL if you don't want to. In contrast, setting window.location = "#foo"; only creates a new history entry if the current hash isn't #foo.
  • You can associate arbitrary data with your new history entry. With the hash-based approach, you need to encode all of the relevant data into a short string.

Note that pushState() never causes a hashchange event to be fired, even if the new URL differs from the old URL only in its hash.

示例:index.html

代码语言:javascript
复制
<!DOCTYPE html>
<html>
<head>
  <title></title>
</head>
<body>
  <h1>Hello World</h1>
  <script type="text/javascript">
    setTimeout(()=>{
      let stateObj = { foo: "bar" }
      history.pushState(stateObj, "page 2", "b.html")
    }, 3000)
</script>
</body>
</html>

2.1.2. replaceState()

history.replaceState() operates exactly like history.pushState(), except that replaceState() modifies the current history entry instead of creating a new one.

replaceState() is particularly useful when you want to update the state object or URL of the current history entry in response to some user action.

示例:

2.2. Reading the current state

When your page loads, it might have a non-null state object. This can happen, for example, if the page sets a state object (using pushState() or replaceState()) and then the user restarts their browser. When the page reloads, the page will receive an onload event, but no popstate event. However, if you read the history.state property, you'll get back the state object you would have gotten if a popstate had fired.

代码语言:javascript
复制
let currentState = history.state

2.3. The popstate event

A popstate event is dispatched to the window each time the active history entry changes between two history entries for the same document. If the activated history entry was created by a call to history.pushState(), or was affected by a call to history.replaceState(), the popstate event's state property contains a copy of the history entry's state object.

Note: Calling history.pushState() or history.replaceState() won't trigger a popstate event.The popstate event is only triggered by performing a browser action, such as clicking on the back button (or calling history.back() in JavaScript), when navigating between two history entries for the same document.

示例:

代码语言:javascript
复制
<!DOCTYPE html>
<html>
<head>
  <title></title>
</head>
<body>
  <h1>Hello World</h1>
  <script type="text/javascript">

    window.onpopstate = function(event) {
      console.log("location: " + document.location + ", state: " + JSON.stringify(event.state));
    };

    function delay(ms){
      return new Promise(resolve => setTimeout(resolve, ms));
    }


    async function init(){
      await delay(1000);
      history.pushState({page: 1}, "title 1", "?page=1");

      await delay(1000);
      history.pushState({page: 2}, "title 2", "?page=2");

      await delay(1000);
      history.replaceState({page: 3}, "title 3", "?page=3");

      await delay(1000);
      history.back();

      await delay(1000);
      history.back();

      await delay(1000);
      history.go(2);
    }
    init();
</script>
</body>
</html>

3. ReactRouter's history

特别注意

"react-router" 目前最新版本是 "5.2.0"

"react-router": "^5.2.0" 中依赖的是 "history": "^4.9.0"

小心:history 5.x 版本与 4.x 版本的实现差异很大

本文分析的是 "4.10.1" 版本源码

3.1. 定位

The history library lets you easily manage session history anywhere JavaScript runs. A history object abstracts away the differences in various environments and provides a minimal API that lets you manage the history stack, navigate, and persist state between sessions.

3.2. 核心构成

ReactRouter 所使用的 history 库(后面称作 react-router's history),主要由以下几部分构成:

  • createBrowserHistory:基于 HTML5 History API 封装
    • Browser history is used in web apps
  • createHashHistory:基于 Hash API 封装
    • Hash history is used in web apps where you don't want to/can't send the URL to the server for some reason
  • createMemoryHistory:使用数组模拟 history,常用于测试环境
    • Memory history - is used in native apps and testing

3.3. 分析源码前的基础知识(path 与 location)

path 与 location 的基本概念

path 与 location 的相互转换

在源码中大量出现,有必要先了解一下

3.3.1. path 与 location

  • path:路由路径
  • location:对 path 的描述,包含
    • pathname:"/",或其他路径,不会为空
    • search:空,或以 "?" 开头的字符串
    • hash:空,或以"#" 开头的字符串

3.3.2. path -> location

位置:LocationUtils.js:createLocation

定义:path -> location,根据 path 创建 location

解析:

深度:resolve-pathname

  • createLocation 依赖 resolve-pathname 进行相对、绝对路径解析
  • resolve-pathname resolves URL pathnames identical to the way browsers resolve the pathname of an <a href> value.
代码语言:javascript
复制
import resolvePathname from 'resolve-pathname';

// Simply pass the pathname you'd like to resolve. Second
// argument is the path we're coming from, or the current
// pathname. It defaults to "/".
resolvePathname('about', '/company/jobs'); // /company/about
resolvePathname('../jobs', '/company/team/ceo'); // /company/jobs
resolvePathname('about'); // /about
resolvePathname('/about'); // /about

// Index paths (with a trailing slash) are also supported and
// work the same way as browsers.
resolvePathname('about', '/company/info/'); // /company/info/about

// In browsers, it's easy to resolve a URL pathname relative to
// the current page. Just use window.location! e.g. if
// window.location.pathname == '/company/team/ceo' then
resolvePathname('cto', window.location.pathname); // /company/team/cto
resolvePathname('../jobs', window.location.pathname); // /company/jobs

深度:PathUtils.js:parsePath(string-> Location)

3.3.3. location -> path

定义:location -> path,根据 location 创建 path

鉴于 createHashHistory、createBrowserHistory 、createMemoryHistory 三者架构极其类似,

所以下面只以 createHashHistory 为例进行源码分析,

另外两个,感兴趣的同学,可以自己研究一下

3.4. 源码分析 —— createHashHistory

3.4.1. HashHistory 的特性

  • HashHistory 不支持 state,只有 BrowserHistory 支持。
  • 如果连续 push 多次相同的 path,history 堆栈历史中只会记录一次。
代码语言:javascript
复制
<!DOCTYPE html>
<html>
<body>
    <script>
        window.addEventListener("hashchange", function () {
            console.log("hashchange", window.location.href);
        });
</script>
</body>
</html>

3.4.2. createHashHistory 的边界特性

先看看 createHashHistory 的入参:

代码语言:javascript
复制
createHashHistory({
  basename: '', // The base URL of the app (see below)
  hashType: 'slash', // The hash type to use (see below)
  // A function to use to confirm navigation with the user (see below)
  getUserConfirmation: (message, callback) => callback(window.confirm(message))
});
  • 注:这里涉及 createTransitionManager 中 confirmTransitionTo 与 getUserConfirmation 之间的联系。主要用于实现路由跳转拦截功能。

再看看 createHashHistory 的返回值:

这个 history 就是我们平常在 react-router 中使用 HashRouter 时,通过 useHistory() 得到的那个对象。

在分析 createHashHistory 内核前,先关注一下它的过渡控制对象 createTransitionManager,它与 createHashHistory 中的参数 getUserConfirmation 有内在联系,提前分析一下

3.4.3. createTransitionManager 又是啥?

  • createTransitionManager 提供了一个简单的过渡控制功能,说简单点就是,它控制你这次路由跳转的请求,能否通过(例如:你使用了 <Prompt>,其实就是被这个过渡控制组件拦截下来了);
    • 备注:VueRouter 中也有类似结构,只不过它更复杂(例如:导航守卫)。

示例:

代码语言:javascript
复制
const createTransitionManager = require("./createTransitionManager")

// callback
function callback(ok){
    console.log(`callback triggered with '${ok}'`);
}

console.log("\n----Test1----\n");

// Test 1
// 不设置 prompt 时,直接触发 callback(true)
const o = new createTransitionManager();
o.confirmTransitionTo(null, null, null, callback); // true

console.log("\n----Test2----\n");

// Test 2
// 设置 string 类型 prompt 时,需要配合 userConfirmation 使用,
// 并且 callback 的触发控制权交给 userConfirmation 处理
const o1 = new createTransitionManager();
o1.setPrompt("This is a promot!");
o1.confirmTransitionTo(null, null, function userConfirmation(prompt, callback){
    console.log(`userConfirmation triggered width '${prompt}'`);
    callback('result from userConfirmation'); //
}, callback); // true

console.log("\n----Test3----\n");

// Test 3
// 设置 function 类型 prompt 时,会获取 prompt(location, action) 的返回值 result
// a. 若 result 是 string 类型,又会交给 userConfirmation 处理,走 Test 2 模式
// b. 否则以 callback(result !== false) 触发 callback
const o2 = new createTransitionManager();
o2.setPrompt(function(location, action){
    console.log(`function prompt triggered with location:'${location}', action:'${action}'`);
    return "result from function prompt."
});
o2.confirmTransitionTo("some-location", "some-action", function userConfirmation(prompt, callback){
    console.log(`userConfirmation triggered width '${prompt}'`);
    callback('result from userConfirmation'); //
}, callback); // true

console.log("\n----Test3----\n");
  • createTransitionManager 还提供了一个简单的事件监听队列

示例:

代码语言:javascript
复制
const createTransitionManager = require("./createTransitionManager");

const o = createTransitionManager();
const unlisten1 = o.appendListener(function(...args){
    console.log('listener1 receives: ', ...args);    
});
const unlisten2 = o.appendListener(function(...args){
    console.log('listener2 receives: ', ...args);    
});

// 消息发布
o.notifyListeners("v1", "v2", "v3");

// listener1 取消监听
unlisten1();

// 消息发布
o.notifyListeners("v4", "v5", "v6"); // 消息发布

此时可以思考一下 createHashHistory 中的参数 getUserConfirmation 的用途

3.4.4. history.listen、history.block

  • history.listen 底层调用 transitionManager.appendListener 添加事件监听
  • history.block 底层调用 transitionManager.setPrompt 添加 Prompt 拦截特性

3.4.5. history.push

  • 有个值得注意的点,location 对象,每次都是新创建的。也就是说 history.location 这个对象的引用,是一直都在发生变化。

3.4.6. history.replace

套路跟 history.push 几乎一样

上面的 history.push、history.replace 的事件发起方为 API 调用,

即 "API调用" -> "浏览器历史变化"

即 "API调用" -> "浏览器 Hash 变化" -> "浏览器 history 变化"

下面将从"浏览器历史变化"(例如:点击"前进"、"后退")为出发点,

观察 createHashHistory 做了什么

3.4.7. handleHashChange

  • createHashHistory 利用 hashchange 事件,监听浏览器历史的变化。
  • 重点看看 createHashHistory 是怎么阻止过渡的,很有意思

3.4.8. history.go、goBack、goForward

  • 可以看出 history.go、history.goBack、history.goForward 这三个函数,并没有做特别包装,直接调用的是 window.history 的 go 方法。
    • window.history.go 会进一步触发 hashchange 事件,后续套路同上。

3.4.9. 总结一下

  • HashHistory 是 HashRouter 的技术基础。
  • HashHistory 是基于 URL 的 hash(#) 与 hashchange 事件封装。
  • 最最最重要的是,URL中的 hash(#) 部分发生变化,不会引发浏览器的刷新,这也是单页App能用它实现前端路由的关键。
  • 但是也需要注意到,ReactRouter 所使用的 history 库,在路由跳转管理方面比较弱,比 VueRouter 中的 history 的导航守卫功能弱很多。

4. VueRouter‘s history

4.1. 怎么又一个 history?

VueRouter 同 ReactRouter 一样,也实现了自己的一套 history 管理。

  • VueRouter's history,由 VueRouter 独立实现,跟 ReactRouter's history 不是一回事。备注:其实在 vue-router’s history 中也可以看到一些 react-router's history 代码的影子。
  • VueRouter's history 与 ReactRouter's history,在总体套路上相似,但前者更复杂,内置的功能特性更多。例如:VueRouter 的导航守卫功能就是由 VueRouter's history 提供底层支持。

4.2. 导航守卫是啥?

正如其名,vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的。 https://router.vuejs.org/zh/guide/advanced/navigation-guards.html

导航守卫在实现路由鉴权、自定义路由跳转方面,功能强大,给开发人员带来了很大的便捷。

4.3. VueRouter 也支持三种模式

同 ReactRouter 类似,VueRouter 也支持三种模式,分别为 history、hash、abstract。这三种模式分别由 html5.js、hash.js、abstract.js 实现。

其中,base.js 是 hash.js、html5.js、abstract.js 的基类

下面的内容,与分析 ReactRouter 的套路相同

还是只以 HashHistory 为例对 VueRouter 进行分析

4.4. VueRouter's history 的重要结构定义?

4.4.1. RouteConfig:路由配置

结构:

示例:

4.4.2. RouteRecord:路由记录

路由记录 是 路由配置的运行时描述。它主要用于 VueRouter 内部描述路由的运行时状态。

结构:

示例:

4.4.3. Route(路由对象)

  • 一个路由对象 (route object) 表示当前激活的(匹配到的)路由的状态信息。它包含从 URL 中解析得到的信息(例:path、hash、params、query、meta等),还有记录从根路由到当前激活路由的整条链路的 RouteRecord 数组(即:matched 字段)。
  • 注:路由对象是不可变 (immutable) 的,每次成功导航后都会产生一个新对象。

4.4.4. Location:描述一个路由位置的对象

Vue-Router 中定义的 Location 数据结构和浏览器提供的 window.location 部分结构有点类似,它们都是对 url 的结构化描述。举个例子:/abc?foo=bar&baz=qux#hello,它的 path 是 /abc,query 是 {foo:'bar',baz:'qux'}。

注意:一个路由目标可以是一个字符串,也可以是一个 Location 对象,RawLocation 是对他们的一个统一描述。

4.5. RouteConfig 与 RouteRecord 之间的转换

  • RouteRecord 是运行时的 RouteConfig
  • RouteConfig 与 RouteRecord 之间的转换,依靠 createRouteMap 函数完成 ,createRouteMap 不仅能完成初始化转换,还可以实现对路由表的动态修改
  • 还有一个叫 Matcher 的对象,它屏蔽了 createRouteMap 的使用细节,对外提供了清晰、良好的:addRoute、addRoutes、getRoutes、match 接口。

先看看 Matcher 对象的定义:

代码语言:javascript
复制
export type Matcher = {
  match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route;
  addRoutes: (routes: Array<RouteConfig>) => void;
  addRoute: (parentNameOrRoute: string | RouteConfig, route?: RouteConfig) => void;
  getRoutes: () => Array<RouteRecord>;
};

再看看 Matcher 如何利用 createRouteMap 对外提供接口的:

createMatcher 接收 2 个参数,一个是 router,它是我们 new VueRouter 返回的实例,一个是 routes,它是用户定义的路由配置。

VueRouter 支持动态路由, pathList, pathMap, nameMap 可以在初始化后由createRouteMap 方法进行增量修改(例如:addRoutes、addRoute)。

createRouteMap 本质是通过 addRouteRecord,完成转换:

createRouteMap 函数的目标是把用户的路由配置转换成一张路由映射表,它包含 3 个部分,pathList 存储所有的 path,pathMap 表示一个 path 到 RouteRecord 的映射关系,而 nameMap 表示 name 到 RouteRecord 的映射关系。

addRouteRecord 通过深度优先遍历,完成转换:

4.6. RouteRecord 到 Route 的转换(匹配过程)

Matcher 对象中,不仅包含 addRoute、addRoutes、getRoutes,还包含一个 match 方法。

代码语言:javascript
复制
export type Matcher = {
  match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route;
  addRoutes: (routes: Array<RouteConfig>) => void;
  addRoute: (parentNameOrRoute: string | RouteConfig, route?: RouteConfig) => void;
  getRoutes: () => Array<RouteRecord>;
};

match 方法接收 3 个参数,其中 raw 是 RawLocation 类型,它可以是一个 url 字符串,也可以是一个 Location 对象;currentRoute 是 Route 类型,它表示当前的路径;redirectedFrom 和重定向相关,这里先忽略。match 方法返回的是一个路径(Route),它的作用是根据传入的 raw 和当前的路径 currentRoute 计算出一个新的路径(Route)并返回。

首先执行了 normalizeLocation,它的定义在 src/util/location.js 中:

normalizeLocation 方法的作用是根据 raw(跳转目标),current(当前路由) 计算出结果 location(标准化的 location)。它主要处理了以下几种情况:

  • 如果跳转目标(next)已经被标准化过(_normalized: true),那么不再做后续处理,直接返回这个 location;
  • 如果跳转目标是命名路由(包含 name 字段),那么也没什么好处理的,clone 一下返回即可;
  • 如果跳转目标是相对参数形式跳转(即:没有 path、但有 params,且有当前路由 current ),则根据当前路由计算结果location
    • 先计算结果参数 params:是 current.params、next.params 的叠加
    • 如果当前路由(current)是命名路由(包含name字段),那么结果location 的 name 保持与当前路由(current)同样的命名,结果 location 中的参数(params)即为叠加后的参数。
    • 如果当前路由(current)存在一条路由线路(Route),那么找出路由线路的叶子节点(current.matched[current.matched.length - 1].path),结果 location 的 path 为叶子节点的path填充叠加后参数的结果。
  • 如果跳转目标是路径形式(path)跳转;
    • 结果 location 中的 path,是根据跳转目标(next)的 path、当前路由(current)的 path 计算出结果。
    • 结果 location 中的 query,是跳转目标(next)与当前路由(current)的 query 参数合并后(resolveQuery)的结果
    • 结果 location 中的 hash,是以跳转目标hash(next)中的 hash 为准;

最后我们来看一下 _createRoute 的实现:

其中redirect,alias最终都会调用createRoute方法。而 createRoute 函数最终会返回一个冻结的Route对象。

createRoute 可以根据 record 和 location 创建出来,最终返回的是一条 Route 路径,我们之前也介绍过它的数据结构。在 Vue-Router 中,所有的 Route 最终都会通过 createRoute 函数创建,并且它最后是不可以被外部修改的。Route 对象中有一个非常重要属性是 matched,它通过 formatMatch(record) 计算而来:

可以看它是通过 record 循环向上找 parent,直到找到最外层,并把所有的 record 都 push 到一个数组中,最终返回的就是 record 的数组,它记录了一条线路上的所有 record,而且顺序为从外向里(树的外层到内层)。

matched 属性非常有用,它为之后渲染组件提供了依据。

4.7. VueRouter、HashHistory 的主体结构

前面分析了很多细节,我们换个角度,看看 VueRouter 的整体结构:

再看看基于 Hash 的 HashHistory 的整体结构:

4.8. 核心分析:History.transitionTo、confirmTransition、updateRoute

history.transitionTo 是 Vue-Router 中非常重要的方法,当我们切换路由线路的时候,就会执行到该方法,前一节我们分析了 matcher 的相关实现,知道它是如何找到匹配的新线路,那么匹配到新线路后又做了哪些事情,接下来我们来完整分析一下 transitionTo 的实现,它的定义在 src/history/base.js 中:

transitionTo 首先根据目标 location 和当前路径 this.current 执行 this.router.match 方法去匹配到目标的路径。

这里 this.current 是 history 维护的当前路径,它的初始值是在 history 的构造函数中初始化的(注:这样就创建了一个初始的 Route,而 transitionTo 实际上也就是在切换 this.current)

transitionTo 的 location 参数是我们的目标路径, 可以是string或者RawLocation对象。我们通过router.match方法(我们在matcher介绍过)返回我们的目标路由对象。紧接着我们会调用confirmTransition函数做真正的切换,由于这个过程可能有一些异步的操作(如异步组件),所以整个 confirmTransition API 设计成带有成功回调函数和失败回调函数。

confirmTransition 函数中会使用,isSameRoute会检测是否导航到相同的路由,如果导航到相同的路由会停止?导航,并执行终止导航的回调。

接着我们调用resolveQueue方法,resolveQueue接受当前的路由和目标的路由的matched属性作为参数,resolveQueue的工作方式可以如下图所示。我们会逐一比较两个数组的路由,寻找出需要销毁的,需要更新的,需要激活的路由,并返回它们(因为我们需要执行它们不同的路由守卫)

然后,我们会把所有要执行的路由守卫,组合成队列 queue,由 runQueue 执行。

按照顺序如下:

  • 在失活的组件里调用 beforeRouteLeave 守卫。
  • 调用全局守卫 beforeEach。
  • 在重用的组件里调用 beforeRouteUpdate 守卫
  • 在激活的路由配置里调用 beforeEnter。
  • 解析异步路由组件。

接下来我们来分别介绍这 5 步的实现。

4.8.1. 第一步,extractLeaveGuards(deactivated)

extractLeaveGuards函数能提取出deactivated中所有需要销毁的组件内的beforeRouteLeave 守卫。extractLeaveGuards函数中会调用extractGuards函数,extractGuards函数会调用flatMapComponents函数,flatMapComponents函数会遍历records(resolveQueue返回deactivated), 在遍历过程中我们将组件,组件的实例,route对象,传入了fn(extractGuards中传入flatMapComponents的回调), 在fn中我们会获取组件中beforeRouteLeave守卫。

4.8.2. 第二步:this.router.beforeHooks

第二步是 this.router.beforeHooks,在 VueRouter 类中定义了 beforeEach 方法:

当用户使用 router.beforeEach 注册了一个全局守卫,就会往 router.beforeHooks 添加一个钩子函数,这样 this.router.beforeHooks 获取的就是用户注册的全局 beforeEach 守卫。

4.8.3. 第三步:extractUpdateHooks(updated)

extractUpdateHooks(updated)和 extractLeaveGuards(deactivated) 很类似,它获取到的就是所有重用的组件中定义的 beforeRouteUpdate 钩子函数。

4.8.4. 第四步:activated.map(m => m.beforeEnter)

第四步获取的是在激活的路由配置中定义的 beforeEnter 函数。

4.8.5. 第五步:resolveAsyncComponents(activated)

第五步是执行 resolveAsyncComponents(activated) 解析异步组件

4.8.6. runQueue —— 执行守卫队列

在获取所有的路由守卫后我们定义了一个迭代器iterator。接着我们使用runQueue遍历queue队列。将queue队列中每一个元素传入fn(迭代器iterator)中,在迭代器中会执行路由守卫,并且路由守卫中必须明确的调用next方法才会进入下一个管道,进入下一次迭代。迭代完成后,会执行runQueue的callback。

这是一个非常经典的异步函数队列化执行的模式, queue 是一个 NavigationGuard 类型的数组,我们定义了 step 函数,每次根据 index 从 queue 中取一个 guard,然后执行 fn 函数,并且把 guard 作为参数传入,第二个参数是一个函数,当这个函数执行的时候再递归执行 step 函数,前进到下一个,注意这里的 fn 就是我们刚才的 iterator 函数。

4.8.7. 小结一下

那么至此我们把所有导航守卫的执行分析完毕了,我们知道路由切换除了执行这些钩子函数,从表象上有 2 个地方会发生变化,一个是 url 发生变化,一个是组件发生变化。

4.9. vue-router's history 的几个重要辅助函数?

4.9.1. util/path.js

  • resolvePath:按照浏览器套路,解析路由路径
  • parsePath:解析路由路径为 path、query、hash;
  • cleanPath:清理路由路径中的配置("//" -> "/");

4.9.2. hsitory/base.js -> normalizeBase

  • base 路径的标准化;
    • base 必然是绝对路径(以 "/" 开头)
    • base 必然不是以 "/" 结尾
    • 用户配置 > HTML.base.href > "/"
  • react-router 的 history 中也有类似实现;

示例:

代码语言:javascript
复制
<!DOCTYPE html>
<html>
<head>
  <base href="http://10.1.60.10:8810/xyz/">
</head>
<body>
  <script>
    const inBrowser = typeof window !== 'undefined'

    function normalizeBase(base) {
      if (!base) {
        if (inBrowser) {
          // respect <base> tag
          const baseEl = document.querySelector('base')
          base = (baseEl && baseEl.getAttribute('href')) || '/'
          // strip full URL origin
          base = base.replace(/^https?:\/\/[^\/]+/, '')
        } else {
          base = '/'
        }
      }
      // make sure there's the starting slash
      if (base.charAt(0) !== '/') {
        base = '/' + base
      }
      // remove trailing slash
      return base.replace(/\/$/, '')
    }

    console.log(normalizeBase());
    console.log(normalizeBase("/xyz/abc/"));
</script>
</body>
</html>

4.9.3. util/async.js

这是一个异步队列执行器,主要用于排队执行路由守卫函数。

示例:

4.10. 路由是怎么渲染的?

路由最终的渲染离不开组件,Vue-Router 内置了 <router-view> 组件。<router-view> 是支持嵌套的,回到 render 函数,其中定义了 depth 的概念,它表示 <router-view> 嵌套的深度。

parent._routerRoot 表示的是根 Vue 实例,那么这个循环就是从当前的 <router-view> 的父节点向上找,一直找到根 Vue 实例,在这个过程,如果碰到了父节点也是 <router-view> 的时候,说明 <router-view> 有嵌套的情况,depth++。遍历完成后,根据当前线路匹配的路径和 depth 找到对应的 RouteRecord,进而找到该渲染的组件。

4.11. 总结一下

那么至此我们把 VueRouter 的主体过程分析完毕了,路径变化是路由中最重要的功能,我们要记住以下内容:路由始终会维护当前的线路,路由切换的时候会把当前线路切换到目标线路,切换过程中会执行一系列的导航守卫钩子函数,会更改 url,同样也会渲染对应的组件,切换完毕后会把目标线路更新替换当前线路,这样就会作为下一次的路径切换的依据。

参考:

History API: https://developer.mozilla.org/zh-CN/docs/Web/API/History_API https://developer.mozilla.org/en-US/docs/Web/API/History_API/Working_with_the_History_API https://developer.mozilla.org/en-US/docs/Web/API/History/pushState https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState https://developer.mozilla.org/en-US/docs/Web/API/History/state https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onpopstate history——ReactRouter https://github.com/ReactTraining/history https://gitee.com/mirrors/history/tree/v4 https://gitee.com/mirrors/history/tree/v4/docs https://github.com/mjackson/value-equal#readme https://github.com/mjackson/resolve-pathname#readme history——VueRouter https://github.com/vuejs/vue-router https://gitee.com/mirrors/vue-router VueRouter: https://router.vuejs.org/zh/

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-03-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 WebJ2EE 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档