一不小心就遇到错误VM12468:1 Uncaught SyntaxError: Failed to execute 'appendChild' on 'Node': Unexpected token '<',原本在layout.tsx
中是这样的方式,直接调用后台文本框输入的统计代码:
{/* 从后台设置注入统计代码 - 直接使用dangerouslySetInnerHTML */}
{settings.analytics.trackingCode && (
<Script
id="analytics-code"
dangerouslySetInnerHTML={{ __html: settings.analytics.trackingCode }}
strategy="afterInteractive"
/>
)}
这里的问题是 next/script
的 Script
组件不支持 dangerouslySetInnerHTML
属性,是遇到错误的根本原因。
(最简单的方法):将 Script
改为普通的 div
元素:
{settings.analytics.trackingCode && (
<div
dangerouslySetInnerHTML={{ __html: settings.analytics.trackingCode }}
/>
)}
没想到后续不小心有犯了以下低级错误:
Error: In HTML, <div> cannot be a child of <head>. This will cause a hydration error.
问题很明确:<div>
不能作为 <head>
的子元素。统计代码被放在了 <head>
部分。需要将其移到 <body>
中,或者使用正确的方式处理。
为什么会出现这个错误:
HTML 规范中,
<head>
只能包含特定元素(<title>
,<meta>
,<link>
,<script>
,<style>
等)<div>
不能直接作为<head>
的子元素Next.js 的水合过程会检查这种违规行为
所以修复过程中,思考了几种方案来解决:
// 解决方案1:将统计代码移到 body 中(推荐)
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh-CN">
<head>
{/* head 中只放 meta 标签、title、link 等 */}
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
{/* 其他 head 内容 */}
</head>
<body>
{children}
{/* 统计代码放在 body 底部 */}
{settings.analytics.trackingCode && (
<div
dangerouslySetInnerHTML={{ __html: settings.analytics.trackingCode }}
/>
)}
</body>
</html>
)
}
// 解决方案2:如果统计代码必须在 head 中,需要解析后正确处理
import { Metadata } from 'next'
// 解析统计代码中的脚本
function parseScriptFromTrackingCode(trackingCode: string) {
const scriptRegex = /<script([^>]*)>(.*?)<\/script>/is
const match = trackingCode.match(scriptRegex)
if (match) {
const attributes = match[1]
const content = match[2]
// 解析属性
const srcMatch = attributes.match(/src=["']([^"']+)["']/)
const deferMatch = attributes.includes('defer')
const asyncMatch = attributes.includes('async')
return {
src: srcMatch ? srcMatch[1] : null,
content: content.trim(),
defer: deferMatch,
async: asyncMatch,
attributes
}
}
return null
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
const scriptInfo = settings.analytics.trackingCode
? parseScriptFromTrackingCode(settings.analytics.trackingCode)
: null
return (
<html lang="zh-CN">
<head>
{/* 正确处理外部脚本 */}
{scriptInfo?.src && (
<script
src={scriptInfo.src}
defer={scriptInfo.defer}
async={scriptInfo.async}
/>
)}
{/* 正确处理内联脚本 */}
{scriptInfo?.content && !scriptInfo.src && (
<script
dangerouslySetInnerHTML={{ __html: scriptInfo.content }}
/>
)}
</head>
<body>
{children}
</body>
</html>
)
}
// 解决方案3:使用客户端组件处理复杂情况
'use client'
import { useEffect } from 'react'
interface DynamicAnalyticsProps {
trackingCode: string
}
function DynamicAnalytics({ trackingCode }: DynamicAnalyticsProps) {
useEffect(() => {
if (!trackingCode) return
// 创建临时容器解析 HTML
const parser = new DOMParser()
const doc = parser.parseFromString(trackingCode, 'text/html')
const scripts = doc.querySelectorAll('script')
scripts.forEach(script => {
const newScript = document.createElement('script')
// 复制所有属性
Array.from(script.attributes).forEach(attr => {
newScript.setAttribute(attr.name, attr.value)
})
// 复制内容
if (script.textContent) {
newScript.textContent = script.textContent
}
// 添加到 head 或 body
if (script.hasAttribute('defer') || script.hasAttribute('async')) {
document.head.appendChild(newScript)
} else {
document.body.appendChild(newScript)
}
})
// 清理函数
return () => {
// 清理动态添加的脚本(可选)
document.querySelectorAll('script[data-dynamic="true"]').forEach(s => s.remove())
}
}, [trackingCode])
return null
}
// 在 layout.tsx 中使用
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh-CN">
<head>
{/* head 内容 */}
</head>
<body>
{children}
{/* 使用客户端组件 */}
{settings.analytics.trackingCode && (
<DynamicAnalytics trackingCode={settings.analytics.trackingCode} />
)}
</body>
</html>
)
}
// 解决方案4:最简单的修复 - 仅移动位置
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh-CN">
<head>
{/* 不要在这里放统计代码的 div */}
</head>
<body>
{children}
{/* 从 head 移动到 body 底部 */}
{settings.analytics.trackingCode && (
<div
dangerouslySetInnerHTML={{ __html: settings.analytics.trackingCode }}
/>
)}
</body>
</html>
)
}
// 解决方案5:使用 next/head(如果需要在 head 中)
import Head from 'next/head'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh-CN">
<Head>
{/* 使用 next/head 处理 head 内容 */}
{settings.analytics.trackingCode && (
<script
dangerouslySetInnerHTML={{ __html: settings.analytics.trackingCode.replace(/<\/?script[^>]*>/g, '') }}
/>
)}
</Head>
<body>
{children}
</body>
</html>
)
}
在 Next.js 中,不建议在 layout.tsx
中使用 DOM 操作方法(如 document.createElement
),因为服务端渲染时 document
不存在。以下是 Next.js 中添加 Umami 统计代码的正确方法:
// 方法1:在 layout.tsx 中直接使用 Script 组件(推荐)
import Script from 'next/script'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
{children}
{/* Umami 统计代码 */}
<Script
src="https://cloud.umami.is/script.js"
data-website-id="fdf9dc7b-d730-4230-94a4-39cfa679ab3a"
strategy="afterInteractive"
defer
/>
</body>
</html>
)
}
// 方法2:创建单独的组件
// components/Analytics.tsx
import Script from 'next/script'
export default function Analytics() {
return (
<Script
src="https://cloud.umami.is/script.js"
data-website-id="fdf9dc7b-d730-4230-94a4-39cfa679ab3a"
strategy="afterInteractive"
defer
/>
)
}
// 然后在 layout.tsx 中使用
import Analytics from '@/components/Analytics'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
{children}
<Analytics />
</body>
</html>
)
}
// 方法3:使用环境变量控制(生产环境才加载)
import Script from 'next/script'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
{children}
{/* 只在生产环境加载统计代码 */}
{process.env.NODE_ENV === 'production' && (
<Script
src="https://cloud.umami.is/script.js"
data-website-id="fdf9dc7b-d730-4230-94a4-39cfa679ab3a"
strategy="afterInteractive"
defer
/>
)}
</body>
</html>
)
}
// 方法4:使用 next/head(适用于 pages 目录)
// 如果您使用的是 pages 目录而不是 app 目录
import Head from 'next/head'
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<Head>
<script
defer
src="https://cloud.umami.is/script.js"
data-website-id="fdf9dc7b-d730-4230-94a4-39cfa679ab3a"
/>
</Head>
{children}
</>
)
}
// 方法5:在 _document.tsx 中添加(pages 目录)
// pages/_document.tsx
import { Html, Head, Main, NextScript } from 'next/document'
import Script from 'next/script'
export default function Document() {
return (
<Html>
<Head>
<script
defer
src="https://cloud.umami.is/script.js"
data-website-id="fdf9dc7b-d730-4230-94a4-39cfa679ab3a"
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
推荐方案:
App Router(app 目录):使用方法1,直接在
layout.tsx
中使用next/script
的Script
组件Pages Router(pages 目录):使用方法4或5,在
_app.tsx
或_document.tsx
中添加
关键优势:
next/script
组件会自动处理服务端渲染兼容性strategy="afterInteractive"
确保脚本在页面交互就绪后加载避免了直接 DOM 操作的问题
更好的性能优化
strategy 选项说明:
beforeInteractive
:在页面交互前加载(阻塞)afterInteractive
:在页面交互后加载(推荐)lazyOnload
:空闲时加载worker
:在 Web Worker 中加载