從 Next.js 看企業(yè)級框架的 SSR 支持
本文轉(zhuǎn)載自微信公眾號「前端向后」,作者黯羽輕揚 。轉(zhuǎn)載本文請聯(lián)系前端向后公眾號。
一.Next.js 簡介
The React Framework for Production
面向生產(chǎn)使用的 React 框架(廢話)。提供了好些開箱即用的特性,支持靜態(tài)渲染/服務(wù)端渲染混用、支持 TypeScript、支持打包優(yōu)化、支持按路由預(yù)加載等等:
Next.js gives you the best developer experience with all the features you need for production: hybrid static & server rendering, TypeScript support, smart bundling, route pre-fetching, and more. No config needed.
其中,完善的靜態(tài)渲染/服務(wù)端渲染支持讓 Next.js 在 React 生態(tài)中獨樹一幟
二.核心特性
如果說 Next.js 只做了一件事,那就是預(yù)渲染(Pre-rendering):
By default, Next.js pre-renders every page. This means that Next.js generates HTML for each page in advance, instead of having it all done by client-side JavaScript.
具體的,預(yù)渲染分為兩種方式:
- SSG(Static Site Generation):也叫 Static Generation,在編譯時生成靜態(tài) HTML
- SSR(Server-Side Rendering):也叫 Server Rendering,用戶請求到來時動態(tài)生成 HTML
與 SSR 相比,Next.js 更推崇的是 SSG,因為其性能優(yōu)勢更大(靜態(tài)內(nèi)容可托管至 CDN,性能提升立竿見影)。因此建議優(yōu)先考慮 SSG,只在 SSG 無法滿足的情況下(比如一些無法在編譯時靜態(tài)生成的個性化內(nèi)容)才考慮 SSR、CSR
P.S.CSR、SSR 等更多渲染模式,見前端渲染模式的探索
圍繞核心的預(yù)渲染功能,延伸出了一系列相關(guān)支持,如:
- 路由(文件規(guī)范、API):多頁面的基礎(chǔ)
- 頁面級預(yù)渲染、代碼拆分:順理成章
- 增量靜態(tài)生成:針對大量頁面的編譯時預(yù)渲染(即靜態(tài)生成)策略
- 按路由預(yù)加載:錦上添花
- 國際化(結(jié)合路由):錦上添花
- 集成 Serverless 函數(shù):錦上添花
- 自動 polyfill、自定義head標(biāo)簽:友情贈送
此外,還提供了一些通用場景支持:
- 開箱即用(0 配置)
- TypeScript
- CSS module、Sass
- Fast Refresh(可靠的 Hot Reload 支持)
- 用戶真實數(shù)據(jù)收集分析(頁面加載性能、體驗評分等)
- 帶默認(rèn)優(yōu)化的Image組件
三.路由支持
Next.js 提供了兩種路由支持,靜態(tài)路由與動態(tài)路由
靜態(tài)路由
靜態(tài)路由通過文件規(guī)范來約定,pages目錄下的js文件都認(rèn)為是路由(每個靜態(tài)路由對應(yīng)一個頁面文件),例如:
- pages/index.js → /
- pages/blog/index.js → /blog
- pages/blog/first-post.js → /blog/first-post
- pages/dashboard/settings/username.js → /dashboard/settings/username
動態(tài)路由
類似的,動態(tài)路由也要在pages目錄下創(chuàng)建文件,只是文件名有些不同尋常:
- pages/blog/[slug].js → /blog/:slug (/blog/hello-world)
- pages/[username]/settings.js → /:username/settings (/foo/settings)
- pages/post/[...all].js → /post/* (/post/2020/id/title)
路徑中變化的參數(shù)通過getStaticPaths來填充:
- // pages/posts/[id].js
- export async function getStaticPaths() {
- return {
- // 必須叫paths,值必須是數(shù)組
- paths: [{
- // 每一項必須是這個形式
- params: {
- // 必須含有id
- id: 'ssg-SSR'
- }
- },{
- params: {
- id: 'pre-rendering'
- }
- }],
- fallback: false
- }
- }
進(jìn)一步傳遞給getStaticProps按參數(shù)獲取數(shù)據(jù),并渲染頁面:
- props: {
- postData
- }
- }
- }
- // 渲染頁面
- export default function Post({ postData }) {
- return (
- <Layout>
- <Head>
- <title>{postData.title}</title>
- </Head>
- <article>
- <h1 className={utilStyles.headingXl}>{postData.title}</h1>
- <div className={utilStyles.lightText}>
- <Date dateString={postData.date} />
- </div>
- <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
- </article>
- </Layout>
- )
- }
可以理解為先創(chuàng)建一個工廠 page(例如pages/[路由參數(shù)1]/[路由參數(shù)2].js),接著getStaticPaths填充路由參數(shù),getStaticProps({ params })根據(jù)參數(shù)請求不同數(shù)據(jù),最后數(shù)據(jù)進(jìn)入頁面組件開始預(yù)渲染:
四.SSG 支持
最簡單,同時性能也最優(yōu)的預(yù)渲染方式就是靜態(tài)生成(SSG),把組件渲染工作完全前移到編譯時:
- (編譯時)獲取數(shù)據(jù)
- (編譯時)渲染組件,生成 HTML
將生成的 HTML 靜態(tài)資源托管到 Web 服務(wù)器或 CDN 即可,兼具 React 工程優(yōu)勢與 Web 極致性能
那么首先要解決如何獲取數(shù)據(jù)的問題,Next.js 的做法是將頁面依賴的數(shù)據(jù)集中管理起來:
- // pages/index.js
- export default function Home(props) { ... }
- // 獲取靜態(tài)數(shù)據(jù)
- export async function getStaticProps() {
- // Get external data from the file system, API, DB, etc.
- const data = ...
- // The value of the `props` key will be
- // passed to the `Home` component
- return {
- props: ...
- }
- }
其中,getStaticProps只在服務(wù)端執(zhí)行(根本不會進(jìn)入客戶端 bundle),返回的靜態(tài)數(shù)據(jù)會傳遞給頁面組件(上例中的Home)。也就是說,要求通過getStaticProps提前備好頁面所依賴的全部數(shù)據(jù),數(shù)據(jù) ready 之后組件才開始渲染,并生成 HTML
P.S.注意,只有頁面能通過getStaticProps聲明其數(shù)據(jù)依賴,普通組件不允許,所以要求將整頁依賴的所有數(shù)據(jù)都組織到一處
至于渲染生成 HTML 的部分,借助React 提供的 SSR API即可完成
至此,只要是依賴數(shù)據(jù)有辦法提前獲取到的頁面,理論上都可以編譯生成靜態(tài) HTML,但 2 個問題也隨之而來:
- 數(shù)據(jù)可能會發(fā)生變化,已經(jīng)生成的靜態(tài)頁面需要更新
- 數(shù)據(jù)量可能會多到“永遠(yuǎn)”編譯不完
以電商頁面為例,要把海量商品數(shù)據(jù)全都編譯成靜態(tài)頁面,幾乎是不可能的(或許要編譯一個世紀(jì)那么長),即便都生成了,商品信息也會時不時地更新,靜態(tài)頁面需要重新生成:
If your app has a very large number of static pages that depend on data (think: a very large e-commerce site). You want to pre-render all product pages, but then your builds would take forever.
因此,增量靜態(tài)再生成(Incremental Static Regeneration)應(yīng)運而生
ISR 支持
對于編譯時無法窮舉的海量頁面以及需要更新的場景,Next.js 允許運行時再生成(相當(dāng)于運行時靜態(tài)化):
Incremental Static Regeneration allows you to update existing pages by re-rendering them in the background as traffic comes in.
例如:
- export async function getStaticProps() {
- const res = await fetch('https://.../posts')
- const posts = await res.json()
- return {
- props: {
- posts,
- },
- // 設(shè)置有效期,開啟ISR
- revalidate: 1, // In seconds
- }
- }
revalidate: 1表示運行時(用戶請求打過來時)嘗試重新生成靜態(tài) HTML,1秒最多重新生成一次
運行時靜態(tài)生成需要一些時間(用戶請求等著要 HTML),在此過程中有 3 種選擇:
- fallback: false:不降級,命中尚未生成靜態(tài)頁面的路由直接 404
- fallback: true:降級,命中尚未生成靜態(tài)頁面的路由先返回降級頁面(此時props為空,一般顯示個 loading),靜態(tài)生成 HTML 的同時會生成一份 JSON 供降級頁面 CSR 使用,完成之后瀏覽器拿到數(shù)據(jù)(在客戶端填上props),渲染出完整頁面
- fallback: 'blocking':不降級,并且要求用戶請求一直等到新頁面靜態(tài)生成結(jié)束(實際上就是 SSR,渲染過程是阻塞的,只是完成之后會保留結(jié)果 HTML)
即結(jié)合路由(getStaticPaths)對尚未生成的頁面進(jìn)行降級,例如:
- // pages/index.js
- import { useRouter } from 'next/router'
- function Post({ post }) {
- const router = useRouter()
- // 渲染降級頁面
- if (router.isFallback) {
- return <div>Loading...</div>
- }
- // Render post...
- }
- export async function getStaticPaths() {
- return {
- paths: [{ params: { id: '1' } }, { params: { id: '2' } }],
- // (頁面級)降級策略,true表示遇到尚未生成的先給個降級頁,生成完畢后客戶端自動更新過來
- fallback: true,
- }
- }
P.S.具體見Incremental Static Regeneration、以及The fallback key
然而,并非所有場景都能愉快地在編譯時靜態(tài)生成。典型的,如果組件依賴的數(shù)據(jù)是動態(tài)的,顯然無法在編譯時預(yù)先取得數(shù)據(jù),靜態(tài)生成就無從談起了
五.SSR 支持
對于編譯時無法生成靜態(tài)頁面的場景,就不得不考慮 SSR 了:
區(qū)別于 SSG getStaticProps,Next.js 提供了 SSR 專用的getServerSideProps(context):
- // pages/index.js
- export async function getServerSideProps(context) {
- const res = await fetch(`https://...`)
- const data = await res.json()
- if (!data) {
- return {
- notFound: true,
- }
- }
- return {
- props: {}, // will be passed to the page component as props
- }
- }
同樣用來獲取數(shù)據(jù),與getStaticProps最大的區(qū)別在于每個請求過來時都執(zhí)行,所以能夠拿到請求上下文參數(shù)(context)
六.總結(jié)
圍繞預(yù)渲染如何獲取數(shù)據(jù)的問題,Next.js 探索出了別致的路由支持和精巧的 SSG、SSR 支持。不僅如此,Next.js 還提供了魚和熊掌可以兼得的混用支持,不同渲染模式結(jié)合起來到底有多厲害,且看下篇分解
參考資料
- Pages
- Data Fetching
- Create a Next.js App:教程有點意思
- vercel/next-learn-starter:示例 Demo
原文鏈接:https://mp.weixin.qq.com/s/bS9GHni4ecnz9UFi9RD24Q