当前位置:   article > 正文

element-plus-admin源码剖析

element-plus-admin

b79907628d31f3a22de92a9b57713a5a.png

前言

前端技术的一个特点是项目之间会使用很多第三方npm包,在学习时,如果我们只关注其中一个框架,是很难有手感的,我自身就是一个具体的例子,花时间阅读完Vue3文档后,具有灵活运用还有一段距离,这个阶段就需要多看他人成熟项目是怎么编写的,多看具体的实例,本文记录我阅读element-plus-admin这个项目时的细节。

element-plus-admin(https://github.com/hsiangleev/element-plus-admin)使用了Vue中最新的技术栈(Vue3 + Vite + Vue-router 4 + Pinia + element-plus + tailwindcss)构建经典后台,理解其他的代码,方便日后“参考”着开发自己的前端项目。

注意,element-plus-admin项目中并没有使用Vue3最新的写法(因为当时Vue3的一些语法还没推出),但不妨碍我们学习,关于Vue3最新的语法可看:TypeScript+Vue3最新语法糖实践

VsCode Debug Vite构建的Vue3项目

看项目代码,第一件事不是直接看,而是应该将项目运行起来,然后再通过Debug的方式去调试其中的代码,这比单纯的静态分析代码快速靠谱很多。

Vue3官网文档中,只有如何使用VScode Debug Webpack构建的Vue项目的描述,对于Vite构建的项目并没有描述,经过查阅和实践,发现很简单。

点击VSCode中的debug,然后在.vscode目录下生成launch.json文件,其配置如下:

  1. {
  2.   "version""0.2.0",
  3.   "configurations": [
  4.     {
  5.       "type""pwa-msedge",
  6.       "request""launch",
  7.       "name""element-plus-admin",
  8.       "url""http://localhost:3002",
  9.       "webRoot""${workspaceFolder}"
  10.     }
  11.   ]
  12. }

这里,我使用Edge浏览器来调试,使用Chrome也是可以的(测试过,可以的),默认生成的launch.json,其url配置如果不符合当前项目启动时的url,那么就需要改一下。

调整好launch.json配置后,直接通过debug形式运行项目,此时VSCode会唤醒Edge并尝试访问launch.json中配置的url,这里就是访问http://localhost:3002,为了让其正常访问,你还需要在命令行中启动项目(如:npm run dev、yarn dev之类的),项目启动后,Edge访问成功,VSCode就会基于你下的断点,停到相应的代码处,你就可以愉快的调试项目了,如下图:

3b3e93b63d5ef6774c226f33a929e957.png

项目启动流程

Vite项目会以项目根目录的index.html为项目入口,element-plus-admin也不例外,在index.html中,发现了它是使用了百度统计的代码,估计是统计有多少人使用了它的项目,代码如下:

  1. <body>
  2.   <div id="app"></div>
  3.   <script type="module" src="/src/main.ts"></script>
  4.   <script>
  5.     var _hmt = _hmt || [];
  6.     // 百度统计代码
  7.     (function() {
  8.       var hm = document.createElement("script");
  9.       hm.src = "https://hm.baidu.com/hm.js?6f30ec463f12087163460a93581d2f3d";
  10.       var s = document.getElementsByTagName("script")[0]; 
  11.       s.parentNode.insertBefore(hm, s);
  12.     })();
  13.     </script>
  14. </body>

果断注释掉百度统计相关的代码。

index.html中引入了/src/main.ts,标准的vite项目流程,看到main.ts的代码,主要是引入了各种组件:

  1. const app = createApp(App)
  2. direct(app)
  3. app.use(ElementPlus)
  4. app.use(router)
  5. app.use(pinia)
  6. app.component('SvgIcon', SvgIcon)
  7. // 载入Icon系统 - 全局载入的形式
  8. const ElIconsData = ElIcons as unknown as Array<() => Promise<typeof import('*.vue')>>
  9. for (const iconName in ElIconsData) {
  10.     app.component(`ElIcon${iconName}`, ElIconsData[iconName])
  11. }
  12.   
  13. app.mount('#app')

其中Element-plus是饿了么提供的CSS样式库(支持Vue3),router则是vue-router,用于实现页面的路由功能,而pinia则是新一代的状态管理工具(用于替代vuex)。Element-plus提供了一套Icon样式,要在项目中随意使用,需要将其全局载入,上述代码中,通过for循环的方式,将Icon全局载入。

主题样式的切换

接着看到App.vue中的代码,template通过ElconfigProvider标签包裹,该标签是Element-plus中用于提供国际化的标签,使用该标签包裹,页面中的文字内容便可以自动切换不同的语言,代码如下:

  1. <template>
  2.     <!-- 国际化 https://segmentfault.com/a/1190000041239124 -->
  3.     <ElConfigProvider :locale='locale'>
  4.         <!-- vue-router 渲染标签 -->
  5.         <router-view />
  6.     </ElConfigProvider> 
  7. </template>

  1. setup() {
  2.     changeThemeDefaultColor()
  3.     const { getSetting } = useLayoutStore()
  4.     // 重新获取主题色
  5.     const f = () => {
  6.         let themeArray = theme()
  7.         return getSetting.theme >= themeArray.length ? themeArray[0] : themeArray[getSetting.theme]
  8.     }
  9.     let themeStyle:Ref<ITheme> = ref(f())
  10.     // 监控变化
  11.     watch(() => getSetting.theme, () => themeStyle.value = f())
  12.     watch(() => getSetting.color.primary, () => themeStyle.value = f())
  13.     // 省略剩余代码...

上述代码中,通过theme函数获得主题数组,然后基于用户的选择,获得数组中相应主题的配色,完整的主题替换逻辑比较多,这里抽一个细节来剖析,整体原理是一致的。

通过f函数,获得如下结构的对象:

  1. {
  2.     tagsActiveColor: '#fff',
  3.     tagsActiveBg: color.primary,
  4.     mainBg: '#f0f2f5',
  5.     sidebarColor: '#fff',
  6.     sidebarBg: '#001529',
  7.     sidebarChildrenBg: '#000c17',
  8.     sidebarActiveColor: '#fff',
  9.     sidebarActiveBg: color.primary,
  10.     sidebarActiveBorderRightBG: '#1890ff'
  11. },

对象很被ref函数包裹,成为响应式数据,以对象中的tagsActiveColor属性为例,通过全局搜索,可以发现,它在layout-tags-active这个css标签中,代码如下:

  1. .layout-tags-active {
  2.     background-color: v-bind(themeStyle.tagsActiveBg);
  3.     color: v-bind(themeStyle.tagsActiveColor);
  4. }

而layout-tags-active会被使用在/src/layout/components/tags.vue中,代码如下:

  1. <span
  2.     v-for='v in tagsList'
  3.     :key='v.path'
  4.     :ref='getTagsDom'
  5.     class='border border-gray-200 px-2 py-1 mx-1 cursor-pointer'
  6.     <!-- 使用在这里 -->
  7.     :class='{"layout-tags-active": v.isActive}'
  8.     @contextmenu.prevent='contextRightMenu(v,$event)'
  9. >

这样,用户选择不同样式时,tagsActiveColor属性对于的颜色不同,那么上述代码的span其显示的样式就会不同。

路由处理

element-plus-admin中的路由处理非常经典,在用户未登录时,会自动跳到登录页面,当用户token失效时,也会立刻调整到登录页面。

当我们看完App.vue后,会好奇最终的页面是怎么显示的?很明显与vue-router脱不开关系,因为页面会通过router-view标签完成渲染,而该标签主要由vue-router处理,需要注意,因为我们看的是Vue3项目,所以vue-router的版本需要大于4,否则还不支持Vue3中的一些写法。

看到element-plus-admin项目的/src/router目录,vue-router相关的逻辑在main.ts的import中就导入了:

  1. /src/main.ts
  2. import router from '/@/router/index'
  3. import '/@/permission'

/src/router/index.ts定义了项目会跳转的路径,形式如下:

  1. export const allowRouter:Array<IMenubarList> = [
  2.     {
  3.         name: 'Dashboard',
  4.         path: '/',
  5.         component: Components['Layout'],
  6.         redirect: '/Dashboard/Workplace',
  7.         meta: { title: '仪表盘', icon: 'el-icon-eleme' },
  8.         children: [
  9.             {
  10.                 name: 'Workplace',
  11.                 path: '/Dashboard/Workplace',
  12.                 component: Components['Workplace'],
  13.                 meta: { title: '工作台', icon: 'el-icon-tools' }
  14.             }
  15.         ]
  16.     },
  17.     // 省略剩余代码...

这是vuex-router的写法,没啥好说的,接着主要来看一下/src/permission.ts的逻辑,该文件的代码实现了权限检测、侧边栏路由载入、标签回切以及无权限重定向到login页面的逻辑。

在permission.ts中,会引入router并调用beforeEach函数执行每次访问路由前要执行的逻辑。

载入侧边栏路由

所谓侧边栏,如下图:

fab8ca0ff74748823fbbc1622e63a4bf.png

当项目第一次运行时,我们会访问根路径(即 / ),此时会先执行这里的逻辑,因为第一次执行,会触发未添加侧边栏路由的判断,从而执行添加路由带状态缓存中的逻辑,后续在router中,再使用此时存储的路由:

  1. // /src/permission.ts
  2. // 判断是否还没添加过路由
  3. if(getMenubar.menuList.length === 0) {
  4.     await GenerateRoutes()
  5.     await getUser()
  6.     for(let i = 0;i < getMenubar.menuList.length;i++) {
  7.         router.addRoute(getMenubar.menuList[i] as RouteRecordRaw)
  8.     }
  9.     concatAllowRoutes()
  10.     return to.fullPath
  11. }

上述代码,通过getMenubar函数判断当前是否载入侧边栏路由,为了理清逻辑,先看一下侧边栏是如何展示的。

当用户完成登录后,会访问/src/layout/index.vue构建的页面,该页面由多个组件构建,其中就包括构成页面侧边栏的组件,代码如下:

  1. // src/layout/index.vue
  2. <div
  3.     v-if='getSetting.mode === "vertical" || getMenubar.isPhone'
  4.     class='layout-sidebar flex flex-col h-screen transition-width duration-200 shadow'
  5.     :class='{ 
  6.         "w-64": getMenubar.status === 0 || getMenubar.status === 2, 
  7.         "w-0": getMenubar.status === 3, 
  8.         "w-16": getMenubar.status === 1, 
  9.         "absolute z-30": getMenubar.status === 2 || getMenubar.status === 3, 
  10.     }'
  11. >
  12.     <div class='layout-sidebar-logo flex h-12 relative flex-center shadow-lg'>
  13.         <img class='w-8 h-8' :src='icon'>
  14.         <span v-if='getMenubar.status === 0 || getMenubar.status === 2' class='pl-2'>hsianglee</span>
  15.     </div>
  16.     <div class='layout-sidebar-menubar flex flex-1 overflow-hidden'>
  17.         <el-scrollbar wrap-class='scrollbar-wrapper'>
  18.             <layout-menubar />
  19.         </el-scrollbar>
  20.     </div>
  21. </div>

其对于的组件为LayoutMenubar.vue,查看相关代码,可知,侧边栏具体的逻辑在LayoutMenubar.vue中,代码如下:

  1. <template>
  2.            <!-- 省略... -->
  3.           <menubar-item v-for='v in filterMenubarData' :key='v.path' :index='v.path' :menu-list='v' />
  4.            <!-- 省略... -->
  5. </template>
  6. <script lang='ts>
  7. export default defineComponent ({
  8.     // 省略...
  9.     setup() {
  10.         const route = useRoute()
  11.         const router = useRouter()
  12.         const { getMenubar, setRoutes, changeCollapsed, getSetting } = useLayoutStore()
  13.         // 获得侧边栏路径
  14.         const filterMenubarData = filterMenubar(getMenubar.menuList)
  15.         setRoutes(filterMenubarData)
  16.     // 省略...
  17. </script>

从上述代码可知,侧边栏路径的数据来自于getMenubar函数,该函数会从状态管理数据(由pinia实现)中获取menubar变量的数据,而menubar变量的数据则来自于permission.ts中的逻辑,回看到permission.ts中的if判断,项目第一次加载时,侧边栏路由数据为空,则会执行if判断中的逻辑,其中的GenerateRoutes函数会获取侧边栏数据,代码如下:

  1. // src/store/modules/layout.ts
  2. async GenerateRoutes():Promise<void> {
  3.     const res = await getRouterList()
  4.     const { Data } = res.data
  5.     generatorDynamicRouter(Data)
  6. }
  7. // src/api/layout/index.ts
  8. const api = {
  9.     login: '/api/User/login',
  10.     getUser: '/api/User/getUser',
  11.     getRouterList: '/api/User/getRoute',
  12.     publickey: '/api/User/Publickey'
  13. }
  14. // 请求API,获得用户可以使用的路由
  15. export function getRouterList(): Promise<AxiosResponse<IResponse<Array<IMenubarList>>>> {
  16.     return request({
  17.         url: api.getRouterList,
  18.         method: 'get'
  19.     })
  20. }

没错,侧边栏的数据是通过API请求获得的,其原因是不同的用户可能用于不同的权限,而不同的选择,可以使用的路由是不同的,这里通过API的形式去请求当前用户所能使用的路由,让后端去处理用户权限的问题。

element-plus-admin没有实现后端,而是使用mock.js提供的mock功能,实现了接口响应,并将响应的数据返回,相关的逻辑在:

  1. // /mock/index.ts
  2. export default [
  3.     // 省略...
  4.     {
  5.         url: '/api/User/getRoute',
  6.         method: 'get',
  7.         timeout: 300,
  8.         response: (req: IReq) => {
  9.             const userName = checkToken(req)
  10.             if(!userName) return responseData(401'身份认证失败''')
  11.             // 通过getRoute函数返回当前用户用于使用权限的路由
  12.             return responseData(200'', getRoute(userName))
  13.         }
  14.     },

判断登录与Token检测

用户登录后,会获得Token,每次访问路由前,会判断Token是否存在,如果不存在,则说明用户未登录,此时跳转到登录页面,此外,也会检Token是否失效,相关代码如下:

  1. /src/permission.ts
  2. // 判断是否登录
  3. if(!getStatus.ACCESS_TOKEN) {
  4.     // 返回login url
  5.     return loginRoutePath + (to.fullPath ? `?from=${encode(to.fullPath)}` : '')
  6. }
  7. // 前端检查token是否失效
  8. useLocal('token')
  9.     .then(d => setToken(d.ACCESS_TOKEN))
  10.     .catch(() => logout())

记录标签的切换

标签的切换如下图所示,用户访问任何路由,都会记录下来,用户后续可以通过标签访问会之前访问过的路由352f4279c7c3870451bbdb7b8b11815e.png

在用户访问任意路由前,都将路由记录起来便可以实现这种效果:

  1. /src/permission.ts
  2. changeTagNavList(to) // 切换导航,记录打开的导航(标签页)

changeTagnavList函数的代码如下:

  1. /src/store/modules/layout.ts
  2. // 切换导航,记录打开的导航
  3. changeTagNavList(cRouter:RouteLocationNormalizedLoaded):void {
  4.     if(!this.setting.showTags) return // 判断是否开启多标签页
  5.     // if(cRouter.meta.hidden && !cRouter.meta.activeMenu) return // 隐藏的菜单如果不是子菜单则不添加到标签
  6.     if(new RegExp('^\/redirect').test(cRouter.path)) return
  7.     const index = this.tags.tagsList.findIndex(v => v.path === cRouter.path)
  8.     this.tags.tagsList.forEach(v => v.isActive = false)
  9.     // 判断页面是否打开过
  10.     if(index !== -1) {
  11.         this.tags.tagsList[index].isActive = true
  12.         return
  13.     }
  14.     const tagsList:ITagsList = {
  15.         name: cRouter.name as string,
  16.         title: cRouter.meta.title as string,
  17.         path: cRouter.path,
  18.         isActive: true
  19.     }
  20.     this.tags.tagsList.push(tagsList)
  21. },

changeTagNavList函数的核心逻辑就是通过访问的路由url构建tagsList,然后将其存到数组中。

登录流程

通过上面对element-plus-admin中路由流程的处理,知道了项目第一次运行时,登录页面会被优先载入,具体的逻辑在/src/views/User/Login.vue中,当用户点击登录时会调用onSubmit函数处理登录逻辑:

  1. /src/views/User/Login.vue
  2. const onSubmit = async() => {
  3.     let { name, pwd } = form
  4.     if(!await validate(ruleForm)) return
  5.     await login({ username: name, password: pwd })
  6.     ElNotification({
  7.         title: '欢迎',
  8.         message: '欢迎回来',
  9.         type'success'
  10.     })
  11. }

先看前端是如何实现表单校验的,验证的主要逻辑在validate函数中。

element-plus框架为我们提供了表单验证的写法,看到element-plus文档中表单相关的内容,对表单验证有所介绍,而element-plus-admin项目中使用的正式文档中的写法,具体依赖于async-validator(因为跟文档一致,就不赘述了)。

看到login函数,其具体逻辑如下:

  1. // /src/store/modules/layout.ts
  2. async login(param: loginParam):Promise<void> {
  3.     // 请求后端接口
  4.     const res = await login(param)
  5.     const token = res.data.Data
  6.     // 设置Token
  7.     this.status.ACCESS_TOKEN = token
  8.     setLocal('token', this.status, 1000 * 60 * 60)
  9.     const { query } = router.currentRoute.value
  10.     router.push(typeof query.from === 'string' ? decode(query.from) : '/')
  11. },
  12.   
  13.   
  14. // /src/api/layout/index.ts
  15.   
  16. export function login(param: loginParam):Promise<AxiosResponse<IResponse<string>>> {
  17.   return request({
  18.       url: api.login,
  19.       method: 'post',
  20.       data: param
  21.   })
  22. }

上述代码中,login函数会调用/src/api/layout/index.ts的login函数对后端接口请求,然后将获得的Token通过setLocal函数添加到状态管理器中(pinia),token是有过期时间的,然后将路由重定向到登录页面。

主页的载入

理一理流程,用户访问项目时,会访问根路由,而访问路由前,会有对应的前置函数需要执行,具体逻辑在permission.ts中,其中会检测用户是否登录,如果没有登录会将路由重定向到登录页面,当用户登录完后,会重定向到主页,主页页面的具体逻辑在/src/layout/index.vue,核心页面代码如下:

  1. <template>
  2.     <div class='layout flex h-screen'>
  3.         <div class='layout-sidebar-mask fixed w-screen h-screen bg-black bg-opacity-25 z-20' :class='{"hidden": getMenubar.status !== 2 }' @click='changeCollapsed' />
  4.         <div
  5.             v-if='getSetting.mode === "vertical" || getMenubar.isPhone'
  6.             class='layout-sidebar flex flex-col h-screen transition-width duration-200 shadow'
  7.             :class='{ 
  8.                 "w-64": getMenubar.status === 0 || getMenubar.status === 2, 
  9.                 "w-0": getMenubar.status === 3, 
  10.                 "w-16": getMenubar.status === 1, 
  11.                 "absolute z-30": getMenubar.status === 2 || getMenubar.status === 3, 
  12.             }'
  13.         >
  14.             <div class='layout-sidebar-logo flex h-12 relative flex-center shadow-lg'>
  15.                 <img class='w-8 h-8' :src='icon'>
  16.                 <span v-if='getMenubar.status === 0 || getMenubar.status === 2' class='pl-2'>hsianglee</span>
  17.             </div>
  18.             <div class='layout-sidebar-menubar flex flex-1 overflow-hidden'>
  19.                 <el-scrollbar wrap-class='scrollbar-wrapper'>
  20.                     <!-- 侧边栏 -->
  21.                     <layout-menubar />
  22.                 </el-scrollbar>
  23.             </div>
  24.         </div>
  25.         <div class='layout-main flex flex-1 flex-col overflow-x-hidden overflow-y-auto'>
  26.             <div class='layout-main-navbar flex justify-between items-center h-12 shadow-sm overflow-hidden relative z-10'>
  27.                 <!-- 顶部状态栏 -->
  28.                 <layout-navbar />
  29.             </div>
  30.             <div
  31.                 v-if='getSetting.showTags'
  32.                 class='layout-main-tags h-8 leading-8 text-sm text-gray-600 relative'
  33.             >
  34.                 <!-- 状态栏下的标签栏 -->
  35.                 <layout-tags />
  36.             </div>
  37.             <div class='layout-main-content flex-1 overflow-hidden'>
  38.                 <!-- 主页的内容主题 -->
  39.                 <layout-content />
  40.             </div>
  41.         </div>
  42.         <div class='layout-sidebar-sidesetting fixed right-0 top-64 z-10'>
  43.             <!-- 设置按钮 -->
  44.             <layout-side-setting />
  45.         </div>
  46.     </div>

直观如下图:

699bd2687095d0524d095d61f0a962f5.png

/src/layout/index.vue的template非常干净,没啥值传递,所以没啥好说的,我们进去看里面的组件,看看具体是如何使用的。

侧边栏

  1. // /src/layout/components/menubar.vue
  2. <template>
  3.     <el-menu
  4.         :mode='getMenubar.isPhone ? "vertical" : getSetting.mode'
  5.         :default-active='activeMenu'
  6.         :collapse='getMenubar.status === 1 || getMenubar.status === 3'
  7.         :class='{ 
  8.             "el-menu-vertical-demo": true,
  9.             "w-64": getMenubar.status === 0 || getMenubar.status === 2, 
  10.             "w-0": getMenubar.status === 3, 
  11.             "w-16": getMenubar.status === 1, 
  12.             "w-full": getSetting.mode === "horizontal" && !getMenubar.isPhone, 
  13.         }'
  14.         :collapse-transition='false'
  15.         :unique-opened='true'
  16.         @select='onOpenChange'
  17.     >
  18.         <menubar-item v-for='v in filterMenubarData' :key='v.path' :index='v.path' :menu-list='v' />
  19.         <!--  -->
  20.     </el-menu>
  21. </template>

上述代码中,通过el-menu构建目录,在el-menu中通过v-bind语法(简写为:)绑定了很多属性,这些属性与TS中的代码相关联,实现动态调整CSS样式的效果,通过v-on语法(简写为@)绑定了select事件,当用户切换侧边栏时,会调用onOpenChange函数:

  1. // /src/layout/components/menubar.vue
  2. // 点击侧边栏,渲染相应的页面
  3. const onOpenChange = (d: any) => {
  4.     router.push({ path: d })
  5.     getMenubar.status === 2 && changeCollapsed()
  6. }

侧别栏中具体的内容有MenubarItem子组件实现,父组件通过v-for语法渲染多个子组件,子组件的template如下:

  1. // /src/layout/components/menubarItem.vue
  2. <template>
  3.     <!-- v-if v-else 判断是否是子侧边栏 -->
  4.     <el-sub-menu v-if='menuList.children && menuList.children.length > 0' :key='menuList.path' :index='menuList.path'>
  5.         <template #title>
  6.             <component :is='UseElIcon(menuList.meta.icon || "el-icon-location")' />
  7.             <span>{{ menuList.meta.title }}</span>
  8.         </template>
  9.         <el-menu-item-group>
  10.             <menubar-item v-for='v in menuList.children' :key='v.path' :index='v.path' :menu-list='v' />
  11.         </el-menu-item-group>
  12.     </el-sub-menu>
  13.     <el-menu-item
  14.         v-else
  15.         :key='menuList.path'
  16.         :index='menuList.path'
  17.     >
  18.         <component :is='UseElIcon(menuList.meta.icon || "el-icon-setting")' />
  19.         <template #title>
  20.             {{ menuList.meta.title }}
  21.         </template>
  22.     </el-menu-item>
  23. </template>

上述代码中通过v-if与v-else实现侧边栏的嵌套渲染,数据通过props对象从父组件中获取,使用v-if与v-else实现嵌套树形结构的方式值得借鉴。

顶部状态栏

顶部状态栏多数元素都是静态的,这里主要关注其中动态的部分,显示当前路由路径的,效果如下图:

eac7c0f25ad75a2441dce9fde5232e34.png

与之相关的template逻辑如下:

  1. // /src/layout/components/navbar.vue
  2. <!-- 面包屑导航 -->
  3. <div class='px-4'>
  4.   <el-breadcrumb separator='/'>
  5.       <transition-group name='breadcrumb'>
  6.           <el-breadcrumb-item key='/' :to='{ path: "/" }'>主页</el-breadcrumb-item>
  7.           <el-breadcrumb-item v-for='v in data.breadcrumbList' :key='v.path' :to='v.path'>{{ v.title }}</el-breadcrumb-item>
  8.       </transition-group>
  9.   </el-breadcrumb>
  10. </div>

上述代码中,通过v-for指令处理data.breadcrumbList中的数据,相关逻辑如下:

  1. // /src/layout/components/navbar.vue
  2. // 面包屑导航
  3. const breadcrumb = (route: RouteLocationNormalizedLoaded) => {
  4.     const fn = () => {
  5.         const breadcrumbList:Array<IBreadcrumbList> = []
  6.         // 工作台与重定向页不显示面包屑导航
  7.         const notShowBreadcrumbList = ['Dashboard''RedirectPage'// 不显示面包屑的导航
  8.         if(route.matched[0] && (notShowBreadcrumbList.includes(route.matched[0].name as string))) return breadcrumbList
  9.         route.matched.forEach(v => {
  10.             const obj:IBreadcrumbList = {
  11.                 title: v.meta.title as string,
  12.                 path: v.path
  13.             }
  14.             breadcrumbList.push(obj)
  15.         })
  16.         return breadcrumbList
  17.     }
  18.     let data = reactive({
  19.         breadcrumbList: fn()
  20.     })
  21.     // 监控路由
  22.     // route.path => 路由路径
  23.     watch(() => route.path, () => data.breadcrumbList = fn())
  24.     return { data }
  25. }
  26. setup() {
  27.     const { getMenubar, getUserInfo, changeCollapsed, logout, getSetting } = useLayoutStore()
  28.     const route = useRoute()
  29.     return {
  30.         
  31.         ...breadcrumb(route),
  32.     }
  33. }

上述代码主要是路由路径的处理逻辑,将当前路由路径添加到数组中,没看这里的代码,我还不知道路由对象可以这样使用。

顶部标签栏

阅读顶部标签栏代码时,发现比较多的功能,但很多功能都没有效果,只有代码,估计有一些问题,先不纠结这些有问题的代码,将有效果的代码学习一遍则可,所谓顶部标签栏如下:

acd268da439c786fea5aff1fa90545a7.png

我们在侧边栏访问任意页面,都会在这里生成相应的标签,标签多时,还可以通过滚动条横向滚动,相关HTML 如下:

  1. // /src/layout/components/tags.vue
  2. <!-- el-srcollbar实现滚动条 -->
  3. <el-scrollbar ref='scrollbar' wrap-class='scrollbar-wrapper'>
  4.     <div class='layout-tags-container whitespace-nowrap'>
  5.         <span
  6.             v-for='v in tagsList'
  7.             :key='v.path'
  8.             :ref='getTagsDom'
  9.             class='border border-gray-200 px-2 py-1 mx-1 cursor-pointer'
  10.             :class='{"layout-tags-active": v.isActive}'
  11.             @contextmenu.prevent='contextRightMenu(v,$event)'
  12.         >
  13.             <i v-if='v.isActive' class='rounded-full inline-block w-2 h-2 bg-white -ml-1 mr-1' />
  14.             <router-link :to='v.path'>{{ v.title }}</router-link>
  15.             <!-- 如果只剩下一个Tab,不显示关闭 -->
  16.             <el-icon v-if='tagsList.length>1' class='text-xs hover:bg-gray-300 hover:text-white rounded-full leading-3 p-0.5 ml-1 -mr-1' @click='removeTag(v)'><el-icon-close /></el-icon>
  17.         </span>
  18.     </div>
  19. </el-scrollbar>

标签栏的核心就是利用状态管理的形式,将其他地方操作的逻辑同步到当前组件,然后通过watch方法监听变化,如果发生了变化,再修改template中的样式,核心代码如下:

  1. // /src/layout/components/tags.vue
  2. const { getTags } = useLayoutStore()
  3. const { tagsList, cachedViews } = getTags

修改template的逻辑有点繁杂,主要是处理滚动条的逻辑,代码如下:

  1. // /src/layout/components/tags.vue
  2. // 监听标签页导航
  3. watch(
  4.     () => tagsList.length,
  5.     () => nextTick(() => {
  6.         if(!scrollbar.value) return
  7.         scrollbar.value.update()
  8.         nextTick(() => {
  9.             const itemWidth = layoutTagsItem.value.filter(v => v).reduce((acc, v) => {
  10.                 const val = v as HTMLElement
  11.                 return acc + val.offsetWidth + 6
  12.             }, 0)
  13.             if(!scrollbar.value) return
  14.             const scrollLeft = itemWidth - scrollbar.value.wrap$.offsetWidth + 70
  15.             if(scrollLeft > 0) scrollbar.value.wrap$.scrollLeft = scrollLeft
  16.         })
  17.     })
  18. )

上述代码中通过Vue3提供的nextTick函数实现在DOM更新后再执行nextTick函数中逻辑的操作,nextTick函数接受的是匿名函数,当DOM更新完后,会执行,nextTick函数的原理与JS异步的原理相关,后续单独开文介绍。

上述代码中的逻辑其实就添加了tagsList.length变动时,使用nextTick函数获取tagsList变动后的DOM执行相应的逻辑。

用户在访问旧标签时,会打开旧标签对应的页面,要实现这个,就需要使用缓存,添加标签缓存的逻辑如下:

  1. // /src/layout/components/tags.vue
  2. const { removeAllTagNav, addCachedViews, removeTagNav } = useLayoutStore()
  3.   onMounted(() => {
  4.       addCachedViews({ name: route.name as string, noCache: route.meta.noCache as boolean })
  5. })
  6. // /src/store/modules/layout.ts
  7. // 添加缓存页面
  8. addCachedViews(obj: {name: string, noCache: boolean}):void {
  9.     if(!this.setting.showTags) return // 判断是否开启多标签页
  10.     if(obj.noCache || this.tags.cachedViews.includes(obj.name)) return
  11.     this.tags.cachedViews.push(obj.name)
  12. },

这里的缓存依旧基于状态管理器实现,不多赘述。

主页内容

主页内容核心HTML如下:

  1. // /src/layout/components/content.vue
  2. <router-view v-slot='{ Component }'>
  3.   <transition name='fade-transform' mode='out-in'>  
  4.       <keep-alive :include='setting.showTags ? data.cachedViews : []'>
  5.           <component :is='Component' :key='key' class='page m-3 relative' />
  6.       </keep-alive>
  7.   </transition>
  8. </router-view>

Vue3提供了transition标签实现类似CSS过渡动画的效果,简单而言,通过transition,你不需要写CSS,就可以得到动画效果。

keep-alive同样是Vue3提供的标签,通过keep-alive可以将组件缓存在内存中,避免DOM的重复渲染,通常会使用keep-alive对常用组件或路由进行缓存,这里就是将主页缓存,只要你访问了这个主页,就将其缓存起来,后续通过标签跳转回之前访问过的主页时,就不需要再次渲染,而且与离开时具有一样的数据。

最外层的router-view则会渲染当前路由下的子路由。

结尾

element-plus-admin还有很多细节没有研究,本文抛砖引玉,大家可以自行将代码拉下来学习,后续会深挖一下element-plus-admin碰到的,但自己不熟悉的部分。

我是二两,下篇文章见。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/119109
推荐阅读
相关标签
  

闽ICP备14008679号