jQuery: Deferred - Promise - căn bản

Bài này tôi viết có sự tham khảo tài liệu sau:

爆速でわかるjQuery.Deferred超入門

jQuery.Deferred là một object có từ phiên bản 1.5 dùng để xử lý vấn đề bất đồng bộ. Dưới đây là các ưu điểm của nó:

  1. Liên kết (chain) các xử lý bất đồng bộ, giúp giải quyết vấn đề Callback hell
  2. Dễ dàng xử lý lỗi
  3. Có thể gom các xử lý bất đồng bộ trong một function nên dễ tái sử dụng.

Deferred và Promise

Đôi dòng giải thích ngữ nghĩa

Deferred

Deferred khi dịch sang tiếng Việt có nghĩa là “trì hoãn”. Vì một Bất đồng bộ không phải xảy ra tức thì mà nó có thể “trì hoãn” và xảy ra bất kỳ thời điểm nào trong tương lai mà ta chưa biết trước được. “Trì hoãn” cũng giống như một công việc mà cần có thời gian để hoàn thành. Công việc đó sẽ mang các trạng thái “Đang thực hiện”, “Đạt”, “Không đạt”.

Promise

Promise khi dịch sang tiếng Việt có nghĩa là “khế ước”, chứa các điều khoản giữa bên A và bên B, chủ yếu ở các điều khoản: “Đạt”, “Không đạt”. Tôi trích nội dung trong một khế ước nghiệm thu như sau:

  1. Khi công trình XYZ kết thúc, nếu được đánh giá “Đạt” thì bên A thanh toán đầy đủ kinh phí cho bên B theo quy định tại Hợp đồng này.
  2. Khi công trình XYZ kết thúc, nếu được đánh giá “Không đạt” thì bên B có trách nhiệm hoàn trả toàn bộ kinh phí … và …

Theo trên, “khế ước” sẽ được xử lý theo trạng thái “Đạt” và “Không đạt” của công trình XYZ.

Deferred và Promise trong jQuery.

Trong jQuery, Deferred là một Object được tạo bởi function $.Deferred():

var d = new $.Deferred();

Khi Object Deferred được tạo thì object Promise cũng tự động sinh ra. Object Deferred bao bọc object Promise.

-----------------
|   Deferred    |
|               |
|               |
|  ------------ |
|  |  Promise | |
|  |----------| |
|_______________|

Và để xử lý các Bất động bộ, cần thực hiện trình tự theo các bước sau:

jquery-deferred

  1. Tạo object Deferred
  2. Start function bất đồng bộ và truyền function thay đổi trạng thái của Deferred (công việc XYZ) vào trong async operaton.
  3. Trả về Promise

Tất nhiên, những function bất đồng bộ của jQuery như $.ajax(), $.getJSON() thì từ đầu đã qua trình tự thiết lập trên rồi, nên ta không cần phải thực hiện lại nữa.

Cấu trúc object Promise

jqueryDeferred1

Về cơ bản, một Promise gồm có:

  • Trạng thái (.state)
  • Callback .done() được thực thi khi trạng thái là resolved
  • Callback .fail() được thực thi khi trạng thái là rejected

Bên dưới là cấu trúc thực tế của Object Promise:

promise.png

Trong đó có 2 Callback function .done(), .fail() và function .then() .

Thực nghiệm

Source code:

<html>
<head>
    <title></title>
    <script type="text/javascript" src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
</head>
<body>
    <script type="text/javascript">
        /**
         * Delay 1 giây rồi log ra Hello! rồi gọi function resolve của Deferred.
         *
         * @returns Promise
         */
        function delayHello()
        {
          var d = new $.Deferred;
          setTimeout(function(){
            console.log('Hello!');
            d.resolve();
          }, 1000);
          return d.promise();
        }
        /**
         * Delay 1 giây rồi log ra Hello! rồi gọi function reject của Deferred.
         *
         * @returns Promise
         */
        function delayError() {
          var d = new $.Deferred;
          setTimeout(function(){
            d.reject('Error!!');
          }, 1000);
          return d.promise();
        }
        function hello1() {
          console.log('Hello sync1');
        }

        function hello2() {
          console.log('Hello sync2');
        }
    </script>
</body>
</html>

.done() và .fail()

Bạn hãy tạo file jqueryDeferred.html chứa source code trện, rồi hãy mở Chrome > Developer Tool, trong tab Console, hãy thử:

var sucess = delayHello(); 
sucess.done(hello1);

//Output
> Object {}
> Hello!
> Hello sync1

sucess.fail(hello1);

//Output
> Object {}


var error = delayError(); 
error.fail(hello1);

//Output
> Object {}
> Hello sync1

function delayHello gọi xử lý resolve() của Deferred, cho nên Callback .done() được thực thi, còn Callback .fail() không được thực thi.

resolve

function delayError gọi xử lý reject() của Deferred, cho nên Callback .fail() được thực thi.

resolve

.then()

Khi sử dụng .then(), ta có thể đăng ký cả 2 callback .done().fail() đồng thời.

.then(
    function(){} /* done */
,   function(){} /* fail */
)

Do đó, nếu không truyền vào callback function thứ 2 thì mặc định nó chỉ đăng ký callback .done thôi

.then(
    function(){} /* done */
)

Đoạn thực nghiệm dưới đây sẽ minh họa ý trên.

delayHello().then(hello1, hello2);

// Output
> Hello!
> Hello sync1

delayHello().then(hello1);

// Output
> Hello!
> Hello sync1

delayError().then(hello1, hello2);
// Output
> Hello sync2

Nếu chỉ để xử lý cho duy nhất một Bất đồng bộ thì sẽ không thấy hết lợi ích của jQuery.Deferred, lợi ích của jQuery.Deferred thể hiện khi xử lý theo dạng chain. Phần tiếp theo sẽ giải thích rõ hơn chain trong Deferred.

.then() chain 1: TH chain các function thông thường

.then() không những dùng để đăng ký .done(), .fail() mà còn trả về một object Promise mới chứ không phải chỉ là this trỏ đến một object Promise.

promise-then.png

(done, fail cũng trả về một object Promise mới)

Ta cũng có thể xác nhận điều đó bằng so sánh giá trị trả về từ các lần gọi .then().

var p1 = delayHello();
var p2 = p1.then(hello1);
var p3 = p2.then(hello1);

console.log(p1 === p2); //false
console.log(p2 === p3); //false
console.log(p3 === p1); //false

Nếu ta không quan tâm đến object trả về giữa chừng:

delayHello()
.then(hello1)
.then(hello1);

// Output
> Hello!
> Hello sync1
> Hello sync1

// Hello sync1 đồng loạt cùng output

Đoạn code trên sẽ tạo ra 3 Promise có trạng thái liên kết với nhau.

1 Promise được sinh ra từ việc tạo Deferred, và 2 Promise còn lại được trả về từ .then()

Promise trả về của .then() sẽ tự động kế thừa trạng thái của Promise trước nó -> Trạng thái của Promise sẽ liên kết nhau. Khi trạng thái Promise trả về của delayHello() là “resolved” thì những Promise sau nó cũng sẽ trở thành “resolved” và các xử lý tương ứng cũng sẽ đồng loạt cùng xử lý.

delayError()
.then(hello1)
.then(hello1);

// Output
-- nothing --

Tương tự như vậy, khi trạng thái Promise trả về của delayError() là “reject” thì những Promise sau nó cũng sẽ trở thành “reject” và các xử lý tương ứng cũng sẽ đồng loạt cùng xử lý. Tuy nhiên trong đoạn code trên, các xử lý cho TH fail không được truyền vào cho nên nó bỏ qua hết.

.then() chain 2:TH liên kết các function có trả về Promise

Như đã nêu ở trên, ở jQuery.Deferred thì function bất đồng bộ, chúng ta cần có bước “Trả về Promise”.
.then() trả về Promise cho nên ta có thể liên kết các xử lý bất đồng bộ lại với nhau, do đó ta có thể thực thi việc xử lý bất đồng bộ theo dạng trình tự từng bước, không cần phải lồng xử lý trong xử lý.

delayHello()
.then(delayHello)
.then(delayHello)
.then(delayHello)
.then(delayHello);

//Output: cứ sau mỗi 1 giây, in ra 'Hello!'

Cấu trúc các object Promise sinh ra trong quá trình này sẽ phức tạp hơn so với TH 1
Dưới đây là hình minh họa khi liên kết 2 delayHello:

delayHello().then(delayHello);

Khác với TH1, trong TH này có tới 3 object Promise.
delayHello() đầu tiên trả về 1 Promise (p1), .done của Promise này sẽ trả về 1 Promise thứ 2 (p2), và Promise thứ 3 (p3) thì được sinh ra theo .then(). Trạng thái p2 sẽ ngay tức khắc truyền cho p3, nên ta cũng không cần quan tâm đến p2 cho lắm.

Trong TH ta sử dụng .done thay cho .then thì nó giống như việc ta đăng ký 2 callback done cho cùng một trạng thái, cho nên khi trạng thái chuyển sang done thì 2 callback này sẽ cũng thực thi.

delayHello().done(delayHello).done(delayHello);

//Output
Chờ 1 giây, output ra `Hello!`
Sau 1 giây nữa thì output cùng lúc 2 `Hello!` 

Cấu trúc Promise sẽ như sau:

Khi liên kết bằng .then():

delayHello().then(delayHello).then(delayHello);

//Out ra `Hello!` theo trình tự 1 giây

.then() chain 3:TH sửa lỗi khi có lỗi.

Với Promise

delayError()
.then(hello1,
  function(e){
    console.log(e);
    console.log('Fixed from Error');
    return new $.Deferred().resolve().promise();
  })
.then(hello1, hello2);

// Output
1 giây sau
'Error!!'
'Fixed from Error'
'Hello sync2'

Dùng $.when() để liên kết song song:

$.when() dùng để tập hợp các Promise và trả về 1 Promise mới. Vì $.when() trả về một object Promise, cho nên ra cũng có thể sử dụng .then(), .done(), .fail() để liên kết:

$.when(delayHello(), delayHello(), delayHello())
.done(hello1);

Hình minh họa:

Nếu tất cả các Promise đều là “Resolved” thì Promise trả về từ $.when() sẽ là “Resolved”. Ngược lại, chỉ cần 1 Promise là “Pending” thì xử lý cũng sẽ pending. Còn nếu chỉ 1 trong các Promise là “Rejected” thì ngay tức khắc Promise trả về từ $.when() sẽ là “Rejected”.

TH Resolved:

TH Rejected:

Trong TH rejected, cho dù Promise trả về từ $.when() là “Rejected” đi nữa nhưng các xử lý song song trong nó vẫn tiếp tục thực hiện. Nếu ta muốn stop việc xử lý này lại thì ta có thể dùng logic sửa lỗi, đăng ký xử lý cho TH .fail().

Function hóa

Trong chuỗi .then(), ta có thể chia các phần xử lý thành các function để có thể tái sử dụng.
Ví dụ:

delayHello()
.then(delayHello)
.then(function(){
  return $.when(delayHello(), delayHello(), delayHello());
})
.then(delayHello)
.then(function(){
  return $.when(delayHello(), delayHello(), delayHello());
})
.then(delayHello);


// Gôm phần xử lý như nhau thành 1 function:
function delayHelloParallel() {
  return $.when(
    delayHello(), delayHello(), delayHello()
  )
  .then(delayHello);
}

//Sử dụng delayHelloParallel trên:
delayHello()
.then(delayHello)
.then(delayHelloParallel)
.then(delayHelloParallel)

Reactjs - Bài 18 - Kiến trúc Flux

Home page:
http://facebook.github.io/flux/
Overview:
“Flux is the application architecture that Facebook uses for building client-side web applications. It complements React’s composable view components by utilizing a unidirectional data flow. It’s more of a pattern rather than a formal framework, and you can start using Flux immediately without a lot of new code.”
Có 3 ý chính:
- Flux là architecture hơn là một formal framework.
- Sử dụng cho việc xây dựng client-side web applications.
- Nó bổ sung khả năng kết hợp các view components của React bằng việc sử dụng một unidirectional data flow
Kiến trúc Flux trên Homepage Flux:
Flux Architech
Như hình minh họa trên, tôi nêu sơ lược các phần trong Flux, còn cụ thể như thế nào thì bạn phải bắt tay vào code thì mới hiểu rõ nhất.
  • View : là các React Component.
  • Store: là nơi chứa, xử lý dữ liệu cho App.
  • Action: là nơi khởi tạo action.
  • Dispatcher: là nơi phát action.
Bây giờ, chúng ta sẽ bắt đầu tìm hiểu về Flux thông qua ví dụ mà chúng ta đã sử dụng. Tôi sẽ đi theo hướng là thay đổi lần lần cách thức hoạt động cũ thành Flux, cho các bạn dễ dàng nhận ra sự khác biệt.
Bạn có thể clone source code trên Github theo link bên dưới, chuyển sang nhánh flux, và chọn từng node commit để theo dõi theo trình tự mà tôi sẽ trình bày sau.
https://github.com/phungnc/reactjs-tut-demo/tree/feature/flux/flux-client

Store - (Static) View

Khoan đi vào Actions, Dispatcher, trước mắt chúng ta thử xem mối quan hệ giữa View và Store.
Chia phần ứng dụng thành các file:
- Member.react.js đóng vai trò như là 1 Item View
- MemberApp.react.js đóng vai trò như là 1 List Item View
- app.js đóng vai trò như là root app, sẽ chứa MemberApp
- index.html vẫn như cũ
- MemberStore.js đóng vai trò như model data của Member.
js
|_index.html
|_app.js
|_stores
| |_ MemberStore.js
|
|_views
| |_ Member.react.js
| |_ MemberApp.react.js
MemberStore.js
// MemberMemberStore.jsjs
var EventEmitter = require('events').EventEmitter;

var _members = {
      members: [
        {id:1, name: "Avatar 1", like: false, src: "http://canime.files.wordpress.com/2010/05/mask-dtb.jpg"},
        {id:2, name: "Avatar 2", like: true, src: "http://z4.ifrm.com/30544/116/0/a3359905/avatar-3359905.jpg"},
        {id:3, name: "Avatar 3", like: false, src: "http://www.dodaj.rs/f/O/IM/OxPONIh/134.jpg"}
      ]
};

class MemberStore extends EventEmitter {
  constructor() {
    super();

  }
  getMembers() {
    return _members;
  }
}
module.exports = new MemberStore();
Ở đây MemberStore chỉ đơn thuần là một Object có phương thức getMembers() trả về dữ liệu members
Ở file MemberApp.react.js, ta chỉ cần thay đổi nội dung trả về từ members bằng việc trả về kết quả từ phương thức getMembers() từ MemberStore. Tất nhiên để làm được chuyện này ta phải require MemberStoreMemberApp.react.js
Với source này tôi có đưa syntax ES6 vào cho nên chúng ta cần babelify kết hợp với browserify.
Rồi dùng command như sau để bundle up file bundle.js
browserify app.js -t babelify -o bundle.js
Sau đó bạn thử open file index.html, thử thao tác các button, ¯_(ツ)_/¯ mọi thứ vẫn như xưa. Như vậy chúng ta đã xong việc lấy dữ liệu từ Store để hiển thị lên View.

View - Actions

Phần này ta sẽ đi vào mối quan hệ giữa View và Action.
Trong kiến trúc Flux, Action đóng vai trò tiếp nhận action từ View vào tạo action cho App.

Thử với delete action - xây dựng mối quan hệ

Thêm file MemberActions.js trong thư mục action. Đầu tiên ta chỉ cần có một destroy action:
module.exports = {
  destroy: id => {
    alert ("Destroy " + id);
  }
};
Trong file, Member.react.js, import file MemberActions.js
var MemberActionCreators = require('../actions/MemberActions');
Nhờ vậy Member.react.js có thể gọi method destroy từ MemberActions.js.
 _onDelete() {
   MemberActions.destroy(this.props.id);
 },

Actions and Action Creators

Ở trên, ta đã xây dựng được mối quan hệ giữa View và Actions: hễ User click vào button delete ở View thì Actions có nhiệm vụ alert dòng chữ “Destroy {id}” với id là dữ liệu được truyền từ View thông qua method MemberActions.destroy();. Chúng ta thử phân tích về thông tin mà nó alert lên:
  • Destroy: là một loại action hay Action Type
  • id: là data mà View truyền tới.
2 yếu tố trên tạo thành 1 action. Chúng ta có thể xem 1 Action = {Action Type, Data}.
Thực ra nếu bạn đã quen thuộc với khái niệm Event thì chắc hẳn sẽ thấy Action rất giống Event.
Action Creators đóng vai trò như một handling giữa Actions và Dispatcher. Tập hợp các Actions Creator sẽ tạo thành một module chứa các API mà ta sẽ dễ dàng sử dụng sau này.
  destroy: id => {
    AppDispatcher.dispatch({
      type: MemberConstants.MEMBER_DESTROY, 
      id: id
    });
  }
{
  type: MemberConstants.MEMBER_DESTROY, 
  id: id
}
là một Action, việc gắn Action với Dispatcher ta gọi là Action Creator.

Dispatcher

Dispatcher đóng vai trò như một bộ phát tín hiệu, phát đi Actions mà nó đã được ‘gắn’ vào.

View - Actions - Dispatcher - Store (- View)

Dispatcher - Store

Dispatcher sẽ phát ‘tín hiệu Actions’ tới Store.
(View thông qua Action Creators để tạo Action và nhờ Dispatcher phát Action này đến Store). Store lúc này sẽ làm nhiệm vụ phân loại Action để đưa ra xử lý, phần xử lý này sẽ nằm trong method constructor():
  constructor() {
    super();
    _dispatchToken = AppDispatcher.register(action => {
      switch(action.type) {

        case MemberConstants.MEMBER_DESTROY:
          alert(action.type + ' ' +action.id);
          break;
      }
    });
  }
Để Dispatcher có thể phát Action Type, ta cần Contants để chứa Action Type đó.
MemberConstants.js
module.exports = {
  MEMBER_DESTROY : 'MEMBER_DESTROY'
};
Hãy thử chạy souce code, bây giờ MemberStore đã nhận được tín hiệu Action bao gồm Action Type và Action Data từ Dispatcher.

Store - View

OK, chúng ta đã thấy “dòng” Action từ View đến Store. Như đã giới thiệu ở trên, Store đóng vai trò quản lý dữ liệu của app, mọi thay đổi dữ liệu của App đều phải được xử lý ở Store. Và dĩ nhiên sự thay đổi dữ liệu này sẽ không có ý nghĩa với User nếu nó không phản ánh lên UI. Do đó chúng ta phải phản ánh Action này lên View: khi user delete Member Item thì Item này sẽ bị remove. Việc này bao gồm 3 bước:
1. Remove dữ liệu ở Store.
2. Phát sự kiện thay đổi từ Store.
3. Bắt sự kiện và thực hiện việc remove Item ở View

1. Remove dữ liệu ở Store:

Dữ liệu ở Store là Collection _members.
Method xóa một Item:
function destroy(id) {
  let memberIndex = _members.findIndex(member => member.id == id);
  delete _members[memberIndex];
};
Ở đây tôi dùng ES6 polyfill array.prototype.findindex để tìm Item cần remove ra khỏi Array _member.
Và tại nơi xử lý loại action, ta sẽ gọi method detroy() này
switch(action.type) {  
  case MemberConstants.MEMBER_DESTROY:
    destroy(action.id);
    break;
}

2. Phát sự kiện thay đổi từ Store.

Sau khi đã thực hiện xong việc remove Item ra khỏi Array, ta sẽ phát sự kiện thay đổi này:
switch(action.type) {  
  case MemberConstants.MEMBER_DESTROY:
    destroy(action.id);
    this.emit('change');
    break;
}

3. Bắt sự kiện và thực hiện việc remove Item ở View

  • Bắt sự kiện:
Việc thiết lập bắt sự kiện này sẽ được thực hiện sau khi component đã được render.
componentDidMount() {
  MemberStore.on('change',this._onChange);
}
Khi có sự kiện ‘change’ thì nó sẽ gọi handler _onChange (sẽ nói sau) để thực hiện việc update thay đổi đó.
  • Remove Item:
_onChange() {
  this.setState({members: MemberStore.getMembers()});
}
Khi có sự thay đổi ở Store, ta chỉ cần update lại state của MemberApp, khi đó MemberApp.react sẽ tự động render lại View (chắc bạn đã làm quen với điều này ở các bài trước).
  • Remove event
Vì sau khi remove Item, Component sẽ render lại, cho nên chúng ta cần remove Event để tránh bị double, và việc removeListener sẽ được thực hiện trước khi render().
componentWillUnmount() {
  MemberStore.removeListener('change',this._onChange);
}
Sau khi Component được render, chúng ta lại thiết lập lại việc bắt sự kiện như ở trên.
MemberApp.react.js
var React   = require('react/addons'),
    request = require('superagent'),
    Router  = require('react-router'),
    mui     = require('material-ui'),
    Member  = require('./Member.react')
    ;

var Route         = Router.Route;
var RouteHandler  = Router.RouteHandler;
var DefaultRoute  = Router.DefaultRoute;
var Link          = Router.Link;

var ThemeManager = new mui.Styles.ThemeManager();

var List     = mui.List;
var ListDivider  = mui.ListDivider;

var MemberStore = require('../stores/MemberStore');


var MemberApp = React.createClass({
  childContextTypes: {
    muiTheme: React.PropTypes.object
  },
  getChildContext() {
    return {
      muiTheme: ThemeManager.getCurrentTheme()
    };
  },
  getInitialState() {
    return {
      members: MemberStore.getMembers()
    }
  },
//  deleteItem(id){
//    this.setState({
//      members: this.state.members.filter(function(member){
//        return member.id !== id;
//      })
//    });
//  },
  componentDidMount() {
    MemberStore.on('change',this._onChange);
  },
  componentWillUnmount() {
    MemberStore.removeListener('change',this._onChange);
  },
  // Event hanlder for 'change' event comming from the MemberStore
  _onChange() {
    this.setState({members: MemberStore.getMembers()});
  },
  render() {
    var members = this.state.members.map(function(member){
      return (
        <div>
          <Member id={member.id} onDelete={this.deleteItem} name={member.name} initialLike={member.like} src={member.src}/>
          <ListDivider inset={true}/>
        </div>
        );
    }, this);
    return (<List>{members}</List>);
  }
})

module.exports = MemberApp;
Lưu ý: chúng ta không còn cần method deleteItem(id) nữa.
MemberStore.js
// MemberStore.js
var AppDispatcher = require('../AppDispatcher');
var MemberConstants = require('../MemberConstants');

var EventEmitter = require('events').EventEmitter;

var _dispatchToken;
// Member Collections
var _members = [
  {id:1, name: "Avatar 1", like: false, src: "http://canime.files.wordpress.com/2010/05/mask-dtb.jpg"},
  {id:2, name: "Avatar 2", like: true, src: "http://z4.ifrm.com/30544/116/0/a3359905/avatar-3359905.jpg"},
  {id:3, name: "Avatar 3", like: false, src: "http://www.dodaj.rs/f/O/IM/OxPONIh/134.jpg"}
];
require('array.prototype.findindex');

function destroy(id) {
  let memberIndex = _members.findIndex(member => member.id == id);
  delete _members[memberIndex];
};

class MemberStore extends EventEmitter {
  constructor() {
    super();
    _dispatchToken = AppDispatcher.register(action => {
      switch(action.type) {
        case MemberConstants.MEMBER_DESTROY:
          destroy(action.id);
          this.emit('change');
          break;
      }
    });
  }

  getDispatchToken() {
    return _dispatchToken;
  }

  getMembers() {
    return _members;
  }
}

module.exports = new MemberStore();
Cấu trúc 1 Store về cơ bản sẽ như sau (theo hướng TOP-DOWN)
TOP:
- require(import) module cần dùng.
- private variables 
- private method: thực hiện việc xử lý dữ liệu, như `function destroy(id)`
MIDDLE:
- switch … case: phân loại Action Type, gọi method xử lý và phát sự kiện tương ứng.
BOTTOM:
- public method (getter) get data từ Store, như method `getMembers()`
Done!

Reactjs - Bài 17 - Tìm hiểu Jest Framework là gì

Goal

Giới thiệu Reactjs Testing Framework: Jest

Testing Framework

Phần TestUtils đã cho bạn khái niệm về các phương thức test React Component, bây giờ chúng ta sẽ thử ứng dụng Testing Framework.

Mặc dù khi tìm hiểu các bài viết về Reactjs Testing Framework, tôi có thấy nhiều ý kiến cho rằng Jest không bằng các Javascript Testing Framework khác như Mocha hay Karma, nhưng vì theo tôi đang viết theo series về Reactjs nên ở đây tôi sẽ tìm hiểu và giới thiệu Jest.

Jest

From Offical Page:

Jest provides you with multiple layers on top of Jasmine:

1.Automatically finds tests to execute in your repo
2.Automatically mocks dependencies for you when running your tests
3.Allows you to test asynchronous code synchronously
4.Runs your tests with a fake DOM implementation (via jsdom) so that your tests can run on the command line
5.Runs tests in parallel processes so that they finish sooner

Mock trong Jest

Tôi nghĩ tôi cần giải thích sơ qua Mock trong Jest.
Mock có nghĩa là làm giả, làm nhái. Để làm quen từ ngữ tôi sẽ dùng từ mock với nghĩa trên.Jest có thể mock các modules, functions, component thành một “mocked version” rồi sử dụng các Mock này mà không phải đụng đến “real version” của nó.

Mock API Reference

jest.mock(moduleName)

-> should always return a mocked version of the specified module from require() (e.g. that it should never return the real module).

jest.dontMock(moduleName)

-> should never return a mocked version of the specified module from require() (e.g. that it should always return the real module)

Getting Started:

Tôi sẽ thử test cái source code trong bài trước là Reusable Component.

Setup

Trước mắt, cần setup source code như sau:

  1. Copy toàn bộ thư mục “material-ui-component” thành thư mục “testing”.
  2. Tách source code trong file main.jsx thành các Component như member.jsx.
  3. Tạo thư mục test để chứa các file test
  4. Tạo npm test command:

    Trong thư mục “testing”:

    npm init

    để tạo file package.json (File này sẽ có vai trò chứa những config cho Jest sau này), khi init nhớ nhập ‘jest’ cho line test command: jest

  5. JSX transformer: vì source code “material-ui-component” viết theo syntax JSX nên khi test ta cũng cần chuyển đổi JSX thành JS, trong file package.json ta thêm đoạn code sau:

    "jest": {
    "scriptPreprocessor": "preprocessor.js"
    }

Và tạo file preprocessor.js

var ReactTools = require('react-tools');
module.exports = {
  process: function(src) {
    return ReactTools.transform(src, {harmony: true});
  }
};

Bạn cũng cần install react-tools.
6. Remove Mock

Trong file package.json, ta thêm như sau để chỉ định việc không mock tất cả các node module


"jest": {
"scriptPreprocessor": "preprocessor.js",
"unmockedModulePathPatterns": ["node_modules/react"]
},

Tạo Unit Test cho Component member

__test__/member-test.js

jest.dontMock('../member.jsx');

var React = require('react/addons');
var Member= require('../member.jsx');
var TestUtils = React.addons.TestUtils;

describe('Member component', function() {

  it('set name for member', function() {

    // Render a member with name is Foo
    var memberFoo = TestUtils.renderIntoDocument(
      <Member name="Foo" />
    );
    // Verify that it's name is Foo
    var memberName = TestUtils.findRenderedComponentWithType(memberFoo, Member);
    expect(memberName.getDOMNode().textContent).toEqual('Foo');

  });

});

Problem:

Với version nodejs tôi đang dùng là v0.11.14 thì khi chạy sample mẫu từ Jest thì OK, tuy nhiên khi thử test với Component member.jsx thì bị lỗi là: “Error: Worker process exited before responding! exit code: null, exit signal: SIGSEGV stderr:”. Và dường như đây là 1 known issue

https://github.com/facebook/jest/issues/243

Và tôi cũng đã thử với version mới nhất của nodejs tại thời điểm này là là v0.12.5 nhưng cũng không khá hơn.

Note: tôi đang dùng nvm để quản lý version của nodejs. Nhờ nó tôi dễ dàng upgrade cũng như downgrade version của nodejs.

Cũng theo một comment trên https://github.com/facebook/jest/issues/243 , tôi thử dùng nodejs 0.10 và thử retest thì OK:

#Chuyển sang node v0.10
nvm use 0.10

#Rebuild all dependencies
npm rebuild

#Run test
npm test

Bạn có thể thử thay đổi ‘Foo’ trong .toEqual('Foo') thành ‘Fool’ và thử chạy lại câu lệnh npm test, nó sẽ báo kết quả Fail.

Source code demo cho ví dụ trên:

https://github.com/phungnc/reactjs-tut-demo/tree/feature/jest/jest

Tôi cũng thử viết thêm một vài test cho member component vào file __test__/member-test.js:

https://github.com/phungnc/reactjs-tut-demo/blob/feature/jest_testing_more_complex/jest/member.jsx

Reactjs - Bài 16 - Testing - sử dụng TestUtils

Goal

Hôm nay tôi sẽ viết về Reactjs Addons TestUtils trong Reactjs.

TestUtils

http://facebook.github.io/react/docs/test-utils.html

TestUtils là một addon của Reactjs, cung cấp các method để test các React components.

Các mục như Simulate, renderIntoDocument đã viết khá rõ trong link trên. Ở đây tôi chỉ cố gắng demo 2 dạng testing là Assert và Component Testing.

Tôi có dùng lại Source Code bài Addon ReactLink để demo, lý do là nó đã có include sẵn React Addons :).

Bạn hãy chạy đoạn code dưới và xem kết quả nhé.

<!DOCTYPE html>
<html>
<head>
  <meta http-equiv='Content-type' content='text/html; charset=utf-8'>
  <title>Basic React TestUtils Testing</title>
</head>
<body>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react.js"></script>  
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/JSXTransformer.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react-with-addons.js"></script>
  <script type="text/jsx">
    var Hello = React.createClass({
      render: function() {
        return (
          <div>{this.props.name}</div>
        );
      }
    });
    var OnlyMe = React.createClass({
      render: function() {
        return (
          <div>{this.props.name}</div>
        );
      }
    });
    var Link = React.createClass({
      mixins: [React.addons.LinkedStateMixin],
      getInitialState: function() {
        return {message: 'Hello!'};
      },
      handleChange: function(newValue) {
        this.setState({message: newValue});
      },
      render: function() {
       var valueLink = {
          value: this.state.message,
          requestChange: this.handleChange
        };
        var valueLinkk =this.linkState('message');
        return (
          <div>
            <Hello name="FOO "/>
            <Hello name="BAR"/>
            <OnlyMe name="OnlyMe"/>
            <p className="title">{valueLink.value}</p>
            <span className="title">World!</span>
            <span className="sub-title">My Name is C.P</span>
            <input type="text" valueLink={valueLink} />
          </div>
        );
      }
    });

    var LinkComponent = React.render(<Link />, document.body);

    // TestUtils
    var TestUtils = React.addons.TestUtils;
    // Assert
    document.write("=== Assert Testing Type ===");
    //
    document.write("<br/>");
    document.write("<br/>");
    document.write("<b>1.isElement: check if Link is an Element</b>");
    document.write("<br/>");
    document.write(">>>Result is: ",TestUtils.isElement(<Link />));
    //
    document.write("<br/>");
    document.write("<br/>");
    document.write("<b>2.isElementOfType : check if Link is is a ReactElement whose type is of a React componentClass</b>");
    document.write("<br/>");
    document.write(">>>Result is: ",TestUtils.isElementOfType(<Link />, Link));
    //
    document.write("<br/>");
    document.write("<br/>");
    document.write("<b>3.isDOMComponent: check if Link instance is a DOM component (such as a <div> or <span>)</b>");
    document.write("<br/>");
    document.write(">>>Result is: ",TestUtils.isDOMComponent(LinkComponent));
    //
    document.write("<br/>");
    document.write("<br/>");
    document.write("<b>4.isCompositeComponent : check if Link instance is a composite component (created with React.createClass())</b>");
    document.write("<br/>");
    document.write(">>>Result is: ",TestUtils.isCompositeComponent(LinkComponent));
    //
    document.write("<br/>");
    document.write("<br/>");
    document.write("<b>5.isCompositeComponentWithType : check if Link instance is a composite component (created with React.createClass()) whose type is of a React componentClass.</b>");
    document.write("<br/>");
    document.write(">>>Result is: ",TestUtils.isCompositeComponentWithType(LinkComponent, Link));
    //
    document.write("<br/>");
    document.write("<br/>");
    document.write("=== Component Testing Type ===");
    document.write("<br/>");
    document.write("<b>1.array findAllInRenderedTree(ReactComponent tree, function test)</b>");
    document.write("<br/>");
    document.write("Traverse all components in tree and accumulate all components where test(component) is true. This is not that useful on its own, but it's used as a primitive for other test utils.");
    document.write("<br/>");
    document.write(">>> Result is: ",
      TestUtils.findAllInRenderedTree(LinkComponent, 
        function(component) { return component.tagName === "P" }
      ).map(function(component){ return component.getDOMNode().textContent })
    );
    //
    document.write("<br/>");
    document.write("<br/>");
    document.write("<b>2.array scryRenderedDOMComponentsWithClass(ReactComponent tree, string className)</b>");
    document.write("<br/>");
    document.write("Finds all instances of components in the rendered tree that are DOM components with the class name matching className");
    document.write("<br/>");
    document.write(">>> Result is: ",TestUtils.scryRenderedDOMComponentsWithClass(LinkComponent, 
      'title'
      ).map(function(component){ return component.getDOMNode().textContent }));
    //
    document.write("<br/>");
    document.write("<br/>");
    document.write("<b>3.ReactComponent findRenderedDOMComponentWithClass(ReactComponent tree, string className)</b>");
    document.write("<br/>");
    document.write("Like scryRenderedDOMComponentsWithClass() but expects there to be one result, and returns that one result, or throws exception if there is any other number of matches besides one.");
    document.write("<br/>");
    document.write(">>> Result is: ",TestUtils.findRenderedDOMComponentWithClass(LinkComponent, 
      'sub-title'
      ).getDOMNode().textContent);
    //
    document.write("<br/>");
    document.write("<br/>");
    document.write("<b>4.array scryRenderedDOMComponentsWithTag(ReactComponent tree, string tagName)</b>");
    document.write("<br/>");
    document.write("Finds all instances of components in the rendered tree that are DOM components with the tag name matching tagName");
    document.write("<br/>");
    document.write(">>> Result is: ",TestUtils.scryRenderedDOMComponentsWithTag(LinkComponent, 
      'span'
      ).map(function(component){ return component.getDOMNode().textContent }));
    //
    document.write("<br/>");
    document.write("<br/>");
    document.write("<b>5.ReactComponent findRenderedDOMComponentWithTag(ReactComponent tree, string tagName)</b>");
    document.write("<br/>");
    document.write("Like scryRenderedDOMComponentsWithTag() but expects there to be one result, and returns that one result, or throws exception if there is any other number of matches besides one.");
    document.write("<br/>");
    document.write(">>> Result is: ",TestUtils.findRenderedDOMComponentWithTag(LinkComponent, 
      'p'
      ).getDOMNode().textContent);
    //
    document.write("<br/>");
    document.write("<br/>");
    document.write("<b>6.array scryRenderedComponentsWithType(ReactComponent tree, function componentClass)</b>");
    document.write("<br/>");
    document.write("Finds all instances of components with type equal to componentClass");
    document.write("<br/>");
    document.write(">>> Result is: ",TestUtils.scryRenderedComponentsWithType(LinkComponent, 
      Hello
      ).map(function(component){ return component.getDOMNode().textContent }));
    //
    document.write("<br/>");
    document.write("<br/>");
    document.write("<b>7.ReactComponent findRenderedComponentWithType(ReactComponent tree, function componentClass)</b>");
    document.write("<br/>");
    document.write("Same as scryRenderedComponentsWithType() but expects there to be one result and returns that one result, or throws exception if there is any other number of matches besides one.");
    document.write("<br/>");
    document.write(">>> Result is: ",TestUtils.findRenderedComponentWithType(LinkComponent, 
      OnlyMe
      ).getDOMNode().textContent);
    //
  </script>
</body>
</html>

Reactjs - Bài 15 - Tái sử dụng Component Material UI trong Reactjs

Goal

Giới thiệu việc sử dụng Component có sẵn cho Reactjs.

Reusalbe Components

“When designing interfaces, break down the common design elements (buttons, form fields, layout components, etc.) into reusable components with well-defined interfaces. That way, the next time you need to build some UI, you can write much less code. This means faster development time, fewer bugs, and fewer bytes down the wire.”
facebook
Lược dịch:
Khi design giao diện, hãy break down những design elements như button, form fields, layout components, v.v… thành các components có thể tái sử dụng và well-defined interfaces (I/F của components). Với cách này, bạn không cần tốn nhiều thời gian cho lần tạo giao diện tiếp theo. Nghĩa là thời gian phát triển nhanh hơn, ít bug hơn, và giảm bớt wire.

Tìm React Component ở đâu?

Bạn có thể tìm kiếm các React Component ở site bên dưới này.

ReactComponent

Đây là site tập hợp các ReactComponent.
http://react-components.com/

React Rocks

http://react.rocks/

Github wiki

https://github.com/facebook/react/wiki/Complementary-Tools#ui-components

Thử sử dụng một ReactComponent

Hiện tại trên trang http://react-components.com/, tôi sẽ thử sử dụng một Component phổ biến nhất là material-ui để tạo giao diện cho demo Avatar.
Source code:
https://github.com/phungnc/reactjs-tut-demo/tree/feature/material-ui-component/material-ui-component
Trong source code mới này:
1. Vì material-ui đã có Componet Avatar, nên tôi sẽ đổi trên các Component trong demo trước từ Avatar thành Member.
2. Các file delete.jsx, toggle-star.jsx là các Icon Component tôi download từ https://github.com/callemall/material-ui/tree/master/src/svg-icons
3. Tôi vẫn dùng Browserify cho nên bạn vẫn phải dùng lệnh browserify -t reactify avatar.jsx > bundle.js để bundle up file bundle.js.
4. Khi thực hiện browserify nếu xảy ra lỗi thiếu module nào thì hãy install module đó.