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 更新
壹. 如何实现前端路由
- hash 实现
- hash 是 URL 中 hash(#)及后面的内容,常用于锚点在页面内进行导航,改变 URL 的 hash 部分不会引起页面刷新
- 改变 URL 的方式
- 通过浏览器的前进、后退改变 URL
- 通过 a 标签改变 URL
- 通过 window.location.hash = xxx 改变 URL
- history 实现
- history 提供了 pushState 和 replaceState 两个方法,这两个方法改变 URL 的 path 部分不会引起页面刷新
- history 提供了类似 hashchange 事件的 popState 事件
- popState 事件只会在浏览器前进、后退时触发,并不会在 pushState、replaceState 时触发
- 通过
pushState/replaceState或者<a>标签改变 URL 不会触发 popState。好在我们可以拦截 pushState、replaceState 的调用及标签的点击事件来检测 URL 变化,在触发这个方法时,执行 hashchange 事件处理函数 - 通过 js 调用 history 的
back,go,forward方法或浏览器的前进、后退按钮时,会触发 popState 事件
<!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 的实现原理
- 首先是一个 class
- 其次是一个 vue 插件
-
实现 Vue.use(router)
- 需要判断插件是否已经注册过,如果已经注册过,则直接返回终止方法执行
- 最后需要将插件加入到 installedPlugins 中,保证插件不会被重复注册
$route和$router:$router是VueRouter的实例对象,$route是当前路由对象;即$route是$router的current属性。注意每个组件
-
实现 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;
面试题
- 路由的 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()三个方法,对应浏览器的前进,后退,跳转操作。
- 修改历史状态:包括了 HTML5 History Interface 中新增的
-
虽然 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 模式都有各自的优势和缺陷,还是要根据实际情况选择性的使用。
- 如何获取页面的 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的值可读可写,读取来判断状态是否改变,写入时可以在不重载网页的前提下,添加一条历史访问记录。
route和router的区别
route是“路由信息对象”,包括path,params,hash,query,fullPath,matched,name等路由信息参数。router是“路由实例对象”,包括了路由的跳转方法(push、replace)、钩子函数等。