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
這個檔案。
為了要 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 上面。
這是一個可以讓程式更嚴謹的設定,明確地指定 function 的回傳型別,一個變數才能正確地接受正確型別的回傳值,或者誤用沒有包含回傳值中的型別。但是隨著 TypeScript 的型別推斷越來越完善,在大部分的情況不一定會需要明確指定 function 的型別,每個 function 都需要指定型別可能會使得撰寫程式碼的體驗不好,這需要看團隊如何規範了。
在 Next.js 也許我們不需要指定 getInitialProsp
的回傳型別,如果想要關閉這個 lint,可以在 .eslint.json
中使用以下設定關閉 lint 的警告:
{
"rules": {
"@typescript-eslint/explicit-module-boundary-types": "off"
}
}
在前面的章節「用 Next.js 做一個簡易產品介紹頁 - file-based routing 的使用」,我們有提到如何在 SSG 或 SSR 的頁面使用 styled-component,如果你嘗試在 Next.js 中使用 styled-components,在沒有 babel 與 Document 的設定下,打開瀏覽器的 console 會看到以下的訊息告訴我們伺服器端跟用戶端的 className
衝突了:
遇到 className
衝突的情況,可以安裝 babel-plugin-styled-components
解決這個問題,在前面的章節已經有提過如何設定 babel,因此就不再贅述。
在設定完 babel 之後,重新啟動伺服器,console 裡面不再出現 className
衝突的警告,看起來好像沒問題了,但是實際上還存在著一個小問題。
雖然頁面看起來正常,styled-component 所設定的樣式都正常出現,還有什麼問題呢?
接下來,我們會需要「禁止網站使用 JavaScript」,以 Chrome 為例在「設定 / 隱私權和安全性 / 網站設定 / 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 的函式庫中提供了 ServerStyleSheet
, ServerStyleSheet
可以用於把 <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();
}
}
}
在設定完 Document 之後,我們需要重啟伺服器 yarn dev
,這時 JavaScript 還是禁止運行的狀態,但是進入頁面之後會發現 styled-component 儘管沒有 JavaScript 的支援,仍然可以在頁面中看到正確的樣式。
打開 Chrome 的檢視網頁原始碼,也可以看到在頁面中已經包含 <style>
tag,裡面有包含一些 styled-component 會用到 className
:
從上面的範例中,我們可以看到 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 initialdocument
markup and renders only on the server side. Commonly used for implementing server side rendering forcss-in-js
libraries.