You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js _document.tsx中window对象undefined 条件渲染GTM问题

当前环境配置

我在_document.tsx文件的<Head>组件内引入了Google Tag Manager脚本标签,实现代码如下:

class MyDocument extends Document {

  render() {
    return (
      <Html>
        <Head>
          <script
            dangerouslySetInnerHTML={{
              __html: `<!-- Google Tag Manager -->`,
            }}
          />
        </Head>
        <body>
          <noscript
            dangerouslySetInnerHTML={{
              __html: `<!-- Google Tag Manager (noscript) -->`,
            }}
          />
          <style
            dangerouslySetInnerHTML={{
              __html: (this.props as any).globalStyles,
            }}
          ></style>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}
预期实现目标

站点内面向国际用户的页面URL末尾均带有--INTL标识(示例地址:https://www.mystore.com/product--INTL)。需要实现当URL包含--INTL字符串时不渲染Google Tag Manager脚本,理想方案是将GTM脚本封装为独立组件,根据当前页面URL实现条件渲染。

尝试的无效实现

我编写了GTMComponent组件,尝试在componentDidMount生命周期中判断window对象是否存在后获取当前页面URL,再根据URL是否包含--INTL决定是否渲染script标签,随后将该组件引入_document.tsx的Head组件中使用,相关代码如下:

GTM组件代码:

class GTMComponent extends React.Component {
    let currentUrl;
    
    componentDidMount(){
        if(typeof window !== "undefined"){
            currentUrl = window.location.href; 
        }
    }

    render(){
           if(!currentUrl.contains("--INTL")){
               return (
                   <script dangerouslySetInnerHTML={{__html: `<!-- 
                   Google Tag Manager Script -->`}}/>
                )
            }
            else {
             return (
              <></>
             )
            }
    }
}

export default GTMComponent

引入GTMComponent后的_document.tsx代码:

import GTMComponent from '@components/common/GTMComponent'

class MyDocument extends Document {
    render(){
        return(
            <HTML>
                <Head>
                    <GTMComponent/>
                </Head>
                <body>
                {/* body code */}
                </body>
            </HTML>    
        )
    }
}

export default MyDocument
遇到的问题

由于Next.js服务端渲染(SSR)机制,服务端执行代码时不存在window对象,无法直接访问window;同时_document.tsx为类组件无法使用useEffect钩子,且componentDidMount生命周期在_document.tsx中无法正常生效,导致无法完成URL判断的条件渲染逻辑。


解决方案

_document.tsx仅在服务端执行,不会触发客户端组件生命周期,也无法响应客户端路由变化,靠componentDidMount拿window的方案从逻辑上就行不通,按以下方式修改即可:

  • 首屏渲染的路径判断放在_document.tsx的getInitialProps里做,直接从请求上下文拿URL,不需要访问window对象,首屏就能决定要不要注入GTM脚本,也不会出现SSR和客户端注水内容不匹配的问题
  • 客户端路由跳转的判断结合Next.js路由事件实现,监听路径变化动态控制GTM的加载

具体实现步骤

  1. 修改_document.tsx,在服务端拿到请求路径,作为参数传给GTM组件:
import Document, { Html, Head, Main, NextScript, DocumentContext } from 'next/document'
import GTMComponent from '@components/common/GTMComponent'

class MyDocument extends Document {
  static async getInitialProps(ctx: DocumentContext) {
    const initialProps = await Document.getInitialProps(ctx)
    // 服务端直接从请求对象取当前路径,无需访问window
    const currentPath = ctx.req?.url || ''
    return { ...initialProps, currentPath }
  }

  render() {
    return (
      <Html>
        <Head>
          <GTMComponent serverPath={this.props.currentPath} />
        </Head>
        <body>
          <noscript
            dangerouslySetInnerHTML={{
              __html: `<!-- Google Tag Manager (noscript) 对应代码 -->`,
            }}
          />
          <style
            dangerouslySetInnerHTML={{
              __html: (this.props as any).globalStyles,
            }}
          ></style>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}

export default MyDocument
  1. 重写GTM组件,同时兼容首屏服务端判断和客户端路由跳转判断。注意原有代码存在两个基础错误:一是类组件顶层不能直接写let currentUrl声明变量,二是JS字符串判断包含的方法是includes(),不存在contains()方法:
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'

const GTMComponent = ({ serverPath }: { serverPath: string }) => {
  const router = useRouter()
  // 初始状态直接用服务端传入的路径判断,避免注水不匹配
  const [loadGTM, setLoadGTM] = useState(!serverPath.includes('--INTL'))

  useEffect(() => {
    const handleRouteChange = (url: string) => {
      const isIntlPage = url.includes('--INTL')
      setLoadGTM(!isIntlPage)
      // 非国际页且GTM未初始化时,动态注入脚本
      if (!isIntlPage && !(window as any).gtmInited) {
        // 此处写入实际的GTM脚本注入逻辑
        ;(window as any).gtmInited = true
      }
    }
    // 监听客户端路由切换
    router.events.on('routeChangeComplete', handleRouteChange)
    return () => router.events.off('routeChangeComplete', handleRouteChange)
  }, [router.events])

  if (!loadGTM) return null

  return (
    <script
      dangerouslySetInnerHTML={{
        __html: `<!-- 写入实际的Google Tag Manager初始化脚本 -->`
      }}
    />
  )
}

export default GTMComponent

noscript标签的条件渲染逻辑和script标签一致,根据loadGTM状态判断是否输出即可。


内容的提问来源于stack exchange,提问作者bryanyi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 02:36:27