
最近入门 Vue3 并完成 3 个项目,遇到问题蛮多的,今天就花点时间整理一下,和大家分享 23 个比较常见的问题,基本都贴出对应文档地址,还请多看文档~
已经完成的 3 个项目基本都是使用 Vue3 (setup-script 模式)全家桶开发,因此主要分几个方面总结:
- Vue3
- Vite
- VueRouter
- Pinia
- ElementPlus
- vue-echarts
-
一、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 个宏,包括:defineProps、defineEmits、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 个全局编译器宏,还没有详细介绍,这一节介绍 defineProps和 withDefaults。
使用 defineProps宏可以用来定义组件的入参,使用如下:
<script setup lang="ts">let props = defineProps<{schema: AttrsValueObject;modelValue: any;}>();</script>
这里只定义props属性中的 schema和 modelValue两个属性的类型, defineProps 的这种声明的不足之处在于,它没有提供设置 props 默认值的方式。
其实我们可以通过 withDefaults 这个宏来实现:
<script setup lang="ts">let props = withDefaults(defineProps<{schema: AttrsValueObject;modelValue: any;}>(),{schema: [],modelValue: ''});</script>
withDefaults 辅助函数提供了对默认值的类型检查,并确保返回的 props 的类型删除了已声明默认值的属性的可选标志。
4. 配置全局自定义参数
在 Vue2.x 中我们可以通过 Vue.prototype 添加全局属性 property。但是在 Vue3.x 中需要将 Vue.prototype 替换为 config.globalProperties 配置:
// Vue2.xVue.prototype.$api = axios;Vue.prototype.$eventBus = eventBus;// Vue3.xconst 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>
5. v-model 变化
当我们在使用 v-model指令的时候,实际上 v-bind 和 v-on 组合的简写,Vue2.x 和 Vue3.x 又存在差异。
- Vue2.x
```vue
在子组件中,如果要对某一个属性进行双向数据绑定,只要通过 `this.$emit('update:myPropName', newValue)` 就能更新其 `v-model`绑定的值。- Vue3.x```vue<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 对于一些开发过程中的异常,做了更友好的提示警告,比如下面这个提示:
这样能够更清楚的告知异常的出处,可以看出大概是 <ElInput 0=......这边的问题,但还不够清楚。
这时候就可以添加 Vue3.x 提供的全局异常处理器,更清晰的输出错误内容和调用栈信息,代码如下:
// main.tsapp.config.errorHandler = (err, vm, info) => {console.log('[全局异常]', err, vm, info)}
这时候就能看到输出内容如下:
一下子就清楚很多。
当然,该配置项也可以用来集成错误追踪服务 Sentry 和 Bugsnag。
推荐阅读:Vue3 如何实现全局异常处理?
7. 观察 ref 的数据不直观,不方便
当我们在控制台输出 ref声明的变量时。
const count = ref<numer>(0);console.log('[测试 ref]', count)
会看到控制台输出了一个 RefImpl对象:
看起来很不直观。我们都知道,要获取和修改 ref声明的变量的值,需要通过 .value来获取,所以你也可以:
console.log('[测试 ref]', count.value);
这里还有另一种方式,就是在控制台的设置面板中开启 「Enable custom formatters」选项。

这时候你会发现,控制台输出的 ref的格式发生变化了:
更加清晰直观了。
这个方法是我在《Vue.js 设计与实现》中发现的,但在文档也没有找到相关介绍,如果有朋友发现了,欢迎告知~
8. watch 数组不会收到更新
文档地址:https://v3.cn.vuejs.org/api/computed-watch-api.html#watcheffect
当我们在操作数组,并使用 watch做监听时,会发现数组变化后,watch的更新事件却只执行一次,后续数组更新不再执行,测试代码如下:
// 1. 使用 reactive 定义数组let arr = reactive<any>([]);const change => {arr.push(...newVal);});watch(arr, (newVal, oldVal) => {console.log('更新数组')})// 2. 使用 ref 定义数组let arr = ref<any>([]);const change => {arr.value.push(...newVal);});watch(arr.value, (newVal, oldVal) => {console.log('更新数组')})
那么应该如何解决呢,我们以 reactive定义的数组为例来看下解决方法,把 watch第一个参数改成方法调用的形式:
watch(() => [...arr], (newVal, oldVal) => {console.log('更新数组')})
二、Vite
1. Vite 动态导入的使用问题
使用 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#defineasynccomponentimport { defineAsyncComponent } from 'vue'const AsyncComp = defineAsyncComponent(() =>import('./components/AsyncComponent.vue'))app.component('async-component', AsyncComp)
2. Vite 配置 alias 类型别名
当项目比较复杂的时候,经常需要配置 alias 路径别名来简化一些代码:
import Home from '@/views/Home.vue'
在 Vite 中配置也很简单,只需要在 vite.config.ts 的 resolve.alias中配置即可:
// vite.config.tsexport default defineConfig({base: './',resolve: {alias: {"@": path.join(__dirname, "./src")},}// 省略其他配置})
如果使用的是 TypeScript 时,编辑器会提示路径不存在的警告⚠️,这时候可以在 tsconfig.json中添加 compilerOptions.paths的配置:
{"compilerOptions": {"paths": {"@/*": ["./src/*"]}}}
3. Vite 配置全局 scss
当我们需要使用 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.ts 的 css.preprocessorOptions.scss.additionalData中:
// vite.config.tsexport 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.$router或 this.$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: {/**/},})
2. VurRouter4.x 实现跳转 404 页面
在 VueRouter 4.x 之前,可以这么配置跳转 404 页面的路由:
// router.ts// ...{path: '*',name: '404',component: () => import('@/page/404')}
但是这样在 VueRouter4.x 是不行的,因为在 VueRouter4.x 中,已经移除 path: '*'的写法,需要改成下面写法:
// router.ts// ...{path: '/:pathMatch(.*)',name: '404',component: () => import('@/page/404')}
更多官方示例如下:
const routes = [// pathMatch 是参数的名称,例如,跳转到 /not/found 会得到// { params: { pathMatch: ['not', 'found'] } }// 这要归功于最后一个 *,意思是重复的参数,如果你// 打算直接使用未匹配的路径名称导航到该路径,这是必要的{ path: '/:pathMatch(.*)*', name: 'not-found', component: NotFound },// 如果你省略了最后的 `*`,在解析或跳转时,参数中的 `/` 字符将被编码{ path: '/:pathMatch(.*)', name: 'bad-not-found', component: NotFound },]// 如果使用命名路由,不好的例子:router.resolve({name: 'bad-not-found',params: { pathMatch: 'not/found' },}).href // '/not%2Ffound'// 好的例子:router.resolve({name: 'not-found',params: { pathMatch: ['not', 'found'] },}).href // '/not/found'
四、Pinia
1. 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 修改数据状态的方式
按照官网给的方案,目前有三种方式修改:
- 通过
store.属性名赋值修改单笔数据的状态;
这个方法就是前面一节使用的:
const changeName = () => {componentStoreObj.name = 'hello pingan8787';}
- 通过
$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';})}
- 通过
action方法修改多笔数据的状态;
也可以在 store 中定义 actions 的一个方法来更新:
// store.tsimport { 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 在开发阶段都是正常,没有提示任何警告,但是在打包过程中,控制台输出下面警告内容:
在官方 issues 中查阅很久:https://github.com/element-plus/element-plus/issues/3219。
尝试在 vite.config.ts中配置 charset: false,结果也是无效:
// vite.config.tsexport default defineConfig({css: {preprocessorOptions: {scss: {charset: false // 无效}}}})
最后在官方的 issues 中找到处理方法:
// vite.config.ts// https://blog.csdn.net/u010059669/article/details/121808645css: {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 的组件是英文状态:
我们可以通过引入中文语言包,并添加到 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 里面组件的文本变成中文了。
3. TypeScript 类型报错
这个经验来自网友“吴一周”同学,报错内容如下:
这时候可以在 tsconfig.json 文件中增加 compilerOptions.skipLibCheck的配置:
// tsconfig.json{"compilerOptions": {"skipLibCheck": true}}
4. 通过路由表动态配置 el-menu 名称和图标
在做后台管理系统的时候,经常需要创建左侧菜单,比如:
在不考虑后台动态配置菜单列表的情况下,我们会在前端维护一个路由表,然后使用 Element-UI 中的 el-menu组件,再通过 v-for遍历出每一个 el-menu-item菜单项。
路由表如下:
export const MenusConfig = [{ label: '创建页面', icon: 'edit', route: '/admin/edit' },{ label: '模版商城', icon: 'shop', route: '/admin/store' },{ label: '页面列表', icon: 'list', route: '/admin/list' },]<el-menu :default-active="currentMenuIndex"><template v-for="(menu, index) in MenusConfig" :key="index"><el-menu-item :index="index+''"><el-icon><icon-menu /></el-icon><template #title>{{menu.label}}</template></el-menu-item></template></el-menu>
这时候我们可以看到 Element-UI 文档示例中,都是直接使用 el-icon组件包裹实际图标的组件,比如:
文档地址:https://element-plus.gitee.io/zh-CN/component/menu.html#%E5%8F%AF%E6%8A%98%E5%8F%A0%E7%9A%84%E8%8F%9C%E5%8D%95<el-menu-item index="2"><el-icon><icon-menu /></el-icon><template #title>Navigator Two</template></el-menu-item>
这时候我们就没办法直接通过 v-for显示出图标,因为遍历出来的是字符串的图标名称。
其实这时候就可以用到 Vue3 提供的动态组件实现,实现如下:
<el-menu :default-active="currentMenuIndex"><template v-for="(menu, index) in MenusConfig" :key="index"><el-menu-item :index="index+''"><el-icon><component :is="menu.icon"></component></el-icon><template #title>{{menu.label}}</template></el-menu-item></template></el-menu>
核心就是 <component :is="menu.icon"></component>,通过 is指定具体组件名称。
5. Table toggleRowSelection 不生效
文档地址:https://element-plus.gitee.io/zh-CN/component/table.html#%E5%A4%9A%E9%80%89 参考方案:https://cloud.tencent.com/developer/article/1917452
当使用 element-plus table 多选时,如果我们首次已经选择几笔数据后,再进入表格准备编修改已经选中的几项(比如增加、移除已选中的项等),这时如果我们按照文档使用的方式,将已选列表当做参数传入,则会出现表格中的项并不会变成选中状态的情况。
const toggleSelection = (rows?: User[]) => {if (rows) {rows.forEach((row) => {multipleTableRef.value!.toggleRowSelection(row, undefined)})} else {multipleTableRef.value!.clearSelection()}}

这个问题文档里面也没有描述,解决方法如下:
将 multipleTableRef.value!.toggleRowSelection方法的第一个参数的数据源改成和 table 绑定的 data相同即可:
const renderSelectedData = (rows?: []) => {if (rows) {nextTick(() => {multipleTableRef.value!.clearSelection();rows.forEach((row: ArticleType) => {if(curData.value.result){// fix: 已选择的数据无法回显到 ElementPlus Table 上multipleTableRef.value!.toggleRowSelection(curData.value.result.find((item: ArticleType) =>item.image === row.image && item.link === row.link), true);}});});} else {multipleTableRef.value!.clearSelection();}};// 省略其他<el-table :data="curData.result" ref="multipleTableRef"@selection-change="selectArticle"></el-table>
如上代码:
curData.value.result和el-table中data的数据源是同一份;- 在
renderSelectedData方法中遍历rows时,将multipleTableRef.value!.toggleRowSelection第一个参数的值设置为curData中的值。
6. 无法覆盖 element-plus 样式
当我们需要自定义 element-plus 部分组件的样式时,通常会在 vue 文件下的 style直接修改对应组件样式,比如修改下拉选择框样式:
<style lang="scss" scoped>.DataDesignerPanel {.values-tag {.el-input__wrapper {background: transparent!important;border: 0!important;box-shadow: 0 0px 0px!important;}}}</style>
可以发现并没有剩下,这时候,可以将 style标签中的 scoped属性去掉即可:
<style lang="scss">.DataDesignerPanel {.values-tag {.el-input__wrapper {background: transparent!important;border: 0!important;box-shadow: 0 0px 0px!important;}}}</style>
六、vue-echarts
当重复生成图表时,控制台会提醒:
There is a chart instance already initialized on the dom.
这时候可以先使用 echarts 提供的 dispose方法删除 DOM:
const renderChart = () => {chart.clear && chart.clear();// 删除 DOMecharts.dispose(document.getElementById('BarChart') as HTMLElement)const chartDom = document.getElementById('BarChart')!;chart = echarts.init(chartDom);chart.setOption(chartData.value);}
七、vue-grid-layout
1. Vue3 使用问题
参考方案:https://github.com/jbaysolutions/vue-grid-layout/issues/637#issuecomment-1114368571
在 Vue3 项目中使用的时候,会提示下面错误信息:
external_commonjs_vue_commonjs2_vue_root_Vue_default.a is not a constructor
这是因为使用默认 npm install vue-grid-layout安装是 Vue2 版本,在 Vue3 中需要这样安装:
pnpm add vue-grid-layout@3.0.0-beta1
然后在 main.ts中引入:
import { createApp } from 'vue'import App from './App.vue'import VueGridLayout from 'vue-grid-layout';let app = createApp(App);app.use(VueGridLayout); // 使用app.mount('#app');
这时候 vue-grid-layout 的组件都会挂载到全局,可以直接使用。
总结
以上是我最近从入门到实战 Vue3 全家桶的 3 个项目后总结避坑经验,其实很多都是文档中有介绍的,只是刚开始不熟悉。也希望大伙多看看文档咯~
Vue3 script-setup 模式确实越写越香。
本文内容如果有问题,欢迎大家一起评论讨论。
