VS vode特点:
开源,免费;
自定义配置
集成git
智能提示强大
支持各种文件格式(html/jade/css/less/sass/xml)
调试功能强大
各种方便的快捷键
强大的插件扩展
对前端这么友好,没理由不用。
Visual Studio Code(VScode )官网 :https://code.visualstudio.com/
Visual Studio Code(VScode )github地址 :https://github.com/Microsoft/vscode
二.怎么安装插件?
方法一:
按F1或Ctrl+Shift+p,输入extensions,点击第一个就可以
方法二:
ctrl + P 然后输入 >ext install
方法三:
点击图中位置
三.插件合集
插件官网:https://marketplace.visualstudio.com/
每一个插件名都超链接到官网,注意查看
a.配置类插件:
1.Settings Sync
最好用的插件,没有之一,一台电脑配置好之后,其它的几台电脑都不用配置。新机器登录一下就搞定了。再也不用折腾环境了,
使用GitHub Gist同步多台计算机上的设置,代码段,主题,文件图标,启动,键绑定,工作区和扩展。
2.Debugger for Chrome
从VS Code调试在Google Chrome中运行的JavaScript代码。
用于在Google Chrome浏览器或支持Chrome DevTools协议的其他目标中调试JavaScript代码的VS Code扩展。
3.beautify
格式化代码工具
美化javascript,JSON,CSS,Sass,和HTML在Visual Studio代码。
4.Atuo Rename Tag
修改 html 标签,自动帮你完成头部和尾部闭合标签的同步修改
5.中文(简体)语言包
Chinese (Simplified) Language Pack for Visual Studio Code
将界面转换为中文,对英语不好的人,非常友好。例如我。。。
6.Code Spell Checker
代码拼写检查器
一个与camelCase代码配合良好的基本拼写检查程序。
此拼写检查程序的目标是帮助捕获常见的拼写错误,同时保持误报数量较低。
7.vscode-icons
显示Visual Studio代码的图标,目前该插件已被vscode内部支持:"文件" -> "首选项" -> "文件图标主题"
8.guides
显示代码对齐辅助线,很好用
9.Rainbow Brackets
为圆括号,方括号和大括号提供彩虹色。这对于Lisp或Clojure程序员,当然还有JavaScript和其他程序员特别有用。
效果如下:
10.Bracket Pair Colorizer
用于着色匹配括号
11.Indent-Rainbow
用四种不同颜色交替着色文本前面的缩进
12.filesize
在状态栏中显示当前文件大小,点击后还可以看到详细创建、修改时间
13.Import Cost
对引入的计算大小
14.Path Intellisense
可自动填充文件名。
15.WakaTime
从您的编程活动自动生成的度量标准,见解和时间跟踪。
16.GitLens
git日志查看插件
GitLens 增强了 Visual Studio Code 中内置的 Git 功能。例如 commits 搜索,历史记录和和查看代码作者身份,还能通过强大的比较命令获得有价值的见解等等
17..REST Client
REST客户端允许您直接发送HTTP请求并在Visual Studio Code中查看响应。
18.Npm Intellisense
用于在 import 语句中自动填充 npm 模块
require 时的包提示(最新版的vscode已经集成此功能)
19.Azure Storage
VS Code的Azure存储扩展允许您部署静态网站并浏览Azure Blob容器,文件共享,表和队列。按照本教程从VS Code部署Web应用程序到Azure存储。
20.Project Manager
它可以帮助您轻松访问项目,无论它们位于何处。不要再错过那些重要的项目了。您可以定义自己的收藏项目,或选择自动检测VSCode项目,Git,Mercurial和SVN存储库或任何文件夹。
从版本8开始,您就有了专门的项目活动栏!
以下是Project Manager提供的一些功能:
将任何项目保存为收藏夹
自动检测VSCode,GIT中,水银或SVN存放区
在相同或新窗口中打开项目
识别已删除/重命名的项目
一个状态栏标识当前项目
专门的活动栏
21.Language Support for Java(TM) by Red Hatredhat.java
这个插件,这个下载次数,安装就对了。
22.Todo Tree
此扩展可以快速搜索(使用ripgrep)您的工作区以获取TODO和FIXME等注释标记,并在资源管理器窗格的树视图中显示它们。单击树中的TODO将打开文件并将光标放在包含TODO的行上。
找到的TODO也可以在打开的文件中突出显示。
b.VS code 主题集合
1.Night Owl
一个非常适合夜猫子的 VS Code 主题。像是为喜欢深夜编码的人精心设计的。
2.Atom One Dark Theme
一个基于Atom的黑暗主题
3.Dracula Official
官方吸血鬼主题,博主用的就是这款,很漂亮
4.One Dark Pro
Atom标志性的One Dark主题,也是VS Code下载次数最多的主题之一!
5.Bimbo
简约而现代的神奇海洋主题
c.代码提示提示类
1.HTML Snippets
完整的HTML代码提示,包括HTML5
2.HTML CSS Support
在 html 标签上写class 智能提示css样式
3.jQuery Code Snippets
jQuery代码提示
超过130个用于JavaScript代码的jQuery代码片段。
只需键入字母'jq'即可获得所有可用jQuery代码片段的列表。
4.HTMLHint
html代码检测,支持html5
d.语言相关
1.C#
适用于.NET Core的轻量级开发工具。
伟大的C#编辑支持,包括语法突出显示,智能感知,转到定义,查找所有引用等。
调试支持.NET Core(CoreCLR)。注意:不支持单声道调试。桌面CLR调试支持有限。
支持Windows,macOS和Linux上的project.json和csproj项目。
2.CodeMetrics
计算TypeScript / JavaScript文件的复杂性。
3.VUE插件
vetur 语法高亮、智能感知、Emmet等
VueHelper snippet代码片段
ESLint 将ESLint JavaScript集成到VS代码中。
prettier 代码规范性插件
4. Java Extension Pack
它是一组流行的扩展,可以帮助在Visual Studio Code中编写,测试和调试Java应用程序。查看VS Code中的Java以开始使用。
作者:杀死哪个崇明岛人
https://www.bilibili.com/read/cv11058549
出处: bilibili
柜 U 位管理是一项突破性创新技术--继承了 RFID 标签(电子标签)的优点的同时,完全解决了 RFID 技术(非接触式的自动识别技术)在机房 U 位资产监控场应用景中的四大缺陷,采用工业互联网云平台监控机房 U 位的方法,具有高可靠性、高准确性、精准定位、免维护的特点,满足了 U 位级实时监控、智能运维闭环管理的需求。设备上架、下架与迁移,自动变更和实时记录,(用户评价):部署工业互联网云平台监控机房 U 位后节省了 99% 的登记变更记录的时间,而且实现了变更后数据 100% 的准确,在这之前是难以想象的,真正实现运维管理最后的工作。
整个 Demo 由最左侧的树,中间部分的列表以及右边的拓扑图整体构成,为了让整个布局干净一点,这里结合 splitView 和 borderPane 两种布局方式来进行。首先将场景分为左右两个部分,左边为树,右边是列表和拓扑图的组合:
布局结束记得将最外层组件的最底层 div 添加到 body 中,HT 的组件一般都会嵌入 BorderPane、SplitView 和 TabView 等容器中使用,而最外层的HT组件则需要用户手工将 getView() 返回的底层 div 元素添加到页面的 DOM 元素中,这里需要注意的是,当父容器大小变化时,如果父容器是 BorderPane 和 SplitView 等这些HT预定义的容器组件,则HT的容器会自动递归调用孩子组件 invalidate 函数通知更新。但如果父容器是原生的 html 元素, 则 HT 组件无法获知需要更新,因此最外层的 HT 组件一般需要监听 window 的窗口大小变化事件,调用最外层组件 invalidate 函数进行更新。为了最外层组件加载填充满窗口的方便性,HT 的所有组件都有 addToDOM 函数,其实现逻辑如下,其中 iv 是 invalidate 的简写:
右边的拓扑图部分是在监听选中变化事件的时候更新的,当然,初始化设置的选中树上的第一个节点就触发了选中变化事件:
上面代码中 splitView.setRightView 函数意为设置右侧组件,有了这个函数,我就可以动态地改变 spliteView 组件中的右侧组件了。
端路由 前端路由是后来发展到SPA(单页应用)时才出现的概念。 SPA 就是一个WEB项目只有一个 HTML 页面,一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转。 前端路由在SPA项目中是必不可少的,页面的跳转、刷新都与路由有关,通过不同的url显示相应的页面。 优点:前后端的彻底分离,不刷新页面,用户体验较好,页面持久性较好。 后端路由 当在地址栏切换不同的url时,都会向服务器发送一个请求,服务器接收并响应这个请求,在服务端拼接好html文件返回给页面来展示。 优点:减轻了前端的压力,html都由后端拼接; 缺点:依赖于网络,网速慢,用户体验很差,项目比较庞大时,服务器端压力较大, 不能在地址栏输入指定的url访问相应的模块,前后端不分离。 路由模式 前端路由实现起来其实很简单,本质是监听 URL 的变化,然后匹配路由规则,在不刷新的情况下显示相应的页面。 hash模式(对应HashHistory)
// onhashchage事件,可以在window对象上监听这个事件
window.onhashchange=function(event){
console.log(event.oldURL, event.newURL)
let hash=location.hash.slice(1)
}
history模式 (对应HTML5History)
history.replaceState({}, null, '/b') // 替换路由
history.pushState({}, null, '/a') // 路由压栈,记录浏览器的历史栈 不刷新页面
history.back() // 返回
history.forward() // 前进
history.go(-2) // 后退2次
history.pushState 修改浏览器地址,而页面的加载是通过 onpopstate 事件监听实现,加载对应的页面内容,完成页面更新。
// 页面加载完毕 first.html
history.pushState({page: 1}, "", "first.html");
window.onpopstate=function(event) {
// 根据当前 URL 加载对应页面
loadPage(location.pathname);
};
// 点击跳转到 second.html
history.pushState({page: 2}, "", "second.html");
function loadPage(url) {
// 加载 url 对应页面内容
// 渲染页面
}
onpopstate 事件是浏览器历史导航的核心事件,它标识了页面状态的变化时机。通过监听这个时机,根据最新的状态信息更新页面 当使用 history.pushState() 或 history.replaceState() 方法修改浏览器的历史记录时,不会直接触发 onpopstate 事件。 但是,可以在调用这些方法时将数据存储在历史记录条目的状态对象中, onpopstate 事件在处理程序中访问该状态对象。这样,就可以在不触发 onpopstate 事件的情况下更新页面内容,并获取到相应的状态值。 history 模式下 404 页面的处理 在 history 模式下,浏览器会向服务器发起请求,服务器根据请求的路径进行匹配: 如果服务器无法找到与请求路径匹配的资源或路由处理器,服务器可以返回 /404 路由,跳转到项目中配置的 404 页面,指示该路径未找到。 对于使用历史路由模式的单页应用(SPA),通常会在服务器配置中添加一个通配符路由,将所有非静态资源的请求都重定向到主页或一个自定义的 404 页面,以保证在前端处理路由时不会出现真正的 404 错误页面。 在项目中配置对应的 404 页面:
export const publicRoutes=[
{
path: '/404',
component: ()=> import('src/views/404/index'),
},
]
vueRouter Vue Router 是 Vue.js 的官方路由。它与 Vue.js 核心深度集成,允许你在 Vue 应用中构建单页面应用(SPA),并且提供了灵活的路由配置和导航功能。让用 Vue.js 构建单页应用变得轻而易举。功能包括:
路由组件
$router 、$route $route: 是当前路由信息对象,获取和当前路由有关的信息。 route 为属性是只读的,里面的属性是 immutable (不可变) 的,不过可以通过 watch 监听路由的变化。
fullPath: "" // 当前路由完整路径,包含查询参数和 hash 的完整路径
hash: "" // 当前路由的 hash 值 (锚点)
matched: [] // 包含当前路由的所有嵌套路径片段的路由记录
meta: {} // 路由文件中自赋值的meta信息
name: "" // 路由名称
params: {} // 一个 key/value 对象,包含了动态片段和全匹配片段就是一个空对象。
path: "" // 字符串,对应当前路由的路径
query: {} // 一个 key/value 对象,表示 URL 查询参数。跟随在路径后用'?'带的参数
$router是 vueRouter 实例对象,是一个全局路由对象,通过 this.$router 访问路由器, 可以获取整个路由文件或使用路由提供的方法。
// 导航守卫
router.beforeEach((to, from, next)=> {
/* 必须调用 `next` */
})
router.beforeResolve((to, from, next)=> {
/* 必须调用 `next` */
})
router.afterEach((to, from)=> {})
动态导航到新路由
router.push
router.replace
router.go
router.back
router.forward
routes 是 router 路由实例用来配置路由对象 可以使用路由懒加载(动态加载路由)的方式
const router=new VueRouter({
routes: [
{
path: '/home',
name: 'Home',
component:()=import('../views/home')
}
]
})
页面中路由展示位置
<div id="app">
<!-- 添加路由 -->
<!-- 会被渲染为 <a href="#/home"></a> -->
<router-link to="/home">Home</router-link>
<router-link to="/login">Login</router-link>
<!-- 展示路由的内容 -->
<router-view></router-view>
</div>
路由模块 引入 vue-router,使用 Vue.use(VueRouter) 注册路由插件 定义路由数组,并将数组传入VueRouter 实例,并将实例暴露出去
import Vue from 'vue'
import VueRouter from 'vue-router'
import { hasVisitPermission, isWhiteList } from './permission'
// 注册路由组件
Vue.use(VueRouter)
// 创建路由: 每一个路由规则都是一个对象
const routers=[
// path 路由的地址
// component 路由的所展示的组件
{
path: '/',
// 当访问 '/'的时候 路由重定向 到新的地址 '/home'
redirect: '/home',
},
{
path: '/home',
component: home,
},
{
path: '/login',
component: login,
},
],
// 实例化 VueRouter 路由
const router=new VueRouter({
mode: 'history',
base: '/',
routers
})
// 路由守卫
router.beforeEach(async (to, from, next)=> {
// 清除面包屑导航数据
store.commit('common/SET_BREAD_NAV', [])
// 是否白名单
if (isWhiteList(to)) {
next()
} else {
// 未登录,先登录
try {
if (!store.state.user.userInfo) {
await store.dispatch('user/getUserInfo')
}
// 登录后判断,是否有访问页面的权限
if (!hasVisitPermission(to, store.state.user.userInfo)) {
next({ path: '/404' })
} else {
next()
}
} catch (err) {
$error(err)
}
}
})
export default router
在 main.js 上挂载路由 将VueRouter实例引入到main.js,并注册到根Vue实例上
import router from './router'
new Vue({
router,
store,
render: h=> h(App),
}).$mount('#app')
动态路由 我们经常需要把某种模式匹配到的所有路由,全都映射到同个组件。例如,我们有一个 User 组件,对于所有 ID 各不相同的用户,都要使用这个组件来渲染。我们可以在 vueRrouter 的路由路径中使用“动态路径参数”(dynamic segment) 来达到这个效果。
当一个路由被匹配时,它的 params 的值将在每个组件中以 this.$route.query 的形式暴露出来。因此,我们可以通过更新 User 的模板来呈现当前的用户 ID:
const routes=[
{
path: '/user/:id'
name: 'User'
components: User
}
]
_vue-router _通过配置 _params _和 _query _来实现动态路由
// 传递参数
this.$router.push({
name: Home,
params: {
number: 1 ,
code: '999'
}
})
// 接收参数
const p=this.$route.params
// 方式一:路由拼接
this.$router.push('/home?username=xixi&age=18')
// 方式二:name + query 传参
this.$router.push({
name: Home,
query: {
username: 'xixi',
age: 18
}
})
// 方式三:path + name 传参
this.$router.push({
path: '/home',
query: {
username: 'xixi',
age: 18
}
})
// 接收参数
const q=this.$route.query
keep-alive keep-alive是vue中的内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染DOM。 keep-alive 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。 和 transition 相似,keep-alive 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。 keep-alive 可以设置以下props属性:
在不缓存组件实例的情况下,每次切换都会重新 render,执行整个生命周期,每次切换时,重新 render,重新请求,必然不满足需求。 会消耗大量的性能 keep-alive 的基本使用 只是在进入当前路由的第一次render,来回切换不会重新执行生命周期,且能缓存router-view的数据。 通过 include 来判断是否匹配缓存的组件名称: 匹配首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称 (父组件 components 选项的键值),匿名组件不能被匹配
<keep-alive>
<router-view></router-view>
</keep-alive>
<keep-alive include="a,b">
<component :is="view"></component>
</keep-alive>
<!-- 正则表达式 (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
<component :is="view"></component>
</keep-alive>
<!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
<component :is="view"></component>
</keep-alive>
在路由中设置 keepAlive 属性判断是否需要缓存
{
path: 'list',
name: 'itemList', // 列表页
component (resolve) {
require(['@/pages/item/list'], resolve)
},
meta: {
keepAlive: true,
compName: 'ItemList'
title: '列表页'
}
}
{
path: 'management/class_detail/:id/:activeIndex/:status',
name: 'class_detail',
meta: {
title: '开班详情',
keepAlive: true,
compName: 'ClassInfoDetail',
hideInMenu: true,
},
component: ()=> import('src/views/classManage/class_detail.vue'),
},
使用
<div id="app" class='wrapper'>
<keep-alive>
<!-- 需要缓存的视图组件 -->
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<!-- 不需要缓存的视图组件 -->
<router-view v-if="!$route.meta.keepAlive"></router-view>
</div>
keepAlive 对生命周期的影响 设置缓存后组件加载的生命周期会新增 actived 与 deactived
keep-alive 组件监听 include 及 exclude 的缓存规则,若发生变化则执行 pruneCache (遍历cache 的name判断是否需要缓存,否则将其剔除) 且 keep-alive 中没有 template,而是用了 render,在组件渲染的时候会自动执行 render 函数,
动态路由缓存的的具体表现在:
如何删除 keep-alive 中的缓存 vue2 中清除路由缓存
在组件内可以通过 this 获取 vuerouter 的缓存
vm.$vnode.parent.componentInstance.cache
或者通过 ref 获取 外级 dom
添加图片注释,不超过 140 字(可选)
<template>
<el-container id="app-wrapper">
<Aside />
<el-container>
<el-header id="app-header" height="45px">
<Header @removeCacheRoute="removeCacheRoute" />
</el-header>
<!-- {{ includeViews }} -->
<el-main id="app-main">
<keep-alive :include="includeViews">
<router-view ref="routerViewRef" :key="key" />
</keep-alive>
</el-main>
</el-container>
</el-container>
</template>
<script>
import Aside from './components/Aside'
import Header from './components/Header'
import { mapGetters } from 'vuex'
export default {
name: 'Layout',
components: {
Aside,
Header,
},
data () {
return {
}
},
computed: {
...mapGetters(['cacheRoute', 'excludeRoute']),
includeViews () {
return this.cacheRoute.map(item=> item.compName)
},
key () {
return this.$route.fullPath
},
},
methods: {
removeCacheRoute (fullPath) {
const cache=this.$refs.routerViewRef.$vnode.parent.componentInstance.cache
delete cache[fullPath]
},
},
}
</script>
路由守卫 导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的。 通俗来讲:路由守卫就是路由跳转过程中的一些生命周期函数(钩子函数),我们可以利用这些钩子函数帮我们实现一些需求。 路由守卫又具体分为 全局路由守卫、独享守卫 及 组件路由守卫。 全局路由守卫
beforeEach(to,from, next) 在路由跳转前触发,参数包括to,from,next 三个,这个钩子作用主要是用于登录验证。 前置守卫也可以理解为一个路由拦截器,也就是说所有的路由在跳转前都要先被前置守卫拦截。
router.beforeEach(async (to, from, next)=> {
// 清除面包屑导航数据
store.commit('common/SET_BREAD_NAV', [])
// 是否白名单
if (isWhiteList(to)) {
next()
} else {
// 未登录,先登录
try {
if (!store.state.user.userInfo) {
await store.dispatch('user/getUserInfo')
// 登录后判断,是否有角色, 无角色 到平台默认页
if (!store.state.user.userInfo.permissions || !store.state.user.userInfo.permissions.length) {
next({ path: '/noPermission' })
}
}
// 登录后判断,是否有访问页面的权限
if (!hasVisitPermission(to, store.state.user.userInfo)) {
next({ path: '/404' })
} else {
next()
}
} catch (err) {
$error(err)
}
}
})
beforeResolve(to,from, next) 在每次导航时都会触发,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被正确调用。 即在 beforeEach 和 组件内 beforeRouteEnter 之后,afterEach之前调用。 router.beforeResolve 是获取数据或执行任何其他操作的理想位置
router.beforeResolve(async to=> {
if (to.meta.requiresCamera) {
try {
await askForCameraPermission()
} catch (error) {
if (error instanceof NotAllowedError) {
// ... 处理错误,然后取消导航
return false
} else {
// 意料之外的错误,取消导航并把错误传给全局处理器
throw error
}
}
}
})
和beforeEach相反,他是在路由跳转完成后触发,参数包括to, from 由于此时路由已经完成跳转 所以不会再有next。
全局后置守卫对于分析、更改页面标题、声明页面等辅助功能以及许多其他事情都很有用。
router.afterEach((to, from)=> {
// 在路由完成跳转后执行,实现分析、更改页面标题、声明页面等辅助功能
sendToAnalytics(to.fullPath)
})
beforeEnter(to,from, next) 独享路由守卫可以直接在路由配置上定义,但是它只在进入路由时触发,不会在 params、query 或 hash 改变时触发。
const routes=[
{
path: '/users/:id',
component: UserDetails,
// 在路由配置中定义守卫
beforeEnter: (to, from,next)=> {
next()
},
},
]
或是使用数组的方式传递给 beforeEnter ,有利于实现路由守卫的重用
function removeQueryParams(to) {
if (Object.keys(to.query).length)
return { path: to.path, query: {}, hash: to.hash }
}
function removeHash(to) {
if (to.hash) return { path: to.path, query: to.query, hash: '' }
}
const routes=[
{
path: '/users/:id',
component: UserDetails,
beforeEnter: [removeQueryParams, removeHash],
},
{
path: '/about',
component: UserDetails,
beforeEnter: [removeQueryParams],
},
]
组件路由守卫 在组件内使用的钩子函数,类似于组件的生命周期, 钩子函数执行的顺序包括
组件内路由守卫的执行时机:
<template>
...
</template>
export default{
data(){
//...
},
// 在渲染该组件的对应路由被验证前调用
beforeRouteEnter (to, from, next) {
// 此时 不能获取组件实例 this
// 因为当守卫执行前,组件实例还没被创建
next((vm)=>{
// next 回调 在 组件 beforeMount 之后执行 此时组件实例已创建,
// 可以通过 vm 访问组件实例
console.log('A组件中的路由守卫==>> beforeRouteEnter 中next 回调 vm', vm)
)
},
// 可用于检测路由的变化
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用 此时组件已挂载完可以访问组件实例 `this`
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
console.log('组件中的路由守卫==>> beforeRouteUpdate')
next()
},
// 在导航离开渲染该组件的对应路由时调用
beforeRouteLeave (to, from, next) {
// 可以访问组件实例 `this`
console.log('A组件中的路由守卫==>> beforeRouteLeave')
next()
}
}
<style>
...
</style>
注意 beforeRouteEnter 是支持给 next 传递回调的唯一守卫。对于 beforeRouteUpdate 和 beforeRouteLeave 来说,this 已经可用了,所以不支持 传递回调,因为没有必要了
页面加载时路由守卫触发顺序:
添加图片注释,不超过 140 字(可选)
当点击切换路由时: A页面跳转至B页面触发的生命周期及路由守卫顺序:
添加图片注释,不超过 140 字(可选)
路由守卫的触发顺序 beforeRouterLeave-->beforeEach-->beforeEnter-->beforeRouteEnter-->beforeResolve-->afterEach--> beforeCreate (新)-->created (新)-->beforeMount(新) -->beforeRouteEnter中的next回调 -->beforeDestory(旧)-->destoryed(旧)-->mounted(新) 当路由更新时:触发 beforeRouteUpdate 注意: 但凡涉及到有next参数的钩子,必须调用next() 才能继续往下执行下一个钩子,否则路由跳转等会停止。 vueRouter 实现原理 vueRouter 实现的原理就是 监听浏览器中 url 的 hash值变化,并切换对应的组件 1.路由注册 通过vue.use()安装vue-router插件,会执行install方法,并将Vue当做参数传入install方法 Vue.use(VueRouter)===VueRouter.install() src/install.js
export function install (Vue) {
// 确保 install 调用一次
if (install.installed && _Vue===Vue) return
install.installed=true
// 把 Vue 赋值给全局变量
_Vue=Vue
const registerInstance=(vm, callVal)=> {
let i=vm.$options._parentVnode
if (isDef(i) && isDef(i=i.data) && isDef(i=i.registerRouteInstance)) {
i(vm, callVal)
}
}
// 为每个组件混入 beforeCreate 钩子
// 在 `beforeCreate` 钩子执行时 会初始化路由
Vue.mixin({
beforeCreate () {
// 判断组件是否存在 router 对象,该对象只在根组件上有
if (isDef(this.$options.router)) {
// 根路由设置为自己
this._routerRoot=this
// this.$options.router就是挂在根组件上的 VueRouter 实例
this._router=this.$options.router
// 执行VueRouter实例上的init方法,初始化路由
this._router.init(this)
// 很重要,为 _route 做了响应式处理
// 即访问vm._route时会先向dep收集依赖, 而修改_router 会触发组件渲染
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
// 用于 router-view 层级判断
this._routerRoot=(this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
/* 在Vue的prototype上面绑定 $router,
这样可以在任意Vue对象中使用this.$router访问,同时经过Object.defineProperty,将 $router 代理到 Vue
访问this.$router 即访问this._routerRoot._router */
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
/* 同理,访问this.$route即访问this._routerRoot._route */
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
// 全局注册组件 router-link 和 router-view
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
}
2. VueRouter 实例化 在安装插件后,对 VueRouter 进行实例化。
//用户定义的路由配置数组
const Home={ template: '<div>home</div>' }
const Foo={ template: '<div>foo</div>' }
const Bar={ template: '<div>bar</div>' }
// 3. Create the router
const router=new VueRouter({
mode: 'hash',
base: __dirname,
routes: [
{ path: '/', component: Home }, // all paths are defined without the hash.
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar }
]
})
VueRouter 构造函数
src/index.js
// VueRouter 的构造函数
constructor(options: RouterOptions={}) {
// ...
// 路由匹配对象 -- 路由映射表
this.matcher=createMatcher(options.routes || [], this)
// 根据 mode 采取不同的路由方式
let mode=options.mode || 'hash'
this.fallback=mode==='history' && !supportsPushState && options.fallback !==false
if (this.fallback) {
mode='hash'
}
if (!inBrowser) {
mode='abstract'
}
this.mode=mode
switch (mode) {
case 'history':
this.history=new HTML5History(this, options.base)
break
case 'hash':
this.history=new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history=new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !=='production') {
assert(false, `invalid mode: ${mode}`)
}
}
}
在实例化 vueRouter 的过程中 通过 createMatcher 创建路由匹配对象(路由映射表),并且根据 mode 来采取不同的路由方式。
src/create-matcher.js
export function createMatcher (
routes: Array<RouteConfig>,
router: VueRouter
): Matcher {
// 创建路由映射表
const { pathList, pathMap, nameMap }=createRouteMap(routes)
function addRoutes (routes) {
createRouteMap(routes, pathList, pathMap, nameMap)
}
// 路由匹配 找到对应的路由
function match (
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {
//...
}
return {
match,
addRoutes
}
}
createMatcher 函数的作用就是创建路由映射表,然后通过闭包的方式让 addRoutes 和 match函数能够使用路由映射表的几个对象,最后返回一个 Matcher 对象。 在createMatcher中通过使用 createRouteMap() 根据用户配置的路由规则来创建对应的路由映射表,返回对应的 pathList, pathMap, nameMap createRouteMap 构造函数 主要用于创建映射表,根据用户的路由配置规则创建对应的路由映射表 src/create-route-map.js
export function createRouteMap (
routes: Array<RouteConfig>,
oldPathList?: Array<string>,
oldPathMap?: Dictionary<RouteRecord>,
oldNameMap?: Dictionary<RouteRecord>
): {
pathList: Array<string>;
pathMap: Dictionary<RouteRecord>;
nameMap: Dictionary<RouteRecord>;
} {
// 创建映射表
const pathList: Array<string>=oldPathList || []
const pathMap: Dictionary<RouteRecord>=oldPathMap || Object.create(null)
const nameMap: Dictionary<RouteRecord>=oldNameMap || Object.create(null)
// 遍历路由配置,为每个配置添加路由记录
routes.forEach(route=> {
addRouteRecord(pathList, pathMap, nameMap, route)
})
// 确保通配符在最后
for (let i=0, l=pathList.length; i < l; i++) {
if (pathList[i]==='*') {
pathList.push(pathList.splice(i, 1)[0])
l--
i--
}
}
return {
pathList,
pathMap,
nameMap
}
}
// 添加路由记录
function addRouteRecord (
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>,
route: RouteConfig,
parent?: RouteRecord,
matchAs?: string
) {
// 获得路由配置下的属性
const { path, name }=route
const pathToRegexpOptions: PathToRegexpOptions=route.pathToRegexpOptions || {}
// 格式化 url,替换 /
const normalizedPath=normalizePath(
path,
parent,
pathToRegexpOptions.strict
)
// 生成记录对象
const record: RouteRecord={
path: normalizedPath,
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
components: route.components || { default: route.component },
instances: {},
name,
parent,
matchAs,
redirect: route.redirect,
beforeEnter: route.beforeEnter,
meta: route.meta || {},
props: route.props==null
? {}
: route.components
? route.props
: { default: route.props }
}
if (route.children) {
// 递归路由配置的 children 属性,添加路由记录
route.children.forEach(child=> {
const childMatchAs=matchAs
? cleanPath(`${matchAs}/${child.path}`)
: undefined
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
})
}
// 如果路由有别名的话
// 给别名也添加路由记录
if (route.alias !==undefined) {
const aliases=Array.isArray(route.alias)
? route.alias
: [route.alias]
aliases.forEach(alias=> {
const aliasRoute={
path: alias,
children: route.children
}
addRouteRecord(
pathList,
pathMap,
nameMap,
aliasRoute,
parent,
record.path || '/' // matchAs
)
})
}
// 更新映射表
if (!pathMap[record.path]) {
pathList.push(record.path)
pathMap[record.path]=record
}
// 命名路由添加记录
if (name) {
if (!nameMap[name]) {
nameMap[name]=record
} else if (process.env.NODE_ENV !=='production' && !matchAs) {
warn(
false,
`Duplicate named routes definition: ` +
`{ name: "${name}", path: "${record.path}" }`
)
}
}
}
当根组件调用 beforeCreate 钩子函数时,会执行插件安装阶段注入的 beforeCreate 函数
beforeCreate () {
// 在option上面存在router则代表是根组件
if (isDef(this.$options.router)) {
this._routerRoot=this
this._router=this.$options.router
// 执行_router实例的 init 方法 在 VueRouter 构造函数中的 init()
this._router.init(this)
// 为 vue 实例定义数据劫持 让 _router 的变化能及时响应页面的更新
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
// 非根组件则直接从父组件中获取
this._routerRoot=(this.$parent && this.$parent._routerRoot) || this
}
// 通过 registerInstance(this, this)这个方法来实现对router-view的挂载操作:主要用于注册及销毁实例
registerInstance(this, this)
},
在根组件中进行挂载,非根组件从父级中获取,保证全局只有一个 路由实例 初始化时执行,保证页面再刷新时也会进行渲染
init() -- vueRouter 构造函数中的路由初始化
src/index.js
init(app: any /* Vue component instance */) {
// 将当前vm实例保存在app中,保存组件实例
this.apps.push(app)
// 如果根组件已经有了就返回
if (this.app) {
return
}
/* this.app保存当前vm实例 */
this.app=app
// 赋值路由模式
const history=this.history
// 判断路由模式,以哈希模式为例
if (history instanceof HTML5History) {
// 路由跳转
history.transitionTo(history.getCurrentLocation())
} else if (history instanceof HashHistory) {
// 添加 hashchange 监听
const setupHashListener=()=> {
history.setupListeners()
}
// 路由跳转
history.transitionTo(
history.getCurrentLocation(),
setupHashListener,
setupHashListener
)
}
// 该回调会在 transitionTo 中调用
// 对组件的 _route 属性进行赋值,触发组件渲染
history.listen(route=> {
this.apps.forEach(app=> {
app._route=route
})
})
}
init() 核心就是进行路由的跳转,改变 URL 然后渲染对应的组件。 路由初始化:
5.路由跳转 transitionTo src/history/base.js
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
// 获取匹配的路由信息
const route=this.router.match(location, this.current)
// 确认切换路由
this.confirmTransition(route, ()=> {
// 以下为切换路由成功或失败的回调
// 更新路由信息,对组件的 _route 属性进行赋值,触发组件渲染
// 调用 afterHooks 中的钩子函数
this.updateRoute(route)
// 添加 hashchange 监听
onComplete && onComplete(route)
// 更新 URL
this.ensureURL()
// 只执行一次 ready 回调
if (!this.ready) {
this.ready=true
this.readyCbs.forEach(cb=> { cb(route) })
}
}, err=> {
// 错误处理
if (onAbort) {
onAbort(err)
}
if (err && !this.ready) {
this.ready=true
this.readyErrorCbs.forEach(cb=> { cb(err) })
}
})
}
updateRoute (route: Route) {
// 更新当前路由信息 对组件的 _route 属性进行赋值,触发组件渲染
const prev=this.current
this.current=route
this.cb && this.cb(route)
// 路由跳转完成 调用 afterHooks 中的钩子函数
this.router.afterHooks.forEach(hook=> {
hook && hook(route, prev)
})
}
在路由跳转前要先匹配路由信息,在确认切换路由后更新路由信息,触发组件的渲染,最后更新 url
Matcher 中的 match() 在路由配置中匹配到相应的路由则创建对应的路由信息
src/create-matcher.js
function match (
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {
// 序列化 url
// 比如对于该 url 来说 /abc?foo=bar&baz=qux#hello
// 会序列化路径为 /abc
// 哈希为 #hello
// 参数为 foo: 'bar', baz: 'qux'
const location=normalizeLocation(raw, currentRoute, false, router)
const { name }=location
// 如果是命名路由,就判断记录中是否有该命名路由配置
if (name) {
const record=nameMap[name]
// 没找到表示没有匹配的路由
if (!record) return _createRoute(null, location)
const paramNames=record.regex.keys
.filter(key=> !key.optional)
.map(key=> key.name)
// 参数处理
if (typeof location.params !=='object') {
location.params={}
}
if (currentRoute && typeof currentRoute.params==='object') {
for (const key in currentRoute.params) {
if (!(key in location.params) && paramNames.indexOf(key) > -1) {
location.params[key]=currentRoute.params[key]
}
}
}
if (record) {
location.path=fillParams(record.path, location.params, `named route "${name}"`)
return _createRoute(record, location, redirectedFrom)
}
} else if (location.path) {
// 非命名路由处理
location.params={}
for (let i=0; i < pathList.length; i++) {
// 查找记录
const path=pathList[i]
const record=pathMap[path]
// 如果匹配路由,则创建路由
if (matchRoute(record.regex, location.path, location.params)) {
return _createRoute(record, location, redirectedFrom)
}
}
}
// 没有匹配的路由 返回空的路由
return _createRoute(null, location)
}
通过matcher的match方法(有name匹配name,没有就匹配path,然后返回,默认重新生成一条路由返回) 解析用户的路由配置并按照route类型返回,然后路由切换就按照这个route来。 根据匹配的条件创建路由 _createRoute() src/create-matcher.js
function _createRoute (
record: ?RouteRecord,
location: Location,
redirectedFrom?: Location
): Route {
// 根据条件创建不同的路由
if (record && record.redirect) {
return redirect(record, redirectedFrom || location)
}
if (record && record.matchAs) {
return alias(record, location, record.matchAs)
}
return createRoute(record, location, redirectedFrom, router)
}
return {
match,
addRoute,
getRoutes,
addRoutes
}
}
createRoute ()
src/util/route.js
export function createRoute (
record: ?RouteRecord,
location: Location,
redirectedFrom?: ?Location,
router?: VueRouter
): Route {
const stringifyQuery=router && router.options.stringifyQuery
let query: any=location.query || {}
try {
// 深拷贝
query=clone(query)
} catch (e) {}
// 创建路由对象
const route: Route={
name: location.name || (record && record.name),
meta: (record && record.meta) || {},
path: location.path || '/',
hash: location.hash || '',
query,
params: location.params || {},
fullPath: getFullPath(location, stringifyQuery),
matched: record ? formatMatch(record) : []
}
if (redirectedFrom) {
route.redirectedFrom=getFullPath(redirectedFrom, stringifyQuery)
}
// 通过Object.freeze定义的只读对象 route
return Object.freeze(route)
}
// 获得包含当前路由的所有嵌套路径片段的路由记录
// 包含从根路由到当前路由的匹配记录,从上至下
function formatMatch (record: ?RouteRecord): Array<RouteRecord> {
const res=[]
while (record) {
res.unshift(record)
record=record.parent
}
return res
}
至此匹配路由已经完成,我们回到 transitionTo 函数中,接下来执行 confirmTransition
confirmTransition(route: Route, onComplete: Function, onAbort?: Function) {
const current=this.current
// 中断跳转路由函数
const abort=err=> {
if (isError(err)) {
if (this.errorCbs.length) {
this.errorCbs.forEach(cb=> {
cb(err)
})
} else {
warn(false, 'uncaught error during route navigation:')
console.error(err)
}
}
onAbort && onAbort(err)
}
// 如果是相同的路由就不跳转
if (
isSameRoute(route, current) &&
route.matched.length===current.matched.length
) {
this.ensureURL()
return abort()
}
// 通过对比路由解析出可复用的组件,需要渲染的组件,失活的组件
const { updated, deactivated, activated }=resolveQueue(
this.current.matched,
route.matched
)
function resolveQueue(
current: Array<RouteRecord>,
next: Array<RouteRecord>
): {
updated: Array<RouteRecord>,
activated: Array<RouteRecord>,
deactivated: Array<RouteRecord>
} {
let i
const max=Math.max(current.length, next.length)
for (i=0; i < max; i++) {
// 当前路由路径和跳转路由路径不同时跳出遍历
if (current[i] !==next[i]) {
break
}
}
return {
// 可复用的组件对应路由
updated: next.slice(0, i),
// 需要渲染的组件对应路由
activated: next.slice(i),
// 失活的组件对应路由
deactivated: current.slice(i)
}
}
// 导航守卫数组
const queue: Array<?NavigationGuard>=[].concat(
// 失活的组件钩子
extractLeaveGuards(deactivated),
// 全局 beforeEach 钩子
this.router.beforeHooks,
// 在当前路由改变,但是该组件被复用时调用
extractUpdateHooks(updated),
// 需要渲染组件 enter 守卫钩子
activated.map(m=> m.beforeEnter),
// 解析异步路由组件
resolveAsyncComponents(activated)
)
// 保存路由
this.pending=route
// 迭代器,用于执行 queue 中的导航守卫钩子
const iterator=(hook: NavigationGuard, next)=> {
// 路由不相等就不跳转路由
if (this.pending !==route) {
return abort()
}
try {
// 执行钩子
hook(route, current, (to: any)=> {
// 只有执行了钩子函数中的 next,才会继续执行下一个钩子函数
// 否则会暂停跳转
// 以下逻辑是在判断 next() 中的传参
if (to===false || isError(to)) {
// next(false)
this.ensureURL(true)
abort(to)
} else if (
typeof to==='string' ||
(typeof to==='object' &&
(typeof to.path==='string' || typeof to.name==='string'))
) {
// next('/') 或者 next({ path: '/' }) -> 重定向
abort()
if (typeof to==='object' && to.replace) {
this.replace(to)
} else {
this.push(to)
}
} else {
// 这里执行 next
// 通过 runQueue 中的 step(index+1) 执行 next()
next(to)
}
})
} catch (e) {
abort(e)
}
}
// 经典的同步执行异步函数
runQueue(queue, iterator, ()=> {
const postEnterCbs=[]
const isValid=()=> this.current===route
// 当所有异步组件加载完成后,会执行这里的回调,也就是 runQueue 中的 cb()
// 接下来执行 需要渲染组件中的 beforeRouteEnter 导航守卫钩子
const enterGuards=extractEnterGuards(activated, postEnterCbs, isValid)
// beforeResolve 解析路由钩子
const queue=enterGuards.concat(this.router.resolveHooks)
runQueue(queue, iterator, ()=> {
// 跳转完成
if (this.pending !==route) {
return abort()
}
this.pending=null
onComplete(route)
if (this.router.app) {
this.router.app.$nextTick(()=> {
postEnterCbs.forEach(cb=> {
cb()
})
})
}
})
})
}
export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {
const step=index=> {
// 队列中的函数都执行完毕,就执行回调函数
if (index >=queue.length) {
cb()
} else {
if (queue[index]) {
// 执行迭代器,用户在钩子函数中执行 next() 回调
// 回调中判断传参,没有问题就执行 next(),也就是 fn 函数中的第二个参数
fn(queue[index], ()=> {
step(index + 1)
})
} else {
step(index + 1)
}
}
}
// 取出队列中第一个钩子函数
step(0)
}
导航守卫在 确认路由跳转中出现
const queue: Array<?NavigationGuard>=[].concat(
// 失活的组件钩子
/*
* 找出组件中对应的钩子函数, 给每个钩子函数添加上下文对象为组件自身
* 数组降维,并且判断是否需要翻转数组,因为某些钩子函数需要从子执行到父,
* 获得钩子函数数组
*/
extractLeaveGuards(deactivated),
// 全局 beforeEach 钩子, 将函数 push 进 beforeHooks 中。
this.router.beforeHooks,
// 在当前路由改变,但是该组件被复用时调用
extractUpdateHooks(updated),
// 需要渲染组件 beforeEnter 守卫钩子
activated.map(m=> m.beforeEnter),
// 解析异步路由组件
resolveAsyncComponents(activated)
)
先执行失活组件 deactivated 的钩子函数 ,找出对应组件中的钩子函数
function extractLeaveGuards(deactivated: Array<RouteRecord>): Array<?Function> {
// 传入需要执行的钩子函数名 失活组件触发 beforeRouteLeave
return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
}
function extractUpdateHooks (updated: Array<RouteRecord>): Array<?Function> {
return extractGuards(updated, 'beforeRouteUpdate', bindGuard)
}
function extractGuards(
records: Array<RouteRecord>,
name: string,
bind: Function,
reverse?: boolean
): Array<?Function> {
const guards=flatMapComponents(records, (def, instance, match, key)=> {
// 找出组件中对应的钩子函数
const guard=extractGuard(def, name)
if (guard) {
// 给每个钩子函数添加上下文对象为组件自身
return Array.isArray(guard)
? guard.map(guard=> bind(guard, instance, match, key))
: bind(guard, instance, match, key)
}
})
// 数组降维,并且判断是否需要翻转数组
// 因为某些钩子函数需要从子执行到父
return flatten(reverse ? guards.reverse() : guards)
}
export function flatMapComponents (
matched: Array<RouteRecord>,
fn: Function
): Array<?Function> {
// 数组降维
return flatten(matched.map(m=> {
// 将组件中的对象传入回调函数中,获得钩子函数数组
return Object.keys(m.components).map(key=> fn(
m.components[key],
m.instances[key],
m, key
))
}))
}
执行全局 beforeEach 钩子函数, 将函数 push 进 beforeHooks 中。
beforeEach(fn: Function): Function {
return registerHook(this.beforeHooks, fn)
}
function registerHook(list: Array<any>, fn: Function): Function {
list.push(fn)
return ()=> {
const i=list.indexOf(fn)
if (i > -1) list.splice(i, 1)
}
}
export function resolveAsyncComponents (matched: Array<RouteRecord>): Function {
return (to, from, next)=> {
let hasAsync=false
let pending=0
let error=null
// 扁平化数组 获取 组件中的钩子函数数组
flatMapComponents(matched, (def, _, match, key)=> {
// 判断是否是异步组件
if (typeof def==='function' && def.cid===undefined) {
// 异步组件
hasAsync=true
pending++
// 成功回调
// once 函数确保异步组件只加载一次
const resolve=once(resolvedDef=> {
if (isESModule(resolvedDef)) {
resolvedDef=resolvedDef.default
}
// 判断是否是构造函数
// 不是的话通过 Vue 来生成组件构造函数
def.resolved=typeof resolvedDef==='function'
? resolvedDef
: _Vue.extend(resolvedDef)
// 赋值组件
// 如果组件全部解析完毕,继续下一步
match.components[key]=resolvedDef
pending--
if (pending <=0) {
next()
}
})
// 失败回调
const reject=once(reason=> {
const msg=`Failed to resolve async component ${key}: ${reason}`
process.env.NODE_ENV !=='production' && warn(false, msg)
if (!error) {
error=isError(reason)
? reason
: new Error(msg)
next(error)
}
})
let res
try {
// 执行异步组件函数
res=def(resolve, reject)
} catch (e) {
reject(e)
}
if (res) {
// 下载完成执行回调
if (typeof res.then==='function') {
res.then(resolve, reject)
} else {
const comp=res.component
if (comp && typeof comp.then==='function') {
comp.then(resolve, reject)
}
}
}
}
})
// 不是异步组件直接下一步
if (!hasAsync) next()
}
}
异步组件解析后会执行 runQueue 中的回调函数
// 经典的同步执行异步函数
runQueue(queue, iterator, ()=> {
const postEnterCbs=[] // 存放beforeRouteEnter 中的回调函数
const isValid=()=> this.current===route
// 当所有异步组件加载完成后,会执行这里的回调,也就是 runQueue 中的 cb()
// 接下来执行 需要渲染组件中的 beforeRouteEnter 导航守卫钩子
const enterGuards=extractEnterGuards(activated, postEnterCbs, isValid)
// beforeResolve 导航守卫钩子
const queue=enterGuards.concat(this.router.resolveHooks)
runQueue(queue, iterator, ()=> {
// 跳转完成
if (this.pending !==route) {
return abort()
}
this.pending=null
onComplete(route)
if (this.router.app) {
this.router.app.$nextTick(()=> {
postEnterCbs.forEach(cb=> {
cb()
})
})
}
})
})
但是该钩子函数在路由确认执行,是唯一一个支持在 next 回调中获取 this 对象的函数。
// beforeRouteEnter 钩子函数
function extractEnterGuards (
activated: Array<RouteRecord>,
cbs: Array<Function>,
isValid: ()=> boolean
): Array<?Function> {
return extractGuards(activated, 'beforeRouteEnter', (guard, _, match, key)=> {
return bindEnterGuard(guard, match, key, cbs, isValid)
})
}
function bindEnterGuard (
guard: NavigationGuard,
match: RouteRecord,
key: string,
cbs: Array<Function>,
isValid: ()=> boolean
): NavigationGuard {
return function routeEnterGuard (to, from, next) {
return guard(to, from, cb=> {
next(cb)
if (typeof cb==='function') {
// 判断 cb 是否是函数
// 是的话就 push 进 postEnterCbs
cbs.push(()=> {
// #750
// if a router-view is wrapped with an out-in transition,
// the instance may not have been registered at this time.
// we will need to poll for registration until current route
// is no longer valid.
// 循环直到拿到组件实例
poll(cb, match.instances, key, isValid)
})
}
})
}
}
// 该函数是为了解决 issus #750
// 当 router-view 外面包裹了 mode 为 out-in 的 transition 组件
// 会在组件初次导航到时获得不到组件实例对象
function poll (
cb: any, // somehow flow cannot infer this is a function
instances: Object,
key: string,
isValid: ()=> boolean
) {
if (instances[key]) {
cb(instances[key])
} else if (isValid()) {
// setTimeout 16ms 作用和 nextTick 基本相同
setTimeout(()=> {
poll(cb, instances, key, isValid)
}, 16)
}
}
updateRoute (route: Route) {
// 更新当前路由信息 对组件的 _route 属性进行赋值,触发组件渲染
const prev=this.current
this.current=route
this.cb && this.cb(route) // 实际执行 init传入的回调, app._route=route 对组件的 _route 属性进行赋值
// 路由跳转完成 调用 afterHooks 中的钩子函数
this.router.afterHooks.forEach(hook=> {
hook && hook(route, prev)
})
}
this.cb 是怎么来的呢? 其实 this.cb 是通过 History.listen 实现的,在VueRouter 的初始化 init 过程中对 this.cb 进行了赋值
// History 类中 的listen 方法对this.cb 进行赋值
listen (cb: Function) {
this.cb=cb
}
// init 中执行了 history.listen,将回调函数赋值给 this.cb
init (app: any /* Vue component instance */) {
this.apps.push(app)
history.listen(route=> {
this.apps.forEach((app)=> {
app._route=route
})
})
}
当app._router 发生变化时触发 vue 的响应式调用render() 将路由相应的组件渲染到中
app._route=route
hash模式的原理是监听浏览器url中hash值的变化,并切换对应的组件
class HashHistory extends History {
constructor (router: Router, base: ?string, fallback: boolean) {
super(router, base)
// check history fallback deeplinking
if (fallback && checkFallback(this.base)) {
return
}
ensureSlash()
}
// 监听 hash 的变化
setupListeners () {
const router=this.router
const expectScroll=router.options.scrollBehavior
const supportsScroll=supportsPushState && expectScroll
if (supportsScroll) {
setupScroll()
}
window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', ()=> {
const current=this.current
if (!ensureSlash()) {
return
}
// 传入当前的 hash 并触发跳转
this.transitionTo(getHash(), route=> {
if (supportsScroll) {
handleScroll(this.router, route, current, true)
}
if (!supportsPushState) {
replaceHash(route.fullPath)
}
})
})
}
}
// 如果浏览器没有 # 则自动补充 /#/
function ensureSlash (): boolean {
const path=getHash()
if (path.charAt(0)==='/') {
return true
}
replaceHash('/' + path)
return false
}
export default HashHistory
如果手动刷新页面的话,是不会触发hashchange事件的,也就是找不出组件来,那咋办呢?刷新页面肯定会使路由重新初始化,咱们只需要在初始化函数init 上执行一次原地跳转就行。 router-view 组件渲染 组件渲染的关键在于 router-view ,将路由变化时匹配到的组件进行渲染。 routerView是一个函数式组件,函数式组件没有data,没有组件实例。 因此使用了父组件中的$createElement函数,用以渲染组件,并且在组件渲染的各个时期注册了hook 如果被 keep-alive 包裹则直接使用缓存的 vnode 通过 depth 实现路由嵌套, 循环向上级访问,直到访问到根组件,得到路由的 depth 深度
export default {
name: 'RouterView',
/*
https://cn.vuejs.org/v2/api/#functional
使组件无状态 (没有 data ) 和无实例 (没有 this 上下文)。他们用一个简单的 render 函数返回虚拟节点使他们更容易渲染。
*/
functional: true,
props: {
name: {
type: String,
default: 'default'
}
},
render (_, { props, children, parent, data }) {
/* 标记位,标记是route-view组件 */
data.routerView=true
/* 直接使用父组件的createElement函数 因此router-view渲染的组件可以解析命名槽*/
const h=parent.$createElement
/* props的name,默认'default' */
const name=props.name
/* option中的VueRouter对象 */
const route=parent.$route
/* 在parent上建立一个缓存对象 */
const cache=parent._routerViewCache || (parent._routerViewCache={})
/* 记录组件深度 用于实现路由嵌套 */
let depth=0
/* 标记是否是待用(非alive状态)) */
let inactive=false
/* _routerRoot中中存放了根组件的势力,这边循环向上级访问,直到访问到根组件,得到depth深度 */
// 用 depth 帮助找到对应的 RouterRecord
while (parent && parent._routerRoot !==parent) {
if (parent.$vnode && parent.$vnode.data.routerView) {
// 遇到其他的 router-view 组件则路由深度+1
depth++
}
/* 如果_inactive为true,代表是在keep-alive中且是待用(非alive状态) */
if (parent._inactive) {
inactive=true
}
parent=parent.$parent
}
/* 存放route-view组件的深度 */
data.routerViewDepth=depth
/* 如果inactive为true说明在keep-alive组件中,直接从缓存中取 */
if (inactive) {
return h(cache[name], data, children)
}
// depth 帮助 route.matched 找到对应的路由记录
const matched=route.matched[depth]
/* 如果没有匹配到的路由,则渲染一个空节点 */
if (!matched) {
cache[name]=null
return h()
}
/* 从成功匹配到的路由中取出组件 */
const component=cache[name]=matched.components[name]
// attach instance registration hook
// this will be called in the instance's injected lifecycle hooks
/* 注册实例的registration钩子,这个函数将在实例被注入的加入到组件的生命钩子(beforeCreate与destroyed)中被调用 */
data.registerRouteInstance=(vm, val)=> {
/* 第二个值不存在的时候为注销 */
// val could be undefined for unregistration
/* 获取组件实例 */
const current=matched.instances[name]
if (
(val && current !==vm) ||
(!val && current===vm)
) {
/* 这里有两种情况,一种是val存在,则用val替换当前组件实例,另一种则是val不存在,则直接将val(这个时候其实是一个undefined)赋给instances */
matched.instances[name]=val
}
}
// also register instance in prepatch hook
// in case the same component instance is reused across different routes
;(data.hook || (data.hook={})).prepatch=(_, vnode)=> {
matched.instances[name]=vnode.componentInstance
}
// resolve props
let propsToPass=data.props=resolveProps(route, matched.props && matched.props[name])
if (propsToPass) {
// clone to prevent mutation
propsToPass=data.props=extend({}, propsToPass)
// pass non-declared props as attrs
const attrs=data.attrs=data.attrs || {}
for (const key in propsToPass) {
if (!component.props || !(key in component.props)) {
attrs[key]=propsToPass[key]
delete propsToPass[key]
}
}
}
return h(component, data, children)
}
}
嵌套路由的实现 routerView的render函数通过定义一个depth参数,来判断当前嵌套的路由是位于matched函数层级,然后取出对应的record对象,渲染器对应的组件。 router-link 组件 router-link 的本质是 a 标签,在标签上绑定了click事件,然后执行对应的VueRouter实例的push()实现的
export default {
name: 'RouterLink',
props: {
to: {
type: toTypes,
required: true
},
tag: {
type: String,
default: 'a'
},
exact: Boolean,
append: Boolean,
replace: Boolean, // 当点击时会调用router.replace()而不是router.push(),这样导航后不会留下history记录
activeClass: String,
exactActiveClass: String,
event: {
type: eventTypes,
default: 'click' // 默认为 click 事件
}
},
render (h: Function) {
// 获取 $router 实例
const router=this.$router
// 获取当前路由对象
const current=this.$route
// 要跳转的地址
const { location, route, href }=router.resolve(this.to, current, this.append)
const classes={}
const globalActiveClass=router.options.linkActiveClass
const globalExactActiveClass=router.options.linkExactActiveClass
// Support global empty active class
const activeClassFallback=globalActiveClass==null
? 'router-link-active'
: globalActiveClass
const exactActiveClassFallback=globalExactActiveClass==null
? 'router-link-exact-active'
: globalExactActiveClass
const activeClass=this.activeClass==null
? activeClassFallback
: this.activeClass
const exactActiveClass=this.exactActiveClass==null
? exactActiveClassFallback
: this.exactActiveClass
const compareTarget=location.path
? createRoute(null, location, null, router)
: route
classes[exactActiveClass]=isSameRoute(current, compareTarget)
classes[activeClass]=this.exact
? classes[exactActiveClass]
: isIncludedRoute(current, compareTarget)
const handler=e=> {
// 绑定点击事件
// 若设置了 replace 属性则使用 router.replace 切换路由
// 否则使用 router.push 更新路由
if (guardEvent(e)) {
if (this.replace) {
// router.replace() 导航后不会留下history记录
router.replace(location)
} else {
router.push(location)
}
}
}
const on={ click: guardEvent } // <router-link> 组件默认都支持的click事件
if (Array.isArray(this.event)) {
this.event.forEach(e=> { on[e]=handler })
} else {
on[this.event]=handler
}
const data: any={
class: classes
}
if (this.tag==='a') { // 如果是 a 标签会绑定监听事件
data.on=on // 监听自身
data.attrs={ href }
} else {
// find the first <a> child and apply listener and href
const a=findAnchor(this.$slots.default) // 如果不是 a标签则会 找到第一个 a 标签
if (a) {
// in case the <a> is a static node // 找到第一个 a 标签
a.isStatic=false
const extend=_Vue.util.extend
const aData=a.data=extend({}, a.data)
aData.on=on
const aAttrs=a.data.attrs=extend({}, a.data.attrs)
aAttrs.href=href
} else {
// doesn't have <a> child, apply listener to self
data.on=on // 如果没找到 a 标签就监听自身
}
}
//最后调用$createElement去创建该Vnode
return h(this.tag, data, this.$slots.default)
}
}
// 阻止浏览器的默认事件,所有的事件都是通过 VueRouter 内置代码实现的
function guardEvent (e) {
// don't redirect with control keys
if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return
// don't redirect when preventDefault called
if (e.defaultPrevented) return
// don't redirect on right click
if (e.button !==undefined && e.button !==0) return
// don't redirect if `target="_blank"`
if (e.currentTarget && e.currentTarget.getAttribute) {
const target=e.currentTarget.getAttribute('target')
if (/\b_blank\b/i.test(target)) return
}
// this may be a Weex event which doesn't have this method
if (e.preventDefault) {
e.preventDefault()
}
return true
}
在混入 beforeCreate 时 对 _route 作了响应式处理,即访问vm._route时会先向dep收集依赖
beforeCreate () {
// 判断组件是否存在 router 对象,该对象只在根组件上有
if (isDef(this.$options.router)) {
// 根路由设置为自己
this._routerRoot=this
// this.$options.router就是挂在根组件上的 VueRouter 实例
this._router=this.$options.router
// 执行VueRouter实例上的init方法,初始化路由
this._router.init(this)
// 很重要,为 _route 做了响应式处理
// 即访问vm._route时会先向dep收集依赖, 而修改 _router 会触发组件渲染
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
// 用于 router-view 层级判断
this._routerRoot=(this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
// 访问vm._route时会先向dep收集依赖
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
访问 $router 时触发依赖收集
何时触发 dep.notify 呢? 路由导航实际执行的history.push方法 会触发 tansitionTo
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute }=this
this.transitionTo(location, route=> {
pushState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
在确认路由后执行回调时会通过 updateRoute 触发 this.$route 的修改
updateRoute (route: Route) {
// 更新当前路由信息 对组件的 _route 属性进行赋值,触发组件渲染
const prev=this.current
this.current=route
this.cb && this.cb(route)
this.router.afterHooks.forEach(hook=> {
hook && hook(route, prev)
})
}
其中 this.cb 在路由初始化过程中 通过history.listen 保存的
// VueRouter 路由初始化时设置的 listen 回调
history.listen(route=> {
this.apps.forEach((app)=> {
// $router 的更新==>> app._route=route则触发了set,即触发dep.notify向watcher派发更新
app._route=route
})
})
// history 类中 cb的取值
listen (cb: Function) {
this.cb=cb
}
当组件重新渲染, vue 通过 router-view 渲染到指定位置 综上所述 路由触发组件更新依旧是沿用的vue组件的响应式核心, 在执行transitionTo 前手动触发依赖收集, 在路由transitionTo 过程中手动触发更新派发以达到watcher的重新update; 而之所以路由能正确的显示对应的组件,则得益于路由映射表中保存的路由树形关系 $router.push 切换路由的过程 vue-router 通过 vue.mixin 方法注入 beforeCreate 钩子,该混合在 beforeCreate 钩子中通过 Vue.util.defineReactive() 定义了响应式的 _route 。所谓响应式属性,即当 _route 值改变时,会自动调用 Vue 实例的 render() 方法,更新视图。 vm.render()是根据当前的_route 的 path,nam 等属性,来将路由对应的组件渲染到 router-view 中
History.replace() 在 hash 模式下
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute }=this
this.transitionTo(location, route=> {
replaceHash(route.fullPath)
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
function replaceHash (path) {
if (supportsPushState) {
replaceState(getUrl(path))
} else {
window.location.replace(getUrl(path))
}
}
通过 window.location.replace 替换当前路由,这样不会将新路由添加到浏览器访问历史的栈顶,而是替换掉当前的路由。
history模式下
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute }=this
this.transitionTo(location, route=> {
replaceState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
在地址栏修改 url 时 vueRouter 会发生什么变化
当路由采用 hash 模式时,监听了浏览器 hashChange 事件,在路由发生变化后调用 replaceHash()
// 监听 hash 的变化
setupListeners () {
const router=this.router
const expectScroll=router.options.scrollBehavior
const supportsScroll=supportsPushState && expectScroll
if (supportsScroll) {
setupScroll()
}
window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', ()=> {
const current=this.current
if (!ensureSlash()) {
return
}
// 传入当前的 hash 并触发跳转
this.transitionTo(getHash(), route=> {
if (supportsScroll) {
handleScroll(this.router, route, current, true)
}
if (!supportsPushState) {
replaceHash(route.fullPath)
}
})
})
}
在路由初始化的时候会添加事件 setupHashListener 来监听 hashchange 或 popstate;当路由变化时,会触发对应的 push 或 replace 方法,然后调用 transitionTo 方法里面的 updateRoute 方法来更新 _route,从而触发 router-view 的变化。 所以在浏览器地址栏中直接输入路由相当于代码调用了replace()方法,将路由替换成输入的 url。 在 history 模式下的路由监听是在构造函数中执行的,对 HTML5History 的 popstate 事件进行监听
window.addEventListener('popstate', e=> {
const current=this.current
const location=getLocation(this.base)
if (this.current===START && location===initLocation) {
return
}
this.transitionTo(location, route=> {
if (supportsScroll) {
handleScroll(router, route, current, true)
}
})
})
小结 页面渲染 1、Vue.use(Router) 注册 2、注册时调用 install 方法混入生命周期,定义 router 和 route 属性,注册 router-view 和 router-link 组件 3、生成 router 实例,根据配置数组(传入的routes)生成路由配置记录表,根据不同模式生成监控路由变化的History对象 4、生成 vue 实例,将 router 实例挂载到 vue 实例上面,挂载的时候 router 会执行最开始混入的生命周期函数 5、初始化结束,显示默认页面 路由点击更新 1、 router-link 绑定 click 方法,触发 history.push 或 history.replace ,从而触发 history.transitionTo 方法 2、ransitionTo 用于处理路由转换,其中包含了 updateRoute 用于更新 _route 3、在 beforeCreate 中有劫持 _route 的方法,当 _route 变化后,触发 router-view 的变化 地址变化路由更新 1、HashHistory 和 HTML5History 会分别监控 hashchange 和 popstate 来对路由变化作对用的处理 2、HashHistory 和 HTML5History 捕获到变化后会对应执行 push 或 replace 方法,从而调用 transitionTo 3、然后更新 _route 触发 router-view 的变化 路由相关问题 1. vue-router响应路由参数的变化
// 监听当前路由发生变化的时候执行
watch: {
$route(to, from){
console.log(to.path)
// 对路由变化做出响应
}
}
在组件被复用的情况下,在同一组件中路由动态传参的变化 如: 动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
beforeRouteUpdate(to, from, next){
// to do somethings
}
在每次组件渲染时执行 beforeRouterEnter
beforeRouteEnter(to, from, next){
next(vm=>{
console.log(vm)
// 每次进入路由执行
vm.getData() // 获取数据
})
},
在 keep-alive 组件被激活时都会执行 actived 钩子
服务器端渲染期间 avtived 不被调用
activated(){
this.getData() // 获取数据
},
总结 当时在写这篇文的时候就是想着尽量能把各个知识点都串联上,建立完善的知识体系 这不写着写着就成了长文, 一旦开始就无法停下,那就硬着头皮继续吧 不过这篇长文真的是有够长的,哈哈哈哈,能坚持看到这里的同学我都感到佩服 如果觉得还有哪里缺失的点可以及时告诉我哦 那么今天就先到这啦
*请认真填写需求信息,我们会在24小时内与您取得联系。