# 15 个 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>
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>
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>
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;
2
3
// Vue3.x
const app = createApp({});
app.config.globalProperties.$api = axios;
app.config.globalProperties.$eventBus = eventBus;
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>
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" />
2
3
4
在子组件中,如果要对某一个属性进行双向数据绑定,只要通过 this.$emit('update:myPropName', newValue) 就能更新其 v-model 绑定的值。
Vue3.x
<ChildComponent v-model="pageTitle" />
<!-- 是以下的简写: -->
<ChildComponent :modelValue="pageTitle" @update:modelValue="pageTitle = $event"/>
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>
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>
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);
};
2
3
# 观察 ref 的数据不直观,不方便
要获取和修改 ref 声明的变量的值,需要通过 .value 来获取,所以你也可以:
console.log("[测试 ref]", count.value);
# 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"),
};
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,
};
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);
2
3
4
5
6
7
8
9
# Vite 配置 alias 类型别名
https://cn.vitejs.dev/config/#resolve-alias
当项目比较复杂的时候,经常需要配置 alias 路径别名来简化一些代码:
import Home from "@/views/Home.vue";
在 Vite 中配置也很简单,只需要在 vite.config.ts 的 resolve.alias 中配置即可:
// vite.config.ts
export default defineConfig({
base: "./",
resolve: {
alias: {
"@": path.join(__dirname, "./src"),
},
},
// 省略其他配置
});
2
3
4
5
6
7
8
9
10
如果使用的是 TypeScript 时,编辑器会提示路径不存在的警告 ⚠️,这时候可以在 tsconfig.json中添加 compilerOptions.paths的配置:
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}
2
3
4
5
6
7
# Vite 配置全局 scss
export default defineConfig({
css: {
preprocessorOptions: {
scss: {
additionalData: `$injectedColor: orange;`,
},
styl: {
additionalData: `$injectedColor ?= orange`,
},
},
},
});
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>
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()],
// 省略其他配置
});
2
3
4
5
6
7
8
9
10
11
12
13
14
如果不想使用 scss 配置文件,也可以直接写成 scss 代码:
export default defineConfig({
css: {
preprocessorOptions: {
scss: {
additionalData: "$primary: #993300",
},
},
},
});
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,
},
});
}
},
};
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);
}
);
},
};
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>
2
3
4
5
6
7
8
9
10
11
12
13
如果要做路由跳转,就可以使用 useRouter 方法的返回值去跳转:
const router = useRouter();
router.push({
name: "search",
query: {
/**/
},
});
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>
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>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Pinia 修改数据状态的方式
- 通过
store.属性名赋值修改单笔数据的状态;
这个方法就是前面一节使用的:
const changeName = () => {
componentStoreObj.name = 'hello pingan8787';
}
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';
})
}
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';
}
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
使用时:
const changeName = () => {
componentStoreObj.updateState();
}
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();
}
}
}
}
],
},
}
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 });
2
3
4
5