5

Vue3+Vite+ElementPlus管理系统常见问题

 9 months ago
source link: https://www.cnblogs.com/guzb/p/vue3-vite-elment-plus-admin-common-questions.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

1. 页面整体布局

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

  • 布局一:纯侧面菜单

    ┌────────────────────────────────────────────────────────────────────────────────┐ │ LOGO Avatar | Exit │ ├─────────────────────┬──────────────────────────────────────────────────────────┤ │ MenuA │ │ ├─────────────────────┤ │ │ MenuItem1OfMenuA │ │ ├─────────────────────┤ │ │ MenuItem2OfMenuA │ │ ├─────────────────────┤ Main Content Area │ │ MenuB │ │ ├─────────────────────┤ │ │ │ │ │ │ │ │ │ │ └─────────────────────┴──────────────────────────────────────────────────────────┘
  • 布局二:顶部菜单 + 侧面二级菜单

    ┌────────────────────────────────────────────────────────────────────────────────┐ │ LOGO ┌───────┐ ┌───────┐ Avatar | Exit │ │ │ MenuA │ │ MenuB │ │ ├─────────────────────┬──┘ └──┴───────┴────────────────────────────────────┤ │ SecondMenu-A-1 │ │ ├─────────────────────┤ │ │ ThirdMenuItem1-A-1 │ │ ├─────────────────────┤ │ │ ThirdMenuItem2-A-1 │ │ ├─────────────────────┤ Main Content Area │ │ SecondMenu-A-2 │ │ ├─────────────────────┤ │ │ │ │ │ │ │ │ │ │ └─────────────────────┴──────────────────────────────────────────────────────────┘
  • 布局三:顶部菜单 + 侧面二级菜单 + 内容区一菜单一TAB

    ┌────────────────────────────────────────────────────────────────────────────────────┐ │ LOGO ┌───────┐ ┌───────┐ Avatar | Exit │ │ │ MenuA │ │ MenuB │ │ ├─────────────────────┬───┘ └──┴───────┴───────────────────────────────────────┤ │ SecondMenu-A-1 │ ┌────────────────────────┐ │ ├─────────────────────┤ │ ThirdMenuItem2-A-1 x │ │ │ ThirdMenuItem1-A-1 ├─┘ └───────────────────────────────────┤ ├─────────────────────┤ │ │ ThirdMenuItem2-A-1 │ │ ├─────────────────────┤ │ │ SecondMenu-A-2 │ Main Content Area │ ├─────────────────────┤ │ │ │ │ │ │ │ │ │ │ └─────────────────────┴──────────────────────────────────────────────────────────────┘

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

Flex样式实现后台管理界面整体布局(点击查看)

<!DOCTYPE html><html lang="en" style="margin:0; padding:0"><head> <title>Flex样式实现后台管理界面整体布局</title></head><body style="margin:0; padding:0"> <div style="display:flex; flex-direction: column; height:100vh; width: 100vw;"> <div style="background-color:red; height: 60px"> 顶部标题栏,特别说明:固定高度的区域,本身的display不能为flex, 否则高度会随内容而变,可以再嵌套一个flex布局的div </div> <!-- 非顶部区域,需要撑满浏览器窗口的剩余部分,因此其 flex 值为 1 --> <div style="background:white; display:flex; flex:1; overflow-y:auto;"> <div style="background:black; width:230px; color:white; overflow-y:auto"> 左侧菜单栏,固定宽度 </div> <div style="overflow-y:auto; flex:1; background-color: yellow; padding: 14px 16px;"> <div style="height=2000px;"> <h2>主内容区</2> <p>这里特意使用了一个 div 来代表具体的业务页面内容,并将其高度设得很大,以使其出现垂直滚动条效果 </p> </div> </div> <div style="background:aqua; height:60px"> 底部信息栏,(但多数管理系统都会取消这它,以留出更多可视区域给内容展示) </div> </div> </div></body></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中实际的路由,需要通过监听这个值的变化才能获取到最真实的路由,示例代码如下:

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

3. el-input 组件换行问题

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

期望的界面: 实际的界面: ┌─────────────────┐ Company Name Company Name │ │ ┌─────────────────┐ └─────────────────┘ │ │ └─────────────────┘

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

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

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

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

    ┌─────────────────┐ ┌─────────────────┐ Student Age Range │ │ ~ │ │ └─────────────────┘ └─────────────────┘

    可以下面这样写

    <el-form-item label="Student Age Range"> <el-input v-model="minAge" placeholder="最小值" clearable style="display:inline-block;" /> <p style="display:inline-block; margin: 0 10px;"> ~ </p> <el-input v-model="maxAge" placeholder="最大值" clearable style="display:inline-block;"/></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章节所述),布局中会有一个主内容区,大量的业务组件就在这个区域内渲染。如果设计得不好,就会出现登录组件也被嵌入到这个主内容区的现象,使其成为非登录页面布局中的一个局部区块了,就像下面这样:

期望的界面:

┌───────────────────────────────────────────────────────┐│ ││ ┌───────────────────┐ ││ Username │ │ ││ └───────────────────┘ ││ ││ ┌───────────────────┐ ││ Password │ │ ││ └───────────────────┘ ││ ││ ┌───────┐ ││ │ Login │ ││ └───────┘ │└───────────────────────────────────────────────────────┘

实际的界面:

┌────────────────────────────────────────────────────────────┐│ LOGO Avatar │├───────────────┬────────────────────────────────────────────┤│ │ ┌────────────────┐ ││ │ Username │ │ ││ │ └────────────────┘ ││ │ ┌────────────────┐ ││ Side Menu │ Passwrod │ │ ││ │ └────────────────┘ ││ │ ┌───────┐ ││ │ │ Login │ ││ │ └───────┘ │└───────────────┴────────────────────────────────────────────┘

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

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

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

App.vue

<template> <router-view/></template>

LoginView.vue

<template> <div> <h2>这是登录页面</h2> </div></template>

MainView.vue

<template> <div class="main-pane-container"> <!-- 顶部栏 --> <div class="header-pane"> <header-content></header-content> </div> <!-- 中央区域 --> <div class="center-pane"> <!-- 中央左侧菜单窗格--> <div class="center-aside-pane"> <center-aside-menu/> </div> <!-- ① 中央主内容显示窗格 --> <div class="center-content-pane"> <router-view/> </div> </div> </div></template> <script setup> import { RouterView } from 'vue-router' import HeaderContent from './components/HeaderContent.vue' import CenterAsideMenu from './components/CenterAsideMenu.vue';</script>

router.js

import { createRouter, createWebHistory } from 'vue-router'import HomeView from '../views/home/HomeView.vue'import LoginHomeView from '../views/login/LoginView.vue'import MainView from '../views/main/MainView.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/login', name: 'login', component: LoginHomeView, meta: { // ② 允许匿名访问,即不需要登录 anonymousAccess: true } }, { path: '/', name: 'main', component: MainView, redirect: {path: '/login'}, children: [ { path: '/home', name: 'home', component: HomeView }, { path: '/xxx', name: 'xxx-home', component: () => import('../views/xxx/XxxHomeView.vue') }, { path: '/yyy', name: 'yyy-home', component: () => import('../views/yyy/YyyHomeView.vue') } ] }, ]})

根据以上路由,当访问 / 或 /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 上,将整个系统的布局划分好,每一个区块都有对应一个命名路由。就像下面这样

<template> <div id="app"> <router-view name="header"></router-view> <router-view name="sidebar"></router-view> <!-- 主内容区 --> <router-view name="content"></router-view>  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ <router-view name="footer"></router-view> </div></template>

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

点击查看代码

const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { name: 'default', path: '/', components: { header: HeaderComponent, sidebar: SidebarComponent, content: ContentComponent, // 非登录页面主内容组件  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ footer: FooterComponent }, redirect: { name: 'login' }, children: [......] }, { name: 'login', path: '/login', components: { // 将登录组件命名为 content, 这样其它的 <router-view> 就不会渲染 // App.vue 将只渲染 <router-view name="content"></router-view> content: resolve => require(['../views/login/LoginView.vue'], resolve)  ̄ ̄ ̄ ̄ ̄ }, // ② 允许匿名访问,即不需要登录 meta: {anonymousAccess: true} } ]})

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

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

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

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

import router from './router' // 全局路由监听router.beforeEach(function (to, from, next) {  ̄ ̄ ̄ ̄ ̄ ̄ ̄ // 无需登录的页面 if (to.meta.anonymousAccess){ next(); return; } // 判断是否已登录 if (isLogin()) { // 可以在此处进一步做页面的权限检查 .... next(); } else { next({path: '/login'}); }}); router.afterEach((to, from, next) => { window.scrollTo(0, 0);});

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

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

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

<template> <div> <img alt="public目录图片示例" src="/images/photo/little-scallion.jpg" /> </div> </template> <style> .photo-gallery { background-image: url(/images/bg/jane-lotus.svg); } </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了。

<template> <div> <img alt="assets目录图片示例" src="@/assets/sports/badminton.jpg" /> </div></template> <style> .album-container { background-image: url(@/assets/bg/album/jane-lotus.svg); }</style>

🔔 关于SRC目录路径问题:

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

import {resolve} from 'path'import { fileURLToPath, URL } from 'node:url' export default defineConfig({ plugins: [vue()], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url))  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ // 下面这种写法也可以,而且更简洁 // '@': resolve(__dirname, "src") } }, ......}

6.3 图片的动态路径

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      点击查看代码

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

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

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

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

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

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

      点击查看代码

      <template> <div> <img :src="dynamicImgRef" style="max-height: 300px"/> <br/> <button @click = "displayNextImage">显示下一张图片</button> </div></tempalte> <script setup>import {ref} from 'vue' // ③ 提前加载指定目录的所有图片,可在编译期间提前生成好图片路径,这里返回的是一个图片module数组let assetsImageFiles = import.meta.glob([ '../sence/**/*.svg' '../assets/vechile/*.png', '../assets/sence/*.png', '../assets/season/*.png' ], {eager: true}); // ④ 从上一步加载的所有图片模块中,提取出图片路径const assetsDynamicImageUrls = []Object.values(assetsImageFiles).forEach(imgModule => { assetsDynamicImageUrls.push(imgModule.default) // default 属性就是编译后的图片路径  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄}) // 默认显示第一张let imageIndex = 0const dynamicImgRef = ref(assetsDynamicImageUrls[imageIndex]) function displayNextImage() { imageIndex ++ if(imageIndex >= assetsDynamicImageUrls.length) { imageIndex = 0 } dynamicImgRef.value = assetsDynamicImageUrls[imageIndex]}</script>

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

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

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

    📌 关于URL函数

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

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

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

      经过实测,大多数情况下,以上代码都会在打包部署后得到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 指令正好可以实现上述方案, 示例代码如下:

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

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

import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router' // Hash 模式路由const hashRouter = createRouter({ history: createWebHashHistory (import.meta.env.BASE_URL), routes: [ ...... ]}) // History 模式路由const historyRouter = createRouter({ history: createWebHistory (import.meta.env.BASE_URL), routes: [ ...... ]})

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

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

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

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步中引入的组件
import { createApp } from 'vue'import ElementPlus from 'element-plus'import zhCn from 'element-plus/es/locale/lang/zh-cn' ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄import App from './App.vue' const app = createApp(App)app.use(ElementPlus, {locale: zhCn})  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄

📌 关于 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的非官方镜像源(我的是淘宝),还原为官方镜像。

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

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

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

VITE v5.0.3 ready in 567 ms ➜ Local: http://localhost:5173/ ➜ Network: use --host to expose ➜ 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 中配置,如下:

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

详细配置可去 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 时,就不会出现滚动条了


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK