赞
踩
前端技术的一个特点是项目之间会使用很多第三方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最新语法糖实践
看项目代码,第一件事不是直接看,而是应该将项目运行起来,然后再通过Debug的方式去调试其中的代码,这比单纯的静态分析代码快速靠谱很多。
Vue3官网文档中,只有如何使用VScode Debug Webpack构建的Vue项目的描述,对于Vite构建的项目并没有描述,经过查阅和实践,发现很简单。
点击VSCode中的debug,然后在.vscode目录下生成launch.json文件,其配置如下:
- {
- "version": "0.2.0",
- "configurations": [
- {
- "type": "pwa-msedge",
- "request": "launch",
- "name": "element-plus-admin",
- "url": "http://localhost:3002",
- "webRoot": "${workspaceFolder}"
- }
- ]
- }
这里,我使用Edge浏览器来调试,使用Chrome也是可以的(测试过,可以的),默认生成的launch.json,其url配置如果不符合当前项目启动时的url,那么就需要改一下。
调整好launch.json配置后,直接通过debug形式运行项目,此时VSCode会唤醒Edge并尝试访问launch.json中配置的url,这里就是访问http://localhost:3002,为了让其正常访问,你还需要在命令行中启动项目(如:npm run dev、yarn dev之类的),项目启动后,Edge访问成功,VSCode就会基于你下的断点,停到相应的代码处,你就可以愉快的调试项目了,如下图:
Vite项目会以项目根目录的index.html为项目入口,element-plus-admin也不例外,在index.html中,发现了它是使用了百度统计的代码,估计是统计有多少人使用了它的项目,代码如下:
- <body>
- <div id="app"></div>
- <script type="module" src="/src/main.ts"></script>
- <script>
- var _hmt = _hmt || [];
- // 百度统计代码
- (function() {
- var hm = document.createElement("script");
- hm.src = "https://hm.baidu.com/hm.js?6f30ec463f12087163460a93581d2f3d";
- var s = document.getElementsByTagName("script")[0];
- s.parentNode.insertBefore(hm, s);
- })();
- </script>
- </body>
果断注释掉百度统计相关的代码。
index.html中引入了/src/main.ts,标准的vite项目流程,看到main.ts的代码,主要是引入了各种组件:
- const app = createApp(App)
- direct(app)
- app.use(ElementPlus)
- app.use(router)
- app.use(pinia)
- app.component('SvgIcon', SvgIcon)
-
- // 载入Icon系统 - 全局载入的形式
- const ElIconsData = ElIcons as unknown as Array<() => Promise<typeof import('*.vue')>>
- for (const iconName in ElIconsData) {
- app.component(`ElIcon${iconName}`, ElIconsData[iconName])
- }
-
- app.mount('#app')
其中Element-plus是饿了么提供的CSS样式库(支持Vue3),router则是vue-router,用于实现页面的路由功能,而pinia则是新一代的状态管理工具(用于替代vuex)。Element-plus提供了一套Icon样式,要在项目中随意使用,需要将其全局载入,上述代码中,通过for循环的方式,将Icon全局载入。
接着看到App.vue中的代码,template通过ElconfigProvider标签包裹,该标签是Element-plus中用于提供国际化的标签,使用该标签包裹,页面中的文字内容便可以自动切换不同的语言,代码如下:
- <template>
- <!-- 国际化 https://segmentfault.com/a/1190000041239124 -->
- <ElConfigProvider :locale='locale'>
- <!-- vue-router 渲染标签 -->
- <router-view />
- </ElConfigProvider>
- </template>
:
- setup() {
- changeThemeDefaultColor()
- const { getSetting } = useLayoutStore()
-
- // 重新获取主题色
- const f = () => {
- let themeArray = theme()
- return getSetting.theme >= themeArray.length ? themeArray[0] : themeArray[getSetting.theme]
- }
-
- let themeStyle:Ref<ITheme> = ref(f())
- // 监控变化
- watch(() => getSetting.theme, () => themeStyle.value = f())
- watch(() => getSetting.color.primary, () => themeStyle.value = f())
-
- // 省略剩余代码...
上述代码中,通过theme函数获得主题数组,然后基于用户的选择,获得数组中相应主题的配色,完整的主题替换逻辑比较多,这里抽一个细节来剖析,整体原理是一致的。
通过f函数,获得如下结构的对象:
- {
- tagsActiveColor: '#fff',
- tagsActiveBg: color.primary,
- mainBg: '#f0f2f5',
- sidebarColor: '#fff',
- sidebarBg: '#001529',
- sidebarChildrenBg: '#000c17',
- sidebarActiveColor: '#fff',
- sidebarActiveBg: color.primary,
- sidebarActiveBorderRightBG: '#1890ff'
- },
对象很被ref函数包裹,成为响应式数据,以对象中的tagsActiveColor属性为例,通过全局搜索,可以发现,它在layout-tags-active这个css标签中,代码如下:
- .layout-tags-active {
- background-color: v-bind(themeStyle.tagsActiveBg);
- color: v-bind(themeStyle.tagsActiveColor);
- }
而layout-tags-active会被使用在/src/layout/components/tags.vue中,代码如下:
- <span
- v-for='v in tagsList'
- :key='v.path'
- :ref='getTagsDom'
- class='border border-gray-200 px-2 py-1 mx-1 cursor-pointer'
- <!-- 使用在这里 -->
- :class='{"layout-tags-active": v.isActive}'
- @contextmenu.prevent='contextRightMenu(v,$event)'
- >
这样,用户选择不同样式时,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中就导入了:
- /src/main.ts
-
- import router from '/@/router/index'
- import '/@/permission'
/src/router/index.ts定义了项目会跳转的路径,形式如下:
- export const allowRouter:Array<IMenubarList> = [
- {
- name: 'Dashboard',
- path: '/',
- component: Components['Layout'],
- redirect: '/Dashboard/Workplace',
- meta: { title: '仪表盘', icon: 'el-icon-eleme' },
- children: [
- {
- name: 'Workplace',
- path: '/Dashboard/Workplace',
- component: Components['Workplace'],
- meta: { title: '工作台', icon: 'el-icon-tools' }
- }
- ]
- },
- // 省略剩余代码...
这是vuex-router的写法,没啥好说的,接着主要来看一下/src/permission.ts的逻辑,该文件的代码实现了权限检测、侧边栏路由载入、标签回切以及无权限重定向到login页面的逻辑。
在permission.ts中,会引入router并调用beforeEach函数执行每次访问路由前要执行的逻辑。
所谓侧边栏,如下图:
当项目第一次运行时,我们会访问根路径(即 / ),此时会先执行这里的逻辑,因为第一次执行,会触发未添加侧边栏路由的判断,从而执行添加路由带状态缓存中的逻辑,后续在router中,再使用此时存储的路由:
- // /src/permission.ts
-
- // 判断是否还没添加过路由
- if(getMenubar.menuList.length === 0) {
- await GenerateRoutes()
- await getUser()
- for(let i = 0;i < getMenubar.menuList.length;i++) {
- router.addRoute(getMenubar.menuList[i] as RouteRecordRaw)
- }
- concatAllowRoutes()
- return to.fullPath
- }
上述代码,通过getMenubar函数判断当前是否载入侧边栏路由,为了理清逻辑,先看一下侧边栏是如何展示的。
当用户完成登录后,会访问/src/layout/index.vue构建的页面,该页面由多个组件构建,其中就包括构成页面侧边栏的组件,代码如下:
- // src/layout/index.vue
-
- <div
- v-if='getSetting.mode === "vertical" || getMenubar.isPhone'
- class='layout-sidebar flex flex-col h-screen transition-width duration-200 shadow'
- :class='{
- "w-64": getMenubar.status === 0 || getMenubar.status === 2,
- "w-0": getMenubar.status === 3,
- "w-16": getMenubar.status === 1,
- "absolute z-30": getMenubar.status === 2 || getMenubar.status === 3,
- }'
- >
- <div class='layout-sidebar-logo flex h-12 relative flex-center shadow-lg'>
- <img class='w-8 h-8' :src='icon'>
- <span v-if='getMenubar.status === 0 || getMenubar.status === 2' class='pl-2'>hsianglee</span>
- </div>
- <div class='layout-sidebar-menubar flex flex-1 overflow-hidden'>
- <el-scrollbar wrap-class='scrollbar-wrapper'>
- <layout-menubar />
- </el-scrollbar>
- </div>
- </div>
其对于的组件为LayoutMenubar.vue,查看相关代码,可知,侧边栏具体的逻辑在LayoutMenubar.vue中,代码如下:
- <template>
- <!-- 省略... -->
- <menubar-item v-for='v in filterMenubarData' :key='v.path' :index='v.path' :menu-list='v' />
- <!-- 省略... -->
- </template>
-
- <script lang='ts>
- export default defineComponent ({
- // 省略...
- setup() {
- const route = useRoute()
- const router = useRouter()
- const { getMenubar, setRoutes, changeCollapsed, getSetting } = useLayoutStore()
- // 获得侧边栏路径
- const filterMenubarData = filterMenubar(getMenubar.menuList)
- setRoutes(filterMenubarData)
- // 省略...
- </script>
从上述代码可知,侧边栏路径的数据来自于getMenubar函数,该函数会从状态管理数据(由pinia实现)中获取menubar变量的数据,而menubar变量的数据则来自于permission.ts中的逻辑,回看到permission.ts中的if判断,项目第一次加载时,侧边栏路由数据为空,则会执行if判断中的逻辑,其中的GenerateRoutes函数会获取侧边栏数据,代码如下:
- // src/store/modules/layout.ts
-
- async GenerateRoutes():Promise<void> {
- const res = await getRouterList()
- const { Data } = res.data
- generatorDynamicRouter(Data)
- }
-
-
- // src/api/layout/index.ts
-
-
- const api = {
- login: '/api/User/login',
- getUser: '/api/User/getUser',
- getRouterList: '/api/User/getRoute',
- publickey: '/api/User/Publickey'
- }
-
-
- // 请求API,获得用户可以使用的路由
- export function getRouterList(): Promise<AxiosResponse<IResponse<Array<IMenubarList>>>> {
- return request({
- url: api.getRouterList,
- method: 'get'
- })
- }
没错,侧边栏的数据是通过API请求获得的,其原因是不同的用户可能用于不同的权限,而不同的选择,可以使用的路由是不同的,这里通过API的形式去请求当前用户所能使用的路由,让后端去处理用户权限的问题。
element-plus-admin没有实现后端,而是使用mock.js提供的mock功能,实现了接口响应,并将响应的数据返回,相关的逻辑在:
- // /mock/index.ts
-
- export default [
- // 省略...
- {
- url: '/api/User/getRoute',
- method: 'get',
- timeout: 300,
- response: (req: IReq) => {
- const userName = checkToken(req)
- if(!userName) return responseData(401, '身份认证失败', '')
- // 通过getRoute函数返回当前用户用于使用权限的路由
- return responseData(200, '', getRoute(userName))
- }
- },
用户登录后,会获得Token,每次访问路由前,会判断Token是否存在,如果不存在,则说明用户未登录,此时跳转到登录页面,此外,也会检Token是否失效,相关代码如下:
- /src/permission.ts
-
- // 判断是否登录
- if(!getStatus.ACCESS_TOKEN) {
- // 返回login url
- return loginRoutePath + (to.fullPath ? `?from=${encode(to.fullPath)}` : '')
- }
-
- // 前端检查token是否失效
- useLocal('token')
- .then(d => setToken(d.ACCESS_TOKEN))
- .catch(() => logout())
标签的切换如下图所示,用户访问任何路由,都会记录下来,用户后续可以通过标签访问会之前访问过的路由
在用户访问任意路由前,都将路由记录起来便可以实现这种效果:
- /src/permission.ts
-
- changeTagNavList(to) // 切换导航,记录打开的导航(标签页)
changeTagnavList函数的代码如下:
- /src/store/modules/layout.ts
-
- // 切换导航,记录打开的导航
- changeTagNavList(cRouter:RouteLocationNormalizedLoaded):void {
- if(!this.setting.showTags) return // 判断是否开启多标签页
- // if(cRouter.meta.hidden && !cRouter.meta.activeMenu) return // 隐藏的菜单如果不是子菜单则不添加到标签
- if(new RegExp('^\/redirect').test(cRouter.path)) return
- const index = this.tags.tagsList.findIndex(v => v.path === cRouter.path)
- this.tags.tagsList.forEach(v => v.isActive = false)
- // 判断页面是否打开过
- if(index !== -1) {
- this.tags.tagsList[index].isActive = true
- return
- }
- const tagsList:ITagsList = {
- name: cRouter.name as string,
- title: cRouter.meta.title as string,
- path: cRouter.path,
- isActive: true
- }
- this.tags.tagsList.push(tagsList)
- },
changeTagNavList函数的核心逻辑就是通过访问的路由url构建tagsList,然后将其存到数组中。
通过上面对element-plus-admin中路由流程的处理,知道了项目第一次运行时,登录页面会被优先载入,具体的逻辑在/src/views/User/Login.vue中,当用户点击登录时会调用onSubmit函数处理登录逻辑:
- /src/views/User/Login.vue
-
- const onSubmit = async() => {
- let { name, pwd } = form
- if(!await validate(ruleForm)) return
- await login({ username: name, password: pwd })
- ElNotification({
- title: '欢迎',
- message: '欢迎回来',
- type: 'success'
- })
- }
先看前端是如何实现表单校验的,验证的主要逻辑在validate函数中。
element-plus框架为我们提供了表单验证的写法,看到element-plus文档中表单相关的内容,对表单验证有所介绍,而element-plus-admin项目中使用的正式文档中的写法,具体依赖于async-validator(因为跟文档一致,就不赘述了)。
看到login函数,其具体逻辑如下:
- // /src/store/modules/layout.ts
-
- async login(param: loginParam):Promise<void> {
- // 请求后端接口
- const res = await login(param)
- const token = res.data.Data
- // 设置Token
- this.status.ACCESS_TOKEN = token
- setLocal('token', this.status, 1000 * 60 * 60)
- const { query } = router.currentRoute.value
- router.push(typeof query.from === 'string' ? decode(query.from) : '/')
- },
-
-
- // /src/api/layout/index.ts
-
- export function login(param: loginParam):Promise<AxiosResponse<IResponse<string>>> {
- return request({
- url: api.login,
- method: 'post',
- data: param
- })
- }
上述代码中,login函数会调用/src/api/layout/index.ts的login函数对后端接口请求,然后将获得的Token通过setLocal函数添加到状态管理器中(pinia),token是有过期时间的,然后将路由重定向到登录页面。
理一理流程,用户访问项目时,会访问根路由,而访问路由前,会有对应的前置函数需要执行,具体逻辑在permission.ts中,其中会检测用户是否登录,如果没有登录会将路由重定向到登录页面,当用户登录完后,会重定向到主页,主页页面的具体逻辑在/src/layout/index.vue,核心页面代码如下:
- <template>
- <div class='layout flex h-screen'>
- <div class='layout-sidebar-mask fixed w-screen h-screen bg-black bg-opacity-25 z-20' :class='{"hidden": getMenubar.status !== 2 }' @click='changeCollapsed' />
- <div
- v-if='getSetting.mode === "vertical" || getMenubar.isPhone'
- class='layout-sidebar flex flex-col h-screen transition-width duration-200 shadow'
- :class='{
- "w-64": getMenubar.status === 0 || getMenubar.status === 2,
- "w-0": getMenubar.status === 3,
- "w-16": getMenubar.status === 1,
- "absolute z-30": getMenubar.status === 2 || getMenubar.status === 3,
- }'
- >
- <div class='layout-sidebar-logo flex h-12 relative flex-center shadow-lg'>
- <img class='w-8 h-8' :src='icon'>
- <span v-if='getMenubar.status === 0 || getMenubar.status === 2' class='pl-2'>hsianglee</span>
- </div>
- <div class='layout-sidebar-menubar flex flex-1 overflow-hidden'>
- <el-scrollbar wrap-class='scrollbar-wrapper'>
- <!-- 侧边栏 -->
- <layout-menubar />
- </el-scrollbar>
- </div>
- </div>
- <div class='layout-main flex flex-1 flex-col overflow-x-hidden overflow-y-auto'>
- <div class='layout-main-navbar flex justify-between items-center h-12 shadow-sm overflow-hidden relative z-10'>
- <!-- 顶部状态栏 -->
- <layout-navbar />
- </div>
- <div
- v-if='getSetting.showTags'
- class='layout-main-tags h-8 leading-8 text-sm text-gray-600 relative'
- >
- <!-- 状态栏下的标签栏 -->
- <layout-tags />
- </div>
- <div class='layout-main-content flex-1 overflow-hidden'>
- <!-- 主页的内容主题 -->
- <layout-content />
- </div>
- </div>
- <div class='layout-sidebar-sidesetting fixed right-0 top-64 z-10'>
- <!-- 设置按钮 -->
- <layout-side-setting />
- </div>
- </div>
直观如下图:
/src/layout/index.vue的template非常干净,没啥值传递,所以没啥好说的,我们进去看里面的组件,看看具体是如何使用的。
- // /src/layout/components/menubar.vue
-
- <template>
- <el-menu
- :mode='getMenubar.isPhone ? "vertical" : getSetting.mode'
- :default-active='activeMenu'
- :collapse='getMenubar.status === 1 || getMenubar.status === 3'
- :class='{
- "el-menu-vertical-demo": true,
- "w-64": getMenubar.status === 0 || getMenubar.status === 2,
- "w-0": getMenubar.status === 3,
- "w-16": getMenubar.status === 1,
- "w-full": getSetting.mode === "horizontal" && !getMenubar.isPhone,
- }'
- :collapse-transition='false'
- :unique-opened='true'
- @select='onOpenChange'
- >
- <menubar-item v-for='v in filterMenubarData' :key='v.path' :index='v.path' :menu-list='v' />
- <!-- -->
- </el-menu>
- </template>
上述代码中,通过el-menu构建目录,在el-menu中通过v-bind语法(简写为:)绑定了很多属性,这些属性与TS中的代码相关联,实现动态调整CSS样式的效果,通过v-on语法(简写为@)绑定了select事件,当用户切换侧边栏时,会调用onOpenChange函数:
- // /src/layout/components/menubar.vue
-
- // 点击侧边栏,渲染相应的页面
- const onOpenChange = (d: any) => {
- router.push({ path: d })
- getMenubar.status === 2 && changeCollapsed()
- }
侧别栏中具体的内容有MenubarItem子组件实现,父组件通过v-for语法渲染多个子组件,子组件的template如下:
- // /src/layout/components/menubarItem.vue
-
- <template>
- <!-- v-if v-else 判断是否是子侧边栏 -->
- <el-sub-menu v-if='menuList.children && menuList.children.length > 0' :key='menuList.path' :index='menuList.path'>
- <template #title>
- <component :is='UseElIcon(menuList.meta.icon || "el-icon-location")' />
- <span>{{ menuList.meta.title }}</span>
- </template>
- <el-menu-item-group>
- <menubar-item v-for='v in menuList.children' :key='v.path' :index='v.path' :menu-list='v' />
- </el-menu-item-group>
- </el-sub-menu>
-
- <el-menu-item
- v-else
- :key='menuList.path'
- :index='menuList.path'
- >
- <component :is='UseElIcon(menuList.meta.icon || "el-icon-setting")' />
- <template #title>
- {{ menuList.meta.title }}
- </template>
- </el-menu-item>
- </template>
上述代码中通过v-if与v-else实现侧边栏的嵌套渲染,数据通过props对象从父组件中获取,使用v-if与v-else实现嵌套树形结构的方式值得借鉴。
顶部状态栏多数元素都是静态的,这里主要关注其中动态的部分,显示当前路由路径的,效果如下图:
与之相关的template逻辑如下:
- // /src/layout/components/navbar.vue
-
- <!-- 面包屑导航 -->
- <div class='px-4'>
- <el-breadcrumb separator='/'>
- <transition-group name='breadcrumb'>
- <el-breadcrumb-item key='/' :to='{ path: "/" }'>主页</el-breadcrumb-item>
- <el-breadcrumb-item v-for='v in data.breadcrumbList' :key='v.path' :to='v.path'>{{ v.title }}</el-breadcrumb-item>
- </transition-group>
- </el-breadcrumb>
- </div>
上述代码中,通过v-for指令处理data.breadcrumbList中的数据,相关逻辑如下:
- // /src/layout/components/navbar.vue
-
- // 面包屑导航
- const breadcrumb = (route: RouteLocationNormalizedLoaded) => {
- const fn = () => {
- const breadcrumbList:Array<IBreadcrumbList> = []
- // 工作台与重定向页不显示面包屑导航
- const notShowBreadcrumbList = ['Dashboard', 'RedirectPage'] // 不显示面包屑的导航
- if(route.matched[0] && (notShowBreadcrumbList.includes(route.matched[0].name as string))) return breadcrumbList
- route.matched.forEach(v => {
- const obj:IBreadcrumbList = {
- title: v.meta.title as string,
- path: v.path
- }
- breadcrumbList.push(obj)
- })
- return breadcrumbList
- }
- let data = reactive({
- breadcrumbList: fn()
- })
- // 监控路由
- // route.path => 路由路径
- watch(() => route.path, () => data.breadcrumbList = fn())
- return { data }
- }
-
- setup() {
- const { getMenubar, getUserInfo, changeCollapsed, logout, getSetting } = useLayoutStore()
- const route = useRoute()
- return {
-
- ...breadcrumb(route),
-
- }
- }
上述代码主要是路由路径的处理逻辑,将当前路由路径添加到数组中,没看这里的代码,我还不知道路由对象可以这样使用。
阅读顶部标签栏代码时,发现比较多的功能,但很多功能都没有效果,只有代码,估计有一些问题,先不纠结这些有问题的代码,将有效果的代码学习一遍则可,所谓顶部标签栏如下:
我们在侧边栏访问任意页面,都会在这里生成相应的标签,标签多时,还可以通过滚动条横向滚动,相关HTML 如下:
- // /src/layout/components/tags.vue
-
- <!-- el-srcollbar实现滚动条 -->
- <el-scrollbar ref='scrollbar' wrap-class='scrollbar-wrapper'>
- <div class='layout-tags-container whitespace-nowrap'>
- <span
- v-for='v in tagsList'
- :key='v.path'
- :ref='getTagsDom'
- class='border border-gray-200 px-2 py-1 mx-1 cursor-pointer'
- :class='{"layout-tags-active": v.isActive}'
- @contextmenu.prevent='contextRightMenu(v,$event)'
- >
- <i v-if='v.isActive' class='rounded-full inline-block w-2 h-2 bg-white -ml-1 mr-1' />
- <router-link :to='v.path'>{{ v.title }}</router-link>
- <!-- 如果只剩下一个Tab,不显示关闭 -->
- <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>
- </span>
- </div>
- </el-scrollbar>
标签栏的核心就是利用状态管理的形式,将其他地方操作的逻辑同步到当前组件,然后通过watch方法监听变化,如果发生了变化,再修改template中的样式,核心代码如下:
- // /src/layout/components/tags.vue
-
- const { getTags } = useLayoutStore()
- const { tagsList, cachedViews } = getTags
修改template的逻辑有点繁杂,主要是处理滚动条的逻辑,代码如下:
- // /src/layout/components/tags.vue
-
- // 监听标签页导航
- watch(
- () => tagsList.length,
- () => nextTick(() => {
- if(!scrollbar.value) return
- scrollbar.value.update()
- nextTick(() => {
- const itemWidth = layoutTagsItem.value.filter(v => v).reduce((acc, v) => {
- const val = v as HTMLElement
- return acc + val.offsetWidth + 6
- }, 0)
- if(!scrollbar.value) return
- const scrollLeft = itemWidth - scrollbar.value.wrap$.offsetWidth + 70
- if(scrollLeft > 0) scrollbar.value.wrap$.scrollLeft = scrollLeft
- })
- })
- )
上述代码中通过Vue3提供的nextTick函数实现在DOM更新后再执行nextTick函数中逻辑的操作,nextTick函数接受的是匿名函数,当DOM更新完后,会执行,nextTick函数的原理与JS异步的原理相关,后续单独开文介绍。
上述代码中的逻辑其实就添加了tagsList.length变动时,使用nextTick函数获取tagsList变动后的DOM执行相应的逻辑。
用户在访问旧标签时,会打开旧标签对应的页面,要实现这个,就需要使用缓存,添加标签缓存的逻辑如下:
- // /src/layout/components/tags.vue
-
- const { removeAllTagNav, addCachedViews, removeTagNav } = useLayoutStore()
- onMounted(() => {
- addCachedViews({ name: route.name as string, noCache: route.meta.noCache as boolean })
- })
-
- // /src/store/modules/layout.ts
-
- // 添加缓存页面
- addCachedViews(obj: {name: string, noCache: boolean}):void {
- if(!this.setting.showTags) return // 判断是否开启多标签页
- if(obj.noCache || this.tags.cachedViews.includes(obj.name)) return
- this.tags.cachedViews.push(obj.name)
- },
这里的缓存依旧基于状态管理器实现,不多赘述。
主页内容核心HTML如下:
- // /src/layout/components/content.vue
-
- <router-view v-slot='{ Component }'>
- <transition name='fade-transform' mode='out-in'>
- <keep-alive :include='setting.showTags ? data.cachedViews : []'>
- <component :is='Component' :key='key' class='page m-3 relative' />
- </keep-alive>
- </transition>
- </router-view>
Vue3提供了transition标签实现类似CSS过渡动画的效果,简单而言,通过transition,你不需要写CSS,就可以得到动画效果。
keep-alive同样是Vue3提供的标签,通过keep-alive可以将组件缓存在内存中,避免DOM的重复渲染,通常会使用keep-alive对常用组件或路由进行缓存,这里就是将主页缓存,只要你访问了这个主页,就将其缓存起来,后续通过标签跳转回之前访问过的主页时,就不需要再次渲染,而且与离开时具有一样的数据。
最外层的router-view则会渲染当前路由下的子路由。
element-plus-admin还有很多细节没有研究,本文抛砖引玉,大家可以自行将代码拉下来学习,后续会深挖一下element-plus-admin碰到的,但自己不熟悉的部分。
我是二两,下篇文章见。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。