likes
comments
collection
share

为“管理后台”打造软硬结合的【路导菜】体系🚢

作者站长头像
站长
· 阅读数 68

“路导菜”:本文所有提到的“路导菜”,就是 “路由”、“导航”、“菜单” 的简称。

它们如此重要,如此紧密结合,又如此需要清晰设计。

📖阅读本文,你将

  1. 学习到一种灵活成熟构建 “路导菜” 体系的实践方法

  2. 深入思考 “路导菜” 解耦的意义与必要性

  3. 透过 “反面教材” 思考当前项目结构的设计

  4. 理解 403404 错误页面在构建上差异

一、为什么值得专门讨论

大人,时代变了。

只靠 “文件夹” 和 “档案袋”,哪怕再精明能干的归纳员、记录员也绝对无法处理当前世界里浩瀚如烟的数据和业务。

“数字化转型” 成为了21世纪各行各业无法绕开的时代话题。

于是,除了人们耳熟能详的 “淘宝”、“抖音”、“微博” 等之外,组成互联网世界的另一种重要形态则貌不惊人的 “后台管理系统” 们。

“后台管理系统” 最经典的形象,莫过于这样:

为“管理后台”打造软硬结合的【路导菜】体系🚢

眼熟吗?眼熟就对了。

左侧的菜单、地址栏中的路由、页面重定向及面包屑里所暗藏的页面导航,构成了 “后台管理系统” 的骨骼与经脉。它们支撑系统完成功能的分割、权限的控制、页面的跳转。

那么,如果 “路导菜” 设计不合理,会产生什么问题?

  • 产品说:“列表页跳转到详情页,详情页的面包屑在列表页的后面;”

    你居然发现:不太好实现。

  • 产品说:“我们要调整一下菜单结构,A菜单和B菜单合并,C菜单拆成D和E。”

    你发现:妈耶,怎么还和路由耦合了!和权限点也耦合了?

  • 测试说:“我登录到/,为啥它跳转到了一个 403 页面。”

    你想反驳说那是因为它要跳的页面你没授权,但是想了想,你又说不出口。

  • 大BOSS说:“有用户管理、或者有单位管理权限二者之一的人,就能访问 区域管理页面”。

    你无奈叹气,又要去写难以维护的订制代码。

合理的需求,因为设计问题而无法完成,实在让人难以启齿。

为“管理后台”打造软硬结合的【路导菜】体系🚢

二、以 vue-element-admin 为例

vue-element-admin (76K Star) 是社区的明星项目,非常多的小公司以它作为脚手架,开发出了茫茫多的项目。

不瞒您说,我也用它作为脚手架开发过多个项目,我非常喜欢它。拿它举例子是因为它容易访问,知名度高,且很多朋友都用过,还开源。

它用非常 简单易懂 的方式,实现了一套 “简单易懂” 的 “路导菜” 体系。

但或许是为了降低初学者接触的难度,它的 “路导菜” 体系设计也存在明显的缺陷。

为了避免 “空口批判” ,让我来简单列举几例:

大家可自行前往 demo 地址验证:panjiachen.github.io/vue-element…

2.1 显示 404 页居然是靠重定向实现的?

当我访问一个不存在的路由时,比如panjiachen.github.io/vue-element… 时,地址栏居然跳转到了 /404 路由上。

为“管理后台”打造软硬结合的【路导菜】体系🚢

乍一想,貌似很寻常。

但这真的符合 “互联网用户习惯” 吗?

也许我辛辛苦苦输了一串地址,不小心输错一个字母,回车,地址栏重定向了??我……

那么,看看最常规的做法是什么?

当我访问 "juejin.cn/春哥没秃" 这个肯定不存在的地址时,得到的反馈是:

为“管理后台”打造软硬结合的【路导菜】体系🚢

地址栏不变,但返回一个 404报错 页面,对比一下两种效果,后者是否让你的访问体验大大增强?

这就是一个典型 “路导菜” 设计上差别。

2.2 无权无页 傻傻分不清楚

vue-element-admin 在生成路由时,采用的是 “按需动态生成” 的策略,简单说就是:

未登录用户只有几个基本路由,登录后,再根据你这个用户来生成路由。

乍一听,是这么回事。

可是问题接踵而来,这样的设计很难区分 “没有当前页” 和 “您没权限访问” 两种状态的差异。

比如:我在 vue-element-admin 页面中将角色调整为 editor 后再访问 panjiachen.github.io/vue-element… 这个管理员专属的页面。

你猜怎么着?又给我定位到 404 页面啦~~

为“管理后台”打造软硬结合的【路导菜】体系🚢

问题来了:这符合 “互联网用户习惯” 吗?

答案不言自明,因为还有大家所熟知的 403 页面;

2.3 路由和菜单的耦合

vue-element-admin 项目中,菜单和路由是耦合的。

一级菜单的路由是 '/admin',那么二级菜单就是:'/admin/list'。

这会导致什么问题呢?本文的 “第三节” 会详细阐述对这方面的思考,不在此节细叙。

2.4 “不聪明” 的面包屑

试着在 demo 页里按如下方式操作:

  1. 角色选择为 editor;
  2. 访问 “权限测试页-权限指令” 页面;

为“管理后台”打造软硬结合的【路导菜】体系🚢

“权限测试页” 这个一级菜单下面只有一个 “权限指令” 二级页,当我们点击 “权限测试页” 的面包屑时,最佳体验当然是依然定位到 “权限指令” 页。

但是实际呢?

实际是:又给我跳到 404 错误页了 🤣

PS:虽然我挑了很多刺,但我依然认为这个项目是优秀的项目;

三、三个概念:菜单、路由、站点地图

在正式开始设计我们的 “路导菜” 体系之前,让我们独立而抽离地认识一下 “菜单”、“路由”、“导航” 这三个概念。

3.1 菜单

我们所谈论的 “网页菜单”,通常情况下是一个树形选项合集 (列表可以算一维的树),它的核心作用是提供有限数量的链接,让用户可以通过点击来访问不同的页面;

为“管理后台”打造软硬结合的【路导菜】体系🚢

因此,菜单的核心业务是:"展现" 与 "跳转"。

那么,按以上描述来看,菜单的核心属性,其实只有三个:

export type MenuItem = {
  title: string, // 菜单文本
  path: string, // 菜单链接
  children: MenuItem[] // 子菜单,用以维持一个树
}

3.2 路由

路由是真正的功能入口。

当我们想访问某个页面、或者抵达某个功能时,可能我们并不需要菜单,只需要记得其 “路由” 就能完成页面的访问。

因此,路由的核心作用是:将 “地址栏的地址” 和 “页面” 链接起来,如图:

为“管理后台”打造软硬结合的【路导菜】体系🚢

因此,一个典型的路由其实只需要两个属性:

type Route = {
  path: string,
  component: any // 指向某个路由组件
}

3.2 导航 (站点地图)

"导航" 这个词容易和菜单弄混,因此采用 "站点地图" 这个更为形象准确的词语来作为别名;

路由是访问页面的直接入口。

菜单则提供了可视化的能力,让用户不用在地址栏输入路由,即可访问部分入口。

没错,只是部分!

没有任何一个菜单可以引导你直接进入 id: 9572 这篇文章的编辑页,但你可以通过菜单进入 "文章列表页",再通过 "文章列表页" 上的链接抵达 id: 9572 这篇文章的编辑页。

为“管理后台”打造软硬结合的【路导菜】体系🚢 "面包屑" 是站点地图最直观的表现之一,它代表了从 "首页" 抵达某个页面的标准路径。

之所以说是标准路径,是因为:

"文章列表" => "编辑某篇文章""文章列表" => "文章详情" => "编辑某篇文章"

以上两种访问路径,最终呈现在面包屑上的表现应该是一致的;只有是一致的,才能保证刷新页面之后面包屑不会出现变动;

否则,当你刷新页面或通过路由直接访问时,你的网站将无法定位你从何而来。

出于对 "标准路径" 的诉求,也就有了 "站点地图" (sitemap) 这个概念。

站点地图的结构应该是清晰简单的:

type Sitemap = {
  title: string, //某个页面的标题
  path: string,
  children: Sitemap[]
}

有同学可能会惊呼:“站点地图和菜单的结构是一致的耶!”

是的,它们在结构上是相似的,但显然站点地图所要表现的信息比 "菜单" 更为全面,它可不仅仅是提供部分功能的入口这样简单,它需要描述出所有页面的 "标准路径"。

以上三个概念 "菜单"、"路由"、"站点地图(导航)" 的概念都不是生造的概念,它们是来自实际生产中总结出的实体,是我们后续设计的关键。

四、耦合还是解耦?设计的艺术

让我们再次简短地梳理一下上面三个概念的司职:

  • 路由(route):功能页面的直接入口。
  • 菜单(menu):可视化树形结构,便捷地访问路由。
  • 站点地图(sitemap):形成通往任何页面的标准路径。

回顾上一节,三种实体的属性,我们会发现:

  • 三种实体都有 path 属性。
  • menusitemap 都有 title 属性。

如果分开维护,分开管理这些属性,显然是个 "灾难"。

为撒我点菜单上的 '组织管理',显示的面包屑却是 '组织结构管理'?

多个 pathtitle 的分开管理虽然完全符合解耦的原则,但光是想一想为了维护其一致性,我们将付出海量的精力来维持一致性。

况且,虽说 “路导菜” 是三个实体,各有司职,但他们并不是毫无关系的。

为“管理后台”打造软硬结合的【路导菜】体系🚢 如图所示,三者之间互有关联,因此,为了便于维护管理,我们可以大胆地进行结构上的调整:

type Route = {
  key: string, // 唯一标识
  component: any,
  children?: Route[]
}

type Menu = {
  key: string, // 唯一标识
  children: Menu[],
  title?:string
}

type Sitemap = {
  key: string, // 唯一标识
  path: string,
  title: string,
  children: Sitemap[]
}

将三者通过 key 这一唯一标识进行关联。

这样虽然增加了实体之间的耦合程度,但好处也是肉眼可见的:

  • 菜单(Menu)只需要专注于维护自身的树形结构。

为啥菜单有个可选 title ?因为有的菜单是外链,不具备对应路由;

  • 路由(Route)只需要专注维护它和页面的关系;(当然还有布局)

为啥路由也有了个 Route ?因为无论是 Vue还是React,都习惯使用嵌套来完成页面布局的处理;

  • 站点地图(Sitemap)则用来维护页面所有能访问的页面的关系,维护titlepath 以及其他属性;

  • 这样设计,依然保证了每一个实体在结构上的完整性、独立性;

菜单结构和路由结构完全解耦、页面标准路径与路由地址完全解耦,它们只需要用一个 key 作为关联彼此的依据即可;

考虑实现以下场景:

人员列表页: /users
人员类别维护页:/user-types

菜单结构上:只需要 "人员列表页"

面包屑表现: 人员列表页 > 人员类别维护页

vue-element-admin 项目里,这似乎不太好实现,甚至不知道该怎么实现。

但如果按我们抽取的三个实体,这就显得很好描述:

const routes = [ // 两个页面在路由上没有从属关系
  {
    key: 'Users',
    component: () => import('@/pages/users/index.vue')
  },
  {
    key: 'UserTypes',
    component: () => import('@/pages/user-types/index.vue')
  },
]

const menus = [
  {
    key: 'Users', // 表示只需要一个菜单
  }
]

const sitemaps = [
  {
    key: 'Users',
    title: '人员列表页',
    children: [
      {
        key: 'UserTypes',
        title: '人员类别维护页' // 描述了两个页面的从属关系
      }
    ],
    meta: {
      authPoint: 111 // 在meta里可以挂载所有你需要使用的其他信息
    }
  }
]

通过先解耦、再耦合,我们实现了一个核心目标:

" 结构互不牵制、内容不重复维护 !"

五、转换、生成

以上三个独立结构生成完之后,你可以生成大部分你想要的东西。

比如:vue-router 的相关配置:

/**
 * 通过Key获取sitemap
 **/
const getSitemapByKey = (key: string): Sitemap | undefined => {
  // 省略实现
}

const vueRoutes = routes.map(t => {
  const sitemap = getSitemapByKey(t.key)
  if (!sitemap) {
    return
  }
  return {
    ...t,
    name: t.key,
    path: sitemap.path,
    meta: t.meta
  }
})

如你所见,通过 key 的关联和 sitemaps 的中转,你可以获取各种你需要的结构,并在项目中得以使用;

包括但不限于:

  • 可以直接提供于页面渲染的 “菜单树” 结构
  • 可以直接提供给框架识别的 “路由列表”
  • 可以直接提供给面包屑的 “页面路径”
  • 等等...

六、“按需建楼” 和 “按需开房”

前文提到 vue-element-admin 对于 “无权限” 的页面采用了和 404 完全相同的策略。

导致这个问题的核心原因是 “路由和权限” 设计的两种不同的策略,我通过打比方给它们分别取了两个有趣的名字:“按需建楼” 和 “按需开房”。

需求:一位客户前来租赁办公室,他需要1楼、3楼、和5楼这三个楼层;此时你有两种策略完成任务:

  • 按需建楼:根据客户的需要,只保留1、3、5这三个楼层的办公室,其他楼层全部拆除。因此每当客户前往2楼或者不存在的10楼时,都看到空空如也(404)。

  • 按需开房:你不会去碰楼房的结构,而是给已有的楼层装上门,并提供给客户 “1、3、5” 这三层的钥匙。客户访问不存在的10楼时,看到 404,访问被紧锁的2楼时,却看到 403

当然,这个类比可能有些片面,对于一些过于庞大的项目,加载完整的路由是种负担,但 “加锁” 并告知 “无权访问” 确实是更优质的体验。

但是,你有思考过 404403 在表现形式、以及实现上应该有所差异吗?

六、403404

403 : 'Forbidden', //禁止

404 : 'Not Found', //没有找到

如果你没有想清楚这两个页面的差别,那么在实现时你可能会将它们混为一谈。

  • 404:未找到目标页面;这是一个不用考虑 “用户态” 的页面;无论用户是否完成登录、存在便是存在,不存在便是不存在。因而它不适合被放在 导航-菜单-内容 布局之中,而是以 “全屏幕” 的方式进行显示;

  • 403: 拒绝访问;这是典型的具备 “用户态” 的页面,如果未曾登录,大概率会被直接重定向到 “登陆页”。当用户访问一个 “存在但无权限” 的页面时,更好的表现形式是 “仍然显示 导航&菜单”,但 “内容区” 却告知无权限并显示 403

基于上面的思考,可以发现这两个页面的实现存在较大区别:

  • 404 通常是 “路由的兜底神器”。
[
  ...,// 前面是其他业务路由
  {
    path: '/:pathMatch(.*)*',
    name: '404',
    component: () => import('@/views/error-page/404')
  }
]

其他路由未匹配上的路由,都会进入 404 的怀抱。

为“管理后台”打造软硬结合的【路导菜】体系🚢

  • 403 则因为需要嵌入 “导航、菜单布局”,更好的办法是:直接在布局文件中引用;

比如 Layout.vue 文件中:

// vue2.0 写法
<template>
  <section class="app-main">
    <page403 v-if="!pageAuth"></page403>
    <router-view v-else> </router-view>
  </section>
</template>

<script>
import page403 from '@/views/error-page/403'
export default {
  components: { page403 },
  computed: {
    pageAuth() {
    // 有权限返回 true, 反之 false
    }
  }
}
</script>

以上两种实现 404403 的方法,可以完美解决 “重定向到 404” 的糟糕体验。

七、更智能的“面包屑”和“首页跳转”

本文第 2.4 节描绘了一种不太聪明的面包屑。

但实际项目中,我们可能希望 “自动跳转” 这件事是 “更聪明” 一点的。

而最常见的跳转场景其实有两个:

  1. 非末端的面包屑 2.4节已有详细描述,不再赘述
  2. 根路由 当你刚刚“完成登录”、或是直接在地址栏输入 /、或是点击页面左上角的 “Logo” 时,最佳的实践是访问根路由:/ 但是 / 根路由应该跳到哪里,也存在和 “面包屑” 同样的问题: “如果指定的页面没有权限,我该何去何从?”

vue-element-admin 选择了最简单,但是体验最差的办法:跳转到 404

仔细思考,这其实是一个 “递归查找可访问子节点” 的问题;

当我们 “点某个面包屑” 或者 “访问项目根节点” 时,我理解的过程应该是这样的:

  1. 它本身是否有指定路径、是否是合法页面?如果是、且有权限,直接访问。
  2. 它本身是否指定了重定向页面?如果是、且有权限,直接重定向。
  3. 递归它的子导航(sitemap.children)的每一项,依次按本过程判断。

注:所有页面都应该算 / 的子导航。

按这个思路,当指定路径没有授权时,通过不断向下寻找可访问页面,页面的重定向逻辑出奇的友好,绝不会轻易给用户报错。

八、这套理论适合在什么项目上使用?

答:管理后台 。 (作者没有什么 To C 的开发经验,因此以上概念、方法、理论的总结全部来自 To B 管理后台的,因此是否在 To C 项目上适用不下论断)

九、适合什么技术栈?

答:不限技术栈 。 至少,我已经在 vue@2.xvue@3.xReact微前端 项目上都实践过了,能够充分且有力地支持各种项目的开发、落地。

十、有实践源码吗?

答:暂时只在项目中使用,没有产出开源作品。会有的,在做了

十一、结束语

我是春哥。 大龄前端打工仔,依然在努力学习。 我的目标是给大家分享最实用、最有用的知识点,希望大家都可以早早下班,并可以飞速完成工作,淡定摸鱼🐟。

你可以在公众号里找到我:前端要摸鱼

转载自:https://juejin.cn/post/7101466998516744200
评论
请登录