本文主要是講講如何使用 Next.js 框架實現服務端渲染,將有效提升網頁的 SEO 和首屏渲染速度,說不定哪天就用上了,是吧!
一、服務端渲染(SSR)
服務端渲染(SSR,Server Side Render)與客戶端渲染(CSR,Client Side Render)的核心區分點簡單來說就是完整的 HTML 文檔在服務端還是瀏覽器里組裝完成。
SSR 的另一概念是同構渲染,可以看看知乎中的討論:什么是前端的同構渲染?[1]
同構渲染簡單來說就是一份代碼,服務端先通過服務端渲染(SSR),生成 HTML 以及初始化數據,客戶端拿到代碼和初始化數據后,通過對 HTML 的 DOM 進行 patch 和事件綁定對 DOM 進行客戶端激活(client-side hydration),該整體過程叫同構渲染。
SSR 的原理,本文就不再贅述了,感興趣的朋友推薦閱讀這篇文章:《徹底理解服務端渲染 - SSR原理》
二、Next.js
Next.js[2] 是一款用于生產環境的 React 框架,無需配置,默認提供了生產環境所需所有功能的最佳開發實踐:支持靜態渲染和服務端渲染、支持 TypeScript、智能打包、路由預加載等功能。
與此同時,Next.js 還提供了如下開箱即用的 SDK 輔助開發 Web 應用:
閱讀過 SSR 原理一文可看到配置支持服務端渲染還是挺麻煩的,但借助 Next.js,可以很輕松的上手改造支持現有 Web 應用服務端渲染。
是否采用服務端渲染還得綜合考慮收益,服務端渲染畢竟會增加服務器的計算開銷,穩定性相較于 CSR 差一些。
三、創建 Next.js 應用
初始化一個 Next.js 應用可以直接通過腳手架快速完成:
- npx create-next-app@latest --ts
- # or
- yarn create next-app --typescript
中途會要求輸入項目名,并自動安裝所需的模塊
執行 yarn dev 后需要手動再瀏覽器打開網址:http://localhost:3000 ,即可看到如下頁面:
首頁的內容對應 ./pages/index.tsx 文件
初始的目錄結構如下:
- .
- ├── pages // 采用約定式路由(文件系統路由)
- │ ├── _app.tsx
- │ ├── api // API 目錄
- | ├── hello.ts
- │ └── index.tsx // 首頁
- ├── public // 公共資源
- │ ├── favicon.ico
- │ └── vercel.svg
- ├── styles // 樣式
- │ ├── Home.module.css
- │ └── globals.css
- ├── next-env.d.ts // Next 相關的 TS 定義
- ├── next.config.js // Next.js 自定義配置
- ├── node_modules
- ├── package.json
- ├── tsconfig.json
- ├── README.md
- └── yarn.lock
四、頁面路由
通常我們的 Web 應用是多頁面、多路由的,因此會涉及到在各個頁面之間跳轉,因此有必要熟悉 Next.js 的路由使用方式。
上述講到了 Next.js 是約定式路由,基于文件系統,對應到 ./pages 目錄下,當添加頁面文件到 ./pages 目錄,Next.js 會自動識別并將對應文件注冊的路由上
4.1 索引路由
Next.js 會自動將文件夾內的 “index” 文件注冊為文件夾的主頁
4.2 嵌套路由
Next.js 支持嵌套文件的路由,如果您創建嵌套文件夾結構,文件仍將自動以相同方式路由解析。
4.3 動態參數路由
常見于比如博客的文章詳情頁面,文章的 id 是動態變化的,Next.js 中可以使用中括號解析到對應的命名參數
更多關于動態路由的解析可參閱:https://nextjs.org/docs/routing/dynamic-routes
4.4 路由跳轉
之前有提到 Next.js 中的路由預加載功能,需借助 Next.js 提供的 next/link,寫法如下:
- <Link href="/blog/hello-world">
- <a>第一篇文章</a>
- </Link>
應用頁面之間的跳轉,可以用 標簽包裹。
屬性 href 的值是跳轉頁面的路徑字符串或 URL 對象:
- import Link from 'next/link'
- function Articles({ articles }) {
- return (
- <ul>
- {articles.map((article) => (
- <li key={article.id}>
- <Link
- href={{
- pathname: '/article/[slug]',
- query: { slug: article.slug },
- }}
- >
- <a>{article.title}</a>
- </Link>
- </li>
- ))}
- </ul>
- )
- }
- export default Articles
如有需要對路由通過 js 跳轉,則可以通過 Next.js 提供的 next/router[3] 中的 useRouter[4] Hook。
4.5 代碼拆分和預加載
通過 Next.js 的路由功能,可以自動完成頁面按需加載當前頁面所需的代碼,同時會自動預加載頁面中屬于自身應用的鏈接。
這意味著在呈現主頁時,最初不會提供其他頁面的代碼,同時可確保即使您有數百個頁面,主頁也能按需快速加載。
僅加載您請求的頁面的代碼也意味著頁面變得獨立,如果某個頁面拋出錯誤,應用程序的其余部分仍然可以工作。
在 Next.js 的生產版本中,每當 Link 組件出現在瀏覽器的視口中時,Next.js 都會在后臺自動預取鏈接頁面的代碼。當您單擊鏈接時,目標頁面的代碼已在后臺加載,頁面轉換將近乎即時。
五、靜態資源
所有靜態資源都可以放到 ./public 目錄下,Next.js 會自動為其中的文件注冊路由,按照文件系統的方式,與 Page 的路由類似。
5.1 圖片元素
一般網頁中的圖片寫法如下:
- <img src="/images/logo.png" alt="logo" />
但這種寫法會需要開發者手動去優化,比如按需加載、錯誤處理等。
Next.js 考慮到這點,為了減輕開發者負擔,于是提供了 next/image[5],開箱即用。
這里其實可以借鑒一下,別的項目中為了業務統一處理圖片,可以封裝一個 Image 組件,提升研發效率。
- import Image from 'next/image'
- const YourComponent = () => (
- <Image
- src="/images/profile.jpg" // 圖片文件路徑
- height={144} // 具有正確縱橫比的所需尺寸
- width={144}
- alt="Image Alt"
- />
- )
- export default YourComponent;
5.2 Meta 數據
網頁的 Meta 數據,也就是在 html->head 標簽中的內容
Next.js 提供了 next/head[6] 用于聲明式編寫網頁的 head 內容。
- import Link from 'next/link'
- import Head from 'next/head'
- export default function FirstPost() {
- return (
- <>
- <Head>
- <meta charset="UTF-8" />
- <title>First Post</title>
- <link rel="shortcut icon" href="/favicon.ico" />
- <meta name="keywords" content="網頁關鍵詞" />
- <meta name="description" content="網頁描述" />
- <meta name="author" content="DYBOY,[email protected]" />
- <meta name="version" content="1.0" />
- <link rel="stylesheet" href="//at.alicdn.com/t/font_2319527_hng3o947ocv.css" />
- <link rel="stylesheet" href="/style/fancybox.css" />
- <link rel="stylesheet" href="/style/app.css" />
- <script src="/scripts/jquery.js"></script>
- </Head>
- <h1>First Post</h1>
- <h2>
- <Link href="/">
- <a>Back to home</a>
- </Link>
- </h2>
- </>
- )
- }
此外,若我們有需要修改 的訴求時,可創建pages/_document.js 文件,并通過“自定義文檔[7]”的方式繼承并統一改造所有網頁輸出的公共內容。
5.3 JS 腳本文件
例如我們使用了三方庫 Jquery,雖然可以直接在 組件中直接寫:
- <script src="/scripts/jquery.js"></script>
但是,這種方式包含腳本并不能明確說明何時加載同一頁面上獲取的其他 JavaScript 代碼。如果某個特定腳本會阻塞渲染并且會延遲頁面內容的加載,則會顯著影響性能。
因此,可以通過 next/script[8] 來優化
- import Link from 'next/link'
- import Head from 'next/head'
- import Script from 'next/script'
- export default function FirstPost() {
- return (
- <>
- <Head>
- <title>First Post</title>
- </Head>
- <Script
- src="/scripts/jquery.js"
- strategy="lazyOnload" // 設置 js 加載的方式
- onLoad={() =>
- // js 腳本文件加載完成后的回調函數
- console.log(`script loaded correctly`)
- }
- />
- <h1>First Post</h1>
- <h2>
- <Link href="/">
- <a>返回首頁</a>
- </Link>
- </h2>
- </>
- )
- }
5.4 CSS 文件
Next.js 已經內置支持了 CSS 和 SASS,允許開發者引入 .css 和 .sass 文件方式引入樣式文件,同時還支持 Tailwind CSS。
需要手動安裝 SASS 模塊
- yarn add sass
默認還支持 CSS-in-JS,借助 styled-jsx[9] 這個模塊,可以直接在 React 組件中直接寫 CSS,同時限制作用域,不會影響其他組件。
如果需要 CSS 模塊化[10],那么 CSS 文件命名應當為 *.module.css 格式。
- import styles from './layout.module.css'
- export default function Layout({ children }) {
- return <div className={styles.container}>{children}</div>
- }
全局 CSS 注入,則在根目錄的 ./styles 目錄編寫,同時也僅在 ./pages/_app.tsx 文件中引入全局樣式文件
- import '../styles/globals.css' // 引入全局樣式
- import type { AppProps } from 'next/app'
- function MyApp({ Component, pageProps }: AppProps) {
- return <Component {...pageProps} />
- }
- export default MyApp
Next.js 使用 PostCSS[11] 編譯 CSS,自定義配置 PostCSS 的方式可參考:【自定義 PostCSS 配置[12]】
六、預渲染和數據獲取
Next.js 支持:
- 在服務端預渲染
- 靜態頁面生成和服務端渲染
- 有數據和無數據的靜態生成
- 一些預定義的方法(生命周期函數)注入數據
6.1 預渲染
默認情況下,Next.js 預渲染每個頁面。這意味著 Next.js 會提前為每個頁面生成 HTML,預渲染可以帶來更好的性能和SEO。
每個生成的 HTML 都與該頁面所需的最少 JavaScript 代碼相關聯。當瀏覽器加載頁面時,其 JavaScript 代碼會運行并使頁面完全交互。
預渲染和無預渲染的對比如下:
6.2 靜態生成和服務端渲染
Next.js 支持兩種形式的預渲染方式:靜態生成和服務端渲染
- 靜態生成: 在構建時生成 HTML 的預渲染方法。然后在每個請求上重用預渲染的 HTML。
- 服務器端渲染: 在每個請求上生成 HTML 的預渲染方法。
6.3 獲取數據
(1)靜態生成時獲取數據
在服務端構建生成靜態頁面之前,有時候需要獲取一些數據,可以借助 getStaticProps 方法。
在頁面組件內,同時導出一個 getStaticProps 方法:
- export default function HomePage(props) { ... }
- // 導出異步獲取數據方法
- export async function getStaticProps() {
- // 獲取數據,例如從數據庫、API、文件等
- const data = ...
- // 返回的參數將會按照 key 值賦值到 HomePage 組件的同名入參中
- return {
- props: ...
- }
- }
注意,僅在頁面組件內導出該方法
(2)服務端渲染時獲取數據
比如用戶的個人中心頁面,該頁面時不需要 SEO 優化的,其數據通常需要實時更新獲取,因此采用 SSR 的方式,而 SSR 在服務端獲取數據可以借助 getServerSideProps 方法
和構建時獲取數據方法類似:
- export default function HomePage(props) { ... }
- // 導出異步獲取數據方法
- export async function getServerSideProps() {
- // 獲取數據,例如從數據庫、API、文件等
- const data = ...
- // 返回的參數將會按照 key 值賦值到 HomePage 組件的同名入參中
- return {
- props: ...
- }
- }
(3)客戶端渲染時獲取數據
如果不需要“預渲染”時候獲取數據,即不需要“靜態生成”和“服務端渲染”的時候獲取數據,則可以在對應頁面組件代內,編寫網絡請求相關代碼。
Next.js 團隊提供了一個基于 React Hooks 的 useSWR 鉤子,推薦使用,該鉤子會處理緩存、重新驗證、焦點跟蹤、間隔重新獲取等。
一個簡單的示例如下:
- import useSWR from 'swr'
- function Profile() {
- const { data, error } = useSWR('/api/user', fetch)
- if (error) return <div>failed to load</div>
- if (!data) return <div>loading...</div>
- return <div>hello {data.name}!</div>
- }
和一些封裝的請求 Hooks 類似,useSWR 還支持自定義請求庫,默認使用的是 fetch 的 pollyfill 模塊(unfetch[13]),提供的中文官方的文檔也非常清晰,地址:https://swr.vercel.app/zh-CN/docs/getting-started
七、動態路由
上面講到了預渲染,如果是動態路由的預渲染該如何處理?這里需要依賴方法 getStaticPaths 獲得動態路由需要生成頁面列表。
- // ./pages/post/[id].tsx
- import Layout from '../../components/layout'
- export default function Post({id, article}) {
- return (
- <Layout>
- <Head>
- <title>{article.title}</title>
- </Head>
- {article.title}
- <br />
- {id}
- <br />
- {article.date}
- </Layout>
- )
- }
- export async function getStaticPaths() {
- // 返回所有可能的文章 id 所對應的列表
- const paths = [
- {
- params: {
- id: 'ssg-SSR'
- }
- },
- {
- params: {
- id: 'pre-rendering'
- }
- }
- ]
- return {
- paths,
- fallback: false, // 如果在 paths 中 id 找不到對應值,則指向 404 頁面
- }
- }
- export async function getStaticProps({ params }) {
- // 通過 params.id 獲取必要的文章數據
- // parmas 即路由中的參數對象
- const article = getContentById(parmas.id);
- return {
- props: {
- id,
- artcile,
- }
- }
- }
關于 fallback 可以參閱:fallback props[14]
自建一個 404 頁面,文件路徑為:./pages/404.tsx
- export default function Custom404() {
- return <h1>404 - Page Not Found</h1>
- }
八、BFF API
在初始化的目錄結構中的 ./pages/api/hello.ts 文件,就是一個 API 頁面,他的路由和頁面路由相同
- import type { NextApiRequest, NextApiResponse } from 'next'
- type Data = {
- name: string
- }
- export default function handler(
- req: NextApiRequest,
- res: NextApiResponse<Data>
- ) {
- res.status(200).json({ name: 'John Doe' })
- }
在 ./pages/api/ 目錄下,前端開發者編寫人意的 API 應用,也就是被稱為 Serverless Functions,類似于字節的“輕服務[15]”
九、部署
官方推薦使用 Vercel[16] 來完成一鍵自動化構建部署
首先執行構建,構建時候會自動做相關優化
- yarn build
- // 實際執行
- next build
然后是啟動服務:
- yarn start
- // 實際執行
- next start -p 8080
在生產環境,再用 PM2[17] 管理守護進程
然后使用 Nginx 作為網關,配置域名,SSL,映射到本地 8080 端口即可。
拓展更多
Next.js 還有更多細節和 API,需要深入了解的小伙伴可以參閱:Next.js API文檔[18]
除了 Next.js,還有 Razzle.js[19] 也可以學習下。
推薦閱讀:
- 《徹底理解服務端渲染 - SSR原理》
- 《魅族官網基于 next.js 重構實踐總結與分享[20]》
- 《SWR - 用于數據請求的 React Hooks 庫[21]》
- 《react 服務端(SSR) 框架next.js開發個人網站分享[22]》
- 《Next.js 應用開發實踐[23]》
總結
通過對 Next.js 的初步上手使用,SSR 確實有助于提升用戶的體驗,比如一些文檔網站、官網、營銷網頁,個人非常推薦這種方式,但其缺點也很明顯,服務端的穩定性會有所降低,穩定性可以通過增加成本提高,相較于其優點,還是值得投入的!
Next.js 把一些生產配置初始化就構建完成,對于開發者來說,開箱即用的感覺真的太棒了!
參考資料
[1]什么是前端的同構渲染?: https://www.zhihu.com/question/325952676
[2]Next.js: https://nextjs.org/
[3]next/router: https://nextjs.org/docs/api-reference/next/link
[4]useRouter: https://nextjs.org/docs/api-reference/next/router#userouter
[5]next/image: https://nextjs.org/docs/api-reference/next/image
[6]next/head: https://nextjs.org/docs/api-reference/next/head
[7]自定義文檔: https://nextjs.org/docs/advanced-features/custom-document
[8]next/script: https://nextjs.org/docs/api-reference/next/script
[9]styled-jsx: https://github.com/vercel/styled-jsx
[10]CSS 模塊化: https://nextjs.org/docs/basic-features/built-in-css-support#adding-component-level-css
[11]PostCSS: https://postcss.org/
[12]自定義配置 PostCSS: https://nextjs.org/docs/advanced-features/customizing-postcss-config
[13]unfetch: https://www.npmjs.com/package/unfetch
[14]fallback props: https://nextjs.org/docs/basic-features/data-fetching#the-fallback-key-required
[15]輕服務: https://qingfuwu.cn/
[16]Vercel: https://vercel.com/
[17]PM2: https://pm2.keymetrics.io/
[18]Next.js API文檔: https://nextjs.org/docs/api-reference/cli
[19]Razzle.js: https://razzlejs.org/
[20]魅族官網基于 next.js 重構實踐總結與分享: https://zhuanlan.zhihu.com/p/113853235
[21]SWR - 用于數據請求的 React Hooks 庫: https://swr.vercel.app/zh-CN/docs/getting-started
[22]react 服務端(SSR) 框架next.js開發個人網站分享: http://www.liuweibo.cn/p/206
原文鏈接:https://mp.weixin.qq.com/s/7c68o90sRJBgTJpwTEi9RQ