iT邦幫忙

2021 iThome 鐵人賽

DAY 21
0
Modern Web

從零開始學習 Next.js系列 第 21

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


上一篇
Day20 - 提開發者體驗 (DX),使用 path alias
下一篇
Day22 - 錯誤捕捉、全域 CSS、共用 Layout,就用 _app.tsx 來搞定吧!
系列文
從零開始學習 Next.js30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
AndrewYEE
iT邦新手 3 級 ‧ 2023-02-16 16:43:02

感謝~ 這篇對我很重要!

我要留言

立即登入留言