mirror of https://github.com/halo-dev/halo
fix: optimize editor performance and resolve freezing issues (#4805)
#### What type of PR is this? /kind bug #### What this PR does / why we need it: 优化了编辑器的性能,并解决了卡死现象。具体措施如下: 1. 编辑器异步加载时,由于其 component 并不会使用响应式,所以也无需进行代理,因此使用 `markRaw` 将其转为普通对象,可以优化 vue 性能。 2. 由于 `DefaultEditor` 有多个根节点导致透传的 attrs 无法设置,因此新增一个 div 节点将其原有节点包裹。参见 https://cn.vuejs.org/guide/components/attrs.html#attribute-inheritance-on-multiple-root-nodes 。 3. 原有编辑器实例在切换路由之后不会释放,此次修改之后,将在 vue 的 `onBeforeUnmount` 时间中手动释放编辑器实例。 #### How to test it? 1. 新建文章,新建一个表格。 2. 不要保存,点击文章路由跳出编辑器界面,再次点击上一次所编辑器的文章,查看是否会卡死。 #### Which issue(s) this PR fixes: Fixes #4798 #### Does this PR introduce a user-facing change? ```release-note 优化编辑器性能并解决切换页面所造成的卡死现象 ```pull/4815/head
parent
b9c0a1f1d0
commit
691cd38c51
|
@ -86,6 +86,7 @@ import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
|
||||||
import { usePluginModuleStore } from "@/stores/plugin";
|
import { usePluginModuleStore } from "@/stores/plugin";
|
||||||
import type { PluginModule } from "@halo-dev/console-shared";
|
import type { PluginModule } from "@halo-dev/console-shared";
|
||||||
import { useDebounceFn } from "@vueuse/core";
|
import { useDebounceFn } from "@vueuse/core";
|
||||||
|
import { onBeforeUnmount } from "vue";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
@ -355,6 +356,10 @@ onMounted(() => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
editor.value?.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
// image drag and paste upload
|
// image drag and paste upload
|
||||||
const { policies } = useFetchAttachmentPolicy();
|
const { policies } = useFetchAttachmentPolicy();
|
||||||
|
|
||||||
|
@ -491,203 +496,205 @@ const currentLocale = i18n.global.locale.value as
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AttachmentSelectorModal
|
<div>
|
||||||
v-model:visible="attachmentSelectorModal"
|
<AttachmentSelectorModal
|
||||||
@select="onAttachmentSelect"
|
v-model:visible="attachmentSelectorModal"
|
||||||
/>
|
@select="onAttachmentSelect"
|
||||||
<RichTextEditor v-if="editor" :editor="editor" :locale="currentLocale">
|
/>
|
||||||
<template #extra>
|
<RichTextEditor v-if="editor" :editor="editor" :locale="currentLocale">
|
||||||
<OverlayScrollbarsComponent
|
<template #extra>
|
||||||
element="div"
|
<OverlayScrollbarsComponent
|
||||||
:options="{ scrollbars: { autoHide: 'scroll' } }"
|
element="div"
|
||||||
class="h-full border-l bg-white"
|
:options="{ scrollbars: { autoHide: 'scroll' } }"
|
||||||
defer
|
class="h-full border-l bg-white"
|
||||||
>
|
defer
|
||||||
<VTabs v-model:active-id="extraActiveId" type="outline">
|
>
|
||||||
<VTabItem
|
<VTabs v-model:active-id="extraActiveId" type="outline">
|
||||||
id="toc"
|
<VTabItem
|
||||||
:label="$t('core.components.default_editor.tabs.toc.title')"
|
id="toc"
|
||||||
>
|
:label="$t('core.components.default_editor.tabs.toc.title')"
|
||||||
<div class="p-1 pt-0">
|
>
|
||||||
<ul v-if="headingNodes?.length" class="space-y-1">
|
<div class="p-1 pt-0">
|
||||||
<li
|
<ul v-if="headingNodes?.length" class="space-y-1">
|
||||||
v-for="(node, index) in headingNodes"
|
<li
|
||||||
:key="index"
|
v-for="(node, index) in headingNodes"
|
||||||
:class="[
|
:key="index"
|
||||||
{ 'bg-gray-100': node.id === selectedHeadingNode?.id },
|
:class="[
|
||||||
]"
|
{ 'bg-gray-100': node.id === selectedHeadingNode?.id },
|
||||||
class="group cursor-pointer truncate rounded-base px-1.5 py-1 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
]"
|
||||||
@click="handleSelectHeadingNode(node)"
|
class="group cursor-pointer truncate rounded-base px-1.5 py-1 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
||||||
>
|
@click="handleSelectHeadingNode(node)"
|
||||||
<div
|
|
||||||
:style="{
|
|
||||||
paddingLeft: `${(node.level - 1) * 0.8}rem`,
|
|
||||||
}"
|
|
||||||
class="flex items-center gap-2"
|
|
||||||
>
|
>
|
||||||
<component
|
|
||||||
:is="headingIcons[node.level]"
|
|
||||||
class="h-4 w-4 rounded-sm bg-gray-100 p-0.5 group-hover:bg-white"
|
|
||||||
:class="[
|
|
||||||
{ '!bg-white': node.id === selectedHeadingNode?.id },
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
<span class="flex-1 truncate">{{ node.text }}</span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<div v-else class="flex flex-col items-center py-10">
|
|
||||||
<span class="text-sm text-gray-600">
|
|
||||||
{{ $t("core.components.default_editor.tabs.toc.empty") }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</VTabItem>
|
|
||||||
<VTabItem
|
|
||||||
id="information"
|
|
||||||
:label="$t('core.components.default_editor.tabs.detail.title')"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col gap-2 p-1 pt-0">
|
|
||||||
<div class="grid grid-cols-2 gap-2">
|
|
||||||
<div
|
|
||||||
class="group flex cursor-pointer flex-col gap-y-5 rounded-md bg-gray-100 px-1.5 py-1 transition-all"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div
|
<div
|
||||||
class="text-sm text-gray-500 group-hover:text-gray-900"
|
:style="{
|
||||||
|
paddingLeft: `${(node.level - 1) * 0.8}rem`,
|
||||||
|
}"
|
||||||
|
class="flex items-center gap-2"
|
||||||
>
|
>
|
||||||
{{
|
<component
|
||||||
$t(
|
:is="headingIcons[node.level]"
|
||||||
"core.components.default_editor.tabs.detail.fields.character_count"
|
class="h-4 w-4 rounded-sm bg-gray-100 p-0.5 group-hover:bg-white"
|
||||||
)
|
:class="[
|
||||||
}}
|
{ '!bg-white': node.id === selectedHeadingNode?.id },
|
||||||
</div>
|
]"
|
||||||
<div class="rounded bg-gray-200 p-0.5">
|
|
||||||
<IconCharacterRecognition
|
|
||||||
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
|
|
||||||
/>
|
/>
|
||||||
|
<span class="flex-1 truncate">{{ node.text }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</li>
|
||||||
<div class="text-base font-medium text-gray-900">
|
</ul>
|
||||||
{{ editor.storage.characterCount.characters() }}
|
<div v-else class="flex flex-col items-center py-10">
|
||||||
</div>
|
<span class="text-sm text-gray-600">
|
||||||
</div>
|
{{ $t("core.components.default_editor.tabs.toc.empty") }}
|
||||||
<div
|
</span>
|
||||||
class="group flex cursor-pointer flex-col gap-y-5 rounded-md bg-gray-100 px-1.5 py-1 transition-all"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div
|
|
||||||
class="text-sm text-gray-500 group-hover:text-gray-900"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
$t(
|
|
||||||
"core.components.default_editor.tabs.detail.fields.word_count"
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div class="rounded bg-gray-200 p-0.5">
|
|
||||||
<IconCharacterRecognition
|
|
||||||
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-base font-medium text-gray-900">
|
|
||||||
{{ editor.storage.characterCount.words() }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</VTabItem>
|
||||||
|
<VTabItem
|
||||||
|
id="information"
|
||||||
|
:label="$t('core.components.default_editor.tabs.detail.title')"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-2 p-1 pt-0">
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<div
|
||||||
|
class="group flex cursor-pointer flex-col gap-y-5 rounded-md bg-gray-100 px-1.5 py-1 transition-all"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div
|
||||||
|
class="text-sm text-gray-500 group-hover:text-gray-900"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
"core.components.default_editor.tabs.detail.fields.character_count"
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<div class="rounded bg-gray-200 p-0.5">
|
||||||
|
<IconCharacterRecognition
|
||||||
|
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-base font-medium text-gray-900">
|
||||||
|
{{ editor.storage.characterCount.characters() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="group flex cursor-pointer flex-col gap-y-5 rounded-md bg-gray-100 px-1.5 py-1 transition-all"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div
|
||||||
|
class="text-sm text-gray-500 group-hover:text-gray-900"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
"core.components.default_editor.tabs.detail.fields.word_count"
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<div class="rounded bg-gray-200 p-0.5">
|
||||||
|
<IconCharacterRecognition
|
||||||
|
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-base font-medium text-gray-900">
|
||||||
|
{{ editor.storage.characterCount.words() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="publishTime" class="grid grid-cols-1 gap-2">
|
<div v-if="publishTime" class="grid grid-cols-1 gap-2">
|
||||||
<div
|
<div
|
||||||
class="group flex cursor-pointer flex-col gap-y-5 rounded-md bg-gray-100 px-1.5 py-1 transition-all"
|
class="group flex cursor-pointer flex-col gap-y-5 rounded-md bg-gray-100 px-1.5 py-1 transition-all"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div
|
<div
|
||||||
class="text-sm text-gray-500 group-hover:text-gray-900"
|
class="text-sm text-gray-500 group-hover:text-gray-900"
|
||||||
>
|
>
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
"core.components.default_editor.tabs.detail.fields.publish_time"
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<div class="rounded bg-gray-200 p-0.5">
|
||||||
|
<IconCalendar
|
||||||
|
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-base font-medium text-gray-900">
|
||||||
{{
|
{{
|
||||||
|
formatDatetime(publishTime) ||
|
||||||
$t(
|
$t(
|
||||||
"core.components.default_editor.tabs.detail.fields.publish_time"
|
"core.components.default_editor.tabs.detail.fields.draft"
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded bg-gray-200 p-0.5">
|
</div>
|
||||||
<IconCalendar
|
</div>
|
||||||
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
|
<div v-if="owner" class="grid grid-cols-1 gap-2">
|
||||||
/>
|
<div
|
||||||
|
class="group flex cursor-pointer flex-col gap-y-5 rounded-md bg-gray-100 px-1.5 py-1 transition-all"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div
|
||||||
|
class="text-sm text-gray-500 group-hover:text-gray-900"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
"core.components.default_editor.tabs.detail.fields.owner"
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<div class="rounded bg-gray-200 p-0.5">
|
||||||
|
<IconUserFollow
|
||||||
|
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-base font-medium text-gray-900">
|
||||||
|
{{ owner }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-base font-medium text-gray-900">
|
</div>
|
||||||
{{
|
<div v-if="permalink" class="grid grid-cols-1 gap-2">
|
||||||
formatDatetime(publishTime) ||
|
<div
|
||||||
$t(
|
class="group flex cursor-pointer flex-col gap-y-5 rounded-md bg-gray-100 px-1.5 py-1 transition-all"
|
||||||
"core.components.default_editor.tabs.detail.fields.draft"
|
>
|
||||||
)
|
<div class="flex items-center justify-between">
|
||||||
}}
|
<div
|
||||||
|
class="text-sm text-gray-500 group-hover:text-gray-900"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
"core.components.default_editor.tabs.detail.fields.permalink"
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<div class="rounded bg-gray-200 p-0.5">
|
||||||
|
<IconLink
|
||||||
|
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a
|
||||||
|
:href="permalink"
|
||||||
|
:title="permalink"
|
||||||
|
target="_blank"
|
||||||
|
class="text-sm text-gray-900 hover:text-blue-600"
|
||||||
|
>
|
||||||
|
{{ permalink }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="owner" class="grid grid-cols-1 gap-2">
|
</VTabItem>
|
||||||
<div
|
</VTabs>
|
||||||
class="group flex cursor-pointer flex-col gap-y-5 rounded-md bg-gray-100 px-1.5 py-1 transition-all"
|
</OverlayScrollbarsComponent>
|
||||||
>
|
</template>
|
||||||
<div class="flex items-center justify-between">
|
</RichTextEditor>
|
||||||
<div
|
</div>
|
||||||
class="text-sm text-gray-500 group-hover:text-gray-900"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
$t(
|
|
||||||
"core.components.default_editor.tabs.detail.fields.owner"
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div class="rounded bg-gray-200 p-0.5">
|
|
||||||
<IconUserFollow
|
|
||||||
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-base font-medium text-gray-900">
|
|
||||||
{{ owner }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="permalink" class="grid grid-cols-1 gap-2">
|
|
||||||
<div
|
|
||||||
class="group flex cursor-pointer flex-col gap-y-5 rounded-md bg-gray-100 px-1.5 py-1 transition-all"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div
|
|
||||||
class="text-sm text-gray-500 group-hover:text-gray-900"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
$t(
|
|
||||||
"core.components.default_editor.tabs.detail.fields.permalink"
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div class="rounded bg-gray-200 p-0.5">
|
|
||||||
<IconLink
|
|
||||||
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
:href="permalink"
|
|
||||||
:title="permalink"
|
|
||||||
target="_blank"
|
|
||||||
class="text-sm text-gray-900 hover:text-blue-600"
|
|
||||||
>
|
|
||||||
{{ permalink }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</VTabItem>
|
|
||||||
</VTabs>
|
|
||||||
</OverlayScrollbarsComponent>
|
|
||||||
</template>
|
|
||||||
</RichTextEditor>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { usePluginModuleStore } from "@/stores/plugin";
|
import { usePluginModuleStore } from "@/stores/plugin";
|
||||||
import type { EditorProvider, PluginModule } from "@halo-dev/console-shared";
|
import type { EditorProvider, PluginModule } from "@halo-dev/console-shared";
|
||||||
import { onMounted, ref, type Ref, defineAsyncComponent } from "vue";
|
import { onMounted, ref, type Ref, defineAsyncComponent, markRaw } from "vue";
|
||||||
import { VLoading } from "@halo-dev/components";
|
import { VLoading } from "@halo-dev/components";
|
||||||
import Logo from "@/assets/logo.png";
|
import Logo from "@/assets/logo.png";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
@ -18,11 +18,13 @@ export function useEditorExtensionPoints(): useEditorExtensionPointsReturn {
|
||||||
{
|
{
|
||||||
name: "default",
|
name: "default",
|
||||||
displayName: t("core.plugin.extension_points.editor.providers.default"),
|
displayName: t("core.plugin.extension_points.editor.providers.default"),
|
||||||
component: defineAsyncComponent({
|
component: markRaw(
|
||||||
loader: () => import("@/components/editor/DefaultEditor.vue"),
|
defineAsyncComponent({
|
||||||
loadingComponent: VLoading,
|
loader: () => import("@/components/editor/DefaultEditor.vue"),
|
||||||
delay: 200,
|
loadingComponent: VLoading,
|
||||||
}),
|
delay: 200,
|
||||||
|
})
|
||||||
|
),
|
||||||
rawType: "HTML",
|
rawType: "HTML",
|
||||||
logo: Logo,
|
logo: Logo,
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue