什么是前端路由?

前端路由的一个大背景就是当下流行的单页应用SPA,一些主流的前端框架,如vue、react、angular都属于SPA,那什么是SPA呢?

SPA

SPA(single-page application)单页面应用,就是浏览器只加载了一个Url地址,应用的所有功能、交互都在这个页面内进行。而实现单页面应用的基础就是ajax,通过异步请求动态的切换页面局部内容、实现交互,页面整体完全没有刷新。这避免了页面Url跳转,用户体验也不会中断,就像原生应用一样,体验比较好。越来越多的系统在使用SPA,尤其是WebApp中使用广泛。
与SPA单页应用对应的就是多页应用MPA,当然两者不是非此即彼的,基于业务需求,是共存的。

区别 单页面应用(SPA) 多页面应用(MPA)
页面组成 一个主页,包含多个页面片段 多个主页面
刷新方式 局部刷新 整页刷新
url模式 hash哈希模式 window.**hashchange** history历史模式
SEO搜索引擎优化 难实现,采用页面静态化方式优化 容易实现
数据传递 同一应用内,容易 通过url、cookie、localStorage等传递,复杂
渲染性能 首次加载资源多稍慢,切换快,体验良好 切换加载资源,速度慢,用户体验差
转场动画 容易实现 好像实现不了
维护成本 相对容易 相对复杂

SPA的主要表现就是更新视图而不重新请求页面,要实现前端的页面的自主路由控制,而不会刷新页面,涉及两种主流的技术:hash模式、history模式。

#hash路由原理

**hash**( /hæʃ/ )是Url地址中#号后面的内容(包括#),原本的作用是用于HTML页面内部定位的描点,描点的变化不会导致页面重新加载。HTTP请求中也不会带#,所以刷新也不影响,这是浏览器端的行为。

  • 页面不刷新:hash的变化不会刷新页面,只会触发浏览器定位锚点,这是hash实现前端路由的基本原理。
  • 获取**hash**window.location.hash
  • **hash**变更事件window.**hashchange**监听hash变化。
  • 不同的hash会进入浏览器历史记录。 :::warning http://www.xxx.cn/#/about
    http://www.xxx.cn/#/pro-info-list ::: 所以,实现过程就比较简单了!
    监测hash变化:通过hashchange事件监测hash变化 。
    加载资源:根据hash值匹配不同资源进行加载、切换,在Vue中切换的其实就是不同的组件。
    vue-router路由之路 - 图1
    hash-简易路由示例:codepen
    1. <div id="app2">
    2. <ul id="nav" v-once>
    3. <li v-for="item in navs" v-if="item.title"><a v-bind:href="'#/'+item.url">{{item.title}}</a></li>
    4. </ul>
    5. <div id="main">
    6. <keep-alive>
    7. <component v-bind:is="currentComponent"></component>
    8. </keep-alive>
    9. </div>
    10. </div>
    11. <script>
    12. //components
    13. const NotFound = { template: '<p>404!Page not found</p>' };
    14. const Home = { template: '<p>首页<br>Home page</p>' };
    15. const Product = { template: '<p>产品页面<br>Product page<br><input></p>' };
    16. const About = { template: '<p>关于我们<br>About page</p>' };
    17. //导航路由数据
    18. function Route(title, url, name, component) {
    19. this.title = title; this.url = url; this.component = component; this.name = name;
    20. }
    21. let routes = [
    22. new Route("首页", "home", 'home', Home), new Route("商品", "protect", 'protect', Product),
    23. new Route("招聘", "hr", null, null), new Route("关于", "about", 'about', About),
    24. new Route(null, "not-found", 'not-found', NotFound)];
    25. let components = {};
    26. routes.forEach(item => { components[item.name] = item.component });
    27. //app
    28. let app2 = new Vue({
    29. el: "#app2",
    30. data: { currentRoute: window.location.hash, navs: Object.freeze(routes) },
    31. computed: {
    32. currentComponent: function () {
    33. const com = this.navs.filter(item => '#/' + item.url === this.currentRoute)[0];
    34. if (com && com.component) {
    35. document.title = com.title;
    36. return com.name;
    37. }
    38. return 'not-found';
    39. }
    40. },
    41. components: components,
    42. created: function () {
    43. window.addEventListener("hashchange", () => {
    44. this.currentRoute = window.location.hash;
    45. });
    46. }
    47. });
    48. </script>
    image.png :::warning 📢注意,页面第一次加载的时候,不会触发**popstate**事件。 :::

    history路由原理

    history 是历史对象,存放当前文档页面(或框架)的会话历史记录(不是浏览器的所有历史记录)。
history 属性/方法 描述
length 会话历史列表的记录数量
state 表示历史堆栈顶部记录的状态值,可以是任意可序列化JavaScript对象,限制为2MB
pushState(stateObj, title[, url]) 向当前会话的历史堆栈中添加一条记录
replaceState(stateObj, title[, url]) 修改 history 对象的当前记录
back() 返回到(历史列表中)上一个URL地址。
forward() 前进,加载(历史列表中)下一个URL地址
go(number) 加载指定相对当前网页索引位置的历史列表URL地址,go(-1)等同于back()

**pushState****replaceState **是HTML5在history上新增的API,用来新增、修改当前文档的历史记录,这两个API就是用来实现SPA单页应用前端路由的关键。他们的参数相同:

  • state:一个关联历史会话记录的状态对象,主要作用是在触发 popstate事件时作为参数传递,不需要可以为null,通过history.state可以获取到当前会话的state
  • title:新页面的标题,大部分浏览器都没有管他,可以空着。
  • url:网址,可以相对、绝对地址,但不可跨域。这个url 会更新到浏览器地址栏,但并不会加载该Url地址,也不检查是否存在,页面也不会刷新!对,要的就是你不刷新。

基于这两个API的特性来实现前端路由。用 pushState还是 replaceState呢?两者作用一样的,唯一的不同就是pushState会产生历史记录,可用于前进、后退。
监测url地址变化

  • **popstate**事件:当state变化时触发该事件,在事件中获取当前url地址。pushState、replaceState并不会触发popstate事件,前进、后退、跳转才会触发。
  • 点击事件:绑定click事件,pushState()更新url。

加载资源:根据url值匹配不同资源进行加载、切换。
history-简易路由示例:codepen

  1. <div id="app3">
  2. <ul id="nav" v-once>
  3. <li v-for="item in navs" v-if="item.title"><a href="#"
  4. v-on:click.prevent="navClick(item)">{{item.title}}</a></li>
  5. </ul>
  6. <div id="main">
  7. <keep-alive>
  8. <component v-bind:is="currentComponent"></component>
  9. </keep-alive>
  10. </div>
  11. </div>
  12. <script>
  13. //components
  14. const NotFound = { template: '<p>404!Page not found</p>' };
  15. const Home = { template: '<p>首页<br>Home page</p>' };
  16. const Product = { template: '<p>产品页面<br>Product page<br><input></p>' };
  17. const About = { template: '<p>关于我们<br>About page</p>' };
  18. //导航路由数据
  19. function Route(title, url, name, component) {
  20. this.title = title; this.url = url; this.component = component; this.name = name;
  21. }
  22. let routes = [
  23. new Route("首页", "home", 'home', Home), new Route("商品", "protect", 'protect', Product),
  24. new Route("招聘", "hr", null, null), new Route("关于", "about", 'about', About),
  25. new Route(null, "not-found", 'not-found', NotFound)];
  26. let components = {};
  27. routes.forEach(item => { components[item.name] = item.component });
  28. //拦截history.pushState,触发一个事件。不拦截换其他方式也可以,比如点击事件里。
  29. history.pushState = (function (type) {
  30. let origin = history[type]; //用闭包来存储原来的方法
  31. return function () {
  32. let out = origin.apply(this, arguments);
  33. let event = new Event(type); //触发一个事件
  34. event.arguments = arguments;
  35. window.dispatchEvent(event);
  36. return out;
  37. }
  38. })('pushState');
  39. //app
  40. let app3 = new Vue({
  41. el: "#app3",
  42. data: { currentRoute: history.state?.url, navs: Object.freeze(routes) },
  43. computed: {
  44. currentComponent: function () {
  45. const com = this.navs.filter(item => item.url === this.currentRoute)[0];
  46. if (com && com.component) {
  47. document.title = com.title;
  48. return com.name;
  49. }
  50. return 'not-found';
  51. }
  52. },
  53. components: components,
  54. methods: {
  55. navClick: function (route) {
  56. history.pushState({ url: route.url }, null, route.url);
  57. }
  58. },
  59. created: function () {
  60. window.addEventListener("popstate", () => {
  61. this.currentRoute = history.state?.url; //也可以用location.pathname获取前端url
  62. });
  63. window.addEventListener("pushState", () => {
  64. this.currentRoute = history.state?.url;
  65. });
  66. }
  67. })
  68. </script>

:::warning 📢 刷新页面时会重新加载当前(本地路由的)url地址,可能就404了,这就需要服务端支持,修改下nginx代理也是可以解决的。
history与hash的主要区别,就是不会出现一个#,看上去更加美观?好像也没啥区别吧! :::


开始vue-router

简介

Vue Router是Vue官方推出的路由组件,与Vue深度集成。

  • Vue2.版本 >对应> vue-router3.版本,vue-router3.* 中文文档
  • Vue3.版本 >对应> vue-router4.版本,vue-router4.* 中文文档

    安装

    直接引用JS:
    1. <script src="https://unpkg.com/vue@2/dist/vue.js"></script>
    2. <script src="https://unpkg.com/vue-router@3/dist/vue-router.js"></script>
    CLI

vue-router3基础

router实例-创建Router()

router实例-属性 描述
app、apps Vue根实例,所有apps实例
options 参数选项
history 历史对象?
currentRoute 当前激活的路由信息对象
mode 路由模式
START_LOCATION 初试导航的路由地址,route对象
Router实例-方法 描述
全局的导航守卫 beforeEach、beforeResolve、afterEach
编程式导航 push(route)、replace(route)、go(index)、back()、forward()
resolve() ❓解析目标位置
addRoute(parent?, RouteConfig) 添加路由记录、子路由,还有批量添加的addRoutes(routes)
getRoutes() 获取所有活跃的路由记录列表 Array
onReady(callback,errorback) 完成初始化后调用,初始化错误则调用errorback
onError(callback) 路由过程中出错时触发,算是一个全局路由异常捕获
  1. 注册插件:Vue.use(VueRouter)
  2. 创建全局共享的router路由器实例,并配置路由记录。
  3. 注入router,在根Vue组件上注入router实例,然后所有地方都可以用 this.$router访问了
  4. 愉快的使用了,在Vue组件中访问路由的几种途径:
    • this.$router,Vue中任意地方可以访问的路由器。
    • this.$route,组件所属的route路由对象。
    • 全局路由器 router,不能在模板中绑定。 ```javascript

      {{$router.mode}}—{{$route.path}}

// 注册插件 Vue.use(VueRouter); //路由配置 let vroutes = [ { path: ‘/user/:id’, component: UserBox,name: ‘用户管理’, meta: { title: ‘系统管理’ } }, { path: ‘/login’, component: Login, beforeEnter(to, from, next) { console.log(‘开始登录’); next() } }]; //创建路由器 let vrouter = new VueRouter({ routes: vroutes, mode: ‘history’, base: ‘/vsystem/‘ }); //路由器的钩子 vrouter.beforeEach((to, form, next) => { if (to.name !== ‘Login’ && !isAuthenticated) next({ name: ‘Login’ }); else next(); }); vrouter.onReady(() => console.log(“路由器准备就绪”)); //app let app4 = new Vue({ el: “#app4”, router: vrouter, //注入路由器,内部通过 this.$router 访问 data: {}, })

  1. <a name="xdLKN"></a>
  2. ## Router选项
  3. | [Router**选项**](https://v3.router.vuejs.org/zh/api/#router-%E6%9E%84%E5%BB%BA%E9%80%89%E9%A1%B9) | **描述** |
  4. | --- | --- |
  5. | **routes** | 路由记录集合,Array<RouteConfig> |
  6. | mode | 路由模式,默认`hash`,选项:hash、history、abstract(NodeJS环境) |
  7. | base | url的基本路径,`"/app/"`,只有history模式有效? |
  8. | **linkActiveClass** | <route-link>激活的class名称,默认值为`router-link-active` |
  9. | linkExactActiveClass | 精确激活的 class,默认值为`router-link-exact-active` ( exact /ɪɡˈzækt/ 精确) |
  10. | scrollBehavior | 路由切换完成后的**滚动行为**,函数 Func(to, from, savedPosition) |
  11. | parseQuery/stringifyQuery | 自定义查询字符串的解析/反解析函数 |
  12. | **routes》route**`RouteConfig` | 配置用的路由记录 |
  13. | **path** | url路径 |
  14. | **component** | Component 组件,可用函数方式`import`懒加载组件,提高初始化的性能 |
  15. | components | 命名视图组件,当有多个命名视图<router-view>时,也要配置对应的组件 |
  16. | name | 给路由取个名字,自己用,没其他用途,可作为显示的中文标题 |
  17. | redirect | 重定向路由,重定向到另外的path、route。 |
  18. | alias | path的别名,可一个或多个(数组)别名,渲染组件一致 |
  19. | parent | 父级路由,根级的`parent`为`undefined` |
  20. | **children** | 嵌套路由Array<RouteConfig>,组件内用<router-view>组件作为嵌套组件的容器 |
  21. | **props** | 用于给Vue组件参数Props传值:boolean &#124; Object &#124; Function<br />**true**:自动赋值为`route.params`;**对象,函数**:把它们的结果赋值给组件props参数(按key) |
  22. | **beforeEnter**(to, from, next) | 执行路由前的一个钩子,同全局的守卫钩子`beforeEach` |
  23. | **meta** | 路由元信息,自定义的个性化配置,在路由钩子中可以访问处理。`meta:{title:'注册'}` |
  24. | 运行态的`$route`路由对象 | 组件内`this.$route`访问;钩子函数、导航函数中的to、from、location都是此路由对象 |
  25. | path | 路由路径 |
  26. | fullPath | 解析后的完整url,包含query |
  27. | **params** | 存放动态路径参数,{key:value }对象,组件内使用`this.$route.params.id` |
  28. | query | url查询参数,{key:value }对象 |
  29. | hash | 当前路由的哈希`hash`值 |
  30. | name | 路由名称 |
  31. | **meta** | 元数据记录 |
  32. | matched | 匹配到的路由记录列表 |
  33. ```javascript
  34. interface RouteConfig = {
  35. path: string,
  36. component?: Component,
  37. name?: string, // 命名路由
  38. components?: { [name: string]: Component }, // 命名视图组件
  39. redirect?: string | Location | Function,
  40. props?: boolean | Object | Function,
  41. alias?: string | Array<string>,
  42. children?: Array<RouteConfig>, // 嵌套路由
  43. beforeEnter?: (to: Route, from: Route, next: Function) => void,
  44. meta?: any,
  45. // 2.6.0+
  46. caseSensitive?: boolean, // 匹配规则是否大小写敏感?(默认值:false)
  47. pathToRegexpOptions?: Object // 编译正则的选项
  48. }

路由对象$route

  1. {
  2. name: "user-box", // 路由名称
  3. fullPath: "/user/21/vip?key=admin"
  4. hash: "" // 当前路由的哈希
  5. matched: [{…}]
  6. meta: {}
  7. params: {id: '21', type: 'vip'}
  8. path: "/user/21/vip"
  9. query: {key: 'admin'}
  10. }

path路径:string

path为路由的地址,当浏览器url地址与path匹配时,就会激活当前route路由对象,并显示器对应组件component/components

  1. let u1 = { path: '/home', component: Home };
  2. let u2 = { path: '/about', component: About };
  3. let u3 = { path: '/user/register', component: Register };
  4. //动态路径
  5. let u1 = { path: '/user/:id/:type', component: UserBox }
  6. //匹配的路径
  7. <router-link to="/user/1/vip">用户1</router-link>

🔸**:**动态路径参数
path中可以设置动态参数,冒号:开头,后面的为参数,支持多个顺序组装:path:'/path/:参数1/:参数2'。这里的参数有什么用呢?

  • 参数都会被放到到路由对象$route.params中。
  • 组件内部直接使用:$route.params.id
  • 通过参数专递,设置路由记录props:true,参数值会传递给组件的参数Props

🔸*****通配符*通配符匹配任意字符,v4版本里删了,改用正则。
🔸优先级:如果相同的path,匹配哪个呢?按照代码的顺序,先到先得。

/view

  1. <div>
  2. <h4>router-link</h4>
  3. <router-link to='/user/1/vip'>用户管理</router-link>
  4. <router-link to='/login'>登录</router-link>
  5. </div>
  6. <div>
  7. <h4>v-for绑定</h4>
  8. <router-link v-for="r in this.$router.options.routes" :to="r.path">{{r.name}}</router-link>
  9. </div>
  10. <transition>
  11. <keep-alive>
  12. <router-view></router-view>
  13. </keep-alive>
  14. </transition>

:::warning 📢 当需要监听原生事件时,要加上原生修饰符@click.native="nav_click" :::

编程式导航

除了使用申明式导航<router-link>组件,也可也使用编程式的导航方法自定义实现导航。

router实例-导航方法
push(location, onComplete?, onAbort?) location可以是url字符,也可以是route对象
replace(location, onComplete?, onAbort?) 同上,不会添加history记录,
go(index)、back()、forward() 和浏览器的history操作一样的,历史页面里跳转
  • 如果定义path,则会忽略params(param /ˈpærəm/ 参数)。
  • 回调 onComplete?、onAbort?,也可以用Promise。
    1. <div>
    2. <h4>a标签,编程式导航</h4>
    3. <a href="#" @click.prevent="$router.push({path:'user/21/vip',query:{key:'admin'}})">用户1</a>
    4. <a href="#" @click.prevent="navClick">click登录</a>
    5. <a @.prevent href="#/login?key=hello">原生a</a>
    6. <br>
    7. <a href="#" @click.prevent="$router.back()">后退</a>
    8. <a href="#" @click.prevent="$router.forward()">前进</a>
    9. </div>
    10. <script>
    11. let app = new Vue({
    12. el: "#app",
    13. router: router,
    14. methods: {
    15. navClick() {
    16. if (this.$router.currentRoute.path == '/login')
    17. return;
    18. this.$router.push('/login',null,()=>{}); //提供一个空的onAbort
    19. this.$router.replace('/login');
    20. //设置了path,params的设置就忽略了
    21. this.$router.push({ path: '/login', query: { key: 'admin' },params:{id:100} });
    22. //也可以用name进行导航。注意后面的catch,因为push、replace都是用promise执行的
    23. this.$router.push({ name: '登录', query: { key: 'admin' }, params: { id: 12 } }).catch(s => { });
    24. }
    25. }
    26. })
    27. </script>
    ⚠️ 这里遇到一个小问题,就是通过编程事件导航的<a>链接重复点击报错:NavigationDuplicated
    image.png
    原来是vue-router的一个问题,3.版本中引入了promise时也引入了这个bug,如果路由没变化(重复)就会抛出一个异常的promise。v4.版本都出来了,这个bug还没修复!正常,只有导航编程才会。
    image.png
    解决方法
  1. 判断一下当前路由是否重复。
  2. 提供一个空的onAbort回调,或者promise的方式捕获异常。
  3. 改造一下VueRouter.prototypepush方法。

    导航守卫-钩子

    在导航过程中提供多种守卫(钩子函数),需要注意的是,动态path参数、都会复用组件,此时组件的生命周期就不完整了,需要根据实际情况选择合适的地方。
    vue-router路由之路 - 图5
router实例-全局钩子守卫 描述
beforeEach(to, from, next) 导航执行前,可通过next取消。可用来验证登陆权限,如果没认证则跳转到登陆
beforeResolve(to, from, next) beforeEach执行后,也是前置守卫
afterEach(to, from) 导航已经离开时触发,这里没有next(不可取消路由),因为已经离开了
路由配置记录route-的独有钩子
beforeEnter(to, from, next) 执行路由前调用
Vue组件中新增的-钩子守卫
beforeRouteEnter(to, from, next) 进入前:组件路由被confirmed(已确认)前,组件还没创建,不能获取this
beforeRouteUpdate(to, from, next) 只有动态path参数复用组件时才触发,更新当前路由。
beforeRouteLeave(to, from, next) 路由将要离开该组件前触发,this可用,next(false)可取消。

🔸钩子的参数

  • **to**: Route:目标路由对象
  • **from**: Route:当前导航路由对象,也是要离开的
  • **next**: Function:本次路由怎么执行?内置的回调,必须调用。
    • **nex**()/next(true):允许执行,并继续,全部钩子执行完毕,导航状态为confirmed(已确认)。
    • **next**(false):不执行,中断当前导航,重置导航到from
    • **next**({route}):中断当前导航,并进行一个新的导航。
    • 特殊next(callback)beforeRouteEnternext接收一个回调函数,参数为组件vm,可用来请求一些ajax数据,回调会在组件创建后调用。

🔸使用意见

  • 如果只是对路由做校验或逻辑处理,建议用路由的全局钩子守卫beforeEach,若只是针对某个特定路由,则用路由记录的独有钩子beforeEnter
  • 如果是需要基于组件做一些操作,如数据加载、未保存提示,则用Vue组件的路由守卫钩子。
  • 在复用组件时,通过watch监测路由对象$route的变化也是一个途径。

vue-router4的一些不同

  • 函数创建createRouter({ }),没有之前的类创建了。
  • mode路由模式:mode没了,函数创建historycreateWebHistory()createWebHashHistory()

    1. const router = createRouter({
    2. history:createWebHashHistory() / createWebHashHistory(),
    3. routes: [],
    4. })
  • **base**放到了上面的创建函数参数里。

  • 实例函数router.onReady() 改为 **IsReady**(),该方法返回一个**Promise**
  • 支持了插槽v-slot,支持就算了,关键是影响有点大。

    • 只能通过插槽嵌入到里面,不像之前是放到外面的。
    • 组件的模板也只能通过v-slot+ 动态组件来实现了。
      1. <router-view v-slot="{ Component }">
      2. <transition>
      3. <keep-alive>
      4. <component :is="Component" />
      5. </keep-alive>
      6. </transition>
      7. </router-view>
  • tag 没了,通过插槽实现。

  • 所有的导航现在都是异步的。

    其他问题

    ❓如何构建多级菜单的导航?基本思路:

  • 首先是路由菜单数据,应该是后台数据库统一管理,包括菜单名称、编码、图标、路径path、上下级结构信息等等。

  • 菜单是多级的,这由功能架构来决定,路由还是一级的,因为视图区域是一致的。so,从上述数据中构建2份数据,一份实现多级菜单,另外一份构建路由数据。

❓多标签怎么实现,可以管理使用多个标签?基本思路:

  • 首先要记录打开的路由信息,可以通过路由守卫拦截监测。
  • 用一个标签栏来显示这些打开的路由信息,自己实现切换路由即可。
  • 菜单、标签相互联动,标签删除时需按照一定规则路由到下一个标签上。
  • 刷新保存视图状态:vuex存储菜单、标签显示状态。