前端路由原理
-
createRouter
* hash * window.addEventListener('hashChange') * 两种实现路由切换的模式:UI组件(router-link,router-view),Api(push()方法) * history * HTML5新增的API ,可以通过不刷新页面前提下,修改页面路由-history.pushState()
-
useRouter
-
router-link
-
router-view
手写实现一个路由跳转
/**
* mini 版本 vue-router
*/
import { ref, provide, inject } from 'vue';
import RouterLink from './router-link.vue';
import RouterView from './router-view.vue'
// 保证全局唯一性,可以用 Symbol 创建
const ROUTER_KEY = '__router__'
//
class Router {
constructor(options) {
// 记录访问历史
this.history = options.history;
// 初始化传入路由表
this.routes = options.routes;
// 当前激活的路由
this.current = ref(this.history.url)
// hashChange 事件触发把当前激活路由的值记录下来
this.history.bindEvents(() => {
this.current.value = window.location.hash.slice(1)
})
}
push(to) {
location.hash = '#' + to;
window.history.pushState({}, '', to)
}
beforeEach(fn) {
this.guards = fn;
}
// app 插件的注册调用函数
// provie/inject,pinia
install(app) {
app.provide(ROUTER_KEY, this)
// 兼容 options API
app.$router = this;
// 注册全局组件
app.component('router-link', RouterLink)
app.component('router-view', RouterView)
}
}
// 1. hash
// hashChange -> View
function createWebHashHistory() {
function bindEvents(fn) {
window.addEventListener('hashchange', fn)
}
return {
bindEvents,
url: window.location.hash.slice(1) || '/'
}
}
// TODO
function createWebHistory() {
}
// 组合式 API,获取当前 vue-router 的实例
// options API, this.$router 来获取vue-router 的实例
function useRouter() {
return inject(ROUTER_KEY)
}
// 暴露一个创建对应类的实例的方法
function createRouter(options) {
return new Router(options)
}
export { createRouter, useRouter, createWebHashHistory }
router-link
<template>
<a :href="'#' + props.to">
<slot />
</a>
</template>
<script setup>
// a -> href
// router-link to
import { defineProps } from 'vue';
let props = defineProps({
to: { type: String, required: true }
})
</script>
<style lang="css" scoped></style>
router-view
<template>
<!-- 动态组件 -->
<component :is="comp"></component>
</template>
<script setup>
import { computed } from 'vue'
import { useRouter } from './mini-router'
// 获取到了 Router 的实例
let router = useRouter();
console.log('router 实例', router)
const comp = computed(() => {
// 根据注册的路由表和当前激活的 route 匹配
const route = router.routes.find(route => {
// 百分百等于,静态路由
return route.path === router.current.value
})
// 路由守卫的激活
const ret = route?.guards
return route.component;
})
</script>
<style lang="scss" scoped></style>
特性原理解析
-
路由匹配规则:静态路由、动态路由、正则匹配
const router = new VueRouter({ routes: [ // 动态路径参数 以冒号开头 { path: '/user/:id', component: User } ] })
-
嵌套路由:
const router = new VueRouter({ routes: [ { path: '/user/:id', component: User, children: [ { // 当 /user/:id/profile 匹配成功, // UserProfile 会被渲染在 User 的 <router-view> 中 path: 'profile', component: UserProfile }, { // 当 /user/:id/posts 匹配成功 // UserPosts 会被渲染在 User 的 <router-view> 中 path: 'posts', component: UserPosts } ] } ] })
-
路由守卫:
-
路由元信息:路由表中配置meta字段
-
滚动行为记录:这个功能只在支持 history.pushState 的浏览器中可用。
const router = new VueRouter({
routes: [...],
scrollBehavior (to, from, savedPosition) {
// return 期望滚动到哪个的位置
}
})
- 路由懒加载和异步组件
const Foo = () => import('./Foo.vue')
把组件按组分块
const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue')
const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')
- 过渡动效
<transition>
<router-view></router-view>
</transition>
<!-- 使用动态的 transition name -->
<transition :name="transitionName">
<router-view></router-view>
</transition>
内置组件
- 异步组件 defineAsyncComponent 、 <component :is=“xxx” / >
import { defineAsyncComponent } from 'vue'
const AsyncHelloWorld = defineAsyncComponent({
//es6 import
loader:()=>import('xxx.vue'),
loadingComponent:LoadingComp,
delay:100,
timeout:300,
errorComponent:xxx
})
异步组件的实现
// 异步组件的实现
// options = object {
// loader: () => Promoise(void)
// }
// options
function defineAsyncComponent(options) {
if (typeof options === 'function') {
options = {
loader: options,
};
}
const { loader } = options;
let InnerComp = null;
// 记录重试次数
let retries = 0;
// 封装 load 函数用来加载异步组件
function load() {
return (
loader()
// 捕获加载器的错误
.catch((err) => {
// 如果用户指定了 onError 回调,则将控制权交给用户
if (options.onError) {
// 返回一个新的 Promise 实例
return new Promise((resolve, reject) => {
// 重试方法
const retry = () => {
resolve(load());
retries++;
};
// 失败
const fail = () => reject(err);
// 作为 onError 回调函数的参数,让用户来决定下一步怎么做
options.onError(retry, fail, retries);
});
} else {
throw error;
}
})
);
};
// 创建一个 vue 组件
return {
name: 'AsyncComponentWrapper',
setup() {
// 标识异步组件是否加载成功
const loaded = ref(false);
const error = shallowRef(null);
const loading = ref(false);
// timeout 默认不超时
const timeout = ref(false);
let loadingTimer = null;
if (options.delay) { // 100ms
loadingTimer = setTimeout(() => {
loading.value = true;
}, options.delay);
} else {
loading.value = true;
}
let timer = null
// 用户配置参数 timeout
if(options.timeout) {
timer = setTimeout(() => {
timeout.value = true;
}, options.timeout);
}
onUmounted(() => clearTimeout(timer))
// 调用 load 函数加载组件
// import(), ES6
load()
.then((c) => {
InnerComp = c;
loaded.value = true;
})
.catch((err) => {
error.value = err;
})
.finally(() => {
loading.value = false;
clearTimeout(loadingTimer);
});
// 占位内容...
const Placeholer = { type: Text, children: '' }
if(loaded.vlaue) {
// 异步组价加载成功,渲染 InnerComp,否则渲染渲染出错组件
return {
type: InnerComp,
}
} else if(timeout.value && options.errorComponent) {
// 超时,并且设置了 Error 组件
return {
type: options.errorComponent,
}
} else if(error.value && options.errorComponent) {
return {
type: options.errorComponent,
}
}
return Placeholer
},
};
}
// load 函数接收一个 onError 回调函数
function load(onError) {
// 请求接口,得到 Promise 实例
const p = fetch(100);
// 捕获错误
return p.catch((err) => {
// 当错误发生时,返回一个新的 Promise 实例,并调用 onError 回调,
// 同时将 retry 函数作为 onError 回调的参数
return new Promise((resolve, reject) => {
// retry 函数,用来执行重试的函数,执行该函数会重新调用 load 函数并发送请求
const retry = () => resolve(load(onError));
const fail = () => reject(err);
onError(retry, fail);
});
});
}
function fetch(ms) {
return new Promise((resolve, reject) => {
// 请求会在 秒后失败
setTimeout(() => {
reject('err');
}, ms);
});
}
- KeepAlive 组件
- Teleport 组件
<Teleport to="body">
<div v-if="open" class="modal">
<p>Hello from the modal!</p>
<button @click="open = false">Close</button>
</div>
</Teleport>
- Transition 组件
< Transition> 会在一个元素或组件进入和离开 DOM 时应用动画
<Transition name="fade">
...
</Transition>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
使用场景
- keep-alive,多标签页交互,多 tab 切换
- teleport,全局弹窗,dom 结构脱离组件树渲染
- transition,实现组件过渡动画
- defineAsyncComponent,声明一个异步组件,实现性能优化和分 chunk 打包