路由基础
1.SPA与路由
1.1 SPA介绍
1.1.1 什么是SPA?
SPA(single-page application),只有一个HTML页面,通过路由实现页面内的局部切换,公共资源部分只加载一次。
我们熟知的JS框架如react,vue,angular,ember都属于SPA。
1.1.2 什么是MPA(多页面应用)
平常写的普通页面就是MPA,通过a标签实现页面切换,每次切换页面都要重新加载公共资源部分。
1.1.3 SPA和MPA的区别
区别 | SPA | MPA |
---|---|---|
组成 | 一个外壳页面和多个页面片段组成 | 多个完整的页面组成 |
公用资源(js,css,img) | 公用,只加载一次 | 每个页面都需要加载 |
刷新方式 | 局部刷新 | 整体刷新 |
URL模式 | 哈希模式(a.com/#/page1)/历史模式(a.com/page1) | 历史模式 |
用户体验 | 用户体验好,页面切换快 | 用户体验不好,页面切换慢 |
转场动画 | 容易实现 | 无法实现 |
数据传递 | 容易 | 依赖URL或本地存储 |
SEO | 实现比较困难 | 简单 |
开发成本 | 难度高,需要借助专业的框架 | 难度低,但是重复代码比较多 |
维护成本 | 相对比较低 | 相对比较高 |
1.1.4 SPA的优缺点
优点:
- 1,用户体验好,快,内容的改变不需要重新加载整个页面,基于这一点spa对服务器压力较小
- 2,前后端分离
- 3,页面效果会比较炫酷(比如切换页面内容时的专场动画)
缺点:
- 1,不利于seo(搜索引擎优化(Search Engine Optimization,SEO)是一种通过了解搜索引擎的运行规则来调整网站,以提高目标网站在有关搜索引擎内排名的方式);
- 2,导航不可用,如果一定要导航需要自行实现前进、后退。(由于是单页面不能用浏览器的前进后退功能,所以需要自己建立堆栈管理)
- 3,不支持低版本的浏览器,最低只支持到IE9
- 4,初次加载时耗时多
- 5,页面复杂度提高很多
1.2 路由介绍
简单举例说明,假如我们有一台提供 Web 服务的服务器的网络地址是:10.0.0.1,而该 Web 服务又提供了三个可供用户访问的页面,其页面 URI 分别是:
http://10.0.0.1/ http://10.0.0.1/about http://10.0.0.1/concat
那么其路径就分别是 /,/about,/concat。
当用户使用 http://10.0.0.1/about 来访问该页面时,Web 服务会接收到这个请求,然后会解析 URL 中的路径 /about,在 Web 服务的程序中,该路径对应着相应的处理逻辑,程序会把请求交给路径所对应的处理逻辑,这样就完成了一次「路由分发」,这个分发就是通过「路由」来完成的。
简单的说,路由是根据不同的 url 地址展示不同的内容或页面。
1.3前端路由原理
路由的概念在软件工程中出现,最早是在后端路由中实现的。服务器直接生产渲染好对应的HTML页面, 返回给客户端进行展示。
前端路由是如何做到URL和内容进行映射呢?监听URL的改变。
1.3.1 URL的hash
URL的hash也就是锚点(#), 本质上是改变window.location的href属性;我们可以通过直接赋值location.hash来改变href, 但是页面不发生刷新;另外每次 hash 值的变化,还会触发hashchange 这个事件,通过这个事件我们就可以知道 hash 值发生了哪些变化。然后我们便可以监听hashchange来实现更新页面部分内容的操作。
hash的优势就是兼容性更好,在老版IE中都可以运行,但是缺陷是有一个#,显得不像一个真实的路径。
1.3.2 HTML5的History
14年后,因为HTML5标准发布。多了两个 API,pushState 和 replaceState,通过这两个 API 可以改变 url 地址且不会发送请求。
通过这些就能用另一种方式来实现前端路由了,但原理都是跟 hash 实现相同的。用了 HTML5 的实现,单页路由的 url 就不会多出一个#,变得更加美观。但因为没有 # 号,所以当用户刷新页面之类的操作时,浏览器还是会给服务器发送请求。
为了避免出现这种情况,所以这个实现需要服务器的支持,需要把所有路由都重定向到根页面;
2.vue-router基础用法
通过vue的路由可实现多视图的单页面Web应用。VueRouter文档
我们之前使用过is特性来实现动态组件,vue-router的实现原理与之类似。vue-router展示不同的页面事实上就是动态加载不同的组件。
当我们的地址栏发生了变化,vue-router就会监听到。根据它里面配置的匹配规则,展示不同的组件。
2.1 下载VueRouter
2.1.1 直接下载
VueRouter下载地址 : https://unpkg.com/vue-router/dist/vue-router.js
2.1.2 使用CDN地址
2.1.3 脚手架
在使用脚手架搭建项目时,选择 Vue-router,项目创建好之后默认安装vue-router。如果创建项目时,没有选择,也可以使用模块化开发的方式。执行如下的命令来安装Vue Router。
npm i vue-router
2.2 一般使用过程
2.2.1 使用router-link组件来定义导航
在app.vue的template中修改成如下代码
关于我们 新闻
2.2.2 指定组件的渲染位置
通过router-view组件指定组件的渲染位置。
当单击链接router-link的时候,会在router-view所在的位置渲染组件的模板内容。可以把router-view理解为占位符。
再准备好样式
2.2.3 定义路由组件
也就是每个链接显示的内容,这里只是演示前端路由的基本用法,所以组件定义很简单。新建两个文件 About.vue和News.vue
关于我们
最热新闻
2.2.4 定义路由,创建路由实例
src目录下新建一个router文件夹,如果创建项目时,已经选择了Vue-router则会自动创建。创建index.js文件,代码如下:
import Vue from 'vue' import VueRouter from 'vue-router' import Films from '@/components/About' import Cinemas from '@/components/News' // 注册路由插件, 两个全局 router-view router-link Vue.use(VueRouter) // 配置表,有多条路由时,通常定义为一个常量数组,里面每个对象就是一个路由 const routes = [ // 定义路由格式:path指定路由的url,component指定当单击path指定的url时显示哪个组件内容。 { path: '/about', component: About }, { path: '/news', component: News } ] //能new VueRouter就是因为引入了vue-router.js。 export default new VueRouter({ // routes:routes//左边routes为要设置的路由选项routes,右边routes为选项值,名字当然可以不同,但是一般设置为相同。第2步不单独定义,直接写在routes里也可以 routes, mode:'history', linkActiveClass:'active' });
2.2.5 挂载路由实例对象
在main的js中,挂在路由实例对象.
import router from './router' //创建vue根实例,并将上面的路由实例挂载到vue实例上,也称为注入路由,就是告诉vue实例有这么一个路由了。 //怎么告诉呢?通过router选项 var vm = new Vue({ el: "#app", // router:router//同样:左右两边名称相同,可以简写 router//注入路由 })
2.3 修改路由模式
上面的案例中,当我们切换路由时发现地址栏后面会自动再加一个“#”。为什么是这种显示模式呢?因为在使用Vue Router创建路由实例时,默认采用的是hash路由配置模式。这种模式的优点是各种浏览器的兼容性比较好。
但是hash模式下url需要带“#”符号,不仅看起来不舒服,而且有些场景下是会破坏路由中的"#"(微信分享页面就会把"#"后边的内容处理掉),所以我们可以将路由实例修改为history路由模式。只需要在创建路由实例时,设置mode选项为history即可。
const router = new VueRouter({ routes, mode:'history' })
这种history路由模式的缺点是URL兼容性不好,并且惧怕刷新。
2.4 为当前导航添加样式
观察生成的HTML代码,会发现在当前导航中多了两个class类名。
但是此类名并没有设置样式,需要自己手动设置具体样式。
.rotuer-link-active{ font-size:20px; color:red; text-decoration: none; }
需要注意的是,样式名取最后一个即可,这两者的区别在后面学习嵌套路由时再说明。
另外,如果觉得系统默认样式名太长,不好记忆可以进行修改。
在创建路由实例时,可通过linkActiveClass选项,设置样式名。
const router = new VueRouter({ routes, mode:'history', linkActiveClass:'active' })
2.5 路由重定向
还有一个问题,不论哪种路由模式,刚打开页面既没有显示【关于我们】的内容,也没显示【新闻】链接的内容。我们可以在配置路由时,在增加一个路由,作用是:找不到路由时,重定向到about。 可以理解为备胎,最后实在没招了,就匹配备胎了。
const routes = [ { path: '/about', component: About }, { path: '/news', component: News }, {path:'*',redirect:'/about'}//*表示找不到路由时,重定向到about ]
2.6 router-link标签中的tag属性
【router-link】默认会转为【a标签】,实际上可以添加tag属性来转为指定的标签,比如转为【buttton标签】或【li标签】等。
登录
不过vue-router4.x 之后废弃了 tag 而使用(v-solt)插槽来实现
我是路由自定义标签
3.前端路由嵌套
如果全是用一级路由时,路由管理就变得很臃肿,有点乱,路由有父子关系的话,嵌套路由会更好。嵌套路由也就是路由中的路由的意思,组件中可以有自己的路由导航和路由容器(router-link、router-view),通过配置children可实现多层嵌套,在vue组件中使用
举例:我们上面的例子中,添加一个books路由,单击“图书”链接,以列表的形式显示所有图书的书名,进一步单击单个书名链接,在Books视图中显示图书的详细信息。
在asssets目录下新建一个books.js文件,里面是图书数据
export default [ {id: 1, title: '三体', desc: '三个天体在互相引力的作用下互相围绕运行,其运行轨迹将产生不可预测的混沌。'}, {id: 2, title: '蛤蟆先生去看心理医生', desc: '蛤蟆先生一向爱笑爱闹,如今却一反常态地郁郁寡欢。'}, {id: 3, title: '活着', desc: '人是为了活着本身而活着的,而不是为了活着之外的任何事物而活着。'} ]
这里只是为了演示需要,真实场景中,图书数据应该是通过ajax请求从服务器端加载而来。
3.1.1. 在父组件中添加导航
修改Books.vue,以列表方式显示图书数据,添加导航链接,并使用【router-view】来指定book组件渲染的位置
图书列表
{{book.title}}
3.1.2.添加子路由显示的组件
新建Book.vue,代码如下。
图书详情页面
3.1.3.配置父路由的children属性
在router目录下的index.js中修改代码如下:
import Book from '@/components/Book' //... const routes = [ //... { path: '/books', component: Books, children: [ {path: 'book', component: Book} ] }, ]
(1)要在嵌套的出口(即Books组件中的router-view)中渲染组件,需要在routes选项的配置中使用children选项。children选项只是路由配置对象的另一个数组,如同routes本身一样,因为,可以根据需要继续嵌套配置。
(2)以‘/’开头的嵌套路径被视为根路径。上面的子路由导航中,如果path是“/book”则会出错。
完成以上步骤后,我们点击图书就可以看到进入图书详情页面了,但是我们还需要在该页面中看到点击图书的详情信息,这就意味这我们需要将父组件Books.vue中的被点击图书的ID传递给Book.vue组件。
4.路由之间的传参
在进行表单提交、组件跳转等操作时需要使用到上一个表单、组件中的一些数据。此时就需要将参数在路由间进行传递。
4.1.query传参
将需要的参数以 【key = value】的方式放在URL地址中。
book?id=1
这里的参数值“id”可以通过当前组件的表单输入来获取。
那么进入book组件后如何获取传入的参数呢?
当将实例化的VueRouter对象挂载到Vue实例后,Vue Router在Vue实例上创建了两个属性对象。即 r o u t e r ( r o u t e r 实 例 ) 和 router(router实例)和 router(router实例)和route(当前页面的路由信息)。通过【this.$route.query】可以获取query查询参数。
这里是Book页面内容,获取参数:{{this.$route.query}},图书ID :{{this.$route.query.id}}
4.2.params方式传参
我们也可以使用动态参数的方式传递参数,比如 【book/1】。那【1】分别代表什么?这个需要在路由中指明。
注册
与query查询参数不同的是,在定义路由信息时,需要以占位符【:参数名】的方式将需要传递的参数指定到路由地址中,否则系统不能识别传递的参数。
在路由中指定参数的含义,代码如下:
children: [{ //books下的子路由 path: 'book/:id', component: Book }]
进入Book.vue组件时如何获取传递过来的参数?这与获取query参数的方式相同,可以通过【 r o u t e 】 获 取 当 前 路 由 信 息 , 通 过 【 route】获取当前路由信息,通过【 route】获取当前路由信息,通过【route.params.参数名】的方式获取动态参数。参数名也就是在路由中设置的参数名(/:后面的)
这里是Book页面内容,获取参数:{{this.$route.params}},图书ID :{{this.$route.params.id}}
4.3 获取当前路由路径
路由对象还有其他属性,如【$route.path】,可以获取当前路由的路径。
这里是Book页面内容,获取参数:{{this.$route.params}},图书ID :{{this.$route.params.id}},路由路径{{$route.path}}
4.4 实现图书详情
接下来实现在Book.vue组件中显示点击书籍的详细信息。
修改Books.vue代码如下:
{{book.title}}
修改book.vue代码如下:
图书ID:{{ book.id }}
书名:{{ book.title }}
说明:{{ book.desc }}
实际场景中,当单击某本图书链接时,应该向服务器端发起Ajax请求来获取图书详细数据,于是我们想到在Book组件中通过生命周期钩子函数来实现,然而,这行不通。这是因为当两个路由都渲染同一个组件时,例如,从book/1导航到book/2时,vue会复用先前的Book实例,比起销毁旧实例再创建新实例,复用会更加高效。但是这就意味着组件的生命周期钩子不会再被调用,所以也就无法在生命周期钩子中根据路由参数的变化来实现更新数据。
要对同一组件中的路由参数更改作出响应,只需监听$route对象。当路由参数变化时,更新图书详细数据。
Book.vue代码修改如下:
created(){ this.book = Books.find((item) => item.id == this.$route.params.id); }, watch: { '$route' (to) { this.book = Books.find((item) => item.id == to.params.id); } }
(1)只有路由参数发生变化时, route 对象的监听器才会被调用 ,这意味着第一次渲染Book组件时 ,通过 route对象的监听器才会被调用,这意味着第一次渲染Book组件时,通过 route对象的监听器才会被调用,这意味着第一次渲染Book组件时,通过route对象的监听器是得不到数据的,因此利用created钩子来获取第一次渲染时的数据,当然,也可以利用immediate选项,将其值设为true,让监听器在监听开始后立即执行,这样就不需要created钩子了。
watch: { '$route': { handler: function(to) { this.book = Books.find((item) => item.id == to.params.id); }, immediate: true } }
(2)$route对象的监听器函数中的to参数表示即将进入的目标路由对象,该函数还可以带一个from参数,表示当前导航正要离开的路有对象。
除了监听$route对象,还可以用到后面要学习的导航守卫。这个以后再说。
5.编程式导航
Vue-router路由分为两种:
- 声明式路由编程式路由
- 声明式导航是写在template标签里,通过标签来触发
编程式导航写在js里,通过this.$router.push(xxx)来触发路径
前面例子中通过在页面上设置【router-link】标签进行路由地址间的跳转,就等同于执行push方法。
5.1 $router.push添加 / 跳转路由
push方法会将一条心的路由记录添加到浏览器的history栈中,通过history的自身特性,驱使浏览器进行页面的跳转。同时,因为在history会话历史中会一直保留这个路由信息,所以当后退时还是可以返回到当前的页面。
在push方法中,参数可以使一个字符串路径,或者是一个描述地址的对象。
this.$router.push('/account/register') this.$router.push({path:'/account/register'})
如果带参数的话,对象方式可以通过query属性来指定参数。
this.$router.push({ //想跳到哪里就设置相应的路由,并传递参数信息。比如跳到注册页面/account/register也行 path:'/account/login',query:{name:this.name,pwd:this.pwd} })
需求:在登录页面中提供用户名和密码输入框以及【提交】按钮,点击【提交】按钮在注册页面中显示出用户名和密码信息。
1.添加tmplogin模板
2.定义组件对象
const login = { template: '#tmplogin', data: { return: { name: '', pwd: '' } }, methods: { submit() { this.$router.push({ //想跳到哪里就设置相应的路由,并传递参数信息。比如跳到注册页面/account/register也行 path:'/account/register',query:{name:this.name,pwd:this.pwd} }) } }, } const register = { template: '这里是注册页面内容,获取传递的参数:{{$route.params}},用户名:{{$route.params.name}},密码:{{$route.params.pwd}},路由路径{{$route.path}}
' }
3.在路由中进行配置
children: [{ //account下的子路由 path: 'login', component: login }]
5.2 $router.replace替换路由
replace方法同样可以实现路由跳转的目的。不过,从名字上也可以看出,与使用push方法跳转不同的是,当使用replace方法时,并不会往history栈中新增一条新的记录,而是会替换当前的记录,因此无法通过后退按钮返回被替换前的页面。
需求:在账户页面下添加一个【替换路由】按钮,点击后跳转到新闻页面。
1.tempaccount模板添加button按钮。
2.在Account组件下,绑定对应方法。
var Account = { template: '#tmpaccount', //这里模板内容变复杂了,所以单独定义 methods:{ replace(){ this.$router.replace({ path:'/news' }) } } }
从【关于我们】到【账户】,点击按钮跳转,进入【新闻】,此时点击浏览器的后退,发现并不会回到【账户】,而是回到【关于我们】。
5.3 $router.go()跳转
当使用go方法时,可以在history记录中向前或向后跳转。也就是说,通过go方法可以在已经存储的history路由历史中前进后退。
//在历史记录中前进一步,等同于 history.forward() this.$router.go(1); //后退一步,等同于history.back() this.$router.go(-1); //前进两步记录 this.$router.go(2);
5.4 router 与 router与 router与route的区别
1.$router对象是全局路由的实例,是VueRouter构造方法的实例。是路由操作对象,可以添加或跳转路由,替换路由等。
2.$route对象表示当前的路由信息,包含了当前 URL 解析得到的信息。包含当前的路径,参数,query对象等。
6.命名路由和命名试图
6.1 命名路由
在某些时候,生成的路由URL地址可能会很长,在使用中可能会显得有些不便。这个时候通过一个名称来标识路由会更方便一些。
const routes = [ { path: '/about',name:'aa', component: About } ]
使用命名路由之后,在使用router-link的to属性跳转路由的时候传一个对象,跳转到指定的路由地址上。
关于我们
6.2 命名视图
当打开一个页面时,整个页面可能是由多个Vue组件构成的。例如,一般后端管理首页可能是由sidebar(侧导航),header(顶部导航)和main(主内容)这三个Vue组件构成的。此时,通过Vue Router构建路由信息时,如果一个URL只能对应一个Vue组件,整个页面将无法正确显示。
通过router-view标签,就可以指定组件渲染显示到什么位置。因此,当需要在一个页面上显示多个组件的时候,就需要在页面中添加多个router-view标签。
那么,是不是可以通过一个路由对应多个组件,然后按需渲染到不同的【router-view】标签上呢?默认情况下不能,当我们将一个路由对象对应到多个组件时,不管有多少个【router-view】标签,程序都会将第一个组件渲染到所有的【router-view】标签上。
那怎么办呢?需要实现的是一个路由信息可以通过设计者的需求去渲染到页面中指定的router-view标签,这可以通过Vue Router命名视图的方式来实现。
命名视图与命名路由的实现方式相似。命名视图通过在【router-view】标签上设定name属性,然后再构建路由与组件的对应关系时,以一种【name:component】的形式构造出一个组件对象,从而指明在哪个【router-view】标签上加载什么组件。
1.三个组件的样式布局
.container{ height: 500px; } .top { background-color:beige; width: 100%; height: 80px; } .left{ float: left; width: 20%; height: 100%; background-color: burlywood; } .right{ float: left; width: 80%; height: 100%; background-color: aquamarine; }
2.html代码部分
const header = { template: 'header' } const sidebar = { template: ' ' } const main = { template: 'main' }
3.定义路由跳转的组件模板
const header = { template: 'header' } const sidebar = { template: ' ' } const main = { template: 'main' }
4.定义路由信息
const routes = [{ path: '/', //一个路由对应多个组件 components: { default: header,//指定什么router-view显示对应的组件,即指明是在哪个 router-view 标签上加载什么组件。 sidebar: sidebar,//格式name:component,name为router-view的name main: main } }]
完整代码
Document
7.组件与路由间的解耦
在前面所讲路由传递参数的办法中,不管是query传参还是param传参,最终都是通过this.$route属性获取参数信息,如$route.query.name或$route.params.name
这意味着组件和路由耦合到了一块,所有需要获取参数值的地方都需要加载VueRouter。那么如何实现组件与路由的解耦呢?
在之前学习组件相关知识时,我们知道可以通过组件的props选项来实现子组件接受父组件传递的值。在Vue Router中,可以通过使用组件的props选项来进行组件与路由之间的解耦。
7.1 设置props:true情况
1.在定义路由规则时,指定props的属性为true。
```bash //定义路由 const router = new VueRouter({ routes: [{ //定义路由 path: '/myRouter/:id', //路由规则通过占位符指明传递的参数为id,同时id要为上面组件props选项中有的值 component: mycomp, props: true //此处props要设置为true,即可以实现组件与Vue Router之间的解耦 }] })
2.在定义组件时,指定props接受的参数
//定义组件 const mycomp = { props: ['id'], //此处没有通过$route.params.id方式获取参数id,也就不需要router实例 template: '组件获取到了路由传递的参数:{{id}},但此处并没有通过$route去获取。
' / }
完整核心代码如下:
需要注意的是,该方法要求路由传递参数方式一定为param方式。
7.2 设置props为对象情况
在将路由规则的props属性定义为对象后,不管路由参数中传递的是什么值,最终获取的都是对象中的值。同时,需要注意的是,props中的属性值必须是静态的,固定的。
上述案例修改下定义路由部分的代码
//定义路由 const router = new VueRouter({ routes: [{ //定义路由 path: '/myRouter/:id', //路由规则通过占位符指明传递的参数为id,同时id要为上面组件props选项中有的值 component: mycomp, props:{ id:'123' //组件获取到的是这里的值 } }] })
与上面案例相比,html与js没有变化,只需要修改下js部分
//定义组件 const mycomp = { props: ['id'], template: '组件获取到了路由传递的参数:{{id}},但此处并没有通过$route去获取。
' } //定义路由 const router = new VueRouter({ routes: [{ //定义路由 path: '/myRouter/:id', //路由规则通过占位符指明传递的参数为id,同时id要为上面组件props选项中有的值 component: mycomp, props:{ id:'123' //组件获取到的是这里的值 } }] }) const vm = new Vue({ el: '#app', data: {}, methods: { goMethod() { //该方法实现路由跳转,跳转到myRouter this.$router.push({ path: '/myRouter/123456'//param方式传参,随便写参数,但是必须有 }) } }, router })
7.3 设置props为函数情况
在对象模式中,只能接受静态的props属性值。而使用函数模式之后,就可以对静态值做数据的进一步交工,或者是与路由传递参数的值进行结合。
js部分代码修改如下
//定义组件 const mycomp = { props: ['id', 'name'], template: '组件获取到了路由传递的参数:{{id}}——{{name}},但此处并没有通过$route去获取。
' } //定义路由 const router = new VueRouter({ routes: [{ //定义路由 path: '/myRouter', component: mycomp, props: route => ({ id: route.query.id,//获取到通过路由传递的参数,这个就可以是动态的 name: 'zhangsan'//这个是静态的 }) }] }) const vm = new Vue({ el: '#app', data: {}, methods: { goMethod() { //该方法实现路由跳转,跳转到myRouter this.$router.push({ path: '/myRouter?id=123'//query方式传参 }) } }, router })
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。