偷師 Next.js:我學(xué)到的 6 個(gè)設(shè)計(jì)技巧
本文轉(zhuǎn)載自微信公眾號(hào)「前端向后」,作者黯羽輕揚(yáng) 。轉(zhuǎn)載本文請(qǐng)聯(lián)系前端向后公眾號(hào)。
本文記錄了我從中發(fā)現(xiàn)的設(shè)計(jì)技巧,包括 API 設(shè)計(jì)、文檔設(shè)計(jì)、框架設(shè)計(jì)等,也分享給你
定義基類(lèi),可能不如定義模塊
首先,類(lèi)(Class)和模塊(Module)都是組織代碼的可選方式,放到 API 設(shè)計(jì)的場(chǎng)景,都能用來(lái)約束寫(xiě)法,暴露框架能力。而在模塊概念成為正統(tǒng)之前,前端框架大多提供基類(lèi)來(lái)滿足這種需要,因?yàn)闆](méi)得選
典型的,React 通過(guò)React.Component基類(lèi)暴露出各種生命周期 Hook,同時(shí)定義了組件寫(xiě)法:
- // Components
- class Clock extends React.Component {
- // Props
- constructor(props) {
- super(props);
- // State
- this.state = {date: new Date()};
- }
- // Lifecycle
- componentDidMount() { }
- componentWillUnmount() { }
- render() {
- // Template
- return (
- <div>
- <h1>Hello, world!</h1>
- <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
- </div>
- );
- }
將 Props、State、Lifecycle、Template 等框架能力整合成一個(gè) Class,稱之為組件。并且,在很長(zhǎng)的一段時(shí)間里,React 中能稱為組件的只有 Class
這段很長(zhǎng)的時(shí)間有多長(zhǎng)?
從 React 誕生之初一直到React Hooks推出并進(jìn)化成完全形態(tài)。目前(2021/1/2)React Hooks 仍然不是完全形態(tài),componentDidCatch、getSnapshotBeforeUpdate、getDerivedStateFromError等特性還不健全,具體見(jiàn)Do Hooks cover all use cases for classes?
也就是說(shuō),時(shí)至今日,React Components 仍等價(jià)于 Class Components,早期的函數(shù)式組件只能叫 Stateless Components,獲得 Hooks 加持之后的函數(shù)式組件雖然擺脫了 Stateless,但與完全形態(tài)的 Class Components 還有一點(diǎn)點(diǎn)差距
將 Components 概念與 Class 強(qiáng)綁定在一起真是個(gè)糟糕的選擇,被寄予厚望的 Hooks 充分說(shuō)明了這一點(diǎn)。但 Props、State、Lifecycle、Template 這些框架能力又總要有東西來(lái)承載,那么,更好的選擇是什么呢?
可能是 Module。強(qiáng)調(diào)可能,是因?yàn)閮H在組織代碼這一點(diǎn)上,Module 比 Class 更純粹。Module 只組織代碼,將變量、函數(shù)等語(yǔ)法元素圈在一起,而不像 Class 會(huì)強(qiáng)加實(shí)例狀態(tài)、成員方法等額外概念
例如,Next.js 的 Page 定義就只是個(gè)文件模塊:
- // pages/about.js
- function About() {
- return <div>About</div>
- }
- export default About
最簡(jiǎn)單的 Page,只要默認(rèn)暴露出一個(gè) React 組件即可。需要用到更多功能,再按需暴露更多的既定 API:
- // pages/blog.js
- function Blog({ posts }) {
- // Render posts...
- }
- // API 1
- export async function getStaticProps() { }
- // API 2
- export async function getStaticPaths() { }
- // API 3
- export async function getServerSideProps() { }
- // API n
- export async function xxx() { }
- export default Blog
對(duì)比 Class 形式的 API 設(shè)計(jì),這種Module 式 API 設(shè)計(jì)更加純粹,不強(qiáng)加額外語(yǔ)法元素(尤其是 Class 這種根基龐大的語(yǔ)法元素,帶來(lái)一眾super()、bind(this)、static),在某些場(chǎng)景下不失為一種更好的選擇
文件約定路由
Next.js 里沒(méi)有Router.register、沒(méi)有new Route()、也沒(méi)有app.use(),沒(méi)有一切你能想到的路由定義 API
因?yàn)楦緵](méi)有 API,路由采用的是文件路徑約定:
- // 靜態(tài)路由
- pages/index.js → /
- pages/blog/index.js → /blog
- pages/blog/first-post.js → /blog/first-post
- pages/dashboard/settings/username.js → /dashboard/settings/username
- // 動(dòng)態(tài)路由
- 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)
也就是說(shuō),通過(guò)源碼所在文件路徑來(lái)標(biāo)識(shí)路由,甚至還能支持通配符,這么神奇,當(dāng)然要親眼看看源碼目錄才能感受到視覺(jué)沖擊:
- pages
- ├── _app.js
- ├── _document.tsx
- ├── api
- │ ├── collection
- │ │ ├── [id].tsx
- │ │ └── index.tsx
- │ ├── photo
- │ │ ├── [id].tsx
- │ │ ├── download
- │ │ │ └── [id].tsx
- │ │ └── index.tsx
- │ ├── stats
- │ │ └── index.tsx
- │ └── user
- │ └── index.tsx
- ├── collection
- │ └── [slug].tsx
- └── index.tsx
API 之間的無(wú)縫聯(lián)動(dòng)
通過(guò)前兩篇文章,我們知道 Next.js 要解決的問(wèn)題是預(yù)渲染,圍繞預(yù)渲染探索出了 SSG、SSR 兩種渲染模式,并在此基礎(chǔ)上支持了包括 CSR 在內(nèi)的不同渲染模式混用:
- ISR(Incremental Static Regeneration):增量靜態(tài)再生成,運(yùn)行時(shí)定期重新生成靜態(tài) HTML
- SSG 降級(jí) SSR:未命中預(yù)先生成的靜態(tài) HTML 時(shí),立即進(jìn)行 SSR
- SSR 帶靜態(tài)緩存:SSR 完成之后,將結(jié)果緩存起來(lái),下次命中靜態(tài)緩存直接返回(相當(dāng)于 SSG)
- SSG 結(jié)合 CSR:編譯時(shí)生成靜態(tài)部分(頁(yè)面外框),CSR 填充動(dòng)態(tài)部分(頁(yè)面內(nèi)容)
- SSR 聯(lián)動(dòng) CSR:URL 直接訪問(wèn)走更快的 SSR,SPA 跳轉(zhuǎn)過(guò)來(lái)走體驗(yàn)更優(yōu)的 CSR
從 API 設(shè)計(jì)的角度乍一看,似乎需要給每種組合取個(gè)別致的名字,并暴露出專(zhuān)門(mén)的 API,就像 SSGwithFallback、SSRwithStaticCache、PartialSSG、SPAMode…
然而,Next.js 不僅支持了所有這些混用特性,而且沒(méi)有增加任何頂層 API,它的做法是增加一些選項(xiàng),例如:
- // SSG 基礎(chǔ)款
- export async function getStaticProps(context) {
- return {
- props: {}, // will be passed to the page component as props
- }
- }
- // SSG 變身 ISR,給返回值添上 revalidate 屬性
- export async function getStaticProps(context) {
- return {
- props: {}, // will be passed to the page component as props
- // Next.js will attempt to re-generate the page:
- // - When a request comes in
- // - At most once every second
- revalidate: 1, // In seconds
- }
- }
- // SSG 感知路由的高級(jí)款,實(shí)現(xiàn)了 getStaticPaths
- export async function getStaticPaths() {
- return {
- paths: [
- { params: { ... } } // See the "paths" section below
- ],
- fallback: false
- };
- }
- // SSG 變身 SSR帶靜態(tài)緩存,fallback選項(xiàng)改為true
- export async function getStaticPaths() {
- return {
- paths: [
- { params: { ... } } // See the "paths" section below
- ],
- fallback: true
- };
- }
- // SSG 變身 SSG降級(jí)SSR,fallback選項(xiàng)改為'blocking'
- export async function getStaticPaths() {
- return {
- paths: [
- { params: { ... } } // See the "paths" section below
- ],
- fallback: 'blocking'
- };
- }
這種基于細(xì)分選項(xiàng)的 API 聯(lián)動(dòng)用起來(lái)更輕量,始終保持帶給用戶的漸進(jìn)式體感,不需要一上來(lái)就了解全部 API、相關(guān)設(shè)計(jì)概念,從頂層區(qū)分我的場(chǎng)景屬于哪類(lèi),該用哪個(gè) API,而是隨著場(chǎng)景的深入,發(fā)現(xiàn)那個(gè)最合適的 API/選項(xiàng)就在那里
能從文檔夠明顯地感受到這種差異,例如,Next.js 介紹 ISR 的地方將用戶指引到與之關(guān)聯(lián)的 SSR 帶靜態(tài)緩存模式:
Incremental Static Regeneration
With getStaticProps you don’t have to stop relying on dynamic content, as static content can also be dynamic. Incremental Static Regeneration allows you to update existing pages by re-rendering them in the background as traffic comes in.
This works perfectly with fallback: true. Because now you can have a list of posts that’s always up to date with the latest posts, and have a blog post page that generates blog posts on-demand, no matter how many posts you add or update.
積分、互動(dòng)式新手教程
這一點(diǎn)算作文檔設(shè)計(jì)技巧(文檔,當(dāng)然也要有設(shè)計(jì)),看過(guò)許多官方文檔/教程,留下深刻印象的只有 3 個(gè):
- Redux 文檔:故事性文檔,手把手一點(diǎn)點(diǎn)把 redux 設(shè)計(jì)出來(lái),讀起來(lái)根本停不下來(lái)
- Electron Demo App:交互式文檔,準(zhǔn)確地說(shuō)是帶完整文檔的 Demo,在體驗(yàn) Demo App 的同時(shí)了解相關(guān)特性用法,是比React 在做中學(xué)更偷懶的辦法了
- Next.js 教程:積分、互動(dòng)式新手教程,幾十頁(yè)的教程一口氣看完
P.S.Redux 文檔指的是2017 年的版本,現(xiàn)在貌似改過(guò)許多版,讀著很差勁了(這么點(diǎn)兒概念怎么能整出來(lái)那么多文檔)
積分、互動(dòng)式新手教程威力大到什么程度?
讓我能在困到迷糊的狀態(tài)下堅(jiān)持看完教程的全部?jī)?nèi)容,答對(duì)所有測(cè)試題目,積滿 500 分(當(dāng)然,不用幻想,全對(duì)是沒(méi)有任何獎(jiǎng)勵(lì)的),事后回想起來(lái)也覺(jué)得不可思議,其中的技巧在于:
- 教程與文檔分離:導(dǎo)航欄一級(jí)菜單明確區(qū)分 Docs 與 Learn,教程中的部分概念有鏈到文檔,但不看完全也完全跟得上
- 積分:教程醒目位置置頂展示獲得積分,每點(diǎn)一篇加分
- 互動(dòng):關(guān)鍵章節(jié)有測(cè)試題,答對(duì)題目也加分,總積分可分享社交平臺(tái)(Twitter)
如此看來(lái),在文檔中融入少量在線教育的成熟模式,可能效果極佳
默認(rèn)提供最佳實(shí)踐
讀過(guò)體驗(yàn)科技與好的產(chǎn)品,對(duì)其中玉伯提出的默認(rèn)好用印象很深,而 Next.js 算是默認(rèn)好用在框架設(shè)計(jì)上的一個(gè)真實(shí)案例
例如:
- Link 自動(dòng)預(yù)加載
- Image 自動(dòng)懶加載
- “自動(dòng)”采用最佳渲染模式:這個(gè)自動(dòng)不同于前兩個(gè),強(qiáng)調(diào)的是框架角度對(duì)用戶按需使用特性的回應(yīng),由框架來(lái)判斷渲染模式(該走 SSR 還是 SSG),而無(wú)需用戶顯式指定/切換
從生產(chǎn)活動(dòng)的角度來(lái)看,最佳實(shí)踐本就應(yīng)該是默認(rèn)提供的,將新出現(xiàn)的最佳實(shí)踐不斷地下沉到環(huán)境層,就像 npm package、ES Module、Babel 等,如今的前端開(kāi)發(fā)者已經(jīng)幾乎不需要關(guān)心這些曾經(jīng)的最佳實(shí)踐
僅從框架設(shè)計(jì)角度而言,默認(rèn)好用要求在提供最佳實(shí)踐的基礎(chǔ)上更進(jìn)一步,要把最佳實(shí)踐做沒(méi),讓使用者能夠偷懶地以為一切本該如此。因此,最佳實(shí)踐只是一個(gè)臨時(shí)態(tài),尚未形成最佳實(shí)踐的部分才是開(kāi)發(fā)者需要關(guān)心,并體現(xiàn)差異化競(jìng)爭(zhēng)力的地方,一旦形成廣泛認(rèn)同的最佳實(shí)踐,就應(yīng)該沉淀成為默認(rèn)的基礎(chǔ)設(shè)施,開(kāi)發(fā)者無(wú)需關(guān)心即可獲得這些最佳實(shí)踐帶來(lái)的種種好處
從尚未形成最佳實(shí)踐,到提供最佳實(shí)踐,到默認(rèn)提供最佳實(shí)踐,這 3 個(gè)階段可以通過(guò)一個(gè)圖片懶加載的示例來(lái)理解:
- // 第一階段:尚未形成最佳實(shí)踐
- scroll
- IntersectionObserver
- // 業(yè)務(wù)各自實(shí)現(xiàn),不存在用法示例
- // 第二階段:提供最佳實(shí)踐
- React Lazy Load Component
- // 用法示例
- <LazyLoad height={683} offsetTop={200}>
- <img src='http://apod.nasa.gov/apod/image/1502/2015_02_20_conj_bourque1024.jpg' />
- </LazyLoad>
- // 第三階段:默認(rèn)提供最佳實(shí)踐
- next/image
- // 用法示例
- <Image
- src="/me.png"
- alt="Picture of the author"
- layout="fill"
- />
第三階段與第二階段的區(qū)別在于,開(kāi)發(fā)者不必關(guān)心哪個(gè)組件能夠提供懶加載功能(選擇最佳實(shí)踐),直接使用組件庫(kù)中最普通的 Image 組件,該有的功能自然就有,而懶加載只是其中一項(xiàng)
向 Serverless 延伸
Serverless 浪潮之下,前端生態(tài)也正在發(fā)生著一些變化,涌現(xiàn)出各式各樣的一體化應(yīng)用:
- 以前端項(xiàng)目/后端項(xiàng)目為主體的一體化應(yīng)用:如 Midway Serverless,支持集成 React、Vue 等前端項(xiàng)目
- 以 SSR 為主體的一體化應(yīng)用:如 Next.js,支持將 SSR 和數(shù)據(jù)接口(API endpoints)部署成 Serverless Functions
Next.js 提供 SSR 支持,本就需要服務(wù)端環(huán)境,Serverless 的興起很好地解決了 SSR 渲染服務(wù)的運(yùn)維問(wèn)題,因此,其 Vercel 平臺(tái)默認(rèn)支持以 Serverless Functions 的形式部署 SSR 服務(wù)與 API:
Pages that use Server-Side Rendering and API routes will automatically become isolated Serverless Functions. This allows page rendering and API requests to scale infinitely.
諸如此類(lèi)的一體化應(yīng)用雖未形成最佳實(shí)踐,但傳統(tǒng)的前端框架正在歷經(jīng)變革。也許,在未來(lái)的某一天,取而代之的是與 Serverless 技術(shù)充分融合的一體化應(yīng)用框架,Universal 體系大行其道也未可知。
原文鏈接:https://mp.weixin.qq.com/s/F_4yg-0hsX0PSQ1oEOopZg



































