Next.js 入門教程

Ozzie發表於2020-02-17

簡介

最近才學習了 React,現在要接觸服務端渲染,趁熱打鐵把 Next 學一下,關於 Next.js,可以移步到知乎討論:
知乎:關於 Next.js 的討論

初識Next.js

安裝

  1. Next.js 支援 Windows、Mac 和 Linux系統,均可安裝,但是前提是你已經安裝了 Node.js
  2. 建立示例專案的過程如下:
     mkdir hello-next
     cd hello-next
     npm init -y
     npm install --save react react-dom next
     mkdir pages

使用

  1. 開啟 hello-next/package.json,替換 scripts
     "scripts": {
         "dev": "next",
         "build": "next build",
         "start": "next start"
     }
  2. 啟動
     npm run dev
    在瀏覽器中開啟 http://localhost:3000,你會看到頁面顯示 404 | This page could not be found.
  3. 建立你的第一個頁面
    • 建立 pages/index.js,並輸入:
        const Index = () => (
          <div>
            <p>Hello Next.js</p>
          </div>
        );
        export default Index;
    • 再次輸入 npm run dev,就能看到效果了
    • 上述案例中,我們在 pages/index.js 模組中預設(default)匯出了一個簡單的 React 元件
  4. 試錯
    • 嘗試著錯一次:將 pages/index.js 改為:
        const Index = () => (
          <div>
            <p>Hello Next.js
          </div>
        );
        export default Index;
    • 重新啟動,瀏覽器顯示:
    • 一般情況下,Next.js 將跟蹤此類錯誤並在瀏覽器中顯示,這便於我們快速發現錯誤,而你修改程式碼並儲存後,頁面將立即出現對應結果,而不會重新載入整個頁面,這是通過 webpack 的 模組熱替換 實現的,Next 預設支援這個功能

頁面間導航

Introduction

  1. 我們的應用程式雖然很簡單,只有一個頁面,但是我們可以新增任意多個頁面,例如:
    • 建立 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 /> 標籤實現,但是結果就是:瀏覽器會向伺服器請求下一頁並重新整理當前頁面,也就是這樣做並不會執行客戶端導航
  2. 為了支援瀏覽器端導航,我們需要使用 Next.js 提供的 Link 元件,這個元件是通過 next/link 匯出的,接下來我們將使用它
  3. 我們需要準備一個簡單的 Next.js 應用課程,請在 hello-next 下輸入:
     git clone https://github.com/zeit/next-learn-demo.git
  4. 現在我們進入 hello-next/next-learn-demo/1-navigate-between-pages 啟動程式:
     cd next-learn-demo/1-navigate-between-pages
     npm install
     npm run dev

使用Link元件

注意,接下來的操作均在 hello-next/next-learn-demo/1-navigate-between-pages 下完成的

  1. 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 埠可檢視結果
  2. 這次點選連結同樣會導航到 “About” 頁面,這是客戶端導航,操作在瀏覽器中進行,而不向瀏覽器傳送請求,你可以通過開啟瀏覽器的 網路請求檢查器(network request inspector) 來驗證這一點
  3. 後退按鈕:
    當你點選連結,再點選後退時,依然會切換到歷史記錄的上一頁,也就是 next/link 為你完成了所有 location.history 的操作

新增連結道具

  1. 或許你需要在連線中新增屬性或道具,比如你需要向連結中新增 title 屬性,我們可以這樣新增它:
     <Link href="/about">
         <a title="About-Pages">About Page</a>
     </Link>
    檢視元素,可以看到結果如下:
  2. 切記不可新增到錯誤的地方去,若寫成如下:
     <Link href="/about" title="About-Pages">
         <a>About Page</a>
     </Link>
    則會在控制檯中報錯:
  3. 實際上,Link 元件上的標題道具無效,是因為 Link 只是一個包裝器元件,只接收 href 和一些類似的道具。如果需要向其新增道具,則需要將道具新增到其子項,這種情況下,Link 元件的子代是錨標記

使用共享元件

Introduction

  1. 我們可以通過匯出 React 元件並將該元件放在 pages 目錄中來建立頁面,每個頁面的 URL 都是基於檔名的,由於匯出的頁面是 JavaScript 模組,因此我們也可以將其他 JavaScript 元件匯入其中
  2. 我們將建立一個公共的 Header 元件並將其用於多個頁面,最後我們將研究實現 Layout 元件,並瞭解它如何幫我們定義多個頁面的外觀

執行

  1. 我們之前已經安裝過了 next-learn-demo,這裡直接使用:
    • 進入 hello-next/next-learn-demo/2-using-shared-components,之後我們的操作也會在此目錄下
  2. 執行:
     npm install
     npm run dev

建立標題元件

  1. 下面建立一個 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;
  2. 現在,匯入 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>
          )
        }
    • 啟動之後可以檢視結果
  3. 試錯:現在將 components 目錄改名為 comps,報錯如下:
    • 我們不需要將我們的元件放在一個特殊的目錄裡,也就是說,該元件目錄名稱可以取為任何,實際上,唯一特殊的目錄是 /pages/public,你甚至可以在 /pages 裡面建立元件.

佈局元件

本節依然是在 2-using-shared-components/ 下完成的

  1. 我們將建立 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;
  2. 完成操作後,我們可以在頁面中使用以下佈局:
    • 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>
          )
        }
    • 啟動,檢視樣式
  3. 試錯:將 MyLayout.js 中的 {props.children} 刪除,再啟動,觀察結果:
    • 頁面上只保留了 Header 的內容,其他的均消失了

渲染子元件

  1. 前一個試錯中,我們刪除了 {props.children},則 Layout 無法呈現我們放入 Layout 元素內的內容,如下所示:
     export default function About() {
       return (
         <Layout>
           <p>This is the about page</p>
         </Layout>
       );
     }
    但這只是建立佈局元件的一種方法,以下是其他方法。
  2. 方法一:佈局為高階元件
     // 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);
  3. 方法二:頁面內容作為道具
     // 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

  1. 之前通過使用元件,我們建立了包含了多個頁面的小案例,之前為了建立一個頁面,我們必須新建一個檔案作為模組匯出,但是在一個真正的應用程式中,我們還需動態地建立頁面以顯示動態內容,接下來我們會使用 查詢字串 來實現這一點
  2. 我們將建立一個簡單的部落格應用,它在主頁上展示一個所有文章的列表,展示如下:
    • 主頁有文章列表
    • 點選某標題的連結,會出現對應的文章

安裝設定

  1. 我們仍然使用之前安裝過的 next-learn-demo,進入 next-learn-demo/3-create-dynamic-pages,接下來的一切也將在這個目錄下完成
  2. 執行
     npm install
     npm run dev

新增文章列表

  1. 首先,我們在文章主頁新增標題列表,如下:
     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>
       )
     }

通過查詢字串傳遞資料

  1. 我們將通過查詢字串作為引數(也稱查詢引數)並傳遞資料,如:
     // pages/index.js
     const PostLink = props => (
       <li>
         <Link href={`/post?title=${props.title}`}>
           <a>{props.title}</a>
         </Link>
       </li>
     );
    • 此例中,查詢引數是 title,我們使用 PostLink 來執行的操作
    • 你也可以檢查 Link 元件的 href 屬性,以此類推,你可以使用查詢字串傳遞任何型別的資料

建立Post頁面

  1. 現在我們需要建立 post 頁面來顯示部落格文章,為此,我們需要從查詢字串中獲得標題,
  2. 建立 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;
    • 啟動專案,並點選三個標題連結
  3. 上面的運作過程如下:
    • 首先從 next/router 匯入並使用 useRouter 函式,該函式返回 Next.js 的是 router 物件
    • 使用路由器(router)中的 query 物件,該物件儲存了所有查詢引數
    • 然後,使用 router.query.title 獲取標題
  4. 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 中進行,請調至指定目錄

  1. 我們已經知道了如何使用查詢字串建立動態頁面,指向我們的某個部落格文章的連結如:
     http:// localhost:3000 / post?title = Hello%20Next.js
    而此連結要表達的卻是:
     http:// localhost:3000 / p / hello-nextjs

動態路由

  1. 啟動專案,在 4-clean-urls/ 下輸入:

     npm install
     npm run dev
  2. 我們將使用 Next.js 的 動態路由 功能,它允許你處理 /pages 動態路由

  3. 現在我們將建立新頁面,並命名為 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>
          );
        };
  4. 前一頁是特殊的,它不會處理 /about 等靜態路由,而是會處理 p/ 之後的路由,例如,此頁面將處理 /p/hello-next.js,而 /p/post-1/another

  5. 頁面名稱中的帶有方括號([])使其成為動態路由,你不能使頁面名稱的一部分成為動態名稱,而只能使全名成為動態名稱,例如,支援 /pages/p/[id].js,但不支援 /pages/p/post-[id].js

  6. 建立動態路線時,我們在方括號([])之間新增了 id,這是頁面接受到查詢引數的名稱,因此對於 /p/hello-nextjs,該 query 物件將具有 { id: 'hello-nextjs' },我們可以使用 useRouter() 進行訪問

  7. 現在,我們新的動態路由新增多個連結,修改 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 的變化!
  8. 動態路由可以很好地和瀏覽器歷史記錄配合使用,而我們要做的就是將 as 新增到連結元件中


為頁面獲取資料

Introduction

接下來的操作都會在 next-learn-demo/6-fetching-data 中進行,請調至指定目錄

  1. 現在我們已經能建立一個相對完整的 Next.js 應用,但還沒有解決的是:如何從遠端資料來源中獲取資料?,Next.js 提供了一個標準 API 來獲取頁面所需的資料,即 getInitialProps 非同步函式
  2. getInitialProps 只能新增到頁面匯出的預設元件中,在其他元件中是不會起作用的,它可以從遠端資料來源為指定頁面獲取資料,並將這些資料通過 props 傳遞到我們的頁面,它會同時在客戶端和伺服器上工作,因為它在兩個環境中都會被呼叫
  3. 我們將利用 getInitialProps 構建一個應用程式來顯示有關 Batman TV Shows 的資訊,利用的是公開的 TVmaze API
  4. 在即將演示的示例中,我們的主頁上有一個文章列表,現在我們來展示 Batman TV shows 的節目列表,我們將從遠端伺服器上獲取這些節目列表,而不是硬編碼
    • 在這個示例中,我們使用的是 TVMaze API 來獲取 TV shows 節目列表,這是一個搜尋電視節目的 API

安裝設定

  1. 進入 next-learn-demo/6-fetching-data,輸入:
     npm install
     npm run dev
  2. 在瀏覽器中開啟 http://localhost:3000/ 檢視專案

獲取 Batman Shows 的資料

  1. 首先,我們需要安裝 isomorphic-unfetch,這是我們用來獲取資料的工具庫,這是瀏覽器的 fetch API 的一個簡單實現,但在客戶端和伺服器環境中都可以使用
     npm install --save isomorphic-unfetch
  2. 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;
  3. 我們著重分析下面這部分:
     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’ 屬性傳遞我們的頁面中
  4. 注意,我們之前有一行用於列印資訊的程式碼:
     console.log(`Show data fetched. Count: ${data.length}`);
    • 那麼到底是在伺服器端輸出呢,還是在瀏覽器端的控制檯輸出呢,現在重新整理一下瀏覽器,會發現之後服務端的控制檯顯示
    • 在這種情況下,訊息只會在服務端輸出,因為我們的頁面是在服務端繪製的,所以,我們在服務端已經有了資料,沒有必要在客戶端再次獲取這些資料

實現 Post 頁面

  1. 現在讓我們把 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 中獲取電視節目資料
  2. 在這個 getInitialProps 函式中,我們新增了一個 console.log 來列印節目的標題,現在我們看看它將列印到哪裡
    • 開啟伺服器和客戶端的控制檯,然後啟動專案,訪問 3000 埠
    • 單擊第一個 Batman show 的標題
  3. 結果是:會在客戶端的控制檯輸出
    • 與之前不同的是,我們這次只能在客戶端看到訊息,這是因為我們通過客戶端導航到了 post 頁面
    • 當我們單擊連結時,由於該連結是被 Next.js 的 <Link> 元件包裝過的元件,所以頁面轉換將在瀏覽器中進行,而不會想伺服器發起請求
    • 但是,如果你直接訪問的是 post 頁面,而不是點選連結(例如,你直接訪問 http://localhost:3000/p/975 ),訊息會被列印在服務端,而不是客戶端

持續更新中……

本作品採用《CC 協議》,轉載必須註明作者和本文連結