React 服務端渲染如此輕鬆 從零開始構建先後端應用

參加或留意了最近舉行的JSConf CN 2017的同窗,想必對 Next.js 再也不陌生, Next.js 的做者之一到場進行了精彩的演講。其實在更早些時候,由 Facebook 舉辦的 React Conf 2017,他就到場並有近40分鐘的分享。但兩次分享帶來的 demo 都是 hacker news。我觀察 Next.js 時間較長,看着它從1.x 版本一直到了今天的 3.x,終於決定寫一篇入門級的新手指導文章。而這篇文章試圖經過一個全新的例子,來讓你們瞭解 Next.js 究竟是如何與 React 配合,達到服務端渲染的。css

「React universal」 是社區上形容基於 React 構建 web 應用,並採用「服務端渲染」方式的一個詞語。也許不少人對 「isomorphic」 這個單詞更加熟悉,其實這兩個詞語想要表達的概念相似。今天這篇文章顯然不是討論這兩個詞語的,咱們要嘗試使用最新版 Next.js,構件一個簡單的服務端渲染 React 應用。前端

最終項目地址能夠點擊這裏查看。node

爲什麼要開發 Universal 應用?

React app 實現了虛擬 DOM,來實現對真實 DOM 的抽象。這樣的設計迅速引領了前端開發浪潮。可是 「Every great thing comes with a price」,虛擬 DOM 一樣帶來了一些弊端,好比在先後端分離的開發模式下,SEO就成了問題;一樣首屏加載時間變長,各類 loading 消磨人的耐心。就像下面截圖所展示的那樣:react

頁面
頁面

查看網頁源碼
查看網頁源碼

使用 Next.js 實現 Universal

Universal 應用架構能夠簡單粗暴先而片面的理解成應用將在客戶端和服務端共同完成渲染。這樣取代了徹底由客戶端渲染(先後端分離方式)模式。在 React 場景下,咱們可使用 React 自身的 renderToString 完成服務端初次渲染。可是若是咱們每次手動來完成這些過程,手動實現服務端繁瑣配置,不免使人頭大心煩。webpack

Next.js 的出現,就是爲你解決這種惱人的問題。咱們先來認識一下它的幾個原則和思想:ios

  • 不須要除 Next 以外,多餘的配置和安裝(好比 webpack,babel);
  • 使用 Glamor 處理樣式;
  • 自動編譯和打包;
  • 熱更新;
  • 方便的靜態資源管理;
  • 成熟靈活的路由配置,包括路由級別 prefetching;

Demo:英超聯賽積分榜

其實關於更多的 Next.js 設計理念我不想再贅述了,讀者均可以在其官網找到豐富的內容。下面,我將使用 Football Data API 來簡單開發一個基於 Next.js 的應用,這個應用將展示英超聯賽的實時積分榜。同時包含了簡單的路由開發和頁面跳轉。git

小試牛刀

相信全部的開發者都厭惡超長時間的安裝和各類依賴、插件配置。不要擔憂,Next.js 做爲一個獨立的 npm package 最大限度的替你完成了不少耗時且無趣的工做。咱們首先須要進行安裝:github

# Start a new project
npm init
# Install Next.js
npm install next --save複製代碼

安裝結束後,咱們就能夠開啓腳本:web

"scripts": {
   "start": "next"
 },複製代碼

Next 安裝的同時,也會安裝 React,因此無需本身費心。接下來所須要作的很簡單,就是在根目錄下建立一個 pages 文件夾,並在其下新建一個 index.js 文件:ajax

// ./pages/index.js

// Import React
import React from 'react'

// Export an anonymous arrow function
// which returns the template
export default () => (
  <h1>This is just so easy!</h1>
)複製代碼

好了,如今就能夠直接看到結果:

# Start your app
npm start複製代碼

頁面
頁面

驗證一下它來自服務端渲染:

查看網頁源碼
查看網頁源碼

就是這麼簡單,清新。若是咱們本身手段實現這一切的話,除了 NodeJS 的種種繁瑣不說,webpack 配置,node_modules 依賴,babel插件等等就夠折騰半天的了。

添加 Page Head

在 ./pages/index.js 文件內,咱們能夠添加頁面 head 標籤、meta 信息、樣式資源等等:

// ./pages/index.js
import React from 'react'
// Import the Head Component
import Head from 'next/head'

export default () => (
  <div>
    <Head>
        <title>League Table</title>
        <meta name="viewport" content="initial-scale=1.0, width=device-width" />
        <link rel="stylesheet" href="https://unpkg.com/purecss@0.6.1/build/pure-min.css" />
    </Head>
    <h1>This is just so easy!</h1>
  </div>
)複製代碼

這個 head 固然不是指真實的 DOM,千萬別忘了 React 虛擬 DOM 的概念。其實這是 Next 提供的 Head 組件,不過最終必定仍是被渲染成爲真實的 head 標籤。

發送 Ajax 請求

Next 還提供了 getInitialProps 方法,這個方法支持異步選項,而且是服務端/客戶端同構的。咱們可使用 async/await 方式,處理異步請求。請看下面的示例:

import React from 'react'
import Head from 'next/head'
import axios from 'axios';

export default class extends React.Component {
    // Async operation with getInitialProps
    static async getInitialProps () {
        // res is assigned the response once the axios
        // async get is completed
        const res = await axios.get('http://api.football-data.org/v1/competitions/426/leagueTable');
        // Return properties
        return {data: res.data}
      }
 }複製代碼

咱們使用了 axios 類庫來發送 HTTP 請求。網絡請求是異步的,所以咱們須要在將來某個合適的時候(請求結果返回時)接收數據。這裏使用先進的 async/await,以同步的方式處理,從而避免了回調嵌套和 promises 鏈。

咱們將異步得到的數據返回,它將自動掛載在 props 上(注意 getInitialProps 方法名,顧名思義),render 方法裏即可以經過 this.props.data 獲取:

import React from 'react'
import Head from 'next/head'
import axios from 'axios';

export default class extends React.Component {
  static async getInitialProps () {
    const res = await axios.get('http://api.football-data.org/v1/competitions/426/leagueTable');
    return {data: res.data}
  }
  render () {
    return (
      <div>
        <Head>
            ......
        </Head>
        <div className="pure-g">
            <div className="pure-u-1-3"></div>
            <div className="pure-u-1-3">
              <h1>Barclays Premier League</h1>
              <table className="pure-table">
                <thead>
                  <tr>
                    ......
                  </tr>
                </thead>
                <tbody>
                {this.props.data.standing.map((standing, i) => {
                  const oddOrNot = i % 2 == 1 ? "pure-table-odd" : "";
                  return (
                      <tr key={i} className={oddOrNot}>
                        <td>{standing.position}</td>
                        <td><img className="pure-img logo" src={standing.crestURI}/></td>
                        <td>{standing.points}</td>
                        <td>{standing.goals}</td>
                        <td>{standing.wins}</td>
                        <td>{standing.draws}</td>
                        <td>{standing.losses}</td>
                      </tr>
                    );
                })}
                </tbody>
              </table>
            </div>
            <div className="pure-u-1-3"></div>
        </div>
      </div>
    );
  }
}複製代碼

這樣,再訪問咱們的頁面,就有了:

頁面
頁面

路由和頁面跳轉

也許你已經有所感知:咱們已經有了最基本的一個路由。Next 不須要任何額外的路由配置信息,你只須要在 pages 文件夾下新建文件,每個文件都將是一個獨立的頁面。

讓咱們來新建一個 team 頁面吧!新建 ./pages/details.js 文件:

// ./pages/details.js
import React from 'react'
export default () => (
  <p>Coming soon. . .!</p>
)複製代碼

咱們使用 Next 已經準備好的組件 來進行頁面跳轉:

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

// Import Link from next
import Link from 'next/link'

export default () => (
  <div>
      <p>Coming soon. . .!</p>
      <Link href="/"><a>Go Home</a></Link>
  </div>
)複製代碼

這個頁面不能老是 「Coming soon. . .!」 的信息,咱們來進行完善以展現更多內容,經過頁面 URL 的 query id 變量,咱們來請求並展示當前相應隊伍的信息:

import React from 'react'
import Head from 'next/head'
import Link from 'next/link'
import axios from 'axios';

export default class extends React.Component {
    static async getInitialProps ({query}) {
        // Get id from query
        const id = query.id;
        if(!process.browser) {
            // Still on the server so make a request
            const res = await axios.get('http://api.football-data.org/v1/competitions/426/leagueTable')
            return {
                data: res.data,
                // Filter and return data based on query
                standing: res.data.standing.filter(s => s.position == id)
            }
        } else {
            // Not on the server just navigating so use
            // the cache
            const bplData = JSON.parse(sessionStorage.getItem('bpl'));
            // Filter and return data based on query
            return {standing: bplData.standing.filter(s => s.position == id)}
        }
    }

    componentDidMount () {
        // Cache data in localStorage if
        // not already cached
        if(!sessionStorage.getItem('bpl')) sessionStorage.setItem('bpl', JSON.stringify(this.props.data))
    }

    // . . . render method truncated
 }複製代碼

這個頁面根據 query 變量,動態展示出球隊信息。具體來看,getInitialProps 方法獲取 URL query id,根據 id 篩選出(filter 方法)展現信息。有意思的是,由於一直球隊的信息比較穩定,因此在客戶端使用了 sessionStorage 進行存儲。

完整的 render 方法:

// . . . truncated

export default class extends React.Component {
    // . . . truncated
    render() {

        const detailStyle = {
            ul: {
                marginTop: '100px'
            }
        }

        return  (
             <div>
                <Head>
                    <title>League Table</title>
                    <meta name="viewport" content="initial-scale=1.0, width=device-width" />
                    <link rel="stylesheet" href="https://unpkg.com/purecss@0.6.1/build/pure-min.css" />
                </Head>

                <div className="pure-g">
                    <div className="pure-u-8-24"></div>
                    <div className="pure-u-4-24">
                        <h2>{this.props.standing[0].teamName}</h2>
                        <img src={this.props.standing[0].crestURI} className="pure-img"/>
                        <h3>Points: {this.props.standing[0].points}</h3>
                    </div>
                    <div className="pure-u-12-24">
                        <ul style={detailStyle.ul}>
                            <li><strong>Goals</strong>: {this.props.standing[0].goals}</li>
                            <li><strong>Wins</strong>: {this.props.standing[0].wins}</li>
                            <li><strong>Losses</strong>: {this.props.standing[0].losses}</li>
                            <li><strong>Draws</strong>: {this.props.standing[0].draws}</li>
                            <li><strong>Goals Against</strong>: {this.props.standing[0].goalsAgainst}</li>
                            <li><strong>Goal Difference</strong>: {this.props.standing[0].goalDifference}</li>
                            <li><strong>Played</strong>: {this.props.standing[0].playedGames}</li>
                        </ul>
                        <Link href="/">Home</Link>
                    </div>
                </div>
             </div>
            )
    }
}複製代碼

注意下面截圖中,同一頁面不一樣 query 值,分別展現了冠軍🏆切爾西和曼聯的信息。

切爾西
切爾西

曼聯
曼聯

別忘了咱們的主頁(排行榜頁面)index.js 中,也要使用相應的 sessionStorage 邏輯。同時,在 render 方法里加入一條連接到詳情頁的 :

<td><Link href={`/details?id=${standing.position}`}>More...</Link></td>複製代碼

錯誤頁面

在 Next 中,咱們一樣能夠經過 error.js 文件定義錯誤頁面。在 ./pages 下新建 error.js:

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

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

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

當傳統狀況下頁面404時,獲得:

404頁面
404頁面

在咱們設置 _ error.js 以後,便有:

自定義錯誤頁面
自定義錯誤頁面

總結

這篇文章實現了一個簡易 demo,只是介紹了最基本的 Next.JS 搭建 React 同構應用的基本步驟。

想一想你是否厭煩了 webpack 惱人的配置?是否對於 Babel 各類插件雲裏霧裏?
使用 Next.js,簡單、清新而又設計良好。這也是它在推出短短期以來,便迅速走紅的緣由之一。

除此以外,Next 還有很是多的功能,很是多的先進理念能夠應用。

  • 好比 搭配 prefetch,預先請求資源;
  • 再如動態加載組件(Next.js 支持 TC39 dynamic import proposal),從而減小首次 bundle size;
  • 雖然它替咱們封裝好了 Webpack、Babel 等工具,可是咱們又能 customizing,根據須要自定義。

最後,對於這些本文章沒有演示到的功能是否有些手癢?感興趣的讀者能夠關注本文 demo 的Github項目地址,本身手動嘗試起來吧~

本文意譯了Chris Nwamba的:React Universal with Next.js: Server-side React 一文,並對原文進行了升級,兼容了最新的 Next 設計。

個人其餘關於 React 文章:

Happy Coding!

PS:
做者Github倉庫知乎問答連接歡迎各類形式交流。

相關文章
相關標籤/搜索