路由是一个比较广义和抽象的概念,路由的本质就是对应关系。
在开发中,路由分为:
后端路由
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RFfXWjYv-1682154131731)(images/后端路由.png)]
SPA(Single Page Application)
前端路由
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zlm0nsVA-1682154131732)(images/前端路由.png)]
基于URL中的hash实现(点击菜单的时候改变URL的hash,根据hash的变化控制组件的切换)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ENKZEDjO-1682154131733)(images/基本案例.png)]
案例代码实现如下:
Document
Vue Router(官网:https://router.vuejs.org/zh/)是 Vue.js 官方的路由管理器。 它和 Vue.js 的核心深度集成,可以非常方便的用于SPA应用程序的开发。
基本使用的步骤:
下面看一下具体的实施过程
User Register
var User = { template: 'User' } var Register = { template: 'Register' }
// 创建路由实例对象 var router = new VueRouter({ // routes 是路由规则数组 routes: [ // 每个路由规则都是一个配置对象,其中至少包含 path 和 component 两个属性: // path 表示当前路由规则匹配的 hash 地址 // component 表示当前路由规则对应要展示的组件 {path:'/user',component: User}, {path:'/register',component: Register} ] })
new Vue({ el: '#app', // 为了能够让路由规则生效,必须把路由对象挂载到 vue 实例对象上 router });
完整代码实现如下:
Document User Register
路由重定向指的是:用户在访问地址 A 的时候,强制用户跳转到地址 C ,从而展示特定的组件页面;
通过路由规则的 redirect 属性,指定一个新的路由地址,可以很方便地设置路由的重定向:
var router = new VueRouter({ routes: [ // 其中,path 表示需要被重定向的原地址,redirect 表示将要被重定向到的新地址 //当用户在地址栏中输入`/`,会自动的跳转到`/user`,而`/user`对应的组件为User {path:'/', redirect: '/user'}, {path:'/user',component: User}, {path:'/register',component: Register} ] })
具体实现的代码如下:
Document User Register
嵌套路由功能分析
点击父级路由链接显示模板内容
模板内容中又有子级路由链接
点击子级路由链接显示子级模板内容
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SFU8QZzZ-1682154131734)(images/嵌套路由.png)]
下面看一下实现的步骤
父路由组件模板
User Register
以上的内容,在前面的课程中已经实现。
子级路由模板
const Register = { template: `` }Register 组件
Tab1 Tab2
嵌套路由配置
父级路由通过children属性配置子级路由
const router = new VueRouter({ routes: [ { path: '/user', component: User }, { path: '/register', component: Register, // 通过 children 属性,为 /register 添加子路由规则 children: [ { path: '/register/tab1', component: Tab1 }, { path: '/register/tab2', component: Tab2 } ] } ] })
具体代码实现如下:
Document User Register
思考:
User1 User2 User3
// 定义如下三个对应的路由规则,是否可行??? { path: '/user/1', component: User } { path: '/user/2', component: User } { path: '/user/3', component: User }
虽然以上规则可以匹配成功,但是这样写比较麻烦。如果有100个规则,那么写起来就会非常的麻烦。
通过观察,可以发现整个路由规则中只有后续的数字是在变化的。所以这里可以通过动态路由参数的模式进行路由匹配。
var router = new VueRouter({ routes: [ // 动态路径参数 以冒号开头 { path: '/user/:id', component: User } ] })
const User = { // 路由组件中通过$route.params获取路由参数 template: 'User {{ $route.params.id }}' }
具体代码实现如下:
Document User1 User2 User3 Register
$route与对应路由形成高度耦合,不够灵活,所以可以使用props将组件和路由解耦
第一种情况:
props的值为布尔类型
const router = new VueRouter({ routes: [ // 如果 props 被设置为 true,route.params 将会被设置为组件属性 { path: '/user/:id', component: User, props: true } ] }) const User = { props: ['id'], // 使用 props 接收路由参数 template: '用户ID:{{ id }}' // 使用路由参数 }
在定义路由规则的时候,为其添加了props属性,并将其值设置为true.那么在组件中就可以通过props:['id']的形式来获取对应的参数值。
具体代码如下:
Document User1 User2 User3 Register
第二种情况: props的值为对象类型
const router = new VueRouter({ routes: [ // 如果 props 是一个对象,它会被按原样设置为组件属性 //这里相当于给组件User,通过路由的形式传递了一个对象,而这时候id在User组件中就无法获取到了。 { path: '/user/:id', component: User, props: { uname: 'lisi', age: 12 }} ] }) const User = { props: ['uname', 'age'], template: ‘用户信息:{{ uname + '---' + age}}' }
具体代码实现如下:
Document User1 User2 User3 Register
在上面的代码中,在路由规则中,通过props向用户组件中传递了一个对象,那么在User用户组件中可以接收到传递过来的对象。但是参数id无法接收到。
如果要解决这个问题,可以使用props的值为函数类型。也就是给props传递一个函数。
第三种情况:props的值为函数类型
const router = new VueRouter({ routes: [ // 如果 props 是一个函数,则这个函数接收 route 对象为自己的形参 //route就是参数对象。 { path: '/user/:id', component: User, props: route => ({ uname: 'zs', age: 20, id: route.params.id })} ] }) const User = { props: ['uname', 'age', 'id'], template: ‘用户信息:{{ uname + '---' + age + '---' + id}}' }
完整代码如下:
Document User1 User2 User3 Register
为了更加方便的表示路由的路径,可以给路由规则起一个别名,即为“命名路由”。
const router = new VueRouter({ routes: [ { path: '/user/:id', name: 'user', component: User } ] })
User
完整代码如下:
Document User1 User2 User3 Register
页面导航的两种方式
声明式导航:通过点击链接实现导航的方式,叫做声明式导航
例如:普通网页中的 链接 或 vue 中的
编程式导航:通过调用JavaScript形式的API实现导航的方式,叫做编程式导航
例如:普通网页中的 location.href
编程式导航基本用法
常用的编程式导航 API 如下:
this.$router.push(‘hash地址’)
this.$router.go(n)
const User = { template: '', methods: { goRegister: function(){ // 用编程的方式控制路由跳转 this.$router.push('/register'); } } }
具体吗实现:
Document User1 User2 User3 Register
router.push() 方法的参数规则
// 字符串(路径名称) router.push('/home') // 对象 router.push({ path: '/home' }) // 命名的路由(传递参数) router.push({ name: '/user', params: { userId: 123 }}) // 带查询参数,变成 /register?uname=lisi router.push({ path: '/register', query: { uname: 'lisi' }})
将素材中的代码修改成如下的形式:
基于vue-router的案例
在上面的代码中,我们导入了Vue与Vue-Router的文件。
然后将核心内容定义到App这个组件中,同时创建了路由对象,并且指定了路由的规则。接下来将路由对象挂载到了Vue的实例中。
同时在中使用router-view定义了一个占位符。当输入的地址为/,对应的App组件就会在该占位符中进行展示。
将模板中的菜单修改成路由连接的形式,如下所示:
用户管理 权限管理 商品管理 订单管理 系统设置
基本组件创建如下:
const Users = { template: ``, }; const Rights = { template: `用户管理区域
`, }; const Goods = { template: `权限管理区域
`, }; const Orders = { template: `商品管理区域
`, }; const Settings = { template: `订单管理区域
`, };系统设置区域
我们知道,当单击左侧的菜单时,上面定义的组件将会在右侧进行展示。
所以需要在右侧,添加一个router-view的占位符。
在上一小节中,我们已经将组件都定义好了,下面需要定义其对应的路由规则。
怎样添加对应的路由规则呢?
我们知道整个页面是App根组件渲染出来的,而前面定义的组件,都是在App根组件中进行渲染的,也就是作为了App组件的子组件。
所以,为上一小节中创建的组件添加路由规则,应该是作为App的子路由来进行添加,这样对应的组件才会在App组件中进行渲染。
// 创建路由对象 const router = new VueRouter({ routes: [ { path: "/", component: App, redirect: "/users", children: [ { path: "/users", component: Users }, { path: "/rights", component: Rights }, { path: "/goods", component: Goods }, { path: "/orders", component: Orders }, { path: "/settings", component: Settings }, ], }, ], });
当用户在浏览器的地址栏中输入’/'的时候,会渲染App组件,同时会重定向到/users,从而将Users组件渲染出来,而Users组件是在整个App组件的右侧进行渲染展示。
当点击左侧的菜单时,对应的组件会在右侧进行展示。
这里将用户组件的内容修改成如下形式:
const Users = { data() { return { userlist: [ { id: 1, name: "张三", age: 10 }, { id: 2, name: "李四", age: 20 }, { id: 3, name: "王五", age: 30 }, { id: 4, name: "赵六", age: 40 }, ], }; }, template: ``, };用户管理区域
编号 姓名 年龄 操作 {{item.id}} {{item.name}} {{item.age}} 详情
在Users组件中定义用户数据,并且在模板中通过循环的方式将数据渲染出来。
当单击"详情"链接时,跳转到对应的详情页面。这里需要用到编程式导航的内容。
首先定义用户详情页组件
//用户详情组件 const UserInfo = { props: ["id"], template: `用户详情页 --- 用户Id为:{{id}}
`, methods: { goback() { // 实现后退功能 this.$router.go(-1); }, }, };
在该组件中通过props方式接收传递过来的的用户编号,并且将其打印出来。
同时在该组件中添加一个后退的按钮,通过编程式导航的方式实现后退。
对应的路由规则如下:
// 创建路由对象 const router = new VueRouter({ routes: [ { path: "/", component: App, redirect: "/users", children: [ { path: "/users", component: Users }, { path: "/userinfo/:id", component: UserInfo, props: true }, { path: "/rights", component: Rights }, { path: "/goods", component: Goods }, { path: "/orders", component: Orders }, { path: "/settings", component: Settings }, ], }, ], });
当输入的地址为:'/userinfo/5’的形式是会渲染UserInfo这个组件,同时将props设置为true,表示会传递对应的id值。
UserInfo这个组件也是App组件的子组件,对应的也会在App组件的右侧进行展示。
同时,在Users组件中,给“详情”链接添加对应的单击事件,
const Users = { data() { return { userlist: [ { id: 1, name: "张三", age: 10 }, { id: 2, name: "李四", age: 20 }, { id: 3, name: "王五", age: 30 }, { id: 4, name: "赵六", age: 40 }, ], }; }, methods: { goDetail(id) { console.log(id); this.$router.push("/userinfo/" + id); }, }, template: ``, };用户管理区域
编号 姓名 年龄 操作 {{item.id}} {{item.name}} {{item.age}} 详情
对应goDetail方法中,通过编程式导航跳转到用户详情页面。
完整代码案例:
基于vue-router的案例
Vue-router中的路由守卫,主要是对其内容进行保护,如果没有对应的权限,则不允许访问。
我们首先来看一下全局守卫,也就是所有的路由都会经过全局守卫来进行检测。
//实现全局守卫 router.beforeEach((to, from, next) => { //to:去哪个页面,from来自哪个页面,next继续执行. //判断哪个路由需要进行守卫,这里可以通过元数据方式 if (to.meta.auth) { if (window.isLogin) { next(); } else { next("/login?redirect=" + to.fullPath); } } else { next(); } });
在上面的代码中,创建了路由守卫,但是需要判断的是需要对哪个路由进行守卫,这里就是通过元数据来进行判断的。如果所跳转到的路由有元数据,并且对应的auth属性为true表明是需要进行守卫的,那么下面就需要校验用户是否登录,这里是通过判断否window.isLogin的值是否为true来进行判断的(这里简化了操作,实际应用中应该存储到sessionStorage),如果条件成立则表明用户登录,就继续访问用户希望访问到的页面,否则跳转到登录页面,而且将用户希望访问的页面地址也传递到了登录页面,这样用户登录成功后,可以直接跳转到要访问的页面。
如果没有元数据,则继续访问用户要访问的页面。
// 创建路由对象 const router = new VueRouter({ routes: [ { path: "/login", component: Login }, { path: "/", component: App, redirect: "/users", children: [ { path: "/users", component: Users, meta: { auth: true, }, }, { path: "/userinfo/:id", component: UserInfo, props: true }, { path: "/rights", component: Rights }, { path: "/goods", component: Goods }, { path: "/orders", component: Orders }, { path: "/settings", component: Settings }, ], }, ], });
在上面的代码中,给/users路由添加了元数据。
登录组件创建如下:
const Login = { data() { return { isLogin: window.isLogin, }; }, template: ``, methods: { login() { window.isLogin = true; this.$router.push(this.$route.query.redirect); }, logout() { this.isLogin = window.isLogin = false; }, }, };
当单击登录按钮后,进行将window.isLogin设置为true, 并且进行跳转。
全部代码如下:
基于vue-router的案例
以上是全局守卫,对所有的路由都起作用。
但是,如果项目比较简单,路由规则定义的比较少,可以将守卫定位到某个路由规则内。这就是路由独享守卫
// 创建路由对象 const router = new VueRouter({ routes: [ { path: "/login", component: Login }, { path: "/", component: App, redirect: "/users", children: [ { path: "/users", component: Users, meta: { auth: true, }, beforeEnter(to, from, next) { if (window.isLogin) { next(); } else { next("/login?redirect=" + to.fullPath); } }, }, { path: "/userinfo/:id", component: UserInfo, props: true }, { path: "/rights", component: Rights }, { path: "/goods", component: Goods }, { path: "/orders", component: Orders }, { path: "/settings", component: Settings }, ], }, ], });
在上面的代码中,给/users这个路由守卫,注意这里的方法名为beforeEnter.同时,这里将守卫定义在/users路由规则内,所以不需要对元数据进行判断,只需要判断用户是否登录就可以了。(注意:在进行以上测试时,需要将全局守卫的代码注释掉)
组件内守卫
可以在路由组件内直接定义以下路由导航守卫。
beforeRouteEnter beforeRouteUpdate beforeRouteLeave
将如下的代码直接添加到组件内。
const Users = { data() { return { userlist: [ { id: 1, name: "张三", age: 10 }, { id: 2, name: "李四", age: 20 }, { id: 3, name: "王五", age: 30 }, { id: 4, name: "赵六", age: 40 }, ], }; }, methods: { goDetail(id) { console.log(id); this.$router.push("/userinfo/" + id); }, }, template: `用户管理区域
编号 姓名 年龄 操作 `, beforeRouteEnter(to, from, next) { if (window.isLogin) { next(); } else { next("/login?redirect=" + to.fullPath); } }, }; {{item.id}} {{item.name}} {{item.age}} 详情
在上面的代码中,直接将路由守卫对应的方法添加到了组件中。
注意:在测试之前将路由规则中定义的路由守卫的代码注释掉。
在前面的案例中,我们都是将路由定义好,然后通过路由守卫来判断,某个用户是否登录,从而决定能否访问某个路由规则对应的组件内容。
但是,如果某些路由规则只能用户登录以后才能够访问,那么我们也可以不用提前定义好,而是在登录后,通过addRoutes方法为其动态的添加。
首先这里需要,还需要全局的路由守卫来进行校验判断,只不过这里全局路由守卫的逻辑发生了变化。
router.beforeEach((to, from, next) => { //to:去哪个页面,from来自哪个页面,next继续执行. if (window.isLogin) { //用户已经登录 if (to.path === "/login") { // 用户已经登录了,但是又访问登录页面,这里直接跳转到用户列表页面 next("/"); } else { //用户已经登录,并且访问其它页面,则运行访问 next(); } } else { //用户没有登录,并且访问的就是登录页,则运行访问登录页 if (to.path === "/login") { next(); } else { //用户没有登录,访问其它页面,则跳转到登录页面。 next("/login?redirect=" + to.fullPath); } } });
下面对登录组件进行修改
const Login = { data() { return { isLogin: window.isLogin, }; }, template: ``, methods: { login() { window.isLogin = true; if (this.$route.query.redirect) { //动态添加路由: this.$router.addRoutes([ { path: "/", component: App, redirect: "/users", children: [ { path: "/users", component: Users, meta: { auth: true, }, // beforeEnter(to, from, next) { // if (window.isLogin) { // next(); // } else { // next("/login?redirect=" + to.fullPath); // } // }, }, { path: "/userinfo/:id", component: UserInfo, props: true }, { path: "/rights", component: Rights }, { path: "/goods", component: Goods }, { path: "/orders", component: Orders }, { path: "/settings", component: Settings }, ], }, ]); this.$router.push(this.$route.query.redirect); } else { this.$router.push("/"); } }, logout() { this.isLogin = window.isLogin = false; }, }, };
在登录成功后,通过addRoutes方法动态的添加路由规则,也就是所添加的路由规则只能是在登录以后才能够访问,所以全局守卫的判断条件发生了变化,不在判断是否有元数据,而只是判断是否登录。如果登录了,想访问上面的路由规则,则运行访问,如果没有登录则不允许访问。
注意:对应的原有的路由规则应该注释掉。
完整代码如下:
基于vue-router的案例
利用keepalive做组件缓存,保留组件状态,提高执行效率。
使用include或者exclude时要给组件设置name(这个是组件的名称,组件的名称通过给组件添加name属性来进行设置)
当我们进行路由切换的时候,对应的组件会被重新创建,同时数据也会不断的重新加载。
如果数据没有变化,就没有必要每次都重新发送异步请求加载数据
现在,在App组件中添加keep-alive
因为切换的组件都是在该router-view中进行展示。
下面可以进行验证。
const Rights = { template: `权限管理区域
`, created() { console.log(new Date()); }, };
在Rights组件中,添加了created方法,该方法中输出日期时间,但是我们不断的切换,发现并不是每次都打印日期时间内容。
当然,以上keep-alive的使用方式,是将所有的组件都缓存了,如果只想缓存某个组件,可以采用如下的方式
在上面的代码中,通过include添加了需要缓存的组件的名称,如果有多个在include中可以继续添加,每个组件名称之间用逗号分隔。
以上的含义就是只有goods组件需要被缓存(goods是组件的name值)
const Goods = { name: "goods", template: `商品管理区域
`, created() { console.log(new Date()); }, };
exclude表示的就是除了指定的组件以外(也是组件的name),其它组件都进行缓存。
应用场景
如果未使用keep-alive组件,则在页面回退时仍然会重新渲染页面,触发created钩子,使用体验不好。 在以下场景中使用keep-alive组件会显著提高用户体验,菜单存在多级关系,多见于列表页+详情页的场景如:
生命周期:
activated和deactivated会在keep-alive内所有嵌套的组件中触发
如:B页面是缓存页面
当A页面跳到B页面时,B页面的生命周期:activated(可在此时更新数据)
B页面跳出时,触发deactivated
B页面自身刷新时,会触发created-mouted-activated
前端路由中,不管是什么实现模式,都是客户端的一种实现方式,也就是当路径发生变化的时候,是不会向服务器发送请求的。
如果需要向服务器发送请求,需要用到ajax方式。
两种模式的区别
首先是表现形式的区别
Hash模式
https://www.baidu.com/#/showlist?id=22256
hash模式中路径带有#, #后面的内容作为路由地址。可以通过问号携带参数。
当然这种模式相对来说比较丑,路径中带有与数据无关的符号,例如#与?
History模式
https://www.baidu.com/showlist/22256
History模式是一个正常的路径的模式,如果要想实现这种模式,还需要服务端的相应支持。
下面再来看一下两者原理上的区别。
Hash模式是基于锚点,以及onhashchange事件。
通过锚点的值作为路由地址,当地址发生变化后触发onhashchange事件。
History模式是基于HTML5中的History API
也就是如下两个方法
history.pushState( ) IE10以后才支持
history.replaceState( )
History模式需要服务器的支持,为什么呢?
因为在单页面的应用中,只有一个页面,也就是index.html这个页面,服务端不存在http://www.test.com/login这样的地址,也就说如果刷新浏览器,
请求服务器,是找不到/login这个页面的,所以会出现404的错误。(在传统的开发模式下,输入以上的地址,会返回login这个页面,而在单页面应用中,只有一个页面为index.html)
所以说,在服务端应该除了静态资源外都返回单页应用的index.html
下面我们开始history模式来演示一下对应的问题。
首先添加一个针对404组件的处理
首先在菜单栏中添加一个链接:
用户管理 权限管理 商品管理 订单管理 系统设置 关于
这里我们添加了一个“关于”的链接,但是我们没有为其定义相应的组件,所以这里需要处理404的情况。
const NotFound = { template: ` 你访问的页面不存在!! `, };
在程序中添加了一个针对404的组件。
const router = new VueRouter({ mode: "history", const router = new VueRouter({ mode: "history", routes: [ { path: "/login", component: Login }, { path: "*", component: NotFound }, { path: "/", component: App, redirect: "/users", children: [ { path: "/users", component: Users, meta: { auth: true, }, // beforeEnter(to, from, next) { // if (window.isLogin) { // next(); // } else { // next("/login?redirect=" + to.fullPath); // } // }, }, { path: "/userinfo/:id", component: UserInfo, props: true }, { path: "/rights", component: Rights }, { path: "/goods", component: Goods }, { path: "/orders", component: Orders }, { path: "/settings", component: Settings }, ], }, ], });
在上面的代码中,指定了处理404的路由规则,同时将路由的模式修改成了history模式。同时,启用这里启用了其它的组件的路由规则配置,也就是不在login方法中使用addRoutes方法来动态添加路由规则了。
login 方法修改成如下形式:
login() { // window.isLogin = true; window.sessionStorage.setItem("isLogin", true); if (this.$route.query.redirect) { // //动态添加路由: // this.$router.addRoutes([ // { // path: "/", // component: App, // redirect: "/users", // children: [ // { // path: "/users", // component: Users, // meta: { // auth: true, // }, // // beforeEnter(to, from, next) { // // if (window.isLogin) { // // next(); // // } else { // // next("/login?redirect=" + to.fullPath); // // } // // }, // }, // { path: "/userinfo/:id", component: UserInfo, props: true }, // { path: "/rights", component: Rights }, // { path: "/goods", component: Goods }, // { path: "/orders", component: Orders }, // { path: "/settings", component: Settings }, // ], // }, // ]); this.$router.push(this.$route.query.redirect); } else { this.$router.push("/"); } }
现在已经将前端vue中的代码修改完毕了,下面我们要将页面的内容部署到node.js服务器中。
而且上面的代码中,我们使用了sessionStorage来保存登录用户的信息,不在使用window下的isLogin
对应的data内容下的代码也要修改:
const Login = { data() { return { isLogin: window.sessionStorage.getItem("isLogin"), }; },
路由守卫中的代码进行如下修改:
jsrouter.beforeEach((to, from, next) => { //to:去哪个页面,from来自哪个页面,next继续执行. if (window.sessionStorage.getItem("isLogin")) { //用户已经登录 if (to.path === "/login") { // 用户已经登录了,但是又访问登录页面,这里直接跳转到用户列表页面 next("/"); } else { //用户已经登录,并且访问其它页面,则运行访问 next(); } } else { //用户没有登录,并且访问的就是登录页,则运行访问登录页 if (to.path === "/login") { next(); } else { //用户没有登录,访问其它页面,则跳转到登录页面。 next("/login?redirect=" + to.fullPath); } } });
在上面的代码中,我们也是通过sessionStorage来获取登录信息。
index.html完整代码如下:
基于vue-router的案例
当然,项目的目录结构做了一定的调整,如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h86uILXd-1682154131736)(images/目录.png)]
在web目录下面,存放的是index.html,在webserver目录下面存放的是node代码。
下面看一下具体的node代码的实现。
app.js文件中的代码如下:
const path = require("path"); //导入处理history模式的模块 const history = require("connect-history-api-fallback"); const express = require("express"); const app = express(); //注册处理history模式的中间件 // app.use(history()) //处理静态资源的中间件,处理web目录下的index.html app.use(express.static(path.join(__dirname, "../web"))); app.listen(3000, () => { console.log("服务器开启"); });
connect-history-api-fallback模块的安装如下(注意在上面的代码中还没有使用该模块)
npm install --save connect-history-api-fallback
下面还需要安装express
npm install express
启动服务
node app.js
现在在地址栏中输入:http://localhost:3000就可以访问网站了。
并且当我们去单击左侧的菜单的时候,可以实现页面的切换,同时单击“关于”的时候,会出现NotFound组件中的内容。
经过测试发现好像没有什么问题,那这是什么原因呢?你想一下当我们单击左侧菜单的时候,路由是怎样工作的呢?
因为现在我们开启了路由的history模式,而该模式是通过HTML5中的history中的api来完成路由的操作的,也就是当我们单击菜单的时候,是通过history.pushState( ) 方法来修改地址栏中的地址,实现组件的切换,而且还会把地址保存的历史记录中(也就是可以单击浏览器中后退按钮,实现后退等操作),但是它并不会向服务器发送请求。
所以说现在整个操作都是在客户端完成的。
但是,当我刷新了浏览器以后,会出现怎样的情况呢?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xjsARMDX-1682154131737)(images/请求.png)]
上图的含义就是,当单击浏览器中的刷新按钮的时候,会向服务器发送请求,要求node服务器处理这个地址,但是服务器并没有处理该地址,所以服务器会返回404
以上就是如果vue-router开启了history模式后,出现的问题。
下面解决这个问题,在服务端启用connect-history-api-fallback模块就可以了,如下代码所示:
const path = require("path"); //导入处理history模式的模块 const history = require("connect-history-api-fallback"); const express = require("express"); const app = express(); //注册处理history模式的中间件 app.use(history()); //处理静态资源的中间件 app.use(express.static(path.join(__dirname, "../web"))); app.listen(3000, () => { console.log("服务器开启"); });
服务端的代码做了修改以后,一定要服务端重新启动node app.js
然后经过测试以后发现没有问题了。
那么现在你考虑一下,具体的工作方式是什么?
当我们在服务端开启对history模式的支持以后,我们刷新浏览器,会想服务器发送请求,例如:http://localhost:3000/orders
服务器接收该请求,那么用于服务器开启了history模式,然后服务器会检查,根据该请求所访问的页面是不存在的,所以会将单页面应用的index.html返回给浏览器。浏览器接收index.html页面后,会判断路由地址,发现地址为orders,所以会加载该地址对应的组件内容。
代理服务器
代理服务器:一般是指局域网内部的机器通过代理服务器发送请求到互联网上的服务器,代理服务器一般作用在客户端。应用比如:GoAgent,翻墙神器.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o87rUZMN-1682154131737)(images/1566823356428.png)]
反向代理服务器
**反向代理服务器:**在服务器端接受客户端的请求,然后把请求分发给具体的服务器进行处理,然后再将服务器的响应结果反馈给客户端。Nginx就是其中的一种反向代理服务器软件。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Akrn4xyk-1682154131737)(images/1566823580536.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jmvLZ0Nd-1682154131738)(images/1566823650555.png)]
Nginx简介
Nginx (“engine x”) ,Nginx (“engine x”) 是俄罗斯人Igor Sysoev(塞索耶夫)编写的一款高性能的 HTTP 和反向代理服务器。也是一个IMAP/POP3/SMTP代理服务器;也就是说,Nginx本身就可以托管网站,进行HTTP服务处理,也可以作为反向代理服务器使用。
Nginx的应用现状
淘宝、新浪博客、新浪播客、网易新闻、六间房、56.com、Discuz!、水木社区、豆瓣、YUPOO、海内、迅雷在线 等多家网站使用 Nginx 作为Web服务器或反向代理服务器。
Nginx的特点
**跨平台:**Nginx 可以在大多数 Unix like OS编译运行,而且也有Windows的移植版本。
**配置异常简单:**非常容易上手。配置风格跟程序开发一样,神一般的配置
**非阻塞、高并发连接:**数据复制时,磁盘I/O的第一阶段是非阻塞的。官方测试能够支撑5万并发连接,在实际生产环境中跑到2~3万并发连接数.
高并发:其实就是使用技术手段使得系统可以并行处理很多的请求!衡量指标常用的有响应时间,吞吐量,每秒查询率QPS,并发用户数。响应时间:系统对请求做出响应的时间。你简单理解为一个http请求返回所用的时间。 吞吐量:单位时间内处理的请求数量。 QPS:每秒可以处理的请求数 并发用户数:同时承载正常使用系统功能的用户数量。也就是多少个人同时使用这个系统,这个系统还能正常运行。这个用户数量就是并发用户数了
**内存消耗小:**处理大并发的请求内存消耗非常小。在3万并发连接下,开启的10个Nginx 进程才消耗150M内存(15M*10=150M)。
**成本低廉:**Nginx为开源软件,可以免费使用。而购买F5 BIG-IP、NetScaler等硬件负载均衡交换机则需要十多万至几十万人民币 。
**内置的健康检查功能:**如果 Nginx Proxy 后端的某台 Web 服务器宕机了,不会影响前端访问。
**节省带宽:**支持 GZIP 压缩,可以添加浏览器本地缓存的 Header 头。
**稳定性高:**用于反向代理,宕机的概率微乎其微
Nginx启动
到官网下载Windows版本,下载地址:http://nginx.org/en/download.html
解压到磁盘任一目录(注意:nginx解压后的文件夹不能放在中文目录下。)
修改配置文件
启动服务:
•直接运行nginx.exe
Nginx服务器默认占用的是80端口号,而在window10中端口号80已经被其它的应用程序占用,所以这里可以修改一下Nginx的端口号,在conf目录下找到nginx.conf文件,该文件就是Nginx服务的配置文件,通过该配置文件可以修改Nginx的端口号,当然后期针对Nginx服务器的配置都是通过该文件完成的。
在这里,我将端口号修改成了:8081,所以在浏览器的地址栏中,输入:http://localhost:8081 可以打开默认的欢迎页面,表示Nginx服务启动成功。
下面我们要做的就是将我们的程序部署到Nginx中。
现在,我们可以将做好的网站页面拷贝到Nginx中的html目录中,然后在地址栏中输入:
http://localhost:8081/
就可以看到对应的页面了,然后单击菜单,发现可以进行留有的切换。当单击刷新按钮后,发现出现了404的错误。
原因,点击刷新按钮就会向服务器发送请求,而在服务端没有对应的文件,所以会出现404的错误。
下面我们需要对Nginx服务器进行配置,找到conf目录下的nginx.conf.
然后进行如下的配置:
location / { root html; index index.html index.htm; try_files $uri $uri/ /index.html; }
当在地址栏中输入/的时候,会请求根目录也就是html目录中的index.html.
现在,我们又加了try_files配置,表示尝试访问文件。
$uri表示根据所请求的url地址查找对应文件,如果找到了返回,没有找到。
将$uri作为目录,查找该目录下的index.html,如果找到就返回,没有找到,则直接返回html目录下面的index.html文件。
而现在我们已经将我们做好的页面拷贝到了html目录下面,所以直接将我们的页面返回了。
下面可以进行测试。
测试之前需要重新启动服务器。
打开cmd,然后定位到Nginx的目录,输入以下命令重新启动服务器。
nginx -s reload
这时的执行流程是:当单击浏览器中的刷新按钮后,会向服务器发送请求,服务接收到请求后,发现没有所访问的文件,但是,由于我们配置了try_files,所以会将html目录下面的index.html页面的内容返回,返回给浏览器后,浏览器会根据路由来进行处理,也就是查找对应组件进行渲染。
现在我们已经掌握了Vue Router的基本使用,下面我们来模拟Vue Router的实现,通过模拟实现,来了解其内部的实现原理。
我们这里模拟的是History模式。Hash模式基本实现上是一样的。
这里先来复习一下Hash模式的工作原理。
下面再来复习一下History模式
在模拟Vue Router之前,
首先来看一下Vue Router的核心代码,做一个简单的分析
//注册插件 Vue.use(VueRouter) //创建路由对象 const router=new VueRouter({ routes:[ {name:'home',path:'/',component:homeComponent} ] }) // 创建Vue实例,注册router对象 new Vue({ router, render:h=>h(App) }).$mount('#apps')
我们知道Vue Router是Vue的插件,所以在上面的代码中,我们首先调用use方法注册该插件。
use方法需要的参数可以是一个函数或者是对象,如果传递的是函数,use内部会直接调用该函数,
如果传递的是一个对象,那么在use内部会调用该对象的install方法。
而我们这里传递的是对象,所以后面在模拟VUe Router的时候,要实现一个install
方法。
下面我们创建了VueRouter实例,所以VueRouter可以是构造方法或者是类,那么我们在模拟的时候,将其定义为类。并且该类中有一个静态的install方法,因为我们将VueRouter传递给了use方法。
在VueRouter类的构造方法中,需要有一个参数,该参数是一个对象,该对象中定义了路由的规则。
最后创建了Vue的实例,并且将创建好的Vue Router对象传递到该实例中。
下面我们在看一下Vue Router的类图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x8BowzuB-1682154131738)(images/类图.png)]
在该类图中,上半部分是VueRouter的属性,而下半部分是VueRouter的方法。
options作用是记录构造函数中传入的对象, 我们在创建Vue Router的实例的时候,传递了一个对象,而该对象中定义了路由规则。而options就是记录传入的这个对象的。
routeMap:是一个对象,记录路由地址与组件的对应关系,也就是一个键值对的形式,后期会options中路由规则解析到routeMap中。
data是一个对象,该对象中有一个属性current,该属性用来记录当前的路由地址,data是一个响应式的对象,因为当前路由地址发生变化后,对应的组件要发生更新(也就说当地址变化后,要加载对应组件)。
install是一个静态方法,用来实现Vue的插件机制。
Constructor是一个构造方法,该该构造方法中会初始化options ,data,routeMap这几个属性。
inti方法主要是用来调用下面的三个方法,也就把不同的代码分隔到不同的方法中去实现。
initEvent方法,用来注册popstate事件,
createRouteMap方法,该方法会把构造函数中传入进来的路由规则,转换成键值对的形式存储到routeMap中。 键就是路由的地址,值就是对应的组件
initComponents方法,主要作用是用来创建router-link和router-view这两个组件的。
现在我们已经对Vue Router做了一个分析。
下面开始创建自己的Vue Router.
在vue_router_app项目的src目录下面创建一个vuerouter目录,同时创建一个index.js文件,在该文件中创建如下的代码。
install方法需要的参数是Vue的构造方法。
let _Vue = null; export default class VueRouter { //调用install方法的时候,会传递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() { //在创建Vue实例的时候 // 也就是new Vue()的时候,才会有$options这个属性, //组件中是没有$options这个属性的。 if (this.$options.router) { _Vue.prototype.$router = this.$options.router; } }, }); } }
在介绍VueRouter的类图时,我们说过
Constructor是一个构造方法,该该构造方法中会初始化options ,data,routeMap这几个属性。
constructor(options) { this.options = options; this.routeMap = {}; this.data = _Vue.observable({ current: "/", }); }
createRouteMap方法,该方法会把构造函数中传入进来的options参数中的路由规则,转换成键值对的形式存储到routeMap中。 键就是路由的地址,值就是对应的组件
createRouteMap() { this.options.routes.forEach((route) => { this.routeMap[route.path] = route.component; }); }
initComponents方法,主要作用是用来创建router-link和router-view这两个组件的。
下面先在这个方法中创建router-link这个组件。
先来看一下router-link这个组件的基本使用
用户管理
我们知道,router-link这个组件最终会被渲染成a标签,同时to作为一个属性,其值会作为a标签中的href属性的值。同时还要获取
initComponents(Vue) { Vue.component("router-link", { props: { to: String, }, template: '', }); }
在上面的代码中,我们通过Vue.component来创建router-link这个组件,同时通过props接收to属性传递过来的值,并且对应的类型为字符串。
最终渲染的模板是一个a标签,href属性绑定了to属性的值,同时使用
现在已经将router-link这个组件创建好了。
下面我们需要对我们写的这些代码进行测试。
要进行测试应该先将createRouteMap方法与initComponents方法都调用一次,那么问题是
在什么时候调用这两个方法呢?
我们可以在VueRoute对象创建成功后,并且将VueRouter对象注册到Vue的实例上的时候,调用这两个方法。
也就是在beforeCreate这个钩子函数中。
当然为了调用这两个方便,在这里我们又定义了init方法,来做了一次封装处理。
init() { this.createRouteMap(); this.initComponents(_Vue); }
对init方法的调用如下:
beforeCreate() { //在创建Vue实例的时候 // 也就是new Vue()的时候,才会有$options这个属性, //组件中是没有$options这个属性的。 if (this.$options.router) { _Vue.prototype.$router = this.$options.router; //调用init this.$options.router.init(); } },
this.$options.router.init();
这句代码的含义:this表示的就是Vue实例,$options表示的就是在创建Vue的实例的时候传递的选项,如下所示:
const vm = new Vue({ el: "#app", router, });
通过上面的代码,我们可以看到,传递过来的选项中是有router.
而这个router是什么呢?
const router = new VueRouter({})
就是VueRouter这个类的实例。而我们当前自己所模拟的路由,所创建的类就叫做VueRouter(也就是以后在创建路由实例的时候,使用我们自己创建的VueRouter这个类来完成).
而init方法就是VueRouter这个类的实例方法。所以可以通过this.$options.router.init()的方式来调用。
下面我们来测试一下。
在vue_router_app项目的src目录下面,创建router.js文件,文件定义路由规则.
如下代码所示:
import Vue from "vue"; // import Router from "vue-router"; import Router from "./vuerouter";//注意:这里导入的是自己定义的路由规则 import Login from "./components/Login.vue"; import Home from "./components/Home.vue"; Vue.use(Router); export default new Router({ model: "history", routes: [ { path: "/", component: Home }, { path: "/login", component: Login }, ], });
在components目录下面分别创建Home.vue与Login.vue.
Home.vue 的代码如下:
登录
Login.vue的代码如下:
登录页面
App.vue组件的内容如下:
在main.js 中完成路由的注册。
import Vue from "vue"; import App from "./App.vue"; //导入router.js import router from "./router"; Vue.config.productionTip = false; new Vue({ router, render: (h) => h(App), }).$mount("#app");
运行上面的代码会出现如下的错误:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pkT9fhBC-1682154131739)(images/错误.png)]
第二个错误是我们还没有创建router-view这个组件,所以才会出现该错误。这里暂时可以先不用考虑。
主要是第一个错误,该错误的含义是,目前我们使用的是运行时版本的Vue, 模板编译器不可用。
你可以使用预编译把模板编译成render函数,或者是使用包含编译版本的Vue.
以上错误说明了Vue的构建版本有两个,分别是“运行时版”和"完整版".
运行时版:不支持template模板,需要打包的时候提前编译。
完整版:包含运行时和编译器,体积比运行时版大10k左右,程序运行的时候把模板转换成render函数。性能低于运行时版本。
使用vue-cli创建的项目默认为运行时版本,而我们创建的VueRouter类中有template模板,所以才会出现第一个错误。
官方文档:https://cn.vuejs.org/v2/guide/installation.html
下面我们看一下解决方案:
在前面我们已经提到过,使用vue-cli 创建的项目是运行时项目,所以没有编译器,如果我们将其修改成完整版,就有编译器,对模板进行编译。
解决的方案:在项目的根目录创建vue.config.js文件,在该文件中添加runtimeCompiler配置项,该配置项表示的是,是否使用包含运行时编译器的Vue构建
版本(完整版)。设置为true后你就可以在Vue组件中使用template选项了,但是这会让你的应用额外增加10kb左右。默认该选项的取值为false.
vue.config.js文件配置如下
module.exports = { runtimeCompiler: true, };
表示使用的是完整版,这时编译器会将template选项转换成render函数。
注意:要想以上配置内容起作用,必须重新启动服务器。
npm run serve
虽然使用完整版Vue可以解决上面的问题,但是由于带有编译器,体积比运行时版本大10k左右,所以性能比运行时版要低。
那么这一小节我们使用运行时版本来解决这个问题。
我们知道,完整版中的编译器的作用就是将template模板转成render函数,所以在运行时版本中我们可以自己编写render函数。
但是在这你肯定也有一个问题,就是在单文件组件中,我们一直都是在写,并且没有写render函数,
但是为什么能够正常的工作呢?这时因为在打包的时候,将编译成了render函数,这就是预编译。
最终代码如下:
//该方法需要一个参数为Vue的构造函数。 //当然也可以使用全局的_Vue. initComponents(Vue) { Vue.component("router-link", { props: { to: String, }, // template: '', render(h) { return h( "a", { attrs: { href: this.to, }, }, [this.$slots.default] ); }, }); }
注意:在测试之前一定要将根目录下的vue.config.js文件删除掉,这样当前的环境为“运行时”环境。
router-view组件就是一个占位符。当根据路由规则找到组件后,会渲染到router-view的位置。
在initComponents方法中创建router-view组件
//该方法需要一个参数为Vue的构造函数。 //当然也可以使用全局的_Vue. initComponents(Vue) { Vue.component("router-link", { props: { to: String, }, // template: '', render(h) { return h( "a", { attrs: { href: this.to, }, }, [this.$slots.default] ); }, }); const self = this;//修改this的指向 Vue.component("router-view", { render(h) { //根据当前的路径从routeMap中查找对应的组件. const component = self.routeMap[self.data.current]; //将组件转换成虚拟dom return h(component); }, }); }
下面,我们可以测试一下效果。
当我们单击链接的时候,发现了浏览器进行了刷新操作。表明向服务器发送了请求,而我们单页面应用中是不希望向服务器发送请求。
修改后的initComponents方法如下:
//该方法需要一个参数为Vue的构造函数。 //当然也可以使用全局的_Vue. initComponents(Vue) { Vue.component("router-link", { props: { to: String, }, // template: '', render(h) { return h( "a", { attrs: { href: this.to, }, on: { click: this.clickHandler, }, }, [this.$slots.default] ); }, methods: { clickHandler(e) { history.pushState({}, "", this.to); this.$router.data.current = this.to; //阻止向服务器发送器。 e.preventDefault(); }, }, }); const self = this; Vue.component("router-view", { render(h) { //根据当前的路径从routeMap中查找对应的组件. const component = self.routeMap[self.data.current]; //将组件转换成虚拟dom return h(component); }, }); }
给a标签添加了单击事件。
现在有一个问题就是,当点击浏览器中的后退与前进按钮的时候,地址栏中的地址发生了变化,但是对应的组件没有发生变化。
这时候要解决这个问题, 就需要用到popstate事件
popstate事件,可以发现浏览器历史操作的变化,记录改变后的地址,单击前进或者是后退按钮的时候触发该事件
initEvent() { window.addEventListener("popstate", () => { this.data.current = window.location.pathname; }); }
针对initEvent方法的调用如下:
init() { this.createRouteMap(); this.initComponents(_Vue); this.initEvent(); }
在第二章中,我们已经对VueRouter做了一个基本的实现,通过这个基本的实现,已经对VueRouter的原理有了一个基本的理解。
但是,我们并没有实现路由嵌套的形式,这次我们重点来实现这一点。
源码位置:vue/src/core/global-api/use.js
export function initUse (Vue: GlobalAPI) { //use方法的参数接收的是一个插件,该插件的类型可以是一个函数,也可以是一个对象 Vue.use = function (plugin: Function | Object) { //_installedPlugins数组中存放已经安装的插件。 const installedPlugins = (this._installedPlugins || (this._installedPlugins = [])) //判断一下传递过来的插件是否在installedPlugins中存在,如果存在,则直接返回 if (installedPlugins.indexOf(plugin) > -1) { return this } // additional parameters //将arguments转换成数组,并且将数组中的第一项去除。 const args = toArray(arguments, 1) //把this(也就是Vue,这里是通过Vue.use来调用的)插入到数组中的第一个元素的位置。 args.unshift(this) //这时plugin是一个对象,看一下是否有install这个函数。 if (typeof plugin.install === 'function') { //如果有install这个函数,直接调用 //这里通过apply将args数组中的每一项展开传递给install这个函数。 // plugin.install(args[0],args[1]) //而args[0],就是上面我们所插入的Vue.这一点与我们前面在模拟install方法的时候是一样的。 plugin.install.apply(plugin, args) } else if (typeof plugin === 'function') { //如果plugin是一个函数,则直接通过apply去调用 plugin.apply(null, args) } //将插件存储到installedPlugins数组中。 installedPlugins.push(plugin) return this } }
我们先来看一下vue-router的目录结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jbUgfyGV-1682154131740)(images/vuerouter目录.png)]
我们先来核心的文件。
components目录下面,有两个文件。分别为link.js和view.js文件。
link.js文件创建RouterLink组件
view.js文件创建RouterView组件。
history目录下的文件是记录路由的历史记录(hash.js文件是关于hash模式,html5.js关于html5的方式,base.js公共的内容,abstract.js是在服务端渲染中实现的路由历史记录)。
index.js文件是用来创建VueRouter
install.js文件是关于install方法
我们自己模拟的VueRouter也实现上面的目录结构。
下面先来在index.js文件中实现基本代码。
export default class VueRouter { //在创建VueRouter对象的时候,会传递选项 constructor(options) { //获取routes选项,该选项中定义路由规则 this._options = options.routes || []; } // 注册路由变化的事件。该方法的参数是一个Vue实例,后期完善 init(Vue) {} }
下面实现install.js基本代码(通过查看源代码来实现)
export let _Vue = null; //将其导出,在其它文件中也可以使用Vue实例,而不需要单独的引入Vue的js文件 export default function install(Vue) { //获取Vue构造函数 _Vue = Vue; _Vue.mixin({ //通过混入以后,所有的Vue实例中都会有beforeCreate方法 beforeCreate() { //判断是否为Vue的实例,如果条件成立为Vue的实例,否则为其它对应的组件(因为在创建Vue实例的时候会传递选项) if (this.$options.router) { //通过查看源码发现,Vue的实例会挂在到当前的私有属性_routerRoot属性上 this._routerRoot = this; this._router = this.$options.router; //调用index.js文件中定义的init方法 this._router.init(this); } else { this._routerRoot = this.$parent && this.$parent._routerRoot; } }, }); }
下面需要将install方法挂载到VueRouter上。
import install from "./install"; export default class VueRouter { //在创建VueRouter对象的时候,会传递选项 constructor(options) { //获取routes选项,该选项中定义路由规则 this._routes = options.routes || []; } // 注册路由变化的事件。 init(Vue) {} } //将install方法挂载到VueRouter上 VueRouter.install = install;
下面,我们可以简单实现一下Router-link组件与Router-view组件,来做一个简单的测试。(接下来讲解如下内容)
components目录下的view.js文件。
export default { render(h) { return h("div", "router-view"); }, };
以上是Router-View组件的基本功能,后面在继续完善。
link.js文件的实现如下:
export default { props: { to: { type: String, required: true, }, }, render(h) { //通过插槽获取`a`标签内的文本。 return h("a", { domProps: { href: "#" + this.to } }, [this.$slots.default]); }, };
在install.js文件中,导入上面的组件进行测试。
import View from "./components/view"; import Link from "./components/link"; export let _Vue = null; //将其导出,在其它文件中也可以使用Vue实例,而不需要单独的引入Vue的js文件 export default function install(Vue) { //获取Vue构造函数 _Vue = Vue; _Vue.mixin({ //通过混入以后,所有的Vue实例中都会有beforeCreate方法 beforeCreate() { //判断是否为Vue的实例,如果条件成立为Vue的实例,否则为其它对应的组件(因为在创建Vue实例的时候会传递选项) if (this.$options.router) { //通过查看源码发现,Vue的实例会挂在到当前的私有属性_routerRoot属性上 this._routerRoot = this; this._router = this.$options.router; //调用index.js文件中定义的init方法 this._router.init(this); } else { this._routerRoot = this.$parent && this.$parent._routerRoot; } }, }); //完成组件的注册 Vue.component("RouterView", View); Vue.component("RouterLink", Link); }
在上面的代码中,导入组件,并且完成组件的注册。
下面,我们测试一下。
在src目录下,在router.js文件中导入自己定义的VueRouter.
import Router from "./my-vue-router";
下面,我们要做的就是对所有的路由规则进行解析,将其解析到一个数组中。方便根据地址找到对应的组件。
在源码的index.js文件中,创建了VueRouter类,对应的构造方法中,有如下代码:
this.matcher = createMatcher(options.routes || [], this)
createMatcher方法是在create-matcher.js文件中创建的。
该方法返回的matcher 就是一个匹配器,其中有两个成员,match,另外一个是addRoutes
match:根据路由地址匹配相应的路由规则对象。
addRoutes动态添加路由
首先在我们自己的index.js文件中添加如下的代码:
import install from "./install"; import createMatcher from "./create-matcher"; export default class VueRouter { //在创建VueRouter对象的时候,会传递选项 constructor(options) { //获取routes选项,该选项中定义路由规则 this._routes = options.routes || []; this.matcher = createMatcher(this._routes); } // 注册路由变化的事件。 init() {} //init(Vue){} } //将install方法挂载到VueRouter上 VueRouter.install = install;
在上面的代码中,导入了createMatcher方法。
并且在调用该方法的时候传递了路由规则。
create-matcher.js 文件的代码如下:
import createRouteMap from "./create-route-map"; export default function createMatcher(routes) { const { pathList, pathMap } = createRouteMap(routes); function match() {} function addRoutes(routes) { createRouteMap(routes, pathList, pathMap); } return { match, addRoutes, }; }
下面,我们需要在create-route-map.js 文件中实现createRouteMap这个方法。
export default function createRouteMap(routes, oldPathList, oldPathMap) { const pathList = oldPathList || []; const pathMap = oldPathMap || {}; //遍历所有的路由规则,进行解析。同时还要考虑children的形式, //所以这里需要使用递归的方式。 routes.forEach((route) => { addRouteRecord(route, pathList, pathMap); }); return { pathList, pathMap, }; } function addRouteRecord(route, pathList, pathMap, parentRecord) { //从路由规则中获取path。 const path = parentRecord ? `${parentRecord.path}/${route.path}` : route.path; //构建记录 const record = { path, component: route.component, parent: parentRecord, //如果是子路由的话,记录子路由对应的父record对象(该对象中有path,component),相当于记录了父子关系 }; //如果已经有了path,相同的path直接跳过 if (!pathMap[path]) { pathList.push(path); pathMap[path] = record; } //判断route中是否有子路由 if (route.children) { //遍历子路由,把子路由添加到pathList与pathMap中。 route.children.forEach((childRoute) => { addRouteRecord(childRoute, pathList, pathMap, record); }); } }
下面测试一下上面的代码。
import createRouteMap from "./create-route-map"; export default function createMatcher(routes) { const { pathList, pathMap } = createRouteMap(routes); console.log("pathList==", pathList); console.log("pathMap==", pathMap); function match() {} function addRoutes(routes) { createRouteMap(routes, pathList, pathMap); } return { match, addRoutes, }; }
在上面的代码中,我们打印了pathList与pathMap.
当然,现在在我们所定义的路由规则中,还没有添加children,构建相应的子路由。下面重新修改一下。
在项目根目录下的router.js文件中,添加对应的子路由规则。
import Vue from "vue"; // import Router from "vue-router"; // import Router from "./vuerouter"; import Router from "./my-vue-router"; import Login from "./components/Login.vue"; import Home from "./components/Home.vue"; import About from "./components/About.vue"; import Users from "./components/Users"; Vue.use(Router); export default new Router({ // model: "history", routes: [ { path: "/", component: Home }, { path: "/login", component: Login }, { path: "/about", component: About, children: [{ path: "users", component: Users }], }, ], });
这时候可以查看对应的输出结果。
在create-matcher.js文件中,我们实现了createRouteMap方法,同时还需要实现match方法。
match方法的作用就是根据路由地址,匹配一个路由对象。其实就是从pathMap中根据路由地址,找出对应的路由记录。路由记录中记录了组件信息,找到以后就可以完成组件的创建,渲染了。
function match(path) { const record = pathMap[path]; if (record) { //根据路由地址,创建route路由规则对象 return createRoute(record, path); } return createRoute(null, path); }
在上面的代码中,我们调用match方法的时候,会传递过来一个路径,我们根据这个路径可以从pathMap中找到对应的路由记录信息(这块在上一小节已经创建完毕),如果找到了,我们还需要做进一步的处理,为什么呢?因为,我们传递过来的路径有可能是子路径,这时不光要获取到对应的子路由信息,我们还需要去查找对应的父路由的信息。所以这里需要进一步的处理,关于这块的处理封装到了createRoute这个方法中,而该方法在其它位置还需要,所以我们定义到util这个目录下import createRoute from "./util/route";。
create-matcher.js文件完整代码如下:
import createRouteMap from "./create-route-map"; import createRoute from "./util/route"; export default function createMatcher(routes) { const { pathList, pathMap } = createRouteMap(routes); console.log("pathList==", pathList); console.log("pathMap==", pathMap); //实现match方法 function match(path) { const record = pathMap[path]; if (record) { //根据路由地址,创建route路由规则对象 return createRoute(record, path); } return createRoute(null, path); } function addRoutes(routes) { createRouteMap(routes, pathList, pathMap); } return { match, addRoutes, }; }
下面我们需要在my-vue-router目录下面在创建一个util目录,在该目录下面创建route.js文件,该文件实现的代码如下:
export default function createRoute(record, path) { const matched = []; while (record) { matched.unshift(record); record = record.parent; } return { path, matched, }; }
总结:match这个方法的作用就是根据路径,创建出路由规则对象,而所谓的路由规则对象其实就是包含了路径以及对应的路由记录的信息(这里有可能包含了父路由以及子路由记录,这块内容存储到一个数组中)。
以后,我们就可以根据路径直接获取到包含了整个路由记录的这个数组,从而可以将对应的组件全部创建出来。
关于路由有三种模式:hash模式,html5模式,abstract模式(该模式与服务端渲染有关)
在这里我们实现hash模式的历史记录管理,不管是哪种模式,都有相同的内容,这里我们相同的内容定义到
父类中。
在该父类中主要有如下内容:
router属性:路由对象(ViewRouter)
current属性,记录当前路径对应的路由规则对象{path:'/',matched:[]},关于该对象,我们在前面已经处理完了。也就是在createRoute方法中返回的内容。
transitionTo()
跳转到指定的路径,根据当前路径获取匹配的路由规则对象route,然后更新视图。
在my-vue-router目录下的,history目录下的base.js文件,编写如下的代码:
import createRoute from "../util/route"; export default class History { // router路由对象ViewRouter constructor(router) { this.router = router; this.current = createRoute(null, "/"); } transitionTo(path, onComplete) { this.current = this.router.matcher.match(path); //该回调函数在调用transitionTo方法的时候,会传递过来。 onComplete && onComplete(); } }
父类已经实现了,下面实现对应的子类。也就是HashHistory
HashHistory继承History, 同时要确保首次访问的地址为#/.
在History中还需要定义两个方法,第一个方法为:getCurrentLocation( ) 获取当前的路由地址(# 后面的部分)
setUpListener( )方法监听路由地址改变的事件(hashchange)。
在history目录下的hash.js文件中的代码实现如下:
import History from "./base"; export default class HashHistory extends History { constructor(router) { //将路由对象传递给父类的构造函数 super(router); //确保 首次 访问地址加上 #/ (//由于没有添加this,为普通方法) ensureSlash(); } // 获取当前的路由地址 (# 后面的部分)所以这里需要去除# getCurrentLocation() { return window.location.hash.slice(1); } // 监听hashchange事件 //也就是监听路由地址的变化 setUpListener() { window.addEventListener("hashchange", () => { //当路由地址发生变化后,跳转到新的路由地址。 this.transitionTo(this.getCurrentLocation()); }); } } function ensureSlash() { //判断当前是否有hash // 如果单击的是链接,肯定会有hash if (window.location.hash) { return; } window.location.hash = "/"; }
我们知道当创建VueRouter 的时候,需要可以传递mode,来指定路由的形式,例如是hash模式还是html5模式等。
所以这里需要根据指定的mode的模式,来选择history目录中中不同js来处理。
所以在my-vue-router目录中的index.js文件中,做如下的修改:
import install from "./install"; import createMatcher from "./create-matcher"; import HashHistory from "./history/hash"; import HTML5History from "./history/html5"; export default class VueRouter { //在创建VueRouter对象的时候,会传递选项 constructor(options) { //获取routes选项,该选项中定义路由规则 this._routes = options.routes || []; this.matcher = createMatcher(this._routes); //获取传递过来的选项中的mode,mode中决定了用户设置的路由的形式。 //这里给VueRouter添加了mode属性 const mode = (this.mode = options.mode || "hash"); switch (mode) { case "hash": this.history = new HashHistory(this); break; case "history": this.history = new HTML5History(this); break; default: throw new Error("mode error"); } } // 注册路由变化的事件。 init() {} //init(Vue){} } //将install方法挂载到VueRouter上 VueRouter.install = install;
首先导入HashHistory 与HTML5History.
import HashHistory from "./history/hash"; import HTML5History from "./history/html5";
下面获取选项中的mode,如果在创建VueRouter对象的时候,没有指定mode,那么默认的值为hash.
下面就对获取到的mode进行判断,根据mode的不同的值,创建不同的history的实例。
//获取传递过来的选项中的mode,mode中决定了用户设置的路由的形式。 //这里给VueRouter添加了mode属性 const mode = (this.mode = options.mode || "hash"); switch (mode) { case "hash": this.history = new HashHistory(this); break; case "history": this.history = new HTML5History(this); break; default: throw new Error("mode error"); }
同时html5.js文件,添加了基本的代码
import History from "./base"; export default class HTML5History extends History {}
关于Html5的形式这里不在实现了。
下面完善一下init方法
// 注册路由变化的事件。 init() {}
具体的实现代码如下:
// 注册路由变化的事件(初始化事件监听器,监听路由地址的变化)。 init() { const history = this.history; const setUpListener = () => { history.setUpListener(); }; history.transitionTo( history.getCurrentLocation(), //如果直接history.setUpListener // 这样的话setUpListener里面的this会有问题。 setUpListener ); }
在这里,调用了transitionTo方法的原因是,在hash.js文件中的ensureSlash方法中,完成了一次地址的修改,所以这里需要跳转一次。
同时完成了hashchange事件的绑定(路由变化的事件)。
下面可以进行测试一下,在base.js文件中的transitionTo方法中,打印出current属性的值。
transitionTo(path, onComplete) { this.current = this.router.matcher.match(path); console.log("current===", this.current); //该回调函数在调用transitionTo方法的时候,会传递过来。 onComplete && onComplete(); }
下面,在浏览器的地址栏中输入了不同的URL地址后,在控制台上呈现出了不同的路由规则对象,也就是路由记录信息。
http://localhost:8080/#/about/users
输入以上地址,该地址为子路由的地址,最终也输出了对应的父路由的记录信息。
后期就可以获取具体的组件来进行渲染。
下面我们要做的就是渲染组件。
这里我们先创建一个与路由有关的响应式属性,当路由地址发生变化了,对应的该属性也要发生变化,从而完成页面的重新渲染。
在install.js文件中添加如下的代码:
Vue.util.defineReactive(this, "_route", this._router.history.current);
以上完成了响应式属性的创建,但是要注意的是defineReactive方法为Vue的内部方法,不建议平时通过该方法来创建响应式对象。
beforeCreate() { //判断是否为Vue的实例,如果条件成立为Vue的实例,否则为其它对应的组件(因为在创建Vue实例的时候会传递选项) if (this.$options.router) { //通过查看源码发现,Vue的实例会挂在到当前的私有属性_routerRoot属性上 this._routerRoot = this; this._router = this.$options.router; //调用index.js文件中定义的init方法 this._router.init(this); //在Vue的实例上创建一个响应式的属性`_route`. Vue.util.defineReactive(this, "_route", this._router.history.current); }
下面要考虑的就是当路由地址发生了变化后,需要修改_route属性的值。
在哪完成_route属性值的修改呢?
在base.js文件中,因为在该文件中定义了transitionTo方法,而该方法就是用来完成地址的跳转,同时完成组件的渲染。
base.js文件修改后的代码如下:
import createRoute from "../util/route"; export default class History { // router路由对象ViewRouter constructor(router) { this.router = router; this.current = createRoute(null, "/"); //这个回调函数是在hashhistory中赋值,作用是更改vue实例上的_route,_route的值发生变化,视图会进行刷新操作 this.cb = null; } //给cb赋值 listen(cb) { this.cb = cb; } transitionTo(path, onComplete) { this.current = this.router.matcher.match(path); // 调用cb this.cb && this.cb(this.current); // console.log("current===", this.current); //该回调函数在调用transitionTo方法的时候,会传递过来。 onComplete && onComplete(); } }
在History中的构造方法中初始化cb函数。
this.cb = null;
定义listen方法给cb函数赋值。
//给cb赋值 listen(cb) { this.cb = cb; }
在transitionTo 方法中调用cb函数,同时传递获取到的当前的路由规则对象也就是路由记录信息。
this.cb && this.cb(this.current);
在什么地方调用listen方法呢?
在index.js文件中的init方法中完成listen方法的调用。
// 注册路由变化的事件(初始化事件监听器,监听路由地址的变化)。 init(app) { const history = this.history; const setUpListener = () => { history.setUpListener(); }; history.transitionTo( history.getCurrentLocation(), //如果直接history.setUpListener // 这样的话setUpListener里面的this会有问题。 setUpListener ); //调用父类的中的listen方法 history.listen((route) => { app._route = route; }); }
在上面的代码中调用了父类中的listen方法,然后将箭头函数传递到了listen中。
这时候,在transitionTo方法中调用cb,也就是调用箭头函数,这时传递过来的参数route,为当前更改后的路由规则信息,交给了app中的_route属性。
app这个参数其实就是Vue的实例,因为在install.js文件中调用了init方法,并且传递的就是Vue的实例。
这样就完成了对Vue实例上的响应式属性_route值的修改,从而会更新组件。
创建$route与$router的目的是能够在所有的Vue实例(组件)中,可以获取到。
$route是路由规则对象,包含了path,component等内容
$router为路由对象(ViewRouter对象)。
通过查看源码(install.js)可以发现,其实就是将$router 与$route挂载到了Vue的原型上。
所以可以直接将源码内容复制过来就可以了。
Object.defineProperty(Vue.prototype, "$router", { get() { return this._routerRoot._router; }, }); Object.defineProperty(Vue.prototype, "$route", { get() { return this._routerRoot._route; }, });
通过上面的代码,可以看到$route与$router 都是只读的,因为对应的值,在前面已经设置完毕,这里只是获取。
$router 是通过_routerRoot来获取。
$route是通过_routerRoot._route来获取。
Vue.util.defineReactive(this, "_route", this._router.history.current);
在Vue对象上创建了_route属性,该属性的值为路由规则内容
router-view就是一个占位符,会用具体的组件来替换该占位符。
router-view的创建过程如下:
my-vue-router/components目录下的view.js代码如下:
export default { render(h) { //获取当前匹配的路由规则对象 const route = this.$route; //获取路由记录对象.只有一个内容,所以获取的是`matched`中的第一项。 const record = route.matched[0]; if (!record) { return h(); } //获取记录中对应的组件 const component = record.component; return h(component); }, };
以上的代码处理的是没有子路由的情况。
下面,看一下子路由情况的处理。
当然在编写子路由的处理代码之前,我们先把案例中的路由完善一下。
在src目录下的App.vue这个组件中,添加一个“关于”的链接。
Home Login About
对应在About 这个组件中,完成子路由应用
关于组件用户
下面完善一下对子路由的处理。
export default { render(h) { //获取当前匹配的路由规则对象 const route = this.$route; let depth = 0; //记录当前组件为RouterView this.routerView = true; let parent = this.$parent; while (parent) { if (parent.routerView) { depth++; } parent = parent.$parent; } //获取路由记录对象. // 如果是子路由,例如:子路由/about/users //子路由是有两部分内容,matched[0]:是父组件内容,matched[1]是子组件内容 const record = route.matched[depth]; if (!record) { return h(); } //获取记录中对应的组件 const component = record.component; return h(component); }, };
假如,现在我们在浏览器的地址栏中输入了:http://localhost:8080/#/about地址,
是没有父组件,那么depth属性的值为0,这时候获取的第一个组件然后进行渲染。
如果地址栏的内容为:http://localhost:8080/#/about/users 这时候有子组件。对应的获取对应的父组件内容,开始进行循环。
的属性_route.
Vue.util.defineReactive(this, “_route”, this._router.history.current);
}
下面要考虑的就是当路由地址发生了变化后,需要修改`_route`属性的值。 在哪完成`_route`属性值的修改呢? 在`base.js`文件中,因为在该文件中定义了`transitionTo`方法,而该方法就是用来完成地址的跳转,同时完成组件的渲染。 `base.js`文件修改后的代码如下: ```js import createRoute from "../util/route"; export default class History { // router路由对象ViewRouter constructor(router) { this.router = router; this.current = createRoute(null, "/"); //这个回调函数是在hashhistory中赋值,作用是更改vue实例上的_route,_route的值发生变化,视图会进行刷新操作 this.cb = null; } //给cb赋值 listen(cb) { this.cb = cb; } transitionTo(path, onComplete) { this.current = this.router.matcher.match(path); // 调用cb this.cb && this.cb(this.current); // console.log("current===", this.current); //该回调函数在调用transitionTo方法的时候,会传递过来。 onComplete && onComplete(); } }
在History中的构造方法中初始化cb函数。
this.cb = null;
定义listen方法给cb函数赋值。
//给cb赋值 listen(cb) { this.cb = cb; }
在transitionTo 方法中调用cb函数,同时传递获取到的当前的路由规则对象也就是路由记录信息。
this.cb && this.cb(this.current);
在什么地方调用listen方法呢?
在index.js文件中的init方法中完成listen方法的调用。
// 注册路由变化的事件(初始化事件监听器,监听路由地址的变化)。 init(app) { const history = this.history; const setUpListener = () => { history.setUpListener(); }; history.transitionTo( history.getCurrentLocation(), //如果直接history.setUpListener // 这样的话setUpListener里面的this会有问题。 setUpListener ); //调用父类的中的listen方法 history.listen((route) => { app._route = route; }); }
在上面的代码中调用了父类中的listen方法,然后将箭头函数传递到了listen中。
这时候,在transitionTo方法中调用cb,也就是调用箭头函数,这时传递过来的参数route,为当前更改后的路由规则信息,交给了app中的_route属性。
app这个参数其实就是Vue的实例,因为在install.js文件中调用了init方法,并且传递的就是Vue的实例。
这样就完成了对Vue实例上的响应式属性_route值的修改,从而会更新组件。
创建$route与$router的目的是能够在所有的Vue实例(组件)中,可以获取到。
$route是路由规则对象,包含了path,component等内容
$router为路由对象(ViewRouter对象)。
通过查看源码(install.js)可以发现,其实就是将$router 与$route挂载到了Vue的原型上。
所以可以直接将源码内容复制过来就可以了。
Object.defineProperty(Vue.prototype, "$router", { get() { return this._routerRoot._router; }, }); Object.defineProperty(Vue.prototype, "$route", { get() { return this._routerRoot._route; }, });
通过上面的代码,可以看到$route与$router 都是只读的,因为对应的值,在前面已经设置完毕,这里只是获取。
$router 是通过_routerRoot来获取。
$route是通过_routerRoot._route来获取。
Vue.util.defineReactive(this, "_route", this._router.history.current);
在Vue对象上创建了_route属性,该属性的值为路由规则内容
router-view就是一个占位符,会用具体的组件来替换该占位符。
router-view的创建过程如下:
my-vue-router/components目录下的view.js代码如下:
export default { render(h) { //获取当前匹配的路由规则对象 const route = this.$route; //获取路由记录对象.只有一个内容,所以获取的是`matched`中的第一项。 const record = route.matched[0]; if (!record) { return h(); } //获取记录中对应的组件 const component = record.component; return h(component); }, };
以上的代码处理的是没有子路由的情况。
下面,看一下子路由情况的处理。
当然在编写子路由的处理代码之前,我们先把案例中的路由完善一下。
在src目录下的App.vue这个组件中,添加一个“关于”的链接。
Home Login About
对应在About 这个组件中,完成子路由应用
关于组件用户
下面完善一下对子路由的处理。
export default { render(h) { //获取当前匹配的路由规则对象 const route = this.$route; let depth = 0; //记录当前组件为RouterView this.routerView = true; let parent = this.$parent; while (parent) { if (parent.routerView) { depth++; } parent = parent.$parent; } //获取路由记录对象. // 如果是子路由,例如:子路由/about/users //子路由是有两部分内容,matched[0]:是父组件内容,matched[1]是子组件内容 const record = route.matched[depth]; if (!record) { return h(); } //获取记录中对应的组件 const component = record.component; return h(component); }, };
假如,现在我们在浏览器的地址栏中输入了:http://localhost:8080/#/about地址,
是没有父组件,那么depth属性的值为0,这时候获取的第一个组件然后进行渲染。
如果地址栏的内容为:http://localhost:8080/#/about/users 这时候有子组件。对应的获取对应的父组件内容,开始进行循环。
在循环的时候,做了一个判断,判断的条件就是当前的父组件必须为:RouterView组件(子组件中router-view与父组件中的router-view构成了父子关系),才让depth加1,然后取出子组件进行渲染。
上一篇:前端项目打包并部署