笔记来源:拉勾教育 大前端高薪训练营
阅读建议:建议通过左侧导航栏进行阅读
编程式导航
this.$router.replace()
this.$router.push()
this.$router.go()
客户端路由的实现模式,当路径发生变化时,不会向服务器发生请求,都是 JavaScript 监视路径的变化,然后根据不同的地址渲染不同的内容。
表现形式的区别
Hash 模式
https://music.163.com/#/playlist?id=3102961863
History 模式
https://music.163.com/playlist/3102961863
需要服务端配置支持
原理的区别
Hash 模式是基于锚点,以及 onhashchange 事件;
通过 锚点的值 作为路由地址,当地址发生变化后,触发 onhashchange 事件。
即 根据路径决定页面中呈现的内容。
History 模式是基于 HTML5中的 History API
history.pushState() IE10 以后才支持
当调用 history.push() 时,路径会发生变化,要向服务器发生请求;
当调用 history.pushState() 时,不会向服务器发生请求,只会改变浏览器路径栏中的地址,并且将地址记录到历史记录中。
也就是说,可以使用 pushState() 实现客户端路由。但是,需要在 IE10
以后使用。
history.replaceState()
History 模式
nginx 配置
从官网下载 nginx 的压缩包
把压缩包解压到 c 盘根目录,c:\nginx-1.18.0 文件夹
注意:目录不能有中文
打开命令行,切换到目录 c:\nginx-1.18.0
启动 nginx
$ start nginx
重启
$ nginx -s reload
停止
$ nginx -s stop
总结
Hash 模式
History 模式
在模拟 Vue-Router 的过程中,我们需要简单了解一些相关的知识。
插件
使用插件
Vue.use()
使用插件。它需要在你调用 new Vue() 启动应用之前完成:// 调用 `MyPlugin.install(Vue)`
Vue.use(MyPlugin)
new Vue({
// ...组件选项
})
Vue.use(MyPlugin, {
someOption: true })
Vue.use
会自动阻止多次注册相同插件,届时即使多次调用也只会注册一次该插件。开发插件
Vue.js 的插件应该暴露一个 install
方法。
/**
* 第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象
*/
MyPlugin.install = function (Vue, options) {
// 1. 添加全局方法或 property
Vue.myGlobalMethod = function () {
// 逻辑...
}
// 2. 添加全局资源
Vue.directive('my-directive', {
bind (el, binding, vnode, oldVnode) {
// 逻辑...
}
...
})
// 3. 注入组件选项
Vue.mixin({
created: function () {
// 逻辑...
}
...
})
// 4. 添加实例方法
Vue.prototype.$myMethod = function (methodOptions) {
// 逻辑...
}
}
组件
注册全局组件
使用 Vue.component
来注册全局组件:
Vue.component('my-component-name', {
// ... 选项 ...
})
第一个参数是 组件名,全局组件在注册之后可以用在任何新创建的 Vue 根实例 (new Vue) 的模板中。
使用 props
向子组件传递数据
Vue.component('blog-post', {
props: ['title']
})
混入 mixin
混入(mixin),用来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
基本实现
// 定义一个混入对象
var myMixin = {
created: function () {
this.hello()
},
methods: {
hello: function () {
console.log('hello from mixin!')
}
}
}
// 定义一个使用混入对象的组件
var Component = Vue.extend({
mixins: [myMixin]
})
var component = new Component() // => "hello from mixin!"
全局混入
混入也可以进行全局注册。使用时格外小心!一旦使用全局混入,它将影响每一个之后创建的 Vue 实例。使用恰当时,这可以用来为自定义选项注入处理逻辑。
// 为自定义的选项 'myOption' 注入一个处理器。
Vue.mixin({
created: function () {
var myOption = this.$options.myOption
if (myOption) {
console.log(myOption)
}
}
})
new Vue({
myOption: 'hello!'
})
// => "hello!"
Vue.observable(object)
基本用法
创建响应式对象,创建出的对象可以直接用在渲染函数或者计算属性上面,并且会在发生变更时触发相应的更新。也可以作为最小化的跨组件状态存储器,用于简单的场景:
const state = Vue.observable({
count: 0 })
const Demo = {
render(h) {
return h('button', {
on: {
click: () => {
state.count++ }}
}, `count is: ${
state.count}`)
}
}
在 Vue 2.x 中,被传入的对象会直接被
Vue.observable
变更,所以如这里展示的,它和被返回的对象是同一个对象。
在 Vue 3.x 中,则会返回一个可响应的代理,而对源对象直接进行变更仍然是不可响应的。
因此,为了向前兼容,我们推荐始终操作使用Vue.observable
返回的对象,而不是传入源对象。
插槽 slot
元素作为承载分发内容的出口。插槽内可以包含任何模板代码,包括 HTML、其他组件等
Vue.component('alert-box', {
template: `
Error!
`
})
render 函数
类型:(createElement: () => VNode) => VNode
详细:
字符串模板的代替方案,允许你发挥 JavaScript 最大的编程能力。该渲染函数接收一个 createElement
方法作为第一个参数用来创建 VNode
。
如果组件是一个函数组件,渲染函数还会接收一个额外的 context
参数,为没有实例的函数组件提供上下文信息。
Vue 选项中的
render
函数若存在,则 Vue 构造函数不会从template
选项或通过el
选项指定的挂载元素中提取出的 HTML 模板编译渲染函数。
构建版本
运行版 Vue
不支持 template 模板,需要打包的时候提前编译。
export default class VueRouter {
initComponents (Vue) {
Vue.component('router-link', {
props: {
to: String
},
// h 函数,创建虚拟 DOM
render (h) {
return h()
}
})
}
}
完整版 Vue
包含运行时和编译器,体积比运行时版大 10K 左右,程序运行的时候把模板转换成 render 函数。
module.exports = {
runtimeCompiler: true // 默认 false
}
下面,我们通过模拟源码的方式,进行分析 Vue-Router
的实现原理。
install() 方法是 VueRouter 类中的静态方法,当使用 Vue.use(fun | obj) 注册插件时,会调用 install() 方法。
let _Vue = null
export default class VueRouter {
/**
* Vue.use() 注册插件时,调用
* @param Vue Vue构造函数
*/
static install(Vue) {
// 1,判断当前插件是否已经被安装
if (VueRouter.install.installed) {
return
}
VueRouter.install.installed = true
// 2,把Vue构造函数记录到全局变量中
_Vue = Vue
// 3,把创建Vue实例时,传入的 router对象 注入到Vue实例上
// 混入
_Vue.mixin({
beforeCreate() {
// 判断当前传入的是否是 router 对象,即排除传入组件的情况
if (this.$options.router) {
_Vue.prototype.$router = this.$options.router
}
}
})
}
}
VueRouter 类的构造函数,接收一个 Options 选项,它的的返回值是一个 VueRouter 对象。
在构造函数中,需要初始化三个属性:
1,options,记录构造函数中传入的对象(路由规则);
2,data,存储当前的路由地址,当路由变化时,需要加载对应的组件,因此,需要设置成一个响应式的对象;
3,routeMap,是一个对象,用来记录路由地址和组件的对应关系,将来会把路由规则解析到 routeMap 中。
let _Vue = null
export default class VueRouter {
// 构造函数
constructor (options) {
// 记录构造函数中传入的选项
this.options = options
// 当options中传入的 routes(路由规则) 解析出来以后,会将其存储到 routeMap 对象中,以便在router-view组件中,可以根据路由地址在routeMap中找到对应的组件,并将其渲染到浏览器中
// 键:路由地址 值:地址所对应的路由组件
this.routeMap = {
}
// 响应式对象,使用 Vue.observable() 创建
this.data = _Vue.observable({
current: '/' // 记录当前的路由地址,默认 '/'
})
}
}
createRouteMap(),会把构造函数中选项的 routes(路由规则),转换成键值对的形式,存储到 routeMap对象中。
export default class VueRouter {
// 其余代码 省略
createRouteMap () {
// 遍历所有的路由规则,把路由规则解析成键值对的形式,存储到 routeMap 中
this.options.routes.forEach(route => {
this.routeMap[route.path] = route.component
})
}
}
initComponents() ,创建 router-link 和 router-view 组件。
initComponents() 源码,注册 router-link 组件,分析如下:
export default class VueRouter {
// 创建 router-link 和 router-view 组件
// 传入Vue构造函数
// 不使用_Vue,减少与外部的依赖
initComponents (Vue) {
Vue.component('router-link', {
props: {
to: String
},
// 使用插槽,进行中间内容的填充
template: ' '
})
}
}
运行结果,如下图所示:
使用 render 函数,解决上述问题,代码如下:
export default class VueRouter {
initComponents (Vue) {
Vue.component('router-link', {
props: {
to: String
},
// h 函数,创建虚拟 DOM
render (h) {
/**
* @param 创建元素对应的选择器
* @param 添加标签属性,对象
* @param 生成标签的子元素,数组
*/
return h('a', {
attrs: {
href: this.to
}
}, [this.$slots.default]) // 获取默认插槽
}
})
}
}
注册 router-view 组件,代码如下:
export default class VueRouter {
// 其余代码 省略
initComponents (Vue) {
Vue.component('router-link', {
props: {
to: String
},
// h 函数,创建虚拟 DOM
render (h) {
return h('a', {
attrs: {
href: this.to,
on: {
click: this.clickHandler // 注册点击事件
}
}
}, [this.$slots.default]) // 获取默认插槽
},
methods: {
clickHandler (e) {
// 改变浏览器的地址栏,但不向服务器发送请求,只在客户端进行操作
/**
* @param data 触发popstate事件,传给 popstate 的事件对象
* @param title 网页的标题
* @param url? 地址
*/
history.pushState({
}, '', this.to)
// 将当前的路径记录到 data.current 中
// 响应式对象,当值改变时,自动加载对应的组件,进行渲染视图
this.$router.data.current = this.to
e.preventDefault() // 阻止默认事件
}
}
})
const self = this
Vue.component('router-view', {
// h 函数,可以直接把一个组件转换成 虚拟 DOM
render (h) {
// 通过当前路由地址,在routeMap中找到对应组件
const component = self.routeMap[self.data.current]
return h(component)
}
})
}
}
initEvent(),注册 popstate 事件,当历史发生变化时,进行触发。即点击浏览器的前进后退按钮时,触发 popstate 事件。
export default class VueRouter {
initEvent () {
window.addEventListener('popstate', () => {
this.data.current = window.location.pathname
})
}
}
init() ,用来初始化调用其他函数。
export default class VueRouter {
// 其余代码省略
static install (Vue) {
_Vue.mixin({
beforeCreate () {
if (this.$options.router) {
_Vue.prototype.$router = this.$options.router
this.$options.router.init()
}
}
})
}
init () {
this.createRouteMap()
this.initComponents(_Vue)
this.initEvent()
}
}
hash
模式下,触发 onhashchange
事件,其余方面与 history
模式类似。
let _Vue = null
export default class VueRouter {
// 使用 Vue.use() 注册插件时,此方法被调用
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()
}
}
})
}
constructor (options) {
this.options = options
this.routeMap = {
}
this.current = ''
// 当模式为 hash,初始进入时,进行拼接
window.location.hash = window.location.hash ? window.location.hash : '#/'
this.data = _Vue.observable({
current: window.location.hash
})
}
init () {
this.createRouteMap()
this.initComponents(_Vue)
this.initEvent()
}
createRouteMap () {
this.options.routes.forEach(route => {
this.routeMap[route.path] = route.component
})
}
// 初始化组件
initComponents (Vue) {
const self = this
// 定义全局组件 router-link
Vue.component('router-link', {
// 使用 props 传递属性
props: {
to: String
},
render (h) {
return h('a', {
attrs: {
href: '#'
},
class: {
'router-link-active': this.to === '/',
'router-link-exact-active': self.data.current === this.to
},
on: {
click: this.clickHandler
}
}, [this.$slots.default]) // 获取默认插槽
},
methods: {
clickHandler (e) {
e.preventDefault()
// 当模式为 hash 时,执行的操作
window.location.hash = this.to
}
}
})
// router-view
_Vue.component('router-view', {
render (h) {
// 当路由路径是 hash 时,去掉 # ,以便获取对应的组件
if (self.data.current.startsWith('#')) {
self.data.current = self.data.current.substr(1)
}
const component = self.routeMap[self.data.current]
return h(component)
}
})
}
initEvent () {
// 当地址发生变化后,触发 onhashchange 事件
window.addEventListener('hashchange', () => {
const current = window.location.hash.substr(1)
this.data.current = current || this.data.current
})
}
}
以上单独模拟了 hash
和 history
模式的路由实现,这两种可以整合成一套代码,进行不同模式下,匹配不同的路由操作。
具体代码实现,参见:
https://gitee.com/zimeng303/lago-training-camp/tree/master/work/Module%2003/01-part-code/vue-hash
VueJs官网