网站首页 > 博客 > 正文

vue.js 前端 javascript 面试 前端框架 Vue-Router与原理

Vue Router

一、Vue Router 回顾

1、路由简介

路由是一个比较广义和抽象的概念,路由的本质就是对应关系。

在开发中,路由分为:

​ 后端路由​ 前端路由

后端路由

概念:根据不同的用户 URL 请求,返回不同的内容本质:URL 请求地址与服务器资源之间的对应关系

SPA(Single Page Application)

Ajax前端渲染(前端渲染提高性能,但是不支持浏览器的前进后退操作)SPA(Single Page Application)单页面应用程序:整个网站只有一个页面,内 容的变化通过Ajax局部更新实现、同时支持浏览器地址栏的前进和后退操作SPA实现原理之一:基于URL地址的hash(hash的变化会导致浏览器记录访问历 史的变化、但是hash的变化不会触发新的URL请求在实现SPA过程中,最核心的技术点就是前端路由

前端路由

概念:根据不同的用户事件,显示不同的页面内容本质:用户事件与事件处理函数之间的对应关系

2、实现简易前端路由

基于URL中的hash实现(点击菜单的时候改变URL的hash,根据hash的变化控制组件的切换)

案例代码实现如下:

Document

3、vue-router的基本使用

Vue Router(官网:https://router.vuejs.org/zh/)是 Vue.js 官方的路由管理器。 它和 Vue.js 的核心深度集成,可以非常方便的用于SPA应用程序的开发。

基本使用的步骤:

引入相关的库文件添加路由链接添加路由填充位定义路由组件配置路由规则并创建路由实例把路由挂载到 Vue 根实例中

下面看一下具体的实施过程

引入相关的库文件

添加路由链接

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}

]

})

把路由挂载到 Vue 根实例中

new Vue({

el: '#app',

// 为了能够让路由规则生效,必须把路由对象挂载到 vue 实例对象上

router

});

完整代码实现如下:

Document

User

Register

4、路由重定向

路由重定向指的是:用户在访问地址 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

5、嵌套路由

嵌套路由功能分析

点击父级路由链接显示模板内容

模板内容中又有子级路由链接

点击子级路由链接显示子级模板内容

下面看一下实现的步骤

父路由组件模板

父级路由链接父组件路由填充位

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

6、动态路由匹配

6.1 动态匹配路由的基本用法

思考:

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

6.2 路由组件传递参数

$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

7、命名路由

为了更加方便的表示路由的路径,可以给路由规则起一个别名,即为“命名路由”。

const router = new VueRouter({

routes: [

{

path: '/user/:id',

name: 'user',

component: User

}

]

})

User

完整代码如下:

Document

User1

User2

User3

Register

8、编程式导航

页面导航的两种方式

声明式导航:通过点击链接实现导航的方式,叫做声明式导航 例如:普通网页中的 链接 或 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' }})

9、路由案例

9.1 抽离并且渲染App根组件。

将素材中的代码修改成如下的形式:

基于vue-router的案例

在上面的代码中,我们导入了Vue与Vue-Router的文件。

然后将核心内容定义到App这个组件中,同时创建了路由对象,并且指定了路由的规则。接下来将路由对象挂载到了Vue的实例中。

同时在

中使用router-view定义了一个占位符。当输入的地址为/,对应的App组件就会在该占位符中进行展示。

9.2 将菜单改造为路由连接

将模板中的菜单修改成路由连接的形式,如下所示:

  • 用户管理
  • 权限管理
  • 商品管理
  • 订单管理
  • 系统设置

9.3 创建菜单对应组件

基本组件创建如下:

const Users = {

template: `

用户管理区域

`,

};

const Rights = {

template: `

权限管理区域

`,

};

const Goods = {

template: `

商品管理区域

`,

};

const Orders = {

template: `

订单管理区域

`,

};

const Settings = {

template: `

系统设置区域

`,

};

我们知道,当单击左侧的菜单时,上面定义的组件将会在右侧进行展示。

所以需要在右侧,添加一个router-view的占位符。

9.4 添加子路由规则并实现路由重定向

在上一小节中,我们已经将组件都定义好了,下面需要定义其对应的路由规则。

怎样添加对应的路由规则呢?

我们知道整个页面是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组件的右侧进行渲染展示。

当点击左侧的菜单时,对应的组件会在右侧进行展示。

9.5 渲染用户列表数据

这里将用户组件的内容修改成如下形式:

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组件中定义用户数据,并且在模板中通过循环的方式将数据渲染出来。

9.6 跳转到详情页

当单击"详情"链接时,跳转到对应的详情页面。这里需要用到编程式导航的内容。

首先定义用户详情页组件

//用户详情组件

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的案例

10、路由守卫

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: `

用户管理区域

编号姓名年龄操作
{{item.id}}{{item.name}}{{item.age}}

详情

`,

beforeRouteEnter(to, from, next) {

if (window.isLogin) {

next();

} else {

next("/login?redirect=" + to.fullPath);

}

},

};

在上面的代码中,直接将路由守卫对应的方法添加到了组件中。

注意:在测试之前将路由规则中定义的路由守卫的代码注释掉。

11、addRoutes动态路由添加

在前面的案例中,我们都是将路由定义好,然后通过路由守卫来判断,某个用户是否登录,从而决定能否访问某个路由规则对应的组件内容。

但是,如果某些路由规则只能用户登录以后才能够访问,那么我们也可以不用提前定义好,而是在登录后,通过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的案例

12、路由组件缓存

利用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

13、Hash模式与History模式

13.1 Hash模式与History模式区别

前端路由中,不管是什么实现模式,都是客户端的一种实现方式,也就是当路径发生变化的时候,是不会向服务器发送请求的。

如果需要向服务器发送请求,需要用到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( )

13.2 History模式的使用

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的案例

当然,项目的目录结构做了一定的调整,如下图所示:

在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( ) 方法来修改地址栏中的地址,实现组件的切换,而且还会把地址保存的历史记录中(也就是可以单击浏览器中后退按钮,实现后退等操作),但是它并不会向服务器发送请求。

所以说现在整个操作都是在客户端完成的。

但是,当我刷新了浏览器以后,会出现怎样的情况呢?

上图的含义就是,当单击浏览器中的刷新按钮的时候,会向服务器发送请求,要求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,所以会加载该地址对应的组件内容。

13.3 在Nginx服务器中配置History模式

代理服务器

代理服务器:一般是指局域网内部的机器通过代理服务器发送请求到互联网上的服务器,代理服务器一般作用在客户端。应用比如:GoAgent,翻墙神器.

反向代理服务器

**反向代理服务器:**在服务器端接受客户端的请求,然后把请求分发给具体的服务器进行处理,然后再将服务器的响应结果反馈给客户端。Nginx就是其中的一种反向代理服务器软件。

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的基本使用,下面我们来模拟Vue Router的实现,通过模拟实现,来了解其内部的实现原理。

我们这里模拟的是History模式。Hash模式基本实现上是一样的。

这里先来复习一下Hash模式的工作原理。

·URL中#后面的内容作为路径地址,当地址改变的时候不会向服务器发送请求,但是会触发hashchange事件。监听hashchange事件,在该事件中记录当前的路由地址,然后根据路由地址找到对应组件。根据当前路由地址找到对应组件重新渲染。

下面再来复习一下History模式

通过history.pushState()方法改变地址栏,并且将当前地址记录到浏览器的历史记录中。当前浏览器不会向服务器发送请求监听popstate事件,可以发现浏览器历史操作的变化,记录改变后的地址,单击前进或者是后退按钮的时候触发该事件根据当前路由地址找到对应组件重新渲染

1、分析Vue Router

在模拟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的类图

在该类图中,上半部分是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.

2、install方法实现

在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;

}

},

});

}

}

3、实现构造函数

在介绍VueRouter的类图时,我们说过

Constructor是一个构造方法,该该构造方法中会初始化options ,data,routeMap这几个属性。

constructor(options) {

this.options = options;

this.routeMap = {};

this.data = _Vue.observable({

current: "/",

});

}

4、createRouteMap方法实现

createRouteMap方法,该方法会把构造函数中传入进来的options参数中的路由规则,转换成键值对的形式存储到routeMap中。 键就是路由的地址,值就是对应的组件

createRouteMap() {

this.options.routes.forEach((route) => {

this.routeMap[route.path] = route.component;

});

}

5、initComponents方法实现

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");

运行上面的代码会出现如下的错误:

第二个错误是我们还没有创建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

6、render函数

虽然使用完整版Vue可以解决上面的问题,但是由于带有编译器,体积比运行时版本大10k左右,所以性能比运行时版要低。

那么这一小节我们使用运行时版本来解决这个问题。

我们知道,完整版中的编译器的作用就是将template模板转成render函数,所以在运行时版本中我们可以自己编写render函数。

但是在这你肯定也有一个问题,就是在单文件组件中,我们一直都是在写,并且没有写render函数,

但是为什么能够正常的工作呢?这时因为在打包的时候,将