# 15 个 Vue3 全家桶开发的避坑经验

返回:vue3

# Vue2.x 和 Vue3.x 生命周期方法的变化

官方文档:https://v3.cn.vuejs.org/guide/composition-api-lifecycle-hooks.html

script-setup 模式

选项式 API Hook inside setup
beforeCreate Not needed
created Not needed
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted
errorCaptured onErrorCaptured
renderTracked onRenderTracked
renderTriggered onRenderTriggered
activated onActivated
deactivated onDeactivated

# script-setup 模式中父组件获取子组件的数据

可以使用全局编译器宏的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>
1
2
3
4
5
6
7
8
9
10
11
12

WARNING

  • 全局编译器宏只能在 script-setup 模式下使用;
  • script-setup 模式下,使用宏时无需 import 可以直接使用;
  • script-setup 模式一共提供了 4 个宏,包括:defineProps、defineEmits、defineExpose、withDefaults

# 为 props 提供默认值

使用 defineProps 宏可以用来定义组件的入参,使用如下:

<script setup lang="ts">
let props = defineProps<{
  schema: AttrsValueObject;
  modelValue: any;
}>();
</script>
1
2
3
4
5
6

这里只定义 props 属性中的 schema 和 modelValue 两个属性的类型, defineProps 的这种声明的不足之处在于,它没有提供设置 props 默认值的方式。 其实我们可以通过 withDefaults 这个宏来实现

<script setup lang="ts">
let props = withDefaults(
  defineProps<{
    schema: AttrsValueObject;
    modelValue: any;
  }>(),
  {
    schema: [],
    modelValue: "",
  }
);
</script>
1
2
3
4
5
6
7
8
9
10
11
12

# 配置全局自定义参数

https://v3.cn.vuejs.org/guide/migration/global-api.html#vue-prototype-替换为-config-globalproperties

在 Vue2.x 中我们可以通过 Vue.prototype 添加全局属性 property。但是在 Vue3.x 中需要将 Vue.prototype 替换为 config.globalProperties 配置:

// Vue2.x
Vue.prototype.$api = axios;
Vue.prototype.$eventBus = eventBus;
1
2
3
// Vue3.x
const app = createApp({});
app.config.globalProperties.$api = axios;
app.config.globalProperties.$eventBus = eventBus;
1
2
3
4

使用时需要先通过 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>
1
2
3
4
5
6
7
8
9
10
11

# v-model 变化

https://v3.cn.vuejs.org/guide/migration/v-model.html

当我们在使用 v-model 指令的时候,实际上 v-bind 和 v-on 组合的简写,Vue2.x 和 Vue3.x 又存在差异。

Vue2.x

<ChildComponent v-model="pageTitle" />

<!-- 是以下的简写: -->
<ChildComponent :value="pageTitle" @input="pageTitle = $event" />
1
2
3
4

在子组件中,如果要对某一个属性进行双向数据绑定,只要通过 this.$emit('update:myPropName', newValue) 就能更新其 v-model 绑定的值。

Vue3.x

<ChildComponent v-model="pageTitle" />

<!-- 是以下的简写: -->

<ChildComponent :modelValue="pageTitle" @update:modelValue="pageTitle = $event"/>
1
2
3
4
5

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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

父组件使用的时候就很简单:

// 父组件 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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 开发环境报错不好排查

https://v3.cn.vuejs.org/api/application-config.html#errorhandler

app.config.errorHandler = (err, vm, info) => {
  console.log("[全局异常]", err, vm, info);
};
1
2
3

# 观察 ref 的数据不直观,不方便

要获取和修改 ref 声明的变量的值,需要通过 .value 来获取,所以你也可以:

console.log("[测试 ref]", count.value);
1

# Vite 动态导入的使用问题

https://cn.vitejs.dev/guide/features.html#glob-import

在 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"),
};
1
2
3
4
5
6
7

# import.meta.globEag

该方法是直接导入所有模块,并且是同步导入,返回结果直接通过 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,
};
1
2
3
4
5
6
7
8
9

如果仅仅使用异步导入 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);
1
2
3
4
5
6
7
8
9

# Vite 配置 alias 类型别名

https://cn.vitejs.dev/config/#resolve-alias

当项目比较复杂的时候,经常需要配置 alias 路径别名来简化一些代码:

import Home from "@/views/Home.vue";
1

在 Vite 中配置也很简单,只需要在 vite.config.ts 的 resolve.alias 中配置即可:

// vite.config.ts
export default defineConfig({
  base: "./",
  resolve: {
    alias: {
      "@": path.join(__dirname, "./src"),
    },
  },
  // 省略其他配置
});
1
2
3
4
5
6
7
8
9
10

如果使用的是 TypeScript 时,编辑器会提示路径不存在的警告 ⚠️,这时候可以在 tsconfig.json中添加 compilerOptions.paths的配置:

{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"]
     }
  }
}
1
2
3
4
5
6
7

# Vite 配置全局 scss

export default defineConfig({
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `$injectedColor: orange;`,
      },
      styl: {
        additionalData: `$injectedColor ?= orange`,
      },
    },
  },
});
1
2
3
4
5
6
7
8
9
10
11
12

当我们需要使用 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>
1
2
3
4
5
6
7
8
9
10
11

我们可以将 scss 主题配置文件,配置在 vite.config.ts 的

css.preprocessorOptions.scss.additionalData 中:

// vite.config.ts
export default defineConfig({
  base: "./",
  css: {
    preprocessorOptions: {
      // 添加公共样式
      scss: {
        additionalData: '@import "./src/style/style.scss";',
      },
    },
  },
  plugins: [vue()],
  // 省略其他配置
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14

如果不想使用 scss 配置文件,也可以直接写成 scss 代码:

export default defineConfig({
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: "$primary: #993300",
      },
    },
  },
});
1
2
3
4
5
6
7
8
9

# script-setup 模式下获取路由参数

// 我们使用 useRouter 函数获取参数
import { useRouter, useRoute } from "vue-router";

export default {
  setup() {
    const router = useRouter();
    const route = useRoute();

    function pushWithQuery(query) {
      router.push({
        name: "search",
        query: {
          ...route.query,
        },
      });
    }
  },
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

route 对象是一个响应式对象,所以它的任何属性都可以被监听,但你应该避免监听整个 route 对象:

import { useRoute } from "vue-router";

export default {
  setup() {
    const route = useRoute();
    const userData = ref();

    // 当参数更改时获取用户信息
    watch(
      () => route.params,
      async (newParams) => {
        userData.value = await fetchUser(newParams.id);
      }
    );
  },
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<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>
1
2
3
4
5
6
7
8
9
10
11
12
13

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

const router = useRouter();
router.push({
  name: "search",
  query: {
    /**/
  },
});
1
2
3
4
5
6
7

# 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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

这时候点击按钮触发 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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# Pinia 修改数据状态的方式

  • 通过 store.属性名赋值修改单笔数据的状态;

这个方法就是前面一节使用的:

const changeName = () => {
  componentStoreObj.name = 'hello pingan8787';
}
1
2
3
  • 通过 $patch方法修改多笔数据的状态

使用 $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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • 通过 action方法修改多笔数据的状态
// 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';
        }
    }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

使用时:

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

# element-plus 打包时 @charset 警告

最后在官方的 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();
            }
          }
        }
      }
    ],
  },
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 中文语言包配置

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 }); 
1
2
3
4
5