Next.js踩坑入門系列(七) —— 其他相關知識

luffyZhou發表於2018-11-04

Next.js踩坑入門系列

獲取資料&&getInitialProps

獲取資料,依然是Next與普通的React SPA應用不同的地方,React應用基本都有自己的路由元件(當然大部分是react-router),我們可以通過路由元件為我們提供的方法,比如react-router的onEnter()方法或者universal-router的beforeEnter()方法。

這裡給大家推薦一個區別於react-router的路由元件universal-router

而Next.js沒有路由元件,所以具體方式肯定不同於路由元件的方式,具體不同就體現在Next.js為我們提供了一個區別於React的新生命週期——getIntialProps(),下面來說說這個API的牛X之處。

使用方法

  • 在React.Component使用
   import React from 'react'

   export default class extends React.Component {
     static async getInitialProps({ req }) {
       const userAgent = req ? req.headers['user-agent'] : navigator.userAgent
       return { userAgent }
     }
   
     render() {
       return (
         <div>
           Hello World {this.props.userAgent}
         </div>
       )
     }
   }

複製程式碼
  • 在stateless元件內使用
   const Page = ({ stars }) =>
     <div>
       Next stars: {stars}
     </div>
   
   Page.getInitialProps = async ({ req }) => {
     const res = await fetch('https://api.github.com/repos/zeit/next.js');
     const json = await res.json();
     return { stars: json.stargazers_count };
   }
   
   export default Page;

複製程式碼

這個生命週期是脫離於React的正常生命週期的,不過我們依然可以在元件里正常使用react元件的各種生命週期函式。

服務端可用

這真是getInitialProps這個生命週期的過人之處了,他可以在服務端執行,這樣做有什麼好處呢?說實話,我真不太清楚,我只知道一點,下面會講,哈哈。如果有大牛知道的話,可以在留言給我講講~話不多說,上圖:

Next.js踩坑入門系列(七) —— 其他相關知識
可以看到,這個生命週期我觸發了action獲取資料,而這個action在控制檯被列印出來了,說明可以執行在服務端~

減少抓取資料的次數

  • React老生命週期內獲取資料

    以抓取使用者列表為例,我們可以在元件裡的componentDidMount生命週期內獲取

 // /components/user/userList.js
 ...
 componentDidMount() {
    this.props.fetchUserList();
 }
複製程式碼

Next.js踩坑入門系列(七) —— 其他相關知識
從上圖我們可以看出來,每次進入使用者列表頁,都會重新抓取使用者資料。有人可能會說,這不廢話嗎,react不就這樣嗎,路由都切換了啊。沒錯,正常就是應該這樣,所以才說Next.js的這個新生命週期牛逼啊。

  • 使用getInitialProps生命週期
// /pages/user/userList.js
import UserList from '../../containers/user/UserList';
import { fetchUserListData } from '../../redux/actions/user';

UserList.getInitialProps = async (props) => {
  const { store, isServer } = props.ctx;
  if (store.getState().user.list.list.length === 0) {
    store.dispatch(fetchUserListData());
  }
  return { isServer };
};

export default UserList;


複製程式碼

Next.js踩坑入門系列(七) —— 其他相關知識
兄弟們,看看上圖,發現沒,進入系統後只會在第一次進入路由的時候獲取資料,之後再進入因為服務端快取過資料,所以不需要重新獲取,減少了獲取次數~

具體原因就是因為static getInitialProps()這個生命週期是可以在服務端執行的,當頁面第一次載入時,伺服器收到請求,getInitialProps()會執行,getInitialProps()返回的資料,會序列化後新增到 window.__NEXT_DATA__.props上,寫入HTML原始碼裡,類似於。這樣服務端的getInitialProps()就實現了把資料傳送給了客戶端。當我們通過Next.js的路由Link來進行頁面跳轉的時候,客戶端就會從window.__NEXT_DATA__裡獲取資料渲染頁面,就無需重新獲取資料,算是提升效能的話一種方式吧~如下圖所示:

Next.js踩坑入門系列(七) —— 其他相關知識

存在問題——踩坑

這裡其實還真遇到一個坑,可能有很多人遇到過了,也可能沒人遇到過。具體問題描述起來大概是這個樣子,我們在getInitialProps裡面預獲取資料,以使用者列表為例,在首次載入的時候都是沒有問題的包括各種客戶端跳轉。不過當我們在使用者列表頁面進行重新整理的時候,其實他就沒有再走getInitialProps這個生命週期了,因此頁面會沒有可以渲染的資料,就會出現空頁面,因為他認為這個應該從window.__Next_DATA__裡面獲取,而不是重新獲取資料~那麼為什麼重新整理頁面之後沒有走這個getIntialProps,講道理,我還真沒太弄清楚,不過確實重新整理頁面next.js會給我們在props裡返回一個isServer:true,但是控制檯並沒有獲取資料。具體問題見下面截圖:

Next.js踩坑入門系列(七) —— 其他相關知識
從截圖我們可以很清楚地看到,頁面資料通過redux-saga獲取,在pages的getIntialProps()裡面,程式碼如下:

import { fetchUserListData } from '../../redux/actions/user';

UserList.getInitialProps = async (props) => {
  const { store, isServer } = props.ctx;
  if (store.getState().user.list.list.length === 0) {
    store.dispatch(fetchUserListData());
  }
  return { isServer };
};
複製程式碼

上面fetchUserListData()就是抓取資料的action,返回值就會存入state,渲染資料列表。很明顯,在第一次載入的時候是抓取成功的。但是重新整理頁面後,沒有dispatch這個action,也就是表明,重新整理頁面沒有走這個getIntialProps這個生命週期!!!

上面才是關鍵問題所在,不重新整理頁面的情況下是正常的,重新整理頁面沒有走這個生命週期,而我們很多資料都是需要預獲取的,所以說還挺坑的,事實上,很多人遇到這個問題,而且我在next官方給出的reudx-demo裡面也發現這個問題,也就是說他們官方的demo重新整理也會出現這個問題。

解決辦法

既然是踩坑,當然有解決辦法啦~而且還是兩種:

  • 第一種:在元件生命週期裡判斷isServer

    剛剛問題描述過了,也就是正常載入和通過路由跳轉頁面,資料會正常渲染且會從瀏覽器的window.__NEXT_DATA__獲取來減少不必要的網路請求~,而在頁面進行重新整理的時候不會重新請求資料並且window.__NEXT_DATA__裡也找不到我們想要的資料。不過通過控制檯資訊我們可以發現問題所在以及解決辦法。那就是,第一次啟動系統的時候返回的isServer是false,而瀏覽器重新整理頁面的時候isServer返回的是true,我們可以在元件裡進行這個變數的判斷,如果是true,就重新進行一次資料抓取。

// /components/user/UserList.js
...
componentDidMount() {
  if(this.props.isServer) {
  // 需要重新抓取資料
    this.props.fetchUserListData();
  }
}
...
複製程式碼

Next.js踩坑入門系列(七) —— 其他相關知識
從上圖可以看到,重新整理頁面的時候,我們會重新獲取資料渲染頁面,如果不重新整理就不會重新獲取。還是可行的這個方法~

  • 第二種:換一種方式預獲取資料

    另一種方法就比較高階了,原理我依然不知道,但是就是好用,哈哈,這東西真是邪門,為什麼這麼說呢,其實本質沒改變什麼,就是換了種寫法就可以。具體就是,上面的寫法我在getInitalProps裡面寫了dispatch了一個獲取資料的action,從上一節或者程式碼裡你們可以看到,其實這個action就是fetch一個api獲取資料返回state。這就是redux一個獲取資料的基本過程,這種方法在重新整理時行不通,而行得通的方法是:不通過dispatch action的方式獲取資料,而是直接在getIntialProps裡面通過fetch api的方式獲取資料,這樣每次重新整理頁面也都可以獲取到資料了。。。就是這麼神奇,我也真不知道為啥。

// /pages/user/userList
import fetch from 'isomorphic-unfetch';
import UserList from '../../containers/user/UserList';
import { fetchUserListDataSuccess } from '../../redux/actions/user';

UserList.getInitialProps = async (props) => {
  const { store, isServer } = props.ctx;
  let userData;
  if (store.getState().user.list.list.length === 0) {
    const res = await fetch('https://jsonplaceholder.typicode.com/users');
    userData = await res.json();
    store.dispatch(fetchUserListDataSuccess(userData));
  }
  return { isServer };
};

export default UserList;
複製程式碼

Next.js踩坑入門系列(七) —— 其他相關知識

就是很神奇有木有,說實話我是真不知道為啥,有大牛的話真心給我講講萬分感謝了~ 不過這兩種寫法我還是比較喜歡上面第一種的,因為覺得第一種在自己可控範圍內,因為以前寫react專案也是在生命週期裡控制一些資料的獲取。可能更習慣吧,不過我承認第二種更牛逼一些,效能也可能更好吧~各取所需吧。

Document

這個元件從我使用的角度來看,作用跟我前幾章有個地方的目的是一樣的,就是我們在Next.js裡沒有類似create-react-app裡面的index.html。因此我們沒有辦法定義最後渲染的html的結構,比如title,meta等標籤。我最開始是通過next/head的Head元件來實現的,但是head元件其實最後生成的就是html的head標籤。而Document元件是完全幫助我們構造html結構。

 // 除去Layout的Head結構
 // pages資料夾新增_document.js檔案

  // ./pages/_document.js
  import Document, { Head, Main, NextScript } from 'next/document';
  
  export default class MyDocument extends Document {
    static async getInitialProps(ctx) {
      const initialProps = await Document.getInitialProps(ctx);
      return { ...initialProps };
    }
  
    render() {
      return (
        <html>
          <Head>
            <meta name='viewport' content='width=device-width, initial-scale=1' />
            <meta charSet='utf-8' />
            <title>Next-Antd-Scafflod</title>
            <link rel='shortcut icon' href='/static/favicon.ico' type='image/ico'/>
            <link rel='stylesheet' href='/_next/static/style.css' />
          </Head>
          <body>
            <Main />
            <NextScript />
          </body>
        </html>
      );
    }
  }
複製程式碼

_document.js是隻在Next.js的服務端來進行渲染的,客戶端只是拿到服務端渲染過後的html字串渲染前端頁面,上面提到的window.__NEXT_DATA__就是存放在NextScript裡的。

Dynamic Import

其實以前在寫服務端渲染專案的時候會遇到很多坑,最常見的就是比如我想引入一些外部元件,這些元件裡有window,document等這種客戶端變數,而這些變數在服務端是不存在的,因此在服務端渲染的時候就會報錯,所以就很麻煩,需要webpack各種配置然後在非同步引入。比如:富文字編輯器。而next直接為我們封裝了動態引入的import,不出意外用的應該就是webpack的import方法,管他呢,好用就行。下面就給大家簡單是演示一下其中一個功能,就是動態引入一個富文字編輯器,然後空白期loading另一個元件~用法非常簡單,就是下面這樣:

import dynamic from 'next/dynamic';

const DynamicComponent = dynamic(import('braft-editor'), {
  loading: () => <p>正在載入元件...</p>
});

render() {
    return (
      <Fragment>
        <h1>使用者資訊:{this.state.username}</h1>
        <div style={{ width: '50%', height: '400px', }}>
          <DynamicComponent />
        </div>
      </Fragment>
    );
  }
複製程式碼

Next.js踩坑入門系列(七) —— 其他相關知識

詳細的Next為我們提供了更多的方法,感興趣的可以去官網看文件,有四種非同步引入的方法,其中還包含只在服務端引入~文件地址

error handling

錯誤處理,目前很多優秀的腳手架都為我們提供了錯誤處理,比如404和500的時候的頁面渲染,Next.js同樣,內部自動為我們封裝了errorPage。也就是我們其實什麼都不用幹,就可以享受這個服務。比如我在系統裡隨便輸入一個網址,會出現下面的結果:

Next.js踩坑入門系列(七) —— 其他相關知識

然後你還可以自己定義你的errorPage頁面,方法非常的簡單,就是在pages資料夾下面新建一個_error.js的檔案,裡面寫上你的errorPage程式碼就可以了,下面就簡單寫一個,其實就是從官網扒下來的~

// /pages/_error.js
import React from 'react'

export default class Error extends React.Component {
  static getInitialProps({ res, err }) {
    const statusCode = res ? res.statusCode : err ? err.statusCode : null;
    return { statusCode }
  }

  render() {
    return (
      <p>
        {this.props.statusCode
          ? `An error ${this.props.statusCode} occurred on server`
          : 'An error occurred on client'}
      </p>
    )
  }
}

複製程式碼

Next.js踩坑入門系列(七) —— 其他相關知識
ok,可以看到,很明顯的生效了。雖然效果差不多,但是你如果按照自己的來寫,肯定是沒問題的。哈哈~

Static HTML export

又一個高階功能,它支援我們把各種路由匯出成靜態頁面,不過你細想其實也沒啥大用,畢竟我們專案都是有邏輯的,匯出靜態頁面也不能操作,哈哈。不過既然是挺牛逼的一個功能,就拿來試試。

  • 第一步,在config資料夾裡配置一下頁面和路由
exportPathMap: async (defaultPathMap) => {
    return {
      '/home': { page: '/' },
      '/userList': { page: '/user/userList' },
    }
  },
複製程式碼
  • 第二步,package.json新增export命令
"scripts": {
    ...
    // 新增匯出命令
    "export": "yarn build && next export"
  },
複製程式碼
  • 第三步,執行yarn export命令

    執行完命令之後,根目錄下會出現一個out資料夾,真的是非常神奇,裡面有頁面資料夾和必要的靜態資源。

Next.js踩坑入門系列(七) —— 其他相關知識
然後我們開啟index.html訪問一下應該就是我們的首頁了,首頁就是下面這個樣子。
Next.js踩坑入門系列(七) —— 其他相關知識
emm...這個首頁有點奇怪,靜態資源和css都不太對勁兒,至於為什麼我就不去追究了,肯定有辦法的。不過我只是試試功能,時間有限準備休息了,哈哈。感興趣的大家自己研究研究。

這裡還有一個高階的Next.js專案推送到github page的功能,依賴的也是這個export,不過時間問題我就沒寫,大家感興趣的去看看官方demo,應該可以解決的~

總結

寫到這裡,Next.js踩坑入門系列就寫完了。非常感謝有很多小夥伴一直在看,還有一些可愛的小夥伴催更,水平有限,完全是踩坑集錦,如果能幫助到大家真的很開心。謝謝大家的閱讀。接下來準備用Next.js搭一個網站。完成後可能會再寫一篇Next.js的建站文章,其他的就不寫了,再寫就是其他內容啦~
本章節程式碼地址

專案程式碼地址,喜歡的給個Star,謝謝米娜桑

相關文章