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

[圖片上傳失敗...(image-a199bd-1659195036496)]
最近入門 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 生命周期 執行時間說明
beforeCreate setup 組件創建前執行
created setup 組件創建后執行
beforeMount onBeforeMount 組件掛載到節點上之前執行
mounted onMounted 組件掛載完成后執行
beforeUpdate onBeforeUpdate 組件更新之前執行
updated onUpdated 組件更新完成之后執行
beforeDestroy onBeforeUnmount 組件卸載之前執行
destroyed onUnmounted 組件卸載完成后執行
errorCaptured onErrorCaptured 當捕獲一個來自子孫組件的異常時激活鉤子函數

目前 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 個宏,包括:definePropsdefineEmits、defineExpose、withDefaults。

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-682088-1659195036496)]

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,這時候需要使用前面有介紹到的 defineProps、defineEmits 兩個宏來實現:

// 子組件 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-e356a0-1659195036496)]

這樣能夠更清楚的告知異常的出處,可以看出大概是 <ElInput 0=......這邊的問題,但還不夠清楚。
這時候就可以添加 Vue3.x 提供的全局異常處理器,更清晰的輸出錯誤內容和調用棧信息,代碼如下

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

這時候就能看到輸出內容如下:
[圖片上傳失敗...(image-d868b0-1659195036496)]

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

7. 觀察 ref 的數據不直觀,不方便

當我們在控制臺輸出 ref聲明的變量時。

const count = ref<numer>(0);

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

會看到控制臺輸出了一個 RefImpl對象:
[圖片上傳失敗...(image-967761-1659195036496)]

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

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

這里還有另一種方式,就是在控制臺的設置面板中開啟 「Enable custom formatters」選項。

[圖片上傳失敗...(image-54ffde-1659195036496)]

[圖片上傳失敗...(image-8f2b2e-1659195036496)]

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

[圖片上傳失敗...(image-42be39-1659195036496)]
更加清晰直觀了。

這個方法是我在《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-ef80be-1659195036496)]

在官方 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-5fb919-1659195036496)]

我們可以通過引入中文語言包,并添加到 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-3dabb1-1659195036497)]

總結

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

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容