vuex4.x 升级pinia,router 中使用同步组件导致项目启动失败

背景描述

升级的项目本来是vue2的项目,先升级成vue3,这个过程相关的问题都被决绝,当时状态管理使用的还是vuex4.x版本。

后面发现变成复杂模块时,后续再对复杂模块的功能进行迭代时,由于js的弱类型,改动时比较容易出现问题,而且接口之间没有类型约束,导致改动时需要对比接口数据,再重新思考整个逻辑,导致维护成本提升。

后面引入TS的过程中,想将 vuex顺便替换成vue3种官方推荐的 pinia。按照pinia官方的文档改动很容易,大概有这个几个地方:

1. 重构store

pinia编写store时,采用扁平化的方式,直接将原有的vuex中state,getter,action迁移,其中mutations也直接写成actions,因为pinia action支持同步和异步,这样大大减少vuex中的概念确认。

2. 替换vue 组件中对store的使用

主要是以下几种方式:
● 通过 s t o r e . c o m m i t 或者 store.commit 或者 store.commit或者store.dispatch使用。
● 通过mapState,mapActions等使用
pinia 官方同样提供了支持,直接替换就好。

升级的问题

看似没有问题

本地dev模式下,由于是边改边看的,而且未完成迁移前vuex和pinia并存,项目都是正常的,整体迁移完后除了肉眼改动错误的地方,一切都是正常的。全部移除vuex的时候突然出现问题:

Uncaught ReferenceError: Cannot access 'useUserStore' before initialization
at main.vue

这个代码中有如下代码:

  methods: {
    ...mapActions(useAppStore, ['setLocal', 'setParentMenu', 'setUserServerData', 'setUserServerCascadeData']),
    ...mapActions(useUserStore, ['setUnReadMsgCount']),
    searchFileOpen(params) {
      this.$bus.emit('on-search-file-open', params)
    },
  }

就是store的action映射到组件。查询资料,官方给出的解释是 pinia实例没有挂载。
实际项目中导致的这种问题是在 router定义文件中,使用main.vue作为导航的主组件,也就是component参数。

import * as Vue from 'vue'
import Main from '@/view/main'
import FullScreenMain from '@/view/main/full-screen-main.vue'
import SliderMain from '@/view/main/slider-main.vue'
import Blank from '@/blank.vue'

export default [
  {
    path: '/',
    name: 'rootPath',
    redirect: '/index',
  },
  {
    path: '/blank',
    name: 'Blank',
    component: Blank,
  },
  {
    path: '/index',
    name: 'index',
    redirect: '/script/owner/-1',
    parentMenu: 1,
    meta: {
      icon: 'ios-navigate',
      title: '工作空间',
      launchItemName: 'script',
    },
    component: Main,
    children: [
      {
        path: '/script/:scriptType/:scriptId',
        name: 'scriptIndex',
        meta: {
          parentMenu: 1,
          icon: 'document-text',
          title: '工作空间',
          launchItemName: 'script',
        },
        component: () => import('@/view/script-manage/script-list.vue'),
      },
    ]
  }
]
这个路由定义在createRouter中使用,于是根据官方的思路应该是创建路由时,加载了组件,而组件中使用的mapActions 方法,但是此时整个vue实例没有挂载,导致pinia未初始化,从而引发useUserStore 函数不可用问题。
解决办法:既然是创建时机的问题,那么在路由中加载main组件时,使用异步加载就好,这样等整个vue实例挂载后,再去访问页面肯定可以的。在上面的router定义中,改动如下:
const Main = () => import('@/view/main')
const FullScreenMain = () => import('@/view/main/full-screen-main.vue')
const SliderMain = () => import('@/view/main/slider-main.vue')

xxx 省

然后 npm run dev ,项目正常启动,功能正常。

惊慌的上线构建发布

在上述问题都解决后,项目发布到准线上运行,在发布流程完成后,开心的打开新页面,一看:我艹,页面的菜单呢。怎么都没有了?急忙打开控制台,想要看到红色的xxx,可一个都没有。顿时慌了,发生了什么?

再次在本地删掉node_modules,再次启动项目,本地还是正常。同样线上重新构建,发现依然没有菜单,也没有任何错误。

接下来怀疑环境,线上构建是node 18.15.0,本地是20.10。尝试指定线上node版本,由于太高,构建工具不支持,只能调低本地版本,切换成18.15.0后,dev模式启动依然正常,瞬间感觉不该搞升级,吃力还出了问题,但是没办法,得继续解决。

解析来本地模拟线上环境,使用同样的构建命令,使用本地nginx作为静态服务器,折腾半天后,本地终于能启动,出现了线上相同的问题。

接下来排查菜单消失的原因。代码中大量使用了动态菜单,即对比路由定义上的元数据和后端接口的权限信息,动态的构建路由和菜单组件,有一个工具方法:

export const getFirstLevelMenuByRouter = (list) => {
  const res = []
  const parentChildrenList = []
  for (let i = 0; i < list.length; i++) {
    if (
      list[i].hasOwnProperty('component') &&
      list[i].component &&
      list[i].component.name === 'Main'
    ) {
      parentChildrenList.push(list[i])
    }
  }
  forEach(parentChildrenList, (item) => {
    if (item.meta && !item.meta.hideInMenu) {
      const obj = {
        icon: (item.meta && item.meta.icon) || '',
        name: item.name,
        meta: item.meta,
        parentMenu: item.parentMenu,
        path: item.path,
      }
      res.push(obj)
    }
  })
  return res
}

这个方法生成一级菜单,对组件进行了名称判断。然后通过打日志,发现新版本经过这个方法后,返回空数组,即 判断 component.name===‘Main’ 失效了。然后突然想起来为决绝pinia异步加载的问题,Main改成了
const Main = () => import(‘@/view/main’) 这种方式,名称变了,自然匹配不上。
该怎么解决:

const Main = defineAsyncComponent(() => import('@/view/main'))
Main.name = 'Main'
const FullScreenMain = defineAsyncComponent(() => import('@/view/main/full-screen-main.vue'))
FullScreenMain.name = 'FullScreenMain'
const SliderMain = defineAsyncComponent(() => import('@/view/main/slider-main.vue'))
SliderMain.name = 'SliderMain'

对于有问题的组件,全部改成异步,并指定名称。

结论

● 使用异步组件解决pinia初始化晚导致如果在路由中引入的组件使用pinia函数报错问题。
● 使用 defineAsyncComponent 定义异步组件,并指定名称,避免业务中对组件的名称进行了判断。

其它

尝试不使用异步组件

在线上构建问题后,代码中去掉了异步加载路由组件的方式,这时候必然出现 pinia中useXXStore未初始化的问题,尝试去掉mapState,mapAction,采用setup函数配合的方式,出现如下问题:
vue ReferenceError: Cannot access ‘Main’ before initialization
这叫莫名其妙了,但是我知道肯定是因为pinia的原因,也即是在Router中使用同步组件,而组件中使用了pinia函数的原因,但是这错误很诡异。这种方法行不通。

未解之谜

在初步使用异步import后,使用dev 模式本地正常,线上异常,这说明dev模式和build 模式存在差异,但是具体是什么,没有细究。

最近更新

  1. TCP协议是安全的吗?

    2024-06-17 08:10:02       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-06-17 08:10:02       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-06-17 08:10:02       19 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-06-17 08:10:02       20 阅读

热门阅读

  1. 苹果新型基于home app的骚扰

    2024-06-17 08:10:02       6 阅读
  2. HTML列表指南:有序、无序与自定义列表的妙用

    2024-06-17 08:10:02       6 阅读
  3. Cweek6

    Cweek6

    2024-06-17 08:10:02      6 阅读
  4. 力扣2517.礼盒的最大甜蜜度

    2024-06-17 08:10:02       7 阅读
  5. 分布式压测

    2024-06-17 08:10:02       7 阅读
  6. 数据驱动和vue的双向绑定有何异同

    2024-06-17 08:10:02       6 阅读
  7. 使用Python进行数据分析与可视化

    2024-06-17 08:10:02       7 阅读
  8. 数据分析------知识点(六)

    2024-06-17 08:10:02       5 阅读