likes
comments
collection
share

Next.js v14 报 document is not defined 这种错怎么办?基本都会遇到,深入解析,收藏备用

作者站长头像
站长
· 阅读数 12

前言

使用 Next.js 开发项目,经常会在引入三方库(比如 antv)的时候,遇到 “document is not defined” 这样的报错:

Next.js v14 报 document is not defined 这种错怎么办?基本都会遇到,深入解析,收藏备用

错误不止这些,也有可能是 “window is not defined”

Next.js v14 报 document is not defined 这种错怎么办?基本都会遇到,深入解析,收藏备用

或者是其他各种各样的报错,但我们隐约知道这是使用了客户端 API 导致,但具体该怎么解决呢?怎么解决会更好呢?这就是本篇文章要讲解的问题。

PS:其实这个问题在开发 Next.js 项目的时候基本都会遇到,所以点赞收藏这篇文章,以防日后用到的时候却找不到……

1. 使用客户端组件

我们的第一反应往往是改为使用客户端组件,这有的时候也确实能解决问题。

我们举个轮播图的例子。运行 npx create-next-app@latest创建一个 Next.js 项目,安装轮播图功能用到的依赖项:

npm install react-slick slick-carousel --save

新建 app/slide/page.js,代码如下:

import React from "react";
import Slider from "react-slick";
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";

export default function SimpleSlider() {
  var settings = {
    dots: true,
    infinite: true,
    speed: 500,
    slidesToShow: 1,
    slidesToScroll: 1,
  };
  return (
    <>
      <div className="h-20 flex items-center justify-center">Import Third Library Example By YaYu</div>
      <div className="slider-container">
        <Slider {...settings}>
          <div className="px-4">
            <div className="h-60 flex items-center justify-center text-3xl bg-teal-400 rounded-md text-white">1</div>
          </div>
          <div className="px-4">
            <div className="h-60 flex items-center justify-center text-3xl bg-teal-500 rounded-md text-white">2</div>
          </div>
          <div className="px-4">
            <div className="h-60 flex items-center justify-center text-3xl bg-teal-600 rounded-md text-white">3</div>
          </div>
          <div className="px-4">
            <div className="h-60 flex items-center justify-center text-3xl bg-teal-700 rounded-md text-white">4</div>
          </div>
          <div className="px-4">
            <div className="h-60 flex items-center justify-center text-3xl bg-teal-800 rounded-md text-white">5</div>
          </div>
          <div className="px-4">
            <div className="h-60 flex items-center justify-center text-3xl bg-teal-900 rounded-md text-white">6</div>
          </div>
        </Slider>
      </div>
    </>
  );
}

如果像这样直接引入使用,会出现报错:

Next.js v14 报 document is not defined 这种错怎么办?基本都会遇到,深入解析,收藏备用

错误看起来有些莫名其妙,但本质还是在服务端使用了客户端 API 导致。在文件顶部添加 "use client"指令,将其改为客户端组件,此时便能正常运行,交互效果如下:

Next.js v14 报 document is not defined 这种错怎么办?基本都会遇到,深入解析,收藏备用

不过从最佳实践的角度来看,我们应该尽可能减少客户端组件的范围(对应组件树中的位置下移),所以建议改成以下这种形式:

新建 app/slider/slider.js,代码如下:

'use client'

import Slider from "react-slick";
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";

export default function SliderComponent() {
  var settings = {
    dots: true,
    infinite: true,
    speed: 500,
    slidesToShow: 1,
    slidesToScroll: 1,
  };

  return (
    <div className="slider-container">
      <Slider {...settings}>
        <div className="px-4">
          <div className="h-60 flex items-center justify-center text-3xl bg-teal-400 rounded-md text-white">1</div>
        </div>
        <div className="px-4">
          <div className="h-60 flex items-center justify-center text-3xl bg-teal-500 rounded-md text-white">2</div>
        </div>
        <div className="px-4">
          <div className="h-60 flex items-center justify-center text-3xl bg-teal-600 rounded-md text-white">3</div>
        </div>
        <div className="px-4">
          <div className="h-60 flex items-center justify-center text-3xl bg-teal-700 rounded-md text-white">4</div>
        </div>
        <div className="px-4">
          <div className="h-60 flex items-center justify-center text-3xl bg-teal-800 rounded-md text-white">5</div>
        </div>
        <div className="px-4">
          <div className="h-60 flex items-center justify-center text-3xl bg-teal-900 rounded-md text-white">6</div>
        </div>
      </Slider>
    </div>
  )
}

app/slider/page.js,代码修改为:

import React from "react";
import Slider from './slider';

export default function SimpleSlider() {

  return (
    <>
      <div className="h-20 flex items-center justify-center">Import Third Library Example By YaYu</div>
      <Slider />
    </>
  );
}

此时效果不变,但相比之前的实现,标题部分会使用服务端渲染,Slider 则继续走客户端渲染。

注:其实对这个例子而言,全部使用客户端组件和部分使用客户端组件区别并不大,但如果标题部分使用了诸如 dayjs 等这样的时间库,使用服务端渲染不会将 dayjs 打包到客户端 bundle 中。所以理论上应该尽可能减少客户端组件的范围。

轮播图组件本应该直接写在 app/slide/page.js 中,但却新建了一个组件,写在了 app/slider/slider.js,轮播图组件在 React 组件树中的位置下移了。这就是“客户端组件树下移”,Next.js 官方文档会建议 “Moving Client Components Down the Tree”,其实就是这个意思。

2. 动态导入

如果总是能够这么简单的解决就好了!

有的时候,即使添加 use client指令还是会出现问题,比如当我们使用 ant-desgin 的图表 @ant-design/charts 时……

让我们举个实战例子,安装依赖项:

npm install @ant-design/charts --save

新建 app/chart/page.js,代码如下:

import React from 'react';
import { Line } from '@ant-design/charts';

const Page = () => {
  const data = [
    { year: '1991', value: 3 },
    { year: '1992', value: 4 },
    { year: '1993', value: 3.5 },
    { year: '1994', value: 5 },
    { year: '1995', value: 4.9 },
    { year: '1996', value: 6 },
    { year: '1997', value: 7 },
    { year: '1998', value: 9 },
    { year: '1999', value: 13 },
  ];

  const config = {
    data,
    height: 400,
    xField: 'year',
    yField: 'value',
  };
  return <Line {...config} />;
};
export default Page;

打开 http://localhost:3000/chart,此时会出现报错:

Next.js v14 报 document is not defined 这种错怎么办?基本都会遇到,深入解析,收藏备用

我们很容易的想到是使用了客户端 API 的缘故,那就改为客户端组件试试。添加 "use client"指令后,页面确实不报错了,能够正常展示出来:

Next.js v14 报 document is not defined 这种错怎么办?基本都会遇到,深入解析,收藏备用

但是,如果你查看页面请求,会发现页面显示 500 错误:

Next.js v14 报 document is not defined 这种错怎么办?基本都会遇到,深入解析,收藏备用

查看命令行中的输出,依然有 document is not defined 错误:

Next.js v14 报 document is not defined 这种错怎么办?基本都会遇到,深入解析,收藏备用

虽然开发者模式看起来能够正常运行,但构建的时候(npm run build)因为有这个报错,最终会构建失败:

Next.js v14 报 document is not defined 这种错怎么办?基本都会遇到,深入解析,收藏备用

所以这个问题还是要解决。

首先我们要明白为什么会出现这个问题,明明我们都已经声明为客户端组件了。

这是因为客户端组件也会进行预渲染。你可以简单粗暴的理解为“客户端组件 = SSR + 水合 + CSR”,也就是说,客户端组件会在构建或者请求的时候在服务端预渲染一次内容,然后在客户端进行水合,最后根据交互进行客户端渲染。

这个问题就出现在当客户端组件在服务端进行预渲染的时候,错误的使用了 document 等客户端 API。

那如何取消客户端组件的预渲染呢?

import dynamic from 'next/dynamic'
 
const WithCustomLoading = dynamic(
  () => import('../components/WithCustomLoading'),
  {
    loading: () => <p>Loading...</p>,
  }
)
 
export default function Page() {
  return (
    <div>
      <WithCustomLoading />
    </div>
  )
}

动态加载本质上是 Suspense 和 React.lazy 的复合实现,Next.js 设计了 dynamic 函数专门用于加载客户端组件。不过加载客户端组件的时候,默认还是会进行预渲染,如果你要取消掉预渲染,需要单独设置 ssr 选项:

const Component = dynamic(() => import('../components/c.js'), { ssr: false })

需要注意的是,当遇到命名导出组件的时候,写法会有些不一样:

'use client'
// components/hello.js
export function Hello() {
  return <p>Hello!</p>
}
// app/page.js
import dynamic from 'next/dynamic'
 
const ClientComponent = dynamic(() =>
  import('../components/hello').then((mod) => mod.Hello)
)

像这个例子中引用 @ant-design/charts便需要命名导出。修改 app/chart/page.js,代码如下:

'use client'

import dynamic from 'next/dynamic'
 
const Line = dynamic(() =>
  import('@ant-design/charts').then((mod) => mod.Line), { ssr: false }
)

const Page = () => {
  const data = [
    { year: '1991', value: 3 },
    { year: '1992', value: 4 },
    { year: '1993', value: 3.5 },
    { year: '1994', value: 5 },
    { year: '1995', value: 4.9 },
    { year: '1996', value: 6 },
    { year: '1997', value: 7 },
    { year: '1998', value: 9 },
    { year: '1999', value: 13 },
  ];

  const config = {
    data,
    height: 400,
    xField: 'year',
    yField: 'value',
  };
  return <Line {...config} />;
};
export default Page;

此时页面能够正常渲染,也不会报错:

Next.js v14 报 document is not defined 这种错怎么办?基本都会遇到,深入解析,收藏备用

3. 使用 window、document 等客户端 API 时

不止引入三方库,当我们在自己的项目代码中使用 window、document 等客户端 API 的时候,也要小心使用。

举个例子,新建 app/window/page.js,代码如下:

export  function Page() {
  return (
    <div className="p-5">
      innerWidth: {window.innerWidth}
    </div>
  )
}

很明显,此时会出现编译错误:

Next.js v14 报 document is not defined 这种错怎么办?基本都会遇到,深入解析,收藏备用

添加 "use client" 指令,此时页面虽然能够运行,但其实页面已经出现了 500 错误:

Next.js v14 报 document is not defined 这种错怎么办?基本都会遇到,深入解析,收藏备用

命令行中依然还有 window is not defined错误提示:

Next.js v14 报 document is not defined 这种错怎么办?基本都会遇到,深入解析,收藏备用

相同的错误场景,出错原理也跟上节是一致的:客户端组件在预渲染的时候出错。

3.1. useEffect

要想避免这个错误,第一种方式是使用 useEffect:

'use client'

import { useState, useEffect } from "react";

export default function Page() {
  const [width, setWidth] = useState(0)

  useEffect(() => {
    setWidth(window.innerWidth)
  }, [])

  return (
    <div className="p-5">
      innerWidth: {width}
    </div>
  )
}

当代码放在 useEffect 中的时候,不会在客户端组件预渲染的时候执行。可以看到,此时页面正常渲染:

Next.js v14 报 document is not defined 这种错怎么办?基本都会遇到,深入解析,收藏备用

3.2. typeof window

第二种方式是进行 typeof window 进行判断,新建 app/window2/page.js,代码如下:

'use client'

export default function Page() {

  if (typeof window !== 'undefined') {
    window.addEventListener("resize", () => {
      console.log(window.innerWidth)
    })
  }

  return (
    <div className="p-5">
      addEventListener resize
    </div>
  )
}

交互效果如下:

Next.js v14 报 document is not defined 这种错怎么办?基本都会遇到,深入解析,收藏备用

3.3. dynamic

第三种方式是使用 dynamic,跟上节讲的动态导入方法一致。新建 app/window3/width.js,代码如下:

export default function With() {
  return (
    <div className="p-5">
      innerWidth: {window.innerWidth}
    </div>
  )
}

新建 app/window3/page.js代码如下:

'use client'

import dynamic from 'next/dynamic'
 
const Width = dynamic(() =>
  import('./width'), { ssr: false }
)

export default function Page() {
  return <Width />;
};

虽然我们在 <Width />组件中直接使用了 window 对象,但通过动态导入的方式,页面依然可以正常运行:

Next.js v14 报 document is not defined 这种错怎么办?基本都会遇到,深入解析,收藏备用

3.4. useMounted hook

第四种方式是写一个 useMounted hook,监听页面是否成功挂载。新建 app/window4/useMounted.js,代码如下:

'use client'

import { useState, useEffect } from 'react'

export function useMounted() {
	const [mounted, setMounted] = useState(false)

	useEffect(() => {
		setMounted(true)
	}, [])

	return mounted
}

新建 app/window4/page.js,代码如下:

'use client'

import { useMounted } from './useMounted'

export default function Page() {
  const mounted = useMounted()
  if (!mounted) return null
      
  return (
    <div className="p-5">
      innerWidth: {window.innerWidth}
    </div>
  )
}

当页面挂载成功的时候,肯定也可以正常获取 window 对象。此时此时页面也能够正常运行:

Next.js v14 报 document is not defined 这种错怎么办?基本都会遇到,深入解析,收藏备用

总结

之所以会出现 window is not defineddocument is not defined 这类错误,本质原因是在服务端调用了客户端 API 导致报错。但使用 'use client' 指令并不一定能够解决问题,'use client'指令只能说明组件可以运行在客户端,但并不说明组件只运行在客户端。客户端组件会在服务端进行预渲染,如果要取消掉这个预渲染,可以使用 dynamic 这个函数动态加载客户端组件。

同时,在自己的项目中使用客户端 API 如 window、document 的时候,也要注意避免出现这类错误。可以使用 useEffect、typeof window、dynamic、useMounted 等方式进行妥善处理。

本篇内容完整的代码仓库地址为:github.com/mqyqingfeng…