路由
路由 ( routing ) 是网络工程里的术语,它通过互联的网络把信息从源地址传输到目的地址的活动。(摘自维基百科)
简单举例
生活中最常见的有关路由的东西是路由器,它提供了两种机制:路由和转送
路由时决定数据包从来源到目的地的路径
转送将输入端的数据转移到合适的输出端。
路由中有一个非常重要的概念叫路由表
- 它本质上就是一个映射表,决定了数据包的指向
当我们从运营商那边拉了一条网线之后(给个猫,猫上连出一条网线),我们虽然可以直接插到电脑上,但更多的是把它插到路由器上,再从路由器上连出来一堆网线,或者无线wifi,我们连接这个,这时候我们电脑就会被分配到一个内网 IP,像这样 192.168.0.100
这种 192 开头的地址。在内网内我们都使用这个通信,我们的公网 IP 在路由器上,路由器上有一个映射表,它类似于这种结构。如果在外网有一个人向你的 QQ 发送了一条信息,它会先经过层层路由,再发送到你的公网,也就是你的路由上,再由你的路由根据这个映射表,找到你这台电脑,并把数据传到你的电脑上。同样的,发送信息就是接收信息的反过程。
1 | [ |
前后端路由发展
后端渲染
最开始只有后端渲染,也因此只有后端路由,那时候前端没有 js,只负责 html + css,相当于美工。页面的渲染交给后端来实现,他们可能会使用 JSP (java server page)、php、asp、net 这些脚本语言来直接获取本地服务器里的数据,并且直接渲染到页面上,并生成一个 html + css 文件。这里面已经把内容都加好了,是个静态网页,被放在服务端,并建立一个映射表,当用户发送 url 时,找到对应的 html + css 返回给用户。 结构可能是这样的,因此叫做后端路由,这个阶段也叫后端渲染,后端基本负责了整个网站,并且 JSP 这些语言与 html、css 混杂在一起,非常难以维护,而前端由于不懂 JSP 这些语言,也无法维护网站,全靠后端。
1 | [ |
这种方式的好处是数据渲染在后端完成,客户端不需要处理什么,只需要解析 html,以及利于 seo 优化。
前后端分离
接着出现了 ajax,直接让前后端分离了。此时的情况是 后端可能会有两台服务器(也可能是一台,同时提供静态资源与 API 接口),一台专门提供 API 接口,一台专门提供静态资源,当用户在发送 url 时,会像静态服务器请求资源,拿到 html + css + js,之后再根据 js 代码,通过 ajax 向 API 服务器请求数据,得到数据后返回到用户端,用户端的 js 代码再根据这些数据,操作 DOM 元素来渲染页面内容。
此时就完成了前后端的分离,前端专注交互与页面渲染,后端专注数据与请求。此时的结构可能是这样的
1 | [ |
可以看到,此时仍然是后端路由,但实现了前后端分离。较上一种的好处是实现了局部渲染,以及利于 seo 优化。
SPA(single page application,单页面应用)
整个网页只有一个页面,用了某种技术(hash 路由 或 h5 的history 模式),使得用户在多次改变 url 的时候,不会再向服务器发送多次请求了。此时的情况是,后端仍然是两台服务器不变,当用户输入了 url,静态资源服务器直接返回完整的、全部页面的 html + css + js,也是该服务器里这个域名对应的唯一的 html + css + js 代码(不会再像之前一样,每个 url 都获取各自不同的 html + css + js,即使是同个域名)。 此时的 url 由前端 JS 掌握,一旦 url 发生改变,先去 JS 代码中找到是否有匹配的 html + css + js(其实这是一个组件),如果有,就直接渲染出来。(如果没有要么报错要么跳转)。此时的结构可能是这样的
1 | [ |
这就是前端路由,根据 url 的不同,由 JS 判断,返回不同的组件,并根据组件里的 JS 内容,发送不同的请求到 API 接口获取数据,并渲染到页面上。
这种渲染方式(客户端渲染)的弊端在于 seo 优化,爬虫无法爬取到真正的网页内容(直接右键查看网页源代码,无法查看到真正内容,只能看到一个未被替换的 <div id="app" />
),以及首屏加载速度(由于需要直接下载一堆的 css + js)的问题。
SSR(server side render,服务端渲染)
为了解决客户端渲染首屏加载速度慢与 SEO 问题,ssr 解决方法出现。服务端渲染出 首屏 (不是首页,而是第一次 url 请求的页面)的 DOM 结构返回,前端拿到内容带上首屏,后续的页面操作,再用单页路由和渲染。实际上仍然是个单页面应用。流程是 客户端发送 url 请求到服务端,服务端读取对应的 url 模板信息,并在服务端做出 html 和数据的渲染,渲染完成之后返回 html 结构,客户端这时拿到了首屏的 html 结构,而不是一个等待渲染的 <div id="app" />
,因此查看网页源代码是可以直接看到 DOM 结构的,并且由于直接拿到了首屏内容,浏览首屏的速度会非常快。
SSR 是客户端渲染与后端渲染的一个折中的方案,在渲染首屏的时候在服务端做出了渲染,注意仅仅是首屏,其他页面仍然需要在客户端渲染,服务端接收到请求之后会渲染出首屏页面,并且携带着剩余的路由信息,预留给客户端去渲染其他路由的页面。
Vue 的 SSR 可以使用 Nuxt.js
,不过这不是本文的重点。由于爬虫爬取的页面是一个一个页面爬取的,也就是说,每次拿到的都是首屏,所以完成了 SEO 优化。
hash 路由 与 h5 的 history 模式
目前主流的三大框架都有自己的路由实现
- Angular 的 ngRouter
- React 的 ReactRouter
- Vue 的 vue-router
当然这里的重点是 vue-router
hash 路由 (默认模式)
location.hash = 'foo'
,地址就会变成 域名/#/foo 这将不会发送请求,通过前端路由决定加载的 html css 内容
location.href = 'bar'
,地址就会变成 域名/bar#/ 这将会发送请求,向服务器获取 html css 等资源
hash 模式下,地址栏最后会默认加上 #/
所以实际上是监听 hash 的改变来决定网页的内容
h5 的 history 对象
history.pushState({}, '', home)
,地址就会变成 域名/home 这也不会发送请求,通过前端路由决定加载的 html css 内容 , home 入栈
history.pushState({}, '', about)
,地址就会变成 域名/about 不会发送请求, about 入栈
history.pushState({}, '', about)
,地址就会变成 域名/about 不会发送请求, about 入栈
history.back()
地址就会变成 域名/home 不会发送请求, about 出栈(如果在 hash 模式下使用这个方法,地址就会变成 域名/home#/ )
history.replaceState({}, '', user)
,地址就会变成 域名/user 不会发送请求,但会清空栈的所有内容,再把 user 入栈,栈中只有 user 一个内容
history.go(-1)
等价于 history.back()
在栈中推出一位, go(-2)
就是出两位
history.go(1)
等价于 history.forward()
在栈中加入一位,go(2)
就是入两位
history.pushState(state, title, url) 、 history.replaceState(state, title, url)
,两者参数相同
state: 可以通过 history.state 读取,这个参数是对象
title: 可选参数,暂时没有用,建议传个空字符串,当前大多数浏览器都会忽略这个参数。
url: 改变后的地址
vue-router
基础
vue-router 有点像中间件,在路由跳转的中间需要经过它的控制。
安装 npm i vue-router -S
使用,新建一个 router 文件夹,下面新建一个 index.js
1 | /* /router/index.js */ |
App.vue 中
1 | <template> |
动态路由
1 | // /router/index.js |
App.vue
1 | <template> |
User.vue
1 | <template> |
懒加载
在 vue-cli3+ 中,我们打包后的 js 文件会默认变成 3 个,app.js、vendor.js、manifest.js(vue-cli4+ 中变成两个,这个 manifest 不见了),这就是分包,把一个很大的 js 代码拆分。vue-cli3 已经帮我们下好了分包插件。
app.js 表示我们自己写的业务代码
vendor.js 表示我们引入的第三方库或插件
manifest.js 表示代码的底层支撑,如需要用到的 CommonJS 等
当日后项目的代码量越来越大的时候,app.js 文件也会越来越大,到时候就用户页面会出现短暂空白的情况,因为一直在下载这个 js 文件。这时候我们的处理办法基本是一个路由(路由对应的组件)打包成一个 js 文件,把它们都分开,并且请求它们时不会被全部返回,而是按需向服务器请求,如果我们只点击了首页,则只返回首页的 js 文件,其他仍只存在于服务器上。
懒加载的三种写法
方法一:结合 Vue 的异步组件和 Webpack 的代码分析,非常繁琐,现在基本只在老项目中可能见到
const Home = resolve => { require.ensure(['../components/Home.vue'], () => { resolve(require('../components/Home.vue')) }) }
方法二:AMD 写法
const About = resolve => require(['../components/About.vue'], resolve)
方法三:在 ES6 中,有更简单的写法来组织 Vue 异步组件和 Webpack 的代码分割,现在基本都是这种
const Home = () => import('../components/Home.vue')
现在,按照上面的方法而完成的懒加载是每个路由对应一个 js 文件,如果我们想要分组打包,比如我们有 home 页面和 login 页面,并且这两个页面关系紧密(或者假设我们有两个页面,一个页面嵌套在另一个页面中,即嵌套路由),我们希望它们可以被一起加载,即被打包成一个 js 文件。我们可以使用 babel 插件 npm i @babel-plugin-syntax-dynamic-import
,并在 babel.config.js 中引入它,之后我们就可以改写成
1 | const Home = () => import(/* webpackChunkName: "Home_Login" */ '../components/Home') |
嵌套路由
1 | // /router/index.js |
Home.vue
1 | <template> |
路由传参
有两种方式,一种是上面提到的动态路由,通过 params 获取参数,一种是传入 query,通过 query 获取参数。
params 的类型:
- 配置路由格式: /router/:id
- 传递方式:在 path 后面跟上对应的值
- 传递后形成的路径: /router/123, /router/xiaoming
- 参数获取方式:
this.$route.params.id
- 额外:当然配置格式也可以写成:/router/:id/:age/:height 这种形式,就可以传入多个参数了,写起来就挺麻烦的,而且形成的路径也会多加好几个
/
,所以传多个参数的时候一般不用这个方式。所以一般传一个参数的时候可以用用这种。
query 的类型
- 配置路由格式:/router,也就是普通的配置
- 传递的方式:对象中使用 query 的 key 作为传递方式
- 传递后形成的路径: /router?id=123, /router?id=xiaoming
- 参数获取方式:
this.$route.query.id
- 额外:传入多个参数的时候可以用这种,写起来比较方便,不过它直接把键名也暴露在 url 地址栏中了。
1 | <!-- App.vue --> |
顺便说一下 URL 的组成
URL: scheme://host:port/path?query
URL: 协议://主机:端口/路径?查询
this.$route.params
获取的就是当前的 path,this.$route.query
获取的就是当前的 query
port
网页默认是 80 端口,可以省略,但如果不是 80 端口,就需要主动在地址栏里指定了。
导航守卫
当我们在每次路由跳转的时候,都想顺带执行什么操作的时候,就需要用到导航守卫。
全局守卫
一般常用的就前置守卫。
1 | // 默认写法 |
举例,通过导航守卫来控制页面标题
1 | // /router/index.js |
另外的几种钩子不太常用,用到的时候查文档。
afterEach(hook: (to: Route, from: Route) => any): Function
后置钩子,可以看到,它叫做 hook 而不是 guard,因为它没有 next,也不需要 next
路由独享的守卫
beforeEnter
路由独享的守卫,如果存在全局守卫,先执行全局守卫再执行路由守卫,它的方法参数与全局守卫一致。
1 | const router = new Router({ |
组件内的守卫
beforeRouteEnter
守卫 不能 访问 this,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。(该内容来自官方文档)
1 | const Foo = { |
知道有这些守卫就行,到时候要用直接翻文档。
keep-alive
keep-alive
是 Vue 内置的一个组件,可以使被包裹的组件保留状态,或避免被重新污染。举例,假设包了个 input 框,里面的值在非激活状态下也不会被抹去,下次重新激活的时候,这个值还在。
router-view
也是一个组件,如果被包在 keep-alive 里,所有路径匹配到的视图组件都会被缓存。
1 | <template> |
被包裹的组件将会多出两个生命周期钩子, activated
和 deactivated
,每次被激活或者取消激活状态的时候触发。
组件的 name
组件的 name 属性在 include
和 exclude
里非常重要。没有 name 就只能用正则匹配了。
include
include
可以是字符串或正则,如果是字符,匹配组件的 name 属性,如果要匹配多个组件,用 ,
隔开,并且这里不要随便加空格 ,这样就会不匹配了!,表示被匹配的组件可以有 keep-alive 的效果,也就是多了
activated
和 deactivated
,并且 created
只会调用一次,destroyed
不会被调用
exclude
exclude
可以是字符串或正则,与 include 作用相反,被匹配的组件没有 keep-alive 的效果,被排除在外,同样不要随便加空格。
同样不能加空格的地方是正则,想匹配 2-9个 数字的时候,我们写成 /\d{2,9}/
但绝对不能写成 /\d{2, 9}/
,这是个错误的正则,所以 一般和正则有关的东西,都不要随便加空格。
1 | <!-- Home 页面 --> |