写在前面
笔记内容大多出自于拉勾教育大前端高薪训练营的教程,因此也许会和其他文章雷同较多,请担待。
hash <------> history
- hash 兼容性好,但是链接有#
- history 链接美观,没有多余的符号等,但只能 IE10 以后使用,因为其利用了 H5 的 historyAPI。而且需要配置服务器参数,例如 Nginx 需要在 location / {} 中加入 try_files $uri $uri/ /index.html;
hash
- 将/#/后的内容作为路径地址,可以直接通过 location.url 来切换浏览器中的地址,如果只改变了/#/后面的内容,是不会向服务器请求这个地址的,但是会将这个地址记录到浏览器的访问历史中
- 当 hash 改变后,需要监听 hash 的变换( hash 一旦改变就会触发 hashchange 事件 )并做相应的处理,可以在 hashchange 中获取到当前的路由地址,并且找到该路径对应的组件然后进行渲染
history
- 通过 history.pushState 方法改变地址栏,并将这个地址记录到浏览器的访问历史中,但不会真正地跳转到指定的路径
- 通过监听 popState 事件可以知道浏览器操作地址的变化,然后找到该路径对应的组件然后进行渲染
abstract
- 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式
使用
- Vue.use 注册 vue-router 插件
import VueRouter from 'vue-router'
Vue.use(VueRouter)
- 实例化 VueRouter 并传入路由数组
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue') // 懒加载动态导入
}
]
const router = new VueRouter({
mode: 'history', // 默认 hash,如果使用 history 需要指定 mode
routes
})
原理
- 使用混入给每一个 Vue 实例注册 router
- Vue.observable() 响应式 router,在页面路由切换时可以自动渲染页面
- Vue.component + 插槽 + render函数
VueRouter 构造函数
- Vue.use() 调用插件其实就是调用模块的 install 方法,但由于 VueRouter是一个类,所以需要将 install 方法变成静态方法,这样就可以在不被实例化的时候也可以调用到
class VueRouter {
options
data
routerMap
constructor(options) {}
static install(Vue) {}
init() {}
initEvent() {}
createRouterMap() {}
initComponents(Vue) {}
}
手写 VueRouter
constructor
- 从*const router = new VueRouter({ mode, routes})*中可以得知,constructor需要接收一个以对象形式的参数,并且内部有 options,data,routeMap 三个成员变量
- 从*new Vue({ router })*中可以得知,router 被放入了 Vue 实例的 $options 中
let _Vue = null
constructor(options) {
this.options = options // 内含 mode 和 routes 信息
this.routeMap = {} // 记录路由地址,以键值对存在,键是route.path,值是route.component
this.data = _Vue.observable({
current: '/'
}) // 记录当前路由地址
// 我们在调用插件的时候是先使用 Vue.use(VueRouter) 调用 install 方法,然后再生成 VueRouter 实例的,所以这时候 _Vue 已经获取到 Vue 的构造函数了
}
static install(Vue) {
_Vue = Vue
}
install
let _Vue = null
static install(Vue) {
// 判断是否已经加载过该插件
if (VueRouter.install.installed) return
VueRouter.install.installed = true
// 挂载 Vue 构造函数到 全局
_Vue = Vue
// 将 Vue 实例的 $options.router 取出挂载到 Vue 构造函数的 $router 上,这样就可以在页面文件中通过 this.$router 来进行调用了
_Vue.mixin({
beforeCreate() {
// 因为无法通过 this 找到 Vue 实例,所以通过使用 mixin 找到当前的 Vue 实例
// 因为只有入口 Vue 实例身上挂在了 router 对象,所以要判断当前 Vue 实例是不是入口生成的 Vue实例
if (this.$options.router) {
_Vue.prototype.$router = this.$options.router
// 将 routes 以键值对缓存到 routeMap 上
// 创建 router-link 和 router-view 标签
}
}
})
}
createRouteMap
- 以键值对将路由缓存到 routeMap 上
createRouteMap() {
this.options.routes.forEach(route => {
this.routeMap[route.path] = route.component
})
}
initComponents
- 创建 router-link 和 router-view 标签
initComponents() {
const self = this // 设置变量,让 this 指向 VueRouter 实例
// 因为该方法在 install 内部调用,所以 _Vue 已经获取到 Vue 的构造函数了
// 调用 Vue.component 全局 API 创建 router-link 标签,其本质就是一个 a 标签
// <router-link to="/">Home</router-link> ------> to & slot
// to 属性可以是对象,也可以是字符串,这里先只使用字符串编写
// 因为 router-link 内部可以拥有 html-DOM 结构,所以需要使用到 slot,在 Vue.component 中使用 slot 就需要调用 Vue 实例的 $slot.default
_Vue.component('router-link', {
props: {
to: String
},
render(h) {
return h('a', {
attrs: {
href: this.to
},
on: {
click: this.handleClick // 因为 a 标签会自动跳转,但这里并不需要,所以要改写 a 标签的点击方法
}
}, [this.$slots.default])
},
methods: {
handleClick(e) {
history.pushState({}, '', this.to) // 监听 history.pushState 方法在浏览器历史中记录路径
self.data.current = this.to // 因为 current 是一个响应式数据,所以在它改变时会引起页面的重绘
e.preventDefault() // 取消 a 标签的默认方法
}
}
})
_Vue.component('router-view', {
render(h) {
return h(self.routeMap[self.data.current])
}
})
}
initEvent
- 当浏览器左上角的前进后退改变时存放在内存里的数据并不会改变,所以需要监听 popstate 方法
initEvent() {
window.addEventListener('popstate', () => {
this.data.current = window.location.pathname
})
}
init
init() {
this.createRouteMap()
this.initComponents()
this.initEvent()
}
VueRouter 简易构造函数
let _Vue = null
export default class VueRouter {
constructor(options) {
this.options = options
this.routeMap = {}
this.data = _Vue.observable({
current: '/'
})
}
static install(Vue) {
if (VueRouter.install.installed) return
VueRouter.install.installed = true
_Vue = Vue
_Vue.mixin({
beforeCreate() {
if (this.$options.router) {
_Vue.prototype.$router = this.$options.router
this.$options.router.init() // 这时候的 this 依然指向 Vue 实例,所以无法通过 this.init 进行调用
}
}
})
}
init() {
this.createRouteMap()
this.initComponents()
this.initEvent()
}
createRouteMap() {
this.options.routes.forEach(route => {
this.routeMap[route.path] = route.component
})
}
initComponents() {
const self = this
_Vue.component('router-link', {
props: {
to: String
},
render(h) {
return h('a', {
attrs: {
href: this.to
},
on: {
click: this.handleClick
}
}, [this.$slots.default])
},
methods: {
handleClick(e) {
history.pushState({}, '', this.to)
self.data.current = this.to
e.preventDefault()
}
}
})
_Vue.component('router-view', {
render(h) {
return h(self.routeMap[self.data.current])
}
})
}
initEvent() {
window.addEventListener('popstate', () => {
this.data.current = window.location.pathname
})
}
}
VueRouter 构造函数(支持 to 对象和 mode)
import { isString, isObject } from '@/utils/isType'
let _Vue = null
export default class VueRouter {
constructor(options) {
this.mode = options.mode
this.options = options
this.routeMap = {}
this.data = _Vue.observable({
current: this.mode === 'history' ? '/' : '/#'
}) // 记录当前路由
}
static install(Vue) {
// 判断插件是否已经安装
if (VueRouter.install.isInstalled) return
VueRouter.install.isInstalled = true
// 将 Vue 构造函数记录到全局变量
_Vue = Vue
// 将创建 Vue 实例时传入的 router 对象注入到 Vue 实例上
/*
_Vue.propertype.$router = this.$options.router
因为 install 是静态方法,左移下面的方式中的 this 指向并非 Vue 实例,而是 VueRouter 类
所以无法获取 this.$options 中的 router,因此采用全局API mixin 的方式找到当前 Vue 的实例
*/
_Vue.mixin({
beforeCreate() {
// 在所有 Vue 实例中混入一个 $router 对象,但是所有组件都属于一个新的 Vue 实例,所以这种方式会加载很多次,其实没必要。
// _Vue.propertype.$router = this.$options.router
/*
new Vue(options) => options -> $options
new Vue({
router, // $options.router
store, // $options.store
render: h => h(App)
}).$mount('#app')
main.js 中可以发现将 router 作为了主 Vue 实例的属性,因此只有这个 Vue 实例上有 router 对象
*/
if (this.$options.router) {
_Vue.prototype.$router = this.$options.router
_Vue.prototype.$router.init()
}
}
})
}
init() {
this.createRouteMap()
this.initComponents()
this.initEvent()
}
createRouteMap() {
// 遍历路由规则,以键值对方法存放到 routeMap
this.options.routes.forEach(route => {
this.routeMap[route.path] = route.component
})
}
initComponents() {
const self = this
_Vue.component('router-link', {
props: {
to: String | Object
},
data() {
return {
url: '',
pathname: '',
params: {},
query: {}
}
},
render(h) {
return h('a', {
attrs: {
href: this.pathname
},
on: {
click: this.handleClick
}
}, [this.$slots.default])
},
methods: {
initRouter() {
this.params = {}
this.query = {}
},
initParams() {
const to = this.to
if (isObject(to)) {
if (to.name) { // 使用 name 则说明使用的是 params 传参
const thisRoute = self.options.routes.filter(route => route.name === to.name)[0]
this.url = thisRoute.path
this.pathname = thisRoute.path
this.params = to.params
} else { // 否则是使用 path 来确定路由,说明使用的是 query 传参
let query = ''
for (const param in to.query) {
query += `${param}=${to.query[param]}&`
}
query = query.substring(0, query.length - 1)
this.url = to.path
this.pathname = `${to.path}?${query}`
this.query = to.query
}
} else if (isString(to)) {
this.url = to
this.pathname = to
} else {
this.url = '/'
this.pathname = '/'
}
},
handleClick(e) {
this.initRouter()
this.initParams()
if (self.mode === 'history') {
history.pushState({}, '', this.pathname)
} else {
window.location.hash = '#' + this.pathname
}
self.data.current = this.url
e.preventDefault()
}
}
})
_Vue.component('router-view', {
render(h) {
if (self.mode === 'history') {
self.data.current = window.location.pathname
} else {
self.data.current = window.location.hash.replace('#', '').split('?')[0]
}
return h(self.routeMap[self.data.current])
}
})
}
initEvent() {
if (this.mode === 'history') {
// 因为浏览器的返回前进按钮不会改变 this.data.current,所以需要监听 popstate 来修改当前页面的渲染
window.addEventListener('popstate', () => {
this.data.current = window.location.pathname
})
} else {
window.addEventListener('hashchange', () => {
this.data.current = window.location.hash.replace('#', '').split('?')[0]
})
}
}
}