本文主要讨论最新版的vue-router(支持Vue3),主要从其出现原因、使用方式、自己实现一个玩具vue-router以及vue-router源码分析这几个角度去讨论,希望你喜欢。
在JQuery时代,多数Web项目的路由都是由后端实现的,用户访问网页时的常见流程为:
1.用户访问路由
2.后端获得路由并匹配相应的模板
3.后端动态渲染模板并返回
4.浏览器加载返回模板中的JS、CSS
前端人员的开发多数都受限于后端框架提供的模板语言,用户每次页面跳转,都由后端动态渲染出相应的模板并返回。
这种项目开发方式有很多缺点:
前后端无法分离
页面调整需要刷新整个页面
等待时间较长、交互体验下降
当Ajax出来后,前后端可以分离了,前端工程师尝试利用JS来构建路由系统,用户访问某路由时,利用Ajax去动态获取数据,再利用JS去动态生产换网页页面,这样页面不需要全局刷新,用户浏览网页的体验也更好了一些。
这种,通过JS去控制路由,让用户一直停留在index.html上,通过JS动态根据不同路由加载不同页面元素的应用便是单页应用程序(SPA,single page application)的早期形态。
vue-router的核心原理也类似,通过JS去匹配URL,然后加载不同的页面内容。
一般,前端JS匹配路由URL有两种方式:
hash模式,通过在URL中加入#来做内容区分
history模式,这种方式的URL看起与正常的一样
在2014年之前,大家主要通过hash模式来匹配URL,其URL形式为:
http://xxx.com/#/home
当页面跳转时,URL中#后的内容变化会触发hashchange事件(这种模式称为hash模式的原因),通过对hashchange事件的监听,便可以实现对网页内容的动态渲染:
window.addEventListener('hashchange', fn)
通过window.addEventListener方法,我们实现了,当hashchange事件改变时,对fn方法的调用,在fn方法中,可以对URL中不同的hash值进行不同的逻辑操作,比如加载不同页面内容。
在2014后,HTML5标准发布,浏览器多了pushState与replaceState这两个API,通过这两个API,我们可以改变URL且浏览器不会向后端服务发起请求,同时还触发popstate事件,利用这两个新增的API和popstate事件,便可以设计出history模式。
已经通过window.addEventListener方法监听popstate事件,然后调用相应的方法则可。
构建一个vue3项目,然后安装vue-router@next。
yarn create vite DashboardFrameWork --template vue
yarn add vue-router@next
在src/components中创建Home.vue和About.vue这两个文件:
主页
关于页
创建src/router目录,用于存放路由相关的逻辑,在src/router目录下,创建index.js,代码如下:
// src/router/index.js
import { createRouter, createWebHashHistory} from 'vue-router'
import Home from '../components/Home.vue'
import About from '../components/About.vue'
// 路由信息
const routes = [
{
path: "/",
name: 'home',
component: Home,
},
{
path: "/about",
name: 'about',
component: About,
},
];
const router = createRouter({
history: createWebHashHistory(),
routes
})
export default router
在index.js中,定义了两条路由信息,然后通过createRouter函数定义路由对象,其中使用了history模式。
在src/main.js中,我们需要导入router对象:
// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router/index'
createApp(App).use(router).mount('#app')
在App.vue使用vue-router:
Home |
About
在App.vue的代码中,我们使用了router-link和router-view,它们是vue-router提供的子组件,之所以可以直接使用,是因为vue-router将这两个组件注册到成了全局组件,所以能在项目的任意位置直接使用。
router-link的作用类似于a标签,用于切换页面内容,相比于a标签,router-link不会让浏览器刷新整个页面。
router-view的作用主要是展示相应组件的内容,比如本例中的Home.vue和About.vue。
将项目run起来,效果如下:
image.png点击Home或About,页面内容与网页URL都会发生改变。
了解了vue-router的基本用法后,我们来手撸一个mini vue-router,来实现【简单使用vue-router】小节中类似的效果。
在src/router下创建grouter文件夹,在src/router/grouter下创建index.js文件。
回顾深入Vuex与Pinia一文中,我们开发MiniVuex的过程,我们通过provide与inject将store对象提供出去,实现跨组件的使用,而MiniVuex管理的数据则全部存储在reactive定义的_state变量中。
这里也是类似的思路,通过provide与inject将router对象提供出去,完整代码如下:
// src/router/grouter/index.js
import {ref, inject} from 'vue'
import RouterLink from './RouterLink.vue'
import RouterView from './RouterView.vue'
const ROUTER_KEY = '__router__'
function createRouter(options) {
return new Router(options)
}
function useRouter() {
return inject(ROUTER_KEY)
}
function createWebHashHistory() {
function bindEvents(fn) {
// 监听hashchange事件
window.addEventListener('hashchange', fn)
}
return {
bindEvents,
url: window.location.hash.slice(1) || '/'
}
}
class Router {
constructor(options) {
this.history = options.history
this.routes = options.routes
this.current = ref(this.history.url)
// URL改变时,bindEvent中传入的方法会被调用
this.history.bindEvents(() => {
this.current.value = window.location.hash.slice(1)
})
}
install(app) {
app.provide(ROUTER_KEY, this)
// 将router-line与router-view注册为全局组件
app.component("router-link", RouterLink)
app.component("router-view", RouterView)
}
}
export {createRouter, createWebHashHistory, useRouter}
上述代码中,在useRouter函数中使用inject,在install函数中使用了provide,当我们需要在组件的script标签中使用router时,通过useRouter获得router对象,便可以使用其中的方法。
看到Router类,在构造方法(constructor)中,我们将options的属性赋值到类对应的数据中心,这里关键的一句是this.history.bindEvents的调用:
// URL改变时,bindEvent中传入的方法会被调用
this.history.bindEvents(() => {
this.current.value = window.location.hash.slice(1)
})
bindEvents函数接收匿名方法,该匿名方法的作用便是获取当前URL中#号后的第一个元素,假设当前URL为http://localhost:3000/#/about,其效果如下:
window.location.hash
'#/about'
window.location.hash.slice(1)
'/about'
在install方法,除了使用provide方法将router对象本身提供出去外,还通过app.component方法将router-link与router-view组件注册成全局组件,这样,router-link与router-view可以在任意组件中使用。
现在我们来试一下router-link与router-view这两个组件,在src/router/grouter下创建RouterLink.vue和RouterView.vue两文件。
先开发RouterLink.vue,代码如下:
从代码可知,RouterLink的本质就是a标签,通过v-bind绑定a标签的href,通过props来接收父组件的传参,通过插槽(slot)将a标签要显示的内容交给父组件决定。
在App.vue中,使用方式为:
Home
About
其中to传入路径,会被router-link的defineProps接收到props对象中,而Home、About等内容,会在router-link的slot中显示。
接着开发RouterView.vue,代码如下:
RouterView.vue的逻辑也很简单,利用了Vue提供的动态组件(上述代码中的component标签)来实现组件的动态显示,动态组件通过is属性来指定要动态显示的组件。
在RouterView.vue的JS中,通过useRouter获得router对象,然后获取其中的routes属性并通过find方法去匹配routes属性中的元素,元素的path与router.current.value相等,则返回相应的组件。
看回src/router/grouter/index.js中的代码(createWebHashHistory函数处),当用户改变URL时,hashchange事件会被监听到,然后将当前URL中hash部分赋值给router.current.value。
至此,Mini vue-router就开发好了,使用方法与vue-router一样,在src/router/index.js中,替换一下导入内容:
import { createRouter, createWebHashHistory} from './grouter/index'
// import { createRouter, createWebHashHistory} from 'vue-router'
运行项目,可以发现,项目成功运行。
在实现这个项目的过程中,我们使用了Vue3的slot与动态组件,这些都是灵活使用Vue3开发项目所必须要掌握的技巧,后续的文章会针对两者进行更深入的剖析,这里就点到为止。
vue-router在使用时,需要在main.js中通过app.use让Vue加载vue-touer插件,在src/router.ts中,可以找到install方法的逻辑:
install(app: App) {
const router = this
// 注册RouterLink
app.component('RouterLink', RouterLink)
// 注册RouterView
app.component('RouterView', RouterView)
// 注册$router作为全局变量
app.config.globalProperties.$router = router
Object.defineProperty(app.config.globalProperties, '$route', {
enumerable: true,
get: () => unref(currentRoute),
})
// ... 省略部分逻辑
// 将router对象提供出去,让其他组件可以使用router对象
app.provide(routerKey, router)
app.provide(routeLocationKey, reactive(reactiveRoute))
app.provide(routerViewLocationKey, currentRoute)
// https://staging-cn.vuejs.org/api/application.html#app-unmount
// 卸载一个已挂载应用实例
const unmountApp = app.unmount
installedApps.add(app)
app.unmount = function () {
// ...
}
},
}
return router
}
我将install方法中的部分逻辑删除了,只看最核心的东西。
在install方法中,一开始便通过app.component注册了RouterLink和RouterView,这点与我们开发的MiniVueRouter一致。
随后,通过app.config.globalProperties将router对象注册为全局变量,以作为全局变量的开头主要是避免全局变量重名冲突。
为了让不同组件可以实验router,在install方法中,还通过app.provide将router对象提供出去,这点也与我们开发MiniVueRouter一样。
大体了解install方法的逻辑后,来看看RouterLink是如何实现的。
与MiniVueRouter中实现的方式不同,MiniVueRouter通过template实现(模板语法),而RouterLink利用了JSX来实现,相比于模板语法,JSX利用JavaScript动态化的优势可以实现更加灵活的效果,但RouterLink是对a标签封装的本质并没有改变,看到src/RouterLink.ts中setup方法中的一段逻辑,便可以知,虽然写法变了,原理依旧。
return () => {
const children = slots.default && slots.default(link)
return props.custom
? children
: h(
'a',
{
'aria-current': link.isExactActive
? props.ariaCurrentValue
: null,
href: link.href,
// this would override user added attrs but Vue will still add
// the listener so we end up triggering both
onClick: link.navigate,
class: elClass.value,
},
children
)
}
上述代码中,通过h函数生成a标签的虚拟DOM,这个便是RouterLink节点。
RouterView最核心的逻辑就是匹配出用户当前访问URL对于的组件并将该组件渲染出来,理解了这个逻辑,看源码就好理解了,不然会陷入RouterView源码细节中。
RouterView依旧使用JSX来开发,通过h函数将组件动态渲染出来,其核心逻辑在src/Router.ts中:
return () => {
const route = routeToDisplay.value
// 匹配出的路由
const matchedRoute = matchedRouteRef.value
// 路由对应的组件(即RouterView要显示的组件)
const ViewComponent = matchedRoute && matchedRoute.components[props.name]
// we need the value at the time we render because when we unmount, we
// navigated to a different location so the value is different
const currentName = props.name
// ... 省略部分逻辑
// h函数将ViewComponent渲染成虚拟DOM
const component = h(
ViewComponent,
assign({}, routeProps, attrs, {
onVnodeUnmounted,
ref: viewRef,
})
)
// ...
// 返回渲染出的组件
return (
// pass the vnode to the slot as a prop.
// h and both accept vnodes
normalizeSlot(slots.default, { Component: component, route }) ||
component
)
}
上述代码中,通过matchedRouteRef匹配出相应的路由,然后获得该路由对于的组件(matchedRoute.components),随后通过h函数将组件动态渲染成虚拟DOM并返回宣传好的组件。
分析RouterView时,需要匹配路由,而这些路由就是通过RouterView传入的,回忆一下它的用法,方便理解该方法的核心逻辑,用法如下:
// 路由信息
const routes = [
{
path: "/",
name: 'home',
component: Home,
},
{
path: "/about",
name: 'about',
component: About,
},
];
const router = createRouter({
history: createWebHashHistory(),
routes
})
看到src/router.ts中的createRouter方法,先看该方法传入参数的类型为RouterOptions:
export interface RouterOptions extends PathParserOptions {
history: RouterHistory
routes: RouteRecordRaw[]
...
嗯,与我们的用法对上了,history对于Hash路由匹配模式还是history路由匹配模式,routers则是我们传入的路由对象。
createRouter方法最终会创建router对象,该对象会有添加路由、删除路由、获得路由等方法和各种属性,相关代码字面意思很好理解,就不赘述了。
因为能力问题,vue-router源码没有分析的特别细致,但也理出了主逻辑,下次有需求时,再细细剖析吧。