簡介
最近才學習了 React,現在要接觸服務端渲染,趁熱打鐵把 Next 學一下,關於 Next.js,可以移步到知乎討論:
知乎:關於 Next.js 的討論
初識Next.js
安裝
- Next.js 支援 Windows、Mac 和 Linux系統,均可安裝,但是前提是你已經安裝了 Node.js
- 建立示例專案的過程如下:
mkdir hello-next cd hello-next npm init -y npm install --save react react-dom next mkdir pages
使用
- 開啟
hello-next/package.json
,替換scripts
:"scripts": { "dev": "next", "build": "next build", "start": "next start" }
- 啟動
在瀏覽器中開啟 http://localhost:3000,你會看到頁面顯示 404 | This page could not be found.npm run dev
- 建立你的第一個頁面
- 建立
pages/index.js
,並輸入:const Index = () => ( <div> <p>Hello Next.js</p> </div> ); export default Index;
- 再次輸入
npm run dev
,就能看到效果了 - 上述案例中,我們在
pages/index.js
模組中預設(default)匯出了一個簡單的 React 元件
- 建立
- 試錯
- 嘗試著錯一次:將
pages/index.js
改為:const Index = () => ( <div> <p>Hello Next.js </div> ); export default Index;
- 重新啟動,瀏覽器顯示:
- 一般情況下,Next.js 將跟蹤此類錯誤並在瀏覽器中顯示,這便於我們快速發現錯誤,而你修改程式碼並儲存後,頁面將立即出現對應結果,而不會重新載入整個頁面,這是通過 webpack 的 模組熱替換 實現的,Next 預設支援這個功能
- 嘗試著錯一次:將
頁面間導航
Introduction
- 我們的應用程式雖然很簡單,只有一個頁面,但是我們可以新增任意多個頁面,例如:
- 建立
pages/about.js
來新建 “About” 頁面const About = () => ( <div> <p>About Page</p> </div> ); export default About;
- 修改
pages/index.js
:const Index = () => ( <div> <p>Hello Next.js</p> <a href="http://localhost:3000/about">This is a link to About-Page</a> </div> ); export default Index;
- 之後我們可以通過 http://localhost:3000/about 來訪問該頁面
- 之後,我們需要連線兩個頁面,首先想到的是可以用一個 HTML 的
<a />
標籤實現,但是結果就是:瀏覽器會向伺服器請求下一頁並重新整理當前頁面,也就是這樣做並不會執行客戶端導航
- 建立
- 為了支援瀏覽器端導航,我們需要使用 Next.js 提供的
Link
元件,這個元件是通過next/link
匯出的,接下來我們將使用它 - 我們需要準備一個簡單的 Next.js 應用課程,請在
hello-next
下輸入:git clone https://github.com/zeit/next-learn-demo.git
- 現在我們進入
hello-next/next-learn-demo/1-navigate-between-pages
啟動程式:cd next-learn-demo/1-navigate-between-pages npm install npm run dev
- 開啟 http://localhost:3000/ 訪問該程式
使用Link元件
注意,接下來的操作均在
hello-next/next-learn-demo/1-navigate-between-pages
下完成的
- 在
pages/index.js
中新增:import Link from 'next/link'; export default function Index() { return ( <div> <p>Hello next.js</p> <Link href="/about"> <a>About Page</a> </Link> </div> ) }
- 在這裡,我們將
next/link
匯入為Link
,並按照如下的方式使用:<Link href="/about"> <a>About Page</a> </Link>
- 訪問 3000 埠可檢視結果
- 在這裡,我們將
- 這次點選連結同樣會導航到 “About” 頁面,這是客戶端導航,操作在瀏覽器中進行,而不向瀏覽器傳送請求,你可以通過開啟瀏覽器的 網路請求檢查器(network request inspector) 來驗證這一點
- 後退按鈕:
當你點選連結,再點選後退時,依然會切換到歷史記錄的上一頁,也就是next/link
為你完成了所有location.history
的操作
新增連結道具
- 或許你需要在連線中新增屬性或道具,比如你需要向連結中新增
title
屬性,我們可以這樣新增它:
檢視元素,可以看到結果如下:<Link href="/about"> <a title="About-Pages">About Page</a> </Link>
- 切記不可新增到錯誤的地方去,若寫成如下:
則會在控制檯中報錯:<Link href="/about" title="About-Pages"> <a>About Page</a> </Link>
- 實際上,
Link
元件上的標題道具無效,是因為 Link 只是一個包裝器元件,只接收href
和一些類似的道具。如果需要向其新增道具,則需要將道具新增到其子項,這種情況下,Link
元件的子代是錨標記
使用共享元件
Introduction
- 我們可以通過匯出 React 元件並將該元件放在
pages
目錄中來建立頁面,每個頁面的 URL 都是基於檔名的,由於匯出的頁面是 JavaScript 模組,因此我們也可以將其他 JavaScript 元件匯入其中 - 我們將建立一個公共的 Header 元件並將其用於多個頁面,最後我們將研究實現 Layout 元件,並瞭解它如何幫我們定義多個頁面的外觀
執行
- 我們之前已經安裝過了
next-learn-demo
,這裡直接使用:- 進入
hello-next/next-learn-demo/2-using-shared-components
,之後我們的操作也會在此目錄下
- 進入
- 執行:
npm install npm run dev
建立標題元件
- 下面建立一個 Header 元件,建立
2-using-shared-components/components/Header.js
:import Link from 'next/link'; const linkStyle = { marginRight: 15, }; const Header = () => ( <div> <Link href="/"> <a style={marginRight}>Home</a> </Link> <Link href="/about"> <a style={marginRight}>About</a> </Link> </div> ); export default Header;
- 現在,匯入 Header 元件並在頁面中使用它:
- 將
index.js
修改為:import Header from '../components/Header'; export default function Index() { return ( <div> <Header /> <p>Hello Next.js</p> </div> ) }
- 將
about.js
修改為:import Header from '../components/Header'; export default function Index() { return ( <div> <Header /> <p>This is the About Page</p> </div> ) }
- 啟動之後可以檢視結果
- 將
- 試錯:現在將
components
目錄改名為comps
,報錯如下:- 我們不需要將我們的元件放在一個特殊的目錄裡,也就是說,該元件目錄名稱可以取為任何,實際上,唯一特殊的目錄是
/pages
和/public
,你甚至可以在/pages
裡面建立元件.
- 我們不需要將我們的元件放在一個特殊的目錄裡,也就是說,該元件目錄名稱可以取為任何,實際上,唯一特殊的目錄是
佈局元件
本節依然是在
2-using-shared-components/
下完成的
- 我們將建立 Layout 元件,以實現各頁面上的通用樣式,在
components/MyLayout.js
中輸入:import Header from './Header'; const layoutStyle = { margin: 20, padding: 20, border: '1px solid #DDD' }; const Layout = props => ( <div style={layoutStyle}> <Header /> {props.children} </div> ); export default Layout;
- 完成操作後,我們可以在頁面中使用以下佈局:
- 在
pages/index.js
中輸入:import Layout from '../components/MyLayout'; export default function Index() { return ( <Layout> <p>Hello Next.js</p> </Layout> ) }
- 在
pages/about.js
中輸入:import Layout from '../components/MyLayout'; export default function About() { return ( <Layout> <p>This is the about page</p> </Layout> ) }
- 啟動,檢視樣式
- 在
- 試錯:將
MyLayout.js
中的{props.children}
刪除,再啟動,觀察結果:- 頁面上只保留了
Header
的內容,其他的均消失了
- 頁面上只保留了
渲染子元件
- 前一個試錯中,我們刪除了
{props.children}
,則Layout
無法呈現我們放入Layout
元素內的內容,如下所示:
但這只是建立佈局元件的一種方法,以下是其他方法。export default function About() { return ( <Layout> <p>This is the about page</p> </Layout> ); }
- 方法一:佈局為高階元件
// components/MyLayout.js import Header from './Header'; const layoutStyle = { margin: 20, padding: 20, border: '1px solid #DDD' }; const withLayout = Page => { return () => ( <div style={layoutStyle}> <Header /> <Page /> </div> ); }; export default withLayout;
// pages/index.js import withLayout from '../components/MyLayout'; const Page = () => <p>Hello Next.js</p>; export default withLayout(Page);
// pages/about.js import withLayout from '../components/MyLayout'; const Page = () => <p>This is the about page</p>; export default withLayout(Page);
- 方法二:頁面內容作為道具
// components/MyLayout.js import Header from './Header'; const layoutStyle = { margin: 20, padding: 20, border: '1px solid #DDD' }; const Layout = props => ( <div style={layoutStyle}> <Header /> {props.content} </div> ); export default Layout;
// pages/index.js import Layout from '../components/MyLayout.js'; const indexPageContent = <p>Hello Next.js</p>; export default function Index() { return <Layout content={indexPageContent} />; }
// pages/about.js import Layout from '../components/MyLayout.js'; const aboutPageContent = <p>This is the about page</p>; export default function About() { return <Layout content={aboutPageContent} />; }
建立動態頁面
Introduction
- 之前通過使用元件,我們建立了包含了多個頁面的小案例,之前為了建立一個頁面,我們必須新建一個檔案作為模組匯出,但是在一個真正的應用程式中,我們還需動態地建立頁面以顯示動態內容,接下來我們會使用 查詢字串 來實現這一點
- 我們將建立一個簡單的部落格應用,它在主頁上展示一個所有文章的列表,展示如下:
- 主頁有文章列表
- 點選某標題的連結,會出現對應的文章
- 主頁有文章列表
安裝設定
- 我們仍然使用之前安裝過的
next-learn-demo
,進入next-learn-demo/3-create-dynamic-pages
,接下來的一切也將在這個目錄下完成 - 執行
npm install npm run dev
新增文章列表
- 首先,我們在文章主頁新增標題列表,如下:
import Link from 'next/link'; import Layout from '../components/MyLayout.js'; const PostLink = props => ( <li> <Link href={`/post?title=${props.title}`}> <a>{props.title}</a> </Link> </li> ); export default function Blog() { return ( <Layout> <h1>My Blog</h1> <ul> <PostLink title="Hello Next.js" /> <PostLink title="Learn Next.js is awesome" /> <PostLink title="Deploy apps with Zeit" /> </ul> </Layout> ) }
通過查詢字串傳遞資料
- 我們將通過查詢字串作為引數(也稱查詢引數)並傳遞資料,如:
// pages/index.js const PostLink = props => ( <li> <Link href={`/post?title=${props.title}`}> <a>{props.title}</a> </Link> </li> );
- 此例中,查詢引數是
title
,我們使用PostLink
來執行的操作 - 你也可以檢查
Link
元件的href
屬性,以此類推,你可以使用查詢字串傳遞任何型別的資料
- 此例中,查詢引數是
建立Post
頁面
- 現在我們需要建立 post 頁面來顯示部落格文章,為此,我們需要從查詢字串中獲得標題,
- 建立
pages/post.js
檔案:import { useRouter } from 'next/router'; import Layout from '../components/MyLayout'; const Page = () => { const router = useRouter(); return ( <Layout> <h1>{router.query.title}</h1> <p>This is the blog post content.</p> </Layout> ); }; export default Page;
- 啟動專案,並點選三個標題連結
- 上面的運作過程如下:
- 首先從
next/router
匯入並使用useRouter
函式,該函式返回 Next.js 的是router
物件 - 使用路由器(router)中的
query
物件,該物件儲存了所有查詢引數 - 然後,使用
router.query.title
獲取標題
- 首先從
- useRouter 函式的介紹:
- useRouter 允許你訪問頁面中的 router 物件,它是一個 React Hook,能與功能元件協同合工作
- 之前的示例中,useRouter 函式被放到預新增的頁面元件中,而下面示例中,useRouter 函式在
Content
元件中,預新增的元件是Page
,但是功能不變import { useRouter } from 'next/router'; import Layout from '../components/MyLayout'; const Content = () => { const router = useRouter(); return ( <> <h1>{router.query.title}</h1> <p>This is the blog post content.</p> </> ); }; const Page = () => ( <Layout> <Content /> </Layout> ); export default Page;
使用動態路由清理URL
Introduction
請確保你正在使用是 Next.js 9 或更高版本
接下來的操作都會在next-learn-demo/4-clean-urls
中進行,請調至指定目錄
- 我們已經知道了如何使用查詢字串建立動態頁面,指向我們的某個部落格文章的連結如:
而此連結要表達的卻是:http:// localhost:3000 / post?title = Hello%20Next.js
http:// localhost:3000 / p / hello-nextjs
動態路由
啟動專案,在
4-clean-urls/
下輸入:npm install npm run dev
我們將使用 Next.js 的 動態路由 功能,它允許你處理
/pages
動態路由現在我們將建立新頁面,並命名為
pages/p/[id].js
,這也是我們建立的第一個動態路由,步驟如下:- 首先,在
/pages
內新增資料夾/p
- 然後,你需要在
/p
資料夾中建立[id].js
,並在這些 js 檔案中新增如下內容:import { useRouter } from 'next/router'; import Layout from '../../components/MyLayout'; export default function Post() { const router = useRouter(); return ( <Layout> <h1>{router.query.id}</h1> <p>This is the blog post content.</p> </Layout> ); };
- 首先,在
前一頁是特殊的,它不會處理
/about
等靜態路由,而是會處理p/
之後的路由,例如,此頁面將處理/p/hello-next.js
,而/p/post-1/another
頁面名稱中的帶有方括號(
[]
)使其成為動態路由,你不能使頁面名稱的一部分成為動態名稱,而只能使全名成為動態名稱,例如,支援/pages/p/[id].js
,但不支援/pages/p/post-[id].js
建立動態路線時,我們在方括號([])之間新增了 id,這是頁面接受到查詢引數的名稱,因此對於
/p/hello-nextjs
,該query
物件將具有{ id: 'hello-nextjs' }
,我們可以使用 useRouter() 進行訪問現在,我們新的動態路由新增多個連結,修改
pages/index.js
:// pages/index.js import Layout from '../components/MyLayout'; import Link from 'next/link'; const PostLink = props => ( <li> <Link href="/p/[id]" as={`/p/${props.id}`}> <a>{props.id}</a> </Link> </li> ) export default function Blog() { return ( <Layout> <h1>My Blog</h1> <ul> <PostLink id="Hello-Next.js" /> <PostLink id="Learn-Next.js" /> <PostLink id="Deploy-Next.js" /> </ul> </Layout> ) }
- 著重看看以下內容:
const PostLink = props => ( <li> <Link href="/p/[id]" as={`/p/${props.id}`}> <a>{props.id}</a> </Link> </li> )
- 在
<Link>
元素中,href
代表的是該頁面在pages
資料夾中的路徑,而as
代表的是該頁面在瀏覽器中的 URL 路徑
- 在
- 現在,你可以重新啟動專案,注意觀察 URL 的變化!
- 著重看看以下內容:
動態路由可以很好地和瀏覽器歷史記錄配合使用,而我們要做的就是將
as
新增到連結元件中
為頁面獲取資料
Introduction
接下來的操作都會在
next-learn-demo/6-fetching-data
中進行,請調至指定目錄
- 現在我們已經能建立一個相對完整的 Next.js 應用,但還沒有解決的是:如何從遠端資料來源中獲取資料?,Next.js 提供了一個標準 API 來獲取頁面所需的資料,即
getInitialProps 非同步函式
getInitialProps
只能新增到頁面匯出的預設元件中,在其他元件中是不會起作用的,它可以從遠端資料來源為指定頁面獲取資料,並將這些資料通過 props 傳遞到我們的頁面,它會同時在客戶端和伺服器上工作,因為它在兩個環境中都會被呼叫- 我們將利用
getInitialProps
構建一個應用程式來顯示有關 Batman TV Shows 的資訊,利用的是公開的 TVmaze API - 在即將演示的示例中,我們的主頁上有一個文章列表,現在我們來展示
Batman TV shows
的節目列表,我們將從遠端伺服器上獲取這些節目列表,而不是硬編碼- 在這個示例中,我們使用的是 TVMaze API 來獲取 TV shows 節目列表,這是一個搜尋電視節目的 API
安裝設定
- 進入
next-learn-demo/6-fetching-data
,輸入:npm install npm run dev
- 在瀏覽器中開啟 http://localhost:3000/ 檢視專案
獲取 Batman Shows 的資料
- 首先,我們需要安裝 isomorphic-unfetch,這是我們用來獲取資料的工具庫,這是瀏覽器的 fetch API 的一個簡單實現,但在客戶端和伺服器環境中都可以使用
npm install --save isomorphic-unfetch
- 將
pages/index.js
替換為以下內容:// pages/index.js import Layout from '../components/MyLayout'; import Link from 'next/link'; import fetch from 'isomorphic-unfetch'; const Index = props => ( <Layout> <h1>Batman TV Shows</h1> <ul> { props.shows.map(show => ( <li key={show.id}> <Link href="/p/[id]" as={`/p/${show.id}`}> <a>{show.name}</a> </Link> </li> )) } </ul> </Layout> ); Index.getInitialProps = async function() { const res = await fetch('https://api.tvmaze.com/search/shows?q=batman'); const data = await res.json(); console.log(`Show data fetched. Count: ${data.length}`); return { shows: data.map(entry => entry.show) }; }; export default Index;
- 我們著重分析下面這部分:
Index.getInitialProps = async function() { const res = await fetch('https://api.tvmaze.com/search/shows?q=batman'); const data = await res.json(); console.log(`Show data fetched. Count: ${data.length}`); return { shows: data.map(entry => entry.show) }; };
- 這是一個靜態非同步函式,可以新增到程式的任何頁面中,使用此函式,我們就可以獲取資料並作為 props 傳遞給我們的頁面
- 以下便是我們的抓取結果,資料被抓取後,將會作為 props 的 ‘show’ 屬性傳遞我們的頁面中
- 注意,我們之前有一行用於列印資訊的程式碼:
console.log(`Show data fetched. Count: ${data.length}`);
- 那麼到底是在伺服器端輸出呢,還是在瀏覽器端的控制檯輸出呢,現在重新整理一下瀏覽器,會發現之後服務端的控制檯顯示
- 在這種情況下,訊息只會在服務端輸出,因為我們的頁面是在服務端繪製的,所以,我們在服務端已經有了資料,沒有必要在客戶端再次獲取這些資料
- 那麼到底是在伺服器端輸出呢,還是在瀏覽器端的控制檯輸出呢,現在重新整理一下瀏覽器,會發現之後服務端的控制檯顯示
實現 Post 頁面
- 現在讓我們把 TV show 的詳細資訊新增到 post 中:將
pages/p/[id].js
替換為以下內容:// pages/p/[id].js import Layout from '../../components/MyLayout'; import fetch from 'isomorphic-unfetch'; const Post = props => ( <Layout> <h1>{props.show.name}</h1> <p>{ props.show.summary.replace(/<[/]?[pb]>/g), '' }</p> </Layout> ); Post.getInitialProps = async function(context) { const { id } = context.query; const res = await fetch(`https://api.tvmaze.com/shows/${id}`); const show = await res.json(); console.log(`Fetched show: ${show.name}`); return { show }; }; export default Post;
- 注意該頁的
getInitialProps
:Post.getInitialProps = async function(context) { const { id } = context.query; const res = await fetch(`https://api.tvmaze.com/shows/${id}`); const show = await res.json(); console.log(`Fetched show: ${show.name}`); return { show }; };
- 該函式的第一個引數是 context 物件,此物件包含一個
query
物件,我們用context.query
來獲取資訊,即id
物件,並使其在 TVMaze API 中獲取電視節目資料
- 注意該頁的
- 在這個
getInitialProps
函式中,我們新增了一個console.log
來列印節目的標題,現在我們看看它將列印到哪裡- 開啟伺服器和客戶端的控制檯,然後啟動專案,訪問 3000 埠
- 單擊第一個 Batman show 的標題
- 結果是:會在客戶端的控制檯輸出
- 與之前不同的是,我們這次只能在客戶端看到訊息,這是因為我們通過客戶端導航到了 post 頁面
- 當我們單擊連結時,由於該連結是被 Next.js 的
<Link>
元件包裝過的元件,所以頁面轉換將在瀏覽器中進行,而不會想伺服器發起請求 - 但是,如果你直接訪問的是 post 頁面,而不是點選連結(例如,你直接訪問 http://localhost:3000/p/975 ),訊息會被列印在服務端,而不是客戶端
持續更新中……
本作品採用《CC 協議》,轉載必須註明作者和本文連結