本人前端小白一枚,只是整理一下自己的学习过程,第一次在掘金发表自己的文章,有问题欢迎大家及时指出
Vue生命周期
在学习vue相关组件,router,响应式原理以及虚拟DOM等时,需要对vue的声明周期有一定的明确
这里首先先对Vue的生命周期进行简单的讲解,对于vue生命周期的描述,网上有很多不错的文章,这里发一个链接,不了解的小伙伴可以参考这个链接对生命周期进行一个具体的学习
vue-router原理实现
vue-router的使用
基本用法
要想了解vue-router的实现原理,需要对vue-router的使用有一定的了解。通常在项目中使用vue-router时,需要对router.js配置文件中对路由的规则进行一系列配置,包括路径、路由名称、子路由规则等等。在配置完成后,对当前的router对象导出,并在main.js中导入
// router.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Index from '../views/Index.vue'
// 1. 注册路由插件
Vue.use(VueRouter)
// 路由规则
const routes = [
{
path: '/',
name: 'Index',
component: Index
},
{
path: '/blog',
name: 'Blog',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "blog" */ '../views/Blog.vue')
},
{
path: '/photo',
name: 'Photo',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "photo" */ '../views/Photo.vue')
}
]
// 2. 创建 router 对象
const router = new VueRouter({
routes
})
export default router
// main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
new Vue({
// 3. 注册 router 对象
router,
render: h => h(App)
}).$mount('#app')
这种使用方式是最基本的vue-router的使用方式。当然,也可以通过配置动态路由的方式对url中的参数传递给组件使用
动态路由
// router.js
{
path: '/detail/:id',
name: 'Detail',
// 开启 props,会把 URL 中的参数传递给组件
// 在组件中通过 props 来接收 URL 参数
props: true,
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "detail" */ '../views/Detail.vue')
}
上述vue-router的配置中开启了props属性,目的是获取url中的id参数,在传递参数的时候,有两种传递方式:
<!-- 方式1: 通过当前路由规则,获取数据 -->
通过当前路由规则获取:{{ $route.params.id }}
<br>
<!-- 方式2:路由规则中开启 props 传参 -->
通过开启 props 获取:{{ id }}
第一种方式是通过编程式的方式获取,这种方式有一个缺点,就是太过于依赖当前路由,所以一般通过在router.js中定义props属性,随后通过第二种方式传递参数
编程式路由
// 跳转push <router-link :to="..."> 等同于调用 router.push(...)
this.$router.push('/')
this.$router.push({ name: ''Detail', params: { id: 1} })
// 替换replace <router-link :to="..." replace> router.replace(...)
this.$router.replace('/login')
// go 在历史记录中向前或向后进行多少步
this.$router.go(-2)
Hash模式和History模式
Hash模式和History模式是vue-router中的两种模式
Hash模式:
在Hash模式中,可以在url中看到#或者?
Hash模式是基于锚点以及onhashchange事件,通过锚点的值作为路由地址,当锚点变化,调用onhashchange
方法,根据路径决定呈现的内容
History模式
History模式是基于HTML5中的History API实现的
·history.pushState()
·history.replaceState()
pushState与push方法的区别是:调用push的时候路径会发生变化,会向服务器发送请求,调用pushState的
时候,不会向服务器发送请求,只会改变地址栏中的地址,并且将地址记录到历史记录中,可以实现客户端路由
history模式的使用
History模式需要服务器的支持,在单页应用中,服务端不存在某一个地址,会返回404,所以,页面在服务端应该除了静态资源外都返回单页应用的index.html
vue-router的实现原理
在hash模式中,会将url中的**#这个锚点** 后面的内容作为路径地址,监听hashchange事件,根据当前路由地址找到对应组件重新渲染
而history模式中,通过 history.pushState() 方法改变地址栏,并把当前地址记录到浏览器中,同时监听 popstate 事件,获取到浏览器历史操作的变化,只有点击浏览器的前进、后退按钮,或者通过编程式导航的方式,才会触发popstate事件,最后根据当前路由地址找到对应组件重新渲染
实现一个vue-router
重头戏来了,我们需要实现一个vue-router
通过前面的讲解,了解了vue-router的实现原理,首先router是通过Vue.use()导入的,所以,需要实现install方法。随后,vue-router会创建路由对象实例,说明router是一个构造函数或类,包含install方法,这里我们以类的方式去实现。router的构造函数接收一个对象参数,记录地址和组件,使用时,创建vue实例,传入router对象,这就是vue-router的实现原理,下面是vue-router的一个类图,描述了vue-router中的参数,属性和方法
根据类图可以看到:
vue-router中有三个属性,options记录的是创建实例时传入的路由规则的参数。data记录的是当前的路由状态。routeMap记录的是路由地址和组件的对应关系。
install的实现
install默认是一个静态方法,需要通过VueRouter.install()方式去调用,所以这里定义一个静态的install方法,在这个方法里需要做三件事:
-
判断是否已经安装
-
把构造函数记录到全局变量中
-
将创建Vue实例时传入的router对象注入到所有实例中
static install (Vue) {
// 判断是否已经安装
if (VueRouter.install.installed) {
return
}
VueRouter.install.installed = true
// 把构造函数记录到全局变量中
_Vue = Vue
// 将创建Vue实例时传入的router对象注入到所有实例中
// 混入mixin
_Vue.mixin({
beforeCreate () {
// 判断当前对象是组件还是vue实例,是组件的话,this.$options.router不存在,不需要调用
if (this.$options.router) {
_Vue.prototype.$router = this.$options.router
// 通过init方法传入createRouteMap方法和initComponents方法
this.$options.router.init()
}
}
})
}
vue-router的构造函数
构造函数constructor中传入options,在这个构造函数中,对三个属性进行操作,分别为上述的options、data和routeMap
// 构造函数
constructor (options) {
this.options = options
this.routeMap = {}
// data 为响应式的对象,通过Vue.observable创建
this.data = _Vue.observable({
// 存储当前路由地址,默认为 '/'
current: '/'
})
}
createRouteMap
createRouteMap方法的作用是遍历路由规则解析成键值对的形式传入 routeMap 中
createRouteMap () {
// 遍历路由规则解析成键值对的形式传入 routeMap 中
this.options.routes.forEach(route => {
this.routeMap[route.path] = route.component
})
}
initComponents
initComponents方法是创建一个vue组件,在这个组件中,对router-link标签中的内容生成对应的a标签。router-link内部的参数to通过组件中的props传入
initComponents (Vue) {
Vue.component('router-link', {
props: {
to: String
},
template: '<a :href="to"><slot></slot></a>'
})
}
init
init方法的作用是将initComponents方法和createRouteMap方法合并到一起,传入到install中,在创建Vue实例时传入的router对象注入到所有实例中时初始化routeMap和router-link
init () {
this.createRouteMap()
this.initComponents(_Vue)
}
通过这些代码,可以基本上实现了vue-router的功能,我们可以来测试一下,将之前导入的vue-router切换到我们自己手写的代码路径,在浏览器中测试一下
控制台报错了,页面内容也没有呈现出来,这是因为什么呢?
原因在于,通过vue-cli构建的项目默认是运行时版本,在运行时版本,不支持template模板,需要打包时提前编译,所以这个版本中不存在编译器,不能将template转换成render函数。所以这里我们有两种解决方案
完整版vue
这种方式是通过对vue-cli构建的项目进行配置,通过配置,将当前项目转换成完整版,在完整版中存在编译器,便可以进行编译。
这里需要在项目根路径下新建vue.config.js配置文件,在文件中对 runtimeCompiler 属性进行配置
module.exports = {
runtimeCompiler: true
}
render
既然运行时版本中没有编译器,那我们就可以将template模板直接通过render函数的方式来实现
initComponents (Vue) {
Vue.component('router-link', {
props: {
to: String
},
render (h) {
// 标签 + dom属性 + 内容
return h('a', {
attrs: {
href: this.to
}
}, [this.$slots.default]
)
}
// template: '<a :href="to"><slot></slot></a>'
})
}
在render函数中,有一个h函数,在返回这个函数时,传入三个参数:
第一个参数为转换的标签;
第二个参数为当前转换标签的属性,一般转换为a标签时,会将a中的href属性值设置为props中的to的值;
第三个参数为当前标签内部的内容。在template中,我们通过slot的方式获取内容,在render函数中,我们通过this.$slots.default的方式获取默认插槽内容。
再次进入浏览器查看:
可以看到问题已经解决了
router-view
在上图可以看到,控制台报错提示没有定义router-view组件,所以这里我们需要实现一个router-view组件
// router-view
// 保存vue-router实例,确保this指向
const self = this
Vue.component('router-view', {
render (h) {
const component = self.routeMap[self.data.current]
return h(component)
}
})
代码中,同样通过render函数定义,在这个函数中,获取到当前vueRouter对象中的routeMap的值,由于routeMap内部存储的是当前路由的路径和模板,通过这种方式将当前内容作为参数传入到h函数中,h函数会自动对其进行渲染
测试后,发现在每次点击对应a标签时,浏览器都会对服务器进行请求,这是因为a标签默认会发出请求,这在单页面应用中是不应该发生的,所以,我们需要对router-link组件进行修改,对定义的a标签添加监听点击事件
Vue.component('router-link', {
props: {
to: String
},
render (h) {
// 标签 + dom属性 + 内容
return h('a', {
attrs: {
href: this.to
},
on: {
click: this.clickHandler
}
}, [this.$slots.default]
)
},
methods: {
clickHandler (e) {
// 通过history.pushState方法改变地址栏,同时不发送请求
// 三个参数,data触发popState方法 title:网页标题 url:url地址内容
history.pushState({}, '', this.to)
// data是响应式的,所以更改current,会自动更新当前组件的内容
this.$router.data.current = this.to
// 阻止默认事件跳转
e.preventDefault()
}
}
// template: '<a :href="to"><slot></slot></a>'
})
在这里,我定义了一个clickHandler函数,这个函数接收一个e参数,通过e.preventDefault()阻止默认跳转事件,随后,通过history.pushState()函数将地址栏内容进行改变,然后将this.router.data属性是响应式的,当内部的current改变,会自动更新当前组件的内容。
当重新定义了router-link和router-view时,再来到浏览器进行查看,发现点击后不会跳转了,同时视图也会正常更新
initEvent
initEvent事件是为了处理点击浏览器前进后退后页面内容的问题,在上面对router-view和router-link进行修改后,浏览器点击不会跳转,但是当我们点击浏览器的前进或者后退按钮时,url发生改变,但是视图并未发生改变,所以,通过initEvent事件解决该问题
// 处理点击浏览器前进后退后页面内容的问题
initEvent () {
window.addEventListener('popstate', () => {
this.data.current = window.location.pathname
})
}
这时,我们点击浏览器的前进后退按钮,视图也会正常更新了,最后不要忘了将initEvent事件放入到init事件中
这样!我们就实现了history模式的vue-router了
完整代码:
// Vue.use() 需要实现install方法
// 创建路由对象实例,说明router是一个构造函数或类,包含install方法
// router的构造函数接收一个对象参数,记录地址和组件
// 创建vue实例,传入router对象
let _Vue = null
export default class VueRouter {
// install
static install (Vue) {
// 判断是否已经安装
if (VueRouter.install.installed) {
return
}
VueRouter.install.installed = true
// 把构造函数记录到全局变量中
_Vue = Vue
// 将创建Vue实例时传入的router对象注入到所有实例中
// 混入mixin
_Vue.mixin({
beforeCreate () {
// 判断当前对象是组件还是vue实例,是组件的话,this.$options.router不存在,不需要调用
if (this.$options.router) {
_Vue.prototype.$router = this.$options.router
this.$options.router.init()
}
}
})
}
// 构造函数
constructor (options) {
this.options = options
this.routeMap = {}
// data 为响应式的对象,通过Vue.observable创建
this.data = _Vue.observable({
// 存储当前路由地址,默认为 '/'
current: '/'
})
}
init () {
this.createRouteMap()
this.initComponents(_Vue)
this.initEvent()
}
createRouteMap () {
// 遍历路由规则解析成键值对的形式传入 routeMap 中
this.options.routes.forEach(route => {
this.routeMap[route.path] = route.component
})
}
initComponents (Vue) {
Vue.component('router-link', {
props: {
to: String
},
render (h) {
// 标签 + dom属性 + 内容
return h('a', {
attrs: {
href: this.to
},
on: {
click: this.clickHandler
}
}, [this.$slots.default]
)
},
methods: {
clickHandler (e) {
// 通过history.pushState方法改变地址栏,同时不发送请求
// 三个参数,data触发popState方法 title:网页标题 url:url地址内容
history.pushState({}, '', this.to)
// data是响应式的,所以更改current,会自动更新当前组件的内容
this.$router.data.current = this.to
// 阻止默认事件跳转
e.preventDefault()
}
}
// template: '<a :href="to"><slot></slot></a>'
})
// router-view
// 保存vue-router实例,确保this指向
const self = this
Vue.component('router-view', {
render (h) {
const component = self.routeMap[self.data.current]
return h(component)
}
})
}
// 处理点击浏览器前进后退后页面内容的问题
initEvent () {
window.addEventListener('popstate', () => {
this.data.current = window.location.pathname
})
}
}
以上就是我对vue-router的手写的代码,有问题欢迎大佬指出,欢迎讨论,欢迎批评改正