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.
>>: Angular 深入浅出三十天:表单与测试 Day21 - E2E 测试实作 - 被保人表单
我们已经有了语音转文字的技术, 那我们也能将文字进行向量化。 那我们是否能收集客服人员顾客的回答, ...
因缘际会,写这次铁人赛内容时会使用不同台电脑做登入操作 WordPress 上的功能,因此会开设无痕...
传值与传址 先来看案例 案例一 let a = 50; let b = a; console.log...
云端的分类 第一次点开AWS官网( https://aws.amazon.com/ )或许会有点眼花...
Build a CPU tags: IT铁人 抽象化设计 建构一台电脑时,他要能执行所有指定ISA的...