React 服務端渲染框架 Next.js 基於 Gank api 實戰

orangexc發表於2017-10-20

最開始先擺出地址,有線上 demo,目前只支援 pc:github.com/OrangeXC/ga…

鑑於最近 vue 相關的文章寫的比較多,抽出時間寫點 react 的專案,當時用 react 還是 v15 現在都 v16 了,感慨跟不上所有框架的節奏(玩笑話),框架的本質都是大同小異的,每次高 star 框架更新看一下 change 是個好習慣。

之前用 Nuxt 寫了個簡單的 v2ex,今天的主角依然是 SSR 服務端渲染

Nuxt 文件裡有寫到靈感源於 Next.js,那麼就是說 Next.js 算是 SSR 框架中的元老級別的了。

為什麼選擇 SSR 框架

前面的文章總是在官方文件上小費功夫說明下,這裡對於不熟悉 Next.js 的讀者建議直接轉到 Github,別猶豫,當然熟悉 Nuxt 也可以無障礙閱讀本文

不論是 Next.js 或 Nuxt,服務端渲染框架主要兩個重要功能

  • 首屏 node.js 服務端渲染
  • 生成純靜態的 web 站

至於它們是基於哪個前端庫封裝的,還要看庫本身是否支援 SSR,然後就是對外提供 render 函式。

用此類庫的原因也不必多說,節省開發成本,不再糾結於環境搭建以及渲染細節。

直接開工

本次要實現的是基於 gank api 的專案,還是看人家支援什麼 api,點前面連結檢視詳細 api

大體總結為 => 列表,搜尋,提叫到稽核

列表分為了許多型別,主要的 menu 也是針對不同型別的列表展開

路由

通過已知的 api 可以輕鬆的定義路由

  • / (主頁,最近的全部型別乾貨列表)
  • /fe (前端乾貨列表)
  • /android (安卓乾貨列表)
  • /ios (iOS乾貨列表)
  • /app (App乾貨列表)
  • /expand (擴充資源乾貨列表)
  • /videos (休息視訊乾貨列表)
  • /welfare (福利列表,前方高能,全是乾貨。。。)
  • /timelien (時間軸,記錄歷史所有更新過乾貨的日期)
  • /day (某天詳情,分為以上幾種型別的 tab 列表)
  • /uplaod (傳送乾貨到稽核)
  • /search (搜尋頁)

同 Nuxt 路由配置檔案不需要手動建立,/pages 下預設會渲染為頁面,檔名自然就是路由名

路由檔案都建立完了,下一步思考如抽離出公共模板 Layout 程式碼,Next.js 提供了 layout-component example

我們可以在裡面定義 Head,Header,Footer,當然要留出一個內容區域的插槽 { children }

引用於 example 的 layout.js 程式碼

import Link from 'next/link'
import Head from 'next/head'

export default ({ children, title = 'This is the default title' }) => (
  <div>
    <Head>
      <title>{ title }</title>
      <meta charSet='utf-8' />
      <meta name='viewport' content='initial-scale=1.0, width=device-width' />
    </Head>
    <header>
      <nav>
        <Link href='/'><a>Home</a></Link> |
        <Link href='/about'><a>About</a></Link> |
        <Link href='/contact'><a>Contact</a></Link>
      </nav>
    </header>

    { children }

    <footer>
      {'I`m here to stay'}
    </footer>
  </div>
)複製程式碼

因為本次使用的是 antd 做 ui,固實現動態的導航展示上要注意些小問題,我們需要根據 path 動態的給 menu 啟用狀態。

兩個解決方案:

1.在 pages 裡面的每一個路由頁面裡獲取 pathname,初始化方法 getInitialProps 裡可以拿到 pathname,全部列表如下

  • pathname - path section of URL
  • query - query string section of URL parsed as an object
  • asPath - String of the actual path (including the query) shows in the browser
  • req - HTTP request object (server only)
  • res - HTTP response object (server only)
  • jsonPageRes - Fetch Response object (client only)
  • err - Error object if any error is encountered during the rendering

呼叫方法也簡單

static async getInitialProps({ pathname }) {
  return { pathname }
}複製程式碼

這樣一來可以通過傳參到 layout 元件的方式 <Layout pathname={this.props.pathname}></Layout>

在 Layout 裡面改變 Meun 的 active

2.寫一個 ActiveLink 元件,再封裝一層原有的 Menu

在選擇方案前還是要看官方有沒有 example,於是找到了 using-with-router

引用於 example 的 ActiveLink.js 程式碼

import { withRouter } from 'next/router'

// typically you want to use `next/link` for this usecase
// but this example shows how you can also access the router
// using the withRouter utility.

const ActiveLink = ({ children, router, href }) => {
  const style = {
    marginRight: 10,
    color: router.pathname === href ? 'red' : 'black'
  }

  const handleClick = (e) => {
    e.preventDefault()
    router.push(href)
  }

  return (
    <a href={href} onClick={handleClick} style={style}>
      {children}
    </a>
  )
}

export default withRouter(ActiveLink)複製程式碼

簡單易懂,在 withRouter 方法裡可以取到 router 例項,這樣可以取到 pathname,query 等等。

這裡只需要稍稍修改下 style,變成 antd 的 className,如下

const ActiveLink = ({ children, router, href }) => {
  const active = router.pathname === href
  const className = active ? 'ant-menu-item-selected ant-menu-item' : 'ant-menu-item'
  return (
    <li href='#' onClick={onClickHandler(href)} className={className} role="menuitem" aria-selected="false">
      {children}
    </li>
  )
}複製程式碼

在 Layout 元件的 Menu 裡直接使用 ActiveLink 元件即可,到這為止解決了全部路由相關問題和 Layout 元件問題

資料流

解決了路由問題下一步就是每個頁面的 content 的資料填充

我們依舊是在 getInitialProps 裡面獲取資料,相當於 prefatch 方法,服務端渲染會提前執行這個方法獲取資料渲染到模板

這裡涉及到一個 node 和 Browserify 同構的 fetch 庫 isomorphic-fetch,cli 工具應該會自帶這個庫,沒有的話提前安裝下。

到這裡就不用擔心 fetch api 在服務端的問題了,這裡獲取的列表資料走的介面基本一致 https://gank.io/api/data/{type}/{perPage}/{page}

三個變數 type-型別、perPage-每頁數量、page-頁數

接下來可以把 List 和 ListItem 抽象出來,成為共用的元件,每個頁面都可以呼叫,這裡不詳細展開說明,簡單的使用 antd 的 Card 元件,沒有特殊功能。

每個頁面的請求資料部分也基本一致,將資料存到 props 裡,傳入 List 元件中去

形成了簡單的單向資料流動

列表頁面

page元件(fetch data) -> List元件(繼承自 Layout) -> ListItem元件

時間軸頁面

page元件(fetch data) -> Timeline元件(繼承自 Layout)

提交乾貨頁面

page元件 -> Form元件(繼承自 Layout) -> post請求(傳送formData)

搜尋頁面

page元件 -> Input元件+空ListItem元件(繼承自 Layout) -> get請求(獲取關鍵詞對應query的列表資料) -> ListItem元件

Mobx

既然前面說清楚了資料流都十分簡單,那麼為什麼要引入全域性狀態管理徒增煩惱呢?

有一點無奈的地方是 getInitialProps 本身 return 的就是 props,在 react 裡面 props 是單向的,只能向下傳遞,且不能修改

這裡我們要分頁功能,但是首屏資料是 props 的,我們換頁之後沒辦法更新 props 的值,也就是沒辦法再次執行 getInitialProps

最簡單粗暴的方式就是放棄 spa 的動態切換資料,我們每次 Router.push({some page}/{per page}/{current page}),一朝回到解放前的 MVC 版路由切換。

能不能解決問題,答案是能解決問題,那麼既然是分頁元件,人家 antd 也提供了 Pagination 元件,問題一個接著一個,人家返回的列表並沒有告訴你 totalCount,沒有 totalCount 就沒辦法知道有多少頁。。。

好尷尬的問題,這個分頁沒法做,怒臉~~~

也不是沒辦法做,這個問題變向思考下可以做 loadMore,沒錯載入更多,當載入到最後一頁(即的列表長度小於 perPage)或是此頁恰巧等於 perPage 但下一頁為空陣列時,我們給一個提示,沒有更多內容了。

涉及到向 props 的 list 裡 concat 陣列,我們不得不引入全域性狀態來解決這個問題,不論是 redux 還是 mobx 都可以解決問題,需要注意的是,next.js 中的用法和普通 spa 的 react 應用有所差別。

還是去找 example,with mobx

引用於 example 的 store.js 程式碼

export function initStore (isServer, lastUpdate = Date.now()) {
  if (isServer) {
    return new Store(isServer, lastUpdate)
  } else {
    if (store === null) {
      store = new Store(isServer, lastUpdate)
    }
    return store
  }
}複製程式碼

這段程式碼太簡單,沒必要解釋了,總之我們在初始化頁面時呼叫 initStore 就好了,isServer 通過 getInitialProps 的 req 引數 !!req 判斷

然後在 loadMore 時出發一個 action

@action loadMoreList = (more) => {
  this.list = this.list.concat(more)
}複製程式碼

到這載入更多的功能也就實現了,不足的一點是 List 元件裡的 handleScroll 方法寫的有點簡陋,雖說能用,但存在問題,如多次觸發、未寫相容程式碼(後續會改進),放出程式碼供大家一笑

handleScroll () {
  if (document.documentElement.offsetHeight + document.documentElement.scrollTop > document.documentElement.scrollHeight - 50) {
    this.handleLoadMore()
  }
}複製程式碼

其它程式碼感興趣可以直接取倉庫看,沒有閱讀難度。

表單提交

說到其它頁的 fetch list 沒什麼可將全都是 get 請求,fetch 發一個 get 請求十分簡單,不用宣告請求型別。

fetch 操作 post 也僅僅在於設定 method 為 POST

之所以單獨一章說表單提交,因為在提交表單時遇到了一些問題,由於要 fetch 模擬 form 的 post 請求

看了這個 issue:github.com/matthew-and…

開始懷疑人生,試了所有方法 POST,也走的通,但是介面返回的 msg 就是沒接收到引數。

想了想還是迴歸到笨方法一個一個將引數拼接進去,沒想到較優雅的方式,給出程式碼,同時歡迎討論

handleSubmit = (e) => {
  e.preventDefault()

  this.props.form.validateFieldsAndScroll(async (err, values) => {
    if (!err) {
      this.setState({ submitLoading: true })

      let strList = []

      Object.keys(values).forEach(item => {
        strList.push(`${item}=${values[item]}`)
      })

      const res = await fetch("https://gank.io/api/add2gank", {
        method: "POST",
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded'
        },
        body: strList.join('&')
      })

      const json = await res.json()

      if (json.error) {
        message.error(json.msg)
      } else {
        message.success(json.msg)
      }

      this.setState({ submitLoading: false })
    }
  })
}複製程式碼

看過網站的讀者也發現提交表單頁面上方有提示語,讓大家文明使用三方 api 提供者 gank 的發表乾貨介面,把真正的好內容提交上去,想測試介面的請走預設的 debug 模式,這裡再次強調下,感謝配合

微互動

既然功能差不多了,再微互動上再加把勁,用過 NUXT 的知道 NUXT 內建了 Loading bar,切換路由時在頁面頂端會有 loading 條,體驗較好。

next.js 並沒有內建這個功能,頁面看起來會顯得十分怪異,點選切換路由沒有反應,頓一下再跳轉,頓的時候在獲取初始化資料。

官方推薦使用 nprogress

關鍵程式碼如下,寫在了 Layout.js 元件裡

Router.onRouteChangeStart = (url) => {
  console.log(`Loading: ${url}`)
  NProgress.start()
}
Router.onRouteChangeComplete = () => NProgress.done()
Router.onRouteChangeError = () => NProgress.done()複製程式碼

這樣整個網站看起來洋氣多了,切換 router 頁面頂端有 loading bar,右上角還有 loading icon

上線

開發 next.js 的組織叫 zeit,在官網他們的得意作品是 now,一個快速部署的工具,同時為免費使用者提供三個免費的服務,支援 docker,node 等

看 5 分鐘文件就能上手部署 node 專案,比 Heroku 簡單的多

這裡使用的就是 now,首先安裝 now-cli

在專案根路徑下一句命令部署

now複製程式碼

線上的路徑就不貼出來了,時刻關注 Github 上方的 website 地址,因為每次部署不繫結域名的情況下是 專案名+隨機雜湊 的域名,繫結域名需要 money。

至於上線就講這麼多,有疑問歡迎交流。

未來

下一步要解決幾個問題

  • 載入更多時的 bug
  • 支援移動端
  • 福利頁面直接展示圖片(點選可以全屏大輪播)
  • 美化時間軸樣式

說到福利頁面本想著不加來著,因為個別寫 demo 的人專門把福利列表拎出來做成妹子 App,既然是乾貨集中營,就應該多些技術元素,福利都是次要的。

總結

到這為止一個 next.js 版本的 gank(乾貨集中營)完成了,感慨現在開發工具越來越好用,還是之前的想法把好用的工具分享給大家,給一個完整的例子供學習者參考,不再每次都看各個版本的 Hacker News,而是給國內的學習者一箇中文版的例子,同時文中也會將實現的時候遇到的問題。

本人 orange 也是再不斷的學習當中,本文也是第一次接觸學習 next.js 寫的專案,文章或專案有不足之處歡迎指正,感謝閱讀!

本文同步更新至我的個人部落格 orangexc.xyz

掘進寫文章有機會得非同步社群的書,非同步社群作為國內頂尖的IT專業圖書社群,發行的技術書籍質量高,為本文打 call 希望可以晉級,我想要這本書 JavaScript框架設計(第2版)

相關文章