點(diǎn)關(guān)注不迷路,建議收藏慢慢讀……
在開始之前我們需要先來(lái)搞清楚一個(gè)問題:什么是服務(wù)端渲染 ?
在以往的概念里,渲染的工作更多的是放在客戶端進(jìn)行的,那么為什么現(xiàn)在我們要讓服務(wù)端來(lái)做這個(gè)工作?
服務(wù)端渲染和客戶端渲染有什么不同之處嗎?
其實(shí)服務(wù)端渲染的工具有很多,看著手冊(cè)很快就能上手,并沒有什么難度,關(guān)鍵在于,我們什么場(chǎng)景下需要使用服務(wù)端渲染,什么樣的渲染方案更適合我們的項(xiàng)目;知其然,知其所以然,我們需要先搞清楚服務(wù)端渲染的基本概念和原理,服務(wù)端渲染為什么會(huì)出現(xiàn),到底解決了我們的什么問題,掌握整體的渲染邏輯和思路,我們才能在學(xué)習(xí)工具使用時(shí),輕松自在,而即便以后工具有了變化和更新,我們也能得心應(yīng)手,不會(huì)再說 “學(xué)不動(dòng)” 了;
這個(gè)邏輯就是所謂的道、法、術(shù)、器的概念;不要僅僅停留在工具的使用和一些工具的奇技淫巧中,更多的要向法、道的層面成長(zhǎng);
什么是 SSR ?
現(xiàn)代化的前端項(xiàng)目,大部分都是單頁(yè)應(yīng)用程序,也就是我們說的 SPA ,整個(gè)應(yīng)用只有一個(gè)頁(yè)面,通過組件的方式,展示不同的頁(yè)面內(nèi)容,所有的數(shù)據(jù)通過請(qǐng)求服務(wù)器獲取后,在進(jìn)行客戶端的拼裝和展示;這就是目前前端框架的默認(rèn)渲染邏輯,我們稱為:客戶端渲染方案( Client Side Render 簡(jiǎn)稱: CSR );
加載渲染過程如下: HTML/CSS 代碼 --> 加載 JavaScript 代碼 --> 執(zhí)行 JavaScript 代碼 --> 渲染頁(yè)面數(shù)據(jù)
SPA 應(yīng)用的客戶端渲染方式,最大的問題有兩個(gè)方面:
1:白屏?xí)r間過長(zhǎng),用戶體驗(yàn)不好;
2:HTML 中無(wú)內(nèi)容,SEO 不友好;
這個(gè)問題的原因在于,首次加載時(shí),需要先下載整個(gè) SPA 腳本程序,瀏覽器執(zhí)行代碼邏輯后,才能去獲取頁(yè)面真正要展示的數(shù)據(jù),而 SPA 腳本的下載需要較長(zhǎng)的等待和執(zhí)行時(shí)間,同時(shí),下載到瀏覽器的 SPA 腳本是沒有頁(yè)面數(shù)據(jù)的, 瀏覽器實(shí)際并沒有太多的渲染工作,因此用戶看到的是沒有任何內(nèi)容的頁(yè)面,不僅如此,因?yàn)轫?yè)面中沒有內(nèi)容,搜索引擎的爬蟲爬到的也是空白的內(nèi)容,也就不利于 SEO 關(guān)鍵字的獲??;
相較于傳統(tǒng)的站點(diǎn),瀏覽器獲取到的頁(yè)面都是經(jīng)過服務(wù)器處理的有內(nèi)容的靜態(tài)頁(yè)面,有過后端編程經(jīng)驗(yàn)的可能會(huì)比較熟悉一些,頁(yè)面結(jié)構(gòu)和內(nèi)容,都是通過服務(wù)器處理后,返回給客戶端;
全宇宙首發(fā)動(dòng)圖,全流程展現(xiàn)
兩相比較我們會(huì)發(fā)現(xiàn),傳統(tǒng)站點(diǎn)的頁(yè)面數(shù)據(jù)合成在后臺(tái)服務(wù)器,而 SPA 應(yīng)用的頁(yè)面數(shù)據(jù)合成在瀏覽器,但是無(wú)論那種,最終的渲染展示,還是交給瀏覽器完成的,所以,不要誤會(huì),我們這里所說的 服務(wù)端渲染 和 客戶端渲染,指的是頁(yè)面結(jié)構(gòu)和數(shù)據(jù)合成的工作,不是瀏覽器展示的工作;
那么能不能借助傳統(tǒng)網(wǎng)站的思路來(lái)解決 SPA 的問題又能夠保留SPA的優(yōu)勢(shì)呢?不管是白屏?xí)r間長(zhǎng)還是 SEO 不友好,實(shí)際都是首屏的頁(yè)面結(jié)構(gòu)先回到瀏覽器,然后再獲取數(shù)據(jù)后合成導(dǎo)致的問題,那么,首屏的頁(yè)面結(jié)構(gòu)和數(shù)據(jù),只要像傳統(tǒng)站點(diǎn)一樣,先在服務(wù)端合成后再返回,同時(shí)將 SPA 腳本的加載依然放到首屏中,此時(shí)返回的頁(yè)面就是結(jié)構(gòu)和數(shù)據(jù)都有的完整內(nèi)容了,這樣瀏覽器在展示首頁(yè)數(shù)據(jù)的同時(shí)也能加載 SPA 腳本,搜索引擎的爬蟲同樣也能獲取到對(duì)應(yīng)的數(shù)據(jù),解決 SEO 的問題;為了更好的理解這個(gè)邏輯,我畫了一個(gè)流程圖:
沒錯(cuò),這就是我們所說的 服務(wù)端渲染的基本邏輯,服務(wù)端渲染也就是 SSR (Server Side Rendering) ;
白屏?xí)r間過長(zhǎng)的問題得以解決,因?yàn)槭状渭虞d時(shí),服務(wù)器會(huì)先將渲染好的靜態(tài)頁(yè)面返回,在靜態(tài)頁(yè)面中再次加載請(qǐng)求 SPA 腳本;
基本原理:首頁(yè)內(nèi)容及數(shù)據(jù),在用戶請(qǐng)求之前生成為靜態(tài)頁(yè)面,同時(shí)加入 SPA 的腳本代碼引入,在瀏覽器渲染完成靜態(tài)頁(yè)面后,請(qǐng)求 SPA 腳本應(yīng)用,之后的頁(yè)面交互依然是客戶端渲染;
明白了其中的原理,也就是到了道、法的境界,接下來(lái),讓我們下凡進(jìn)入術(shù)、器的應(yīng)用層面感受一下;
其中 Vue 框架和 React 框架都有對(duì)應(yīng)的比較成熟的 SSR 解決方案,React對(duì)應(yīng)的是 Next.js 框架,Vue 對(duì)應(yīng)的就是 Nuxt.js,當(dāng)然,如果你對(duì)這些都不感興趣,也可以自己實(shí)現(xiàn)一個(gè) SSR 的服務(wù)端應(yīng)用,我自己之前也寫過一個(gè),如果你感興趣,想看看我實(shí)現(xiàn)的代碼,可以留言給我,回頭做成教程發(fā)出來(lái);
我們以 React 對(duì)應(yīng)的 Next.js 為例,來(lái)具體感受服務(wù)端渲染;
Next.js 框架
中文官網(wǎng)首頁(yè),已經(jīng)介紹的非常清楚了:https://www.nextjs.cn/
基本應(yīng)用
安裝部署
項(xiàng)目安裝命令: npx create-next-app
或 npm init next-app my-next-project
運(yùn)行:npm run dev
命令,啟動(dòng)項(xiàng)目
同時(shí),也可以查看 ./package.json
文件中的腳本配置
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
}
這些腳本涉及開發(fā)應(yīng)用程序的不同階段:
dev
- 運(yùn)行next dev
,以開發(fā)模式啟動(dòng) Next.jsbuild
- 運(yùn)行next build
,以構(gòu)建用于生產(chǎn)環(huán)境的應(yīng)用程序start
- 運(yùn)行next start
,將啟動(dòng) Next.js 生產(chǎn)環(huán)境服務(wù)器
訪問 http://localhost:3000
即可查看我們的應(yīng)用程序了。
頁(yè)面路由
在 Next.js 中,頁(yè)面是被放置在 pages 文件夾中的 Reac 組件,這是框架定義好的;
組件需要被默認(rèn)導(dǎo)出;組件文件中不需要引入 React;頁(yè)面地址與文件地址是對(duì)應(yīng)的關(guān)系;
頁(yè)面(page) 根據(jù)其文件名與路由關(guān)聯(lián)。例如,pages/about.js
被映射到 /about
。
在你的項(xiàng)目中的 pages
目錄創(chuàng)建一個(gè) about.js
。
為 ./pages/about.js
文件填充如下內(nèi)容:
// 類組件需要引入 react 繼承
import React from 'react'
class AboutPage extends React.Component {
render(){
return <h3>class Component -- About Page</h3>
}
}
export default AboutPage
// 函數(shù)組件不需要引入 React
function AboutPage(){
return <h3>Function Component -- About Page</h3>
}
export default AboutPage
頁(yè)面跳轉(zhuǎn)
Link 組件默認(rèn)使用 Javascript 進(jìn)行頁(yè)面跳轉(zhuǎn),即SPA形式的跳轉(zhuǎn) 如果瀏覽器中 Javascript 被禁用,則使用鏈接跳轉(zhuǎn) Link組件中不應(yīng)添加除 href 屬性以外的屬性,其余屬性添加到a標(biāo)簽上 Link組件通過 預(yù)取(在生產(chǎn)中)功能自動(dòng)優(yōu)化應(yīng)用程序以獲得最佳性能
// 引入組件
import Link from 'next/link'
// 函數(shù)組件不需要引入 React
function AboutPage() {
return (
<div>
{/* Link 中必須要寫 a 標(biāo)簽,href 必須寫在 Link 中 */}
<Link href="/list"><a>Go to list Page</a></Link>
<h3>Function Component</h3>
</div>
)
}
export default AboutPage</pre>
靜態(tài)資源
應(yīng)用程序根目錄中的 public 文件夾用于存放靜態(tài)資源; 通過以下形式進(jìn)行訪問: public/img/1.png->/img/1.png
public/css/style.css->/css/style.css
import React from 'react'
class ListPage extends React.Component {
render(){
return <div>
<h3>ListPage</h3>
{/* 引入圖片文件 */}
<img src="/img/1.png" />
</div>
}
}
export default ListPage</pre>
CSS樣式
內(nèi)置 styled-jsx
https://github.com/vercel/styled-jsx#readme
在 Next.js中內(nèi)置了 styled-jsx ,它是一個(gè)CSS-in-JS庫(kù),允許在 React 組件中編寫 CSS,CSS 僅作用于當(dāng)前組件內(nèi)部;
import React from 'react'
class ListPage extends React.Component {
render(){
return <div>
<h3 >ListPage</h3>
<p className="pra"> 這是一個(gè)p標(biāo)簽里面的內(nèi)容 </p>
<style jsx>
{`
.pra{
color: red;
}
`}
</style>
</div>
}
}
export default ListPage</pre>
CSS模塊
通過使用CSS模塊功能,允許將組件的 CSS 樣式編寫在單獨(dú)的 CSS 文件中 CSS 模塊約定樣式文件的名稱必須為: 組件文件名稱.module.css
創(chuàng)建 ./pages/list.module.css
文件并填寫如下內(nèi)容:
.prag{
color:brown;
font-size: 20px;
}
import React from 'react'
// 引入樣式文件
import ListStyle from './list.module.css'
class ListPage extends React.Component {
render(){
return <div>
<h3 >ListPage</h3>
{/* 使用樣式 */}
<p className={ListStyle.prag}> 這是一個(gè)p標(biāo)簽里面的內(nèi)容 </p>
</div>
}
}
export default ListPage</pre>
全局樣式文件
1:在 pages 文件夾中新建 _ app. js
文件(文件名固定) 2:在項(xiàng)目根目錄下創(chuàng)建 styles 文件夾,并在其中創(chuàng)建 global.css 3:在 _app.js
中通過 import 引入 global.css
global.css 中的樣式,將會(huì)全局起作用
/pages/_app.js
文件中的內(nèi)容如下:
import '../styles/globals.css'
// 固定代碼
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />
}
export default MyApp
在新版框架中,已經(jīng)幫我們做好相關(guān)內(nèi)容及文件和代碼
服務(wù)端渲染方法
getServerSideProps() 這個(gè)方法是服務(wù)端渲染的方法,適合頁(yè)面數(shù)據(jù)實(shí)時(shí)變化的應(yīng)用;getServerSideProps() 方法接收一個(gè)參數(shù),是當(dāng)前的 HTTP 客戶端請(qǐng)求對(duì)象;
import React from 'react'
// 類組件
class ListPage extends React.Component {
render(){
return <div>
<h3 >ListPage</h3>
<h4>服務(wù)器數(shù)據(jù):</h4>
{/* 類組件的方式展示數(shù)據(jù)內(nèi)容 */}
<p> {this.props.data[0].name} </p>
</div>
}
}
// // 函數(shù)組件
// function ListPage({data}){
// return (
// <div>
// <h3 >ListPage</h3>
// <h4>服務(wù)器數(shù)據(jù):</h4>
// {/* 類組件的方式展示數(shù)據(jù)內(nèi)容 */}
// <p> {data[1].name} </p>
// </div>
// )
// }
// Node 環(huán)境下執(zhí)行
// 文件讀寫,數(shù)據(jù)庫(kù)鏈接,網(wǎng)絡(luò)通信
// export async function getStaticProps(){
export async function getServerSideProps(){
const res = await fetch('http://localhost:80/');
const backData = await res.json();
const data = JSON.parse(backData);
console.log(data);
return {
props:{data}
}
}
export default ListPage
項(xiàng)目構(gòu)建與運(yùn)行
項(xiàng)目構(gòu)建:npm run build
啟動(dòng)運(yùn)行項(xiàng)目: npm run start
靜態(tài)站點(diǎn)生成
next.js 不僅提供服務(wù)端渲染的方式,同時(shí)還提供了靜態(tài)站點(diǎn)生成的解決方案;
靜態(tài)站點(diǎn)生成也被稱為 SSG(Static Site Generators),就是將應(yīng)用中用到的所以頁(yè)面,全部生成靜態(tài)文件的方案;
next 官方建議在大多數(shù)情況下使用靜態(tài)站點(diǎn)生成,靜態(tài)站點(diǎn)生成方案,更適合 CDN、緩存、內(nèi)容數(shù)據(jù)無(wú)變化的頁(yè)面,比如:宣傳頁(yè)、博客文章、幫助文檔、新聞頁(yè)面、電商產(chǎn)品列表等眾多應(yīng)用場(chǎng)景;
Next.js 中的 getStaticProps 、 getStaticPaths 就是靜態(tài)站點(diǎn)生成;是在構(gòu)建時(shí)生成 HTML 的方法,以后的每個(gè)請(qǐng)求都共用構(gòu)建時(shí)生成好的 HTML;
Next.js 建議大多數(shù)頁(yè)面使用靜態(tài)生成,因?yàn)轫?yè)面都是事先生成好的,一次構(gòu)建,反復(fù)使用,訪問速度快。 服務(wù)器端渲染訪問速度不如靜態(tài)生成快,但是由于每次請(qǐng)求都會(huì)重新渲染,所以適用數(shù)據(jù)頻繁更新的頁(yè)面或頁(yè)面內(nèi)容隨請(qǐng)求變化而變化的頁(yè)面;
在 next.js 中,靜態(tài)生成分為 無(wú)數(shù)據(jù)和有數(shù)據(jù)兩種情況; 如果組件不需要在其他地方獲取數(shù)據(jù),默認(rèn)直接進(jìn)行靜態(tài)生成,如果組件需要在其他地方獲取數(shù)據(jù),在構(gòu)建時(shí) Next.js 會(huì)預(yù)先獲取組件需要的數(shù)據(jù),然后再對(duì)組件進(jìn)行靜態(tài)生成
我們來(lái)對(duì)比一下,開發(fā)環(huán)境不會(huì)打包靜態(tài)文件,生產(chǎn)環(huán)境打包,默認(rèn)生成靜態(tài)文件
那么,有數(shù)據(jù)的情況應(yīng)該怎么做呢?
有數(shù)據(jù)的靜態(tài)生成
getStaticProps() 這個(gè)方法官方翻譯為 靜態(tài)生成。是把組件提前編譯成 html 文件,然后把整個(gè) html 文件響應(yīng)到客戶端,從而達(dá)到預(yù)渲染的目的。
getStaticProps() 方法是個(gè)異步方法,在 Node 環(huán)境下執(zhí)行(構(gòu)建時(shí)執(zhí)行),因此可以進(jìn)行文件讀寫,數(shù)據(jù)庫(kù)鏈接,網(wǎng)絡(luò)通信等一些列操作
對(duì)于這個(gè)方法的使用,先看 demo:
import React from 'react'
import Axios from "axios"
// 類組件
class ListPage extends React.Component {
render(){
return <div>
<h3 >ListPage</h3>
<h4>服務(wù)器數(shù)據(jù):</h4>
{/* 類組件的方式展示數(shù)據(jù)內(nèi)容 */}
<p> {this.props.data[0].name} </p>
</div>
}
}
// // 函數(shù)組件
// function ListPage({data}){
// return (
// <div>
// <h3 >ListPage</h3>
// <h4>服務(wù)器數(shù)據(jù):</h4>
// {/* 類組件的方式展示數(shù)據(jù)內(nèi)容 */}
// <p> {data[1].name} </p>
// </div>
// )
// }
// Node 環(huán)境下執(zhí)行
// 文件讀寫,數(shù)據(jù)庫(kù)鏈接,網(wǎng)絡(luò)通信
export async function getStaticProps(){
const d3 = await Axios.get('http://localhost:80/');
const data = JSON.parse(d3.data);
console.log(data)
// 返回的 Props 屬性的值會(huì)傳遞給組件
return {
props:{data}
}
}
export default ListPage
getStaticProps 方法內(nèi)部必須返回一個(gè)對(duì)象,這個(gè)對(duì)象中的 props 屬性講傳遞到組件中 。
getStaticPaths() 這個(gè)方法也是靜態(tài)生成。與 getStaticProps 共同使用,會(huì)根據(jù)不同的請(qǐng)求參數(shù)生成不同的靜態(tài)頁(yè)面,它的使用方式比較特殊,代碼文件要放在一個(gè)目錄中,同時(shí)代碼文件的文件名,要使用 可選項(xiàng) 文件名的形式,如\pages\props\[id].js
的形式,在項(xiàng)目構(gòu)建時(shí),next 會(huì)根據(jù)不同的 ID 值,生成不同的對(duì)應(yīng)的 靜態(tài)文件,如下代碼
import React from 'react'
import Axios from "axios"
// 類組件
class ListPage extends React.Component {
render() {
return <div>
<h3 >ListPage - Class</h3>
<p>{this.props.backData.name}</p>
</div>
}
}
// 根據(jù)客戶端參數(shù)生成靜態(tài)頁(yè)面
export async function getStaticPaths() {
return {
// 匹配客戶端請(qǐng)求的 id 值
paths: [{ params: { id: "1" } }, { params: { id: "2" } }],
fallback: false
}
}
// 自動(dòng)調(diào)用當(dāng)前方法,將客戶端參數(shù)傳入; { params } 接受到的客戶端參數(shù)
export async function getStaticProps({ params }) {
const d3 = await Axios.get('http://localhost:80/');
const data = JSON.parse(d3.data);
console.log(params)
// 循環(huán)服務(wù)器數(shù)據(jù),獲取
for (let i = 0; i < data.length; i++) {
// console.log(data[i].id)
if (params.id == data[i].id) {
// 返回對(duì)應(yīng)數(shù)據(jù)
const backData = data[i];
return {
props: { backData }
}
}
}
}
export default ListPage
最終構(gòu)建后,會(huì)生成不同的靜態(tài)頁(yè)面:
靜態(tài)站點(diǎn)導(dǎo)出
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"export": "next build && next export"
},
執(zhí)行命令 npm run export
,進(jìn)行構(gòu)建和導(dǎo)出操作,生成 out 文件夾,獲取靜態(tài)站點(diǎn)資源;
除此之外,還有專門針對(duì) React 的 SSG 靜態(tài)站點(diǎn)生成方案:Gatsby https://www.gatsbyjs.cn/ ,感興趣的可以自己去看看
當(dāng)然,你 React 有的,我 Vue 怎么可能沒有呢:Gridsome https://www.gridsome.cn/
斗爭(zhēng)吧,前端框架們……