react,next.js, getInitialProps 自動切換服務端渲染和瀏覽器渲染


我們已經知道了服務器端渲染的原理,你只需要搭建一個 Express 服務器,在服務器端手工打造『脫水』,在瀏覽器端做『注水』,完成某個頁面的服務器端渲染並不難。

不過,服務器端渲染的問題並不這么簡單,一個最直接的問題,就是怎么處理多個頁面的『單頁應用』(Single-Page-Application)?

所以單頁應用,就是雖然用戶感覺有多個頁面,但是實現上只有一個頁面,用戶感覺到頁面可以來回切換,但其實只是一個頁面並沒有完全刷新,只是局部界面更新而已。

假設一個單頁應用有三個頁面 Home、Prodcut 和 About,分別對應的的路徑是 /home、/product和 /about,而且三個頁面都依賴於 API 調用來獲取外部數據。

現在我們要做服務器端渲染,如果只考慮用戶直接在地址欄輸入 /home、/product 和 /about 的場景,很容易滿足,按照上面說的套路做就是了。但是,這是一個單頁應用,用戶可以在 Home 頁面點擊鏈接無縫切換到 Product,這時候 Product 要做完全的瀏覽器端渲染。換句話說,每個頁面都需要既支持服務器端渲染,又支持完全的瀏覽器端渲染,更重要的是,對於開發者來說,肯定不希望為了這個頁面實現兩套程序,所以必須有同時滿足服務器端渲染和瀏覽器端渲染的代碼表示方式。

讀者可以思考一下什么樣的代碼表示合適,也可以直接往下,看看業界公認最科學的實現方式 Next.js 是如何做的。

快速創建 Next.js 項目
在說明 Next.js 的工作原理之前,我們先看怎么快速創建 Next.js 項目,這個問題用代碼來說明會更順暢。

我們也可以手工創建 Next.js 項目,不過更簡單的方式是用自動化工具 create-next-app,這個 create-next-app 類似於 create-react-app,一個命令就創建一個可以運行的應用。

首先安裝 create-next-app。

npm install -g create-next-app
然后,就可以在你專門存放項目的目錄下執行 create-next-app,產生一個使用 Next.js 的 React 應用,下面的命令創建一個叫 next_demo 的應用:

create-next-app next_demo
進入新生成的項目目錄 next_demo 里檢查一下,可以看到文件結構非常簡潔,pages 目錄下是頁面文件,package.json 中差不是下面這樣,沒有繁冗的 webpack 和 babel 依賴包,因為一切都被 Next.js 封裝起來了。

{
"name": "create-next-example-app",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "^6.0.3",
"react": "^16.5.2",
"react-dom": "^16.5.2"
}
}
雖然有不少框架都表示自己的功能很強大,但其中有很多框架的設計並不中立,用這些框架去開發某些特定應用或許還行,如果放到一個更大范圍的應用類型中,就會發現無法滿足要求,這樣的框架通用性不足,開發者一定要謹慎使用。

講良心話,Next.js 真的是一個通用性非常高的框架,因為 Next.js 完全遵從了 React 的技術哲學:一切皆為組件。

在 Next.js 中,創造一個頁面,其實就是創造一個 React 組件,接下來我們看看如何創建一個頁面。

編寫頁面
使用下面的命令啟動 Next.js 應用,進入的是開發者模式,這時候對代碼的改變,會立刻體現在網頁上。

npm run dev
請注意,這一點上 Next.js 的習慣用法和 create-react-app 產生的應用不一樣。在 create-react-app 產生的應用中, npm run start 啟動是開發者模式,但在 Next.js 應用中,習慣上 npm rum start 以產品模式啟動,所以要先運行 npm run build 然后才能運行 npm run start。

Next.js 遵從『協定優於配置』(convention over configuration)的設計原則,根據『協定』,在 pages 中每個文件對應一個網頁文件,文件名對應的就是網頁的路徑名,比如 pages/home.js 文件對應的就是 /home 路徑的頁面,當然 pages/index.js 比較特殊,對應的是默認根路徑 / 的頁面。

我們修改 pages/index.js,讓它更簡單一些,如下:

import React from 'react'

const Home = (props) => (
<h1>
Hello World
</h1>
)

export default Home
這樣會在頁面上顯示出一個 Hello World,而這個頁面代碼就是一個普通的 React 組件而已。

頁面都是 React 組件,這就是 Next.js 的哲學。

getInitialProps
我們還是要回到本來的話題,如何優雅地實現服務器端渲染,上面的 Home 頁面雖然能夠渲染出完整包含 Hello World 的 HTML,但是並沒有調用任何外部 API 資源,所以也沒有異步操作,並不能體現服務器端渲染的難度。

我們用一個函數來實現異步操作,以此模擬調用 API 的延遲效果,如下:

const timeout = (ms, result) => {
return new Promise(resolve => setTimeout(() => resolve(result), ms));
};

然后,我們利用這個 timeout 來獲得展示網頁所需的數據。比如說,獲取用戶名,那么我們的 Home 組件就要換一個寫法,像下面那樣,增加 getInitialProps 的定義:

const Home = (props) => (
<h1>
Hello {props.userName}
</h1>
)

Home.getInitialProps = async () => {
return await timeout(200, {userName: 'Morgan'});
};
這個 getiInitialProps 是 Next.js 最偉大的發明,它確定了一個規范,一個頁面組件只要把訪問 API 外部資源的代碼放在 getInitialProps 中就足夠,其余的不用管,Next.js 自然會在服務器端或者瀏覽器端調用 getInitialProps 來獲取外部資源,並把外部資源以 props 的方式傳遞給頁面組件。

注意 getInitialProps 是頁面組件的靜態成員函數,可以用下面的方法定義:

Home.getInitialProps = async () = {...};
也可以在組件類中加上 static 關鍵字定義:

class Home extends React.Component {
static async getInitialProps() {
...
}
}
通過上面的代碼,我么也可以注意到,getInitialProps 是一個 async 函數,所以,在 getInitialProps 函數中可以使用 await 關鍵字,用同步的方式編寫異步邏輯。

我們可以這樣來看待 getInitialProps,它就是 Next.js 對代表頁面的 React 組件生命周期的擴充。React 組件的生命周期函數缺乏對異步操作的支持,所以 Next.js 干脆定義出一個新的生命周期函數 getInitialProps,在調用 React 原生的所有生命周期函數之前,Next.js 會調用 getInitialProps 來獲取數據,然后把獲得數據作為 props 來啟動 React 組件的原生生命周期過程。

這個生命周期函數的擴充十分巧妙,因為:

沒有侵入 React 原生生命周期函數,以前的 React 組件該怎么寫還是怎么寫;
getInitialProps 只負責獲取數據的過程,開發者不用操心什么時候調用 getInitialProps,依然是 React 哲學的聲明式編程方式;
getInitialProps 是 async 函數,可以利用 JavaScript 語言的新特性,用同步的方式實現異步功能。
Next.js 的“脫水”和“注水”
我們說過服務器端渲染的關鍵是如何“脫水”和“注水”,如果你對 Next.js 如何實現這兩個關鍵點好奇(實際上你確實應該感到好奇),那么在瀏覽器中使用“顯示網頁源代碼”就可以讓你一目了然。

在網頁的 HTML 中,可以看到類似下面的內容:

<script>
__NEXT_DATA__ = {
"props":{
"pageProps": {"userName":"Morgan"}},
"page":"/","pathname":"/","query":{},"buildId":"-","assetPrefix":"","nextExport":false,"err":null,"chunks":[]}
</script>
Next.js 在做服務器端渲染的時候,頁面對應的 React 組件的 getInitialProps 函數被調用,異步結果就是“脫水”數據的重要部分,除了傳給頁面 React 組件完成渲染,還放在內嵌 script 的 __NEXT_DATA__ 中,這樣,在瀏覽器端渲染的時候,是不會去調用 getInitialProps 的,直接通過 __NEXT_DATA__ 中的“脫水”數據來啟動頁面 React 組件的渲染。

這樣一來,如果 getInitialProps 中有調用 API 的異步操作,只在服務器端做一次,瀏覽器端就不用做了。

那么,getInitialProps 什么時候會在瀏覽器端調用呢?

當在單頁應用中做頁面切換的時候,比如從 Home 頁切換到 Product 頁,這時候完全和服務器端沒關系,只能靠瀏覽器端自己了,Product頁面的 getInitialProps 函數就會在瀏覽器端被調用,得到的數據用來開啟頁面的 React 原生生命周期過程。

關鍵點是,瀏覽器可能會直接訪問 /home 或者 /product,也可能通過網頁切換訪問這兩個頁面,也就是說 Home 或者 Product 都可能被服務器端渲染,也可能完全只有瀏覽器端渲染,不過,這對應用開發者來說無所謂,應用開發者只要寫好 getInitialProps,至於調用 getInitialProps 的時機,交給 Next.js 處理就好了。

你可以發明自己的服務器端框架,但很可能最后你發現,如果要做得通用性好,最后都會做到和 Next.js 一樣的模式上來。

值得一提的是,getInitialProps 返回的應該是“純數據”,也就是不要返回一個定制類的實例。比如,有一個類 Foo 有一個成員函數 bar,不要在 getInitialProps 返回一個 Foo 實例。不然,經過“脫水”和“注水”過程,網頁組件獲得的那個“Foo 實例”不再是你想的那個 Foo 實例了,它變成了一個純粹的數據,不會包含成員函數 bar的。
---------------------
作者:前端工程師
來源:CSDN
原文:https://blog.csdn.net/gwdgwd123/article/details/85030708 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM