MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Next.js自定义Document组件初探与应用

2022-05-137.2k 阅读

Next.js 中的 Document 组件基础认知

在 Next.js 应用开发里,Document 组件起着至关重要的作用。它代表了应用的顶级 HTML 文档结构,是 Next.js 应用在服务器端渲染(SSR)和静态站点生成(SSG)过程中生成 HTML 页面的基础模板。

默认情况下,Next.js 会自动生成一个基本的 Document 结构。例如,在一个新建的 Next.js 项目中,无需手动创建 Document 组件,Next.js 会默认生成类似如下的 HTML 结构:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charSet="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="next-head-count" content="2" />
    <title>My Next.js App</title>
    <style id="__next_styles__"></style>
  </head>
  <body>
    <div id="__next"></div>
    <script id="__NEXT_DATA__" type="application/json">{"props":{...}}</script>
  </body>
</html>

这里,<head> 标签包含了字符集、视口等元数据,<title> 设置了页面标题,<body> 中的 __next 是 Next.js 应用挂载的节点,__NEXT_DATA__ 脚本则包含了应用所需的初始数据。

然而,在实际项目开发中,默认的 Document 结构往往不能满足所有需求。比如,你可能需要添加自定义的 meta 标签,引入特定的 CSS 样式,或者更改 HTML 标签的属性等。这就需要我们自定义 Document 组件。

自定义 Document 组件的创建

要自定义 Document 组件,首先要在项目的 pages 目录下创建一个 _document.js 文件。这个文件的命名是固定的,Next.js 会根据这个文件名来识别并使用该组件作为自定义的 Document 组件。

下面是一个简单的 _document.js 示例:

import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  render() {
    return (
      <Html lang="zh-CN">
        <Head>
          <meta name="description" content="这是一个自定义的 Next.js Document 组件示例" />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;

在这个示例中,我们从 next/document 导入了必要的组件。Document 是基础类,我们通过继承它来创建自己的 MyDocument 类。Html 组件包裹整个 HTML 结构,Head 组件用于放置头部信息,Main 组件会渲染 Next.js 应用的页面内容,NextScript 组件用于加载 Next.js 的客户端脚本。

这里我们给 Html 标签设置了 lang 属性为 zh - CN,并在 Head 中添加了一个自定义的 meta 标签,用于设置页面的描述信息。

自定义 Document 组件中的 Head 部分深入应用

  1. 添加自定义 meta 标签:在实际项目中,根据页面的不同需求添加特定的 meta 标签是常见的操作。比如,对于一个产品详情页面,可能需要添加 og(Open Graph)协议相关的 meta 标签,以优化在社交媒体上分享时的展示效果。
import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  render() {
    return (
      <Html lang="en">
        <Head>
          {/* 通用 meta 标签 */}
          <meta name="description" content="产品详情页面" />
          {/* og 协议 meta 标签 */}
          <meta property="og:title" content="示例产品" />
          <meta property="og:description" content="这是一个示例产品的详细介绍" />
          <meta property="og:image" content="https://example.com/product.jpg" />
          <meta property="og:url" content="https://example.com/product" />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;
  1. 引入外部 CSS 样式:有时候,我们需要引入外部的 CSS 框架,如 Bootstrap 或 Tailwind CSS。可以在 Head 中通过 <link> 标签来引入。
import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  render() {
    return (
      <Html lang="en">
        <Head>
          <link
            rel="stylesheet"
            href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
            integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM"
            crossorigin="anonymous"
          />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;
  1. 添加自定义字体:如果项目需要使用自定义字体,可以通过 @font - face 规则在 Head 中引入。
import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  render() {
    return (
      <Html lang="en">
        <Head>
          <style>{`
            @font-face {
              font-family: 'MyCustomFont';
              src: url('/fonts/mycustomfont.woff2') format('woff2'),
                   url('/fonts/mycustomfont.woff') format('woff');
              font-weight: normal;
              font-style: normal;
            }
          `}</style>
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;

自定义 Document 组件中的 Body 部分应用

  1. 添加全局 JavaScript 脚本:在某些情况下,我们可能需要在页面的 body 中添加全局的 JavaScript 脚本。比如,添加 Google Analytics 脚本。
import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  render() {
    return (
      <Html lang="en">
        <Head />
        <body>
          <Main />
          <script
            async
            src="https://www.googletagmanager.com/gtag/js?id=G - YOUR - TRACKING - ID"
          />
          <script dangerouslySetInnerHTML={{
            __html: `
              window.dataLayer = window.dataLayer || [];
              function gtag(){dataLayer.push(arguments);}
              gtag('js', new Date());
              gtag('config', 'G - YOUR - TRACKING - ID');
            `
          }} />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;

这里使用了 dangerouslySetInnerHTML 来设置脚本的内部内容。需要注意的是,使用 dangerouslySetInnerHTML 时要确保内容的安全性,避免 XSS(跨站脚本攻击)风险。

  1. 添加自定义 body 类名:有时候,根据项目的设计需求,可能需要给 body 标签添加自定义类名,以便在全局 CSS 中进行样式控制。
import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  render() {
    return (
      <Html lang="en">
        <Head />
        <body className="custom - body - class">
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;

然后在全局 CSS 文件中,可以这样定义样式:

.custom - body - class {
  background - color: #f0f0f0;
  font - family: Arial, sans - serif;
}

自定义 Document 组件与页面特定需求结合

  1. 基于页面动态生成 meta 标签:在 Next.js 中,不同的页面可能需要不同的 meta 标签。我们可以通过在页面组件中传递数据,然后在 Document 组件中根据这些数据来动态生成 meta 标签。

首先,在页面组件(例如 pages/product/[id].js)中:

import React from'react';
import { useRouter } from 'next/router';

const ProductPage = () => {
  const router = useRouter();
  const { id } = router.query;

  const productMeta = {
    title: `产品 ${id} 详情`,
    description: `这是产品 ${id} 的详细介绍`
  };

  return (
    <div>
      <h1>{productMeta.title}</h1>
      <p>{productMeta.description}</p>
    </div>
  );
};

ProductPage.getInitialProps = async (context) => {
  const { id } = context.query;
  // 这里可以根据 id 从 API 获取产品的详细信息
  return { id };
};

export default ProductPage;

然后,在 _document.js 中:

import Document, { Html, Head, Main, NextScript } from 'next/document';
import { ServerStyleSheet } from 'styled - components';

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const sheet = new ServerStyleSheet();
    const originalRenderPage = ctx.renderPage;

    try {
      ctx.renderPage = () =>
        originalRenderPage({
          enhanceApp: (App) => (props) =>
            sheet.collectStyles(<App {...props} />)
        });

      const initialProps = await Document.getInitialProps(ctx);
      return {
      ...initialProps,
        // 这里获取页面传递的 meta 数据
        productMeta: ctx.pageProps.productMeta
      };
    } finally {
      sheet.seal();
    }
  }

  render() {
    const { productMeta } = this.props;
    return (
      <Html lang="en">
        <Head>
          {productMeta && (
            <>
              <meta name="title" content={productMeta.title} />
              <meta name="description" content={productMeta.description} />
            </>
          )}
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;

在上述代码中,我们通过 getInitialProps 方法在 Document 组件中获取页面传递的 productMeta 数据,并根据这些数据动态生成 meta 标签。

  1. 针对特定页面引入不同的 CSS 样式:同样,不同页面可能需要引入不同的 CSS 样式。例如,管理后台页面可能需要引入一套专门的管理风格 CSS,而前台展示页面使用另一套样式。

假设我们有一个管理后台页面 pages/admin/dashboard.js,可以在 _document.js 中这样处理:

import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  render() {
    const isAdminPage = typeof window === 'undefined'
     ? this.props.__NEXT_DATA__.route.startsWith('/admin')
      : window.location.pathname.startsWith('/admin');

    return (
      <Html lang="en">
        <Head>
          {isAdminPage && (
            <link
              rel="stylesheet"
              href="/styles/admin.css"
            />
          )}
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;

这里通过判断当前页面是否是管理后台页面,来决定是否引入 admin.css 样式文件。

自定义 Document 组件在服务器端渲染和静态站点生成中的作用

  1. 服务器端渲染(SSR)中的 Document 组件:在 SSR 模式下,Document 组件在服务器端生成 HTML 页面时起到关键作用。它会根据应用的需求生成完整的 HTML 结构,包括头部信息、页面内容和必要的脚本。由于是在服务器端生成,所以可以更好地优化搜索引擎优化(SEO),因为搜索引擎爬虫可以直接获取到完整的页面结构和内容。

例如,当一个用户请求访问一个 SSR 的 Next.js 页面时,服务器会首先执行 Document 组件的 getInitialProps 方法(如果存在),获取必要的数据,然后根据这些数据和页面组件生成 HTML 页面。这个 HTML 页面会包含所有的 meta 标签、样式和初始的页面内容,直接返回给客户端。客户端接收到页面后,Next.js 会通过 NextScript 进行 hydration(水合作用),将静态的 HTML 页面转化为交互式的 React 应用。

  1. 静态站点生成(SSG)中的 Document 组件:在 SSG 模式下,Document 组件同样重要。在构建阶段,Next.js 会根据 Document 组件的定义生成静态 HTML 文件。每个页面的 HTML 文件都会包含符合该页面需求的头部信息、内容和脚本。

例如,对于一个博客网站,在构建时,Document 组件会为每篇博客文章生成相应的 HTML 文件,其中包含了文章标题、描述等 meta 标签,以及文章内容。这些静态 HTML 文件可以直接部署到 CDN 上,提供快速的访问速度。同时,由于静态文件已经包含了完整的页面结构,搜索引擎爬虫可以更方便地索引页面内容,提升网站的 SEO 效果。

自定义 Document 组件的性能优化

  1. 减少不必要的渲染:在 Document 组件中,要尽量避免在 render 方法中进行复杂的计算或频繁的状态更新。因为 Document 组件在每个页面请求时都会被渲染,过多的计算会影响性能。例如,如果有一些数据只需要在组件初始化时获取一次,而不是每次渲染都重新获取,可以将这些逻辑放在 getInitialProps 方法中。
import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx);
    // 这里获取一次数据
    const someData = await fetchSomeData();
    return {
    ...initialProps,
      someData
    };
  }

  render() {
    const { someData } = this.props;
    // 使用 someData 进行简单的展示逻辑
    return (
      <Html lang="en">
        <Head>
          <meta name="description" content={someData.description} />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

async function fetchSomeData() {
  // 模拟数据获取
  return { description: '示例数据描述' };
}

export default MyDocument;
  1. 优化 CSS 和脚本加载:在引入外部 CSS 和脚本时,要注意优化加载顺序和方式。对于 CSS,尽量将关键样式放在 <Head> 中尽早加载,以避免页面出现 “FOUC”(Flash of Unstyled Content)现象。对于脚本,如果不是立即需要执行的,可以使用 asyncdefer 属性来异步加载或延迟加载。
import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  render() {
    return (
      <Html lang="en">
        <Head>
          {/* 关键 CSS 样式 */}
          <link rel="stylesheet" href="/styles/global.css" />
          {/* 异步加载外部脚本 */}
          <script async src="https://example.com/script.js"></script>
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;
  1. 利用缓存:在获取数据或执行一些操作时,可以考虑使用缓存机制。例如,如果在 getInitialProps 中获取的数据不经常变化,可以将其缓存起来,下次请求时直接从缓存中读取,而不需要再次获取。
import Document, { Html, Head, Main, NextScript } from 'next/document';

let cachedData;

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx);
    if (!cachedData) {
      cachedData = await fetchSomeData();
    }
    return {
    ...initialProps,
      cachedData
    };
  }

  render() {
    const { cachedData } = this.props;
    return (
      <Html lang="en">
        <Head>
          <meta name="description" content={cachedData.description} />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

async function fetchSomeData() {
  // 模拟数据获取
  return { description: '示例缓存数据描述' };
}

export default MyDocument;

通过以上对 Next.js 自定义 Document 组件的多方面探索和应用,我们可以更好地满足项目在页面结构、样式、脚本等方面的多样化需求,同时优化应用的性能和用户体验。无论是小型项目还是大型企业级应用,合理利用自定义 Document 组件都能为前端开发带来很大的便利和提升。