路由Vue-router 及 异步组件

139 阅读7分钟

Vue 异步组件

  • defineAsyncComponent 函数
import { defineAsyncComponent } from "vue";
const AsyncComp = defineAsyncComponent(
	() =>
		new Promise((res, rej) => {
			res({
				template: "<div>hello</div>",
			});
		})
);

/* *** */
import { defineAsyncComponent } from "vue";
const LoginPopup = defineAsyncComponent(() => import("./LoginPopup.vue"));

/* *** */
const asyncPopup = defineAsyncComponent({
	loader: () => import("./LoginPopup.vue"),
	// 加载异步组件时要调用的组件
	loadingComponent: LoadingComponent,
	// 加载失败时的组件
	errorComponent: errorComponent,
	delay: 1000,
	timeout: 1000 * 3,
});
  • 使用 defineAsyncComponent 函数,可以定义一个异步组件,当组件加载完成时,会自动渲染组件.实现异步加载组件
  • 如何与异步的setup 函数结合使用
/* Vue3 中使用 */
<template>
    <Suspense>
        <login-popup />
        <template #fallback>
            <div>loading...</div>
        </template>
    </Suspense>
</template>;

const LoginPopup = defineAsyncComponent(() => import("./LoginPopup.vue"));

const getArticleInfo = async () => {
    await new Promise((res) => setTimeout(res, 2000));
    const article = {
        title: "xxx",
    };
    return article;
};
export default {
    async setup() {
        const article = await getArticleInfo();
        return {
            article,
        }
    },
};

vue-router

  • 什么是前端路由?
    • 在 SPA 应用,描述的是 URL 和 UI 的映射关系,这种映射是单向的,即 URL 变,UI 变(无需刷新页面)
  • 前端路由原理
    • 如何改变 URL,不引起页面刷新
    • 如何检测 URL 变化,同时触发 UI 更新

壹. 如何实现前端路由

<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>Router</title>
	</head>
	<body>
		<!-- <h1>hash 路由</h1>
		<ul>
			<li><a href="#/home">home</a></li>
			<li><a href="#/about">about</a></li>
			<li><a href="#/user">user</a></li>
		</ul> -->
		<h1>history 路由</h1>
		<ul>
			<li><a href="/home">home</a></li>
			<li><a href="/about">about</a></li>
			<li><a href="/user">user</a></li>
		</ul>
		<div id="routerView">routerView</div>
	</body>
	<!-- <script>
		/**
		 * @Author: 花生
		 * @description:基于 hash 实现前端路由
		 * @return {*}
		 */
		let routerView = document.getElementById("routerView");

		window.addEventListener("hashchange", () => {
			console.log("hashchange", location.hash);
			let hashStr = window.location.hash.slice(1);
			routerView.innerHTML = hashStr; // 更新视图
		});
		window.addEventListener("DOMContentLoaded", () => {
			if (!location.hash) {
				location.hash = "#/home"; // 默认路由为 home
			} else {
				let hashStr = window.location.hash.slice(1);
				routerView.innerHTML = hashStr;
			}
		});
	</script> -->
	<script>
		/**
		 * @Author: 花生
		 * @description: 基于 history 实现前端路由
		 * @return {*}
		 */
		let routerView = document.getElementById("routerView");

		window.addEventListener("popstate", () => {
			console.log("popstate", location.pathname);
			let hash = location.hash.slice(1);
			routerView.innerHTML = location.pathname; // 更新视图
		});
		window.addEventListener("DOMContentLoaded", () => {
			routerView.innerHTML = location.pathname; // 更新视图
			var linkList = document.querySelectorAll("a[href]"); // 获取所有链接
			// 遍历所有链接,给每个链接添加点击事件
			for (var i = 0; i < linkList.length; i++) {
				linkList[i].onclick = function (e) {
					e.preventDefault(); // 阻止默认行为
					var href = this.getAttribute("href");
					history.pushState({}, "", href); // 更新浏览器地址栏
					routerView.innerHTML = href; // 更新视图
					console.log("href", href); // 打印当前路由
				};
			}
		});
	</script>
</html>

贰. vue-router 的使用

Vue Router

实现一个 vue-router

  • 分析 vue-router 的实现原理

    • 首先是一个 class
    • 其次是一个 vue 插件
  • 实现 Vue.use(router)

    • 需要判断插件是否已经注册过,如果已经注册过,则直接返回终止方法执行
    • 最后需要将插件加入到 installedPlugins 中,保证插件不会被重复注册
    • $route$router: $routerVueRouter的实例对象,$route是当前路由对象;即$route$routercurrent属性。注意每个组件
  • 实现 install 方法

    • 就是给 Vue 实例添加一些属性或方法
  • 完善 VueRouter 类

    • $route$router
    • 路由的映射表
  • 实现 $router

    • 我们通过defineReactive实现响应式更新,当history更改的时候,自动触发视图的修改,进而完成路由切换
    • johniexu.github.io/xx-blog/%E6…
  • VueRouter实现:

let Vue = null;

// 定义一个历史记录类
class HistoryRoute {
  constructor(router) {
    this.current = null; // 当前路由
    // this.router = router; // 路由实例
  }
}

// 定义一个VueRouter类
class VueRouter {
  constructor(options) {
    this.mode = options.mode || 'hash'; // 默认使用 hash 模式
    this.routes = options.routes || []; // 传递的是一个数组表示的路由表
    this.routesMap = this.createMap(this.routes); // 创建路由映射表
    this.history = new HistoryRoute(this); // 创建历史记录对象
    this.init(); // 初始化路由
  }

  createMap(routes) {
    return routes.reduce((memo, route) => {
      memo[route.path] = route; // 将路由路径作为键,路由对象作为值
      return memo;
    }, {});
  }

  init() {
    if (this.mode === 'hash') {
      // 先判断用户打开的时候,有没有 hash 值,如果有就设置当前路由为 hash 值,没有就设置为根路径 '/'
      // 监听 hashchange 事件,更新当前路由
      location.hash ? '' : (location.hash = '/');
      // 监听 load 事件,更新当前路由. 这里的 load 事件是指页面加载完成后触发,而不是指路由变化后触发
      window.addEventListener('load', () => {
        this.history.current = location.hash.slice(1);
      });
      window.addEventListener('hashchange', () => {
        this.history.current = location.hash.slice(1);
      });
    } else {
      location.pathname ? '' : (location.pathname = '/');
      window.addEventListener('DOMContentLoaded', () => {
        this.history.current = location.pathname;
      });
      // 监听 popstate 事件,更新当前路由
      window.addEventListener('popstate', () => {
        this.history.current = location.pathname;
      });
    }
  }
}

VueRouter.install = function (v) {
  Vue = v;

  Vue.mixin({
    beforeCreate() {
      if (this.$options && this.$options.router) {
        this._root = this; // 把当前实例挂载到 _root (根)上
        this._router = this.$options.router; // 将 router 实例挂载到当前实例上

        Vue.util.defineReactive(this, 'current', this._router.history); // 将当前路由对象设置为响应式

        /**
         * 1. 监听路由变化
         * 2. 根据当前路由,渲染对应的组件
         * 当我们第一次渲染 router-view的时候,可以获取到 this._router.history 对象,从而就会被监听到
         * 就会把 router-view 组件的依赖 watcher 收集到 this._router.history 对象的 deps 中
         * 这样当路由变化时,就会触发 this._router.history 的 notify 方法,从而触发 watcher 的 update 方法,从而重新渲染 router-view 组件
         *  */
      } else {
        this._root = this.$parent && this.$parent._root; // 如果没有 router 实例,则将根实例赋值给 _root
      }

      // 返回 $router 对象
      Object.defineProperty(this, '$router', {
        get() {
          return this._root._router; // 获取当前路由实例
        },
      });
      // 返回 $route 对象
      Object.defineProperty(this, '$route', {
        get() {
          return this._root._router.history.current; // 获取当前路由
        },
      });
    },
  });

  // router-link 和 router-view 组件的实现
  // router-link 组件用于生成路由链接,点击链接时,会更新当前路由
  Vue.component('router-link', {
    props: {
      to: String, // 路由路径
    },
    render(h) {
      let mode = this._self._root._router.mode;
      let to = mode === 'hash' ? `#${this.to}` : this.to; // 根据路由模式生成链接
      return h(
        'a',
        {
          attrs: { href: to },
          on: {
            click: (e) => {
              e.preventDefault(); // 阻止默认行为
              this._self._root._router.history.current = this.to; // 更新当前路由
              // if (mode === 'hash') {
              //   location.hash = this.to; // 如果是 hash 模式,则更新 hash
              // } else {
              //   history.pushState({}, '', this.to); // 如果是 history 模式,则使用 pushState 更新路径
              // }
            },
          },
        },
        this.$slots.default
      ); // 渲染一个链接
    },
  });

  // router-view 组件用于渲染匹配到的组件
  Vue.component('router-view', {
    render(h) {
      // render 函数中的 this 指向的是一个 Proxy 对象,代理当前 Vue 组件
      let current = this._self._root._router.history.current; // 获取当前路由
      let routerMap = this._self._root._router.routesMap; // 获取路由映射表
      let matchedComponent = routerMap[current]?.component; // 获取当前路由对应的组件
      return matchedComponent ? h(matchedComponent) : null; // 如果有对应的组件,则渲染该组件,否则不渲染
    },
  });
};

export default VueRouter;
   

面试题

  1. 路由的 hash 和 history 模式的区别 Vue-Router 有两种模式:hash 模式和 history 模式。默认的路由模式是 hash 模式。
  • hash 模式:
  • hash 模式是开发中默认的模式,它的 URL 带着一个#,例如:http://www.abc.com/#/vue,它的 hash 值就是#/vue
  • hash 值会出现在 URL 里面,但是不会出现在 HTTP 请求中,对后端完全没有影响。所以改变 hash 值,不会重新加载页面。这种模式的浏览器支持度很好,低版本的 IE 浏览器也支持这种模式。hash 路由被称为是前端路由,已经成为 SPA(单页面应用)的标配。
  • 原理:hash 模式的主要原理就是onhashchange()事件
window.onhashchange = function (event) {
	console.log(event.oldURL, event.newURL);
	let hash = location.hash.slice(1); // 获取 hash 值
	document.body.style.color = hash; // 改变 body 颜色
};
  • 使用 onhashchange()事件的好处就是,在页面的 hash 值发生变化时,无需向后端发起请求,window 就可以监听事件的改变,并按规则加载相应的代码。除此之外,hash 值变化对应的 URL 都会被浏览器记录下来,这样浏览器就能实现页面的前进和后退。虽然是没有请求后端服务器,但是页面的 hash 值和对应的 URL 关联起来了。

  • history 模式:

  • history 模式的 URL 中没有#,它使用的是传统的路由分发模式,即用户在输入一个 URL 时,服务器会接收这个请求,并解析这个 URL,然后做出相应的逻辑处理。

  • 当使用 history 模式时,URL 就像这样:http://abc.com/user/id。相比 hash 模式更加好看。但是,history 模式需要后台配置支持。如果后台没有正确配置,访问时会返回 404。

  • API:history api 可以分为两大部分,切换历史状态和修改历史状态:

    • 修改历史状态:包括了 HTML5 History Interface 中新增的pushState()replaceState()方法,这两个方法应用于浏览器的历史记录栈,提供了对历史记录进行修改的功能。只是当他们进行修改时,虽然修改了 url,但浏览器不会立即向后端发送请求。如果要做到改变 url 但又不刷新页面的效果,就需要前端用上这两个 API。
    • 切换历史状态:包括forward()back()go()三个方法,对应浏览器的前进,后退,跳转操作。
  • 虽然 history 模式丢弃了丑陋的#。但是,它也有自己的缺点,就是在刷新页面的时候,如果没有相应的路由或资源,就会刷出 404 来。

  • 如果想要切换到 history 模式,就要进行以下配置(后端也要进行配置):

const router = new VueRouter({
  mode:'history',
  routes: [...]
})
  • 两种模式对比:
  • 调用 history.pushState() 相比于直接修改hash,存在以下优势:
    • pushState() 设置的新 URL 可以是与当前 URL 同源的任意 URL;而 hash 只可修改 # 后面的部分,因此只能设置与当前 URL 同文档的 URL;
    • pushState() 设置的新 URL 可以与当前 URL 一模一样,这样也会把记录添加到栈中;而 hash 设置的新值必须与原来不一样才会触发动作将记录添加到栈中;
    • pushState() 通过 stateObject 参数可以添加任意类型的数据到记录中;而 hash 只可添加短字符串;
    • pushState() 可额外设置 title 属性供后续使用。
    • hash 模式下,仅 hash 符号之前的 url 会被包含在请求中,后端如果没有做到对路由的全覆盖,也不会返回 404 错误;history 模式下,前端的 url 必须和实际向后端发起请求的 url 一致,如果没有对用的路由处理,将返回 404 错误。
  • hash 模式和 history 模式都有各自的优势和缺陷,还是要根据实际情况选择性的使用。
  1. 如何获取页面的 hash 变化
  • 监听$route的变化
// 当路由发生变化的时候执行
watch:{
	$route:{
		handler(val,oldVal){
		  console.info(val,oldVal)
		}
	},
	deep:true
}
  • window.location.hash读取 hash 值,window.location.hash = xxx设置 hash 值。window.location.hash 的值可读可写,读取来判断状态是否改变,写入时可以在不重载网页的前提下,添加一条历史访问记录。
  1. routerouter的区别
  • route是“路由信息对象”,包括path,params,hash,query,fullPath,matched,name等路由信息参数。
  • router是“路由实例对象”,包括了路由的跳转方法(pushreplace)、钩子函数等。