前端路由原理
-
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 APIapp.$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?.guardsreturn 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 importloader:()=>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// 用戶配置參數 timeoutif(options.timeout) {timer = setTimeout(() => {timeout.value = true;}, options.timeout);}onUmounted(() => clearTimeout(timer))// 調用 load 函數加載組件// import(), ES6load().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 打包