Day21 - _ document 可以做什麽呢?

_document 可以做什麽呢?

Next.js 除了 _app.tsx 之外,还提供另外一个 _document.tsx 让我们使用,在进入怎麽使用 _document.tsx 之前,我们先来了解为什麽 Next.js 需要这个设定,究竟可以用它做什麽?

在使用 React、Vue 时都会有一个专案的进入点,可能是在 public/ 资料夹里面会有一个 index.html ,在 HTML 会有个像是 <div id="app"></div> 的节点,让 React、Vue 可以抓到该节点,并把 element 动态地加入在这个节点上。

不知道你有没有发现,在 Next.js 没有一个资料夹包含像是 index.html 的档案,整个专案直接从 pages/ 这个资料夹开始,像是 <head><body> 都不见踪迹,究竟它们跑哪里去了呢?

看到这里你应该已经猜到,Next.js 把专案的进入点隐藏在背後了,平常我们看不到它,如果想要 override 专案的进入点靠的就是 _document.tsx 这个档案。

Getting started

为了要 override 预设的专案进入点,我们需要修改 pages/_document.tsx 这个档案中的内容,目前从官方文件的范例中看到的程序码还是 class component,必须要继承 Doucment 这个 class。

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

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx);
    return { ...initialProps };
  }

  render() {
    return (
      <Html>
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;

如果你不需要客制化 getInitialProps()render() ,是可以从 MyDocument 中删除的,但是 如果需要客制化 render() 要注意不能删除:

  • <Html>
  • <Head>
  • <Main>
  • <NextScript>

以上四个 component,因为 Next.js 会使用它们渲染专案的进入点。

MyDocument 中看到的 <Head> 实际上与 next/head 不太一样,主要是因为 Document 只会在服务器端渲染,而且只会渲染一次,在 <Head> 里面的设定让整个专案所有的页面都会是一样的,因此官方在 Next.js 10 的版本中建议使用者不要再 <Head> 里面使用像是 <title> 的 tag,它应该被使用在 next/head 里面。

适合在 Document 中设定的是像 google analytics、google font 这类所有的页面都会用到的函式库,或是全域 bootstrap css 等,在服务器端处理完毕後,才将 HTML 回传给使用者。

违反 @typescript-eslint

在前面的章节中我们设定了 ESLint,让程序帮我们维护程序,由於加入了 @typescript-eslint 在撰写 function 预设是需要明确指定回传的型别,例如在 Mydoucment 中的 getInitialProps 因为没有定义回传值,在 lint 阶段跳出了错误讯息,因此无法顺利推到 git 上面。

违反 @typescript-eslint

这是一个可以让程序更严谨的设定,明确地指定 function 的回传型别,一个变数才能正确地接受正确型别的回传值,或者误用没有包含回传值中的型别。但是随着 TypeScript 的型别推断越来越完善,在大部分的情况不一定会需要明确指定 function 的型别,每个 function 都需要指定型别可能会使得撰写程序码的体验不好,这需要看团队如何规范了。

在 Next.js 也许我们不需要指定 getInitialProsp 的回传型别,如果想要关闭这个 lint,可以在 .eslint.json 中使用以下设定关闭 lint 的警告:

{
  "rules": {
    "@typescript-eslint/explicit-module-boundary-types": "off"
  }
}

Server Side Rendering - styled-components

在前面的章节「用 Next.js 做一个简易产品介绍页 - file-based routing 的使用」,我们有提到如何在 SSG 或 SSR 的页面使用 styled-component,如果你尝试在 Next.js 中使用 styled-components,在没有 babel 与 Document 的设定下,打开浏览器的 console 会看到以下的讯息告诉我们服务器端跟用户端的 className 冲突了:

className 冲突

遇到 className 冲突的情况,可以安装 babel-plugin-styled-components 解决这个问题,在前面的章节已经有提过如何设定 babel,因此就不再赘述。

在设定完 babel 之後,重新启动服务器,console 里面不再出现 className 冲突的警告,看起来好像没问题了,但是实际上还存在着一个小问题。

server-side rendering 的样式不见了!!

虽然页面看起来正常,styled-component 所设定的样式都正常出现,还有什麽问题呢?

接下来,我们会需要「禁止网站使用 JavaScript」,以 Chrome 为例在「设定 / 隐私权和安全性 / 网站设定 / JavaScript」里面可以看到以下设定,选择禁止使用 JavaScript 的选项:

禁止使用 JavaScript

然後,重新整理页面後,会发现原本 styled-component 设定的样式都消失了,网页看起来像是只有原生的 tag。看到 babel-plugin-styled-components 这个 babel plugin 的文件中写道:

consistently hashed component classNames between environments (a must for server-side rendering)

这个 plugin 解决的问题是同步服务器端跟用户端的 className ,并没有处理 server-side rendering 时页面中样式预载入的问题,所以我们需要额外设定 Document,让服务器能够预先把样式都放在 <head> 里面,在没有执行 JavaScript 的情况下也可以显示正确的样式。

Styled-component 的函式库中提供了 ServerStyleSheetServerStyleSheet 可以用於把 <App /> 里面的样式抽离出来,然後再注入到 HTML 里面。

  • sheet.collectStyles 可以把 <App /> 中所有 component 的样式搜集起来
  • sheet.getStyleElement() 则是产生 <styled> 并且把 styled-component 的样式都放入里面,最後回传时放在 styles 这个 key 里面。
import Document, { DocumentContext } from "next/document";
import { ServerStyleSheet } from "styled-components";

export default class MyDocument extends Document {
  static async getInitialProps(ctx: DocumentContext) {
    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,
        styles: (
          <>
            {initialProps.styles}
            {sheet.getStyleElement()}
          </>
        ),
      };
    } finally {
      sheet.seal();
    }
  }
}

重新在 SSR 页面测试 styled-component

在设定完 Document 之後,我们需要重启服务器 yarn dev ,这时 JavaScript 还是禁止运行的状态,但是进入页面之後会发现 styled-component 尽管没有 JavaScript 的支援,仍然可以在页面中看到正确的样式。

打开 Chrome 的检视网页原始码,也可以看到在页面中已经包含 <style> tag,里面有包含一些 styled-component 会用到 className

ssr with styled-components

支援 functional component ?

从上面的范例中,我们可以看到 Document 跟 class component 整合的很好,也许你会想「Document 可以支援 functional component 的写法吗」,这个问题在 Next.js 的 GitHub discussion 有些人也在讨论什麽时候要支援 functional component 的写法。

在这个 PR#28515 里面在 8 月底悄悄的上了 11.1.1 版本的 Next.js,目的是为了支援 React 18 的 server component,而且提供了 functional component 的使用案例。

如果尝试在自己的专案里面使用 functional component 撰写 Document,在多数的情况下可能不会有什麽问题,但是目前还不支援 React hooks、suspense、context 等等的功能,得等到未来才有机会逐渐支援。此外,现在搭配 TypeScript 使用起来还不是非常好用,例如 Document 的型别该如何定义,原本可以直接继承 Document ,但是 functional component 该如何定义型别呢?

关於这个问题可以再等等,官方文件尚未更新,讨论 functional component 也是近期的事情,Next.js 11.1.1 版本在 8 月底才上线,从型别定义档里面 class Document 的注解也写了经常是为支援 css-in-js,维持 class components 也不是一件坏事。

Document component handles the initial document markup and renders only on the server side. Commonly used for implementing server side rendering for css-in-js libraries.

Reference


<<:  Day36 ( 游戏设计 ) 钓鱼游戏

>>:  Angular 深入浅出三十天:表单与测试 Day21 - E2E 测试实作 - 被保人表单

AI ninja project [day 16] 文字处理 -- 回归

我们已经有了语音转文字的技术, 那我们也能将文字进行向量化。 那我们是否能收集客服人员顾客的回答, ...

【领域展开 18 式】 我的 Bluehost 帐密不是我 WordPress 的帐密

因缘际会,写这次铁人赛内容时会使用不同台电脑做登入操作 WordPress 上的功能,因此会开设无痕...

[ Day 10 ] - 传值与传址

传值与传址 先来看案例 案例一 let a = 50; let b = a; console.log...

Day 2 云端上的资料流

云端的分类 第一次点开AWS官网( https://aws.amazon.com/ )或许会有点眼花...

Day-6 Build a CPU

Build a CPU tags: IT铁人 抽象化设计 建构一台电脑时,他要能执行所有指定ISA的...