Next.js + TypeScript 搭建一個簡易的博客系統(tǒng)
最近想攻關(guān)一個 node.js 框架。希望找到一個能夠幫我們把大部分事情都做好的框架,可以直接上手快速開發(fā)。不像傳統(tǒng)的 Express、Koa 需要配置大量中間件。按照這個想法,谷歌了一下就是 —— Next.js 了。最后完成了一個簡易的博客系統(tǒng),
代碼地址: https://github.com/Maricaya/nextjs-blog
預覽地址:http://121.36.50.175/
不得不說 SSR 真香,幾乎沒有白屏時間,加載非常快。
來記錄下學習(踩坑)的過程,這篇文章的代碼都在https://github.com/Maricaya/nextjs-blog-1啦。
先來看看 Next.js 是什么吧。
Next.js 是一個全棧框架
Next.js 是一個輕量級的 React 服務(wù)端渲染應(yīng)用框架。
它支持多種渲染方式:客戶端渲染、靜態(tài)頁面生成、服務(wù)端渲染。
使用Next.js 實現(xiàn) SSR 是一件很簡單的事,我們完全可以不用自己去寫webpack等配置,Next.js 都幫我們做好了。
弱項
上面討論了 Next.js 的很多優(yōu)點,但每個框架都有不完美的地方,尤其是在 Node.js 社區(qū)。
作為一個后端框架,Next.js 完全沒有提供操作數(shù)據(jù)庫的相關(guān)功能,只能自行搭配其他框架。(比如 Sequelize 或者 TypeORM)。
也沒有提供測試相關(guān)功能,也需要自行搭配,可以選擇 Jest 或者 Cypress。
現(xiàn)在我們基本了解了 Next.js,接下來跟著官網(wǎng)做一個簡單的項目吧。
創(chuàng)建項目
#?nextjs-blog-1?是我們的項目名稱
npm?init?next-app?nextjs-blog-1
選擇 Default starter app。
進入 nextjs-blog-1,用命令行啟動項目 yarn dev。
看到下面這個頁面?,就說明你的項目啟動成功啦。

下面我們?yōu)轫椖考由?TypeScript!
啟動 TypeScrip!
第一步就是安裝 TypeScript。
yarn?global?add?typescript
創(chuàng)建 tsconfig.json
然后我們運行 tsc \--init,得到 tsconfig.json,這是 TypeScript 的配置文件。
接下來安裝類型聲明文件,然后重啟項目。
yarn?add?--dev?typescript?@types/react?@types/node
yarn?dev
然后我們將文件名 index.js 改為 index.tsx。
創(chuàng)建第一篇文章
根目錄下創(chuàng)建 posts 文件夾,我們的文章放在這個路徑下。
創(chuàng)建 posts/first-post.tsx 文件,寫入代碼:
//?第一篇文章
import?React?from?"react"
import?{NextPage}?from?'next';
const?FirstPost:?NextPage?=?()?=>?{
??return?(
????First?Post</div>
??)
}
export?default?FirstPost;
這個時候訪問 http://localhost:3000/hosts/first-post 就能看見頁面了。
Link 快速導航
官網(wǎng)中介紹了 Link 快速導航。
稍微了解前端同學們可能會有這樣的問題,不是有 a 標簽可以導航嗎,Next.js 為什么要多此一舉。
據(jù)官網(wǎng)介紹,Link 可以實現(xiàn)快速導航。我們來做個實驗,看看它和 a 標簽有什么不同。
先在項目分別中使用 a 標簽、Link 標簽導航,實現(xiàn)首頁和第一篇文章互相跳轉(zhuǎn)。
index.tsx
<h1?className="title">
??第一篇文章
????<a?href="/posts/first-post">a?點擊這里a>
????<Link?href="/posts/first-post"><a?>link?點擊這里a>Link>
h1>
/posts/first-post.tsx
//?回到首頁
<hr/>
<a?href="/">a?點擊這里a>
<Link><a?href="/">link?點擊這里a>Link>
點擊 a 標簽,每次進入 first-post、index 頁面,瀏覽器都會重新請求所有的 html、css、js。

接下來使用 Link 標簽導航,神奇的事情發(fā)生了,瀏覽器只發(fā)送了 2 個請求。

第二個請求是 webpack,所以真實的請求只有 1 個,就是 first-post.js。
反復在兩個頁面中跳轉(zhuǎn),除了 webpack,瀏覽器沒有發(fā)出任何請求。
Next.js 到底做了什么?快速導航和傳統(tǒng)導航有什么區(qū)別?
傳統(tǒng)導航
我們先來看看從 page1 到 page2,傳統(tǒng)導航是怎么實現(xiàn)的?

訪問第一個頁面 page1 時,瀏覽器請求 html,然后依次加載 css、js。
當用戶點擊 a 標簽,就重定向到 page2,瀏覽器請求 html,然后再次加載 css、js。
Link 快速導航
再看相同的過程,Next.js 中的快速導航是怎么實現(xiàn)的。

首先訪問 page1,瀏覽器下載 html,然后依次加載 css、js。這些和傳統(tǒng)導航一樣。
但是當用戶點擊 Link 標簽時, page1 會執(zhí)行一個 js,這個js 會對 Link 標簽進行解析,點擊 Link 之后請求 page2 的 page2.js,這個 page2.js 就是 page2 的 html+css+js。
請求完 page2.js 之后,會回到 page1 的頁面,把 page2 的 html、css、js 更新到 page1 上。也就是把 page1 更新為 page2。
所以,瀏覽器沒有親自訪問過 page2,而是 page1 通過 ajax 來獲取 page2 的內(nèi)容。
優(yōu)點
所以,Link 快速導航(客戶端導航)有這么多優(yōu)點:
頁面不會刷新,用 AJAX 請求新頁面內(nèi)容。 不會請求重復的 HTML、CSS、JS。 自動在頁面插入新內(nèi)容,刪除舊內(nèi)容。 因為省了一些請求和解析過程,所以速度極快。
同構(gòu)代碼
什么是同構(gòu)?
同構(gòu)是指同開發(fā)一個可以跑在不同的平臺上的程序, 這里指 js 代碼可以同時運行在 node.js 的 web server 和瀏覽器中。
也就是代碼運行在兩端。
做個試驗,我們在組件里寫一句 console.log('aaa')。
結(jié)果 Node 控制臺、Chrome 控制臺都會打印出 aaa。
注意差異
但并不是所有的代碼都會運行在兩端。
比如需要用戶觸發(fā)的代碼,只會運行在瀏覽器端。
我們的代碼也不能隨意編寫,必須保證在兩端都能運行。比如 window,在 Node.js 中沒有這個對象,就會報錯。
優(yōu)點
減少代碼開發(fā)量, 提高代碼復用量。
一份代碼能同時跑在瀏覽器和服務(wù)器,因此代碼量減少了。 業(yè)務(wù)邏輯也不需要在瀏覽器和服務(wù)端同時維護,減小了程序出錯的可能。
全局配置 Head, Metadata, CSS
Head
title
我們想讓頁面的 title 不同,應(yīng)該怎么配置?
在 Head 中配置 title,Head 會幫我們寫入 title。
我的博客
Metadata
meta 也是一樣
<Head>
????<meta?name="viewport"?content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover"/>
Head>
但是目前 index.tsx 和 first-post.tsx 是兩個文件,難道要寫入兩遍嗎?有沒有統(tǒng)一寫入的方法?
全局配置
創(chuàng)建 pages/_app.js,從官網(wǎng)上抄下代碼,寫入我們的 tie然后重啟 yarn dev。
export?default?function?App({?Component,?pageProps?})?{
??//?Component
??return?<div>
<Component?{...pageProps}?/>
div>
}
其中 Component 就是我們定義的 index 和 first-post;pageProps 是頁面的選項,目前是空對象。
export default function App 是每個頁面的根組件。頁面切換時 App 不會銷毀,App 里面的組件會銷毀。我們可以用 App 保存全局狀態(tài)。
CSS
也是一樣,全局的 CSS 放在 _app.js 中。因為切頁面的時候 App 不會被銷毀,其他地方只能寫局部 CSS。
imprort?'../styles/global.css'。
絕對引用
寫相對路徑有點麻煩,能不能指定根目錄寫絕對路徑呢?翻了翻官網(wǎng),發(fā)現(xiàn) Next.js 提供了類似的功能。
配置 tsconfig.json,定義根目錄。
{
??"compilerOptions":?{
????"baseUrl":?"."
??}
}
重啟項目,就可以絕對引入 css 啦:
imprort?'styles/global.css'
靜態(tài)資源
next 推薦放在 public/ 里,但是我并不推薦這種做法,因為不支持改文件名。
有前端基礎(chǔ)的同學就知道,不支持改文件名,會影響我們的緩存策略。
如果 public 中的靜態(tài)資源沒有加緩存,這樣每次請求資源都會去請求服務(wù)器,造成資源浪費。
但是如果加了緩存,我們每次更新靜態(tài)資源就必須更新資源名稱,否則瀏覽器還是會加載舊資源。
所以,我們在根目錄新建 /assets 來放置靜態(tài)資源,并且需要在 next.js 中配置 webpack。
根據(jù)官網(wǎng),在根目錄創(chuàng)建 next.config.js,自定義 webpack 配置。
圖片
配置 image-loader
配置 file-loader。
安裝 yarn add \--dev file-loader。
next.config.js
module.exports?=?{
??webpack:?(config,?options)?=>?{
????config.module.rules.push({
??????test:?/\.(png|jpg|jpeg|gif|svg)$/,
??????use:?[
????????{
??????????loader:?'file-loader',
??????????options:?{
????????????//?img?路徑名稱.hash.ext
????????????//?比如?1.png?路徑名稱為
????????????//?_next/static/1.29fef1d3301a37127e326ea4c1543df5.png
????????????name:?'[name].[contenthash].[ext]',
????????????//?硬盤路徑
????????????outputPath:?'static',
????????????//?網(wǎng)站路徑是
????????????publicPath:?'_next/static'
??????????}
????????}
??????]
????})
????return?config
??}
}
直接使用 next-images
如果不想自己配置,也可以直接使用 next-images。
yarn?add?--dev?next-images
next.config.js
const?withImages?=?require('next-images')
module.exports?=?withImages({
??webpack(config,?options)?{
????return?config
??}
})
使用方法
TypeScript
現(xiàn)在導入圖像的文件還是會報錯,因為我們使用了 TypeScript,而 Typescript 不知道如何解釋導入的圖像。
next-images 很貼心地準備了圖像模塊的定義文件。
所以,我們只需要在 next-env.d.ts 文件中添加 next-images 類型的引用就好啦。
///?
更多的其他文件
自己找到 loader,然后配置 next.config.js,或者看看有沒有封裝成 next 插件。
這些屬于 webpack 的范圍,大家可以自己探索。這篇文章就不啰嗦了。
Next.js API
到現(xiàn)在為止,我們的 index 和 posts/first-post 都是 HTML 頁面。
但實際開發(fā)中我們需要請求 /user、 /shops 等 API,它們返回的內(nèi)容是 JSON 格式的字符串。在 Next.js 中怎么實現(xiàn)呢?
使用 Next.js 的 API 模式。
使用 Next.js API
demo
API 的默認路徑為 /api/v1/xxx,我們新建一個測試接口 demo.ts 。
在 api 目錄下的代碼只運行在 Node.js 里,不會運行在瀏覽器中。
demo.tsx
//?ts?就是加上了類型
import?{NextApiHandler}?from?'next';
const?Demo:NextApiHandler?=?(req,?res)?=>?{
????//?其他的操作和?js?一樣
??res.statusCode?=?200;
??res.setHeader('Content-Type',?'application/json');
??res.write(JSON.stringify({name:?'狗子'}));
??res.end();
};
export?default?Demo;
訪問 http://localhost:3000/api/demo,得到數(shù)據(jù)。

posts
接下來我們完成一個正式博客 API,posts 接口。
首先準備博客文件,根目錄下創(chuàng)建 markdown 文檔,寫入幾篇 md 格式的博客。
然后我們借助 gray-matter 從 md 文件中解析數(shù)據(jù)。
lib/posts.tsx 這個文件導出 JSON 數(shù)據(jù)。
import?path?from?"path";
import?fs,?{promises?as?fsPromise}?from?"fs";
import?matter?from?"gray-matter";
export?const?getPosts?=?async?()?=>?{
??const?markdownDir?=?path.join(process.cwd(),?'markdown');
??const?fileNames?=?await?fsPromise.readdir(markdownDir);
??const?x?=?fileNames.map(fileName?=>?{
????const?fullPath?=?path.join(markdownDir,?fileName);
????const?id?=?fileName.replace(fullPath,?'');
????const?text?=?fs.readFileSync(fullPath,?'utf8');
????const?{data:?{title,?date},?content}?=?matter(text);
????return?{
??????id,?title,?date
????}
??});
??console.log('x');
??console.log(x);
??return?x;
};
搞定了數(shù)據(jù),下面就簡單多了,posts API 接口直接從上面的代碼中獲取數(shù)據(jù),然后返回給前端即可。pages/api/posts.tsx
import?{NextApiHandler}?from?'next';
import?{getPosts}?from?'lib/posts';
const?Posts:?NextApiHandler?=?async?(req,?res)?=>?{
??const?posts?=?await?getPosts();
??res.statusCode?=?200;
??res.setHeader('Content-Type',?'application/json');
??res.write(JSON.stringify(posts));
??res.end();
};
export?default?Posts;
ps:Next.js 基于 Express,所以支持 Express 的中間件。如果有復雜的操作,可以借助 Express 中間件。
Next.js 三種渲染方式
下面我們來做前端部分,用三種渲染方式實現(xiàn)。
客戶端渲染
只在瀏覽器上執(zhí)行的渲染。
也就是最原始的前端渲染方式,頁面在瀏覽器獲取到 JavaScript 和 CSS 等文件后開始渲染。路由是客戶端路由,也就是目前最常見的 SPA 單頁應(yīng)用。
缺點
但這種方式會造成兩個問題。一是白屏,目前解決方法是在 AJAX 得到相應(yīng)之前,頁面中先加入 Loading。二是 SEO 不友好,因為搜索引擎訪問頁面時,默認不會執(zhí)行 JS,只能看到 HTML,看不到 AJAX 請求的數(shù)據(jù)。
代碼
pages/posts/BSR.tsx
import?{NextPage}?from?'next';
import?axios?from?'axios';
import?{useEffect,?useState}?from?"react";
import?*?as?React?from?"react";
type?Post?=?{
????id:?string,
????id:?string,
????title:?string
}
const?PostsIndex:?NextPage?=?()?=>?{
????//?[]?表示只在第一次渲染的時候請求
????const?[posts,?setPosts]?=?useState([]);
????const?[isLoading,?setIsLoading]?=?useState(false);
????useEffect(()?=>?{
????????setIsLoading(true);
????????axios.get('/api/posts').then(response?=>?{
??????????setPosts(response.data);
??????????setIsLoading(false);
????????},?()?=>?{
????????????setIsLoading(true);
????????})
????},?[]);
????return?(
????????
????????????文章列表</h1>
????????????{isLoading???加載中div>?:
????????????????posts.map(p?=>?
????????????????{p.id}
????????????</div>)}
????????div>
????)
};
export?default?PostsIndex;
訪問 http://localhost:3000/posts/BSR,如果網(wǎng)絡(luò)不好,白屏時間很長。
因為數(shù)據(jù)本來不在頁面上,通過 ajax 請求后渲染到頁面上。
文章列表都是前端渲染的,我們稱之為客戶端渲染。
靜態(tài)頁面生成(SSG) Static Site Generation
我們做的博客網(wǎng)站,其實每個人看到的文章列表都是一樣的。
那為什么還需要在每個人的瀏覽器上渲染一次呢?
能不能直接在后端渲染好,瀏覽器直接請求呢?
這樣的話,N 次渲染就變成了 1 次渲染,N 次客戶端渲染變成了 1 次靜態(tài)頁面生成。
這個過程就叫做動態(tài)內(nèi)容靜態(tài)化。
優(yōu)缺點
這種方式可以解決白屏問題、SEO 問題。
但這種方式所有用戶請求的內(nèi)容都一樣,無法生成用戶相關(guān)內(nèi)容。
代碼:getStaticProps 獲取 posts
顯然,后端最好不要通過 AJAX 來獲取 posts。
我們的數(shù)據(jù)就在文件夾里面,直接讀取數(shù)據(jù)就可以,沒必要發(fā)送 AJAX。
那么,應(yīng)該如何獲取獲取 posts 呢?
使用 Next.js 提供的方法 getStaticProps 導出數(shù)據(jù),NextPage 的 props 參數(shù)會自動獲取導出的數(shù)據(jù)。
具體來看看代碼吧:
SSG.tsx
import?{GetStaticProps,?NextPage}?from?'next';
import?{getPosts}?from?'../../lib/posts';
import?Link?from?'next/link';
import?*?as?React?from?'react';
type?Post?=?{
??id:?string,
??title:?string
}
type?Props?=?{
??posts:?Post[];
}
//?props?中有下面導出的數(shù)據(jù)?posts
const?PostsIndex:?NextPage?=?(props)?=>?{
??const?{posts}?=?props;
????//?前后端控制臺都能打印?->?同構(gòu)
??console.log(posts);
??return?(
????
??????文章列表</h1>
??????{posts.map(p?=>?
????????posts/${p.id}`}>
??????????
????????????{p.id}
??????????
????????
??????)}
????
??);
};
export?default?PostsIndex;
//?實現(xiàn)SSG
export?const?getStaticProps:?GetStaticProps?=?async?()?=>?{
??const?posts?=?await?getPosts();
??return?{
????props:?{
??????posts:?JSON.parse(JSON.stringify(posts))
????}
??};
};
訪問 http://localhost:3000/posts/SSG,頁面訪問成功。
前端怎么不通過 AJAX 獲取數(shù)據(jù)?
posts 數(shù)據(jù)我們只傳遞給了服務(wù)器,為什么在前端也能打印出來?
我們來看看此時的頁面:

現(xiàn)在前端不用 AJAX 也能拿到 posts 了,直接通過 __NEXT_DATA__ 獲取數(shù)據(jù)。這就是同構(gòu) SSR 的好處:后端數(shù)據(jù)可以直接傳給前端,前端 JSON.parse 一下子就能得到 posts。
getStaticProps 靜態(tài)化的時機
在開發(fā)環(huán)境,每次請求都會運行一次 getStaticProps,這是為了方便我們修改代碼重新運行。
而在生產(chǎn)環(huán)境,getStaticProps 只在 build 時運行,這樣可以提供一份 HTML 給所有用戶下載。
來體驗下生產(chǎn)環(huán)境吧,打包我們的項目。
yarn?build
yarn?start
打包之后,我們得到三種類型的文件:
λ (Server) SSR 不能自動創(chuàng)建 HTML(等會再說)
○ (Static) 自動創(chuàng)建 HTML (發(fā)現(xiàn)你沒用到 props)
● (SSG) 自動創(chuàng)建 HTML + JSON (等你用到 props)
創(chuàng)建出了這三種文件:posts.html = posts.js + posts.json
posts.html 含有靜態(tài)內(nèi)容,用于用戶直接訪問 post.js 也含有靜態(tài)內(nèi)容,用于快速導航(與 HTML 對應(yīng)) posts.json 含有數(shù)據(jù),跟 posts.js 結(jié)合得到頁面
那為什么不直接把數(shù)據(jù)放入 posts.js 呢?顯然,是為了讓 posts.js 接受不同的數(shù)據(jù)。
當我們展示每篇博客的時候,他們的樣式相同,內(nèi)容不同,就會用到這個功能了。
小結(jié)
如果動態(tài)內(nèi)容與用戶無關(guān),那么可以提前靜態(tài)化。 通過 getStaticProps 可以獲取數(shù)據(jù),靜態(tài)內(nèi)容 + 數(shù)據(jù)(本地獲取)就得到了完整頁面。代替了之前的 靜態(tài)內(nèi)容+動態(tài)數(shù)據(jù)(AJAX獲取)。 靜態(tài)化是在 yarn build 的時候?qū)崿F(xiàn)的 優(yōu)點 生產(chǎn)環(huán)境直接給出完整頁面 首屏不會白屏 搜索引擎能看到頁面內(nèi)容(方便 SEO)
服務(wù)端渲染(SSR)
如果頁面跟用戶相關(guān)呢?這種情況較難提前靜態(tài)化。
那怎么辦呢?
要么客戶端渲染,下拉更新 要么服務(wù)的渲染,下拉 AJAX 更新(沒有白屏
優(yōu)點
這種方式可以解決白屏問題、SEO 問題。可以生成用戶相關(guān)內(nèi)容(不同用戶結(jié)果不同)。
代碼
和 SSG 代碼基本一致,不過使用的函數(shù)換成 getServerSideProps。
寫一段代碼,顯示當前用戶瀏覽器是什么。
import?{GetServerSideProps,?NextPage}?from?'next';
import?*?as?React?from?'react';
import?{IncomingHttpHeaders}?from?'http';
type?Props?=?{
??browser:?string
}
const?index:?NextPage?=?(props)?=>?{
??return?(
????
??????你的瀏覽器是?{props.browser}</h1>
????div>
??);
};
export?default?index;
export?const?getServerSideProps:?GetServerSideProps?=?async?(context)?=>?{
??const?headers:IncomingHttpHeaders?=?context.req.headers;
??const?browser?=?headers['user-agent'];
??return?{
????props:?{
??????browser
????}
??};
};
getServerSideProps
無論是開發(fā)環(huán)境還是生產(chǎn)環(huán)境,都是在請求到來之后運行 getServerSideProps。
回顧一下 getStaticProps,看看他們的區(qū)別。
開發(fā)環(huán)境,每次請求到來后運行,方便開發(fā) 生產(chǎn)環(huán)境,build 時運行
參數(shù)
context,類型為 NextPageContext context.req/context.res 可以獲取請求和響應(yīng) 一般只需要用到 context.req
SSR 原理
最后我們來看看 SSR 到底是怎么實現(xiàn)的。
我們都知道 SSR 是提前渲染好靜態(tài)內(nèi)容,這些靜態(tài)內(nèi)容是在服務(wù)端渲染,還是在客戶端渲染的?
具體渲染幾次呢?一次還是兩次?
參考 React SSR 的官方文檔
推薦 在后端調(diào)用 renderToString() 的方法,把整個頁面渲染成字符串。
然后前端調(diào)用 hydrate() 方法,把后端傳遞的字符串和自己的實例混合起來,保留 HTML 并附上事件監(jiān)聽。
以上就是 Next.js 實現(xiàn) SSR 的主要方法,也就是后端會渲染 HTML, 前端添加監(jiān)聽。
前端也會渲染一次,以確保前后端渲染結(jié)果一致。如果結(jié)果不一致,控制臺會報錯提醒我們。
總結(jié)
創(chuàng)建項目 npm init next-app 項目名 快速導航 同構(gòu)代碼:一份代碼,兩端運行 全局組件:pages/_app.js 全局 CSS:在 _app.js 里 import
自定義 head:使用 組件 Next.js API:都放在 /pages/api 目錄中 三種渲染的方式:BSR、SSG、SSR 動態(tài)內(nèi)容 術(shù)語:客戶端渲染,通過 AJAX 請求,渲染成 HTML。 動態(tài)內(nèi)容靜態(tài)化 術(shù)語:SSG,通過 getStaticProps 獲取用戶無關(guān)內(nèi)容 用戶相關(guān)動態(tài)內(nèi)容靜態(tài)化 術(shù)語:SSR,通過 getServerSideProps 獲取請求 缺點:無法獲取客戶端信息,如瀏覽器窗口大小 靜態(tài)內(nèi)容 直接輸出 HTML,沒有術(shù)語。
篇幅有限,更多可前往 https://github.com/Maricaya/nextjs-blog-1
“在看和轉(zhuǎn)發(fā)”就是最大的支持
瀏覽
45
評論圖片表情
免费黄色欧美
|
美女性爱网|
91精品国产福利在线观看
|
麻豆出品必是精品(3)
|
少妇激情五月天
|
