我有一碗酒,可以慰风尘
前端路由定义
在SPA中,路由指的是URL与UI之间的映射,这种映射是单向的,即URL变化引起UI更新(无需刷新界面)
实现前端路由
实现前端路由,需要解决两个核心问题
- 如何改变URL却不引起页面刷新
- 如何检测URL变化了
vue-router里面有hash和history两种方式,下面介绍一下这两种方式
hash实现
hash指的是URL中hash(#)及后面的那一part,改变URL中的hash部分不会引起页面刷新,并且可以通过hashchange事件监听URL的变化。改变URL的方式有如下几种:
- 通过浏览器的前进后退改变URL
- 通过<a>标签改变URL
- 通过window.location改变URL
history实现
HTML5中的history提供了pushState和replaceState两个方法,这两个方法改变URL的path部分不会引起页面刷新
history提供类似hashchange事件的popstate事件,但又有不同之处
- 通过浏览器前进后退改变URL是会触发popstate事件
- 通过pushState/replaceState或者<a>标签改变URL不会触发popstate事件
- 我们可以拦截pushState/replaceState的调用和<a>标签的点击事件来检测URL的变化
- 通过js调用history的back,go,forward方法来触发该事件
实操js实现前端路由
基于hash
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title></title>
</head>
<body>
<ul>
<li><a href="#/home">home</a></li>
<li><a href="#/about">about</a></li>
</ul>
<!-- 路由渲染口子 -->
<div id="routerView"></div>
</body>
<script>
let routerView = document.getElementById('routerView')
window.addEventListener('hashchange', () => {
const hash = location.hash
routerView.innerHTML = hash
})
window.addEventListener('DOMContentLoaded', () => {
if (!location.hash) {
location.hash = "/"
} else {
const hash = location.hash
routerView.innerHTML = hash
}
})
</script>
</html>
效果图
上面的代码干了哪些活?
- 通过<a>标签的href属性来改变URL中的hash值
- 监听了hashchange事件,当事件触发的时候,改变routerView中的内容
- 监听了DOMContentLoaded事件,初次的时候需要渲染成对应的内容
基于HTML5 history实现
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title></title>
</head>
<body>
<ul>
<li><a href="#/home">home</a></li>
<li><a href="#/about">about</a></li>
</ul>
<!-- 路由渲染口子 -->
<div id="routerView"></div>
<script>
const router = document.getElementById('routerView')
window.addEventListener('DOMContentLoaded', () => {
const linkList = document.querySelectorAll('a[href]')
linkList.forEach(el => el.addEventListener('click', function(e) {
e.preventDefault()
history.pushState(null, '', el.getAttribute('href'))
router.innerHTML = location.pathname
}))
})
window.addEventListener('popstate', () => {
router.innerHTML = location.pathname
})
</script>
</body>
</html>
- 我们通过a标签的href属性来改变URL的path值(当然,你触发浏览器的前进后退按钮也可以,或者在控制台输入history.go,back,forward赋值来触发popState事件)。这里需要注意的就是,当改变path值时,默认会触发页面的跳转,所以需要拦截 <a> 标签点击事件默认行为, 点击时使用 pushState 修改 URL并更新手动 UI,从而实现点击链接更新 URL 和 UI 的效果。
- 我们监听popState事件。一旦事件触发,就改变routerView的内容
vue 中vue-router
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import About from "../views/About.vue"
Vue.use(VueRouter)
const routes = [
{
path: '/home',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
}
]
const router = new VueRouter({
mode:"history",
routes
})
export default router
// App.vue
<template>
<div id="app">
<div id="nav">
<router-link to="/home">Home</router-link> |
<router-link to="/about">About</router-link>
</div>
<router-view/>
</div>
</template>
截图如下
改造vue-router文件
import Vue from 'vue'
// import VueRouter from 'vue-router'
import VueRouter from './myVueRouter'
import Home from '../views/Home.vue'
import About from "../views/About.vue"
Vue.use(VueRouter)
const routes = [
{
path: '/home',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
}
]
const router = new VueRouter({
mode:"history",
routes
})
export default router
分析vue-router文件干了啥
1, 通过import VueRouter from 'vue-router' 引入了VueRouter
2,const router = new VueRouter({})
3,Vue.use(VueRouter)使得每个组件都可以拥有router实例
- 通过new VueRouter({})获得实例,也就是说VueRouter其实是一个类
class VueRouter {
}
- 使用Vue.use(),而Vue.use其实就是执行对象的install这个方法
class VueRouter {
}
VueRouter.install = function () {
}
export default VueRouter
分析Vue.use
Vue.use(plugin)
1,参数
{ object | Function } plugin
2,用法
安装Vue.js插件。如果插件是一个对象,必须提供install方法。如果插件是一个函数,它会被作为install方法。调用install方法时,会将Vue作为参数传入。install方法被同一个插件多次调用时,插件也只会被安装一次。
3, 作用
注册插件,此时只需要调用install方法并将Vue作为参数传入即可。但在细节上有两部分逻辑要处理:
1、插件的类型,可以是install方法,也可以是一个包含install方法的对象。
2、插件只能被安装一次,保证插件列表中不能有重复的插件。
4,实现
Vue.use = function(plugin) {
const installPlugins = (this._installedPlugins || (this._installedPlugins = []))
if (installPlugins.indexOf(plugin) > -1) {
return
}
const args = toArray(arguments, 1)
args.unshift(this)
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
plugin.apply(null, plugin, args)
}
installPlugins.push(plugin)
return this
}
1、在Vue.js上新增了use方法,并接收一个参数plugin。
2、首先判断插件是不是已经别注册过,如果被注册过,则直接终止方法执行,此时只需要使用indexOf方法即可。
3、toArray方法我们在就是将类数组转成真正的数组。使用toArray方法得到arguments。除了第一个参数之外,剩余的所有参数将得到的列表赋值给args,然后将Vue添加到args列表的最前面。这样做的目的是保证install方法被执行时第一个参数是Vue,其余参数是注册插件时传入的参数。
4、由于plugin参数支持对象和函数类型,所以通过判断plugin.install和plugin哪个是函数,即可知用户使用哪种方式祖册的插件,然后执行用户编写的插件并将args作为参数传入。
5、最后,将插件添加到installedPlugins中,保证相同的插件不会反复被注册。
了解以上开始写myVueRouter.js
let Vue = null
class VueRouter {
}
VueRouter.install = function (v) {
Vue = v
Vue.component('router-link',{
render(h) {
return h('a', {}, 'Home')
}
})
Vue.component('router-view', {
render(h) {
return h('h1', {}, '我是首页')
}
})
}
export default VueRouter
简易版果然跑起来了,截图如下
完善install方法
install 是给每个vue实例添加东西的,在router中给每个组件添加了$route和$router
这俩的区别是:
$router是VueRouter的实例对象,$route是当前路由对象,也就是说$route是$router的一个属性
// main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
new Vue({
router,
render: h => h(App),
}).$mount('#app')
我们可以发现这里只是将router ,也就是./router导出的router实例,作为Vue 参数的一部分。
但是这里就是有一个问题咯,这里的Vue 是根组件啊。也就是说目前只有根组件有这个router值,而其他组件是还没有的,所以我们需要让其他组件也拥有这个router。
因此,install方法我们可以这样完善
let Vue = null
class VueRouter {
}
VueRouter.install = function (v) {
Vue = v
Vue.mixin({
beforeCreate() {
if (this.$options && this.$options.router) { // 如果是根组件
this._root = this // 把当前实例挂载到_root上
this._router = this.$options.router
} else {
this._root = this.$parent && this.$parent._root
}
Object.defineProperty(this, '$router', {
get () {
return this._root._router
}
})
}
})
Vue.component('router-link',{
render(h) {
return h('a', {}, 'Home')
}
})
Vue.component('router-view', {
render(h) {
return h('h1', {}, '我是首页')
}
})
}
export default VueRouter
一通操作之下的解释
- 参数Vue,我们在分析Vue.use的时候,再执行install的时候,将Vue作为参数传进去。,
- mixin的作用是将mixin的内容混合到Vue的初始参数options中。相信使用vue的同学应该使用过mixin了。
- 为什么是beforeCreate而不是created呢?因为如果是在created操作的话,$options已经初始化好了。
- 如果判断当前组件是根组件的话,就将我们传入的router和_root挂在到根组件实例上。
- 如果判断当前组件是子组件的话,就将我们_root根组件挂载到子组件。注意是引用的复制,因此每个组件都拥有了同一个_root根组件挂载在它身上。
? 为啥判断是子组件就直接取父组件的_root根组件呢
来,看一下父子组件执行顺序先
父beforeCreate-> 父created -> 父beforeMounte -> 子beforeCreate ->子create ->子beforeMount ->子 mounted -> 父mounted
在执行子组件的beforeCreate的时候,父组件已经执行完beforeCreate了,拿到_root那就没问题了
在vueRouter文件中
const router = new VueRouter({
mode:"history",
routes
})
我们传了两个参数,一个模式mode,一个是路由数组routes
let Vue = null
class VueRouter {
constructor (options) {
this.mode = options.mode || 'hash'
this.routes = options.routes || []
this.routesMap = this.createMap(this.routes)
}
createMap (routes) {
return routes.reduce((pre, current) => {
pre[current.path] = current.component
return pre
}, {})
}
}
VueRouter.install = function (v) {
Vue = v
Vue.mixin({
beforeCreate() {
if (this.$options && this.$options.router) { // 如果是根组件
this._root = this // 把当前实例挂载到_root上
this._router = this.$options.router
} else {
this._root = this.$parent && this.$parent._root
}
Object.defineProperty(this, '$router', {
get () {
return this._root._router
}
})
}
})
Vue.component('router-link',{
render(h) {
return h('a', {}, 'Home')
}
})
Vue.component('router-view', {
render(h) {
return h('h1', {}, '我是首页')
}
})
}
export default VueRouter
路由中需要存放当前的路径,来表示当前的路径状态,为了方便管理,用一个对象来表示。初始化的时候判断是那种模式,并将当前的路径保存到current中
let Vue = null
class HistoryRoute {
constructor () {
this.current = null
}
}
class VueRouter {
constructor (options) {
this.mode = options.mode || 'hash'
this.routes = options.routes || []
this.routesMap = this.createMap(this.routes)
this.history = new HistoryRoute()
this.init()
}
init () {
if (this.mode === 'hash') {
// 先判断用户打开是是否有hash值,没有跳转到#/
location.hash ? '' : location.hash = '/'
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('load', () => {
this.history.current = location.pathname
})
window.addEventListener('popstate', () => {
this.history.current = location.pathname
})
}
}
createMap (routes) {
return routes.reduce((pre, current) => {
pre[current.path] = current.component
return pre
}, {})
}
}
VueRouter.install = function (v) {
Vue = v
Vue.mixin({
beforeCreate() {
if (this.$options && this.$options.router) { // 如果是根组件
this._root = this // 把当前实例挂载到_root上
this._router = this.$options.router
} else {
this._root = this.$parent && this.$parent._root
}
Object.defineProperty(this, '$router', {
get () {
return this._root._router
}
})
}
})
Vue.component('router-link',{
render(h) {
return h('a', {}, 'Home')
}
})
Vue.component('router-view', {
render(h) {
return h('h1', {}, '我是首页')
}
})
}
export default VueRouter
完善$route
其实/$route就是获取当前的路径
VueRouter.install = function (v) {
Vue = v
Vue.mixin({
beforeCreate() {
if (this.$options && this.$options.router) { // 如果是根组件
this._root = this // 把当前实例挂载到_root上
this._router = this.$options.router
} else {
this._root = this.$parent && this.$parent._root
}
Object.defineProperty(this, '$router', {
get () {
return this._root._router
}
})
Object.defineProperty(this, '$route', {
get () {
return this._root._router.history.current
}
})
}
})
Vue.component('router-link',{
render(h) {
return h('a', {}, 'Home')
}
})
Vue.component('router-view', {
render(h) {
return h('h1', {}, '我是首页')
}
})
}
完善router-view
Vue.component('router-view', {
render(h) {
let current = this._self._root._router.history.current
let routeMap = this._self._root._router.routesMap
return h(routeMap[current])
}
})
render函数里的this指向的是一个Proxy代理对象,代理Vue组件,而我们前面讲到每个组件都有一个_root属性指向根组件,根组件上有_router这个路由实例。
所以我们可以从router实例上获得路由表,也可以获得当前路径。
然后再把获得的组件放到h()里进行渲染。
现在已经实现了router-view组件的渲染,但是有一个问题,就是你改变路径,视图是没有重新渲染的,所以需要将_router.history进行响应式化。
VueRouter.install = function (v) {
Vue = v
Vue.mixin({
beforeCreate() {
if (this.$options && this.$options.router) { // 如果是根组件
this._root = this // 把当前实例挂载到_root上
this._router = this.$options.router
// 新增
Vue.util.defineReactive(this, 'xxx', this._router.history)
} else {
this._root = this.$parent && this.$parent._root
}
Object.defineProperty(this, '$router', {
get () {
return this._root._router
}
})
Object.defineProperty(this, '$route', {
get () {
return this._root._router.history.current
}
})
}
})
Vue.component('router-link',{
render(h) {
return h('a', {}, 'Home')
}
})
Vue.component('router-view', {
render(h) {
let current = this._self._root._router.history.current
let routeMap = this._self._root._router.routesMap
return h(routeMap[current])
}
})
}
效果如下
完善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}}, this.$slots.default)
}
})
截图如下
myVueRouter.js完整代码
let Vue = null
class HistoryRoute {
constructor () {
this.current = null
}
}
class VueRouter {
constructor (options) {
this.mode = options.mode || 'hash'
this.routes = options.routes || []
this.routesMap = this.createMap(this.routes)
this.history = new HistoryRoute()
this.init()
}
init () {
if (this.mode === 'hash') {
// 先判断用户打开是是否有hash值,没有跳转到#/
location.hash ? '' : location.hash = '/'
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('load', () => {
this.history.current = location.pathname
})
window.addEventListener('popstate', () => {
this.history.current = location.pathname
})
}
}
createMap (routes) {
return routes.reduce((pre, current) => {
pre[current.path] = current.component
return pre
}, {})
}
}
VueRouter.install = function (v) {
Vue = v
Vue.mixin({
beforeCreate() {
if (this.$options && this.$options.router) { // 如果是根组件
this._root = this // 把当前实例挂载到_root上
this._router = this.$options.router
// 新增
Vue.util.defineReactive(this, 'xxx', this._router.history)
} else {
this._root = this.$parent && this.$parent._root
}
Object.defineProperty(this, '$router', {
get () {
return this._root._router
}
})
Object.defineProperty(this, '$route', {
get () {
return this._root._router.history.current
}
})
}
})
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}}, this.$slots.default)
}
})
Vue.component('router-view', {
render(h) {
let current = this._self._root._router.history.current
let routeMap = this._self._root._router.routesMap
return h(routeMap[current])
}
})
}
export default VueRouter