Next.js--React服務端渲染實踐

Next.js服務端渲染

先來說一下服務端渲染吧,目前做的項目確實是服務端渲染的,但是不是我搭的,我個人寫的一些東西都不是服務端渲染的,所以自己動動手試試官方的Next.js,知識這東西就是拿來分享學習的嘛,學會了就是我的~哈哈

1.1 客戶端渲染

最原始的前端,頁面在瀏覽器獲取到JavaScript和CSS等文件之後開始渲染,完全在客戶端(也就是瀏覽器),路由是客戶端路由,也就是現在大部分的SPA單頁應用。

1.2服務端渲染

頁面由服務端渲染過後直接返回html頁面給前端,url的變更會刷新整個頁面,也就是以前的php和jsp模式

1.3 同構

高端點的詞Universal APP,爲什麼要同構,因爲客戶端渲染存在一個缺點,就是首屏加載過大文件或過多文件會變得特別慢,因此可以把首屏放在客戶端來渲染來提升首屏速度,首屏加載過後路又開始交給客戶端控制,又變成了SPA應用,整個代碼都是用JavaScript來編寫,服務端採用nodejs。

2 開始一個Nextjs應用

學習編程的我們都知道,入門系列是hello world,這裏我覺得next.js還是挺友好的,因爲它真的免除了我們平時所理解的服務端渲染的各種繁雜配置,只需要簡單的幾步就可以新建一個Universal App。

2.1 安裝依賴

好吧,你只需要新建一個文件夾,然後運行下面一段命令

// 初始化應用
yarn init
// 安裝三個依賴
yarn add react react-dom next
// package.json配置啓動
{
    ...
    // 新增啓動方式,選擇使用next啓動
    "script": {
        "dev": "next"
    }
    ...
}

OK,你已經完成了基於next的服務端搭建,是不是真的很簡單。

接下來我們運行一下yarn dev。

what?居然報錯了,好吧,原來Next.js默認從 pages 目錄下取頁面進行渲染返回給前端展示,並默認取 pages/index.js 作爲系統的首頁進行展示。因此,我們需要新建一個pages目錄。接下在再重新啓動一下試試。

2.2 新建pages文件夾以及文件

因爲3000經常被另一個項目使用,所以我把啓動端口改成3006了,修改script的啓動方式即可:

"script": {
    "dev": "next -p 3006"
}

然後我們訪問 http://localhost:3006

ok,得到了一個非常簡潔的一個頁面,講道理,我很喜歡這種簡潔的頁面。爲啥404了呢,原來我們只新建了pages目錄,剛剛也說了,它默認根路由頁面是pages下的index.js,所以我們新建一個index.js。

const Home = () => (
  <h1>我是Next的首頁</h1>
);
export default Home;

ok,現在就沒啥問題了。因爲next.js默認開啓服務端渲染,也就無需我們進行任何的配置,因此現在這個極其簡單的應用就是一個Universal React APP。從頁面元素我們也可以看出來:

2.3 Next路由

看到這裏,新手小夥伴應該跟我一樣感嘆Next.js強大的同時也會有一個疑問,等一下,怎麼就渲染了?路由你都沒配置憑啥就出首頁了,一般的SPA至少也會配置路由才能進行頁面跳轉,這裏沒配置路由首頁出來了我還有其他頁面呢,怎麼辦?

這些東西還都是Next給我們配置好的,剛纔說了Next.js默認匹配pages目錄的index.js作爲根路徑/,其他的路徑也是這樣按文件名匹配的。

而我們的各種路由跳轉依賴的不再是react-router而是next.js給我們封裝好的路由(其實redux也是,後面會說到)。

// /pages/index.js頁面 ----> /
import React, { Fragment } from 'react';
import Link from 'next/link';
const Home = () => (
  <Fragment>
    <h1>我是Next的首頁</h1>
    <Link href='/userList'>
      <a>用戶列表頁</a>
    </Link>
  </Fragment>
);
export default Home;
// -- /pages/userList.js ----> /userList
const UserList = () => (
  <h2>我是用戶列表頁</h2>
);
export default UserList;

厲害了我的哥,不僅跳轉成功,而且對應的history也都幫我們封裝好了,後退也都正常,而且可以看到,我們無需在前端或者node端配置任何路由相關,只需要按照它的模板去寫就可以了。並且前端頁面的跳轉也是無刷新的~

不過蘿蔔白菜各有所愛,雖然個人覺得很強大但是看不到路由還是感覺怪怪的,而且寫法也有變化,也就是路由必須在pages路徑下纔可以,這樣工程大了以後豈不是會很混亂,而且頁面和路由融合在一起redux怎麼辦,感覺很臃腫啊,作爲一個剛從純前端SPA過度過來的肯定很彆扭,我覺得不可能這麼low吧,肯定不會吧,稍後再探索吧。

3. 接入AntDesign

個人對於腳手架的UI有一種執念,如果搭建出來就是一個首頁+a標籤跳轉,實在不是我這個處女座的風格,因此第二步我就想引用UI框架 —— ant-design,相信很多使用react的開發者用的也都是這個UI框架吧。因爲以前自己在配製的時候也經常採坑,所以還是在這裏記錄一下~

3.1 安裝依賴

既然是安裝ant-design,那麼這兩個東西肯定是不能少的,一個是antd另一個就是antd官方的按需加載babel插件babel-plugin-import

// 安裝依賴
yarn add antd babel-plugin-import 

因爲現在開發環境大部分過渡到ES6/ES7語法了,因此還需要安裝一個babel的裝飾器轉化插件babel-plugin-transform-decorators-legacy,說實話這個插件具體是幹啥的我還真沒太仔細看,不過裝上它在babel裏配置就可以使用antd了。

當然還有其他方法,我這裏只是使用了這一種方法~

// 根目錄新建.babelrc文件
{
  "presets": ["next/babel"],
  "plugins": [
    "transform-decorators-legacy",
    [
      "import",
      {
        "libraryName": "antd",
        "style": "css"
      }
    ]
  ]
}

配置好了,我們來試一試,yarn dev啓動項目,額,一大堆報錯,爲啥呢?因爲原本在其他腳手架配置的時候需要在webpack裏配置一些東西嘛,這個怎麼可能沒有配置文件呢?
當然有了,只不過改名了,叫做next.config.js了,我們在服務端跑正常的css是不可以的。我們可以引入一下next-css這個包,然後require.extensions['.css'],還是那句話,我不理解,以後再深入研究一下,目前目的是可用~但是配置方案查到了就在這裏寫一下。

// 安裝依賴
yarn add @zeit/next-css
// 根目錄下創建next.config.js,內容如下
/* eslint-disable */
const withCss = require('@zeit/next-css');
// fix: prevents error when .css files are required by node
if (typeof require !== 'undefined') {
  require.extensions['.css'] = (file) => {}
}

module.exports = withCss();

好了,現在我們在啓動,就沒有報錯了,畢竟是官方解決方案,還是好使~把首頁的a標籤換成antd的button試試效果,效果是下面這樣:

額,果然沒這麼簡單,這又咋的了,也沒有任何報錯,也沒有任何提示,顯而易見就是樣式沒加載進來吧。。。繼續查,OK,明白了,其實antd的樣式已經有了,只不過在頁面上沒被引進來。爲什麼這麼說呢?

  • 第一個就是渲染出來的頁面head標籤裏沒有任何的CSS樣式,
  • 第二個就是antd的樣式文件已經被打包放進.next文件夾的static文件夾裏面了。
    原因找到了,接下來就是解決問題了

3.2 Next.js Head組件

解決問題就是我們需要把那個style.css放到頁面裏,但是我翻遍了整個工程目錄,都沒有找到正常React SPA的那個index.html,尷尬了,有問題還是得找官方文檔啊,查完過後發現了這個東西,Head,想看具體的可以點進去看官網,寫的挺詳細的~,就是我們可以使用這個head組件來爲我們的頁面添加head信息。

// /pages/index.js
import React, { Fragment } from 'react';
import { Button } from 'antd';
import Link from 'next/link';
import Head from 'next/head';
const Home = () => (
  <Fragment>
    <Head>
      <meta name='viewport' content='width=device-width, initial-scale=1' />
      <meta charSet='utf-8' />
      <title>Next-Antd-Demo</title>
      <link rel='stylesheet' href='/_next/static/style.css' />
    </Head>
    <Fragment>
      <h1>我是Next的首頁</h1>
      <Link href='/userList'>
        <Button type='primary'>用戶列表頁</Button>
      </Link>
    </Fragment>
  </Fragment>
);
export default Home;

OK,到現在而言是不是有點NB了,O(∩_∩)O哈哈~,真的是採坑系列啊,配置一個UI組件就這麼麻煩。估計接下來有坑可踩啦!

3.3 抽離Head爲Layout

一般的應用都會有個菜單Menu導航條之類的嘛,所以頁面就做頁面的事情,head放裏面感覺怪怪的,還是按照習慣把Head抽離出來當成一個高級父組件吧。個人習慣,就新建了一個components文件夾,裏面新建Layout.js。

// /components/Layout.js
import Head from 'next/head';
export default ({ children }) => (
  <div>
    <Head>
      <meta name='viewport' content='width=device-width, initial-scale=1' />
      <meta charSet='utf-8' />
      <title>Next-Antd-Scafflod</title>
      <link rel='stylesheet' href='/_next/static/style.css' />
    </Head>
    <style jsx global>{`
      body {
      }
    `}</style>
    {children}
  </div>
);
// /pages/index.js
import React, { Fragment } from 'react';
import { Button } from 'antd';
import Link from 'next/link';
import Layout from '../components/Layout';
const Home = () => (
  <Layout>
    <Fragment>
      <h1>Hello Next.js</h1>
      <Link href='/userList'>
        <Button type='primary'>用戶列表頁</Button>
      </Link>
    </Fragment>
  </Layout>
);
export default Home;

講到這裏,整個Antd的配置基本就完成了吧,哈哈,沒想到講個antd配置能寫這麼多,真實厲害了~既然UI框架嘛,順便我就把CSS也寫了吧。看Next官網可以很明確瞭解到它推崇的是css-in-js,具體鏈接大家請點這裏Next Css-in-Js,說白了,可以把它理解成用類Vue的形式寫React,組件內部使用下面這種形式來修改樣式

 <style jsx>{`
      p {
        color: blue;
      }
      div {
        background: red;
      }
      @media (max-width: 600px) {
        div {
          background: blue;
        }
      }
    `}</style>
    <style global jsx>{`
      body {
        background: black;
      }
    `}</style>

這裏需要注意的是,組件內部的css並不是子組件繼承父組件,就是組件內部使用,如果想要子組件繼承父組件樣式,需要將style jsx改成style global jsx這種形式,說實話,越看越像Vue,
除了上面那種官方推薦的方法以外,還有其他很多種Css-in-Js的樣例,其中個人還是比較推薦styled-components的,大家感興趣可以去看官方文檔,寫的真的很不錯。

以前我在用antd的時候,都會根據重置一下自帶配色以及一些其他的默認屬性,這裏我才用了以前的方式結果出錯了,以前的方式是依賴babel-plugin-import,在babelrc文件裏將"style": “css"改成"style”: true,這樣,babel-plugin-import會加載.less文件,然後在webpack裏面配置less-loader的modifyVars變量進行覆蓋:

config.module.rules.push({
        test: /\.less$/,
        use: [
          {
            loader: "style-loader"
          }, {
              loader: "css-loader"
          }, {
              loader: "less-loader",
              options: {
                sourceMap: true,
                modifyVars: AntdTheme
              }
          }
        ]
    })

但是在next框架裏如果使用less方式引入服務端渲染會過不去,這算是一個坑?用下面這種方式就好了,無關痛癢~

<style jsx global>{`
      .ant-btn-primary {
        background-color: #ec6a00
      }
    `}</style>

你看,也可以改,不過個人覺得antd的配色還是挺不錯的,哈哈,就別改了。我認爲官方後續會增強的吧!

可能官方早就有解決方案了吧,只不過我還是不太會用?因爲我看除了next-css包以外還提供了next-less包,這個包應該就是用來加載less文件的吧我看了一下這個包還支持css-modules,不過我配置了一下還是不太對,並且我對目前這種寫法還覺得挺舒服的,就不多浪費時間了,大家感興趣的可以攻克一下,解決了可以留言個地址給我,萬分感謝~

4 目錄重構

來說一說爲什麼要目錄重構吧,Next.js很強大,爲我們封裝了路由,只需要在pages下面新建js文件,裏面寫上我們熟悉的頁面也就是react組件,就會渲染出來!
其實對於開發來說沒區別,但是項目龐大以後,一個路由對應一個js文件,但是如果頁面很複雜其實不是這個React組件也會很複雜,不是很符合組件化理念,後期也不好維護啊。而且,肯定要加redux的,這樣的話就更加混亂了。所以現在趁着還清醒,趕快重新構建一下~

4.1 抽離Layout

首先,我們在跟目錄下新建一個components文件夾,專門用來放我們的組件,新寫一個Header.js:

// /components/Header.js
import React, { Component } from 'react';
import Link from 'next/link';
import { color_youdao, color_youdao_border } from '../constants/CustomTheme';

class Header extends Component {
  constructor(props) {
    super(props);
    const { title } = props;
    this.state = { title };
  }

  render() {
    const { title } = this.state;
    return (
      <div className='header-container'>
        <Link href='/'>
          <div className='logo-container'>
            <img className='logo' alt='logo' src='/static/logo.png' />
            <span className='sys-name'>XXX系統</span>
          </div>
        </Link>
        <h2>{title}</h2>
        <style jsx>{`
          .header-container {
            height: 60px;
            background-color: ${color_youdao};
            border: 1px solid ${color_youdao_border};
            margin-bottom: 10px;
          }
          h2 {
            text-align: center;
            line-height: 60px;
            font-size: 1.6rem;
            font-weight: 500;
            color: #fff;
          }
          .logo-container {
            position: absolute;
            display: flex;
            justify-content: center;
            align-items: center;
            top: 15px;
            left: 20px;
            cursor: pointer;
          }
          .sys-name {
            display: inline-block;
            margin-left: 10px;
            font-size: 20px;
            color: #fff;
            font-weight: 600;
          }
          .logo {
            width: 30px;
            height: 30px;
          }
        `}</style>
      </div>
    )
  }
}

export default Header;

然後,把Layout.js裏面加上Header,然後我們每個組件都使用Layout嵌套,就完成了整個骨架的搭建~真的很簡單!

// /components/Layout.js

import { Fragment } from 'react';
import Head from 'next/head';
import Header from './Header';
export default ({title, children }) => (
  <Fragment>
    <Head>
      <meta name='viewport' content='width=device-width, initial-scale=1' />
      <meta charSet='utf-8' />
      <title>Next-Antd-Demo</title>
      <link rel='stylesheet' href='/_next/static/style.css' />
    </Head>
    <style jsx global>{`
      * {
        margin: 0;
        padding: 0;
      }
      body {
        font-family: Helvetica, 'Hiragino Sans GB', 'Microsoft Yahei', '微軟雅黑', Arial, sans-serif;
      }
      .content-container {
        display: flex;
        flex-direction: column;
        align-items: center;
      }
    `}</style>
    <Header title={title} />
    <div className='content-container'>
      {children}
    </div>
  </Fragment>
);

OK,現在Layout組件就暫時算完成了。

4.2 抽離頁面組件

上面提到過,pages作爲next的路由索引目錄,那麼我就想讓它專心的做路由,就不要做組件的複雜邏輯了,因此,我想把組件的內部實現全部放在components文件夾下。然後路由頁面只需要直接引用組件就可以啦~

// /components/Home/Home.js 頁面組件
import React, { Fragment } from 'react';
import { Button } from 'antd';
import Link from 'next/link';
import Layout from '../Layout';
const Home = () => (
  <Layout title='首頁'>
    <Fragment>
      <h1>Hello Next.js</h1>
      <Link href='/userList'>
        <Button type='primary'>用戶列表頁</Button>
      </Link>
    </Fragment>
  </Layout>
);
export default Home;
// /pages/index.js 路由組件
import Home from '../components/Home/Home';

export default Home;

其實很簡單,但是這麼看起來就很清晰嘛,O(∩_∩)O哈哈~

4.3 靜態資源引用

靜態資源的引用比如圖片,你可以使用CDN然後src直接填寫url,也可以通過工程內部的靜態文件引用。Next同樣爲我們提供了非常簡便的方式,與引用css一樣,css是通過Head組件來引入頁面的,靜態文件next官網推薦我們在根目錄新建一個static文件夾,然後靜態文件放在static文件夾下,引用的時候使用絕對路徑的形式,next就會找到它們~就像下面這樣:

<img className='logo' alt='logo' src='/static/logo.png' />

4.4 抽離靜態常量

然後就是抽離靜態常量,這個就很簡單了,新建一個constants文件夾,把我們經常用到的常量在裏面定義好,然後就可以使用了,比如CSS的配色(從我選擇的系統配色不知道小夥伴是不是能猜出來我是哪個公司的),_比如後面引入Redux的Action type。

// /constants/ConstTypes.js
export const RoleType = {
  1: '管理員',
  10: '普通用戶'
}

// 使用
import { RoleType } from '../../constants/ConstTypes';

現在基本暫時完成了目錄重構(將來引入redux肯定還得重構一次)。目錄結構如下:

-- root  
   | -- components // 組件目錄
   | -- constants  // 常量目錄
   | -- pages      // 路由目錄
   | -- static     // 靜態資源目錄
   | -- .babelrc
   | -- .eslintrc
   | -- .gitignore
   | -- package.json
   | -- ...其他配置文件

5 再談路由

Next.js的路由剛開始給我的感覺就是,我靠,很NB啊。但是慢慢的用起來發現,坑還真不少。前面也提到了,它是以pages下面的js文件作爲路由路徑驚醒匹配的,所以也就是說你想用到的頁面全要以js文件的形式放進pages,那麼層級嵌套關係怎麼辦?ok,嘗試了一下,很容易解決了。

5.1 路由層級

[需求]: 與用戶相關的包括用戶列表,用戶詳細信息等等…這兩個功能應該是同屬於用戶子模塊,所以應該與首頁不是同級關係。
[解決辦法]:pages下面新建子目錄user裏面包括userList.js和userDetail.js。
– pages
| – user
| – userList.js
| – userDetail.js
| – index.js

可以看到,這樣算是解決了一個問題。

5.2 路由參數

緊接着,問題又出現了,我們需要查看用戶詳情,以往來說,很容易想到 /user/userDetail/:username,這種嘛,參數通過url的params獲取,但是,悲劇了。查了一下Next.js路由API,人家沒給你提供params,只提供了query。

5.3 query形式路由

也就是說,暫時我們需要/user/userDetail?username=XXX的形式來實現工程,雖然說沒什麼問題,但是可能每個人習慣不一樣吧。當然,對於我這種好說話的人,我可以接受O(∩_∩)O哈哈~
// 其實Next的Link組件的href屬性可以傳入一個對象

 <Link href={{ pathname: '/user/userDetail', query: { username: text } }}>
   <a>{text}</a>
 </Link>

ok,實現效果就是這樣,反正符合預期,只是使用query代替params了。

P.S.真實是我不想費事搞這個東西,應該是可以解決的,稍後說我的想法

5.4 params形式路由

下面我來說說我的理解吧:

首先,是爲什麼它不支持params形式的路由,前面提到過了,他是根據pages下的js文件來匹配路由的,那麼你用params的路由勢必/user/userDetail/:username,那麼解析器會以爲我應該尋找的是pages目錄下面user目錄下面UserDetail目錄下面的${username}文件,不用想肯定找不到啊,這時候就是404頁面了。所以這是我的理解,他爲什麼只使用query。
其次,我認爲兩者只是形式上的區別,並沒有本質上的區別,也就是實現效果是一樣的,都能跳轉到指定頁面嘛,何必糾結呢?
最後,就是我看完路有部分的文檔,我認爲是可以做到params形式的跳轉的,官方文檔裏可以自定義server:

// 官方文檔自定義server
const { createServer } = require('http')
const { parse } = require('url')
const next = require('next')

const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

app.prepare().then(() => {
  createServer((req, res) => {
    // Be sure to pass `true` as the second argument to `url.parse`.
    // This tells it to parse the query portion of the URL.
    const parsedUrl = parse(req.url, true)
    const { pathname, query } = parsedUrl

    if (pathname === '/a') {
      app.render(req, res, '/b', query)
    } else if (pathname === '/b') {
      app.render(req, res, '/a', query)
    } else {
      handle(req, res, parsedUrl)
    }
  }).listen(3000, err => {
    if (err) throw err
    console.log('> Ready on http://localhost:3000')
  })
})

從上面可以看出來,我們可以將a路由匹配到b頁面。也就是我們可以把/user/userDetail/:username的路由匹配到/user/userDetail?username=${username}上面嘛。不就解決問題了O(∩_∩)O 哈哈,機智如我,不過我沒試驗過,只是猜測,目前優先想開發一個系統,這裏留坑,以後有機會再填~

5.5 路由填坑

上面在談到params路由和query路由的時候,留了一個坑,在這裏還是來解決一下

事實證明,就是我想的那樣,可以使用custom server然後重新匹配路由渲染的頁面就可以了。不過這樣會出現一個小問題,就是在網速過慢的時候重新匹配的頁面沒渲染出來之前,控制檯會出現報錯,重新渲染之後消失。這就類似於302,剛開始是404頁面,然後被重定向到另一個,哈哈~

可以看一下控制檯,會出現一個報錯,但是這樣確實會成功使用params的路由。而且我發現一個問題,第一次進新頁面的時候Next.js渲染會特別的慢,不知道是不是一個bug,還是我哪裏寫的有問題,再多研究研究~

5.6 報錯問題

好吧,去官網github查了一圈,也有人跟我一樣提了相同的issue,最後我看了一下發現,原來寫法出問題了,雖然可以正常執行,但是會在正確頁面出現之前404以下。處女座的我不能忍受還是改回來吧~

// 路由應該這麼寫
<Link href={`/user/userDetail?username=${text}`} as={`/user/userDetail/${text}`}>
  <a>{text}</a>
</Link>
// server.js
 server.get('/user/userDetail', (req, res) => {
  return app.render(req, res, `/user/userDetail/${req.query.username}`);
});

上面那樣就可以了,具體代碼在下方~

5.7 開發模式下首次加載antd不出樣式

最後,我還是把antd的css形式換成了less形式,一方面是因爲確實配置主題色以及其他覆蓋樣式還是有需求的,另一方面是重點了,在開發模式下,next.js打開新頁面的pending time實在過長,這個過長的pending time導致第一次antd的樣式加載不出來。而換成less的模式雖然也很慢,但是樣式卻不會發生改變,所以還是切換到less了。

可以看出來,同樣是到新頁面去渲染一個table組件,雖然第一次加載時間都很長,但是less的形式是可以加載出來css的。哈哈。所以還是使用less吧,開發模式下,所有頁面的第二次加在都沒有問題,速度也很快。

next.js的生產環境還是比較快的,開發環境打開第一次新頁面確實有點慢,基本都要5s左右…

5.8 生產模式

上面截圖也看到了,Next.js在開發模式下頁面第一次的pending時間是非常長的,基本都要達到5s左右,當然也可能是我寫的代碼有問題?不過我去官方demo下面隨便用了一個,也是很慢的。

不禁我就思考了,如果上線項目第一次加載也這麼慢,怎麼可以呢?正在我考慮要不要半途而廢的時候,我嘗試了一下用生產模式打包一下,如果打包完生產環境首次加載也特別慢,那麼不扯淡呢嗎?拜拜了您嘞~

因爲我用的custom-server,所以scripts變成了下面這樣:

"scripts": {
    "dev": "node server.js",
    "build": "next build",
    "start": "set NODE_ENV=production && node server.js"
  },

打包完之後也是正常訪問,下面是打包完以後的訪問效果。

可以看到,無論是首次加載頁面,還是第一次進入其他頁面,速度都是挺快的, 所以我原諒了開發時的慢速度了,還是接着學吧,O(∩_∩)O哈哈~

6 集成 Redux

一個程序員爲了不長房租答應房東教他孩子學習編程_北漂不易,且行且珍惜希望每一個北漂程序員都能早日財富自由,如果實在太累了就換個城市吧

6.1 繼續填坑

上一講有關路由的坑還是沒填明白,原本params路由自認爲已經沒問題了,不過最近在測試的時候,發現進入系統的時候是沒問題的,但是如果在params路由頁面進行刷新,會404頁面。所以,繼續fix~

// server.js
server.get('/user/userDetail', (req, res) => {
      return app.render(req, res, `/user/userDetail/${req.query.username}`);
    });

    server.get('*', (req, res) => {
      const parsedUrl = parse(req.url, true);
      const { pathname } = parsedUrl;
      if (typeof pathname !== 'undefined' && pathname.indexOf('/user/userDetail/') > -1) {
        const query = { username: pathname.split('/')[3] };
        return app.render(req, res, '/user/userDetail', query);
      }
      return handle(req, res);
});

上面這樣就真的可以了,刷新頁面也沒有任何問題~

寫過react SPA的大家應該基本都用過redux,按照官方教程一頓複製粘貼基本都能用,需要注意的就是redux會創建一個全局唯一的store包在整個應用的最外層。喏,這個是redux官方的示例:

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'

let store = createStore(todoApp)

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')

那麼問題來了,我得有個東西讓他包起來對不對,在Next.js上來就跟我說了,默認是index,然後在組件裏再使用link來進行跳轉,這跟傳統的router有點區別啊。怎麼辦呢?官方給我們的解決辦法就是APP,用它來實現將應用包成一個整體(原諒我這麼理解了)。

注意了:下面也是約定俗成的
我們需要在pages文件夾下新建一個_app.js文件,不好意思其他名字不可以,然後寫上如下代碼,就可以啦~

// /pages/_app.js
export default class MyApp extends App {
  render () {
    const {Component, pageProps} = this.props
    return (
      <Container>
        <Component {...pageProps} />
      </Container>
    )
  }
}

ok,這樣就可以了。因爲我們什麼也沒幹,只是在pages文件夾下增加了一個_app.js,怎麼來看是否起作用了呢,我打印了一下props的router(因爲稍後重構頁面的時候會用到),可以看出來,雖然還是渲染的首頁,但是控制檯可以打印出router信息,所以還是那句話,既然選擇了Next.js就需要按照它制定的規則來~

6.2 重構Layout

前面文章說了,整個系統的架構大概就是上下佈局,頂部導航欄是固定的,所以抽離出來了一個Layout組件,這樣的話每一次每一個新組建外部都需要包一層Layout並且需要手動傳title,才能正確展示,有了APP這個組件我們就可以來重構一下Layout,這樣就不需要每個頁面都包一層Layout了~

// constants.js
// 路由對應頁面標題
export const RouterTitle = {
  '/': '首頁',
  '/user/userList': '用戶列表',
  '/user/userDetail': '用戶詳情'
};
```// components/Home/Home.js
import { Fragment } from 'react';
import { Button } from 'antd';
import Link from 'next/link';

const Home = () => (
  <Fragment>
    <h1>Hello Next.js</h1>
    <Link href='/user/userList'>
      <Button type='primary'>用戶列表頁</Button>
    </Link>
  </Fragment>
);
export default Home;
// /pages/_app.js

import App, {Container} from 'next/app';
import Layout from '../components/Layout';
import { RouterTitle } from '../constants/ConstTypes';

export default class MyApp extends App {
 constructor(props) {
    super(props);
    const { Component, pageProps, router } = props;
    this.state = { Component, pageProps, router };
  }

  static getDerivedStateFromProps(nextProps, prevState) {
    if (nextProps.Component !== prevState.Component
      || nextProps.pageProps !== prevState.pageProps
      || nextProps.router !== prevState.router) {
      return {
        Component: nextProps.Component,
        pageProps: nextProps.pageProps,
        router: nextProps.router
      };
    }
    return null;
  }
  
  render () {
    const { Component, pageProps, router } = this.props;
    
    return (
      <Container>
        <Layout title={RouterTitle[router.pathname]}>
          <Component {...pageProps} />
        </Layout>
      </Container>
    );
  }
}

好啦,現在這樣就可以了,內部可能也需要小改一下。總之Layout部分就抽離出來了。越來越有規範化的系統樣子了~

這裏說一點我的感想,因爲Next幫我們做了很多配置的東西,所以在寫起來的時候就是需要按照它的約定俗成的規則,比如路由,APP,靜態資源這種。我覺得這樣寫有好處也有壞處吧,仁者見仁智者見智,至少我是挺喜歡的,因爲出問題了看文檔很快就會解決,其他的自行配置的SSR框架就會因人而異的出現各種莫名bug,還不知道要怎麼去解決~

6.3 狀態管理Redux準備

react這個框架只專注於View層,其他很多東西都需要額外引入,狀態管理redux就是一個React應用必備的東西,所以慢慢的也就變成是React全家桶一員,關於狀態管理機制不是這裏所要講的,太深奧了,還不太會的應該好好看看react相關知識了,這裏只是講在Next.js裏如何引入redux以及redux-saga(如果喜歡用redux-thunk可以用redux-thunk,不過我覺得thunk不需要配置啥,所以就用saga寫例子了)。還是老樣子,引入了新東西,就需要提前安裝啊~

// 安裝redux相關依賴
yarn add redux redux-saga react-redux
// 安裝next.js對於redux的封裝依賴包
yarn add next-redux-wrapper next-redux-saga

如果你使用的是單純的客戶端SPA應用(類似於create-react-app創建的那種),那麼只安裝redux和redux-saga就可以了,因爲我們是基於next.js來搭建的腳手架,所以還是按照人家的標準來的~

瞭解redux的都知道,store,reducer,action這些合起來共同完成redux的狀態管理機制, 因爲我們選擇使用redux-saga來處理異步函數,所以還需要一個saga文件。因此我們一個一個來:

store
// /redux/store.js
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';

import rootReducer, { exampleInitialState } from './reducer';
import rootSaga from './saga';

const sagaMiddleware = createSagaMiddleware();

const bindMiddleware = (middleware) => {
  if (process.env.NODE_ENV !== 'production') {
    const { composeWithDevTools } = require('redux-devtools-extension');
    // 開發模式打印redux信息
    const { logger } = require('redux-logger');
    middleware.push(logger);
    return composeWithDevTools(applyMiddleware(...middleware));
  }
  return applyMiddleware(...middleware);
};

function configureStore (initialState = exampleInitialState) {
  const store = createStore(
    rootReducer,
    initialState,
    bindMiddleware([sagaMiddleware])
  );
  // saga是系統的常駐進程
  store.runSagaTask = () => {
    store.sagaTask = sagaMiddleware.run(rootSaga);
  };

  store.runSagaTask();
  return store;
}

export default configureStore;

爲了方便調試,開發時我又引入了redux-logger,用於打印redux相關信息。
老生常談,這次我也簡單的來用redux官方最簡單的示例計數器Counter來簡單地實現了,最後的視線效果如下圖:

//actions
// /redux/actions.js
export const actionTypes = {
  FAILURE: 'FAILURE',
  INCREMENT: 'INCREMENT',
  DECREMENT: 'DECREMENT',
  RESET: 'RESET',
};

export function failure (error) {
  return {
    type: actionTypes.FAILURE,
    error
  };
}

export function increment () {
  return {type: actionTypes.INCREMENT};
}

export function decrement () {
  return {type: actionTypes.DECREMENT};
}

export function reset () {
  return {type: actionTypes.RESET};
}

export function loadData () {
  return {type: actionTypes.LOAD_DATA};
}
// reducer
import { actionTypes } from './actions';

export const exampleInitialState = {
  count: 0,
};

function reducer (state = exampleInitialState, action) {
  switch (action.type) {
    case actionTypes.FAILURE:
      return {
        ...state,
        ...{error: action.error}
      };

    case actionTypes.INCREMENT:
      return {
        ...state,
        ...{count: state.count + 1}
      };

    case actionTypes.DECREMENT:
      return {
        ...state,
        ...{count: state.count - 1}
      };

    case actionTypes.RESET:
      return {
        ...state,
        ...{count: exampleInitialState.count}
      };

    default:
      return state;
  }
}

export default reducer;

6.4 redux-saga

上面兩個內容還沒有涉及到saga部分,因爲簡單的reudx計數器並沒有涉及到異步函數,所以使用saga這麼高級的功能我們還需要請求一下數據~😄。正好有個用戶列表頁,我們這裏使用下面這個API獲取一個線上可用的用戶列表數據用戶數據接口

/* global fetch */
import { all, call, put, take, takeLatest } from 'redux-saga/effects';
import { actionTypes, failure, loadDataSuccess } from './actions';

function * loadDataSaga () {
  try {
    const res = yield fetch('https://jsonplaceholder.typicode.com/users');
    const data = yield res.json();
    yield put(loadDataSuccess(data));
  } catch (err) {
    yield put(failure(err));
  }
}

function * rootSaga () {
  yield all([
    takeLatest(actionTypes.LOAD_DATA, loadDataSaga)
  ]);
}

export default rootSaga;

然後在我們用用戶列表頁初始化獲取數據,代碼如下:

import { connect } from 'react-redux';
import UserList from '../../components/User/UserList';
import { loadData } from '../../redux/actions';

UserList.getInitialProps = async (props) => {
  const { store, isServer } = props.ctx;
  if (!store.getState().userData) {
    store.dispatch(loadData());
  }
  return { isServer };
};

const mapStateToProps = ({ userData }) => ({ userData });

export default connect(mapStateToProps)(UserList);

說實話這個地方稀裏糊塗弄出來的,next.js與原本的react寫法還是有些區別,狀態容器和展示容器劃分的也不是很分明,我暫時使用路由部分來做狀態容器,反正也成功了,下一節來重新劃分一下redux目錄結構,爭取讓項目更加合理一些~

這次時間拖的比較久,真的抱歉,最近思路也有點斷,不在科研狀態,哈哈。希望大家不要見怪,開始靜下心了!這裏還是偏使用,遠離還是建議大家去看redux相關文檔,講得更清楚,這裏只是next.js怎麼使用redux-saga。接下來想了一下,讓工程目錄更加合理,然後就是把Next.js還沒涉及到的統一寫幾個Demo給大家示範一下~

7 目錄再重構

上一節引入了redux以及使用redux-saga來進行異步函數的處理,而上一節的目錄只是簡單的引入redux而已,redux可是相當龐大和複雜的,並且也算是個人習慣了吧。action分離,reducer分離,狀態組件container等等。我喜歡把這些東西劃分的清清楚楚,這樣一個項目維護起來纔會方便~這一節就從頭到尾來進行目錄的劃分,因爲Next.js和原本的React SPA項目有一定的區別,主要體現在路由部分,所以我也是按照自己的理解和舒服的方式進行目錄重構!

重構完的目錄

// ================ 目錄結構 ================== //
——————
  | -- asserts         // ant-design全局less變量設置文件夾
  | -- components      // React展示組件(也就是UI組件)文件夾
  | -- constants       // 整個應用的常量文件夾
      | -- ActionsTypes.js   // 存放所有action type的常量文件
      | -- ApiUrlForBE.js    // 存放所有後端數據的apiUrl
      | -- ...
  | -- containers      // React狀態組件文件夾
  | -- pages           // Next.js路由文件夾
  | -- redux
      | -- actions     // 處理整個應用所有的action
      | -- middlewares // 中間件,處理各種特殊情況,比如獲取失敗之後的message提醒
      | -- reducers    // 處理整個應用所有的reducer
      | -- sagas       // 處理整個應用所有的saga
      | -- store.js  
  | -- static          // 存放整個應用所有的靜態資源(如圖片等)
  | -- .babelrc
  | -- .eslintrc
  | -- .gitignore
  | -- next.config.js  // Next.js配置文件
  | -- package.json   
  | -- server.js       // 服務端server文件
  | ...

原諒我臭不要臉一下,個人認爲這個結構還是非常清晰的,只不過可能新手寫起來可能會覺得有些繁瑣,不過項目大的情況下,state樹很大,這種結構非常的清晰~

7.1 重構actions

其實actions完全可以放在一個文件裏使用,不過項目龐大了以後維護起來還是有些麻煩的,所以按照組件化思想,每一個組件對應一個action,或者每一個大功能塊對應一個action還是比較合理的。

 -- redux
   | -- actions
       |  -- home.js // 處理首頁action
       |  -- user.js // 處理與用戶有關action
       |  ...        // 其他action

7.2 重構reducers

reducer部分肯定是要分離的,因爲redux的官方爲我們提供combineReducer這個API就是合併不同組件的reducer的,所以可以理解爲redux的reducer推薦就是根據組件進行劃分的~就如同整個應用只有一個狀態樹一樣,每一個reducer負責處理樹的不同枝葉派發出來的action。具體reducer內容還是去看redux官方文檔吧。

7.3 重構sagas

 -- redux
   | -- reducers
       |  -- home    // 首頁部分reducer
       |  -- user    // 用戶相關reducer
       |  ...        // 其他reducer
       | index.js    // rootReducer,由combineReducer生成

7.4 抽離container

這裏需要特別說明一下~~~由於Next.js的特殊原因,其實已經做到了UI組件的分離,其實這一層container完全可以由pages文件夾代替,也就是可以用路由組件通過react-redux的connect函數封裝一下,這樣就變成了一個帶狀態的路由組件,不知道大家明不明白我說的話。。。下面是兩種方法,大家按需自己採取,以UserList組件爲例:

  • 第一種,抽離container
    // /conatiners/user/UserList.js
    import { connect } from 'react-redux';
    import { fetchUserListData } from '../../redux/actions/user';
    import UserList from '../../components/User/UserList';
    
    const mapStateToProps = state => ({
      list: state.user.list.list,
    });
    
    const mapDispatchToProps = dispatch => ({
      fetchUserListData() {
        dispatch(fetchUserListData());
      }
    });
    
    export default connect(mapStateToProps, mapDispatchToProps)(UserList);

    // 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;

簡單來說其實就是路由組件導入的是狀態組建UserList.js,而狀態組建是通過react-redux的connect方法封裝UI組件UserList.js而得來的。

  • 第二種,帶狀態的路由組件
// /pages/user/userList.js
import { connect } from 'react-redux';
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 };
};

const mapStateToProps = state => ({
  list: state.user.list.list,
});

const mapDispatchToProps = dispatch => ({
  fetchUserListData() {
    dispatch(fetchUserListData());
  }
});

export default connect(mapStateToProps, mapDispatchToProps)(UserList);

簡單來說,就是在路由組件內把UI組件UserList.js通過connect變成了狀態組件。

個人推薦第一種方法,雖然寫起來稍微麻煩了一些,但是第二種方法完全是因爲Next.js的特殊性才能實現的,當然,對於Next.js來說,第二種方式確實更簡單一些~

經歷了上面幾個部分的重構,整個基於Next.js的服務端渲染腳手架基本結構也就成型了。在搭建過程中還是遇到了很多坑的,不過也都一點點的踩過去了。希望對大家有些幫助,個人認爲這個結構還是值得參考一下的~原本到這裏就可以結束文章了,不過我在使用過程又發現了一些坑,順便的Next.js還有一些內容我還沒碰過,就幫大家都踩一踩,下一節來一個其他內容的大雜燴~

8. Next.js其他需要了解的知識點

8.1 獲取數據&&getInitialProps

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

這裏給大家推薦一個區別於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組件的各種生命週期函數。

8.2 服務端可用

這真是getInitialProps這個生命週期的過人之處了,他可以在服務端運行,這樣做有什麼好處呢?減少抓取數據的次數

  • React老生命週期內獲取數據
    以抓取用戶列表爲例,我們可以在組件裏的componentDidMount生命週期內獲取
 // /components/user/userList.js
 ...
 componentDidMount() {
    this.props.fetchUserList();
 }

從上圖我們可以看出來,每次進入用戶列表頁,都會重新抓取用戶數據。有人可能會說,這不廢話嗎,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;

兄弟們發現沒,進入系統後只會在第一次進入路由的時候獲取數據,之後再進入因爲服務端緩存過數據,所以不需要重新獲取,減少了獲取次數~

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

8.3 存在問題——踩坑

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

我們可以很清楚地看到,頁面數據通過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刷新也會出現這個問題。

8.4 解決辦法

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

  • 第一種:在組件生命週期裏判斷isServer
    剛剛問題描述過了,也就是正常加載和通過路由跳轉頁面,數據會正常渲染且會從瀏覽器的window.__NEXT_DATA__獲取來減少不必要的網絡請求~,而在頁面進行刷新的時候不會重新請求數據並且window.__NEXT_DATA__裏也找不到我們想要的數據。不過通過控制檯信息我們可以發現問題所在以及解決辦法。那就是,第一次啓動系統的時候返回的isServer是false,而瀏覽器刷新頁面的時候isServer返回的是true,我們可以在組件裏進行這個變量的判斷,如果是true,就重新進行一次數據抓取。
// /components/user/UserList.js
...
componentDidMount() {
  if(this.props.isServer) {
  // 需要重新抓取數據
    this.props.fetchUserListData();
  }
}
...

從上圖可以看到,刷新頁面的時候,我們會重新獲取數據渲染頁面,如果不刷新就不會重新獲取。還是可行的這個方法~

  • 第二種:換一種方式預獲取數據
    另一種方法就比較高級了,原理我依然不知道,但是就是好用,哈哈,這東西真是邪門,爲什麼這麼說呢,其實本質沒改變什麼,就是換了種寫法就可以。具體就是,上面的寫法我在getInitalProps裏面寫了dispatch了一個獲取數據的action,從上一節或者代碼裏你們可以看到,其實這個action就是fetch一個api獲取數據返回state。這就是redux一個獲取數據的基本過程,這種方法在刷新時行不通,而行得通的方法是:要通過isomorphic-unfetch這個來拉取服務端的數據。
// /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;

8.5 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裏的。

8.6 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爲我們提供了更多的方法,感興趣的可以去官網看文檔,有四種異步引入的方法,其中還包含只在服務端引入~文檔地址

8.7 error handling

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

然後你還可以自己定義你的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>
    )
  }
}

ok,可以看到,很明顯的生效了。雖然效果差不多,但是你如果按照自己的來寫,肯定是沒問題的。哈哈~

8.8 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文件夾,真的是非常神奇,裏面有頁面文件夾和必要的靜態資源。

然後我們打開index.html訪問一下應該就是我們的首頁了。

emm…這個首頁有點奇怪,靜態資源和css都不太對勁兒,至於爲什麼我就不去追究了,肯定有辦法的。不過我只是試試功能,時間有限準備休息了,哈哈。感興趣的大家自己研究研究。

9 總結

寫到這裏,Next.js踩坑入門系列就寫完了。非常感謝有很多小夥伴一直在看~~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章