From 5910c520210f47bf9a5c457bb955635cce32775d Mon Sep 17 00:00:00 2001 From: naiba Date: Tue, 17 Dec 2024 22:03:21 +0800 Subject: [PATCH] feat: implement InjectContext for dynamic resource injection --- src/lib/inject.ts | 104 ++++++++++++++++++++++++++++++++++++++++++++ src/routes/root.tsx | 19 +++----- 2 files changed, 111 insertions(+), 12 deletions(-) create mode 100644 src/lib/inject.ts diff --git a/src/lib/inject.ts b/src/lib/inject.ts new file mode 100644 index 0000000..81705fb --- /dev/null +++ b/src/lib/inject.ts @@ -0,0 +1,104 @@ +export const InjectContext = (content: string) => { + const tempDiv = document.createElement("div") + tempDiv.innerHTML = content + + const INJECTION_MARK = "data-injected" // 自定义属性标识 + + // 清理已有的注入资源 + const cleanInjectedResources = () => { + document.querySelectorAll(`[${INJECTION_MARK}]`).forEach((node) => node.remove()) + } + + const loadExternalScript = (scriptElement: HTMLScriptElement): Promise => { + return new Promise((resolve, reject) => { + const script = document.createElement("script") + script.src = scriptElement.src + script.async = false // 保持顺序执行 + script.setAttribute(INJECTION_MARK, "true") // 添加标识 + script.onload = () => resolve() + script.onerror = () => reject(new Error(`Failed to load script: ${scriptElement.src}`)) + document.head.appendChild(script) + }) + } + + const executeInlineScript = (scriptElement: HTMLScriptElement): void => { + const script = document.createElement("script") + script.textContent = scriptElement.textContent + script.setAttribute(INJECTION_MARK, "true") // 添加标识 + document.body.appendChild(script) + } + + const loadStyle = (styleElement: HTMLStyleElement): Promise => { + return new Promise((resolve, reject) => { + if ((styleElement as any).href) { + // 处理 + const link = document.createElement("link") + link.rel = "stylesheet" + link.href = (styleElement as any).href + link.setAttribute(INJECTION_MARK, "true") // 添加标识 + link.onload = () => resolve() + link.onerror = () => reject(new Error(`Failed to load stylesheet: ${link.href}`)) + document.head.appendChild(link) + } else { + const style = document.createElement("style") + style.textContent = styleElement.textContent + style.setAttribute(INJECTION_MARK, "true") // 添加标识 + document.head.appendChild(style) + resolve() + } + }) + } + + const handlers: { [key: string]: (element: HTMLElement) => Promise } = { + SCRIPT: (element) => { + const scriptElement = element as HTMLScriptElement + if (scriptElement.src) { + // 加载外部脚本 + return loadExternalScript(scriptElement) + } else { + // 推迟执行内联脚本,后续手动执行 + return Promise.resolve() + } + }, + STYLE: (element) => loadStyle(element as HTMLStyleElement), + META: (element) => { + const meta = element.cloneNode(true) as HTMLElement + meta.setAttribute(INJECTION_MARK, "true") // 添加标识 + document.head.appendChild(meta) // 将 meta 标签插入到 + return Promise.resolve() + }, + DEFAULT: (element) => { + element.setAttribute(INJECTION_MARK, "true") // 添加标识 + document.body.appendChild(element) + return Promise.resolve() + }, + } + + // 开始注入前清理已有资源 + cleanInjectedResources() + + const externalScriptQueue: Promise[] = [] + + Array.from(tempDiv.childNodes).forEach((node) => { + if (node.nodeType === Node.ELEMENT_NODE) { + const element = node as HTMLElement + if (element.tagName === "SCRIPT" && !(element as HTMLScriptElement).src) { + // 直接执行内联脚本 + executeInlineScript(element as HTMLScriptElement) + } else { + const handler = handlers[element.tagName] || handlers.DEFAULT + externalScriptQueue.push(handler(element)) + } + } else if (node.nodeType === Node.TEXT_NODE) { + document.body.appendChild(document.createTextNode(node.textContent || "")) + } + }) + + return Promise.all(externalScriptQueue) + .then(() => { + console.log("All resources have been injected successfully.") + }) + .catch((error) => { + console.error("Error during resource injection:", error) + }) +} diff --git a/src/routes/root.tsx b/src/routes/root.tsx index 9dc83ef..4c36684 100644 --- a/src/routes/root.tsx +++ b/src/routes/root.tsx @@ -3,7 +3,8 @@ import { ThemeProvider } from "@/components/theme-provider" import { Toaster } from "@/components/ui/sonner" import useSetting from "@/hooks/useSetting" import i18n from "@/lib/i18n" -import { useCallback, useEffect } from "react" +import { InjectContext } from "@/lib/inject" +import { useEffect } from "react" import { useTranslation } from "react-i18next" import { Outlet } from "react-router-dom" @@ -15,14 +16,6 @@ export default function Root() { document.title = settingData?.site_name || "哪吒监控 Nezha Monitoring" }, [settingData]) - const InjectContext = useCallback((content: string) => { - document.getElementById("nezha-custom-code")?.remove() - const tempDiv = document.createElement("div") - tempDiv.id = "nezha-custom-code" - tempDiv.innerHTML = content - document.body.appendChild(tempDiv) - }, []) - if (error) { throw error } @@ -35,9 +28,11 @@ export default function Root() { i18n.changeLanguage(settingData?.language) } - if (settingData?.custom_code_dashboard) { - InjectContext(settingData?.custom_code_dashboard) - } + useEffect(() => { + if (settingData?.custom_code_dashboard) { + InjectContext(settingData?.custom_code) + } + }, [settingData?.custom_code_dashboard]) return (