分享 15 個 Vue3 全家桶開發的避坑經驗

pingan8787發表於2022-04-09

最近入門 Vue3 並完成 3 個專案,遇到問題蠻多的,今天就花點時間整理一下,和大家分享 15 個比較常見的問題,基本都貼出對應文件地址,還請多看文件~
已經完成的 3 個專案基本都是使用 Vue3 (setup-script 模式)全家桶開發,因此主要分幾個方面總結:

  • Vue3
  • Vite
  • VueRouter
  • Pinia
  • ElementPlus

更多文章,歡迎關注我的主頁。

一、Vue3

1. Vue2.x 和 Vue3.x 生命週期方法的變化

文件地址:https://v3.cn.vuejs.org/guide/composition-api-lifecycle-hooks.html

Vue2.x 和 Vue3.x 生命週期方法的變化蠻大的,先看看:

2.x 生命週期3.x 生命週期執行時間說明
beforeCreatesetup元件建立前執行
createdsetup元件建立後執行
beforeMountonBeforeMount元件掛載到節點上之前執行
mountedonMounted元件掛載完成後執行
beforeUpdateonBeforeUpdate元件更新之前執行
updatedonUpdated元件更新完成之後執行
beforeDestroyonBeforeUnmount元件解除安裝之前執行
destroyedonUnmounted元件解除安裝完成後執行
errorCapturedonErrorCaptured當捕獲一個來自子孫元件的異常時啟用鉤子函式

目前 Vue3.x 依然支援 Vue2.x 的生命週期,但不建議混搭使用,前期可以先使用 2.x 的生命週期,後面儘量使用 3.x 的生命週期開發。

由於我使用都是 script-srtup模式,所以都是直接使用 Vue3.x 的生命週期函式:

// A.vue
<script setup lang="ts">
import { ref, onMounted } from "vue";
let count = ref<number>(0);

onMounted(() => {
  count.value = 1;
})
</script>

每個鉤子的執行時機點,也可以看看文件:
https://v3.cn.vuejs.org/guide/instance.html#生命週期圖示

2. script-setup 模式中父元件獲取子元件的資料

文件地址:https://v3.cn.vuejs.org/api/sfc-script-setup.html#defineexpose

這裡主要介紹父元件如何去獲取子元件內部定義的變數,關於父子元件通訊,可以看文件介紹比較詳細:
https://v3.cn.vuejs.org/guide/component-basics.html

我們可以使用全域性編譯器巨集defineExpose巨集,將子元件中需要暴露給父元件獲取的引數,通過 {key: vlaue}方式作為引數即可,父元件通過模版 ref 方式獲取子元件例項,就能獲取到對應值:

// 子元件
<script setup>
    let name = ref("pingan8787")
    defineExpose({ name }); // 顯式暴露的資料,父元件才可以獲取
</script>

// 父元件
<Chlid ref="child"></Chlid>
<script setup>
    let child = ref(null)
    child.value.name //獲取子元件中 name 的值為 pingan8787
</script>

注意

  • 全域性編譯器巨集只能在 script-setup 模式下使用;
  • script-setup 模式下,使用巨集時無需 import可以直接使用;
  • script-setup 模式一共提供了 4 個巨集,包括:definePropsdefineEmitsdefineExposewithDefaults

3. 為 props 提供預設值

definedProps 文件:https://v3.cn.vuejs.org/api/sfc-script-setup.html#defineprops-%E5%92%8C-defineemits
withDefaults 文件:https://v3.cn.vuejs.org/api/sfc-script-setup.html#%E4%BB%85%E9%99%90-typescript-%E7%9A%84%E5%8A%9F%E8%83%BD

前面介紹 script-setup 模式提供的 4 個全域性編譯器巨集,還沒有詳細介紹,這一節介紹 definePropswithDefaults
使用 defineProps巨集可以用來定義元件的入參,使用如下:

<script setup lang="ts">
let props = defineProps<{
    schema: AttrsValueObject;
    modelValue: any;
}>();
</script>

這裡只定義props屬性中的 schemamodelValue兩個屬性的型別, defineProps 的這種宣告的不足之處在於,它沒有提供設定 props 預設值的方式。
其實我們可以通過 withDefaults 這個巨集來實現:

<script setup lang="ts">
let props = withDefaults(
  defineProps<{
    schema: AttrsValueObject;
    modelValue: any;
  }>(),
  {
    schema: [],
    modelValue: ''
  }
);
</script>
withDefaults 輔助函式提供了對預設值的型別檢查,並確保返回的 props 的型別刪除了已宣告預設值的屬性的可選標誌。

4. 配置全域性自定義引數

文件地址:https://v3.cn.vuejs.org/guide/migration/global-api.html#vue-prototype-%E6%9B%BF%E6%8D%A2%E4%B8%BA-config-globalproperties

在 Vue2.x 中我們可以通過 Vue.prototype 新增全域性屬性 property。但是在 Vue3.x 中需要將 Vue.prototype 替換為 config.globalProperties 配置:

// Vue2.x
Vue.prototype.$api = axios;
Vue.prototype.$eventBus = eventBus;

// Vue3.x
const app = createApp({})
app.config.globalProperties.$api = axios;
app.config.globalProperties.$eventBus = eventBus;

使用時需要先通過 vue 提供的 getCurrentInstance方法獲取例項物件:

// A.vue

<script setup lang="ts">
import { ref, onMounted, getCurrentInstance } from "vue";

onMounted(() => {
  const instance = <any>getCurrentInstance();
  const { $api, $eventBus } = instance.appContext.config.globalProperties;
  // do something
})
</script>

其中 instance內容輸出如下:
image.png

5. v-model 變化

文件地址:https://v3.cn.vuejs.org/guide/migration/v-model.html

當我們在使用 v-model指令的時候,實際上 v-bindv-on 組合的簡寫,Vue2.x 和 Vue3.x 又存在差異。

  • Vue2.x
<ChildComponent v-model="pageTitle" />

<!-- 是以下的簡寫: -->
<ChildComponent :value="pageTitle" @input="pageTitle = $event" />

在子元件中,如果要對某一個屬性進行雙向資料繫結,只要通過 this.$emit('update:myPropName', newValue) 就能更新其 v-model繫結的值。

  • Vue3.x
<ChildComponent v-model="pageTitle" />

<!-- 是以下的簡寫: -->

<ChildComponent :modelValue="pageTitle" @update:modelValue="pageTitle = $event"/>

script-setup模式下就不能使用 this.$emit去派發更新事件,畢竟沒有 this,這時候需要使用前面有介紹到的 definePropsdefineEmits 兩個巨集來實現:

// 子元件 child.vue
// 文件:https://v3.cn.vuejs.org/api/sfc-script-setup.html#defineprops-%E5%92%8C-defineemits
<script setup lang="ts">
import { ref, onMounted, watch } from "vue";
const emit = defineEmits(['update:modelValue']); // 定義需要派發的事件名稱

let curValue = ref('');
let props = withDefaults(defineProps<{
    modelValue: string;
}>(), {
    modelValue: '',
})

onMounted(() => { 
  // 先將 v-model 傳入的 modelValue 儲存
  curValue.value = props.modelValue;
})

watch(curValue, (newVal, oldVal) => {
  // 當 curValue 變化,則通過 emit 派發更新
  emit('update:modelValue', newVal)
})

</script>

<template>
    <div></div>
</template>

<style lang="scss" scoped></style>

父元件使用的時候就很簡單:

// 父元件 father.vue

<script setup lang="ts">
import { ref, onMounted, watch } from "vue";
let curValue = ref('');
  
watch(curValue, (newVal, oldVal) => {
  console.log('[curValue 發生變化]', newVal)
})
</script>

<template>
    <Child v-model='curValue'></Child>
</template>

<style lang="scss" scoped></style>

6. 開發環境報錯不好排查

文件地址:https://v3.cn.vuejs.org/api/application-config.html#errorhandler

Vue3.x 對於一些開發過程中的異常,做了更友好的提示警告,比如下面這個提示:
image.png

這樣能夠更清楚的告知異常的出處,可以看出大概是 <ElInput 0=......這邊的問題,但還不夠清楚。
這時候就可以新增 Vue3.x 提供的全域性異常處理器,更清晰的輸出錯誤內容和呼叫棧資訊,程式碼如下

// main.ts
app.config.errorHandler = (err, vm, info) => {
    console.log('[全域性異常]', err, vm, info)
}

這時候就能看到輸出內容如下:
image.png

一下子就清楚很多。
當然,該配置項也可以用來整合錯誤追蹤服務 SentryBugsnag
推薦閱讀:Vue3 如何實現全域性異常處理?

7. 觀察 ref 的資料不直觀,不方便

當我們在控制檯輸出 ref宣告的變數時。

const count = ref<numer>(0);

console.log('[測試 ref]', count)

會看到控制檯輸出了一個 RefImpl物件:
image.png

看起來很不直觀。我們都知道,要獲取和修改 ref宣告的變數的值,需要通過 .value來獲取,所以你也可以:

console.log('[測試 ref]', count.value);

這裡還有另一種方式,就是在控制檯的設定皮膚中開啟 「Enable custom formatters」選項。

image.png

image.png

這時候你會發現,控制檯輸出的 ref的格式發生變化了:

image.png
更加清晰直觀了。

這個方法是我在《Vue.js 設計與實現》中發現的,但在文件也沒有找到相關介紹,如果有朋友發現了,歡迎告知~

二、Vite

1. Vite 動態匯入的使用問題

文件地址:https://cn.vitejs.dev/guide/features.html#glob-import

使用 webpack 的同學應該都知道,在 webpack 中可以通過 require.context動態匯入檔案:

// https://webpack.js.org/guides/dependency-management/
require.context('./test', false, /\.test\.js$/);

在 Vite 中,我們可以使用這兩個方法來動態匯入檔案:

  • import.meta.glob

該方法匹配到的檔案預設是懶載入,通過動態匯入實現,構建時會分離獨立的 chunk,是非同步匯入,返回的是 Promise,需要做非同步操作,使用方式如下:

const Components = import.meta.glob('../components/**/*.vue');

// 轉譯後:
const Components = {
  './components/a.vue': () => import('./components/a.vue'),
  './components/b.vue': () => import('./components/b.vue')
}
  • import.meta.globEager

該方法是直接匯入所有模組,並且是同步匯入,返回結果直接通過 for...in迴圈就可以操作,使用方式如下:

const Components = import.meta.globEager('../components/**/*.vue');

// 轉譯後:
import * as __glob__0_0 from './components/a.vue'
import * as __glob__0_1 from './components/b.vue'
const modules = {
  './components/a.vue': __glob__0_0,
  './components/b.vue': __glob__0_1
}

如果僅僅使用非同步匯入 Vue3 元件,也可以直接使用 Vue3 defineAsyncComponent API 來載入:

// https://v3.cn.vuejs.org/api/global-api.html#defineasynccomponent

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() =>
  import('./components/AsyncComponent.vue')
)

app.component('async-component', AsyncComp)

2. Vite 配置 alias 型別別名

文件地址:https://cn.vitejs.dev/config/#resolve-alias

當專案比較複雜的時候,經常需要配置 alias 路徑別名來簡化一些程式碼:

import Home from '@/views/Home.vue'

在 Vite 中配置也很簡單,只需要在 vite.config.tsresolve.alias中配置即可:

// vite.config.ts
export default defineConfig({
  base: './',
  resolve: {
    alias: {
      "@": path.join(__dirname, "./src")
    },
  }
  // 省略其他配置
})

如果使用的是 TypeScript 時,編輯器會提示路徑不存在的警告⚠️,這時候可以在 tsconfig.json中新增 compilerOptions.paths的配置:

{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"]
     }
  }
}

3. Vite 配置全域性 scss

文件地址:https://cn.vitejs.dev/config/#css-preprocessoroptions

當我們需要使用 scss 配置的主題變數(如 $primary)、mixin方法(如 @mixin lines)等時,如:

<script setup lang="ts">
</script>
<template>
  <div class="container"></div>
</template>

<style scoped lang="scss">
  .container{
    color: $primary;
    @include lines;
  }
</style>

我們可以將 scss 主題配置檔案,配置在 vite.config.tscss.preprocessorOptions.scss.additionalData中:

// vite.config.ts
export default defineConfig({
  base: './',
  css: {
    preprocessorOptions: {
      // 新增公共樣式
      scss: {
        additionalData: '@import "./src/style/style.scss";'
      }

    }
  },
  plugins: [vue()]
  // 省略其他配置
})

如果不想使用 scss 配置檔案,也可以直接寫成 scss 程式碼:

export default defineConfig({
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: '$primary: #993300'
      }
    }
  }
})

三、VueRouter

1. script-setup 模式下獲取路由引數

文件地址:https://router.vuejs.org/zh/guide/advanced/composition-api.html

由於在 script-setup模式下,沒有 this可以使用,就不能直接通過 this.$routerthis.$route來獲取路由引數和跳轉路由。
當我們需要獲取路由引數時,就可以使用 vue-router提供的 useRoute方法來獲取,使用如下:

// A.vue

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import router from "@/router";

import { useRoute } from 'vue-router'

let detailId = ref<string>('');

onMounted(() => {
    const route = useRoute();
    detailId.value = route.params.id as string; // 獲取引數
})
</script>

如果要做路由跳轉,就可以使用 useRouter方法的返回值去跳轉:

const router = useRouter();
router.push({
  name: 'search',
  query: {/**/},
})

四、Pinia

1. store 解構的變數修改後沒有更新

文件地址:https://pinia.vuejs.org/core-concepts/#using-the-store

當我們解構出 store 的變數後,再修改 store 上該變數的值,檢視沒有更新:

// A.vue
<script setup lang="ts">
import componentStore from "@/store/component";
const componentStoreObj = componentStore();
  
let { name } = componentStoreObj;
  
const changeName = () => {
  componentStoreObj.name = 'hello pingan8787';
}
</script>

<template>
  <span @click="changeName">{{name}}</span>
</template>

這時候點選按鈕觸發 changeName事件後,檢視上的 name 並沒有變化。這是因為 store 是個 reactive 物件,當進行解構後,會破壞它的響應性。所以我們不能直接進行解構。
這種情況就可以使用 Pinia 提供 storeToRefs工具方法,使用起來也很簡單,只需要將需要解構的物件通過 storeToRefs方法包裹,其他邏輯不變:

// A.vue
<script setup lang="ts">
import componentStore from "@/store/component";
import { storeToRefs } from 'pinia';
const componentStoreObj = componentStore();
  
let { name } = storeToRefs(componentStoreObj); // 使用 storeToRefs 包裹
  
const changeName = () => {
  componentStoreObj.name = 'hello pingan8787';
}
</script>

<template>
  <span @click="changeName">{{name}}</span>
</template>

這樣再修改其值,變更馬上更新檢視了。

2. Pinia 修改資料狀態的方式

按照官網給的方案,目前有三種方式修改:

  1. 通過 store.屬性名賦值修改單筆資料的狀態;

這個方法就是前面一節使用的:

const changeName = () => {
  componentStoreObj.name = 'hello pingan8787';
}
  1. 通過 $patch方法修改多筆資料的狀態;
文件地址:https://pinia.vuejs.org/api/interfaces/pinia._StoreWithState.html#patch

當我們需要同時修改多筆資料的狀態時,如果還是按照上面方法,可能要這麼寫:

const changeName = () => {
  componentStoreObj.name = 'hello pingan8787'
  componentStoreObj.age = '18'
  componentStoreObj.addr = 'xiamen'
}

上面這麼寫也沒什麼問題,但是 Pinia 官網已經說明,使用 $patch的效率會更高,效能更好,所以在修改多筆資料時,更推薦使用 $patch,使用方式也很簡單:

const changeName = () => {
  // 引數型別1:物件
  componentStoreObj.$patch({
    name: 'hello pingan8787',
    age: '18',
    addr: 'xiamen',
  })
  
  // 引數型別2:方法,該方法接收 store 中的 state 作為引數
  componentStoreObj.$patch(state => {
    state.name = 'hello pingan8787';
    state.age = '18';
    state.addr = 'xiamen';
  })
}
  1. 通過 action方法修改多筆資料的狀態;

也可以在 store 中定義 actions 的一個方法來更新:

// store.ts
import { defineStore } from 'pinia';

export default defineStore({
    id: 'testStore',
    state: () => {
        return {
            name: 'pingan8787',
            age: '10',
            addr: 'fujian'
        }
    },
    actions: {
        updateState(){
            this.name = 'hello pingan8787';
            this.age = '18';
            this.addr = 'xiamen';
        }
    }
})

使用時:

const changeName = () => {
  componentStoreObj.updateState();
}

這三種方式都能更新 Pinia 中 store 的資料狀態。

五、Element Plus

1. element-plus 打包時 @charset 警告

專案新安裝的 element-plus 在開發階段都是正常,沒有提示任何警告,但是在打包過程中,控制檯輸出下面警告內容:
image.png

在官方 issues 中查閱很久:https://github.com/element-plus/element-plus/issues/3219

嘗試在 vite.config.ts中配置 charset: false,結果也是無效:

// vite.config.ts
export default defineConfig({
  css: {
    preprocessorOptions: {
      scss: {
        charset: false // 無效
      }
    }
  }
})

最後在官方的 issues 中找到處理方法:

// vite.config.ts

// https://blog.csdn.net/u010059669/article/details/121808645
css: {
  postcss: {
    plugins: [
      // 移除打包element時的@charset警告
      {
        postcssPlugin: 'internal:charset-removal',
        AtRule: {
          charset: (atRule) => {
            if (atRule.name === 'charset') {
              atRule.remove();
            }
          }
        }
      }
    ],
  },
}

2. 中文語言包配置

文件地址:https://element-plus.gitee.io/zh-CN/guide/i18n.html#%E5%85%A8%E5%B1%80%E9%85%8D%E7%BD%AE

預設 elemnt-plus 的元件是英文狀態:
image.png

我們可以通過引入中文語言包,並新增到 ElementPlus 配置中來切換成中文:

// main.ts

// ... 省略其他
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
import locale from 'element-plus/lib/locale/lang/zh-cn'; // element-plus 中文語言包

app.use(ElementPlus, { locale }); // 配置中文語言包

這時候就能看到 ElementPlus 裡面元件的文字變成中文了。
image.png

總結

以上是我最近從入門到實戰 Vue3 全家桶的 3 個專案後總結避坑經驗,其實很多都是文件中有介紹的,只是剛開始不熟悉。也希望大夥多看看文件咯~
Vue3 script-setup 模式確實越寫越香。
本文內容如果有問題,歡迎大家一起評論討論。

相關文章