分享 15 個(gè) Vue3 全家桶開發(fā)的避坑經(jīng)驗(yàn)

[圖片上傳失敗...(image-a199bd-1659195036496)]
最近入門 Vue3 并完成 3 個(gè)項(xiàng)目,遇到問題蠻多的,今天就花點(diǎn)時(shí)間整理一下,和大家分享 15 個(gè)比較常見的問題,基本都貼出對應(yīng)文檔地址,還請多看文檔~
已經(jīng)完成的 3 個(gè)項(xiàng)目基本都是使用 Vue3 (setup-script 模式)全家桶開發(fā),因此主要分幾個(gè)方面總結(jié):

  • 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 生命周期 執(zhí)行時(shí)間說明
beforeCreate setup 組件創(chuàng)建前執(zhí)行
created setup 組件創(chuàng)建后執(zhí)行
beforeMount onBeforeMount 組件掛載到節(jié)點(diǎn)上之前執(zhí)行
mounted onMounted 組件掛載完成后執(zhí)行
beforeUpdate onBeforeUpdate 組件更新之前執(zhí)行
updated onUpdated 組件更新完成之后執(zhí)行
beforeDestroy onBeforeUnmount 組件卸載之前執(zhí)行
destroyed onUnmounted 組件卸載完成后執(zhí)行
errorCaptured onErrorCaptured 當(dāng)捕獲一個(gè)來自子孫組件的異常時(shí)激活鉤子函數(shù)

目前 Vue3.x 依然支持 Vue2.x 的生命周期,但不建議混搭使用,前期可以先使用 2.x 的生命周期,后面盡量使用 3.x 的生命周期開發(fā)。

由于我使用都是 script-srtup模式,所以都是直接使用 Vue3.x 的生命周期函數(shù):

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

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

每個(gè)鉤子的執(zhí)行時(shí)機(jī)點(diǎn),也可以看看文檔:
https://v3.cn.vuejs.org/guide/instance.html#生命周期圖示

2. script-setup 模式中父組件獲取子組件的數(shù)據(jù)

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

這里主要介紹父組件如何去獲取子組件內(nèi)部定義的變量,關(guān)于父子組件通信,可以看文檔介紹比較詳細(xì):
https://v3.cn.vuejs.org/guide/component-basics.html

我們可以使用全局編譯器宏defineExpose宏,將子組件中需要暴露給父組件獲取的參數(shù),通過 {key: vlaue}方式作為參數(shù)即可,父組件通過模版 ref 方式獲取子組件實(shí)例,就能獲取到對應(yīng)值:

// 子組件
<script setup>
    let name = ref("pingan8787")
    defineExpose({ name }); // 顯式暴露的數(shù)據(jù),父組件才可以獲取
</script>

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

注意

  • 全局編譯器宏只能在 script-setup 模式下使用;
  • script-setup 模式下,使用宏時(shí)無需 import可以直接使用;
  • script-setup 模式一共提供了 4 個(gè)宏,包括:definePropsdefineEmitsdefineExposewithDefaults

3. 為 props 提供默認(rèn)值

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 個(gè)全局編譯器宏,還沒有詳細(xì)介紹,這一節(jié)介紹 definePropswithDefaults
使用 defineProps宏可以用來定義組件的入?yún)ⅲ褂萌缦拢?/p>

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

這里只定義props屬性中的 schemamodelValue兩個(gè)屬性的類型, defineProps 的這種聲明的不足之處在于,它沒有提供設(shè)置 props 默認(rèn)值的方式。
其實(shí)我們可以通過 withDefaults 這個(gè)宏來實(shí)現(xiàn):

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

withDefaults 輔助函數(shù)提供了對默認(rèn)值的類型檢查,并確保返回的 props 的類型刪除了已聲明默認(rèn)值的屬性的可選標(biāo)志。

4. 配置全局自定義參數(shù)

文檔地址: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;

使用時(shí)需要先通過 vue 提供的 getCurrentInstance方法獲取實(shí)例對象:

// 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內(nèi)容輸出如下:
[圖片上傳失敗...(image-682088-1659195036496)]

5. v-model 變化

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

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

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

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

在子組件中,如果要對某一個(gè)屬性進(jìn)行雙向數(shù)據(jù)綁定,只要通過 this.$emit('update:myPropName', newValue) 就能更新其 v-model綁定的值。

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

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

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

script-setup模式下就不能使用 this.$emit去派發(fā)更新事件,畢竟沒有 this,這時(shí)候需要使用前面有介紹到的 definePropsdefineEmits 兩個(gè)宏來實(shí)現(xiàn):

// 子組件 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']); // 定義需要派發(fā)的事件名稱

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

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

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

</script>

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

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

父組件使用的時(shí)候就很簡單:

// 父組件 father.vue

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

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

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

6. 開發(fā)環(huán)境報(bào)錯(cuò)不好排查

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

Vue3.x 對于一些開發(fā)過程中的異常,做了更友好的提示警告,比如下面這個(gè)提示:
[圖片上傳失敗...(image-e356a0-1659195036496)]

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

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

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

一下子就清楚很多。
當(dāng)然,該配置項(xiàng)也可以用來集成錯(cuò)誤追蹤服務(wù) SentryBugsnag
推薦閱讀:Vue3 如何實(shí)現(xiàn)全局異常處理?

7. 觀察 ref 的數(shù)據(jù)不直觀,不方便

當(dāng)我們在控制臺(tái)輸出 ref聲明的變量時(shí)。

const count = ref<numer>(0);

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

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

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

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

這里還有另一種方式,就是在控制臺(tái)的設(shè)置面板中開啟 「Enable custom formatters」選項(xiàng)。

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

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

這時(shí)候你會(huì)發(fā)現(xiàn),控制臺(tái)輸出的 ref的格式發(fā)生變化了:

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

這個(gè)方法是我在《Vue.js 設(shè)計(jì)與實(shí)現(xiàn)》中發(fā)現(xiàn)的,但在文檔也沒有找到相關(guān)介紹,如果有朋友發(fā)現(xiàn)了,歡迎告知~

二、Vite

1. Vite 動(dòng)態(tài)導(dǎo)入的使用問題

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

使用 webpack 的同學(xué)應(yīng)該都知道,在 webpack 中可以通過 require.context動(dòng)態(tài)導(dǎo)入文件:

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

在 Vite 中,我們可以使用這兩個(gè)方法來動(dòng)態(tài)導(dǎo)入文件:

  • import.meta.glob

該方法匹配到的文件默認(rèn)是懶加載,通過動(dòng)態(tài)導(dǎo)入實(shí)現(xiàn),構(gòu)建時(shí)會(huì)分離獨(dú)立的 chunk,是異步導(dǎo)入,返回的是 Promise,需要做異步操作,使用方式如下:

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

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

該方法是直接導(dǎo)入所有模塊,并且是同步導(dǎo)入,返回結(jié)果直接通過 for...in循環(huán)就可以操作,使用方式如下:

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

// 轉(zhuǎn)譯后:
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
}

如果僅僅使用異步導(dǎo)入 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

當(dāng)項(xiàng)目比較復(fù)雜的時(shí)候,經(jīng)常需要配置 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 時(shí),編輯器會(huì)提示路徑不存在的警告??,這時(shí)候可以在 tsconfig.json中添加 compilerOptions.paths的配置:

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

3. Vite 配置全局 scss

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

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

<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 模式下獲取路由參數(shù)

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

由于在 script-setup模式下,沒有 this可以使用,就不能直接通過 this.$routerthis.$route來獲取路由參數(shù)和跳轉(zhuǎn)路由。
當(dāng)我們需要獲取路由參數(shù)時(shí),就可以使用 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; // 獲取參數(shù)
})
</script>

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

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

四、Pinia

1. store 解構(gòu)的變量修改后沒有更新

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

當(dāng)我們解構(gòu)出 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>

這時(shí)候點(diǎn)擊按鈕觸發(fā) changeName事件后,視圖上的 name 并沒有變化。這是因?yàn)?store 是個(gè) reactive 對象,當(dāng)進(jìn)行解構(gòu)后,會(huì)破壞它的響應(yīng)性。所以我們不能直接進(jìn)行解構(gòu)。
這種情況就可以使用 Pinia 提供 storeToRefs工具方法,使用起來也很簡單,只需要將需要解構(gòu)的對象通過 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 修改數(shù)據(jù)狀態(tài)的方式

按照官網(wǎng)給的方案,目前有三種方式修改:

  1. 通過 store.屬性名賦值修改單筆數(shù)據(jù)的狀態(tài);

這個(gè)方法就是前面一節(jié)使用的:

const changeName = () => {
  componentStoreObj.name = 'hello pingan8787';
}
  1. 通過 $patch方法修改多筆數(shù)據(jù)的狀態(tài);

文檔地址:https://pinia.vuejs.org/api/interfaces/pinia._StoreWithState.html#patch

當(dāng)我們需要同時(shí)修改多筆數(shù)據(jù)的狀態(tài)時(shí),如果還是按照上面方法,可能要這么寫:

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

上面這么寫也沒什么問題,但是 Pinia 官網(wǎng)已經(jīng)說明,使用 $patch的效率會(huì)更高,性能更好,所以在修改多筆數(shù)據(jù)時(shí),更推薦使用 $patch,使用方式也很簡單:

const changeName = () => {
  // 參數(shù)類型1:對象
  componentStoreObj.$patch({
    name: 'hello pingan8787',
    age: '18',
    addr: 'xiamen',
  })
  
  // 參數(shù)類型2:方法,該方法接收 store 中的 state 作為參數(shù)
  componentStoreObj.$patch(state => {
    state.name = 'hello pingan8787';
    state.age = '18';
    state.addr = 'xiamen';
  })
}
  1. 通過 action方法修改多筆數(shù)據(jù)的狀態(tài);

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

// 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';
        }
    }
})

使用時(shí):

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

這三種方式都能更新 Pinia 中 store 的數(shù)據(jù)狀態(tài)。

五、Element Plus

1. element-plus 打包時(shí) @charset 警告

項(xiàng)目新安裝的 element-plus 在開發(fā)階段都是正常,沒有提示任何警告,但是在打包過程中,控制臺(tái)輸出下面警告內(nèi)容:
[圖片上傳失敗...(image-ef80be-1659195036496)]

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

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

// 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時(shí)的@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

默認(rèn) elemnt-plus 的組件是英文狀態(tài):
[圖片上傳失敗...(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 }); // 配置中文語言包

這時(shí)候就能看到 ElementPlus 里面組件的文本變成中文了。
[圖片上傳失敗...(image-3dabb1-1659195036497)]

總結(jié)

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容