React-入门指南-二-

76 阅读42分钟

React 入门指南(二)

原文:zh.annas-archive.org/md5/7f9f750654ee9c470c2b501ee0ba1d55

译者:飞龙

协议:CC BY-NC-SA 4.0

第六章. 使用 Flux 进行响应

到目前为止,在前面的章节中,我们已经深入探讨了 React 世界。现在让我们探索 React 世界的另一个维度,Flux,它实际上只是一个单向数据流架构。Flux 是由 Facebook 内部开发团队开发的,用于在 Facebook 构建客户端 Web 应用程序。

随着我们的进展,我们将涵盖以下主题:

  • Flux 与 MVC 架构的概述

  • 动作

  • 调度器

  • 存储

  • 控制器-视图和视图

Flux 概述

Flux不应与基于 ReactJS 的框架混淆。Flux 是一种架构,旨在减少使用模型-视图-控制器MVC)架构构建的大型应用程序的复杂性,并且被设计为 MVC 的替代方案。

以下是一些不同的 Flux 组件:

  • 视图—这就像任何 Web 应用程序一样,视图(基本上是 React 组件)接收事件并将其传递给动作。

  • 动作—它们是辅助方法(actionCreators),将来自外部 API/视图的数据(有效载荷)和actionType传递给调度器。

  • 调度器—这些是所有注册回调的中心枢纽。它接收动作,并在将其传递给存储之前充当“交通控制器”。

  • Store—它是一个数据层,存储所有计算和业务逻辑。它还负责存储应用程序状态和应用程序状态的单一真相来源。它根据注册的回调从调度器接收动作。

  • 控制器-视图—它根据changeEvents从存储中接收状态,并通过属性将状态传递给 React 视图组件。

以下图表说明了这一点:

Flux 概述

典型的 Flux 数据流架构

Flux 与 MVC 架构

在基于 MVC 架构的典型应用程序中,视图从数据更新,这些数据通常存储在模型中。随着应用程序的增长,模型和视图的数量也增加,各种模型之间的相互依赖性也随之增加。因此,视图也变得依赖于多个模型,从而增加了应用程序的复杂性。

视图和模型之间的相互依赖性可能导致真相来源的扩散,导致应用程序复杂性和不可预测性的增加。因此,需要有一种解决方案,通过将所有控制移入各个部分来内部化控制。

Flux 与 MVC 架构

使用 MVC 构建的日益增长的应用程序的问题

Flux 优势

根据 Facebook Flux 开发团队的说法,Flux 应用程序中的对象高度解耦,并且非常严格地遵循德米特法则的第一部分:系统中的每个对象应尽可能少地了解系统中的其他对象。这导致软件更加。

  • 可维护性

  • 可适应性

  • 可测试性

  • 对于新工程团队成员来说更容易理解且更可预测

以下是我们 library_app 应用程序的 Flux 应用程序结构。

Flux 优势

我们的图书馆应用程序结构

流量组件

让我们深入一个使用 React 视图构建的 Flux 架构的应用程序。在这里,我们将构建一个名为 library_app 的应用程序。这是一个基本的基于 Flux 的 ReactJS 应用程序,我们可以从 library_app 存储库借阅书籍到我们的阅读列表。一旦我们完成阅读书籍,我们可以将其从阅读列表中删除。

注意

在命令行执行:

sudo npm install flux

上述操作将安装 flux 包作为节点模块,并且你的 library_app 应用程序将有一个名为 node_modules 的目录,其中包含安装的 flux 库。

动作

动作通常是进入应用程序的数据,无论是直接来自视图还是来自外部 Web API。每个动作不过是一个 JavaScript 方法,它包含两部分:actionType 和实际数据。actionCreators 方法仅仅是离散的、语义化的辅助函数,它简化了将数据以 action 的形式传递给 dispatcher 的过程。不同类型的动作被声明为一个 JavaScript 对象,在一个名为 App-Constants.js 的文件中。根据 Flux 应用程序层次结构,App-Constants.js 文件位于 src/js/constants 目录下。此类文件的典型示例如下:

module.exports = {
 ADD_BOOK: 'ADD_BOOK',
 DELETE_BOOK: 'DELETE_BOOK',
 INC_BOOK_COUNT: 'INC_BOOK_COUNT',

DEC_BOOK_COUNT: 'DEC_BOOK_COUNT'
}

在这里,ADD_BOOKDELETE_BOOK 是动作。

注意

动作本身不包含任何功能。动作通常由 stores 执行,并且可用于触发视图。在 React 中,我们有少量名为 actionCreators 的辅助方法,理想情况下,这些方法创建动作对象并将动作传递给 Flux 分发器(AppDispatcher)。

AppConstants 中定义的所有动作都在 AppActions 中声明。

AppConstants 中使用常量来声明动作名称,有助于开发者理解应用程序的功能。正如我们的案例,它处理书籍。

在以下示例中,在向 library_app 存储库添加书籍时,我们处理了四个 actionTypes

  • ADD_BOOK

  • DELETE_BOOK

  • INC_BOOK_COUNT

  • DEC_BOOK_COUNT

动作(如 addBookremoveBookincBookCountdecBookCount)根据它们的 actionType 属性是唯一的。因此,当这些动作由 dispatchers 分发到 stores 时,stores 会根据与 dispatchers 注册的特定回调进行自我更新。

典型的动作文件位于 library_app/src/js/actions/app-actions.js

var AppConstants = require('../constants/app-constants');
var AppDispatcher = require('../dispatchers/app-dispatchers');

var AppActions = {
  addBook:function(item){
        AppDispatcher.handleViewAction({
          actionType: AppConstants.ADD_BOOK,
          item: item
        })
  },
  removeBook:function(index){
        AppDispatcher.handleViewAction({
          actionType: AppConstants.REMOVE_BOOK,
          index: index
        })
  },
  incBookCount:function(index){
        AppDispatcher.handleViewAction({
          actionType: AppConstants.INC_BOOK_COUNT,
          item: index
        })
  },
  decBookCount:function(index){
        AppDispatcher.handleViewAction({
          actionType: AppConstants.DEC_BOOK_COUNT,
          item: index
        })
  }
}

module.exports =  AppActions;

分发器

正如其名所定义的,Flux 分发器将动作分发到后续的 stores。分发器可以被视为回调的注册表。所有 stores 都注册在分发器上。

分发器的一些关键点如下:

  • 每个应用程序只有一个分发器。

  • Dispatchers 被用作所有注册回调的中心。

  • 它充当所有动作到商店的广播器。调度器作为一个队列,依次广播动作。这与通用的 pub-sub 系统在以下两个方面不同:

    1. 回调没有订阅特定的事件。每个有效负载都会分发到每个已注册的回调。

    2. 回调可以全部或部分地延迟,直到其他回调已执行。

  • 调度器具有按指定顺序调用回调的能力,并且它等待其他更新(waitFor() 方法就是这样做的)。

  • 在 flux 库(npm install flux)的 node_module 中,定义了 register()dispatch() 方法,在 flux library_app 中的调度器类中。

查看位于 library_app/node_modules/Flux/lib/Dispatcher.js 的文件:

   // Registers a callback to be invoked with every dispatched payload. Returns
   // a token that can be used with `waitFor()`.   

  Dispatcher.prototype.register = function register(callback) {
    var id = _prefix + this._lastID++;
    this._callbacks[id] = callback;
    return id;
  };

因此,当调度器从 Actions 接收触发(动作)时,它会逐个将所有动作分发到已注册的商店。这种分发流程是通过 dispatch() 方法启动的,该方法将有效负载(数据)传递到已注册的商店,并注册了回调。

以下代码是从 node_modules 中的 Flux.js 库的调度器摘录:

  /**
   * Dispatches a payload to all registered callbacks. The highlighted code below ensures the fact that dispatches cannot be triggered in the middle of another dispatch.

   */

 Dispatcher.prototype.dispatch = function dispatch(payload) {
 !!this._isDispatching ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch.') : invariant(false) : undefined;
    this._startDispatching(payload);
    try {
      for (var id in this._callbacks) {
        if (this._isPending[id]) {
          continue;
        }
        this._invokeCallback(id);
      }
    } finally {
      this._stopDispatching();
    }
  };
app-dispatcher class.

文件位置在 library_app/src/js/dispatchers/app-dispatchers.js

var Dispatcher = require('flux').Dispatcher;
var assign = require('react/lib/Object.assign');

var AppDispatcher = assign(new Dispatcher(),{
        handleViewAction: function(action){
          console.log('action',action);
          this.dispatch ({
            source: 'VIEW_ACTION',
            action: action
          })
        }
});

module.exports = AppDispatcher;

在实现 library_app 商店之前,让我们检查我们的有效负载(数据)是否在控制台中打印出来。为此,在 React component app.js 中创建了一个处理函数,当点击标题 My First Flux App 的任何部分时会被调用。

文件位置是 library_app/src/js/components/app.js

var React = require('react');
var ReactDOM = require('react-dom');

//call the AppActions directly, before creation of the Store
var AppActions = require('../actions/app-actions');

//create a App component
var App = React.createClass({
        handler: function(){
          AppActions.addBook('This is the book..Sherlock Holmes')
        },
        render:function(){
          return <h1 onClick={this.handler}>My First Flux App </h1>
        }

});
module.exports = App;

注意

从应用程序的根目录运行 httpster:

doel@doel-Vostro:~/reactjs/ch6_flux_library$httpster
Starting HTTPster v1.0.1 on port3333 from /home/doel/reactjs/ch6_flux_library

打开浏览器并检查控制台,点击标题后:

调度器

library_app 的截图

为了快速回顾我们书店应用到目前为止的流程:

默认的 index.html 页面在 localhost:3333 上提供静态内容(示例应用)。

index.html 页面内部调用 main.js,然后内部创建 React 类并在 <App /> React 组件(来自 src/js/components/app.js)中渲染内容。React 组件在具有 ID maindiv 标签中渲染。

一旦我们点击 <App /> 组件(My First Flux App)的任何部分,一个 onClick 事件处理程序会触发 handler() 函数,该函数调用 AppActions.addBook (This is the book..Sherlock Holmes),在这里,AppActionsAppConstant 中。AddBook 是要调用的特定动作,带有 payload / item/ data (This is the book..Sherlock Holmes)。

一旦调用 AppActions.addBook 方法,它就会被分配给调度器的回调 handleViewAction,如下所示:

  • actionType: AppConstants.ADD_BOOK

  • item: This is the book..Sherlock Holmes

  • 调度器的 handleViewAction 方法传递动作(带有 action_typeitem)并在控制台记录输出,然后分发它。

  • 在点击我的第一个 Flux 应用程序后,我们在console.log中看到以下输出:

    action Object { actionType: "ADD_BOOK", item: "This is the book..Sherlock Holmes" }

  • 这只是以统一和预期的方式将 JS 对象(item: "This is the book..Sherlock Holmes")传递给商店处理的一种方式。它简化了应用程序的数据流,并使得跟踪和调试更容易。

商店

Flux 商店可以与 MVC 中的模型相比较,尽管本质上它们并不相同。从类似的角度来看,它们与所有业务逻辑和计算都在 Flux 商店中发生是一样的。根据 Facebook 团队的说法,“商店管理许多对象的状态——它们不表示 ORM 模型那样的单个数据记录。它们也不与 Backbone 的集合相同。商店不仅管理 ORM 风格的集合,还管理应用程序中特定领域的应用状态。”

来源 en.wikipedia.org/wiki/Object-relational_mapping.

在计算机科学中,对象关系映射(ORM)是一种编程技术,用于在面向对象编程语言中将数据在不可兼容的类型系统之间进行转换。实际上,它创建了一个“虚拟对象数据库”,可以在编程语言中使用。在面向对象编程中,数据管理任务作用于面向对象(OO)对象,这些对象几乎总是非标量值。例如,考虑一个代表一个人以及零个或多个电话号码和零个或多个地址的地址簿条目。这可以通过面向对象的实现来建模,通过“Person 对象”具有属性/字段来保存条目包含的每个数据项:人的名字、电话号码列表和地址列表。电话号码列表本身将包含“PhoneNumber 对象”,依此类推。编程语言将地址簿条目视为单个对象(例如,可以通过包含指向对象的指针的单个变量来引用它)。可以与对象关联各种方法,例如返回首选电话号码、家庭地址等方法。

商店从调度器接收动作。根据注册的回调(与调度器相关),商店决定是否应该响应调度器分发的动作。应用程序外部的任何对象都不负责更改商店或视图内的值。因此,任何由动作带来的更改,都是基于注册的回调导致的数据更改,而不是任何设置方法。

由于 Flux 存储可以在没有任何外部干预的情况下自行更新,因此它减少了在 MVC 应用中通常发现的复杂性。Flux 存储控制其内部发生的事情,只有输入是通过调度器进行的。在 MVC 应用中,各种模型与各种视图之间的相互依赖可能导致不稳定和复杂的测试用例。

一个应用可以根据其功能拥有多个存储,但每个存储只处理一个域。存储既表现出模型集合的特征,也表现出逻辑域的单例模型特征。

以下是对存储功能的快速回顾:

  • 存储注册自身到调度器中,通过回调函数。

  • 业务逻辑的计算位于存储中,作为 JS 函数。

  • 在调度器将操作从调度器发送到存储后,它们通过已注册的回调函数被识别。

  • 通过状态更新在存储中执行操作。

  • JS 数组:_library_readingItems存储可用的书籍和读者想要阅读的内容。

  • EventEmitter是事件模块的一个类,它是 Node.js 核心库的一部分。在这个例子中,事件发射器功能是通过eventEmitter.on()方法完成的,其中第一个参数是事件,第二个参数是要添加的函数。因此,eventEmitter.on()方法只是注册函数。当调用emit()方法时,它将执行通过 on 方法注册的所有函数。

  • 公共方法getReadingList()getLibrary()允许我们从_readingItems_readingListJS 数组中获取计算后的数据。

  • app-stores.js代码中的dispatcherIndex用于存储调度器注册方法的返回值。

  • 在调度器广播的情况下,switch 语句是决定要执行哪些操作的确定因素。如果采取了相关操作,则发出一个变更事件,并更新监听此事件的视图的状态。

以下是我们library_appapp_stores.js代码的代码,它包含了我们应用的所有业务逻辑和计算:

var AppDispatcher = require('../dispatchers/app-dispatchers');
var AppConstants = require('../constants/app-constants');
var assign = require('react/lib/Object.assign');

//eventEmitter allows the Stores to listen/broadcast changes to the 
//Controller-Views/React-Components
var EventEmitter = require('events').EventEmitter;

var CHANGE_EVENT = 'change';

var _library = [];

for(var i=1; i<6; i++){
  _library.push({
    'id': 'Book_' + i,
    'title':'Sherlock Holmes Story ' + i,
    'description': 'Sherlock Series by Sir Arthur Conan Doyle'
  });
}

var _readingItems = [];

function _removeItem(index){
  _readingItems[index].inReadingList = false;
  _readingItems.splice(index, 1);
}

function _increaseItem(index){
  _readingItems[index].qty++;
}

function _decreaseItem(index){
  if(_readingItems[index].qty>1){
    _readingItems[index].qty--;
  }
  else {
    _removeItem(index);
  }
}

function _addItem(item){
  if(!item.inReadingList){
    item['qty'] = 1;
    item['inReadingList'] = true;
    _readingItems.push(item);
  }
  else {
    _readingItems.forEach(function(cartItem, i){
      if(cartItem.id===item.id){
        _increaseItem(i);
      }
    });
  }
}
var AppStore = assign(EventEmitter.prototype, {
  emitChange: function(){
    this.emit(CHANGE_EVENT)
  },
  addChangeListener: function(callback){
    this.on(CHANGE_EVENT, callback)
  },
  removeChangeListener: function(callback){
    this.removeListener(CHANGE_EVENT, callback)
  },
  getReadingList: function(){
    return _readingItems
  },
  getLibrary: function(){
    return _library
  }

注意

dispatcherIndex用于存储调度器注册方法的返回值。在waitFor()方法的情况下使用dispatcherIndex,即当应用的一部分需要等待应用的其他部分更新时。

以下代码展示了dispatcherIndex

dispatcherIndex: AppDispatcher.register(function(payload){
    var action = payload.action; 
    switch(action.actionType){
      case AppConstants.ADD_BOOK:
        _addItem(payload.action.item);
        break;

      case AppConstants.DELETE_BOOK:
        _removeItem(payload.action.index);
        break;

      case AppConstants.INC_BOOK_COUNT:
        _increaseItem(payload.action.index);
        break;

      case AppConstants.DEC_BOOK:
        _decreaseItem(payload.action.index);
        break;
    }

    AppStore.emitChange();

    return true;
  })
})
module.exports = AppStore;

控制器-视图和视图

视图主要是 React 视图,它们本质上生成操作。控制器-视图监听我们的存储,以获取任何已广播的changeEventemitChange事件让我们的控制器-视图知道是否需要在视图的状态中执行任何更改。它们本质上都是 React 组件。在我们的代码中,我们有五个这样的 React 组件,如下所示:

  • app-addbooktoreadinglist.js

  • app-booklist.js

  • app.js

  • app-readinglist.js

  • app-removefromreadinglist.js

以下为app-booklist.js的代码:

var React = require('react');
var AppStore = require('../stores/app-stores');
var AddBookToReadingList = require('./app-addbooktoreadinglist')

function getLibrary(){
  return {items: AppStore.getLibrary()}
}

var BookList = React.createClass({
  getInitialState:function(){
    return getLibrary()
  },
  render:function(){
    var items = this.state.items.map(function(item){
      return (
        <tr key={item.id}>
          <td>{item.title}</td>
          <td><AddBookToReadingList item={item} /></td>
        </tr>
      );
    })
    return (
      <table className="table table-hover">
        {items}
      </table>
    )
  }
});

module.exports = BookList;

以下是在AddBookToReadingList React 组件内部调用的代码:

var React = require('react');
var AppActions = require('../actions/app-actions');

//create a AddBookToLibrary component
var AddBookToReadingList = React.createClass({
        handleClick: function(){
          AppActions.addBook(this.props.item)
        },
        render:function(){
          return <button onClick={this.handleClick}>I want to borrow </button>
        }

});
module.exports = AddBookToReadingList;

最后,在app.js中添加了以下组件<Booklist \>。这主要是为了用户可以在ReadingList列表部分看到他们拥有的书籍:

var React = require('react');
var AppStore = require('../stores/app-stores.js');
var RemoveFromReadingList = require('./app-removefromreadinglist');

function readingItems(){
  return {items: AppStore.getReadingList()}
}

var ReadingList = React.createClass({
  getInitialState:function(){
    return readingItems()
  },
  componentWillMount:function(){
    AppStore.addChangeListener(this._onChange)
  },
  _onChange: function(){
    this.setState(readingItems())
  },
  render:function(){
    var total = 0;
    var items = this.state.items.map(function(item, i){
      return (
          <tr key={i}>
            <td><RemoveFromReadingList index={i} /></td>
            <td>{item.title}</td>
            <td>{item.qty}</td>
          </tr>
      );
    })
    return (
      <table className="table table-hover">
          <thead>
              <tr>
                <th></th>
                <th>Book Name</th>
                <th>Qty</th>
                <th></th>
              </tr>
            </thead>
            <tbody>
          </table>
    )
  }
});
module.exports = ReadingList

重新审视代码

在每个 React 组件(readingListbookList)中,getInitialState()分别使用存储的公共方法getReadingList()getLibrary()初始化。

在组件的生命周期中,各种方法在精确的点被执行。

  • componentWillMount()是 React 的生命周期方法。它在客户端和服务器上都会被调用一次,在初始渲染发生之前立即执行。如果你在这个方法中调用setStaterender()将看到更新的状态,并且即使状态发生变化,也只会执行一次:

    componentWillMount:function(){
        AppStore.addChangeListener(this._onChange)
      },
      _onChange: function(){
        this.setState(readingItems())
      }
    
  • 因此,componentWillMount()正在监听addChangeListener(在AppStore存储中定义)。如果传递了_onChange参数,则当前对象(_this)会更新(setState)为新/更新的数据/有效载荷(readingItems)。

  • 为了从阅读列表中删除项目,事件监听器(handleClick)被卸载。

以下为app-removebookfromreadinglist.js`的代码:

var React = require('react');
var AppActions = require('../actions/app-actions');

//create a DeleteBookFromLibrary component
var DeleteBookFromReadingList = React.createClass({
        handleClicr: function(){
          AppActions.deleteBook(this.props.index)
        },
        render:function(){
          return <button onClick={this.handleClicr}>Book Completed</button>
        }
});
module.exports = DeleteBookFromReadingList;

以下为app.js的代码:

var React = require('react');
var BookList = require('./app-booklist');
var ReadingList = require('./app-readinglist');
//create a App component
var App = React.createClass({
        render:function(){
          return <div><h1>Book List</h1><BookList /><h1>Reading List</h1><ReadingList /></div>
        }
});
module.exports = App

我们library_app Flux 应用程序的最终视图

点击按钮我想借阅时,相应的书籍将出现在我的阅读列表中。一旦我完成这本书,点击按钮书籍完成,从阅读列表中删除这本书。

以下是我们library_app应用程序的截图。

重新审视代码

如何运行这个 Flux 应用将在构建和部署结构中稍后介绍。

以下是基于 Flux 的应用程序组件的详细信息:

  • 动作

  • 派发器(回调注册的注册表)

  • 存储(与派发器注册的回调)

  • 视图

  • 控制器 视图重新审视代码

    Flux 应用中的数据流

摘要

通过我们的libary_app应用程序,我们探讨了简单 Flux 应用中单向数据流的工作方式。用户可以在视图中看到书单。他们可以在阅读列表中添加书籍,因此动作(添加书籍)被传递到派发器。内部,派发器有与存储注册的回调。然后存储根据用户的动作添加/删除书籍,并计算业务逻辑,相应地重新渲染视图。

在下一章中,我们将介绍 React 的良好实践和模式。这包括开发可重用组件的实践,如何以更好的数据流结构化组件层次结构,以及如何验证组件的行为。在我们的应用中,我们将改进到目前为止开发的组件。

第七章:使你的组件可重用

到目前为止,我们已经深入探讨了 React 组件的生命周期、属性、状态以及与 React 0.1.13 和未来版本相关的 ECMAScript。在本章中,我们还将看到如何在 React 应用程序中编写可重用的组件/代码。React 中的这些可重用组件被称为混合。此外,我们将探讨如何验证 React 组件的属性。

本章将涵盖以下主题:

  • 理解混合

  • ECMA6 中的高阶组件(因为混合在 ECMA6 中不受支持)

  • React 应用程序中的不同类型的验证

  • React 组件和应用程序架构的结构

理解混合

混合(可重用组件)通常是那些在多个地方使用的 React 组件,因此可以重用。通常,设计元素,如按钮、布局组件、表单字段或任何使用超过一次的代码逻辑/计算,都被提取到名为混合的代码中。因此,混合通过作为助手来帮助我们向现有的 React 组件中添加一些额外的功能。

注意

与前几章一样,index.html 的内容保持不变。只有相应的 js(包含 React 组件)的内容发生变化。

通过示例探索混合

在此示例中,我们为 window 全局对象设置了每 100 毫秒的间隔:

index.html 的内容:

<!DOCTYPE html>
            <html>
<head>
<script src="img/react.min.js"></script>
<script src="img/JSXTransformer.js"></script>
  <script src="img/react-dom.js"></script>

<meta charset="utf-8">
  <title>JS Bin</title>
</head>
<body>
  <div id="myReactContainer">
        <script type="text/jsx", src="img/index.js"></script>
  </div>
</body>
</html>

index.js 的内容:

//Defining the Mixin
 . var ReactMixin = {
 . getInitialState:function(){
 .    return {count:0};
 .  },

// componentWillMount, a  lifecycle method, is added as a part of the Mixin.
 . componentWillMount:function(){
    console.log('Component will mount!');
  },
  increaseCountBy10: function(){
    this.setState({count: this.state.count+10})
 }
  }

//This method displays text to display
     var App = React.createClass({
  render:function(){
   return (
     <div>
     <Label txt="SetInterval increase by 10 in every 100ms" />
        </div>
     )
  }
  });

// React component (<Label />), called from the <App /> component.
  var Label = React.createClass({

// Mixins are called using the keyword Mixin, followed by the Mixin name within an array.
  mixins:[ReactMixin],
  componentWillMount:function(){

    //setting the interval to 100ms
       interval = setInterval(this.increaseCountBy10,100);
  },

//The function is called for the second time to update the interval every 100ms
  componentWillUnMount:function(){
  clearInterval(this.interval);
  },
  render:function(){
   return <label>{this.props.txt} : {this.state.count}</label>
  }
 });

ReactDOM.render(<App />, document.getElementById('myReactContainer'));

注意

从应用程序的根目录运行 httpserver:

doel@doel-Vostro-3500:~/reactjs/ch7_mixins_validationProps/app1_mixin$ httpster
Starting HTTPster v1.0.1 on port 3333 from /home/doel/reactjs/ch7_mixins_validationProps/app1_mixin

在打开localhost:3333时,以下是对此代码的输出:

通过示例探索混合

使用生命周期方法的混合的应用程序截图

执行代码的解释:

混合(Mixin)不过是一个可以被 React 组件后来重用的 JavaScript 对象。我们首先定义混合(Mixin)。

componentWillMount是一个生命周期方法,它作为混合的一部分被添加。稍后,当混合从 React 组件中调用时,可以在网页的开发者工具部分底部看到console.log的日志,以展示Component Will Mount

我们添加了一个典型的 React 组件(<App />),它调用了<Label />组件。它是一个渲染函数,用于显示标签上呈现的文本。App 组件可以有多个 React 组件,这些组件将内部调用不同的 React 组件。

在下一个示例中,我们将看到这样的例子。

React 组件(<Label />)是从<App />组件中调用的。它使用了 React 混合(ReactMixin)。

行内混合:[ReactMixin],React 中的混合(Mixins)是通过关键字 Mixin 后跟混合名称(在本例中为 ReactMixin),在数组内调用。我们可以定义多个混合,作为 JavaScript 对象。所有这些独立的混合都可以从单个 React 组件(每个混合代表数组中的一个单独元素)中调用。

我们将在本章后面探索这样一个例子,其中包含多个混合。

然后我们添加setInterval()函数

  • setInterval()方法是 JavaScript 中的 window 函数。

  • 它被声明为window.setInterval(function, milliseconds)

  • 尽管这是一个基于 window 对象的方法,但不需要在 window 对象上调用setInterval()方法,例如在之前提到的代码中。它可以不带 window 前缀调用。

  • 第一个参数是执行的功能(this.increaseCountBy10)。

  • 第二个参数是每个函数执行之间的时间间隔,this.increaseCountBy10。在这种情况下,间隔设置为100ms

在之前提到的代码中,生命周期方法(componentWillMount)被第二次调用。第一次调用是在 Mixins 体内,它记录了日志中的Component Will Mount

第二次调用是在 React 组件(<Label />)内部。由于第二次调用,setInterval()方法将值从0(最初将计数设置为0)增加到10,每次增加间隔为100毫秒。

注意

查看 Facebook 文档facebook.github.io/react/docs/reusable-components.html

"Mixins 的一个优点是,如果一个组件正在使用多个 Mixins,并且几个 Mixins 定义了相同的生命周期方法(即几个 Mixins 想在组件销毁时进行一些清理),所有生命周期方法都将得到保证被调用。Mixins 上定义的方法按照 Mixins 列出的顺序运行,然后是组件上的方法调用。"

在下一节中,我们将看到 Mixins 的另一个示例:

Calling Multiple Mixins from a single React Component

现在,我们将看到另一个示例,其中多个 Mixins 将从单个 React 组件中调用。以下代码被声明:

首先,我们将声明两个 react Mixins:

var ReactMixin1= {

     getDefaultProps: function () {

        return {text1: "I am from first Mixin"};

    }

};

var ReactMixin2 = {

      getDefaultProps: function () {

        return {text2: "I am from second Mixin"};

    }

};

在代码的第二部分,我们将从 react 组件<App />中调用两个 React Mixins:

var App = React.createClass({

  Mixins:[ReactMixin, ReactMixin2],

  render: function () {

    return (

    <div>

        <p>Mixin1: {this.props.text1} </p>

        <p>Mixin2: {this.props.text2}</p>

    </div>

    );

  }

});

ReactDOM.render(<App />, document.getElementById('myReactContainer'));
\\

直接从应用程序根目录执行 httpster 命令,就像之前一样,以查看两个 Mixins 的输出:

通过示例探索 Mixins

使用多个 Mixins 的应用程序截图

注意以下内容:

  • 在 Mixins 中相同的属性名,例如,text,在这种情况下,将引发错误

  • 不同 Mixins 中的相同方法名将引发错误

  • 相同的生命周期方法可以在 Mixins 和 React 组件内部调用。这些生命周期方法的执行顺序是 Mixins,然后是 React 组件。

  • 如果在多个 Mixins 中调用相同生命周期方法,则执行顺序是 Mixins 在数组中调用的顺序(从低到高索引)。

Mixins 中的高阶组件

在 ReactJS 中使用 ES6,Mixins 不再被支持。取而代之的是,他们引入了高阶组件。

这些高阶组件在 Relay 框架中被广泛使用,Relay 是由 Facebook 发布的一个完整的基于 React 的框架。高阶组件封装了子 UI 组件。因此,当这些组件被调用时,它们会首先执行其查询,从而渲染子 UI 组件。当查询传递时,数据会从子组件以 props 的形式传递给高阶组件。

验证

验证 是任何处理用户输入的应用程序的一个基本组成部分。在 ReactJS 中,库提供了一些验证,使得开发者能够验证接收到的数据。

在 React 应用程序中,数据大多以属性(props)的形式接收。各种验证器从 React.PropTypes 中导出。如果发生验证错误,它将出现在 JavaScript 控制台中。由于性能原因,只有开发模式下才会因为验证检查而发生此类错误。

查看 Facebook ReactJS 开发团队文档 facebook.github.io/react/docs/reusable-components.html#prop-validation。以下是一些验证器的示例:

React.createClass({
  propTypes: {
    // You can declare that a prop is a specific JS primitive. By default, these
    // are all optional.
    optionalArray: React.PropTypes.array,
    optionalBool: React.PropTypes.bool,
    optionalFunc: React.PropTypes.func,
    optionalNumber: React.PropTypes.number,
    optionalObject: React.PropTypes.object,
    optionalString: React.PropTypes.string,

    // Anything that can be rendered: numbers, strings, elements or an array
    // (or fragment) containing these types.
    optionalNode: React.PropTypes.node,

    // A React element.
    optionalElement: React.PropTypes.element,

    // You can also declare that a prop is an instance of a class. This uses
    // JS's instanceof operator.
    optionalMessage: React.PropTypes.instanceOf(Message),

    // You can ensure that your prop is limited to specific values by treating
    // it as an enum.
    optionalEnum: React.PropTypes.oneOf(['News', 'Photos']),

    // An object that could be one of many types
    optionalUnion: React.PropTypes.oneOfType([
      React.PropTypes.string,
      React.PropTypes.number,
      React.PropTypes.instanceOf(Message)
    ]),

    // An array of a certain type
    optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number),

    // An object with property values of a certain type
    optionalObjectOf: React.PropTypes.objectOf(React.PropTypes.number),

    // An object taking on a particular shape
    optionalObjectWithShape: React.PropTypes.shape({
      color: React.PropTypes.string,
      fontSize: React.PropTypes.number
    }),

    // You can chain any of the above with `isRequired` to make sure a warning
    // is shown if the prop isn't provided.
    requiredFunc: React.PropTypes.func.isRequired,

    // A value of any data type
    requiredAny: React.PropTypes.any.isRequired,

    // You can also specify a custom validator. It should return an Error
    // object if the validation fails. Don't `console.warn` or throw, as this
    // won't work inside `oneOfType`.
    customProp: function(props, propName, componentName) {
      if (!/matchme/.test(props[propName])) {
        return new Error('Validation failed!');
      }
    }
  },
  /* ... */
});

使用 isRequired 验证器的示例

index.html 页面。使用不同的 JS 页面来检查使用的不同版本的验证:

<!DOCTYPE html>
<html>
<head>
<script src="img/react.js"></script>
<script src="img/JSXTransformer.js"></script>
  <script src="img/react-dom.js"></script>
  <script type="text/jsx", src="img/index4.js"></script>

<meta charset="utf-8">
  <title>JS Bin</title>
</head>
<body>
  <div id="myReactContainer">
        <script type="text/jsx", src="img/index.js"></script>
  </div>
</body>
</html>

如验证器名称所示,isRequired 验证器确保 React 组件的属性始终存在。否则,它将在 JS 控制台中抛出错误。React.PropTypes.{foo} 属性是 JavaScript 函数,它们内部检查一个 prop 是否有效。当 prop 有效时,它将返回 null,但当 prop 无效时,它将返回一个错误。在 第四章 中,我们深入探讨了 ES6。在下一个示例中,我们将使用 ES6 语法:

"use strict"

class App extends React.Component {

  render () {

    return (

      <div className="app">

        <h1 ref="title" className="app__title"></h1>

        <div ref="content" className="widget__content">{this.props.content}</div>

      </div>

    )

  }

}

App.propTypes = {

  title: React.PropTypes.string.isRequired,

  content: React.PropTypes.node.isRequired

}

ReactDOM.render(<App content="I am learning react"/>,document.getElementById('myReactContainer'));

注意

在你的应用根目录下运行 httpster,以便在浏览器中的 localhost:3333 看到输出。

输出将如下所示:

使用 isRequired 验证器的示例

应用程序截图——React 组件 prop 中的 isRequired 验证

从 ES6 视角来看,关于之前提到的代码的一些要点:

use strict 已经被用来选择性地启用 JavaScript 的一个受限版本。我们使用它是因为我们用 let 代替了 var。use strict 允许将组件置于一个 strict 运行环境中,并阻止某些操作被执行,同时抛出更多的异常。

let 声明变量,其作用域限制在使用的块、语句或表达式中。

详细信息请查看 developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/

使用自定义验证器的示例

以下是在使用自定义验证时通常使用的模板:

error = propTypespropName; 

让我们通过一个自己的例子来演示,使用自定义错误消息,并使用这些验证之一,看看它在 JavaScript 控制台中的验证情况:

var ValidationApp = React.createClass({

  propTypes: {

    name: function(props, propName,componentName){

      if(!(propName in props))  {

        throw new Error("Property Name Missing ")

      }
  },

  render:function(){
    return <h1>{this.props.name}</h1>
  }
});
 ReactDOM.render(<ValidationApp />, document.getElementById('myReactContainer')); //missing prop name

注意

从你的应用程序根目录运行 httpster,以便在你的浏览器中的 localhost:3333 看到输出

以下代码的输出显示在本截图:

使用自定义验证器的示例

应用程序截图——React 组件属性中的自定义验证

我们可以在属性(名称)中添加更多的验证,如下所示:

var ValidationApp = React.createClass({

  propTypes: {

    name: function(props, propName,componentName){

      if(!(propName in props))  {

        throw new Error("Property Name Missing ")

      }

 if(props[propName].length < 7) {

 throw new Error("Can you add a longer Property Name, more than 7chars")

 }

 }

  },

  render:function(){

    return <h1>{this.props.name}</h1>

  }

});

// ReactDOM.render(<ValidationApp />, document.getElementById('myReactContainer')); //missing prop name

ReactDOM.render(<ValidationApp name="react" />, document.getElementById('myReactContainer')); //prop length should be more than 7 chars

注意

从你的应用程序根目录运行 httpster,以便在你的浏览器中的 localhost:3333 看到输出

以下代码的输出如下所示:

使用自定义验证器的示例

应用程序截图——React 组件属性中的验证

因此,如果我们传递的名称属性超过七个字符,那么在预期的 JS 控制台中将不会有错误。

组件的结构

现在我们已经对 ReactJS 进行了相当多的探索,你心中可能会有关于如何架构一个 React 组件或更广泛地说是一个 React 应用程序的问题。还没有设定任何基本规则,这在基于 ReactJS 编写应用程序时是理想的。然而,如果我们深入研究 Facebook 文档团队提供的教程,超链接 facebook.github.io/react/docs/tutorial.html,我们将能够理解他们在编写这样的应用程序时所使用的底层方式。

让我们先探索一下组件通常是如何结构的:

  1. 组件 declaredData 是在需要时从服务器获取的 [如果需要]。

  2. 组件的 propTypes 已声明 [用于验证]。

  3. 组件生命周期方法 [componentWillMountcomponentDidMountcomponentDidUpdatecomponentWillUnmount 等等] 已定义。

  4. 在每个生命周期方法中,这些方法的功能要么是声明的,要么是从另一个为特定任务明确定义的 JS 函数内部调用的。需要记住的是,之前提到的生命周期方法不是在应用程序中同时或任何情况下都必须使用。

  5. 必须存在于任何 React 组件中的渲染方法。因此,任何基于 React 的应用程序的结构方式因应用而异。尽管没有最佳方式,但像任何其他应用程序一样,建议将代码分块以遵循关注点分离。我们应该将 React 视图、组件和数据分开。一个组件目录可以根据需要调用其他子组件,从而提高代码的可读性和可测试性。

由于 React 是一个开源的 JavaScript 库,因此有各种开源网站和开发者每天都在为此库工作,以增强和调整库,以满足需求。

对于一个使用 ReactJS 库的应用程序,通常根据其功能将视图(React 视图)分开(例如,主页、管理页面和产品目录)。在每个视图的子文件夹中,你可以添加 test.js 文件,或者你可以将所有与测试相关的文件放在同一个 tests 文件夹下。如果你需要一些应该跨其他组件共享的 react 视图,你可以将这些相关文件放在 shared/lib 文件夹下。

摘要

在本章中,我们探讨了如何在 ReactJS 中开发可重用组件(在 ES6 实现之前,使用 Mixins)。我们还了解了高阶组件,这些组件在 ReactJS 的后续版本(从 0.13 版开始)中被使用,支持 ES6 但不支持 Mixins。验证是任何应用程序的一个基本组成部分,尤其是那些使用用户输入(即表单输入)的应用程序。我们探讨了 ReactJS 如何处理验证,以及我们如何使用自定义验证。我们还概述了 react 组件的结构。在下一章中,我们将处理 React 应用程序的测试。

第八章. 测试 React 组件

到目前为止,我们已经探讨了 React 的组件生命周期、属性、状态、验证以及与 React 0.1.13 和未来版本相关的 ECMAScript。在本章中,我们将探讨 JavaScript 和 ReactJS 相关内容的测试。首先,我们将通过不同的 JavaScript 测试框架来整体了解测试,以及如何运行测试,然后测试使用 ReactJS 库构建的视图。

本章将涵盖以下内容:

  • 使用 Chai 和 Mocha 在 JavaScript 中进行测试

  • 使用 ReactTestUtils 测试 React 组件

  • 探索 Jest

  • 使用 Expect、Mocha 和浅渲染测试基于 React 的应用

在测试 JavaScript 时,你可以混合匹配各种方法。让我们简要概述一下各种事物,如框架、断言库和测试工具。这里给出的列表并不全面,详细涵盖所有这些内容超出了本书的范围。

MochaJasmine是测试框架。它们可以与各种测试断言库一起使用,如下所示:

  • should.js是一个断言库。它是框架无关的,并且从 IE9 及以上版本工作。有关库的详细信息,请参阅www.npmjs.com/package/should

  • chaijs也是一个断言库,其中我们添加插件。它也适用于测试框架。有关库的详细信息,请在线查阅chaijs.com/karma。它是一个 JavaScript 测试工具,可以测试浏览器中的 JavaScript 代码。它是框架无关的(可以用于运行 Mocha、Jasmine、Qunit 等)。详细信息请参阅www.npmjs.com/package/karma

应当记住,karma 既不是像 Jasmine 或 Mocha 这样的 JavaScript 框架,也不是像chaijsshould.js这样的断言库。因此,我们应该根据需要使用断言库和框架,以及 karma 一起启动 HTTP 服务器,以便我们可以在浏览器中测试 JS 代码。

Jest也是 Jasmine 框架上的一个框架。Facebook 开发者团队建议使用 Jest 来测试基于 React 的应用。根据 Jest 网站(facebook.github.io/jest/),以下是使用 Jest 而不是 vanilla jasmine 进行测试的一些优点:

  • Jest 在 Jasmine 之上提供了多个层级

  • 它会自动搜索并找到要执行的测试

  • 在你运行测试时,它会为你模拟依赖

  • 它并行运行测试,因此可以更快地完成执行

  • 它允许你同步测试异步代码

  • 它允许你通过 jsdom 的模拟 DOM 实现,在命令行上运行测试

使用 Chai 和 Mocha 在 JavaScript 中进行测试

如前所述,为了编写 React 代码的测试用例,我们将安装一些测试库来运行测试和编写断言。让我们了解 Chai 断言库和 Mocha 测试框架的设置。我们需要使用 npm 安装这些库。

在终端输入:

npm i -D mocha chai

注意

install 简称:i

devDependencies 简称:D(该包仅在开发环境中安装)

在通过之前提到的命令安装了 Chai 和 Mocha 库之后,它们可以在 node_modules 目录下找到。

我们需要在 package.json 文件中添加 Mocha 和 Chai 的条目。

package.json 代码

{
  "name": "JSApp",
  "version": "1.0.0",
  "description": "Get random numbers",
  "main": "index.js",
 "scripts": {
 "test": "mocha test.js"
 },
 "devDependencies": {
 "chai": "3.2.0",
 "mocha": "2.2.5"
 }
}

根据 docs.nodejitsu.com/articles/getting-started/npm/what-is-the-file-package-json

所有 npm 包都包含一个名为 package.json 的文件。此文件通常位于项目根目录。此文件包含与项目相关的所有元数据。package.json 文件用于向 npm 提供信息,从而使其能够识别项目以及有效地处理项目的依赖项。

  • name:这表示应用程序的名称。

  • version:这是应用程序的版本。

  • description:这是应用程序的一般描述。

  • main:这是主 JavaScript 文件,它可能内部调用其他 JS 文件。在这个例子中,是 index.js 文件。

  • scripts:这是在调用 npm start 时要执行的脚本。它应该执行测试(mocha test.js 文件)。

  • devDependencies:这些是在与 package.json 相同的目录中安装的包,除非传递了 –production 标志。除非传递了 –dev 选项,否则这些包不会安装在任何其他目录。

添加一个 test.js 文件。为了检查设置是否正常工作,我们添加了一个简单的单个测试断言。

Test.js file code
var expect = require('chai').expect
, name = 'my Name';

var random = require('./index');

describe('random', function() {
  it('should work!', function() {
    expect(false).to.be.false;
  });

  it ('return my Name', function() {
        expect(name).to.be.a('string');
        expect(name).to.equal('my Name');
        expect(name).to.have.length(7);
        })
});

注意

assertions 是从 Chai 调用的。

describe 是从 Mocha 框架中调用来描述测试的。

现在我们运行测试,从应用程序的根目录在终端中,如下所示:

npm test

使用 Chai 和 Mocha 在 JavaScript 中进行测试

使用 Mocha 和 Chai 设置的控制台截图

使用 ReactTestUtils 进行测试

ReactTestUtils 用于测试基于 React 的组件。它可以模拟 ReactJS 支持的所有基于 JavaScript 的事件。文档可以在 Facebook 开发者网站上找到(facebook.github.io/react/docs/test-utils.html)。

以下是对模拟函数的代码:

Simulate.{eventName}(
  DOMElement element,
  [object eventData]
)

安装 React 和 JSX

如前所述,在安装 Chai 和 mocha 时,我们在此安装 React 和 JSX 特定的测试工具(ReactTestUtils),以便简化我们的任务。让我们借助一些基于 React 的组件并刺激它们来测试行为和功能。

以下是一个此类代码的示例。

我们需要在终端使用以下代码通过npm安装jest包:

sudo npm install jest-cli –save-dev

在需要安装 node 包的机器/服务器上,需要 sudo/root 访问权限。这特别需要,因为 node 安装的目录。我们可以使用以下命令检查已安装的目录:

npm config get prefix

根据这里的截图,它安装在/usr目录中,权限设置为 root。因此,我们需要使用sudo选项安装npm包。

安装 React 和 JSX

/usr目录文件所有者/权限的控制台截图。

另一种方法是设置/usr目录的权限为用户,该用户可以拥有和修改目录中的文件:

sudo chown -R $(whoami) $(npm config get prefix)/{lib/node_modules,bin,share}

让我们尝试采用测试驱动开发TDD)的方法,即我们将创建一个失败的测试用例,然后根据实际代码来通过测试。

创建一个 JS 文件,该文件将用hi问候任何名字:

// greeting.js

module.exports = greetings;

现在,让我们在名为__test__的目录中创建一个test文件:

// __tests__/greeting-test.js

 jest.dontMock('../greetings');

//executed when the test runs
 describe('greetings', function() {
 it('greets the name', function() {
 var greet = require('../greetings');
 expect(greet("react")).toBe("hi react");
 });
 });

让我们回顾一下之前提到的代码中的 jest 属性:

  • jest.dontMock在这里被明确提及,因为 jest 默认会模拟一切。因此,为了测试实际代码而不进行模拟,我们需要要求 jest 不要模拟需要测试的代码(greetings.js)。

  • describe('greetings', function())每个 describe 块是测试套件,当运行测试时(npm test/jest)将被执行。一个 describe 块可以有多个测试用例。

  • it('greets the name', function(), 在 describe 块内阻塞实际的测试规格/用例。

为了在_test__目录中执行测试,我们需要有一个包含以下条目的package.json文件:

注意

在下一章中,我们将更详细地介绍打包。

下面是package.json文件的代码:

{
  "dependencies": {
    "react": "~0.14.0",
    "react-dom": "~0.14.0"
  },
  "devDependencies": {
    "jest-cli": "⁰.8.2",
    "react-addons-test-utils": "~0.14.0"
  },
  "scripts": {
    "test": "jest"
  },
  "jest": {
    "unmockedModulePathPatterns": [
      "<rootDir>/node_modules/react",
      "<rootDir>/node_modules/react-dom",
      "<rootDir>/node_modules/react-addons-test-utils",
      "<rootDir>/node_modules/fbjs"
    ]
  }
}

让我们对package.json中的这段代码进行快速回顾。

一切准备就绪后,我们可以在终端使用以下命令运行测试:

npm test

输出如下所示:

安装 React 和 JSX

使用 jest 的 TDD 控制台截图,显示失败测试。

现在,让我们添加代码,以便用名字问候名字,并且测试通过:

// greeting.js

function greetings(name) {
  return "hi "+name;
}
module.exports = greetings;

现在,当我们执行测试时,我们将看到一个通过测试用例:

安装 React 和 JSX

使用 npm test 的 TDD 控制台截图,显示通过测试。

执行测试的另一种方法是安装jest并通过终端调用 jest 来执行它们:

sudo npm install -g jest-cli

输出如下所示:

安装 React 和 JSX

使用 jest 的 TDD 控制台截图,显示通过测试。

因此,我们可以看到使用npm test/jest中的任何一个命令,我们都会得到相同的输出。

下面是使用 Mocha、expect、ReactTestUtils 和 Babel 的 jest 测试套件的典型示例。

让我们看看package.json的典型示例,它使用以下内容:

  • Mocha 作为测试框架

  • Expect 作为一个断言库

  • ReactTestUtils 用于测试基于 React 的 JavaScript 组件

  • Babel 作为转编译器,将 ES6 代码转换为当前兼容(ES5)的 JavaScript 代码。

package.json文件的示例:

"scripts": {
 "test": "mocha './src/**/*.test.js' --compilers js:babel-core/register",
  },
  "devDependencies": {
 "babel-core": "6.1.4",
 "babel-loader": "6.1.0",
 "babel-preset-es2015": "6.1.4",
 "babel-preset-react": "6.1.4",
 "babel-preset-stage-2": "6.1.2",
    "mocha": "2.3.3",
    "react-addons-test-utils": "0.14.3",
  }
}

如前例所示,在脚本对象中,我们保留测试文件,并且所有测试文件都遵循以.test.js扩展名结尾的约定。测试文件可以使用任何扩展名。对于从 ES6 代码编译到浏览器兼容的 JS 代码,脚本中添加了–compiler标签。

安装以下所有包,如package.json中所述:

npm install babel-loader babel-preset-es2015 babel-preset-react babel-preset-stage-2 react-addons-test-utilsBabel being the transpiler, we need to add the following entry to enable the import (reserver keyword) in the following .babelrc file:

{
    "presets": ["es2015"]
}

这里是转译器的定义。来源 en.wikipedia.org/wiki/Source-to-source_compiler

"源到源编译器、转编译器或转译器是一种编译器,它接受用一种编程语言编写的程序源代码作为输入,并生成另一种编程语言的等效源代码。"

.babelrc文件包含所有 Babel API 选项。以下是与测试套件设置相关的应用程序文件结构截图。详细信息可以在 Babel 文档中找到,网址为babeljs.io/docs/usage/babelrc/

Mocha、expect、ReactTestUtils 和 Babel 的 jest 典型测试套件示例

展示典型 JS 应用程序目录结构的截图,包括 test、node_modules、package.json 和.babelrc

使用与之前相同的greetings.js文件,但在greetings.test.jsindex.test.js文件中使用新的 ES6 语法进行测试,让我们测试测试套件。

代码 test/greetings.test.js(使用 ES6 语法)

import expect from 'expect';
describe('greetings', () => {
   it('greets the name', () => {
     var greet = require('../greetings');
     expect(greet("react")).toBe("hi react");
   });
});

代码 test/index.test.js(使用 ES6 语法)

import expect from 'expect';
  describe('setup',() => {
    it('testing the setup is working', () => {
      expect(true).toEqual(true);
   });
      });

Mocha、expect、ReactTestUtils 和 Babel 的 jest 典型测试套件示例

展示使用 ES6 语法、mocha 和 babel 进行测试的截图

使用 ES6 语法和 mocha 测试框架、expect 断言库执行此测试文件,并在经过 Babel 转译后产生了与之前相同的结果。

使用浅渲染进行测试

浅渲染是在测试 React 组件时使用的一种方法,其中组件是“单层深”。这样的浅渲染测试组件具有关于返回内容的render方法的事实。这些组件没有附加子组件,并且不需要 DOM。

因此,在测试使用浅渲染方法时,应记住,任何具有 DOM 更改的父组件或任何已更改的子组件的更改可能需要重写测试。

让我们通过一些代码来探索这个问题。在以下示例中,我们将创建一个 React 组件(GreetingComponent),其中render方法将返回一个包含两个子元素(h2span元素)的div

greeting.js的代码:

// greeting.js

import React from 'react';

const { div, h2, span} = React.DOM;

export default React.createClass({

  displayName: 'GreetingComponent',

  render(){

    return(

    div({classname: 'Greeting'},

      h2({classname: "heading2"}, "Hi"),

      span({classname: "like"},"ReactJs")

    )

    );

  }

});

让我们使用浅渲染方法为这个 React 代码编写测试。

test/greeting.test.js 的代码

// Importing the necessary libraries and JavaScript code to be tested
import expect from 'expect';
import React from 'react';
import TestUtils from 'react-addons-test-utils';
import GreetingComponent from '../greetings.js';
describe('GreetingComponent', () => {
  it('should greet with the name', () => {

// Creating a shallow rendered object and stored within  renderer
    const renderer = TestUtils.createRenderer();

/*creating the react element (GreetingComponent, declared in the greeting.js code). This might be comparable to the "place" where the component to be tested is rendered. This component can respond to events and update itself
*/
    renderer.render(React.createElement(GreetingComponent));
/* method is called on the renderer (TestUtils.createRenderer()) and stored within output. We can inspect this output in the console */
    const output = renderer.getRenderOutput();
    console.log(output);
    expect(output.type).toBe('div');

输出值将在控制台中打印。基于此,我们可以看到相关 react 组件的不同层次和值。以下是从console.log输出的内容(输出)

使用浅渲染进行测试

展示在控制台中渲染输出()方法的截图。

让我们深入一层,检查以下值:const output = renderer.getRenderOutput().props.children

因此,我们可以看到GreetingComponent React div元素的确切两个子元素及其类型和值:

使用浅渲染进行测试

展示在控制台中渲染输出()方法的子元素的截图。

根据输出,我们可以测试div元素的子元素(h2span)如下:

Code of __test__/greeting.test.js
import React from 'react';
import TestUtils from 'react-addons-test-utils';
import GreetingComponent from '../greetings.js';

describe('GreetingComponent', () => {

  it('should greet with the greeting Hi', () => {

    const renderer = TestUtils.createRenderer();
    renderer.render(React.createElement(GreetingComponent));
    const output = renderer.getRenderOutput();
    console.log(output);
    expect(output.type).toBe('div');

 expect(output.props.children[0].type).toBe('h2');
 expect(output.props.children[0].props.classname).toBe('heading2');
 expect(output.props.children[0].props.children).toBe('Hi');

  });

  it('should return the like as ReactJs', () => {

    const renderer = TestUtils.createRenderer();

    renderer.render(React.createElement(GreetingComponent));

    const output = renderer.getRenderOutput();

    console.log(output);

 expect(output.type).toBe('div');

 expect(output.props.children[1].type).toBe('span');

 expect(output.props.children[1].props.classname).toBe('like');

 expect(output.props.children[1].props.children).toBe('ReactJs');

  });

});

我们可以看到两个it块之间有若干行共同的代码。因此,我们可以将这些共同代码分离出来,并如以下所示进行重构:

// __tests__/sum-test.js

//jest.dontMock('../greetings.js');

import expect from 'expect';
import React from 'react';
import TestUtils from 'react-addons-test-utils';
import GreetingComponent from '../greetings.js';

describe('GreetingComponent', () => {
  describe('Common code', () => {
    const renderer = TestUtils.createRenderer();
    renderer.render(React.createElement(GreetingComponent));
    const output = renderer.getRenderOutput();
//    console.log(renderer);
    console.log("From Common Code");
    console.log(output);

  it('should greet with the greeting Hi', () => {
//    console.log(renderer);
    console.log("h2 component");
    console.log(output);
    expect(output.props.children[0].type).toBe('h2');
  expect(output.props.children[0].props.classname).toBe('heading2');
    expect(output.props.children[0].props.children).toBe('Hi');
  });

  it('should return the like as ReactJs', () => {
//    console.log(renderer);
    console.log("span component");
    console.log(output);

    expect(output.props.children[1].type).toBe('span');

    expect(output.props.children[1].props.classname).toBe('like');

    expect(output.props.children[1].props.children).toBe('ReactJs');

  });

});

});

在执行代码时,我们可以使用以下命令在文件中获取输出:

npm test > test_output.txt

以下是在test_output.txt文件中的输出。您可以播放并检查 React 元素的各个不同属性。每个属性的解释超出了本书的范围。但我们可以看到,所有 React 组件不过是 JavaScript 对象。

使用浅渲染进行测试

摘要

我们看到了如何在基于 React 的应用程序中测试不同的组件以及 JavaScript 本身。为了测试 JavaScript 代码,我们使用了 chai 和 expect 作为断言库,jasmine 和 jest 作为测试框架。为了测试 React 应用程序,我们使用了 ReactTestUtils 和浅渲染。在下一章中,你将学习关于 React 应用程序的部署过程。我们将更深入地探讨package.json,这是我们本章中提到的。

第九章.为部署准备代码

在学习 ReactJS 基础和 flux 之后,我们几乎接近了这本书的结尾。在开发任何应用程序后,我们面临的最关键部分是将应用程序提供给外部世界,即部署应用程序。将代码保存在源代码控制仓库(如 GitHub 或 Bitbucket)中,并使用 Git 进行代码版本控制是一个好习惯。这些在团队合作和必要时检索任何代码时都有帮助。如何设置前面提到的内容的解释超出了本书的范围,但有许多资源可供参考。

在本章中,我们将探讨以下主题:

  • Webpack 简介

  • 使用 Webpack 和 Gulp 部署 React 应用程序的方法

  • 用于 browserify 的配置选项

  • 安装简单的 Web 服务器

Webpack 简介

Webpack是一个模块打包器,用于部署基于 JavaScript 的应用程序。它将输入作为具有依赖关系的模块,然后将其输出为静态资源。

从 Webpack 文档网站(webpack.github.io/docs/what-is-webpack.html#how-is-webpack-different),以下图像解释了相同的内容。

Webpack 简介

构建简单的 React 应用程序

如前几章所述,让我们构建一个简单的基于 React 的应用程序,我们将集成 Webpack 并在之后部署它。

从终端安装vis npm包,如下所示:

sudo npm install babel-loader babel-preset-es2015 babel-preset-react babel-preset-stage-2
npm -g install httpster

注意

httpster: 这是一个简单的 http 服务器,用于运行静态内容。在 Chrome 浏览器中,由于 X-origin 错误,index.html文件有时无法渲染。因此,从您的应用程序目录运行此 Web 服务器将更容易测试您的应用程序。只需运行命令httpster

默认情况下,服务器在端口3333上运行,因此浏览器中的localhost:3333应该渲染您的应用程序的index.html页面。

我们创建了以下文件:

  • src/bundle.js: 这是在 Webpack 将代码转换为普通 JS 并执行任何其他文件转换后,Webpack 将其输出到的地方。该文件的详细信息将在后面的部分讨论。

  • index.html: 应用程序着陆页。

  • index.js: 基于 React 的组件。

  • .babelrc: 在这里声明了 babel 的预设和环境。

  • node_modules: 存储已安装的npm包。

  • Webpack.config.js: 在这里存在 Webpack 相关的配置。

以下是一个控制台截图,显示了使用 Webpack 的应用程序目录结构:

构建简单的 React 应用程序

查看简单的 React 应用代码示例:

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <script src="img/react.js"></script>
<script src="img/react-dom.js"></script>
<script src="img/JSXTransformer.js"></script>

  <title>React App with Webpack</title>
</head>
<body>
  <div id="app"></div>
  <script type = "text/jsx" src="img/index.js"></script>
</body>
</html>

index.js:

"use strict";
class App extends React.Component {
  render() {
    return <div>Hello </div>;
  }

}
ReactDOM.render(<App />, document.getElementById('app'));

.babelrc:
{
  "presets": ["es2015",  "react"]
}

设置 Webpack

现在我们已经了解了 Webpack 是什么,让我们安装和配置它,以便我们可以在 React 应用程序中使用它,如后面所述。

在你的终端中,输入以下内容:

 sudo npm install -g webpack

注意

-g 选项将 Webpack 库全局安装到你的计算机上。

如你所见,在下面的截图中,有许多依赖包,这些包在安装 Webpack 时也会被安装:

设置 Webpack

显示 webpack 包安装及其所有依赖项的控制台截图

在安装 Webpack 之后,我们将创建一个 webpack-config.js 文件,其中包含以下条目:

// The declaration of the object having all the Webpack-related 
configuration details.
module.exports = { 

//entry point of the application
  entry: "./app/components/index.js",

/* In this bundle.js file,Webpack will have the output  after transpilation of the code from index.js (ES6 to ES5) & combining all the components' and it's children js files are present.
*/
  output: { 
    filename: "src/bundle.js" 
  }, 
  module: { 
// Loading the test loader, it is used to transform any JSX code in the tests into plain JavaScript code.

    loaders: [ 
      { 
// All the packages which are installed within the node_modules directories are to be excluded.
        test: /\.jsx?$/, 
        exclude: /(node_modules)/,
//specifying which one to use
        loader: 'babel', 
        query: { 
          presets: ['react', 'es2015'] 
        } 
      } 
    ] 
  } 
}

让我们解释前面的代码。

我们从应用程序的入口点开始。由于基于 React 的应用程序通常有很多组件,为所有这些组件有一个共同的入口点将更容易管理,并且对于结构良好的模块化应用来说很重要。

然后我们将输出指向一个名为 bundle.js 的文件,并将所有组件及其子组件合并。

在加载测试加载器后,我们说明在 node_modules 目录中要排除哪些包。

然后我们使用加载器,指定使用哪一个。预设加载器在将 ES6 代码转换为当前浏览器兼容代码时执行 Babel 所做的所有转换。

现在我们将在我们的终端中运行 Webpack 命令,

sudo webpack -w -v

  • 使用 sudo,因为我们需要 sudo/root 权限来执行 Webpack 命令,或者我们需要更改特定目录的所有权/权限。

    -w 选项确保监视任何发生变化的文件。它将监视源文件的变化,当发生变化时,捆绑包将被重新编译。(来源:webpack.github.io/docs/webpack-dev-server.html)。

    -v 选项给出详细输出。

  • webpack --help:此命令给出所有选项及其对应含义的输出,这些选项可以作为参数传递。

设置 Webpack

在终端上执行 Webpack 后的控制台截图

因此,所有代码的转换和转换都在 src/bundle.js 输出文件中。

从前面提到的应用中典型的 bundle.js 文件输出:

/******/ (function(modules) { // webpackBootstrap

/******/    // The module cache

/******/        var installedModules = {};

/******/    // The require function

/******/    function __webpack_require__(moduleId) {

/******/ 	      // Check if module is in cache

/******/ 	      if(installedModules[moduleId])

/******/             return installedModules[moduleId].exports;

/******/        // Create a new module (and put it into the cache)

/******/        var module = installedModules[moduleId] = {

/******/            exports: {},

/******/            id: moduleId,

/******/            loaded: false

/******/        };

/******/        // Execute the module function

/******/        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

/******/        // Flag the module as loaded

/******/        module.loaded = true;

/******/        // Return the exports of the module

/******/        return module.exports;

/******/    }

/******/    // expose the modules object (__webpack_modules__)

/******/    __webpack_require__.m = modules;

/******/    // expose the module cache

/******/    __webpack_require__.c = installedModules;

/******/    // __webpack_public_path__

/******/    __webpack_require__.p = "";

/******/    // Load entry module and return exports

/******/    return __webpack_require__(0);

/******/ })

/************************************************************************/

/******/ ([

/* 0 */

/***/ function(module, exports) {

	"use strict";

    var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

    function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

    function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }

    function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
    var App = function (_React$Component) {
          _inherits(App, _React$Component);

        function App() {
        _classCallCheck(this, App);

        return _possibleConstructorReturn(this, Object.getPrototypeOf(App).apply(this, arguments));
        }
    _createClass(App, [{
        key: "render",
        value: function render() {
            return React.createElement(
            "div",
            null,
            "Hello "
        );
        }
    }]);
    return App;
    }(React.Component);

    ReactDOM.render(React.createElement(App, null), document.getElementById('app'));
/***/ }
/******/ ]);

请参阅 Webpack 文档webpack.github.io/docs/webpack-dev-server.html

新生成的 bundle.js 存储在内存中,位置是 publicPath 中指定的相对路径。

例如,使用前面的配置,捆绑包将在 localhost:8080/assets/bundle.js 可用。

为了加载捆绑文件,我们在 build 文件夹中创建一个 html 文件(通常命名为 index.html 文件),从该文件夹提供静态文件:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
  <script src="img/bundle.js"></script>
</body>
</html>

默认情况下,应用程序在 localhost:8080/ 运行以启动你的应用。例如,使用前面提到的配置(带有 publicPath),访问 localhost:8080/assets/

Webpack 的优势

在使用 Webpack 作为另一个打包器时,以下是一些重要的优势:

  1. 代码拆分:基于代码大小,它有助于模块化代码块,并在需要时加载这些模块。你可以在你的代码中定义拆分点,根据这些拆分点,将使用代码块。因此,它有助于加快页面加载速度和性能提升。

  2. 加载器:如前所述的图片所示,在左侧,你可以看到除了 JavaScript 和.less代替.css之外,还有各种其他格式,如coffescripts/jsx。因此,这些加载器(npm包)被用来将这些其他格式转换为接受的标准化格式,这使得开发者能够更容易地将代码编码成他们想要的任何格式。在我们之前看到的基于 React 的应用程序中,JSX 格式被广泛使用。因此,这些加载器将非常有用。

  3. 聪明的解析:它有助于解析大多数第三方库,并处理 CommonJS 和 AMD 中广泛使用的样式。

  4. 插件系统:如果你想扩展 Webpack 以在构建过程中创建一个步骤,你可以创建一个使用回调到 Webpack 引用点的插件,在那里你想调用它。

Gulp 简介

现在我们已经看到了一个模块打包器,让我们看看 Gulp 能为我们做什么。Gulp 是一个用于编译和压缩 JS/资源的构建工具,并且它可以在浏览器上执行实时重新加载。Gulp 文件基本上是一个包含 Gulp 应该执行的一组指令的文件。该文件可以有一个默认任务或几个其他任务,可以从一个任务调用另一个任务。

安装 Gulp 和创建 Gulp 文件

让我们安装gulp并配置它与我们的现有应用程序:

npm install -g gulp (for globally installing gulp)
npm install gulp –save-dev (as a developer dependancy)

接下来,在应用程序目录的根目录下创建一个简单的gulpfile.js文件:

var gulp = require('gulp');

gulp.task('default', function() {
  // tasks goes here
});

让我们从终端执行命令:

安装 Gulp 和创建 Gulp 文件

执行 gulp 命令后的控制台截图

然后,我们为 Gulp 相关的任务安装了一些其他包。我们在package.json文件中添加这些包,并运行npm install来安装这些包:

Package.json
{
  "name": "app1",
  "version": "1.0.0",
  "description": "ReactApp",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Doel Sengupta",
  "license": "ISC",
  "dependencies": {
    "react": "⁰.14.3",
    "react-dom": "⁰.14.3"
  },
  "devDependencies": {
    "babel-core": "⁶.3.13",
    "babel-loader": "⁶.2.0",
    "babel-preset-es2015": "⁶.3.13",
    "babel-preset-react": "⁶.3.13",
    "browser-sync": "².9.6",
    "gulp": "³.9.0",
    "gulp-babel": "⁶.1.1",
    "gulp-concat": "².6.0",
    "gulp-eslint": "¹.1.1",
    "gulp-filter": "³.0.1",
    "gulp-notify": "².2.0",
 }
}

注意

Gulp 中的一些关键事项:

  • 初始时,我们需要引入所有在执行任务时所需的 gulp 和相关 gulp 插件/包。

  • 使用gulp.task声明 gulp 任务。

  • .pipe命令用于流式传输需要处理的数据。这个命令用于连接,结果是得到输出。

现在如果我们向 Gulp 文件添加一些任务,它将看起来像以下这样:

var gulp         = require('gulp');
var babel        = require('gulp-babel');
var browserSync  = require('browser-sync');
var concat       = require('gulp-concat');
var eslinting    = require('gulp-eslint');
var notify       = require('gulp-notify');
var reload       = browserSync.reload;

var jsFiles = {
  vendor: [

  ],
  source: [
    '*.js',
    '*.jsx',
  ]
};

// Lint JS/JSX files
gulp.task('eslinting', function() {
  return gulp.src(jsFiles.source)
    .pipe(eslinting({
      baseConfig: {
        "ecmaFeatures": {
           "jsx": true
         }
      }
    }))
    .pipe(eslinting.format())
    .pipe(eslinting.failAfterError());
});

// Watch JS/JSX  files
gulp.task('watch', function() {
   gulp.watch('*.{js,jsx,html}').on("change",reload);
});

// BrowserSync
gulp.task('browsersync', function() {
  browserSync({
    server: {
      baseDir: './'
    },
    open: false,
    online: false,
    notify: false,
  });
});
gulp.task('default', ['eslinting', 'browsersync', 'watch']);

让我们分析 前面的代码

  • 之前声明了四个 gulp 任务,并已突出显示。默认强制任务在最后一行突出显示时内部调用三个任务。在 Gulp 术语中,调用其他任务的任何任务都被提及为父任务的数组元素。

  • gulp.task ('eslinting', function): 这个任务用于检查js & jsx文件中的任何代码问题。为了使用gulp-eslint插件检查jsx,设置了ecmaFeature: {"jsx": true}选项。

  • gulp.watch:正如其名所示,这个任务会监视 JS 文件中的任何变化,并在之后重新编译这些文件。如果不需要监视任何文件,我们需要将 read: false 传递给 options 对象。在 js/jsx 文件发生变化后,我们可以调用 browserSync.reload 或添加任务以重新加载您的 HTML 页面。

  • browsersync:这个插件不是官方为 gulp 设计的;尽管它可以与任何 gulp 任务一起工作。js/jsx 文件中的任何变化都会同步到浏览器。

在终端中从应用程序的根目录执行 gulp 命令后,我们应该能够在终端中看到这样的输出。请参阅以下图片:

安装 Gulp 和创建 Gulp 文件

执行带有任务 gulp 命令后的控制台截图

让我们检查一下 gulp-eslint 的工作方式。在 index.js 文件的开始处添加一行,例如 require 'react'

require "react";
var ReactApp1 = React.createClass({
  render: function(){
    return (
      <div>
        Hello World
      </div>
    )
  }
});

ReactDOM.render(<ReactApp1 />, document.getElementById('app'));

安装 Gulp 和创建 Gulp 文件

执行带有包含错误 eslint 任务的 gulp 命令后的控制台截图

正如我们所知,var React = require("react") 应该是正确引入 React 包的方式。

对于 Gulp 插件来说,有很多,除了在前面提到的示例中提到的那些,它们在我们的日常应用开发中也非常有帮助。请随意查看 Gulp 文档以及他们网站上的相关插件 gulpjs.com/

摘要

在本章中,我们了解到如何使用 Webpack 部署我们的 React 应用程序,以及 Gulp 如何通过自动化任务、压缩我们的资源(JS、JSX、CSS、SASS、图片等)、监视这些文件的变化以及内置在浏览器中的实时重新加载来简化我们的生活。在 第十章,接下来是什么,我们将探讨一些 ReactJS 的高级概念。

第十章。接下来是什么

到目前为止,我们已经从零开始构建基于 React 的 JavaScript 应用程序,将其与 Facebook Graph API 集成,深入探讨组件的各个阶段,生命周期,验证,测试和部署应用程序的所有主题。有了这些,我们已经到达了这本书的结尾,但让我们探索 React 世界的一些高级主题。

在本章中,我们将简要探讨以下主题,因为在一章中不可能详细涵盖所有内容:

  • React 中的 Ajax

  • React Router

  • 服务器端渲染

  • 同构应用程序

  • 热重载

  • Redux React

  • Relay 和 GraphQL

  • React Native

React 中的 Ajax

就像在任何其他应用程序中一样,在基于 React 的应用程序中,可以使用 Ajax 异步获取数据。根据 Facebook 关于使用 Ajax 从服务器加载数据的文档(facebook.github.io/react/tips/initial-ajax.html),你需要记住以下提到的关键点:

  • 在你的 HTML 中包含 jQuery 库:

    <script src="img/"></script>
    

    由于没有单独的 Ajax 库可以从 jQuery 中引用,因此在 React 应用程序中使用 Ajax 时,必须使用整个 jQuery,这会导致下载 jQuery 的压缩版本,从而大大减少加载时间。

    componentDidMount的生命周期阶段加载数据。此方法在客户端的生命周期中只发生一次,并且在这个阶段可以访问任何子组件。建议在此阶段执行任何外部 js 库或使用 Ajax 加载数据。

  • isMounted方法用于检查组件是否已挂载在 DOM 中。尽管这个方法在setState()之前与 Ajax 一起使用,但在使用 ES6 语法(使用React.component)时,这个方法将被弃用,并且可能在未来的 React 版本中完全删除。请参阅facebook.github.io/react/docs/component-api.html

下面是index.html的代码:

<!DOCTYPE html>
<html>
<head>
<script src="img/react.min.js"></script>
<script src="img/JSXTransformer.js"></script>
  <script src="img/react-dom.js"></script>
<script src="img/"></script>
<meta charset="utf-8">
  <title>JS Bin</title>
</head>
<body>
  <div id="app">
        <script type="text/jsx", src="img/index.js"></script>
  </div>
</body>
</html>

以下为index.js的代码:

var GithubUser = React.createClass({
  getInitialState: function() {
    return {
      username: '',
      user_url: '' 
    };
  },

 componentDidMount: function() {
 $.get(this.props.source, function(result) {
 console.log(result);
 var user = result;
 if (this.isMounted()) {
 this.setState({
 username: user.login,
 user_url: user.html_url
 });
 }
 }.bind(this));
 },

  render: function() {
    return (
      <div>
        {this.state.username}'s last gist is
        <a href={this.state.user_url}>here</a>.
      </div>
    );
  }
});

ReactDOM.render(
  <User source="https://api.github.com/users/doel" />,
  document.getElementById('app')
);

React 中的 Ajax

使用 Ajax 的 React (facebook.github.io/react/tips/initial-ajax.html)

React Router

React Router 是基于 React 库的库,它有助于轻松快速地路由具有多个页面的应用程序。尽管在没有 React-router 的情况下可能构建这样的应用程序流程,但随着应用程序的增长,拥有许多页面,识别页面之间的父子关系变得繁琐。这就是 React-router 发挥作用的地方,它确定了如何构建嵌套 UI。

来源:

服务器端渲染

ReactJS 的服务器端渲染是通过 JavaScript(NodeJS 或 io.js)完成的。这种方法实际上在服务器端预先渲染 React 组件的初始状态。因此,它有助于快速渲染网页,因为用户可以在客户端的 JavaScript 完成加载之前看到网页。

然而,这种渲染方式不适用于那些需要从服务器向客户端传输大量数据的应用程序,这可能会减慢页面加载速度。在这种情况下,我们可以使用分页或分块批量加载数据,这样不会减慢页面加载速度,但可以在特定的时间间隔内从服务器端获取。

React API 的以下两种方法提供了服务器端渲染的骨干(facebook.github.io/react/docs/top-level-api.html)。

ReactDOMServer

react-dom/server 包允许您在服务器上渲染您的组件。

ReactDOMServer.renderToString 方法返回一个字符串。它生成两个额外的 DOM 属性—data-React-iddata-React-checksum—这些属性由 ReactJS 库内部使用。

此方法将 ReactElement 的元素渲染到视图的初始 HTML 中,并返回一个 HTML 字符串。

应仅在服务器端渲染和服务器端使用。

在初始页面加载期间,从服务器向客户端发送此方法会导致页面加载速度更快,并启用针对 搜索引擎优化SEO)的网页抓取。

ReactDOM.render() 被调用到任何先前节点时,React 将在这些节点上附加事件处理器,从而加快页面加载速度。

语法是:

string renderToString(ReactElement element)

ReactDOMServer.renderToStaticMarkup 方法与 renderToString 类似。

它主要用于生成静态页面。

语法是:

string renderToStaticMarkup(ReactElement element)

为了说明 ReactJS 服务器端渲染的示例,我们可以在服务器端使用 express(NodeJS 应用程序的简约型网络框架)。

  • npm update

  • npm install express

  • npm init:这将生成一个 package.json 文件

将稍后提到的内容添加到 index.js 文件中,以使用 express 在端口 3000 上启动一个简单的网络应用程序。相同的示例可以在 node_modules/express 目录的 readme 文件中找到:

var express = require('express');
//....Initialise the app variable requiring the express.
var app = express();

/* Denotes the basic routing by the express server.  */
app.get('/', function (request, response) {

  response.send('Hello World');

})

//  The following code will make the app listen in your localhost, i port 3000
app.listen(3000);

我们首先声明应用程序为 express 的一个实例。

我们使用 express 服务器来表示基本路由。在这个例子中,express 实例(app)正在使用 GET HTTP 方法。因此,当 app.get 调用默认路径(/)或服务器上的任何路径时,第三个参数作为 HANDLER,应该当路由匹配时向客户端(浏览器)发送响应 Hello World

应用程序在端口 3000 上运行。您可以根据需要运行应用程序在任何端口上。

使用 node 命令在 express 文件上执行应用程序:`

node index.js

使用 express,我们现在可以看到 ReactJS 服务器端渲染的示例:

  • 在您的应用程序目录中,执行以下命令来下载 express:

    npm install react express
    
    
  • express.js 文件中,我们将调用 React 组件

这是创建 ReactComponent 的代码,不使用 JSX:

ReactComponent.js 文件:

var React = require('react')

var ReactComponent = React.createClass({

  render:function(){

 return React.createElement('li', null, this.props.argument)

  }

});

module.exports = ReactComponent;

在终端中从您的应用程序根目录运行以下命令 node index.js 来启动 express 后,我们将在浏览器中的 localhost:3000 看到以下截图。

ReactDOMServer

Express JS 简单应用程序

这里是对之前提到的代码的解释。

createElement 是 React 的主要类型,它有四个属性(typespropertieskeysref)。之前提到的突出显示的代码意味着它将创建一个类型为列表(li)的 React 元素,它没有任何属性,但将从 React 渲染组件的属性(其键名为argument)传递值。

根据 Facebook 文档(facebook.github.io/react/docs/top-level-api.html)中的 React API,关于 renderToStaticMarkup 的突出显示代码。

string renderToStaticMarkup(ReactElement element),

"类似于 renderToString,但这个不会创建 React 内部使用的额外 DOM 属性,如 data-react-id。如果您想将 React 用作简单的静态页面生成器,移除这些额外属性可以节省大量字节。"

renderToStringReactElement 渲染为其初始 HTML。这应该在服务器上使用。React 将返回一个 HTML 字符串。您可以使用此方法在服务器上生成 HTML,并在初始请求中发送标记,以加快页面加载速度,并允许搜索引擎为了 SEO 目的抓取您的页面。

如果您在已经具有此服务器端渲染标记的节点上调用 ReactDOM.render(),React 将保留它,并且只附加事件处理器,这样您就可以拥有非常快速的首次加载体验。

express.js 文件的代码如下:

var express = require('express');

var React = require('react');

var ReactComponent = React.createFactory(require('./ReactComponent'));

var app = express();

function landingPage(request, response){

  var argument = request.params.argument || 'This is the default Landing Page in absence of any parameter in url';

  var reactComponent = ReactComponent({argument:argument});

 response.send(React.renderToStaticMarkup(reactComponent));

}

app.get('', landingPage);

app.get('/:argument', landingPage)

app.listen(4000)

在终端中从您的应用程序根目录运行以下命令 node index.js 来启动 express 后,我们将在浏览器中的 localhost:4000 看到以下截图。

ReactDOMServer

这是应用程序的截图,显示了 React 服务器端渲染的默认页面。正如我们所见,应用程序监听的端口是 4000

在动态路由的情况下,这是 React 服务器端渲染的截图,显示了其他页面。

ReactDOMServer

如前所述,如果我们使用 renderToString 而不是 renderToStaticMarkup,我们可以在 React 组件中看到两个属性,例如 data-react-iddata-react-checksum

data-react-id:是 ReactJS 库用来在 DOM 中特定识别其组件的自定义数据属性。它可以在客户端或服务器端存在,而服务器端存在的 ID 以点开头,后面跟一些字母和数字,客户端侧的 ID 仅是数字。

以下示例显示了早期的方法rederToString()

function landingPage(request, response){

  var argument = request.params.argument || 'This is the default Landing Page in absence of any parameter in url';

  var reactComponent = ReactComponent({argument:argument});

 response.send(React.renderToString(reactComponent));

}

重新运行 express 并应用上述更改,将在浏览器中的localhost:4000渲染以下内容,如以下截图所示。

ReactDOMServer

应用程序的截图,使用服务器端渲染的 React,采用renderToString方法。

总结来说,我们可以看到 React-router 是一个能够在服务器端和客户端(浏览器)运行的库。为了使用服务器端渲染,我们使用renderToString()方法和路由。在请求-响应周期中,服务器端的 React-router 与请求的路由匹配,并使用 React 库的renderToString()方法将正确的路由从服务器渲染到客户端(浏览器)。

同构应用程序

同构 JavaScript 应用程序是指 JavaScript 同时在服务器和客户端使用的情况。因此,相同的 React 组件可以在客户端以及服务器端使用。构建此类应用程序的一些优点包括:

  • 当需要时,根据应用程序状态在服务器端渲染视图。

    • 服务器将以与客户端相同的方式渲染应用程序,以增加一致性。
  • 如果浏览器中的 JavaScript 无法正常工作,应用程序仍然可以工作,因为服务器端也存在相同的 JavaScript。您需要将操作发送到服务器以获得相同的结果。

热重载

热重载是 JavaScript 世界中的一个术语,用来指代浏览器中的实时更改,而无需刷新浏览器。在 React 生态系统中,React Hot Loader 被广泛用于相同的目的。

React Hot Loader 是一个针对 Webpack 的插件,它可以在浏览器中实现即时和实时的更改,而不会丢失状态。在编辑 React 组件和函数时,这些更改可以可见,就像 React 的LiveReload一样。

作者(Dan Abramov)在这里讨论了 React Hot Loader 第一版本的局限性medium.com/@dan_abramov/the-death-of-react-hot-loader-765fa791d7c4#.fc78lady9。项目的详细信息可以在gaearon.github.io/react-hot-loader/找到。

Redux React

Redux 是由 Dan Abramov 设计的 JavaScript 库,它帮助 JavaScript 应用程序的状态容器化。随着应用程序的增长,由于模型和视图之间需要双向状态可更新性的要求,复杂性也随之增加。Redux 的出现是为了解决这种扭曲的复杂状态变更和异步问题。因此,它将自己定义为尝试使状态变更可预测。

它可以与 React 或任何其他视图库一起使用。在使用 Redux 时需要记住的一些关键点如下:

  • JavaScript 应用程序的状态完全存储在同一个对象树中的单个存储中。因此,即使应用程序增长,调试也更容易。由于整个应用程序状态都在一个地方,开发阶段也更快。状态是只读的;状态中只有获取器,没有设置器,因为你无法写入这个存储。

  • 对状态的任何更改都只能通过发出一个动作来完成。动作不过是一个描述发生变化的对象。这些动作对象可以被记录、序列化、存储,并在以后重新播放以进行调试。除了这些动作之外,没有任何视图或网络回调可以更改状态。这种限制使得状态变更可预测,无需寻找任何瞬时的隐藏变化。

  • Redux 的第三个组件是reducers。Reducers 告诉动作如何改变状态树。Reducers 实际上就是具有前一个状态和动作的函数。因此,reducers 充当状态存储的设置器,因为它们正在设置新状态。任何要执行的改变都不是在实际的状态对象上,而是在状态对象的副本(新状态对象)上。在简单应用程序中可以使用单个根 reducer,而当任务数量增加时,可以委托给多个子 reducers(通过传递额外的数据)。

来源:

Relay 和 GraphQL

Relay 是 ReactJS 中的一个用于声明式数据获取的框架,它解决了在基于 React 的应用程序中更新数据以及确切更新位置的问题。使用 GraphQL,Relay 框架解耦了要获取的数据与如何获取数据之间的关系。

GraphQL 就像是一种查询语言,用于查询一个图,尽管它通常不是一个像饼图、x 轴、y 轴或维恩图所表示的图。

  • 它用于从关系图中查询,其中每个节点以及它们之间的关系都表示为边。

  • 为了从基于关系图的一个子集中获取数据,GraphQL 非常有用。

  • 与在表示状态传输(REST)中不同,在 REST 中,数据是根据服务器端点使用资源从服务器获取的,而在 GraphQL 中,数据是根据客户端的要求从服务器获取的。

  • 因此,数据被解耦,所有数据都在单个网络请求中一次性从服务器获取。

  • 数据可以轻松地存储和检索缓存,这导致性能更快。

  • 任何写操作都被称为变异。GraphQL 存储在磁盘上的数据变化与返回给开发者的数据之间不是一对一的关系。最好的方法是使用一个查询,它是缓存数据和可能发生变化的数据的交集。

为了深入了解 Relay 框架,请参阅facebook.github.io/relay/docs/thinking-in-relay.html#content

React Native

如其名所示,React Native用于在 iOS 和 Android 平台使用 JavaScript 和 ReactJS 构建原生应用程序。以下是 React Native 的一些关键特性,这些特性受到 Facebook 开发团队的青睐(facebook.github.io/react-native/),用于原生平台:

  • 它使用 React 组件的对应物具有外观和感觉的一致性

  • 您可以使用 Chrome 开发者工具开发应用程序并在模拟器中运行

  • 应用程序和原生平台之间的所有代码都有异步执行

  • React Native 无缝处理触摸事件、polyfills、StyleSheet 抽象、设计常见的 UI 布局

  • 它被广泛用于扩展原生代码,创建 iOS 和 Android 模块和视图,并且可以轻松地稍后重用

  • React Native 的声明性、异步性和响应性特性对于 iOS 开发非常有用

参考文献

注意,这里的列表远非详尽无遗,有大量的优秀文章、博客文章和每天新涌现的更多内容。

这里有一些需要关注的网站:

以下是一些社交媒体上的社区:

摘要

ReactJS 是一个充满活力的 JS 社区。JavaScript 生态系统每天都在发生许多变化和进步。保持我们自己的更新是一个庞大而必要的任务。我们可以通过在社交平台上关注他们、问答论坛、他们的网站、参加会议以及最后但同样重要的是,始终保持我们的实践来密切跟踪 JS 世界的最新动态。

对于任何评论、建议或讨论,请随时通过@doelsengupta@singhalmanu联系我们。