Skip to content

Commit

Permalink
pref: optimize the caching functionality of content editing (#4458)
Browse files Browse the repository at this point in the history
#### What type of PR is this?

/kind improvement

#### What this PR does / why we need it:

本 PR 优化了文章、内容编辑时的本地缓存功能,之前内容对于本地缓存的依赖性非常强,因此导致了部分问题,如 #3820 ,本 PR 计划之后本地缓存将仅用于特殊情况下的内容恢复,因此尝试做了如下修改:
1. 当前正在编辑的文章,会在正常模式(切换路由、刷新页面、失去当前窗口焦点、停止编写一定时间均属于正常模式)下进行自动保存后删除本地缓存。
2. 若本地具有缓存,则会在进入编辑页面时,比对一次 content 的 version,若缓存 version 小于线上 version,则抛弃缓存,若一致,则使用缓存。

但经过测试,本 PR 无法解决如下问题:
1. 同时编辑一篇文章时,内容会被覆盖的问题。
2. 对比版本后会抛弃缓存,而实际上应当将本地缓存加入历史版本中。

#### How to test it?

1. 新建一篇文章,编写任意内容,返回文章页查看是否已经具有新的文章,且内容已被保存。
2. 修改一篇文章的内容,然后返回文章页,查看是否不是从缓存中加载。
3. 断网模式下修改一篇文章的内容,然后返回文章页,联网后,再次打开此文章,查看是否显示从缓存中加载。
4. 断网模式下修改一篇文章的内容,然后返回文章页。在另一个浏览器或页面修改此文章并保存后,回到断网页面联网后,查看是否更新为最新内容。

#### Which issue(s) this PR fixes:

Fixes #3820 
Fixes #4223 

#### Does this PR introduce a user-facing change?
```release-note
减少内容编辑对本地缓存依赖,支持内容自动保存至服务端。
```
  • Loading branch information
LIlGG authored Aug 25, 2023
1 parent 8ad5963 commit a28c9a9
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 58 deletions.
40 changes: 40 additions & 0 deletions console/src/composables/use-auto-save-content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useWindowFocus } from "@vueuse/core";
import { watch, type Ref } from "vue";
import { onBeforeRouteLeave } from "vue-router";
import { useTimeoutFn } from "@vueuse/core";
import type { ContentCache } from "./use-content-cache";

export function useAutoSaveContent(
currentCache: Ref<ContentCache | undefined>,
raw: Ref<string | undefined>,
callback: () => void
) {
// TODO it may be necessary to know the latest version before saving, otherwise it will overwrite the latest version
const handleAutoSave = () => {
callback();
};

onBeforeRouteLeave(() => {
handleAutoSave();
});

watch(useWindowFocus(), (newFocus) => {
if (!newFocus && currentCache.value) {
handleAutoSave();
}
});

window.addEventListener("beforeunload", () => {
handleAutoSave();
});

const { start, isPending, stop } = useTimeoutFn(() => {
handleAutoSave();
}, 20 * 1000);
watch(raw, () => {
if (isPending.value) {
stop();
}
start();
});
}
90 changes: 52 additions & 38 deletions console/src/composables/use-content-cache.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { useLocalStorage } from "@vueuse/core";
import { Toast } from "@halo-dev/components";
import type { Ref } from "vue";

interface ContentCache {
import { ref, watch, type Ref } from "vue";
export interface ContentCache {
name: string;
content?: string;
version: number;
}
import debounce from "lodash.debounce";
import { useI18n } from "vue-i18n";
Expand All @@ -13,81 +13,95 @@ interface useContentCacheReturn {
handleResetCache: () => void;
handleSetContentCache: () => void;
handleClearCache: (name?: string) => void;
currentCache: Ref<ContentCache | undefined>;
}

export function useContentCache(
key: string,
name: Ref<string | undefined>,
raw: Ref<string | undefined>
raw: Ref<string | undefined>,
contentVersion: Ref<number>
): useContentCacheReturn {
const content_caches = useLocalStorage<ContentCache[]>(key, []);
const currentCache = ref<ContentCache | undefined>(undefined);
const { t } = useI18n();

const handleResetCache = () => {
let cache: ContentCache | undefined;
if (name.value) {
const cache = content_caches.value.find(
cache = content_caches.value.find(
(c: ContentCache) => c.name === name.value
);
if (cache) {
Toast.info(t("core.composables.content_cache.toast_recovered"));
raw.value = cache.content;
}
} else {
const cache = content_caches.value.find(
cache = content_caches.value.find(
(c: ContentCache) => c.name === "" && c.content
);
if (cache) {
Toast.info(t("core.composables.content_cache.toast_recovered"));
raw.value = cache.content;
}
}
if (!cache) {
return;
}
if (cache.version != contentVersion.value) {
// TODO save the local offline cached content as a historical version
handleClearCache(name.value);
return;
}
Toast.info(t("core.composables.content_cache.toast_recovered"));
raw.value = cache.content;
handleClearCache(name.value);
};

const handleSetContentCache = debounce(() => {
let cache: ContentCache | undefined;
if (name.value) {
const cache = content_caches.value.find(
cache = content_caches.value.find(
(c: ContentCache) => c.name === name.value
);
if (cache) {
cache.content = raw?.value;
} else {
content_caches.value.push({
name: name.value || "",
content: raw?.value,
});
}
} else {
const cache = content_caches.value.find(
(c: ContentCache) => c.name === ""
);
if (cache) {
cache.content = raw?.value;
} else {
content_caches.value.push({
name: "",
content: raw?.value,
});
}
cache = content_caches.value.find((c: ContentCache) => c.name === "");
}
if (cache) {
cache.content = raw?.value;
} else {
content_caches.value.push({
name: name.value || "",
content: raw?.value,
version: contentVersion.value,
});
}
}, 500);

const handleClearCache = (name?: string) => {
let index: number;
if (name) {
const index = content_caches.value.findIndex(
index = content_caches.value.findIndex(
(c: ContentCache) => c.name === name
);
index > -1 && content_caches.value.splice(index, 1);
} else {
const index = content_caches.value.findIndex(
index = content_caches.value.findIndex(
(c: ContentCache) => c.name === ""
);
index > -1 && content_caches.value.splice(index, 1);
}
index > -1 && content_caches.value.splice(index, 1);
};

watch(content_caches, (newCaches) => {
if (newCaches.length > 0) {
if (name.value) {
currentCache.value = newCaches.find(
(c: ContentCache) => c.name === name.value
);
} else {
currentCache.value = newCaches.find((c: ContentCache) => c.name === "");
}
} else {
currentCache.value = undefined;
}
});

return {
handleClearCache,
handleResetCache,
handleSetContentCache,
currentCache,
};
}
34 changes: 34 additions & 0 deletions console/src/composables/use-content-snapshot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { apiClient } from "@/utils/api-client";
import { watch, type Ref, ref, nextTick } from "vue";

interface SnapshotContent {
version: Ref<number>;
handleFetchSnapshot: () => Promise<void>;
}

export function useContentSnapshot(
snapshotName: Ref<string | undefined>
): SnapshotContent {
const version = ref(0);
watch(snapshotName, () => {
nextTick(() => {
handleFetchSnapshot();
});
});

const handleFetchSnapshot = async () => {
if (!snapshotName.value) {
return;
}
const { data } =
await apiClient.extension.snapshot.getcontentHaloRunV1alpha1Snapshot({
name: snapshotName.value,
});
version.value = data.metadata.version || 0;
};

return {
version,
handleFetchSnapshot,
};
}
37 changes: 27 additions & 10 deletions console/src/modules/contents/pages/SinglePageEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import { useI18n } from "vue-i18n";
import UrlPreviewModal from "@/components/preview/UrlPreviewModal.vue";
import { contentAnnotations } from "@/constants/annotations";
import { usePageUpdateMutate } from "./composables/use-page-update-mutate";
import { useAutoSaveContent } from "@/composables/use-auto-save-content";
import { useContentSnapshot } from "@/composables/use-content-snapshot";
const router = useRouter();
const { t } = useI18n();
Expand Down Expand Up @@ -143,22 +145,22 @@ const handleSave = async (options?: { mute?: boolean }) => {
formState.value.page = data;
} else {
// Clear new page content cache
handleClearCache();
const { data } = await apiClient.singlePage.draftSinglePage({
singlePageRequest: formState.value,
});
formState.value.page = data;
routeQueryName.value = data.metadata.name;
// Clear new page content cache
handleClearCache();
}
if (!options?.mute) {
Toast.success(t("core.common.toast.save_success"));
}
handleClearCache(routeQueryName.value as string);
handleClearCache(formState.value.page.metadata.name as string);
await handleFetchContent();
await handleFetchSnapshot();
} catch (error) {
console.error("Failed to save single page", error);
Toast.error(t("core.common.toast.save_failed_and_retry"));
Expand Down Expand Up @@ -330,13 +332,28 @@ onMounted(async () => {
handleResetCache();
});
const headSnapshot = computed(() => {
return formState.value.page.spec.headSnapshot;
});
const { version, handleFetchSnapshot } = useContentSnapshot(headSnapshot);
// SinglePage content cache
const { handleSetContentCache, handleResetCache, handleClearCache } =
useContentCache(
"singlePage-content-cache",
routeQueryName,
toRef(formState.value.content, "raw")
);
const {
currentCache,
handleSetContentCache,
handleResetCache,
handleClearCache,
} = useContentCache(
"singlePage-content-cache",
routeQueryName,
toRef(formState.value.content, "raw"),
version
);
useAutoSaveContent(currentCache, toRef(formState.value.content, "raw"), () => {
handleSave({ mute: true });
});
// SinglePage preview
const previewModal = ref(false);
Expand Down
37 changes: 27 additions & 10 deletions console/src/modules/contents/posts/PostEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import { useI18n } from "vue-i18n";
import UrlPreviewModal from "@/components/preview/UrlPreviewModal.vue";
import { usePostUpdateMutate } from "./composables/use-post-update-mutate";
import { contentAnnotations } from "@/constants/annotations";
import { useAutoSaveContent } from "@/composables/use-auto-save-content";
import { useContentSnapshot } from "@/composables/use-content-snapshot";
const router = useRouter();
const { t } = useI18n();
Expand Down Expand Up @@ -146,21 +148,21 @@ const handleSave = async (options?: { mute?: boolean }) => {
formState.value.post = data;
} else {
// Clear new post content cache
handleClearCache();
const { data } = await apiClient.post.draftPost({
postRequest: formState.value,
});
formState.value.post = data;
name.value = data.metadata.name;
// Clear new post content cache
handleClearCache();
}
if (!options?.mute) {
Toast.success(t("core.common.toast.save_success"));
}
handleClearCache(name.value as string);
handleClearCache(formState.value.post.metadata.name as string);
await handleFetchContent();
await handleFetchSnapshot();
} catch (e) {
console.error("Failed to save post", e);
Toast.error(t("core.common.toast.save_failed_and_retry"));
Expand Down Expand Up @@ -346,13 +348,28 @@ onMounted(async () => {
handleResetCache();
});
const headSnapshot = computed(() => {
return formState.value.post.spec.headSnapshot;
});
const { version, handleFetchSnapshot } = useContentSnapshot(headSnapshot);
// Post content cache
const { handleSetContentCache, handleResetCache, handleClearCache } =
useContentCache(
"post-content-cache",
name,
toRef(formState.value.content, "raw")
);
const {
currentCache,
handleSetContentCache,
handleResetCache,
handleClearCache,
} = useContentCache(
"post-content-cache",
name,
toRef(formState.value.content, "raw"),
version
);
useAutoSaveContent(currentCache, toRef(formState.value.content, "raw"), () => {
handleSave({ mute: true });
});
// Post preview
const previewModal = ref(false);
Expand Down

0 comments on commit a28c9a9

Please sign in to comment.