最近想攻關一個 node.js 框架。但願找到一個可以幫咱們把大部分事情都作好的框架,能夠直接上手快速開發。不像傳統的 Express、Koa 須要配置大量中間件。按照這個想法,谷歌了一下就是 —— Next.js 了。最後完成了一個簡易的博客系統,css
代碼地址: https://github.com/Maricaya/nextjs-bloghtml
預覽地址:http://121.36.50.175/前端
不得不說 SSR 真香,幾乎沒有白屏時間,加載很是快。node
來記錄下學習(踩坑)的過程,這篇文章的代碼都在https://github.com/Maricaya/nextjs-blog-1啦。react
先來看看 Next.js 是什麼吧。webpack
Next.js 是一個全棧框架
Next.js 是一個輕量級的 React 服務端渲染應用框架。ios
它支持多種渲染方式:客戶端渲染、靜態頁面生成、服務端渲染。git
使用Next.js 實現 SSR 是一件很簡單的事,咱們徹底能夠不用本身去寫webpack等配置,Next.js 都幫咱們作好了。github
弱項
上面討論了 Next.js 的不少優勢,但每一個框架都有不完美的地方,尤爲是在 Node.js 社區。web
做爲一個後端框架,Next.js 徹底沒有提供操做數據庫的相關功能,只能自行搭配其餘框架。(好比 Sequelize 或者 TypeORM)。
也沒有提供測試相關功能,也須要自行搭配,能夠選擇 Jest 或者 Cypress。
如今咱們基本瞭解了 Next.js,接下來跟着官網作一個簡單的項目吧。
建立項目
# nextjs-blog-1 是咱們的項目名稱
npm init next-app nextjs-blog-1
選擇 Default starter app。
進入 nextjs-blog-1,用命令行啓動項目 yarn dev
。
看到下面這個頁面👇,就說明你的項目啓動成功啦。
下面咱們爲項目加上 TypeScript!
啓動 TypeScrip!
第一步就是安裝 TypeScript。
yarn global add typescript
建立 tsconfig.json
而後咱們運行 tsc \--init
,獲得 tsconfig.json,這是 TypeScript 的配置文件。
接下來安裝類型聲明文件,而後重啓項目。
yarn add --dev typescript @types/react @types/node
yarn dev
而後咱們將文件名 index.js 改成 index.tsx。
建立第一篇文章
根目錄下建立 posts 文件夾,咱們的文章放在這個路徑下。
建立 posts/first-post.tsx 文件,寫入代碼:
// 第一篇文章
import React from "react"
import {NextPage} from 'next';
const FirstPost: NextPage = () => {
return (
<div>First Post</div>
)
}
export default FirstPost;
這個時候訪問 http://localhost:3000/hosts/first-post
就能看見頁面了。
Link 快速導航
官網中介紹了 Link 快速導航。
稍微瞭解前端同窗們可能會有這樣的問題,不是有 a 標籤能夠導航嗎,Next.js 爲何要畫蛇添足。
據官網介紹,Link 能夠實現快速導航。咱們來作個實驗,看看它和 a 標籤有什麼不一樣。
先在項目分別中使用 a 標籤、Link 標籤導航,實現首頁和第一篇文章互相跳轉。
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 標籤導航,神奇的事情發生了,瀏覽器只發送了 2 個請求。
第二個請求是 webpack,因此真實的請求只有 1 個,就是 first-post.js。
反覆在兩個頁面中跳轉,除了 webpack,瀏覽器沒有發出任何請求。
Next.js 到底作了什麼?快速導航和傳統導航有什麼區別?
傳統導航
咱們先來看看從 page1 到 page2,傳統導航是怎麼實現的👇
訪問第一個頁面 page1 時,瀏覽器請求 html,而後依次加載 css、js。
當用戶點擊 a 標籤,就重定向到 page2,瀏覽器請求 html,而後再次加載 css、js。
Link 快速導航
再看相同的過程,Next.js 中的快速導航是怎麼實現的。
首先訪問 page1,瀏覽器下載 html,而後依次加載 css、js。這些和傳統導航同樣。
可是當用戶點擊 Link 標籤時, page1 會執行一個 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 的內容。
優勢
因此,Link 快速導航(客戶端導航)有這麼多優勢:
-
頁面不會刷新,用 AJAX 請求新頁面內容。 -
不會請求重複的 HTML、CSS、JS。 -
自動在頁面插入新內容,刪除舊內容。 -
由於省了一些請求和解析過程,因此速度極快。
同構代碼
什麼是同構?
同構是指同開發一個能夠跑在不一樣的平臺上的程序, 這裏指 js 代碼能夠同時運行在 node.js 的 web server 和瀏覽器中。
也就是代碼運行在兩端。
作個試驗,咱們在組件裏寫一句 console.log('aaa')
。
結果 Node 控制檯、Chrome 控制檯都會打印出 aaa
。
注意差別
但並非全部的代碼都會運行在兩端。
好比須要用戶觸發的代碼,只會運行在瀏覽器端。
咱們的代碼也不能隨意編寫,必須保證在兩端都能運行。好比 window
,在 Node.js 中沒有這個對象,就會報錯。
優勢
減小代碼開發量, 提升代碼複用量。
-
一份代碼能同時跑在瀏覽器和服務器,所以代碼量減小了。 -
業務邏輯也不須要在瀏覽器和服務端同時維護,減少了程序出錯的可能。
全局配置 Head, Metadata, CSS
Head
title
咱們想讓頁面的 title 不一樣,應該怎麼配置?
在 Head 中配置 title,Head 會幫咱們寫入 title。
<Head>
<title>個人博客</title>
</Head>
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 是兩個文件,難道要寫入兩遍嗎?有沒有統一寫入的方法?
全局配置
建立 pages/_app.js,從官網上抄下代碼,寫入咱們的 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 保存全局狀態。
CSS
也是同樣,全局的 CSS 放在 _app.js 中。由於切頁面的時候 App 不會被銷燬,其餘地方只能寫局部 CSS。
imprort '../styles/global.css'。
絕對引用
寫相對路徑有點麻煩,能不能指定根目錄寫絕對路徑呢?翻了翻官網,發現 Next.js 提供了相似的功能。
配置 tsconfig.json,定義根目錄。
{
"compilerOptions": {
"baseUrl": "."
}
}
重啓項目,就能夠絕對引入 css 啦:
imprort 'styles/global.css'
靜態資源
next 推薦放在 public/ 裏,可是我並不推薦這種作法,由於不支持改文件名。
有前端基礎的同窗就知道,不支持改文件名,會影響咱們的緩存策略。
若是 public 中的靜態資源沒有加緩存,這樣每次請求資源都會去請求服務器,形成資源浪費。
可是若是加了緩存,咱們每次更新靜態資源就必須更新資源名稱,不然瀏覽器仍是會加載舊資源。
因此,咱們在根目錄新建 /assets 來放置靜態資源,而且須要在 next.js 中配置 webpack。
根據官網,在根目錄建立 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',
// 網站路徑是
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
}
})
使用方法
<img src={require('./my-image.jpg')}/>
TypeScript
如今導入圖像的文件仍是會報錯,由於咱們使用了 TypeScript,而 Typescript 不知道如何解釋導入的圖像。
next-images 很貼心地準備了圖像模塊的定義文件。
因此,咱們只須要在 next-env.d.ts
文件中添加 next-images 類型的引用就好啦。
/// <reference types="next-images" />
更多的其餘文件
本身找到 loader,而後配置 next.config.js,或者看看有沒有封裝成 next 插件。
這些屬於 webpack 的範圍,你們能夠本身探索。這篇文章就不囉嗦了。
Next.js API
到如今爲止,咱們的 index 和 posts/first-post 都是 HTML 頁面。
但實際開發中咱們須要請求 /user、 /shops 等 API,它們返回的內容是 JSON 格式的字符串。在 Next.js 中怎麼實現呢?
使用 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
,獲得數據。
posts
接下來咱們完成一個正式博客 API,posts 接口。
首先準備博客文件,根目錄下建立 markdown 文檔,寫入幾篇 md 格式的博客。
而後咱們藉助 gray-matter 從 md 文件中解析數據。
lib/posts.tsx 這個文件導出 JSON 數據。
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;
};
搞定了數據,下面就簡單多了,posts API 接口直接從上面的代碼中獲取數據,而後返回給前端便可。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 三種渲染方式
下面咱們來作前端部分,用三種渲染方式實現。
客戶端渲染
只在瀏覽器上執行的渲染。
也就是最原始的前端渲染方式,頁面在瀏覽器獲取到 JavaScript 和 CSS 等文件後開始渲染。路由是客戶端路由,也就是目前最多見的 SPA 單頁應用。
缺點
但這種方式會形成兩個問題。一是白屏,目前解決方法是在 AJAX 獲得相應以前,頁面中先加入 Loading。二是 SEO 不友好,由於搜索引擎訪問頁面時,默認不會執行 JS,只能看到 HTML,看不到 AJAX 請求的數據。
代碼
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<Post[]>([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
setIsLoading(true);
axios.get('/api/posts').then(response => {
setPosts(response.data);
setIsLoading(false);
}, () => {
setIsLoading(true);
})
}, []);
return (
<div>
<h1>文章列表</h1>
{isLoading ? <div>加載中</div> :
posts.map(p => <div key={p.id}>
{p.id}
</div>)}
</div>
)
};
export default PostsIndex;
訪問 http://localhost:3000/posts/BSR
,若是網絡很差,白屏時間很長。
由於數據原本不在頁面上,經過 ajax 請求後渲染到頁面上。
文章列表都是前端渲染的,咱們稱之爲客戶端渲染。
靜態頁面生成(SSG) Static Site Generation
咱們作的博客網站,其實每一個人看到的文章列表都是同樣的。
那爲何還須要在每一個人的瀏覽器上渲染一次呢?
能不能直接在後端渲染好,瀏覽器直接請求呢?
這樣的話,N 次渲染就變成了 1 次渲染,N 次客戶端渲染變成了 1 次靜態頁面生成。
這個過程就叫作動態內容靜態化。
優缺點
這種方式能夠解決白屏問題、SEO 問題。
但這種方式全部用戶請求的內容都同樣,沒法生成用戶相關內容。
代碼:getStaticProps 獲取 posts
顯然,後端最好不要經過 AJAX 來獲取 posts。
咱們的數據就在文件夾裏面,直接讀取數據就能夠,不必發送 AJAX。
那麼,應該如何獲取獲取 posts 呢?
使用 Next.js 提供的方法 getStaticProps
導出數據,NextPage 的 props 參數會自動獲取導出的數據。
具體來看看代碼吧:
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 中有下面導出的數據 posts
const PostsIndex: NextPage<Props> = (props) => {
const {posts} = props;
// 先後端控制檯都能打印 -> 同構
console.log(posts);
return (
<div>
<h1>文章列表</h1>
{posts.map(p => <div key={p.id}>
<Link href={`/posts/${p.id}`}>
<a>
{p.id}
</a>
</Link>
</div>)}
</div>
);
};
export default PostsIndex;
// 實現SSG
export const getStaticProps: GetStaticProps = async () => {
const posts = await getPosts();
return {
props: {
posts: JSON.parse(JSON.stringify(posts))
}
};
};
訪問 http://localhost:3000/posts/SSG
,頁面訪問成功。
前端怎麼不經過 AJAX 獲取數據?
posts 數據咱們只傳遞給了服務器,爲何在前端也能打印出來?
咱們來看看此時的頁面:
如今前端不用 AJAX 也能拿到 posts 了,直接經過 __NEXT_DATA__
獲取數據。這就是同構 SSR 的好處:後端數據能夠直接傳給前端,前端 JSON.parse 一會兒就能獲得 posts。
getStaticProps 靜態化的時機
在開發環境,每次請求都會運行一次 getStaticProps,這是爲了方便咱們修改代碼從新運行。
而在生產環境,getStaticProps 只在 build 時運行,這樣能夠提供一份 HTML 給全部用戶下載。
來體驗下生產環境吧,打包咱們的項目。
yarn build
yarn start
打包以後,咱們獲得三種類型的文件:
-
λ (Server) SSR 不能自動建立 HTML(等會再說)
-
○ (Static) 自動建立 HTML (發現你沒用到 props)
-
● (SSG) 自動建立 HTML + JSON (等你用到 props)
建立出了這三種文件:posts.html = posts.js + posts.json
-
posts.html 含有靜態內容,用於用戶直接訪問 -
post.js 也含有靜態內容,用於快速導航(與 HTML 對應) -
posts.json 含有數據,跟 posts.js 結合獲得頁面
那爲何不直接把數據放入 posts.js 呢?顯然,是爲了讓 posts.js 接受不一樣的數據。
當咱們展現每篇博客的時候,他們的樣式相同,內容不一樣,就會用到這個功能了。
小結
-
若是動態內容與用戶無關,那麼能夠提早靜態化。 -
經過 getStaticProps 能夠獲取數據,靜態內容 + 數據(本地獲取)就獲得了完整頁面。代替了以前的 靜態內容+動態數據(AJAX獲取)。 -
靜態化是在 yarn build 的時候實現的 -
優勢 -
生產環境直接給出完整頁面 -
首屏不會白屏 -
搜索引擎能看到頁面內容(方便 SEO)
服務端渲染(SSR)
若是頁面跟用戶相關呢?這種狀況較難提早靜態化。
那怎麼辦呢?
-
要麼客戶端渲染,下拉更新 -
要麼服務的渲染,下拉 AJAX 更新(沒有白屏
優勢
這種方式能夠解決白屏問題、SEO 問題。能夠生成用戶相關內容(不一樣用戶結果不一樣)。
代碼
和 SSG 代碼基本一致,不過使用的函數換成 getServerSideProps。
寫一段代碼,顯示當前用戶瀏覽器是什麼。
import {GetServerSideProps, NextPage} from 'next';
import * as React from 'react';
import {IncomingHttpHeaders} from 'http';
type Props = {
browser: string
}
const index: NextPage<Props> = (props) => {
return (
<div>
<h1>你的瀏覽器是 {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
不管是開發環境仍是生產環境,都是在請求到來以後運行 getServerSideProps。
回顧一下 getStaticProps,看看他們的區別。
-
開發環境,每次請求到來後運行,方便開發 -
生產環境, build 時運行
參數
-
context,類型爲 NextPageContext -
context.req/context.res 能夠獲取請求和響應 -
通常只須要用到 context.req
SSR 原理
最後咱們來看看 SSR 究竟是怎麼實現的。
咱們都知道 SSR 是提早渲染好靜態內容,這些靜態內容是在服務端渲染,仍是在客戶端渲染的?
具體渲染幾回呢?一次仍是兩次?
參考 React SSR 的官方文檔
推薦 在後端調用 renderToString()
的方法,把整個頁面渲染成字符串。
而後前端調用 hydrate()
方法,把後端傳遞的字符串和本身的實例混合起來,保留 HTML 並附上事件監聽。
以上就是 Next.js 實現 SSR 的主要方法,也就是後端會渲染 HTML, 前端添加監聽。
前端也會渲染一次,以確保先後端渲染結果一致。若是結果不一致,控制檯會報錯提醒咱們。
總結
-
建立項目 npm init next-app 項目名
-
快速導航 <Link href=xxx><a></a></Link>
-
同構代碼:一份代碼,兩端運行 -
全局組件: pages/_app.js
-
全局 CSS:在 _app.js 裏 import -
自定義 head:使用 組件 -
Next.js API:都放在 /pages/api 目錄中 -
三種渲染的方式:BSR、SSG、SSR -
動態內容 術語:客戶端渲染,經過 AJAX 請求,渲染成 HTML。 -
動態內容靜態化 術語:SSG,經過 getStaticProps 獲取用戶無關內容 -
用戶相關動態內容靜態化 術語:SSR,經過 getServerSideProps 獲取請求 缺點:沒法獲取客戶端信息,如瀏覽器窗口大小 -
靜態內容 直接輸出 HTML,沒有術語。
篇幅有限,更多可前往 https://github.com/Maricaya/nextjs-blog-1
回覆「加羣」與大佬們一塊兒交流學習~
點擊「閱讀原文」查看 80+ 篇原創文章
本文分享自微信公衆號 - 前端自習課(FE-study)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。