欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Vue3+Vite+ElementPlus管理系統(tǒng)常見(jiàn)問(wèn)題

 更新時(shí)間:2023年12月07日 09:11:39   作者:顧志兵  
本文記錄了使用Vue3+Vite+ElementPlus從0開(kāi)始搭建一個(gè)前端工程會(huì)面臨的常見(jiàn)問(wèn)題,沒(méi)有技術(shù)深度,但全都是解決實(shí)際問(wèn)題的干貨,可以當(dāng)作是問(wèn)題手冊(cè)以備后用,感興趣的朋友參考下

本文本記錄了使用 Vue3+Vite+ElementPlus 從0開(kāi)始搭建一個(gè)前端工程會(huì)面臨的常見(jiàn)問(wèn)題,沒(méi)有技術(shù)深度,但全都是解決實(shí)際問(wèn)題的干貨,可以當(dāng)作是問(wèn)題手冊(cè)以備后用。本人日常工作偏后端開(kāi)發(fā),因此,文中的一些前端術(shù)語(yǔ)描述可能不嚴(yán)謹(jǐn),敬請(qǐng)諒解。重點(diǎn)是:這里記錄的解決方案都是行之有效果的,拿來(lái)即可用 ??‍?? ??

1. 頁(yè)面整體布局

通常管理后臺(tái)有以下幾種經(jīng)典布局

布局一:純側(cè)面菜單

  ┌────────────────────────────────────────────────────────────────────────────────┐
  │ LOGO                                                             Avatar | Exit │
  ├─────────────────────┬──────────────────────────────────────────────────────────┤
  │ MenuA               │                                                          │
  ├─────────────────────┤                                                          │
  │    MenuItem1OfMenuA │                                                          │
  ├─────────────────────┤                                                          │
  │    MenuItem2OfMenuA │                                                          │
  ├─────────────────────┤                  Main Content Area                       │
  │ MenuB               │                                                          │
  ├─────────────────────┤                                                          │
  │                     │                                                          │
  │                     │                                                          │
  │                     │                                                          │
  └─────────────────────┴──────────────────────────────────────────────────────────┘

布局二:頂部菜單 + 側(cè)面二級(jí)菜單

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

布局三:頂部菜單 + 側(cè)面二級(jí)菜單 + 內(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                       │
  ├─────────────────────┤                                                              │
  │                     │                                                              │
  │                     │                                                              │
  │                     │                                                              │
  └─────────────────────┴──────────────────────────────────────────────────────────────┘

這個(gè)與 VUE 無(wú)關(guān),是純 HTML + CSS 基本功的問(wèn)題,實(shí)現(xiàn)方案有多種,下面是一種基于 flex 的精簡(jiǎn)參考方案:

Flex樣式實(shí)現(xiàn)后臺(tái)管理界面整體布局

<!DOCTYPE html>
<html lang="en" style="margin:0; padding:0">
<head>
 <title>Flex樣式實(shí)現(xiàn)后臺(tái)管理界面整體布局</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)題欄,特別說(shuō)明:固定高度的區(qū)域,本身的display不能為flex, 否則高度會(huì)隨內(nèi)容而變,可以再嵌套一個(gè)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>這里特意使用了一個(gè) div 來(lái)代表具體的業(yè)務(wù)頁(yè)面內(nèi)容,并將其高度設(shè)得很大,以使其出現(xiàn)垂直滾動(dòng)條效果 </p>
         </div>
	  </div>
	  <div style="background:aqua; height:60px">
		底部信息欄,(但多數(shù)管理系統(tǒng)都會(huì)取消這它,以留出更多可視區(qū)域給內(nèi)容展示)
	  </div>
	</div>
 </div>
</body>
</html>

對(duì)于主內(nèi)容區(qū)的「一菜單一TAB」模式,需要編寫JS代碼來(lái)完成,一般都是通過(guò) el-menu + el-tabs 的組合來(lái)實(shí)現(xiàn)的。監(jiān)聽(tīng) el-menu 組件的 @change 事件,根據(jù)所激活的菜單項(xiàng)名稱,動(dòng)態(tài)地在主內(nèi)容區(qū)添加TAB

2. 頁(yè)面刷新后,菜單激活頁(yè)面的高亮展示問(wèn)題

el-menu 組件有個(gè) router屬性,將其設(shè)置為 true 后,點(diǎn)擊菜單項(xiàng),vue 路由就會(huì)自動(dòng)變成 el-menu-item 組件中 index 屬性指向的內(nèi)容,并且該菜單項(xiàng)也會(huì)高亮顯示

如果點(diǎn)擊瀏覽器的刷新按鈕,el-menu 通常會(huì)不再高亮顯示當(dāng)前打開(kāi)的路由頁(yè)面。

當(dāng)然,如果 el-menu 指定了default-active屬性,則刷新頁(yè)面后,無(wú)論實(shí)際路由是什么,菜單欄都會(huì)高亮顯示default-active屬性對(duì)應(yīng)的菜單項(xiàng)。因?yàn)樗⑿马?yè)面后,el-menu 組件也重新初始化了,因此它總是高亮default-active指向的菜單項(xiàng)。如果通過(guò)代碼,將default-active的值改為刷新后的實(shí)際路由,則可解決此問(wèn)題。

需要特別注意的是:簡(jiǎn)單通過(guò)router.CurrentRoute.value的方式獲取的當(dāng)前路由,在一般情況下是ok的,但在刷新時(shí),獲取到的值要么為null,要么為/, 而不是url中實(shí)際的路由,需要通過(guò)監(jiān)聽(tīng)這個(gè)值的變化才能獲取到最真實(shí)的路由,示例代碼如下:

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 組件換行問(wèn)題

這通常是我們?cè)诮oel-input組件添加一個(gè)label時(shí),會(huì)看到的現(xiàn)象,就像下面這樣

     期望的界面:                                    實(shí)際的界面:
                ┌─────────────────┐             Company Name
   Company Name │                 │             ┌─────────────────┐
                └─────────────────┘             │                 │
                                                └─────────────────┘

不只是el-input組件,只要是表單輸入類組件,都會(huì)換行,有3種解決辦法

方法 1
<el-input><el-form-item>組件包裹起來(lái),如下所示:

<el-form-item label="公司名稱" style="width: 200px">
     <el-input v-model="companyName" placeholder="請(qǐng)輸入公司名稱" clearable />
</el-form-item>

方法 2
自己寫一個(gè)div, 設(shè)置樣式display:flex; fext-wrap:nowrap;, 然后將<el-input>放置該div內(nèi)即可

方法 3
<el-input>組件添加display:inlinedisplay:inline-block樣式,比如我們要實(shí)現(xiàn)下面這個(gè)效果

                    ┌─────────────────┐   ┌─────────────────┐ 
  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的上下不對(duì)稱, 在頁(yè)面上表現(xiàn)為視覺(jué)上的不對(duì)稱,需要手動(dòng)設(shè)置樣式,建議全局為 .el-from-item 類添加對(duì)稱的 padding

5. 登錄頁(yè)面+非登錄頁(yè)面+路由處理+App.vue的組合協(xié)調(diào)問(wèn)題

一套管理管理系統(tǒng),需具備以下基礎(chǔ)特性:

  • a. 首次訪問(wèn)系統(tǒng)根 url 時(shí),應(yīng)該顯示「登錄」頁(yè)面
  • b. 登錄成功后,應(yīng)該進(jìn)入管理系統(tǒng)的「主頁(yè)面」
  • c. 在管理系統(tǒng)的主頁(yè)面,做任何菜單切換,主頁(yè)面的主體結(jié)構(gòu)不變,只在內(nèi)容區(qū)展示菜單項(xiàng)對(duì)應(yīng)的業(yè)務(wù)內(nèi)容
    這里的主體結(jié)構(gòu)是指:標(biāo)題欄、菜單欄、底部信息欄(如果有的話)
  • d. 管理主頁(yè)面應(yīng)該提供「退出」入口,點(diǎn)擊入口時(shí),顯示「登錄」頁(yè)面
  • e. 在瀏覽器地址欄直接輸入一個(gè)「非登錄」類 url 后,如果用戶已經(jīng)登錄過(guò),且憑證沒(méi)有過(guò)期,則應(yīng)該直接顯示該 url 對(duì)應(yīng)的內(nèi)容,包括管理「主頁(yè)面」的主體部分 和 url 指向的實(shí)際內(nèi)容部分
  • f. 在瀏覽器地址欄直接輸入一個(gè)「非登錄」類 url 后,如果用戶未登錄,或登錄憑證已過(guò)期,則應(yīng)該跳轉(zhuǎn)到「登錄」頁(yè)面
  • g. 在瀏覽器地址欄直接輸入「登錄」頁(yè)面 的URL后,如果如果用戶已經(jīng)登錄過(guò),且憑證沒(méi)有過(guò)期,則應(yīng)該直接進(jìn)入管理「主頁(yè)面」并展示「管理首頁(yè)菜單」的內(nèi)容

這些基本特征看似很多,其實(shí)核心問(wèn)題就二個(gè):如何實(shí)現(xiàn)登錄頁(yè)面與非登錄頁(yè)面的單獨(dú)渲染,以及以匿名方式訪問(wèn)非登錄頁(yè)面時(shí),自動(dòng)跳轉(zhuǎn)到登錄頁(yè)面,下面分別說(shuō)明。

5.1 登錄頁(yè)面與非登錄頁(yè)面的獨(dú)立渲染

因?yàn)榉堑卿涰?yè)面,通常有固定的布局(如本文第1章節(jié)所述),布局中會(huì)有一個(gè)主內(nèi)容區(qū),大量的業(yè)務(wù)組件就在這個(gè)區(qū)域內(nèi)渲染。如果設(shè)計(jì)得不好,就會(huì)出現(xiàn)登錄組件也被嵌入到這個(gè)主內(nèi)容區(qū)的現(xiàn)象,使其成為非登錄頁(yè)面布局中的一個(gè)局部區(qū)塊了,就像下面這樣:

期望的界面:

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

實(shí)際的界面:

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

出現(xiàn)這個(gè)現(xiàn)象的原因是:Vue所有組件的統(tǒng)一入口是App.vue,其它組件都是在這個(gè)組件內(nèi)渲染的。如果我們將非登錄頁(yè)面的布局寫在App.vue里,就會(huì)出現(xiàn)上面的情況。

方案一:?jiǎn)我?<router-view/> 方式

這個(gè)方法是讓App.vue內(nèi)容只有一個(gè) <roter-view/> 組件,這樣最靈活,然后再配置路由,將登錄組件與非登錄組件分成兩組路由。示例代碼如下:

App.vue

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

LoginView.vue

<template>
    <div> <h2>這是登錄頁(yè)面</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: {
                // ② 允許匿名訪問(wèn),即不需要登錄 
                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)訪問(wèn) / 或 /home 或 /xxx-home 或 /yyy-home 時(shí),App.vue 中的 <router-view/> 會(huì)替換成 MainView 組件,而 MainView 組件實(shí)現(xiàn)了一個(gè)頁(yè)面主體布局,主內(nèi)容區(qū)(MainView.vue的代碼①處)內(nèi)部又是一個(gè) <router-view/>, 它的內(nèi)容由 / 后面的路由組件替換。/home 時(shí)由 HomeView 組件替換,/xxx-home 時(shí)由 XxxHomeView 組件替換。

當(dāng)訪問(wèn) /login 時(shí),App.vue 中的 <router-view/> 會(huì)替換成 LoginView 組件,與 MainView 組件毫無(wú)關(guān)系,此時(shí)不會(huì)加載 MainView 組件,因此頁(yè)面UI效果就不會(huì)出現(xiàn) MainView 中的布局了,至此便實(shí)現(xiàn)了登錄頁(yè)面與非登錄頁(yè)面獨(dú)立渲染的目的。

方案二:多個(gè) <router-view name="xxx"/> 方式

該方式利用路由的namen屬性指定渲染組件,同樣可以實(shí)現(xiàn)登錄頁(yè)面與非登錄頁(yè)面的獨(dú)立渲染。其原理是在 App.vue 上,將整個(gè)系統(tǒng)的布局劃分好,每一個(gè)區(qū)塊都有對(duì)應(yīng)一個(gè)命名路由。就像下面這樣

<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>

對(duì)非登錄頁(yè)面,將其歸屬到統(tǒng)一的一個(gè)根路由上,這個(gè)根路由擁有 header、sidebar、content 、footer 四個(gè)組件,這樣只要在是匹配非登錄頁(yè)面的路由,這四個(gè)組件就一定會(huì)為渲染。對(duì)于非登錄頁(yè)面的路由,只提供一個(gè)content組件,這樣 header、sidebar 和 footer 就都不會(huì)渲染了。比如下面這個(gè)路由

const router = createRouter({
    history: createWebHistory(import.meta.env.BASE_URL),
    routes: [
        {
            name: 'default',
            path: '/',
            components: {
                header: HeaderComponent,
                sidebar: SidebarComponent,
                content: ContentComponent,     // 非登錄頁(yè)面主內(nèi)容組件
                 ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
                footer: FooterComponent
            },
            redirect: { name: 'login' },
            children: [......]
        },
        {
            name: 'login',
            path: '/login',
            components: {
                // 將登錄組件命名為 content, 這樣其它的 <router-view> 就不會(huì)渲染
                // App.vue 將只渲染 <router-view name="content"></router-view>
                content: resolve => require(['../views/login/LoginView.vue'], resolve)
                 ̄ ̄ ̄ ̄ ̄
            },
            // ② 允許匿名訪問(wèn),即不需要登錄
            meta: {anonymousAccess: true}
        }
    ]
})

5.2 匿名訪問(wèn)非登錄頁(yè)面時(shí),跳轉(zhuǎn)到登錄頁(yè)面

利用路由跳轉(zhuǎn)期間的鉤子函數(shù)(官方的術(shù)語(yǔ)為導(dǎo)航守衛(wèi)),在跳轉(zhuǎn)前做如下判斷:

  • 目的頁(yè)面是否允許匿名訪問(wèn), 如果是則放行,這需要在路由上添加一個(gè)匿名訪問(wèn)標(biāo)志,見(jiàn)上述代碼的 ② 處
  • 如果不允許匿名訪問(wèn),則進(jìn)一步判斷當(dāng)前用戶是否已登錄,已登錄則放行,反之則將目的頁(yè)面改為登錄頁(yè)面

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

import router from './router'
// 全局路由監(jiān)聽(tīng)
router.beforeEach(function (to, from, next) {
       ̄ ̄ ̄ ̄ ̄ ̄ ̄
    // 無(wú)需登錄的頁(yè)面
    if (to.meta.anonymousAccess){
        next();
        return;
    } 
    // 判斷是否已登錄
    if (isLogin()) {
        // 可以在此處進(jìn)一步做頁(yè)面的權(quán)限檢查
        ....
        next();
    } else {
        next({path: '/login'});
    }
});
router.afterEach((to, from, next) => {
    window.scrollTo(0, 0);
});

6. 非開(kāi)發(fā)環(huán)境中CSS、圖片、JS等靜態(tài)資源訪問(wèn)404問(wèn)題

6.1 public 目錄下的靜態(tài)資源 <推薦>

這個(gè)目錄應(yīng)該放置那些幾乎不會(huì)改動(dòng)的靜態(tài)資源,代碼中應(yīng)該使用絕對(duì)路徑來(lái)引用它們。且 路徑不能以public開(kāi)頭,示例如下:

  <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)該放在這個(gè)目錄下,但對(duì)于圖片,只要不是用來(lái)制作獨(dú)立組件,建議還是放在/public目錄下。

當(dāng)然,這里要針對(duì)的就是圖片在assets目錄下的情況,代碼中應(yīng)該使用絕對(duì)路徑下引用它們。但該目錄下的文件,在開(kāi)發(fā)環(huán)境和非開(kāi)發(fā)環(huán)境下有些差異,比如:

  • src 目錄在非開(kāi)發(fā)環(huán)境中是沒(méi)有的,因此代碼中不能直接以 /src/assets 開(kāi)頭
  • assets 下的文件名,在編譯后會(huì)追加隨機(jī)hash碼,且沒(méi)有二級(jí)目錄 ①

在代碼中可以通過(guò) @ 來(lái)代表 src 目錄在具體運(yùn)行環(huán)境中的位置,至于文件名中追加的 hash 值則不用關(guān)心,打包構(gòu)建時(shí),會(huì)一并將代碼中的引用也改過(guò)來(lái)。簡(jiǎn)而言之,像下面示例中這樣書寫就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目錄路徑問(wèn)題:

SRC 目錄的路徑,是可以通過(guò)代碼解析出來(lái)的,但需要好幾個(gè)方便嵌套調(diào)用才行,代碼就變得很長(zhǎng)了,因此才引入了 @ 這個(gè)特殊的路徑別名,以方便在vue文件中使用。這個(gè)別名是在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))
       ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
      // 下面這種寫法也可以,而且更簡(jiǎn)潔
      // '@': resolve(__dirname, "src")
    }
  },
  ......
}

6.3 圖片的動(dòng)態(tài)路徑

這是一個(gè)經(jīng)典問(wèn)題,也需要區(qū)分圖片是位于public目錄下,還是assets目錄下。二者的處理方式差異巨大,為此,特意創(chuàng)建了一個(gè)工程來(lái)演示不同目錄下,動(dòng)態(tài)路徑圖片的處理效果,見(jiàn)下圖:

請(qǐng)點(diǎn)擊 這里 下載該演示效果的工程源碼 ⑴

對(duì) public 目錄下的圖片做動(dòng)態(tài)路徑指定<推薦>

由于public目錄下的所有文件都會(huì)原樣保留,因此,動(dòng)態(tài)路路徑只需要保證最后生成的路徑串以 / 開(kāi)頭就可以了。因此強(qiáng)烈建議,當(dāng)需要在運(yùn)行期間動(dòng)態(tài)指定本地的圖片地址時(shí),把這些圖片都放置在 public 目錄下吧。

assets 目錄下的圖片動(dòng)態(tài)路徑處理

首先說(shuō)下結(jié)論,要對(duì)此目錄下的圖片在運(yùn)行期做動(dòng)態(tài)引用,非常麻煩。核心原因還是上面①處提到的對(duì)assets目錄的處理。或許有個(gè)疑問(wèn),Vite 或 Webpack 打包構(gòu)建時(shí),為什么要這樣做。 因?yàn)?Web 的基礎(chǔ)就是 HTML + CSS + JS,盡管JS代碼運(yùn)行在客戶端瀏覽器上,但業(yè)務(wù)數(shù)據(jù)和圖片、視頻等資源都在遠(yuǎn)程服務(wù)器上,前端工程源碼目錄結(jié)構(gòu)一定與最終部署的目錄結(jié)構(gòu)是不一樣的。前端在之前的非工程化時(shí)期,是沒(méi)有編譯這一階段的,源碼目錄結(jié)構(gòu),就是最終部署的結(jié)構(gòu)。

Vite 打包后的目錄中,除了 index.html 文件和 public 目錄下的文件外,其它所有文件都被編譯構(gòu)建到了 assets 目錄,如下所示

dist
 ├─ favicon.ico        # 來(lái)自public目錄,原樣保留
 ├─ img/               # 來(lái)自public目錄,原樣保留
 ├─ css/               # 來(lái)自public目錄,原樣保留
 ├─ assets/            # 來(lái)自src/asset目錄和src/views目錄,內(nèi)容經(jīng)過(guò)編譯,路徑剪裁至assets目錄,文件名追加hash值
 └─ index.html         # 來(lái)自源碼工程的根目錄,原樣保留

此目錄下動(dòng)態(tài)圖片解決方案的核心問(wèn)題是:必須讓構(gòu)建過(guò)程對(duì)涉及的圖片文件進(jìn)行編譯。 編譯過(guò)程的主要特征為:

  • 只對(duì)代碼中用到了的圖片進(jìn)行編譯
  • 保證編譯后新的文件名能與代碼中原來(lái)的引用關(guān)聯(lián)上

可以看出,由于編譯后圖片名稱變了,而在源代碼中引用圖片時(shí),名稱還是編譯前的名字,因此,編譯過(guò)程必須要對(duì)代碼中的文件名進(jìn)行修改??梢韵胂螅绻创a中的文件名不是字面量(如:'avator/anaonymous.jpg'), 而僅僅是一個(gè)變量的話,編譯器是極難推斷出需要對(duì)哪些圖片資源進(jìn)行編譯的。事實(shí)上也是如此,如果文件名就是一個(gè)普通變量,則會(huì)原樣保留代碼。打包后,源碼引用的圖片不會(huì)被編譯到目標(biāo)目錄中,也就沒(méi)有這個(gè)圖片了。

花費(fèi)一翻功夫后,最終得到兩種解決方案

方案一: 利用 URL 函數(shù)手動(dòng)提前解析所有圖片路徑 <推薦>

<template>
    <div>
         <img :src="dynamicImgRef" style="max-height: 300px"/>
         <br/>
         <input v-model="dynamicImageName" /> &nbsp; &nbsp;
         <button @click = "showInputImage">顯示輸入的圖片</button>
    </div>
</tempalte>
<script setup>
import {ref} from 'vue'
// ② 需要在運(yùn)行期動(dòng)態(tài)指定路徑的所有圖片
const assetsDynamicImages = {
     // 1. 一定要用相對(duì)路徑
     // 2. 假定本代碼文件所在目錄與assets目錄是平級(jí)關(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ù)輸入的圖片名稱顯示對(duì)應(yīng)圖片」的場(chǎng)景,它的特點(diǎn)為:

適用場(chǎng)景:于需要根據(jù)條件來(lái)獲取相應(yīng)圖片路徑的情況。

  • 適用場(chǎng)景:于需要根據(jù)條件來(lái)獲取相應(yīng)圖片路徑的情況。

  • 缺陷:需要在代碼中,以字符串明文方式將所有的圖片都寫進(jìn)去,即上面②處

因?yàn)橹挥羞@樣,編譯器才能識(shí)別出是哪些圖片需要處理。如果把這個(gè)圖片的相對(duì)路徑都寫到另外一個(gè)數(shù)組,然后以遍歷的方式來(lái)生成運(yùn)行時(shí)路徑都是不行的,構(gòu)建過(guò)程依然不會(huì)對(duì)圖片做編譯處理。

本demo對(duì)應(yīng)的演示效果為 ⑴ 處動(dòng)圖的「assets目錄·方式一」部分,但動(dòng)態(tài)圖工程的源碼與該demo代碼并不完全相同。

方式二:通過(guò) 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'
// ③ 提前加載指定目錄的所有圖片,可在編譯期間提前生成好圖片路徑,這里返回的是一個(gè)圖片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)顯示一組圖片」的場(chǎng)景,它的特點(diǎn)為:

  • 可以遍歷一組圖片,而無(wú)需要提前知道圖片名稱
  • 這組圖片路徑雖然也是在編譯階段提前加載的,但不用在代碼中以一圖一碼的方式硬編碼加載(就像上面的方式一)
  • 很難通過(guò)圖片名稱的方式單獨(dú)提取其中的一張圖片路徑
    因?yàn)榫幾g后圖片名稱加了Hash后綴,同時(shí)圖片的目錄層級(jí)也沒(méi)有了。如果工程中不同目錄下,存在相同名稱的圖片,就無(wú)法在編譯后通過(guò)原始名稱來(lái)精準(zhǔn)提取圖片路徑

本demo對(duì)應(yīng)的演示效果為 ⑴ 處動(dòng)圖的「assets目錄·方式二」部分,但動(dòng)態(tài)圖工程的源碼與該demo代碼并不完全相同。

?? 關(guān)于URL函數(shù)

  • 示例中的這個(gè) URL 函數(shù)是HTML的客戶端JS運(yùn)行環(huán)境標(biāo)準(zhǔn)庫(kù)中的函數(shù),不是Node的 URL 模塊,Node的URL模塊只能用于服務(wù)器端,或前端的打包構(gòu)建工具。
  • vite文檔 中提到,如果URL函數(shù)的文件路徑是 es6 語(yǔ)言規(guī)范的模板字符串,編譯器也會(huì)支持對(duì)該模板字符串所指向的圖片路徑做編譯轉(zhuǎn)化,比如:
function getImageUrl(name) {
   return new URL(`./dir/${name}.png`, import.meta.url).href
                   ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
}

經(jīng)過(guò)實(shí)測(cè),大多數(shù)情況下,以上代碼都會(huì)在打包部署后得到404響應(yīng)。因?yàn)?,上述代碼如果要在部署后正確訪問(wèn)圖片,必須保證模板字符串(上述代碼的下劃線部分)在編譯階段是可解析執(zhí)行的,即它可以被解析成普通字符串,然后編譯器再對(duì)解析后的普通字符串所指向的圖片路徑,進(jìn)行轉(zhuǎn)化(追加hash + 去除中間路徑)。

上述代碼中,如果name這個(gè)變量指向了一個(gè)明確的字符串,則 `./dir/${name}.png` 這個(gè)模板字符串在編譯期間是可解析成普通字符串的,反之則不可以。由于多數(shù)情況下,動(dòng)態(tài)圖片的名稱不會(huì)是一個(gè)固定值,因此name變量或許在一開(kāi)始可以指向一個(gè)明確的串,但在運(yùn)行期一定會(huì)變化,而變化后所指向的圖片路徑,在編譯期是無(wú)法感知到的,這些圖片也就不會(huì)做轉(zhuǎn)化了

?? 關(guān)于 import.meta.glob 方法

import.metea.glob 方法是 vite 引入的,它支持將多個(gè)文件以 module 的方式加載,默認(rèn)是異步加載,也可以通過(guò)參數(shù)指定為同步加載

7. 非開(kāi)發(fā)環(huán)境中業(yè)務(wù)路徑404問(wèn)題

除了動(dòng)態(tài)圖片的404問(wèn)題,另一類更常見(jiàn)的是頁(yè)面路徑404問(wèn)題。由于是單頁(yè)應(yīng)用,所有的頁(yè)面都是在瀏覽器客戶端完成的,在訪問(wèn)都頁(yè)時(shí),所有的頁(yè)面信息其實(shí)就已經(jīng)加載完了。只需要在瀏覽器本地加載不同的vue頁(yè)面即可,這是通過(guò)變更本地路由地址來(lái)實(shí)現(xiàn)的。Vue提供了兩種路由模式,分別是 Hash 和 History:

  • Hash 路由

這是早期vue的默認(rèn)模式,該模式?jīng)]有404問(wèn)題,它在語(yǔ)義上它更符合單頁(yè)面應(yīng)用,比如:http://localhost:5173/#/userManage , 其中 # 表示定位到當(dāng)前頁(yè)面的某個(gè)位置。這種定位語(yǔ)義是 HTML 標(biāo)準(zhǔn),因此它天然就適合用作單頁(yè)面應(yīng)用。當(dāng)切換路由時(shí),只變更 # 號(hào)后面的值,然后 vue 的路由組件會(huì)根據(jù) # 后的內(nèi)容重新加載本地頁(yè)面??梢钥闯觯?yè)面變更全過(guò)程中,客戶端均不會(huì)請(qǐng)求服務(wù)器,因此不會(huì)出現(xiàn)404問(wèn)題。

  • History 路由

會(huì)出現(xiàn)404問(wèn)題的就是這種模式,由于Hash模式url中的 # 明顯暴露了應(yīng)用的技術(shù)細(xì)節(jié),且看上去不像是一個(gè)網(wǎng)站。vue 路由便引入了history 模式。該模式最大的特點(diǎn)是url的內(nèi)容看上去與正常的網(wǎng)站沒(méi)有區(qū)別,變更路由時(shí),也會(huì)向服務(wù)器發(fā)請(qǐng)求,即:無(wú)論是在視覺(jué)上還是行為上,整個(gè)路由切換(頁(yè)面變更)過(guò)程都與普通網(wǎng)站訪問(wèn)相同。

但 vue 項(xiàng)目終究是單頁(yè)面應(yīng)用,頁(yè)面的變更最終還在客戶端完成的。當(dāng)客戶端向服務(wù)器端請(qǐng)求 http://loalhost:5173/userManage 頁(yè)面時(shí),服務(wù)器端是沒(méi)有這個(gè)頁(yè)面的,它只有 index.html 和 assets 目錄下的image、css、js, 因此會(huì)返回404。如果服務(wù)器不返回404,而是再次返回到index.html的話,客戶端就可以根據(jù)請(qǐng)求的 url,來(lái)變更單頁(yè)應(yīng)用的界面了。

結(jié)合 nginx 服務(wù)器的 try_files 指令和 命名location 指令正好可以實(shí)現(xiàn)上述方案, 示例代碼如下:

server {
   listen 31079;
   location / {
       root /www/vue-demo;          # vue工程打包后部署到服務(wù)器上的目錄
       index index.html index.htm;
       # 凡是在服務(wù)器上找不到文件的 uri,都轉(zhuǎn)交給 @vue-router 這個(gè)命名Location來(lái)處理
       try_files $uri $uri/ @vue-router;
        ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
   }
   # 將所有請(qǐng)求路徑,都重寫到index.html,這樣就又回到了單頁(yè)面應(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. 請(qǐng)求被瀏覽器本地緩存的問(wèn)題

瀏覽器默認(rèn)會(huì)在客戶端電腦上緩存 GET 請(qǐng)求方式獲得的 http 響應(yīng)內(nèi)容,當(dāng)再次請(qǐng)求時(shí),會(huì)直接從緩存中讀取,不再向后端服務(wù)器發(fā)送請(qǐng)求了。這是屬于早期 HTML 協(xié)議的約定。解決辦法為,每次請(qǐng)求時(shí),在 url 后拼接一段隨機(jī)數(shù),使得每次 GET 請(qǐng)求的地址都不一樣,瀏覽器的緩存里也就沒(méi)有當(dāng)前這個(gè)URL的內(nèi)容了,便會(huì)向后端服務(wù)器發(fā)送請(qǐng)求,同時(shí)又不影響正常業(yè)務(wù)參數(shù)的傳遞。

比如,我們約定這個(gè)隨機(jī)數(shù)的參數(shù)為 rid, 即 RequestIdentifier 的意思,可以像下面這樣拼接 url 串

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

9. 將 el-pagination 分頁(yè)組件的語(yǔ)言由默認(rèn)的英文改為中文

1. 在main.js文件中,引入element-plus/es/locale/lang/zh-cn 這個(gè)本地化組件

2. 在 app 應(yīng)用 ElementPlus 組件時(shí),指定 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 這個(gè)中文 locale 的路徑問(wèn)題:

  • 進(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版本不同且不兼容的問(wèn)題

最好的辦法是安裝 nvm(Node Version Manager) 來(lái)實(shí)現(xiàn)同一電腦上同時(shí)安裝和使用多個(gè) node 的目的,windows 系統(tǒng)上請(qǐng)安裝 nvm-windows

需要注意的是,如果在安裝 nvm 時(shí),你的系統(tǒng)已經(jīng)安裝了node, 則需要將其卸載,并可可能清除干凈,其它則按照官網(wǎng)文檔安裝即可。下面列出最常用的幾個(gè)命令:

命令功能
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的。后來(lái)我把 Node 卸載了,重新安裝了 nvm-windows, 然后以 nvm 的方式安裝了 node,再使用 npm 安裝它工具包,便出現(xiàn)了安裝過(guò)程阻塞在 reify 這個(gè)階段,有時(shí)候2分鐘后完成安裝,有時(shí)候就一直阻塞在哪里,直到超時(shí)。

我的解決辦法是將NPM的非官方鏡像源(我的是淘寶),還原為官方鏡像。

D:\SourceCode\cnblos > npm get registry
https://registry.npmmirror.com                                            # 之前是淘寶鏡像源
D:\SourceCode\cnblos > npm set registry https://registry.npmjs.org/       # 還原為官方鏡像源

12. Vite 命令啟動(dòng)項(xiàng)目成功,但localhost訪問(wèn)時(shí)返回404

我的情況是這樣的,通過(guò)命令 npm run serve 啟動(dòng)項(xiàng)目后,可以正常訪問(wèn)。退出后再通過(guò)命令npx vite 啟動(dòng)項(xiàng)目成功,輸出內(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

然后在瀏覽器里訪問(wèn) http://localhost:5173/ 返回404狀態(tài)碼,再次使用 npx vite --debug 方式啟動(dòng),刷新頁(yè)面后,可以看到控制臺(tái)有「路徑 / 到 /index.html」的redirect內(nèi)容輸出,但頁(yè)面狀態(tài)依然是404。

最終發(fā)現(xiàn),該工程在創(chuàng)建時(shí),使用的命令是 vue create xxxx,改用 npm init vite 創(chuàng)建項(xiàng)目后,再以 npx vite 啟動(dòng)便可正常訪問(wèn)了。

事實(shí)上,更正統(tǒng)的vite項(xiàng)目創(chuàng)建命令是 npm create vite@latest 工程名 -- --template vue, 在 vite官網(wǎng) 上有創(chuàng)建 vite 工程的詳細(xì)說(shuō)明,是我自己將它與 vue 二者的關(guān)系搞混了。經(jīng)過(guò)對(duì)比,可以看到兩種方式創(chuàng)建的工程,其 vite.config.js 文件內(nèi)容是差異的。

?? 關(guān)于vite server更常見(jiàn)的404問(wèn)題

另一種常見(jiàn)的404問(wèn)題,是非本機(jī)訪問(wèn)時(shí)(局域網(wǎng)的其它電腦訪問(wèn)),會(huì)報(bào)無(wú)法建立連接的錯(cuò)誤。原因是 vite 默認(rèn)只監(jiān)聽(tīng)了 localhost 這個(gè)主機(jī)名。

最高效簡(jiǎn)單的辦法是啟動(dòng)命令加上 --host 選項(xiàng),如:npx vite --host [本機(jī)在局域網(wǎng)的IP地址],方括號(hào)的內(nèi)容為可選。多數(shù)情況下,前端項(xiàng)目都只在本機(jī)自己調(diào)度,偶爾才需要他人來(lái)訪問(wèn),因此,這個(gè)辦法足夠了。

如果不想每次都在命令上加 --host 選項(xiàng),可直接在 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)聽(tīng)的IP地址
        port: 5173,            // 監(jiān)聽(tīng)的端口
        open: true             // 啟動(dòng)后是否打開(kāi)瀏覽器訪問(wèn)
    }
})

詳細(xì)配置可去 vite官網(wǎng)配置文檔 查閱

?? 工程名不要帶有空格

如果vite創(chuàng)建的工程名帶有空格,在本機(jī)開(kāi)發(fā)調(diào)試階段,可能會(huì)遭遇用 localhost 訪問(wèn)也返回404的情況。2022年時(shí)已經(jīng)有老外在 GitHub上提出這個(gè) bug,至少到當(dāng)前(2023-11)為止,該bug依然未修復(fù)。但經(jīng)過(guò)嘗試,發(fā)現(xiàn)通過(guò)腳手架命令無(wú)法創(chuàng)建名稱帶有空格的工程,估計(jì)這個(gè)老外是手動(dòng)創(chuàng)建的工程結(jié)構(gòu)。

13. el-row 組件的 gutter 屬性導(dǎo)致出現(xiàn)水平滾動(dòng)條

解決方案:給 el-row 的父組件設(shè)置一個(gè)合適的左右 padding 值,比如:padding:0 12px;

OK,問(wèn)題來(lái)了,如何知道這個(gè)合適的 padding 值是多少?有兩個(gè)辦法:

  • 肉眼觀察,直到不再出水平滾動(dòng)條為止 ??
  • 根據(jù)gutter的原理來(lái)推算,雖然從原理上操作看似治本,但效率還不如肉眼嘗試來(lái)得快 ??

實(shí)際上 el-row 用的是flex布局,它的gutter效果,是通過(guò)以下css組合來(lái)實(shí)現(xiàn)的:

  • el-row 組件自身使用相對(duì)定位(有無(wú)gutter均是如此)
  • el-row 組件自身左右的margin值均為: - gutter/2 px
  • 組件內(nèi)的所有元素左右padding值均為: gutter/2 px

如下圖所示:

由此可推斷, 當(dāng)父元素單側(cè)的 padding >= gutter/2 時(shí),就不會(huì)出現(xiàn)滾動(dòng)條了

到此這篇關(guān)于Vue3+Vite+ElementPlus管理系統(tǒng)常見(jiàn)問(wèn)題的文章就介紹到這了,更多相關(guān)Vue3+Vite+ElementPlus管理系統(tǒng)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評(píng)論