这个模块主要是对Vue.js进行深度的原理剖析,本任务包含以下几点:
- 快速回顾Vue基础用法
- Vue-Router原理分析及实践
- 介绍虚拟DOM、分析Snabbdom源码
- 实现最小版本Vue,主要进行响应式原理分析及实现
- Vue源码分析,初始化过程、首次渲染过程、响应式过程、编译模板过程
Vue-Router浅析
本次回顾的Vue路由相关内容有:
- 动态路由,类似:id的路由形式,可以拿参数。
- 嵌套路由,路由配置中的children属性。
- 编程式导航,一般路由跳转是使用
router-link组件,编程式是使用$router的push、replace、go等等方法,实现路由跳转。 - Hash和HIstory模式区别,它们都是客户端路由的实现方式,路径发生变化都不会刷新浏览器,是由JS来监控路由变化,渲染不同的组件。想要用好HIstory模式,还需要服务端配置支持。
-
- hash模式基于锚点,锚点作为路由地址,路由变化触发onhashchange事件,根据路径决定页面显示内容。
- history模式基于HTML5中的History API,主要使用pushState和replaceState。
- pushState和push方法有区别,push方法会使路径发生变化,向服务器发送请求;而pushState方法只会改变浏览器地址栏地址,并记录到路由栈历史记录里。
- pushState是IE 10以后才支持,IE 10之前还是得使用hash模式
History模式
这里有个新概念,History模式需要服务器支持,为什么呢?
- 比如在单页应用中,只有index.html,而没有login.html。刷新浏览器请求login页面应该会返回404。
- 而Vue-Cli帮我们做好了配置处理,所以刷新不会报错。
在NodeJS环境的History模式
在nodeJs中可以使用connect-history-api-fallback这个中间件来处理history模式,其原理是:当浏览器发送一个页面请求时,服务器如果没有这个页面,就会将index.html页面返回,然后浏览器再根据url去渲染对应的组件出来。
具体原理可以查查百度,参考链接
在Nginx下的History模式
首先自己配置一个Nginx服务器,将示例代码放在html目录下。如果未进行history配置,他依然会进行404报错,所以我们就需要修改Nginx配置文件了。
在Nginx的nginx.conf文件下配置根路径,增加try_files字段:
location / {
root html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
$uri就是当前请求的路径,它会去找路径对应文件,找到了则返回,没找到则接着找默认首页文件,也就是index.html。如果还没找到则直接返回首页,也是index.html。
当我们访问单页面应用的特定路径时,nginx服务器则会返回index.html,然后在客户端根据路由地址去解析对应的组件。
模拟实现Vue-Router
通过模拟了解内部原理,主要实现History模式,这里有些Vue相关概念的前置知识:
- 插件
- 混入
- Vue.observable()
- 插槽
- render函数
- 运行时和完整版的Vue
这里简单讨论下Vue下的两种路由模式:
Hash模式
- URL中#后面的内容作为路径地址。
-
- #后面的地址改动,不会触发浏览器的重新刷新。
- 监听hashchange事件。
- 根据当前路由地址找到对应组件重新渲染。
History模式
- 通过history.pushState方法改变地址栏,将当前地址记录到浏览器访问历史中,并不会真正跳转指定路径,不会向服务器发送请求。
- 监听popState事件。
-
- 要注意pushState、replaceState不会触发该事件,点击浏览器的前进后退,或调用go、forward方法会触发。
- 根据当前路由地址找到对应的组件重新渲染。
原理分析
使用Vue-Router的核心代码:
Vue-Router的类图,它描述了Vue-Router包含的属性和方法:
我们就根据这张类图来进行实现。先来介绍一下属性:
- options:记录构造函数中传入的对象。
- data:它是一个对象,它有一个属性current,记录当前路由地址。它是一个响应式对象。
- routeMap:记录路由地址和组件对应关系,它是一个对象。
然后是其方法:
- 带+号的是对外公开的方法,带_的是静态方法,例如_Install方法,它用来实现Vue插件机制。
- init方法是用来调用下面的三个方法。将不同功能代码分割到不同方法中实现。
-
- initEvent:注册popState事件。
- createRouteMap:初始化routeMap属性,将在构造函数中,传入的路由规则转换成键值对形式。
- initComponents:创建
router-link和router-view组件的。
在使用自己写的router-link组件时,报了如下错误:
意思是正在使用运行时版本的Vue,不支持template,要么将模板预编译为render函数,要么使用带编译器的构建。这里说说Vue的构建版本,分为运行时版和完整版:
- 运行时版: 不支持Template模板,需要打包的时候提前编译,将其编译为render函数,然后使用render函数创建虚拟DOM,渲染到视图。
- 完整版: 包含运行时和编译器,因为多了编译器,所以体积比运行时版大了10K左右,它的作用是在程序运行时把模板转换为render函数,所以性能不如前者。
Vue-Cli创建的项目默认使用运行时版Vue,那么我们如何来解决呢?这里有两种方式:
- 切换为完整版本的Vue:官方文档。我们创建一个
vue.config.js文件,然后配置runtimeCompiler属性为true。 - 运行时版本,则使用 render函数替换Template:
-
- 对于单文件组件,它也是使用Template,它能正常使用是因为在打包时,将其编译成了render函数,也就是预编译。
- 使用
render函数的h函数对Template进行编译。h函数接收三个参数:元素选择器、设置属性、子元素。
完整Vue-Router实现代码
// vue-router.js
let _Vue = null
export default class VueRouter {
static install(Vue) {
// 1 判断当前插件是否被安装
if (VueRouter.install.installed) {
return
}
VueRouter.install.installed = true
// 2 把Vue的构造函数记录在全局中,将来要在Vue-Router实例方法中,使用到这个Vue的构造函数,比如创建组件时需使用Vue.Component
_Vue = Vue
// 3 把创建Vue的实例时传入的router对象注入到所有Vue实例,让所有实例共享一个成员
// 在创建Vue实例时,Vue会将options中自定义的属性和Vue构造函数中定义的属性合并为vm.$options
_Vue.mixin({
beforeCreate() {
// 此处只执行一次,如果是组件则不执行,是Vue实例则执行
if (this.$options.router) {
_Vue.prototype.$router = this.$options.router
this.$options.router.init()
}
}
})
}
constructor(options) {
// 初始化options、routeMap、data属性
this.options = options
// 在router-view组件中,会根据路由地址去routeMap中找对应组件
this.routeMap = {}
// data是一个响应式对象,因为它存储着当前路由地址,地址变化时要加载对应组件
this.data = _Vue.observable({
current: location.pathname || '/' // 默认为 /
})
this.init()
}
init() {
this.createRouteMap()
this.initComponent(_Vue)
this.initEvent()
}
// 将构造函数传入的routes(路由配置表),转换成键值对形式,传入routeMap对象中
// 健就是路由地址,值就是所对应的组件
createRouteMap() {
// 遍历所有的路由规则,把路由规则解析成键值对的形式存储到routeMap中
this.options.routes.forEach(route => {
this.routeMap[route.path] = route.component
})
}
initComponent(Vue) {
// 通过Vue.component注册组件
Vue.component('router-link', {
props: {
to: String
},
render(h) {
return h('a', {
attrs: {
href: this.to
},
on: {
click: this.clickhander
}
}, [this.$slots.default])
},
methods: {
clickhander(e) {
// 改变地址栏路径、不会向服务器发请求、记录到历史记录
history.pushState({}, '', this.to)
// window.location = this.to
this.$router.data.current = this.to
e.preventDefault()
}
}
// template: "<a :href='to' @click={{this.clickhander}}><slot></slot><>"
})
const self = this
Vue.component('router-view', {
render(h) {
// 需找到当前路由地址,根据路由地址到routerMap中找到对应的组件
// 使用h函数将组件转换为虚拟DOM
const cm = self.routeMap[self.data.current] || self.routeMap['*']
return h(cm)
}
})
}
initEvent() {
window.addEventListener('popstate', () => {
this.data.current = window.location.pathname
})
}
}