小猿大圣

基于 React 的通用框架 Next.js:服务端 React

小猿大圣 · 2017-01-01翻译 · 1299阅读 原文链接

“通用(universal)”是一个社区创造的术语,是指构建的 web 应用程序能使服务端更简单便利的渲染页面。你可能更熟悉“同构(isomorphic)”这种叫法,但是这篇文章的目的不是为了讨论这些命名。我们将学习如何使用 Next.js 构建由服务端渲染的 React 应用程序。

我们之前讨论了构建 服务端 React 。今天我们将讨论更多话题,因为它很重要。

为什么构建通用应用程序

React 应用程序实现了虚拟 DOM 的思想,那比真实 DOM 要抽象的多。这种抽象在应用程序的性能上是非常有用的,因为我们可以只取一部分 DOM,绑定我们想绑定的各种数据,然后插入到真实的 DOM 树中。 这绝不是标准化,只是前端框架为了实现更好的用户体验而提出的一个概念。

正如每一个伟大的事情都是有代价的,虚拟 DOM 也带来了一个问题。原始DOM接收的基于服务器提供的一些信息已经丢失。你可能会想知道为什么会出现这样的问题。它确实如此,接下来,我们将看到为什么会这样。

搜索引擎不在乎你的应用程序的架构,也不在乎调节和获取正确内容的概念是什么样的。他们的爬虫不会像一个真实的用户那样聪明的使用你的应用程序。 他们关心的是,一旦他们发送蜘蛛抓取和索引你的网站,无论服务器提供的第一个请求是什么都将被索引。这是坏消息,因为那时你的应用程序缺少服务器上的实际内容。这是蜘蛛从你的网站爬取的内容:

React 服务端预览

预览

React 服务端预览

源码

当你试图在 Facebook 或 Twitter 这样的社交媒体平台上分享你的应用时,情况会变得更糟。你可能必须结束这一行动,因为你的实际内容不是装在服务器上,它只是入口点(可能只是一些index.html内容)。

我们如何解决这些问题?

使用通用框架 Next.js

通用应用程序是使用可以使你的应用程序在客户端和服务器端同时渲染的方式架构的。在 React 的示例中,如果你没有选择正确的工具的话,虚拟 DOM 最终会落到服务器端以及使用的一些机制,可能使你感到头痛。

我尝试过几个解决方案,而 Next.js 是非常有前途的。Next 灵感来自于7 个富应用原理。 这种想法是为了使用Web应用程序以及构建它时有好的体验。这种体验应该是自然的。

Next 提供了更多:

  • 没有配置或附加设置
  • 简单组件和Glamor全局风格
  • 自动打包构建(用 webpack 和 babel)
  • 代码热加载
  • 静态文件服务 . ./static/ 会映射到 /static/
  • 路由预加载 马上就来

演示: 英超联赛表

让我们使用 Next.js 一起做一些有趣的东西。我们将使用足球数据 API构建一个简单的应用程序显示英超排名表。如果你感兴趣的话,那么让我们开始。

预备知识

当创建一个新项目时,开发者都很讨厌那些长长的安装介绍。不同担心,Next.js 仅仅需要一个npm 包你需要做的就是本地安装,并且开始构建你的应用程序:

# 开始一个新项目
npm init
# 安装 Next.js
npm install next --save

安装完成后, 你可以重写start 脚本来运行next:

"scripts": {
   "start": "next"
 },

Next 安装了 React 所以我们就不需要再安装 React 了。你现在需要做的是,在你的应用程序的根目录创建一个pages目录并且添加一个index.js 文件。

// ./pages/index.js

//引入 React
import React from 'react'

// 导出一个匿名箭头函数
// 返回模板
export default () => (
  <h1>This is just so easy!</h1>
)

现在运行下面的命令,并且访问 localhost:3000:看一下你的应用程序

# 启动你的应用
npm start

预览

源码

我打赌这比你想象的要容易。你只用了大约5分钟就运行起一个服务器渲染的应用程序。我们正在创造历史!

页面头部

我们可以在页面添加一个head 部分,以便定义全局样式并且设置 meta 细节:

// ./pages/index.js
import React from 'react'
// 引入头部组件
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>
)

Next 提供了Head 组件关联页面的 head元素。 我们只需要把原本 head 元素里的内容用 Next 的Head 组件的包裹起来就可以了。

Ajax 请求

Next 提供了能够异步 (可选择)的getInitialProps 属性,协助执行异步操作。 你能使用await 关键字控制延迟操作。看下面的示例:

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

export default class extends React.Component {
    // getInitialProps 异步操作
    static async getInitialProps () {
        // 一旦axios响应返回数据 res就会赋值
        // 异步完成
        const res = await axios.get('http://api.football-data.org/v1/competitions/426/leagueTable');
        // 返回结果
        return {data: res.data}
      }
 }

我们使用 axios 库处理 HTTP 请求。 请求是异步的,所以我们需要一种方法在请求成功后,能够获取响应的信息。使用async...await,我们可以更实际的处理异步请求,而不需要使用回调或者 promises 链。

我们通过返回对象向 props 传递值,我们能从props 像这样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>
            <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-1-3"></div>
            <div className="pure-u-1-3">
              <h1>Barclays Premier League</h1>
              <table className="pure-table">
                <thead>
                  <tr>
                    <th>Position</th>
                    <th>Team</th>
                    <th>P</th>
                    <th>GL</th>
                    <th>W</th>
                    <th>D</th>
                    <th>L</th>
                  </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></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>
    );
  }
}

React 通用应用程序进程

我们现在可以将数据中standing 属性里的内容迭代的绑定到要渲染的模板上 ,并且打印每个standing。 其中使用到的类名,是为了快速开发,在 head 中引入的一个非常简单 的纯 CSS 样式库。

路由

你可能没有意识到,但我们的应用程序已经有路由了。Next 不需要任何其他额外的配置去设置路由。你只需要把要页面添加到page 目录,路由会自动生成。让我们创建另一个路由来显示团队的更多细节:

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

在 Next 中,你可以用 Link 组件从一个路由跳转到另一个路由。

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

// 从 Next 引入 Link
import Link from 'next/link'

export default () => (
  <div>
      <p>Coming soon. . .!</p>
      <Link href="/">Go Home</Link>
  </div>
)

现在更新详细信息页,来显示更多被给定团队的信息。团队的位置将作为查询参数传递。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}) {
        // 查询到 id
        const id = query.id;
        if(!process.browser) {
            // 向服务器发送请求
            const res = await axios.get('http://api.football-data.org/v1/competitions/426/leagueTable')
            return {
                data: res.data,
                // 基于查询过滤数据
                standing: res.data.standing.filter(s => s.position == id)
            }
        } else {
            // Not on the server just navigating so use
            // 缓存
            const bplData = JSON.parse(sessionStorage.getItem('bpl'));
            // 基于查询过滤数据并返回
            return {standing: bplData.standing.filter(s => s.position == id)}
        }
    }

    componentDidMount () {
        // 如果已经没有缓存了,在 localStorage 中缓存
        if(!sessionStorage.getItem('bpl')) sessionStorage.setItem('bpl', JSON.stringify(this.props.data))
    }

    // . . .渲染部分省略
 }

此页根据查询参数显示动态内容。我们从getInitialProps 接收query,然后用 ID 值根据一个给定的团队的位置筛选数据。

上述逻辑的最有趣的方面是,我们不是在应用程序的生命周期中不止一次请求数据。一旦服务器渲染,我们获取数据并且把它缓存到 sessionStorage。后续导航将基于缓存的数据。sessionStoragelocalStorage更好,因为当前窗口退出后数据不会保存。

componentDidMount 的时候去存储,并且不需要getInitialProps。因为当触发getInitialProps 时,浏览器还没有准备,此时 sessionStorage 不会保存数据。 因为这样, 我们需要等待浏览器准备好,之后好的方法是用componentDidMount React 生命周期方式 catch。getInitialProps 被说是同构的。

现在,让我们渲染到浏览器:

// . . . 省略

export default class extends React.Component {
    // . . . 省略
    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>
                        <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>
            )
    }
}

我们的index页不会实现和详情页相关的特性的性能。我们可以相应地更新组件:

// . . . 省略引入部分

export default class extends React.Component {
  static async getInitialProps () {
    if(!process.browser) {
      const res = await axios.get('http://api.football-data.org/v1/competitions/426/leagueTable')
      return {data: res.data}
    } else {
      return {data: JSON.parse(sessionStorage.getItem('bpl'))}
    }
  }

  componentDidMount () {
    if(!sessionStorage.getItem('bpl')) sessionStorage.setItem('bpl', JSON.stringify(this.props.data))
  }
  // . . . 省略渲染方式
}

index 模板也应该更新,包络每一个指向团队详情页的链接:

// . . . 省略引入部分
import Link from 'next/link'

export default class extends React.Component {
 // . . . 省略
  render () {
    const logoStyle = {
      width: '30px'
    }

    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-1-3"></div>
            <div className="pure-u-1-3">
              <h1>Barclays Premier League</h1>

              <table className="pure-table">
                <thead>
                  <tr>
                    <th>Position</th>
                    <th>Team</th>
                    <th>P</th>
                    <th>GL</th>
                    <th>W</th>
                    <th>D</th>
                    <th>L</th>
                    <th></th>
                  </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></td>
                        <td>{standing.points}</td>
                        <td>{standing.goals}</td>
                        <td>{standing.wins}</td>
                        <td>{standing.draws}</td>
                        <td>{standing.losses}</td>
                        <td><Link href={`/details?id=${standing.position}`}>More...</Link></td>
                      </tr>
                    );
                })}
                </tbody>
              </table>
            </div>
            <div className="pure-u-1-3"></div>
        </div>
      </div>
    );
  }
}

错误页

你可以通过新建一个_error.js 文件创建一个传统的错误页面,来处理4xx 或者5xx错误 。但是 Next 已经内置了处理错误页的逻辑,你不需要创建新的错误页面了。

// ./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错误页面看起来像这样:

React Universal 404

...但传统的错误页面,你会看到这样的:

部署

Next 总是随时准备生产环境的部署。让我们使用 now ,看看部署是多么简单。首先安装now:

npm install -g now

你需要下载一个 桌面 应用程序,获得一个账号

更新 package.json 中的脚本,在我的应用程序中添加build命令:

"scripts": {
    "dev": "next",
    "build": "next build",
    "start": "next start"
  },

现在运行构建和开始命令,为部署做准备:

# 构建
npm run build
# 开始
npm start

部署仅仅需要运行now:

now

额外资源

相关文章