经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » JS/JS库/框架 » Vue.js » 查看文章
Vue3+Vite+ElementPlus管理系统常见问题
来源:cnblogs  作者:顾志兵  时间:2023/12/8 11:48:14  对本文有异议

本文本记录了使用 Vue3+Vite+ElementPlus 从0开始搭建一个前端工程会面临的常见问题,没有技术深度,但全都是解决实际问题的干货,可以当作是问题手册以备后用。本人日常工作偏后端开发,因此,文中的一些前端术语描述可能不严谨,敬请谅解。重点是:这里记录的解决方案都是行之有效果的,拿来即可用 ????? ??

1. 页面整体布局

通常管理后台有以下几种经典布局

  • 布局一:纯侧面菜单

    1. ┌────────────────────────────────────────────────────────────────────────────────┐
    2. LOGO Avatar | Exit
    3. ├─────────────────────┬──────────────────────────────────────────────────────────┤
    4. MenuA
    5. ├─────────────────────┤
    6. MenuItem1OfMenuA
    7. ├─────────────────────┤
    8. MenuItem2OfMenuA
    9. ├─────────────────────┤ Main Content Area
    10. MenuB
    11. ├─────────────────────┤
    12. └─────────────────────┴──────────────────────────────────────────────────────────┘

  • 布局二:顶部菜单 + 侧面二级菜单

    1. ┌────────────────────────────────────────────────────────────────────────────────┐
    2. LOGO ┌───────┐ ┌───────┐ Avatar | Exit
    3. MenuA MenuB
    4. ├─────────────────────┬──┘ └──┴───────┴────────────────────────────────────┤
    5. SecondMenu-A-1
    6. ├─────────────────────┤
    7. ThirdMenuItem1-A-1
    8. ├─────────────────────┤
    9. ThirdMenuItem2-A-1
    10. ├─────────────────────┤ Main Content Area
    11. SecondMenu-A-2
    12. ├─────────────────────┤
    13. └─────────────────────┴──────────────────────────────────────────────────────────┘

  • 布局三:顶部菜单 + 侧面二级菜单 + 内容区一菜单一TAB

    1. ┌────────────────────────────────────────────────────────────────────────────────────┐
    2. LOGO ┌───────┐ ┌───────┐ Avatar | Exit
    3. MenuA MenuB
    4. ├─────────────────────┬───┘ └──┴───────┴───────────────────────────────────────┤
    5. SecondMenu-A-1 ┌────────────────────────┐
    6. ├─────────────────────┤ ThirdMenuItem2-A-1 x
    7. ThirdMenuItem1-A-1 ├─┘ └───────────────────────────────────┤
    8. ├─────────────────────┤
    9. ThirdMenuItem2-A-1
    10. ├─────────────────────┤
    11. SecondMenu-A-2 Main Content Area
    12. ├─────────────────────┤
    13. └─────────────────────┴──────────────────────────────────────────────────────────────┘

这个与 VUE 无关,是纯 HTML + CSS 基本功的问题,实现方案有多种,下面是一种基于 flex 的精简参考方案:

Flex样式实现后台管理界面整体布局(点击查看)
  1. <!DOCTYPE html>
  2. <html lang="en" style="margin:0; padding:0">
  3. <head>
  4. <title>Flex样式实现后台管理界面整体布局</title>
  5. </head>
  6. <body style="margin:0; padding:0">
  7. <div style="display:flex; flex-direction: column; height:100vh; width: 100vw;">
  8. <div style="background-color:red; height: 60px">
  9. 顶部标题栏,特别说明:固定高度的区域,本身的display不能为flex, 否则高度会随内容而变,可以再嵌套一个flex布局的div
  10. </div>
  11. <!-- 非顶部区域,需要撑满浏览器窗口的剩余部分,因此其 flex 值为 1 -->
  12. <div style="background:white; display:flex; flex:1; overflow-y:auto;">
  13. <div style="background:black; width:230px; color:white; overflow-y:auto">
  14. 左侧菜单栏,固定宽度
  15. </div>
  16. <div style="overflow-y:auto; flex:1; background-color: yellow; padding: 14px 16px;">
  17. <div style="height=2000px;">
  18. <h2>主内容区</2>
  19. <p>这里特意使用了一个 div 来代表具体的业务页面内容,并将其高度设得很大,以使其出现垂直滚动条效果 </p>
  20. </div>
  21. </div>
  22. <div style="background:aqua; height:60px">
  23. 底部信息栏,(但多数管理系统都会取消这它,以留出更多可视区域给内容展示)
  24. </div>
  25. </div>
  26. </div>
  27. </body>
  28. </html>

对于主内容区的「一菜单一TAB」模式,需要编写JS代码来完成,一般都是通过 el-menu + el-tabs 的组合来实现的。监听 el-menu 组件的 @change 事件,根据所激活的菜单项名称,动态地在主内容区添加TAB

2. 页面刷新后,菜单激活页面的高亮展示问题

el-menu 组件有个 router属性,将其设置为 true 后,点击菜单项,vue 路由就会自动变成 el-menu-item 组件中 index 属性指向的内容,并且该菜单项也会高亮显示

如果点击浏览器的刷新按钮,el-menu 通常会不再高亮显示当前打开的路由页面。

当然,如果 el-menu 指定了default-active属性,则刷新页面后,无论实际路由是什么,菜单栏都会高亮显示default-active属性对应的菜单项。因为刷新页面后,el-menu 组件也重新初始化了,因此它总是高亮default-active指向的菜单项。如果通过代码,将default-active的值改为刷新后的实际路由,则可解决此问题。

需要特别注意的是:简单通过router.CurrentRoute.value的方式获取的当前路由,在一般情况下是ok的,但在刷新时,获取到的值要么为null,要么为/, 而不是url中实际的路由,需要通过监听这个值的变化才能获取到最真实的路由,示例代码如下:

  1. import {watch} from 'vue'
  2. import {useRouter} from 'vue-router';
  3. let router = useRouter()
  4. watch(
  5. () => router.currentRoute.value,
  6. (newRoute) => {
  7. // 这里已拿到最新的路由地址,可将其设置给 el-menu 的 default-active 属性
  8. console.log(newRoute.path)
  9. },
  10. { immediate: true }
  11. )

3. el-input 组件换行问题

这通常是我们在给el-input组件添加一个label时,会看到的现象,就像下面这样

  1. 期望的界面: 实际的界面:
  2. ┌─────────────────┐ Company Name
  3. Company Name ┌─────────────────┐
  4. └─────────────────┘
  5. └─────────────────┘

不只是el-input组件,只要是表单输入类组件,都会换行,有3种解决办法

  • 方法 1
    <el-input><el-form-item>组件包裹起来,如下所示:

    1. <el-form-item label="公司名称" style="width: 200px">
    2. <el-input v-model="companyName" placeholder="请输入公司名称" clearable />
    3. </el-form-item>
  • 方法 2
    自己写一个div, 设置样式display:flex; fext-wrap:nowrap;, 然后将<el-input>放置该div内即可

  • 方法 3
    <el-input>组件添加display:inlinedisplay:inline-block样式,比如我们要实现下面这个效果

    1. ┌─────────────────┐ ┌─────────────────┐
    2. Student Age Range ~
    3. └─────────────────┘ └─────────────────┘

    可以下面这样写

    1. <el-form-item label="Student Age Range">
    2. <el-input v-model="minAge" placeholder="最小值" clearable style="display:inline-block;" />
    3. <p style="display:inline-block; margin: 0 10px;"> ~ </p>
    4. <el-input v-model="maxAge" placeholder="最大值" clearable style="display:inline-block;"/>
    5. </el-form-item>

4. el-form-item 组件设置了padding-bottom属性,但未设置padding-top

由于其padding的上下不对称, 在页面上表现为视觉上的不对称,需要手动设置样式,建议全局为 .el-from-item 类添加对称的 padding

5. 登录页面+非登录页面+路由处理+App.vue的组合协调问题

一套管理管理系统,需具备以下基础特性:

  • a. 首次访问系统根 url 时,应该显示「登录」页面
  • b. 登录成功后,应该进入管理系统的「主页面」
  • c. 在管理系统的主页面,做任何菜单切换,主页面的主体结构不变,只在内容区展示菜单项对应的业务内容
    这里的主体结构是指:标题栏、菜单栏、底部信息栏(如果有的话)
  • d. 管理主页面应该提供「退出」入口,点击入口时,显示「登录」页面
  • e. 在浏览器地址栏直接输入一个「非登录」类 url 后,如果用户已经登录过,且凭证没有过期,则应该直接显示该 url 对应的内容,包括管理「主页面」的主体部分 和 url 指向的实际内容部分
  • f. 在浏览器地址栏直接输入一个「非登录」类 url 后,如果用户未登录,或登录凭证已过期,则应该跳转到「登录」页面
  • g. 在浏览器地址栏直接输入「登录」页面 的URL后,如果如果用户已经登录过,且凭证没有过期,则应该直接进入管理「主页面」并展示「管理首页菜单」的内容

这些基本特征看似很多,其实核心问题就二个:如何实现登录页面与非登录页面的单独渲染,以及以匿名方式访问非登录页面时,自动跳转到登录页面,下面分别说明。

5.1 登录页面与非登录页面的独立渲染

因为非登录页面,通常有固定的布局(如本文第1章节所述),布局中会有一个主内容区,大量的业务组件就在这个区域内渲染。如果设计得不好,就会出现登录组件也被嵌入到这个主内容区的现象,使其成为非登录页面布局中的一个局部区块了,就像下面这样:

期望的界面:

  1. ┌───────────────────────────────────────────────────────┐
  2. ┌───────────────────┐
  3. Username
  4. └───────────────────┘
  5. ┌───────────────────┐
  6. Password
  7. └───────────────────┘
  8. ┌───────┐
  9. Login
  10. └───────┘
  11. └───────────────────────────────────────────────────────┘

实际的界面:

  1. ┌────────────────────────────────────────────────────────────┐
  2. LOGO Avatar
  3. ├───────────────┬────────────────────────────────────────────┤
  4. ┌────────────────┐
  5. Username
  6. └────────────────┘
  7. ┌────────────────┐
  8. Side Menu Passwrod
  9. └────────────────┘
  10. ┌───────┐
  11. Login
  12. └───────┘
  13. └───────────────┴────────────────────────────────────────────┘

出现这个现象的原因是:Vue所有组件的统一入口是App.vue,其它组件都是在这个组件内渲染的。如果我们将非登录页面的布局写在App.vue里,就会出现上面的情况。

方案一:单一 <router-view/> 方式

这个方法是让App.vue内容只有一个 <roter-view/> 组件,这样最灵活,然后再配置路由,将登录组件与非登录组件分成两组路由。示例代码如下:

App.vue
  1. <template>
  2. <router-view/>
  3. </template>
LoginView.vue
  1. <template>
  2. <div> <h2>这是登录页面</h2> </div>
  3. </template>
MainView.vue
  1. <template>
  2. <div class="main-pane-container">
  3. <!-- 顶部栏 -->
  4. <div class="header-pane">
  5. <header-content></header-content>
  6. </div>
  7. <!-- 中央区域 -->
  8. <div class="center-pane">
  9. <!-- 中央左侧菜单窗格-->
  10. <div class="center-aside-pane">
  11. <center-aside-menu/>
  12. </div>
  13. <!-- ① 中央主内容显示窗格 -->
  14. <div class="center-content-pane">
  15. <router-view/>
  16. </div>
  17. </div>
  18. </div>
  19. </template>
  20. <script setup>
  21. import { RouterView } from 'vue-router'
  22. import HeaderContent from './components/HeaderContent.vue'
  23. import CenterAsideMenu from './components/CenterAsideMenu.vue';
  24. </script>
router.js
  1. import { createRouter, createWebHistory } from 'vue-router'
  2. import HomeView from '../views/home/HomeView.vue'
  3. import LoginHomeView from '../views/login/LoginView.vue'
  4. import MainView from '../views/main/MainView.vue'
  5. const router = createRouter({
  6. history: createWebHistory(import.meta.env.BASE_URL),
  7. routes: [
  8. {
  9. path: '/login',
  10. name: 'login',
  11. component: LoginHomeView,
  12. meta: {
  13. // ② 允许匿名访问,即不需要登录
  14. anonymousAccess: true
  15. }
  16. },
  17. {
  18. path: '/',
  19. name: 'main',
  20. component: MainView,
  21. redirect: {path: '/login'},
  22. children: [
  23. {
  24. path: '/home',
  25. name: 'home',
  26. component: HomeView
  27. },
  28. {
  29. path: '/xxx',
  30. name: 'xxx-home',
  31. component: () => import('../views/xxx/XxxHomeView.vue')
  32. },
  33. {
  34. path: '/yyy',
  35. name: 'yyy-home',
  36. component: () => import('../views/yyy/YyyHomeView.vue')
  37. }
  38. ]
  39. },
  40. ]
  41. })

根据以上路由,当访问 / 或 /home 或 /xxx-home 或 /yyy-home 时,App.vue 中的 <router-view/> 会替换成 MainView 组件,而 MainView 组件实现了一个页面主体布局,主内容区(MainView.vue的代码①处)内部又是一个 <router-view/>, 它的内容由 / 后面的路由组件替换。/home 时由 HomeView 组件替换,/xxx-home 时由 XxxHomeView 组件替换。

当访问 /login 时,App.vue 中的 <router-view/> 会替换成 LoginView 组件,与 MainView 组件毫无关系,此时不会加载 MainView 组件,因此页面UI效果就不会出现 MainView 中的布局了,至此便实现了登录页面与非登录页面独立渲染的目的。

方案二:多个 <router-view name="xxx"/> 方式

该方式利用路由的namen属性指定渲染组件,同样可以实现登录页面与非登录页面的独立渲染。其原理是在 App.vue 上,将整个系统的布局划分好,每一个区块都有对应一个命名路由。就像下面这样

  1. <template>
  2. <div id="app">
  3. <router-view name="header"></router-view>
  4. <router-view name="sidebar"></router-view>
  5. <!-- 主内容区 -->
  6. <router-view name="content"></router-view>
  7.  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
  8. <router-view name="footer"></router-view>
  9. </div>
  10. </template>

对非登录页面,将其归属到统一的一个根路由上,这个根路由拥有 header、sidebar、content 、footer 四个组件,这样只要在是匹配非登录页面的路由,这四个组件就一定会为渲染。对于非登录页面的路由,只提供一个content组件,这样 header、sidebar 和 footer 就都不会渲染了。比如下面这个路由

点击查看代码
  1. const router = createRouter({
  2. history: createWebHistory(import.meta.env.BASE_URL),
  3. routes: [
  4. {
  5. name: 'default',
  6. path: '/',
  7. components: {
  8. header: HeaderComponent,
  9. sidebar: SidebarComponent,
  10. content: ContentComponent, // 非登录页面主内容组件
  11.  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
  12. footer: FooterComponent
  13. },
  14. redirect: { name: 'login' },
  15. children: [......]
  16. },
  17. {
  18. name: 'login',
  19. path: '/login',
  20. components: {
  21. // 将登录组件命名为 content, 这样其它的 <router-view> 就不会渲染
  22. // App.vue 将只渲染 <router-view name="content"></router-view>
  23. content: resolve => require(['../views/login/LoginView.vue'], resolve)
  24.  ̄ ̄ ̄ ̄ ̄
  25. },
  26. // ② 允许匿名访问,即不需要登录
  27. meta: {anonymousAccess: true}
  28. }
  29. ]
  30. })

5.2 匿名访问非登录页面时,跳转到登录页面

利用路由跳转期间的钩子函数(官方的术语为导航守卫),在跳转前做如下判断:

  • 目的页面是否允许匿名访问, 如果是则放行,这需要在路由上添加一个匿名访问标志,见上述代码的 ② 处
  • 如果不允许匿名访问,则进一步判断当前用户是否已登录,已登录则放行,反之则将目的页面改为登录页面

示例代码如下(位于main.js文件中):

  1. import router from './router'
  2. // 全局路由监听
  3. router.beforeEach(function (to, from, next) {
  4.  ̄ ̄ ̄ ̄ ̄ ̄ ̄
  5. // 无需登录的页面
  6. if (to.meta.anonymousAccess){
  7. next();
  8. return;
  9. }
  10. // 判断是否已登录
  11. if (isLogin()) {
  12. // 可以在此处进一步做页面的权限检查
  13. ....
  14. next();
  15. } else {
  16. next({path: '/login'});
  17. }
  18. });
  19. router.afterEach((to, from, next) => {
  20. window.scrollTo(0, 0);
  21. });

6. 非开发环境中CSS、图片、JS等静态资源访问404问题

6.1 public 目录下的静态资源 <推荐>

这个目录应该放置那些几乎不会改动的静态资源,代码中应该使用绝对路径来引用它们。且 路径不能以public开头,示例如下:

  1. <template>
  2. <div>
  3. <img alt="public目录图片示例" src="/images/photo/little-scallion.jpg" />
  4. </div>
  5. </template>
  6. <style>
  7. .photo-gallery {
  8. background-image: url(/images/bg/jane-lotus.svg);
  9. }
  10. </style>

6.2 assets 目录下的静态资源

自己编写的大多数公共css、js都应该放在这个目录下,但对于图片,只要不是用来制作独立组件,建议还是放在/public目录下。

当然,这里要针对的就是图片在assets目录下的情况,代码中应该使用绝对路径下引用它们。但该目录下的文件,在开发环境和非开发环境下有些差异,比如:

  • src 目录在非开发环境中是没有的,因此代码中不能直接以 /src/assets 开头
  • assets 下的文件名,在编译后会追加随机hash码,且没有二级目录
    比如:/src/assets/images/sports/badminton.png 会变成 /assets/badminton-04c6f8ef.png

在代码中可以通过 @ 来代表 src 目录在具体运行环境中的位置,至于文件名中追加的 hash 值则不用关心,打包构建时,会一并将代码中的引用也改过来。简而言之,像下面示例中这样书写就OK了。

  1. <template>
  2. <div>
  3. <img alt="assets目录图片示例" src="@/assets/sports/badminton.jpg" />
  4. </div>
  5. </template>
  6. <style>
  7. .album-container {
  8. background-image: url(@/assets/bg/album/jane-lotus.svg);
  9. }
  10. </style>

?? 关于SRC目录路径问题:

SRC 目录的路径,是可以通过代码解析出来的,但需要好几个方便嵌套调用才行,代码就变得很长了,因此才引入了 @ 这个特殊的路径别名,以方便在vue文件中使用。这个别名是在vite.js中声明的,下面是相关片段:

  1. import {resolve} from 'path'
  2. import { fileURLToPath, URL } from 'node:url'
  3. export default defineConfig({
  4. plugins: [vue()],
  5. resolve: {
  6. alias: {
  7. '@': fileURLToPath(new URL('./src', import.meta.url))
  8.  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
  9. // 下面这种写法也可以,而且更简洁
  10. // '@': resolve(__dirname, "src")
  11. }
  12. },
  13. ......
  14. }

6.3 图片的动态路径

这是一个经典问题,也需要区分图片是位于public目录下,还是assets目录下。二者的处理方式差异巨大,为此,特意创建了一个工程来演示不同目录下,动态路径图片的处理效果,见下图:

请点击 这里 下载该演示效果的工程源码

Vue3图片动态路径效果演示

  • 对 public 目录下的图片做动态路径指定<推荐>

    由于public目录下的所有文件都会原样保留,因此,动态路路径只需要保证最后生成的路径串以 / 开头就可以了。因此强烈建议,当需要在运行期间动态指定本地的图片地址时,把这些图片都放置在 public 目录下吧。

  • assets 目录下的图片动态路径处理

    首先说下结论,要对此目录下的图片在运行期做动态引用,非常麻烦。核心原因还是上面①处提到的对assets目录的处理。或许有个疑问,Vite 或 Webpack 打包构建时,为什么要这样做。 因为 Web 的基础就是 HTML + CSS + JS,尽管JS代码运行在客户端浏览器上,但业务数据和图片、视频等资源都在远程服务器上,前端工程源码目录结构一定与最终部署的目录结构是不一样的。前端在之前的非工程化时期,是没有编译这一阶段的,源码目录结构,就是最终部署的结构。

    Vite 打包后的目录中,除了 index.html 文件和 public 目录下的文件外,其它所有文件都被编译构建到了 assets 目录,如下所示

    1. dist
    2. ├─ favicon.ico # 来自public目录,原样保留
    3. ├─ img/ # 来自public目录,原样保留
    4. ├─ css/ # 来自public目录,原样保留
    5. ├─ assets/ # 来自src/asset目录和src/views目录,内容经过编译,路径剪裁至assets目录,文件名追加hash值
    6. └─ index.html # 来自源码工程的根目录,原样保留

    此目录下动态图片解决方案的核心问题是:必须让构建过程对涉及的图片文件进行编译。 编译过程的主要特征为:

    • 只对代码中用到了的图片进行编译

    • 保证编译后新的文件名能与代码中原来的引用关联上

    可以看出,由于编译后图片名称变了,而在源代码中引用图片时,名称还是编译前的名字,因此,编译过程必须要对代码中的文件名进行修改。可以想象,如果源码中的文件名不是字面量(如:'avator/anaonymous.jpg'), 而仅仅是一个变量的话,编译器是极难推断出需要对哪些图片资源进行编译的。事实上也是如此,如果文件名就是一个普通变量,则会原样保留代码。打包后,源码引用的图片不会被编译到目标目录中,也就没有这个图片了。

    花费一翻功夫后,最终得到两种解决方案

    • 方案一: 利用 URL 函数手动提前解析所有图片路径 <推荐>

      点击查看代码
      1. <template>
      2. <div>
      3. <img :src="dynamicImgRef" style="max-height: 300px"/>
      4. <br/>
      5. <input v-model="dynamicImageName" /> &nbsp; &nbsp;
      6. <button @click = "showInputImage">显示输入的图片</button>
      7. </div>
      8. </tempalte>
      9. <script setup>
      10. import {ref} from 'vue'
      11. // ② 需要在运行期动态指定路径的所有图片
      12. const assetsDynamicImages = {
      13. // 1. 一定要用相对路径
      14. // 2. 假定本代码文件所在目录与assets目录是平级关系,否则需要调整 ../assets 的值
      15. 'train.png': new URL('../assets/images/vechile/tain.png', import.meta.url).href,
      16. 'painting.png': new URL('../assets/images/sence/painting.png', import.meta.url).href,
      17. 'sunset.png': new URL('../assets/images/sence/sunset.png', import.meta.url).href,
      18. 'winter.png': new URL('../assets/images/season/winter.png', import.meta.url).href
      19. }
      20. // 输入框中的图片名称,双向绑定
      21. let dynamicImageName = 'sunset.png'
      22. const dynamicImgRef = ref(assetsDynamicImages[dynamicImageName])
      23. // 点击按钮后,显示输入框中的图片
      24. const showInputImage = () => {
      25. dynamicImgRef.value = assetsDynamicImages[dynamicImageName]
      26. }
      27. </script>

      上述 demo 演示的是「根据输入的图片名称显示对应图片」的场景,它的特点为:

      • 适用场景:于需要根据条件来获取相应图片路径的情况。

      • 缺陷:需要在代码中,以字符串明文方式将所有的图片都写进去,即上面②处
        因为只有这样,编译器才能识别出是哪些图片需要处理。如果把这个图片的相对路径都写到另外一个数组,然后以遍历的方式来生成运行时路径都是不行的,构建过程依然不会对图片做编译处理。

      本demo对应的演示效果为 ⑴ 处动图的「assets目录·方式一」部分,但动态图工程的源码与该demo代码并不完全相同。

    • 方式二:通过 import.meta.glob 方法提前加载所有图片路径

      点击查看代码
      1. <template>
      2. <div>
      3. <img :src="dynamicImgRef" style="max-height: 300px"/>
      4. <br/>
      5. <button @click = "displayNextImage">显示下一张图片</button>
      6. </div>
      7. </tempalte>
      8. <script setup>
      9. import {ref} from 'vue'
      10. // ③ 提前加载指定目录的所有图片,可在编译期间提前生成好图片路径,这里返回的是一个图片module数组
      11. let assetsImageFiles = import.meta.glob([
      12. '../sence/**/*.svg'
      13. '../assets/vechile/*.png',
      14. '../assets/sence/*.png',
      15. '../assets/season/*.png'
      16. ],
      17. {eager: true}
      18. );
      19. // ④ 从上一步加载的所有图片模块中,提取出图片路径
      20. const assetsDynamicImageUrls = []
      21. Object.values(assetsImageFiles).forEach(imgModule => {
      22. assetsDynamicImageUrls.push(imgModule.default) // default 属性就是编译后的图片路径
      23.  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
      24. })
      25. // 默认显示第一张
      26. let imageIndex = 0
      27. const dynamicImgRef = ref(assetsDynamicImageUrls[imageIndex])
      28. function displayNextImage() {
      29. imageIndex ++
      30. if(imageIndex >= assetsDynamicImageUrls.length) {
      31. imageIndex = 0
      32. }
      33. dynamicImgRef.value = assetsDynamicImageUrls[imageIndex]
      34. }
      35. </script>

      上述 demo 演示的是「循环显示一组图片」的场景,它的特点为:

      • 可以遍历一组图片,而无需要提前知道图片名称
      • 这组图片路径虽然也是在编译阶段提前加载的,但不用在代码中以一图一码的方式硬编码加载(就像上面的方式一)
      • 很难通过图片名称的方式单独提取其中的一张图片路径
        因为编译后图片名称加了Hash后缀,同时图片的目录层级也没有了。如果工程中不同目录下,存在相同名称的图片,就无法在编译后通过原始名称来精准提取图片路径

      本demo对应的演示效果为 ⑴ 处动图的「assets目录·方式二」部分,但动态图工程的源码与该demo代码并不完全相同。

    ?? 关于URL函数

    1. 示例中的这个 URL 函数是HTML的客户端JS运行环境标准库中的函数,不是Node的 URL 模块,Node的URL模块只能用于服务器端,或前端的打包构建工具。

    2. vite文档 中提到,如果URL函数的文件路径是 es6 语言规范的模板字符串,编译器也会支持对该模板字符串所指向的图片路径做编译转化,比如:

      1. function getImageUrl(name) {
      2. return new URL(`./dir/${name}.png`, import.meta.url).href
      3.  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
      4. }

      经过实测,大多数情况下,以上代码都会在打包部署后得到404响应。因为,上述代码如果要在部署后正确访问图片,必须保证模板字符串(上述代码的下划线部分)在编译阶段是可解析执行的,即它可以被解析成普通字符串,然后编译器再对解析后的普通字符串所指向的图片路径,进行转化(追加hash + 去除中间路径)。

      上述代码中,如果name这个变量指向了一个明确的字符串,则 `./dir/${name}.png` 这个模板字符串在编译期间是可解析成普通字符串的,反之则不可以。由于多数情况下,动态图片的名称不会是一个固定值,因此name变量或许在一开始可以指向一个明确的串,但在运行期一定会变化,而变化后所指向的图片路径,在编译期是无法感知到的,这些图片也就不会做转化了

    ?? 关于 import.meta.glob 方法

    import.metea.glob 方法是 vite 引入的,它支持将多个文件以 module 的方式加载,默认是异步加载,也可以通过参数指定为同步加载

7. 非开发环境中业务路径404问题

除了动态图片的404问题,另一类更常见的是页面路径404问题。由于是单页应用,所有的页面都是在浏览器客户端完成的,在访问都页时,所有的页面信息其实就已经加载完了。只需要在浏览器本地加载不同的vue页面即可,这是通过变更本地路由地址来实现的。Vue提供了两种路由模式,分别是 Hash 和 History:

  • Hash 路由
    这是早期vue的默认模式,该模式没有404问题,它在语义上它更符合单页面应用,比如:http://localhost:5173/#/userManage , 其中 # 表示定位到当前页面的某个位置。这种定位语义是 HTML 标准,因此它天然就适合用作单页面应用。当切换路由时,只变更 # 号后面的值,然后 vue 的路由组件会根据 # 后的内容重新加载本地页面。可以看出,页面变更全过程中,客户端均不会请求服务器,因此不会出现404问题。

  • History 路由
    会出现404问题的就是这种模式,由于Hash模式url中的 # 明显暴露了应用的技术细节,且看上去不像是一个网站。vue 路由便引入了history 模式。该模式最大的特点是url的内容看上去与正常的网站没有区别,变更路由时,也会向服务器发请求,即:无论是在视觉上还是行为上,整个路由切换(页面变更)过程都与普通网站访问相同。

    但 vue 项目终究是单页面应用,页面的变更最终还在客户端完成的。当客户端向服务器端请求 http://loalhost:5173/userManage 页面时,服务器端是没有这个页面的,它只有 index.html 和 assets 目录下的image、css、js, 因此会返回404。如果服务器不返回404,而是再次返回到index.html的话,客户端就可以根据请求的 url,来变更单页应用的界面了。

    结合 nginx 服务器的 try_files 指令和 命名location 指令正好可以实现上述方案, 示例代码如下:

    1. server {
    2. listen 31079;
    3. location / {
    4. root /www/vue-demo; # vue工程打包后部署到服务器上的目录
    5. index index.html index.htm;
    6. # 凡是在服务器上找不到文件的 uri,都转交给 @vue-router 这个命名Location来处理
    7. try_files $uri $uri/ @vue-router;
    8.  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
    9. }
    10. # 将所有请求路径,都重写到index.html,这样就又回到了单页面应用上,但浏览器地址栏的url变了
    11. location @vue-router {
    12. rewrite ^.*$ /index.html last;
    13. }
    14. }

Hash 模式与 History 模式的声明示例代码如下:

  1. import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
  2. // Hash 模式路由
  3. const hashRouter = createRouter({
  4. history: createWebHashHistory (import.meta.env.BASE_URL),
  5. routes: [ ...... ]
  6. })
  7. // History 模式路由
  8. const historyRouter = createRouter({
  9. history: createWebHistory (import.meta.env.BASE_URL),
  10. routes: [ ...... ]
  11. })

8. 请求被浏览器本地缓存的问题

浏览器默认会在客户端电脑上缓存 GET 请求方式获得的 http 响应内容,当再次请求时,会直接从缓存中读取,不再向后端服务器发送请求了。这是属于早期 HTML 协议的约定。解决办法为,每次请求时,在 url 后拼接一段随机数,使得每次 GET 请求的地址都不一样,浏览器的缓存里也就没有当前这个URL的内容了,便会向后端服务器发送请求,同时又不影响正常业务参数的传递。

比如,我们约定这个随机数的参数为 rid, 即 RequestIdentifier 的意思,可以像下面这样拼接 url 串

  1. let url= 'http://localhost:3751/company/getByName?name=同仁堂&rid=' + Math.random() * 100000

9. 将 el-pagination 分页组件的语言由默认的英文改为中文

  • 1. 在main.js文件中,引入element-plus/es/locale/lang/zh-cn 这个本地化组件
  • 2. 在 app 应用 ElementPlus 组件时,指定 locale 属性值为第1步中引入的组件
  1. import { createApp } from 'vue'
  2. import ElementPlus from 'element-plus'
  3. import zhCn from 'element-plus/es/locale/lang/zh-cn'
  4.  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
  5. import App from './App.vue'
  6. const app = createApp(App)
  7. app.use(ElementPlus, {locale: zhCn})
  8.  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄

?? 关于 zh-cn 这个中文 locale 的路径问题:

部分ElementPlus版本,zh-cn 语言包的路径为:element-plus/lib/locale/lang/zh-cn。本文本当前(2023-11-24)使用的这个路径,是基于2.4.1版本的。没准以后该语言包的位置还会移动,不过可以按照以下步骤来定位zh-cn的位置

  • 进入当前工程的 node_modules 目录中
  • 找到 elment-plus 目录,并进入
  • 在该目录中搜索 zh-cn

10. 不同工程的Node版本不同且不兼容的问题

最好的办法是安装 nvm(Node Version Manager) 来实现同一电脑上同时安装和使用多个 node 的目的,windows 系统上请安装 nvm-windows

需要注意的是,如果在安装 nvm 时,你的系统已经安装了node, 则需要将其卸载,并尽可能清除干净,其它则按照官网文档安装即可。下面列出最常用的几个命令:

命令 功能
nvm -v 查看 nvm 的版本
nvm ls 查看已安装的 node 版本和当前正在使用的 node 版本
nvm ls available 列出所有可安装的 node 版本
nvm install <version> 安装指定的 node 版本
nvm use <version> 在当前shell环境,使用指定的 node 版本,该版本必须先安装

11. npm 安装时进度卡在 reify 阶段的问题

这里讲述的方案仅适用于我的的环境,不能保证其它环境也能用同样的方式解决。

我之前是直接从 官网 安装的nodejs, 然后直接使用了配套的 npm 命令安装其它依赖包,这些操作就是OK的。后来我把 Node 卸载了,重新安装了 nvm-windows, 然后以 nvm 的方式安装了 node,再使用 npm 安装它工具包,便出现了安装过程阻塞在 reify 这个阶段,有时候2分钟后完成安装,有时候就一直阻塞在哪里,直到超时。

我的解决办法是将NPM的非官方镜像源(我的是淘宝),还原为官方镜像。

  1. D:\SourceCode\cnblos > npm get registry
  2. https://registry.npmmirror.com # 之前是淘宝镜像源
  3. D:\SourceCode\cnblos > npm set registry https://registry.npmjs.org/ # 还原为官方镜像源

12. Vite 命令启动项目成功,但localhost访问时返回404

我的情况是这样的,通过命令 npm run serve 启动项目后,可以正常访问。退出后再通过命令npx vite 启动项目成功,输出内容如下:

  1. VITE v5.0.3 ready in 567 ms
  2. ? Local: http://localhost:5173/
  3. ? Network: use --host to expose
  4. ? press h + enter to show help

然后在浏览器里访问 http://localhost:5173/ 返回404状态码,再次使用 npx vite --debug 方式启动,刷新页面后,可以看到控制台有「路径 / 到 /index.html」的redirect内容输出,但页面状态依然是404。

最终发现,该工程在创建时,使用的命令是 vue create xxxx,改用 npm init vite 创建项目后,再以 npx vite 启动便可正常访问了。

事实上,更正统的vite项目创建命令是 npm create vite@latest 工程名 -- --template vue, 在 vite官网 上有创建 vite 工程的详细说明,是我自己将它与 vue 二者的关系搞混了。经过对比,可以看到两种方式创建的工程,其 vite.config.js 文件内容是有差异的。

?? 关于vite server更常见的404问题

另一种常见的404问题,是非本机访问时(局域网的其它电脑访问),会报无法建立连接的错误。原因是 vite 默认只监听了 localhost 这个主机名。

最高效简单的办法是启动命令加上 --host 选项,如:npx vite --host [本机在局域网的IP地址],方括号的内容为可选。多数情况下,前端项目都只在本机自己调试,偶尔才需要他人来访问,因此,这个办法足够了。

如果不想每次都在命令上加 --host 选项,可直接在 vite.config.js 中配置,如下:

  1. import { defineConfig } from 'vite'
  2. import vue from '@vitejs/plugin-vue'
  3. export default defineConfig({
  4. plugins: [vue()],
  5. server: {
  6. host: '0.0.0.0', // 监听的IP地址
  7. port: 5173, // 监听的端口
  8. open: true // 启动后是否打开浏览器访问
  9. }
  10. })

详细配置可去 vite官网配置文档 查阅

?? 工程名不要带有空格

如果vite创建的工程名带有空格,在本机开发调试阶段,可能会遭遇用 localhost 访问也返回404的情况。2022年时已经有老外在 GitHub上提出这个 bug,至少到当前(2023-11)为止,该bug依然未修复。但经过尝试,发现通过脚手架命令无法创建名称带有空格的工程,估计这个老外是手动创建的工程结构。

13. el-row 组件的 gutter 属性导致出现水平滚动条

解决方案:给 el-row 的父组件设置一个合适的左右 padding 值,比如:padding:0 12px;

OK,问题来了,如何知道这个合适的 padding 值是多少?有两个办法:

  1. 肉眼观察,直到不再出水平滚动条为止 ??
  2. 根据gutter的原理来推算,虽然从原理上操作看似治本,但效率还不如肉眼尝试来得快 ??

实际上 el-row 用的是flex布局,它的gutter效果,是通过以下css组合来实现的:

  • el-row 组件自身使用相对定位(有无gutter均是如此)
  • el-row 组件自身左右的margin值均为: - gutter/2 px
  • 组件内的所有元素左右padding值均为: gutter/2 px

如下图所示:

el-row的gutter原理解析图

由此可推断, 当父元素单侧的 padding >= gutter/2 时,就不会出现滚动条了

原文链接:https://www.cnblogs.com/guzb/p/vue3-vite-elment-plus-admin-common-questions.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号