Vue3+Vite+ElementPlus管理系統(tǒng)常見問題
本文本記錄了使用 Vue3+Vite+ElementPlus 從0開始搭建一個前端工程會面臨的常見問題,沒有技術(shù)深度,但全都是解決實際問題的干貨,可以當(dāng)作是問題手冊以備后用。本人日常工作偏后端開發(fā),因此,文中的一些前端術(shù)語描述可能不嚴(yán)謹(jǐn),敬請諒解。重點(diǎn)是:這里記錄的解決方案都是行之有效果的,拿來即可用 ???? ??
1. 頁面整體布局
通常管理后臺有以下幾種經(jīng)典布局
布局一:純側(cè)面菜單
┌────────────────────────────────────────────────────────────────────────────────┐ │ LOGO Avatar | Exit │ ├─────────────────────┬──────────────────────────────────────────────────────────┤ │ MenuA │ │ ├─────────────────────┤ │ │ MenuItem1OfMenuA │ │ ├─────────────────────┤ │ │ MenuItem2OfMenuA │ │ ├─────────────────────┤ Main Content Area │ │ MenuB │ │ ├─────────────────────┤ │ │ │ │ │ │ │ │ │ │ └─────────────────────┴──────────────────────────────────────────────────────────┘
布局二:頂部菜單 + 側(cè)面二級菜單
┌────────────────────────────────────────────────────────────────────────────────┐ │ LOGO ┌───────┐ ┌───────┐ Avatar | Exit │ │ │ MenuA │ │ MenuB │ │ ├─────────────────────┬──┘ └──┴───────┴────────────────────────────────────┤ │ SecondMenu-A-1 │ │ ├─────────────────────┤ │ │ ThirdMenuItem1-A-1 │ │ ├─────────────────────┤ │ │ ThirdMenuItem2-A-1 │ │ ├─────────────────────┤ Main Content Area │ │ SecondMenu-A-2 │ │ ├─────────────────────┤ │ │ │ │ │ │ │ │ │ │ └─────────────────────┴──────────────────────────────────────────────────────────┘???????
布局三:頂部菜單 + 側(cè)面二級菜單 + 內(nèi)容區(qū)一菜單一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 無關(guān),是純 HTML + CSS 基本功的問題,實現(xiàn)方案有多種,下面是一種基于 flex 的精簡參考方案:
Flex樣式實現(xiàn)后臺管理界面整體布局
<!DOCTYPE html> <html lang="en" style="margin:0; padding:0"> <head> <title>Flex樣式實現(xiàn)后臺管理界面整體布局</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"> 頂部標(biāo)題欄,特別說明:固定高度的區(qū)域,本身的display不能為flex, 否則高度會隨內(nèi)容而變,可以再嵌套一個flex布局的div </div> <!-- 非頂部區(qū)域,需要撐滿瀏覽器窗口的剩余部分,因此其 flex 值為 1 --> <div style="background:white; display:flex; flex:1; overflow-y:auto;"> <div style="background:black; width:230px; color:white; overflow-y:auto"> 左側(cè)菜單欄,固定寬度 </div> <div style="overflow-y:auto; flex:1; background-color: yellow; padding: 14px 16px;"> <div style="height=2000px;"> <h2>主內(nèi)容區(qū)</2> <p>這里特意使用了一個 div 來代表具體的業(yè)務(wù)頁面內(nèi)容,并將其高度設(shè)得很大,以使其出現(xiàn)垂直滾動條效果 </p> </div> </div> <div style="background:aqua; height:60px"> 底部信息欄,(但多數(shù)管理系統(tǒng)都會取消這它,以留出更多可視區(qū)域給內(nèi)容展示) </div> </div> </div> </body> </html>
對于主內(nèi)容區(qū)的「一菜單一TAB」模式,需要編寫JS代碼來完成,一般都是通過 el-menu + el-tabs 的組合來實現(xiàn)的。監(jiān)聽 el-menu 組件的 @change 事件,根據(jù)所激活的菜單項名稱,動態(tài)地在主內(nèi)容區(qū)添加TAB
2. 頁面刷新后,菜單激活頁面的高亮展示問題
el-menu 組件有個 router
屬性,將其設(shè)置為 true 后,點(diǎn)擊菜單項,vue 路由就會自動變成 el-menu-item 組件中 index 屬性指向的內(nèi)容,并且該菜單項也會高亮顯示
如果點(diǎn)擊瀏覽器的刷新按鈕,el-menu 通常會不再高亮顯示當(dāng)前打開的路由頁面。
當(dāng)然,如果 el-menu 指定了default-active
屬性,則刷新頁面后,無論實際路由是什么,菜單欄都會高亮顯示default-active
屬性對應(yīng)的菜單項。因為刷新頁面后,el-menu 組件也重新初始化了,因此它總是高亮default-active
指向的菜單項。如果通過代碼,將default-active
的值改為刷新后的實際路由,則可解決此問題。
需要特別注意的是:簡單通過router.CurrentRoute.value
的方式獲取的當(dāng)前路由,在一般情況下是ok的,但在刷新時,獲取到的值要么為null,要么為/
, 而不是url中實際的路由,需要通過監(jiān)聽這個值的變化才能獲取到最真實的路由,示例代碼如下:
import {watch} from 'vue' import {useRouter} from 'vue-router'; let router = useRouter() watch( () => router.currentRoute.value, (newRoute) => { // 這里已拿到最新的路由地址,可將其設(shè)置給 el-menu 的 default-active 屬性 console.log(newRoute.path) }, { immediate: true } )
3. el-input 組件換行問題
這通常是我們在給el-input
組件添加一個label時,會看到的現(xiàn)象,就像下面這樣
期望的界面: 實際的界面: ┌─────────────────┐ 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, 設(shè)置樣式display:flex; fext-wrap:nowrap;
, 然后將<el-input>
放置該div內(nèi)即可
方法 3
給<el-input>
組件添加display:inline
或display:inline-block
樣式,比如我們要實現(xiàn)下面這個效果
┌─────────────────┐ ┌─────────────────┐ 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 組件設(shè)置了padding-bottom屬性,但未設(shè)置padding-top
由于其padding的上下不對稱, 在頁面上表現(xiàn)為視覺上的不對稱,需要手動設(shè)置樣式,建議全局為 .el-from-item 類添加對稱的 padding
5. 登錄頁面+非登錄頁面+路由處理+App.vue的組合協(xié)調(diào)問題
一套管理管理系統(tǒng),需具備以下基礎(chǔ)特性:
- a. 首次訪問系統(tǒng)根 url 時,應(yīng)該顯示「登錄」頁面
- b. 登錄成功后,應(yīng)該進(jìn)入管理系統(tǒng)的「主頁面」
- c. 在管理系統(tǒng)的主頁面,做任何菜單切換,主頁面的主體結(jié)構(gòu)不變,只在內(nèi)容區(qū)展示菜單項對應(yīng)的業(yè)務(wù)內(nèi)容
這里的主體結(jié)構(gòu)是指:標(biāo)題欄、菜單欄、底部信息欄(如果有的話) - d. 管理主頁面應(yīng)該提供「退出」入口,點(diǎn)擊入口時,顯示「登錄」頁面
- e. 在瀏覽器地址欄直接輸入一個「非登錄」類 url 后,如果用戶已經(jīng)登錄過,且憑證沒有過期,則應(yīng)該直接顯示該 url 對應(yīng)的內(nèi)容,包括管理「主頁面」的主體部分 和 url 指向的實際內(nèi)容部分
- f. 在瀏覽器地址欄直接輸入一個「非登錄」類 url 后,如果用戶未登錄,或登錄憑證已過期,則應(yīng)該跳轉(zhuǎn)到「登錄」頁面
- g. 在瀏覽器地址欄直接輸入「登錄」頁面 的URL后,如果如果用戶已經(jīng)登錄過,且憑證沒有過期,則應(yīng)該直接進(jìn)入管理「主頁面」并展示「管理首頁菜單」的內(nèi)容
這些基本特征看似很多,其實核心問題就二個:如何實現(xiàn)登錄頁面與非登錄頁面的單獨(dú)渲染,以及以匿名方式訪問非登錄頁面時,自動跳轉(zhuǎn)到登錄頁面,下面分別說明。
5.1 登錄頁面與非登錄頁面的獨(dú)立渲染
因為非登錄頁面,通常有固定的布局(如本文第1章節(jié)所述),布局中會有一個主內(nèi)容區(qū),大量的業(yè)務(wù)組件就在這個區(qū)域內(nèi)渲染。如果設(shè)計得不好,就會出現(xiàn)登錄組件也被嵌入到這個主內(nèi)容區(qū)的現(xiàn)象,使其成為非登錄頁面布局中的一個局部區(qū)塊了,就像下面這樣:
期望的界面:
┌───────────────────────────────────────────────────────┐ │ │ │ ┌───────────────────┐ │ │ Username │ │ │ │ └───────────────────┘ │ │ │ │ ┌───────────────────┐ │ │ Password │ │ │ │ └───────────────────┘ │ │ │ │ ┌───────┐ │ │ │ Login │ │ │ └───────┘ │ └───────────────────────────────────────────────────────┘
實際的界面:
┌────────────────────────────────────────────────────────────┐ │ LOGO Avatar │ ├───────────────┬────────────────────────────────────────────┤ │ │ ┌────────────────┐ │ │ │ Username │ │ │ │ │ └────────────────┘ │ │ │ ┌────────────────┐ │ │ Side Menu │ Passwrod │ │ │ │ │ └────────────────┘ │ │ │ ┌───────┐ │ │ │ │ Login │ │ │ │ └───────┘ │ └───────────────┴────────────────────────────────────────────┘
出現(xiàn)這個現(xiàn)象的原因是:Vue所有組件的統(tǒng)一入口是App.vue,其它組件都是在這個組件內(nèi)渲染的。如果我們將非登錄頁面的布局寫在App.vue里,就會出現(xiàn)上面的情況。
方案一:單一 <router-view/> 方式
這個方法是讓App.vue內(nèi)容只有一個 <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> <!-- 中央?yún)^(qū)域 --> <div class="center-pane"> <!-- 中央左側(cè)菜單窗格--> <div class="center-aside-pane"> <center-aside-menu/> </div> <!-- ① 中央主內(nèi)容顯示窗格 --> <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') } ] }, ] })
根據(jù)以上路由,當(dāng)訪問 / 或 /home 或 /xxx-home 或 /yyy-home 時,App.vue 中的 <router-view/> 會替換成 MainView 組件,而 MainView 組件實現(xiàn)了一個頁面主體布局,主內(nèi)容區(qū)(MainView.vue的代碼①處)內(nèi)部又是一個 <router-view/>, 它的內(nèi)容由 / 后面的路由組件替換。/home 時由 HomeView 組件替換,/xxx-home 時由 XxxHomeView 組件替換。
當(dāng)訪問 /login 時,App.vue 中的 <router-view/> 會替換成 LoginView 組件,與 MainView 組件毫無關(guān)系,此時不會加載 MainView 組件,因此頁面UI效果就不會出現(xiàn) MainView 中的布局了,至此便實現(xiàn)了登錄頁面與非登錄頁面獨(dú)立渲染的目的。
方案二:多個 <router-view name="xxx"/> 方式
該方式利用路由的namen屬性指定渲染組件,同樣可以實現(xiàn)登錄頁面與非登錄頁面的獨(dú)立渲染。其原理是在 App.vue 上,將整個系統(tǒng)的布局劃分好,每一個區(qū)塊都有對應(yīng)一個命名路由。就像下面這樣
<template> <div id="app"> <router-view name="header"></router-view> <router-view name="sidebar"></router-view> <!-- 主內(nèi)容區(qū) --> <router-view name="content"></router-view>  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ <router-view name="footer"></router-view> </div> </template>
對非登錄頁面,將其歸屬到統(tǒng)一的一個根路由上,這個根路由擁有 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, // 非登錄頁面主內(nèi)容組件  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ 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 匿名訪問非登錄頁面時,跳轉(zhuǎn)到登錄頁面
利用路由跳轉(zhuǎn)期間的鉤子函數(shù)(官方的術(shù)語為導(dǎo)航守衛(wèi)),在跳轉(zhuǎn)前做如下判斷:
- 目的頁面是否允許匿名訪問, 如果是則放行,這需要在路由上添加一個匿名訪問標(biāo)志,見上述代碼的 ② 處
- 如果不允許匿名訪問,則進(jìn)一步判斷當(dāng)前用戶是否已登錄,已登錄則放行,反之則將目的頁面改為登錄頁面
示例代碼如下(位于main.js文件中):
import router from './router' // 全局路由監(jiān)聽 router.beforeEach(function (to, from, next) {  ̄ ̄ ̄ ̄ ̄ ̄ ̄ // 無需登錄的頁面 if (to.meta.anonymousAccess){ next(); return; } // 判斷是否已登錄 if (isLogin()) { // 可以在此處進(jìn)一步做頁面的權(quán)限檢查 .... next(); } else { next({path: '/login'}); } }); router.afterEach((to, from, next) => { window.scrollTo(0, 0); });
6. 非開發(fā)環(huán)境中CSS、圖片、JS等靜態(tài)資源訪問404問題
6.1 public 目錄下的靜態(tài)資源 <推薦>
這個目錄應(yīng)該放置那些幾乎不會改動的靜態(tài)資源,代碼中應(yīng)該使用絕對路徑來引用它們。且 路徑不能以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 目錄下的靜態(tài)資源
自己編寫的大多數(shù)公共css、js都應(yīng)該放在這個目錄下,但對于圖片,只要不是用來制作獨(dú)立組件,建議還是放在/public目錄下。
當(dāng)然,這里要針對的就是圖片在assets目錄下的情況,代碼中應(yīng)該使用絕對路徑下引用它們。但該目錄下的文件,在開發(fā)環(huán)境和非開發(fā)環(huán)境下有些差異,比如:
- src 目錄在非開發(fā)環(huán)境中是沒有的,因此代碼中不能直接以 /src/assets 開頭
- assets 下的文件名,在編譯后會追加隨機(jī)hash碼,且沒有二級目錄 ①
在代碼中可以通過 @ 來代表 src 目錄在具體運(yùn)行環(huán)境中的位置,至于文件名中追加的 hash 值則不用關(guān)心,打包構(gòu)建時,會一并將代碼中的引用也改過來。簡而言之,像下面示例中這樣書寫就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>
?? 關(guān)于SRC目錄路徑問題:
SRC 目錄的路徑,是可以通過代碼解析出來的,但需要好幾個方便嵌套調(diào)用才行,代碼就變得很長了,因此才引入了 @ 這個特殊的路徑別名,以方便在vue文件中使用。這個別名是在vite.js中聲明的,下面是相關(guān)片段:
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 圖片的動態(tài)路徑
這是一個經(jīng)典問題,也需要區(qū)分圖片是位于public目錄下,還是assets目錄下。二者的處理方式差異巨大,為此,特意創(chuàng)建了一個工程來演示不同目錄下,動態(tài)路徑圖片的處理效果,見下圖:
請點(diǎn)擊 這里 下載該演示效果的工程源碼 ⑴
對 public 目錄下的圖片做動態(tài)路徑指定<推薦>
由于public目錄下的所有文件都會原樣保留,因此,動態(tài)路路徑只需要保證最后生成的路徑串以 /
開頭就可以了。因此強(qiáng)烈建議,當(dāng)需要在運(yùn)行期間動態(tài)指定本地的圖片地址時,把這些圖片都放置在 public 目錄下吧。
assets 目錄下的圖片動態(tài)路徑處理
首先說下結(jié)論,要對此目錄下的圖片在運(yùn)行期做動態(tài)引用,非常麻煩。核心原因還是上面①處提到的對assets目錄的處理。或許有個疑問,Vite 或 Webpack 打包構(gòu)建時,為什么要這樣做。 因為 Web 的基礎(chǔ)就是 HTML + CSS + JS,盡管JS代碼運(yùn)行在客戶端瀏覽器上,但業(yè)務(wù)數(shù)據(jù)和圖片、視頻等資源都在遠(yuǎn)程服務(wù)器上,前端工程源碼目錄結(jié)構(gòu)一定與最終部署的目錄結(jié)構(gòu)是不一樣的。前端在之前的非工程化時期,是沒有編譯這一階段的,源碼目錄結(jié)構(gòu),就是最終部署的結(jié)構(gòu)。
Vite 打包后的目錄中,除了 index.html 文件和 public 目錄下的文件外,其它所有文件都被編譯構(gòu)建到了 assets 目錄,如下所示
dist ├─ favicon.ico # 來自public目錄,原樣保留 ├─ img/ # 來自public目錄,原樣保留 ├─ css/ # 來自public目錄,原樣保留 ├─ assets/ # 來自src/asset目錄和src/views目錄,內(nèi)容經(jīng)過編譯,路徑剪裁至assets目錄,文件名追加hash值 └─ index.html # 來自源碼工程的根目錄,原樣保留
此目錄下動態(tài)圖片解決方案的核心問題是:必須讓構(gòu)建過程對涉及的圖片文件進(jìn)行編譯。 編譯過程的主要特征為:
- 只對代碼中用到了的圖片進(jìn)行編譯
- 保證編譯后新的文件名能與代碼中原來的引用關(guān)聯(lián)上
可以看出,由于編譯后圖片名稱變了,而在源代碼中引用圖片時,名稱還是編譯前的名字,因此,編譯過程必須要對代碼中的文件名進(jìn)行修改??梢韵胂?,如果源碼中的文件名不是字面量(如:'avator/anaonymous.jpg'), 而僅僅是一個變量的話,編譯器是極難推斷出需要對哪些圖片資源進(jìn)行編譯的。事實上也是如此,如果文件名就是一個普通變量,則會原樣保留代碼。打包后,源碼引用的圖片不會被編譯到目標(biāo)目錄中,也就沒有這個圖片了。
花費(fèi)一翻功夫后,最終得到兩種解決方案
方案一: 利用 URL 函數(shù)手動提前解析所有圖片路徑 <推薦>
<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' // ② 需要在運(yùn)行期動態(tài)指定路徑的所有圖片 const assetsDynamicImages = { // 1. 一定要用相對路徑 // 2. 假定本代碼文件所在目錄與assets目錄是平級關(guān)系,否則需要調(diào)整 ../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]) // 點(diǎn)擊按鈕后,顯示輸入框中的圖片 const showInputImage = () => { dynamicImgRef.value = assetsDynamicImages[dynamicImageName] } </script>
上述 demo 演示的是「根據(jù)輸入的圖片名稱顯示對應(yīng)圖片」的場景,它的特點(diǎn)為:
適用場景:于需要根據(jù)條件來獲取相應(yīng)圖片路徑的情況。
適用場景:于需要根據(jù)條件來獲取相應(yīng)圖片路徑的情況。
缺陷:需要在代碼中,以字符串明文方式將所有的圖片都寫進(jìn)去,即上面②處
因為只有這樣,編譯器才能識別出是哪些圖片需要處理。如果把這個圖片的相對路徑都寫到另外一個數(shù)組,然后以遍歷的方式來生成運(yùn)行時路徑都是不行的,構(gòu)建過程依然不會對圖片做編譯處理。
本demo對應(yīng)的演示效果為 ⑴ 處動圖的「assets目錄·方式一」部分,但動態(tài)圖工程的源碼與該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數(shù)組 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 屬性就是編譯后的圖片路徑  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ }) // 默認(rèn)顯示第一張 let imageIndex = 0 const dynamicImgRef = ref(assetsDynamicImageUrls[imageIndex]) function displayNextImage() { imageIndex ++ if(imageIndex >= assetsDynamicImageUrls.length) { imageIndex = 0 } dynamicImgRef.value = assetsDynamicImageUrls[imageIndex] } </script>
上述 demo 演示的是「循環(huán)顯示一組圖片」的場景,它的特點(diǎn)為:
- 可以遍歷一組圖片,而無需要提前知道圖片名稱
- 這組圖片路徑雖然也是在編譯階段提前加載的,但不用在代碼中以一圖一碼的方式硬編碼加載(就像上面的方式一)
- 很難通過圖片名稱的方式單獨(dú)提取其中的一張圖片路徑
因為編譯后圖片名稱加了Hash后綴,同時圖片的目錄層級也沒有了。如果工程中不同目錄下,存在相同名稱的圖片,就無法在編譯后通過原始名稱來精準(zhǔn)提取圖片路徑
本demo對應(yīng)的演示效果為 ⑴ 處動圖的「assets目錄·方式二」部分,但動態(tài)圖工程的源碼與該demo代碼并不完全相同。
?? 關(guān)于URL函數(shù)
- 示例中的這個 URL 函數(shù)是HTML的客戶端JS運(yùn)行環(huán)境標(biāo)準(zhǔn)庫中的函數(shù),不是Node的 URL 模塊,Node的URL模塊只能用于服務(wù)器端,或前端的打包構(gòu)建工具。
- vite文檔 中提到,如果URL函數(shù)的文件路徑是 es6 語言規(guī)范的模板字符串,編譯器也會支持對該模板字符串所指向的圖片路徑做編譯轉(zhuǎn)化,比如:
function getImageUrl(name) { return new URL(`./dir/${name}.png`, import.meta.url).href  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ }
經(jīng)過實測,大多數(shù)情況下,以上代碼都會在打包部署后得到404響應(yīng)。因為,上述代碼如果要在部署后正確訪問圖片,必須保證模板字符串(上述代碼的下劃線部分)在編譯階段是可解析執(zhí)行的,即它可以被解析成普通字符串,然后編譯器再對解析后的普通字符串所指向的圖片路徑,進(jìn)行轉(zhuǎn)化(追加hash + 去除中間路徑)。
上述代碼中,如果name這個變量指向了一個明確的字符串,則 `./dir/${name}.png` 這個模板字符串在編譯期間是可解析成普通字符串的,反之則不可以。由于多數(shù)情況下,動態(tài)圖片的名稱不會是一個固定值,因此name變量或許在一開始可以指向一個明確的串,但在運(yùn)行期一定會變化,而變化后所指向的圖片路徑,在編譯期是無法感知到的,這些圖片也就不會做轉(zhuǎn)化了
?? 關(guān)于 import.meta.glob 方法
import.metea.glob 方法是 vite 引入的,它支持將多個文件以 module 的方式加載,默認(rèn)是異步加載,也可以通過參數(shù)指定為同步加載
7. 非開發(fā)環(huán)境中業(yè)務(wù)路徑404問題
除了動態(tài)圖片的404問題,另一類更常見的是頁面路徑404問題。由于是單頁應(yīng)用,所有的頁面都是在瀏覽器客戶端完成的,在訪問都頁時,所有的頁面信息其實就已經(jīng)加載完了。只需要在瀏覽器本地加載不同的vue頁面即可,這是通過變更本地路由地址來實現(xiàn)的。Vue提供了兩種路由模式,分別是 Hash 和 History:
- Hash 路由
這是早期vue的默認(rèn)模式,該模式?jīng)]有404問題,它在語義上它更符合單頁面應(yīng)用,比如:http://localhost:5173/#/userManage , 其中 # 表示定位到當(dāng)前頁面的某個位置。這種定位語義是 HTML 標(biāo)準(zhǔn),因此它天然就適合用作單頁面應(yīng)用。當(dāng)切換路由時,只變更 # 號后面的值,然后 vue 的路由組件會根據(jù) # 后的內(nèi)容重新加載本地頁面。可以看出,頁面變更全過程中,客戶端均不會請求服務(wù)器,因此不會出現(xiàn)404問題。
- History 路由
會出現(xiàn)404問題的就是這種模式,由于Hash模式url中的 # 明顯暴露了應(yīng)用的技術(shù)細(xì)節(jié),且看上去不像是一個網(wǎng)站。vue 路由便引入了history 模式。該模式最大的特點(diǎn)是url的內(nèi)容看上去與正常的網(wǎng)站沒有區(qū)別,變更路由時,也會向服務(wù)器發(fā)請求,即:無論是在視覺上還是行為上,整個路由切換(頁面變更)過程都與普通網(wǎng)站訪問相同。
但 vue 項目終究是單頁面應(yīng)用,頁面的變更最終還在客戶端完成的。當(dāng)客戶端向服務(wù)器端請求 http://loalhost:5173/userManage 頁面時,服務(wù)器端是沒有這個頁面的,它只有 index.html 和 assets 目錄下的image、css、js, 因此會返回404。如果服務(wù)器不返回404,而是再次返回到index.html的話,客戶端就可以根據(jù)請求的 url,來變更單頁應(yīng)用的界面了。
結(jié)合 nginx 服務(wù)器的 try_files 指令和 命名location 指令正好可以實現(xiàn)上述方案, 示例代碼如下:
server { listen 31079; location / { root /www/vue-demo; # vue工程打包后部署到服務(wù)器上的目錄 index index.html index.htm; # 凡是在服務(wù)器上找不到文件的 uri,都轉(zhuǎn)交給 @vue-router 這個命名Location來處理 try_files $uri $uri/ @vue-router;  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ } # 將所有請求路徑,都重寫到index.html,這樣就又回到了單頁面應(yīng)用上,但瀏覽器地址欄的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. 請求被瀏覽器本地緩存的問題
瀏覽器默認(rèn)會在客戶端電腦上緩存 GET 請求方式獲得的 http 響應(yīng)內(nèi)容,當(dāng)再次請求時,會直接從緩存中讀取,不再向后端服務(wù)器發(fā)送請求了。這是屬于早期 HTML 協(xié)議的約定。解決辦法為,每次請求時,在 url 后拼接一段隨機(jī)數(shù),使得每次 GET 請求的地址都不一樣,瀏覽器的緩存里也就沒有當(dāng)前這個URL的內(nèi)容了,便會向后端服務(wù)器發(fā)送請求,同時又不影響正常業(yè)務(wù)參數(shù)的傳遞。
比如,我們約定這個隨機(jī)數(shù)的參數(shù)為 rid, 即 RequestIdentifier 的意思,可以像下面這樣拼接 url 串
let url= 'http://localhost:3751/company/getByName?name=同仁堂&rid=' + Math.random() * 100000
9. 將 el-pagination 分頁組件的語言由默認(rèn)的英文改為中文
1. 在main.js文件中,引入element-plus/es/locale/lang/zh-cn
這個本地化組件
2. 在 app 應(yīng)用 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})  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
?? 關(guān)于 zh-cn 這個中文 locale 的路徑問題:
- 進(jìn)入當(dāng)前工程的 node_modules 目錄中
- 找到 elment-plus 目錄,并進(jìn)入
- 在該目錄中搜索 zh-cn
進(jìn)入當(dāng)前工程的 node_modules 目錄中找到 elment-plus 目錄,并進(jìn)入在該目錄中搜索 zh-cn
10. 不同工程的Node版本不同且不兼容的問題
最好的辦法是安裝 nvm(Node Version Manager) 來實現(xiàn)同一電腦上同時安裝和使用多個 node 的目的,windows 系統(tǒng)上請安裝 nvm-windows。
需要注意的是,如果在安裝 nvm 時,你的系統(tǒng)已經(jīng)安裝了node, 則需要將其卸載,并可可能清除干凈,其它則按照官網(wǎng)文檔安裝即可。下面列出最常用的幾個命令:
命令 | 功能 |
---|---|
nvm -v | 查看 nvm 的版本 |
nvm ls | 查看已安裝的 node 版本和當(dāng)前正在使用的 node 版本 |
nvm ls available | 列出所有可安裝的 node 版本 |
nvm install <version> | 安裝指定的 node 版本 |
nvm use <version> | 在當(dāng)前shell環(huán)境,使用指定的 node 版本,該版本必須先安裝 |
這里講述的方案僅適用于我的的環(huán)境,不能保證其它環(huán)境也能用同樣的方式解決。
我之前是直接從 官網(wǎng) 安裝的nodejs, 然后直接使用了配套的 npm 命令安裝其它依賴包,這些操作就是OK的。后來我把 Node 卸載了,重新安裝了 nvm-windows, 然后以 nvm 的方式安裝了 node,再使用 npm 安裝它工具包,便出現(xiàn)了安裝過程阻塞在 reify 這個階段,有時候2分鐘后完成安裝,有時候就一直阻塞在哪里,直到超時。
我的解決辦法是將NPM的非官方鏡像源(我的是淘寶),還原為官方鏡像。
D:\SourceCode\cnblos > npm get registry https://registry.npmmirror.com # 之前是淘寶鏡像源 D:\SourceCode\cnblos > npm set registry https://registry.npmjs.org/ # 還原為官方鏡像源
12. Vite 命令啟動項目成功,但localhost訪問時返回404
我的情況是這樣的,通過命令 npm run serve
啟動項目后,可以正常訪問。退出后再通過命令npx vite
啟動項目成功,輸出內(nèi)容如下:
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狀態(tài)碼,再次使用 npx vite --debug
方式啟動,刷新頁面后,可以看到控制臺有「路徑 / 到 /index.html」的redirect內(nèi)容輸出,但頁面狀態(tài)依然是404。
最終發(fā)現(xiàn),該工程在創(chuàng)建時,使用的命令是 vue create xxxx
,改用 npm init vite
創(chuàng)建項目后,再以 npx vite
啟動便可正常訪問了。
事實上,更正統(tǒng)的vite項目創(chuàng)建命令是 npm create vite@latest 工程名 -- --template vue
, 在 vite官網(wǎng) 上有創(chuàng)建 vite 工程的詳細(xì)說明,是我自己將它與 vue 二者的關(guān)系搞混了。經(jīng)過對比,可以看到兩種方式創(chuàng)建的工程,其 vite.config.js 文件內(nèi)容是差異的。
?? 關(guān)于vite server更常見的404問題
另一種常見的404問題,是非本機(jī)訪問時(局域網(wǎng)的其它電腦訪問),會報無法建立連接的錯誤。原因是 vite 默認(rèn)只監(jiān)聽了 localhost 這個主機(jī)名。
最高效簡單的辦法是啟動命令加上 --host 選項,如:npx vite --host [本機(jī)在局域網(wǎng)的IP地址]
,方括號的內(nèi)容為可選。多數(shù)情況下,前端項目都只在本機(jī)自己調(diào)度,偶爾才需要他人來訪問,因此,這個辦法足夠了。
如果不想每次都在命令上加 --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', // 監(jiān)聽的IP地址 port: 5173, // 監(jiān)聽的端口 open: true // 啟動后是否打開瀏覽器訪問 } })
詳細(xì)配置可去 vite官網(wǎng)配置文檔 查閱
?? 工程名不要帶有空格
如果vite創(chuàng)建的工程名帶有空格,在本機(jī)開發(fā)調(diào)試階段,可能會遭遇用 localhost 訪問也返回404的情況。2022年時已經(jīng)有老外在 GitHub上提出這個 bug,至少到當(dāng)前(2023-11)為止,該bug依然未修復(fù)。但經(jīng)過嘗試,發(fā)現(xiàn)通過腳手架命令無法創(chuàng)建名稱帶有空格的工程,估計這個老外是手動創(chuàng)建的工程結(jié)構(gòu)。
13. el-row 組件的 gutter 屬性導(dǎo)致出現(xiàn)水平滾動條
解決方案:給 el-row 的父組件設(shè)置一個合適的左右 padding 值,比如:padding:0 12px;
OK,問題來了,如何知道這個合適的 padding 值是多少?有兩個辦法:
- 肉眼觀察,直到不再出水平滾動條為止 ??
- 根據(jù)gutter的原理來推算,雖然從原理上操作看似治本,但效率還不如肉眼嘗試來得快 ??
實際上 el-row 用的是flex布局,它的gutter效果,是通過以下css組合來實現(xiàn)的:
- el-row 組件自身使用相對定位(有無gutter均是如此)
- el-row 組件自身左右的margin值均為: - gutter/2 px
- 組件內(nèi)的所有元素左右padding值均為: gutter/2 px
如下圖所示:
由此可推斷, 當(dāng)父元素單側(cè)的 padding >= gutter/2 時,就不會出現(xiàn)滾動條了
到此這篇關(guān)于Vue3+Vite+ElementPlus管理系統(tǒng)常見問題的文章就介紹到這了,更多相關(guān)Vue3+Vite+ElementPlus管理系統(tǒng)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于vue開發(fā)的在線付費(fèi)課程應(yīng)用過程
這篇文章主要介紹了基于vue開發(fā)的在線付費(fèi)課程應(yīng)用過程,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2018-01-01vue實現(xiàn)帶小數(shù)點(diǎn)的星星評分
這篇文章主要為大家詳細(xì)介紹了vue實現(xiàn)帶小數(shù)點(diǎn)的星星評分,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-09-09Vue-input框checkbox強(qiáng)制刷新問題
這篇文章主要介紹了Vue-input框checkbox強(qiáng)制刷新問題,非常不錯,具有一定的參考借鑒價值,需要的朋友可以參考下2019-04-04vue實現(xiàn)el-select默認(rèn)選擇第一個或者第二個
這篇文章主要介紹了vue實現(xiàn)el-select默認(rèn)選擇第一個或者第二個,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-09-09