关于 Vben5 热更新 Cannot read properties of null (reading 'nextSibling')的分析

428 阅读4分钟

问题

保存时仍需报错 但是F5正常 打包也正常

image.png

chunk-ZHNFVA5M.js?v=ac948eb1:11432 Uncaught (in promise) TypeError: Cannot read properties of null (reading 'nextSibling')
    at nextSibling (chunk-ZHNFVA5M.js?v=ac948eb1:11432:35)
    at removeFragment (chunk-ZHNFVA5M.js?v=ac948eb1:6679:14)
    at remove2 (chunk-ZHNFVA5M.js?v=ac948eb1:6650:9)
    at unmount (chunk-ZHNFVA5M.js?v=ac948eb1:6628:9)
    at Object.remove (chunk-ZHNFVA5M.js?v=ac948eb1:9303:13)
    at unmount (chunk-ZHNFVA5M.js?v=ac948eb1:6603:20)
    at unmountComponent (chunk-ZHNFVA5M.js?v=ac948eb1:6713:7)
    at unmount (chunk-ZHNFVA5M.js?v=ac948eb1:6593:7)
    at unmountComponent (chunk-ZHNFVA5M.js?v=ac948eb1:6713:7)
    at unmount (chunk-ZHNFVA5M.js?v=ac948eb1:6593:7)

环境准备

官方原版前端环境(5.5.9版本) 未调整任何配置 具体为

  • transition开
  • keepAlive关

(后话) 跟上面的都没关系

在什么情况才会出现

之前在项目中测试 只要是xxx.ts(tsx)保存 就会出现该问题 xxx.vue则不会出现

测试非useVbenXXX的页面

先使用playground/src/views/demos/access/index.vue来测试

image.png

新建data.ts 然后vue文件引用

data.ts

console.log('code code');

index.vue

import './data';

修改data.ts里的代码(或者直接保存也会触发热更新) 页面正常热更新 没有出现问题

测试useVbenModal/Drawer

文件位置playground/src/views/examples/modal/index.vue

image.png

还是同样 引用data.ts

流程跟上面一样 也能正常热更新

测试useVxe

文件位置playground/src/views/examples/vxe-table/basic.vue

data.ts

console.log('code code');

export const column = [
  { title: '序号', type: 'seq', width: 50 },
  { field: 'name', title: 'Name' },
  { field: 'age', sortable: true, title: 'Age' },
  { field: 'nickname', title: 'Nickname' },
  { field: 'role', title: 'Role' },
  { field: 'address', showOverflow: true, title: 'Address' },
];

vue

onMounted(() => {
  console.log('mounted');
});

onBeforeUnmount(() => {
  console.log('unmount');
});

然后保存data.ts 也能正常更新 看看Log

image.png

卸载再挂载 看起来流程没有问题

换个符合业务带查询的表格试试呢 playground/src/views/examples/vxe-table/form.vue

保存也是正常的 (后话: 因为没有Modal/Drawer) 也没有公用data.ts

混合

使用这个页面测试

image.png

保存data.ts后 一定会复现

image.png

先把drawer相关的代码移除 发现保存正常了(热更新)

image.png

那么确定是useForm的问题?

然后试了 单独 drawer+form 的情况(不包含表格) 没问题?

这就奇了怪了 三个单独使用都没问题 混在一次就有问题了?

然后回到role菜单来测试 保留drawer 但是去除useForm 这样热更新也没问题

只保留一个空drawer 难道是useForm的问题 控制变量进一步探究

<script lang="ts" setup>
import type { SystemRoleApi } from '#/api/system/role';

import { computed, ref } from 'vue';

import { useVbenDrawer } from '@vben/common-ui';

import { $t } from '#/locales';

const formData = ref<SystemRoleApi.SystemRole>();

const [Drawer] = useVbenDrawer({});
</script>
<template>
  <Drawer />
</template>

研究过程不赘述 发现将schema改为内部(不引用data.ts) 则不报错

原版 从外部data.ts导入 报错
const [Form, formApi] = useVbenForm({
  schema: useFormSchema(),
  showDefaultActions: false,
});

内部 不报错
const [Form, formApi] = useVbenForm({
  schema: [],
  showDefaultActions: false,
});

最终发现 只要把data.ts中引用的schema移动到新的ts 或者 直接在modal.vue 都能正常运行(算一种解决方案)

const [BasicForm, formApi] = useVbenForm({
  commonConfig: {
    labelWidth: 80,
  },
  schema: [直接在这里写 不从data.ts导入],
  showDefaultActions: false,
});

或者

// 跟data.ts文件分开
import { modalSchema } from './other';

const [BasicForm, formApi] = useVbenForm({
  commonConfig: {
    labelWidth: 80,
  },
  schema: modalSchema(),
  showDefaultActions: false,
});

这是一种解决方案 但不是最终的解决方案

另外的解决方案

使用热更新api来操作 也能正常 不推荐

<script setup lang="ts">
const key = ref(0);
if (import.meta.hot) {
  import.meta.hot.accept(() => {
    key.value++;
  });
}
</script>

<template>
  <BasicModal :key="key" :title="title" class="w-[550px]">
    <BasicForm />
  </BasicModal>
</template>

继续分析

问题跟useForm也无关 去除form后保存data.ts 热更新还是报错 那么问题就是在modal(drawer)上

那么需要确定是modal造成的还是useVbenModal造成

使用控制变量的方法 直接不使用useVbenModal方法 导入modal并且更改props来直接显示

packages/@core/ui-kit/popup-ui/src/modal/modal.vue

发现保存 热更新也是在报错 那么确定就是组件的问题

通过一个个删除元素 最终确认!!!

问题出在这个组件 DialogContent!!!

image.png

内部是这个组件 radix-vue提供的DialogPortal 确认是这个组件产生的问题

image.png

只要这个删除(换成div) 热更新就不会报错(当然样式也会有异常)

到github查看源码

github.com/unovue/reka…

image.png

使用的是TeleportPrimitive 问题出在这里 查看源码

image.png

贴上关键代码

<script setup lang="ts">
import { useMounted } from '@vueuse/core'

withDefaults(defineProps<TeleportProps>(), {
  to: 'body',
})

const isMounted = useMounted()
</script>

<template>
  <Teleport
    v-if="isMounted || forceMount"
    :to="to"
    :disabled="disabled"
    :defer="defer"
  >
    <slot />
  </Teleport>
</template>

本质就是对Teleport的封装(且参数都和Vue的一致) 看起来也没啥问题 那直接把上面的组件换成vue的Teleport试试 发现

热更新正常了 不会报错了!!!

而且外部只使用了to参数 切换到teleport也是无缝

image.png

after

image.png

经过测试 热更新不会出现问题

原因分析

将文件粘贴到本地来测试

image.png

DialogContent.vue 引用 DialogPortal.vue 引用 MyTeleport.vue

保存 果然出现了问题

image.png

然后直接引用MyTeleport.vue来测试

image.png

也报错

image.png

当直接使用Teleport是正常了 只要包装了一层 就会产生问题

目前看使用Teleport替换也没有产生什么副作用 先这样吧 有空再分析补充

解决方案

packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogContent.vue

DialogPortal替换为Teleport

Drawer也是一样的改法

packages/@core/ui-kit/shadcn-ui/src/ui/sheet/SheetContent.vue

DialogPortal替换为Teleport