概述
- Vue Router 基础回顾
- Hash模式和History模式
- 模拟实现自己的Vue Router
Vue Router 基础回顾
基本使用
"vue-router": "^3.1.6"
目录结构
router/index.js
- 注册路由插件: Vue.use 用来注册插件,传入函数的话就直接调用,传入对象就会调用对象的 install 方法
- 传入路由规则创建 router 对象并导出
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
- 注册 router 对象
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选项,此时会给Vue实例注入两个属性,$route(路由规则),$router(Vue Router 提供路由信息、方法等)。
App.vue
- 创建路由组件的占位
- 创建链接
<template>
<div id="app">
<div>
<img src="@/assets/logo.png" alt="">
</div>
<div id="nav">
<!-- 5. 创建链接 -->
<router-link to="/">Index</router-link> |
<router-link to="/blog">Blog</router-link> |
<router-link to="/photo">Photo</router-link>
</div>
<!-- 4. 创建路由组件的占位 -->
<router-view/>
</div>
</template>
效果
动态路由传参
目录
router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Index from '../views/Index.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Index',
component: Index
},
{
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')
}
]
const router = new VueRouter({
routes
})
export default router
获取路由参数的方式:
- 通过当前路由规则,获取数据(组件依赖路由)
- 路由规则中开启 props 传参,会把 URL 中的参数传递给组件,在组件中通过 props 来接收 URL 参数(推荐)
Detail.vue
<template>
<div>
<!-- 方式1: 通过当前路由规则,获取数据 -->
通过当前路由规则获取:{{ $route.params.id }}
<br>
<!-- 方式2:路由规则中开启 props 传参 -->
通过开启 props 获取:{{ id }}
</div>
</template>
<script>
export default {
name: 'Detail',
props: ['id']
}
</script>
效果
嵌套路由
当多个路由组件有相同的内容,可以把这些相同的内容提取到一个公共组件当中。
目录
其中Layout.vue包含了首页和详情页相同的header和footer,通过router-view切换首页和详情页的内容。
Layout.vue
<template>
<div>
<div>
<img width="25%" src="@/assets/logo.png">
</div>
<div>
<router-view></router-view>
</div>
<div>
Footer
</div>
</div>
</template>
<script>
export default {
name: 'layout'
}
</script>
router.js
import Vue from 'vue'
import VueRouter from 'vue-router'
// 加载组件
import Layout from '@/components/Layout.vue'
import Index from '@/views/Index.vue'
import Login from '@/views/Login.vue'
Vue.use(VueRouter)
const routes = [
{
name: 'login',
path: '/login',
component: Login
},
// 嵌套路由
{
path: '/',
component: Layout,
children: [
{
name: 'index',
path: '',
// path: '/',
component: Index
},
{
name: 'detail',
path: 'detail/:id',
// path: '/detail/:id',
props: true,
component: () => import('@/views/Detail.vue')
}
]
}
]
const router = new VueRouter({
routes
})
export default router
编程式导航
之前路由跳转是在页面通过router-link生成超链接,很多情况下需要在代码里加入跳转逻辑。
编程式导航中常用的方法:
- $router.push() 传参方式
-
路径
$router.push('/') -
组件名称
$router.push({ name: 'Detail', params: { id: 1 } })
- $router.replace() 传参与push方法相同
replace方法不会记录本次历史
- $router.go()
跳转到历史某一次
$router.go(-1) 与$router.back()效果一致
Hash 模式和 History 模式的区别
不管哪种模式都是客户端路由的实现方式,也就是当路径发生变化之后不会向服务器发送请求,是由JS监视路由的变化,然后根据不同的地址渲染不同的内容。如果需要服务器端内容的话会发起Ajax请求来获取。
表现形式的区别
- Hash 模式
https://music.163.com/#/playlist?id=3102961863
带 # 号,# 后面的内容作为路由地址,可以通过 ? 携带url参数,这些路径很丑,路径中带着一些和数据无关的符号 # ?。
- History 模式
https://music.163.com/playlist/3102961863
正常的url,History 模式还需要服务端配置支持。
原理的区别
- Hash 模式是基于锚点,以及
onhashchange事件 通过锚点的值作为地址,当地址发生变化后触发 onhashchange 事件,根据路径呈现页面上不同的内容。 - History 模式是基于 HTML5中的 History API
-
history.pushState()IE10 以后才支持 与history.push()区别是:调用history.push()方法时,路径会发生变化,这时要向服务器发送请求;而调用history.pushState()不会向服务器发送请求只会改变地址,并且将地址记录到历史记录,所以通过history.pushState()可以实现客户端路由。因为 IE10 以后才支持history.pushState(),所以想要兼容IE9以下的版本只能选择Hash模式。 -
history.replaceState()
History 模式
使用
- History 模式需要服务器的支持
因为单页面应用(只有一个页面 index.html)中,服务器不存在
http://www.testurl.com/login这样的地址(这样的页面)会返回找不到该页面,所以应该在服务端配置除了静态资源外都返回单页应用的index.html。
Node.js 服务器配置
目录结构
前端目录结构
打包后将打包后的dist文件夹放到web目录下(相当于网站的根目录),这就把前端项目部署好了。
后端目录结构
app.js
const path = require('path')
// 导入处理 history 模式的模块
const history = require('connect-history-api-fallback')
// 导入 express 基于 node 的 web开发框架
const express = require('express')
const app = express()
// 注册处理 history 模式的中间件
app.use(history())
// 处理静态资源的中间件,网站根目录 ../web
app.use(express.static(path.join(__dirname, '../web')))
// 开启服务器,端口是 3000
app.listen(3000, () => {
console.log('服务器开启,端口:3000')
})
这是基于node的express开发的web服务器,运行node app.js,当在浏览器中的输入localhost:3000 就可以访问该服务器看到该网站的内容。
Nginx 服务器配置
安装
$ brew search nginx
$ brew install nginx
启动(会在后台启动,不会阻塞命令行的运行)
$ nginx
然后在浏览器中输入:http://localhost:8080/ 查看 Nginx 首页。
其他操作
$ nginx -s reload:重新加载配置
$ nginx -s stop:停止
$ nginx -s reopen:重启
$ nginx -s quit:退出
把打包好的项目放到nginx的/usr/local/var/www(网站的根目录)
这时候需要修改nignx配置(/usr/local/etc/nginx/nginx.conf)来支持history模式。
location / {
root html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
Vue Router 实现原理
基本原理
Hash 模式
- URL中 # 后面的内容作为路径地址
- 监听 hashchange 事件
- 根据当前路由地址找到对应组件重新渲染 History 模式
- 通过history.pushState()方法改变地址栏
- 监听popstate事件(点击前进后退才会被触发)
- 根据当前路由地址找到对应组件重新渲染
模拟实现分析
Vue Router 的核心代码
Vue Router 类图
实现
install
- 判断当前插件是否被安装
- 把Vue的构造函数记录在全局
- 把创建Vue的实例传入的router对象注入到Vue实例
let _Vue = null
class VueRouter {
static install(Vue){
//1 判断当前插件是否被安装
if(VueRouter.install.installed){
return;
}
VueRouter.install.installed = true
//2 把Vue的构造函数记录在全局
_Vue = Vue
//3 把创建Vue的实例传入的router对象注入到Vue实例
_Vue.mixin({
beforeCreate(){
if(this.$options.router){
_Vue.prototype.$router = this.$options.router
}
}
})
}
}
构造函数
constructor(options){
this.options = options
this.routeMap = {}
// observable
this.data = _Vue.observable({
current:"/"
})
this.init()
}
createRouteMap
createRouteMap(){
//遍历所有的路由规则 把路由规则解析成键值对的形式存储到routeMap中
this.options.routes.forEach(route => {
this.routeMap[route.path] = route.component
});
}
router-link
Vue 的构建版本
- 运行时版: 不支持template模板,需要打包的时候提前编译
- 完整版:包含运行时和编译器,体积比运行时版本大10K左右,程序运行的时候把模板转换成render函数。
initComponent(Vue){
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)
this.$router.data.current=this.to
e.preventDefault()
}
}
// template:"<a :href='to'><slot></slot><>"
})
}
router-view
initComponent(Vue){
const self = this
Vue.component("router-view",{
render(h){
// self.data.current
const cm=self.routeMap[self.data.current]
return h(cm)
}
})
}
initEvent
initEvent(){
//
window.addEventListener("popstate",()=>{
this.data.current = window.location.pathname
})
}