Giới thiệu webpack

Khi tôi tìm hiểu về React.js, tôi thấy họ có dùng webpack để “bundle” các module. Trong các bài giới thiệu về Reactjs trước tôi có dùng Browserify để “bundle” các module của Node.js thấy đơn giản và cũng khá hay. Tôi có qua trang tài liệu của webpack-doc để thử tìm hiểu thì thấy hơi khó nhai nên tôi quyết định viết blog để có thể tìm hiểu sâu hơn cũng như có thể chia sẽ với những ai muốn tìm hiểu về nó.

Webpack là gì

Ngay tại trang document:
http://webpack.github.io/docs/what-is-webpack.html

webpack is a module bundler.

webpack takes modules with dependencies and generates static assets representing those modules.

Tôi nghĩ cách hiểu nhanh nhất về nó là ta phải “đụng tay đụng chân” với nó thôi.

Hướng tìm hiểu

  1. Bundle các file JS thành 1 file JS.
  2. Bundle ra các file JS chính từ nhiều file JS
  3. Chia file JS và load khi cần
  4. Nhúng HTML vào trong file JS
  5. Nhúng CSS vào trong file JS
  6. Nhúng file hình Base64 vào trong file JS

1. Bundle các file JS thành 1 file JS

Chuẩn bị:

./
 |_app.js
 |_sub.js
 |_dist
    |_index.html

app.js

var sub = require('./sub.js');
sub("Hello world");

sub.js

module.exports = function(msg){
  alert(msg);
}

index.html

<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Webpack Tutorial Demo</title>
  </head>
  <body>
    <script src="bundle.js"></script>
  </body>
</html>

Nếu bạn lười thực hiện các bước trên, các bạn có thể clone từ github repo demo của tôi:

https://github.com/phungnc/webpack-tut/tree/master/demo01

Bundle command:

webpack app.js dist/bundle.js

Sau đó chạy file index.html thì nó sẽ hiển thị message “Hello world”.

Bundle command trên có cú pháp như khi sử dụng Browserify (browserify app.js > dist/bundle.js). Tuy nhiên khác với Browserify, Webpack có thể config việc bundle đó trong file: webpack.config.js. Hãy tạo file webpack.config.js đồng cấp với file app.js

module.exports = {
  entry: {
    app: './app.js'
  },
  output: {
    path: __dirname + '/dist',
    filename: 'bundle.js'
  }
}

Sau đó chỉ cần bạn chạy lệnh

webpack

Thì nó cũng tạo ra file bundle.js giống như trước.

2. Bundle ra các file JS chính từ nhiều file JS

Source code demo:
https://github.com/phungnc/webpack-tut/tree/master/demo02

webpack.config.js

module.exports = {
  entry: {
    app: './app.js',
    list: './list.js',
    item: './item.js',
  },
  output: {
    path: __dirname + '/dist',
    filename: '[name].js'
  }
}

Run webpack command, hãy xem output in ra:

Hash: 42caea66a4d1045d2c6c
Version: webpack 1.12.2
Time: 68ms
  Asset     Size  Chunks             Chunk Names
 app.js  1.56 kB       0  [emitted]  app
item.js  1.56 kB       1  [emitted]  item
list.js  1.56 kB       2  [emitted]  list
   [0] ./app.js 43 bytes {0} [built]
   [0] ./item.js 43 bytes {1} [built]
   [0] ./list.js 44 bytes {2} [built]
   [1] ./sub.js 47 bytes {0} {1} {2} [built]

Nó built ra các file trong thư mục dist các file app.js,list.js, item.js tương ứng đã được config trong file webpack.config.js. Khả năng có thể build ra nhiều file js cũng là điểm thú vị của webpack.

Nhưng mà, nếu ta xem trong các file app.js, list.js hay item.js thì có phần source code cả 3 file đều trùng nhau:

/******/ (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);
/******/ })
/************************************************************************/

Để giải quyết vấn đề này ta có thể sử dụng plugin:

webpack.config.js

var webpack = require('webpack');
module.exports = {
  entry: {
    app: './app.js',
    list: './list.js',
    item: './item.js',
  },
  output: {
    path: __dirname + '/dist',
    filename: '[name].js'
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin('app','app.js')
  ]
}

Chạy lại command

webpack

Bây giờ bạn thử check lại trong các file dist/list.jsdist/item.js xem. Phần source code giống nhau bây giờ chỉ còn lại ở file dist/app.js.

3. Chia file JS, chỉ load khi cần thiết.

Nếu bạn nào đã từng dùng qua requirejs rồi thì sẽ dễ dàng hiểu phần này hơn.
Ta có thể gọi chức năng này là module loader, tức là ta có thể load một module khi nào cần thiết. Khác với việc tạo một file bundle bao gồm tất tần tật các module JS vào, với webpack ta có thể cấu hình để chỉ load module cần thiết khi cần thiết thôi. Việc này có lợi cực kỳ khi hệ thống ngày càng lớn, khi đó nếu chỉ dùng 1 file bundle thì việc load ban đầu sẽ trở nên chậm chạp làm giảm performance của app, ngược lại việc load module một cách bất đồng bộ sẽ giúp tăng performance của app.

Ta sẽ demo bằng cách là trong file app.js sẽ load module sub.js sau 3 giây.

Source code:
https://github.com/phungnc/webpack-tut/tree/master/demo04

app.js

window.setTimeout(function() {
    require.ensure([],function(sub) {
        var sub = require('./sub');
        sub('App');
    });
},3000);

Run WebPack command

webpack

Hãy thử mở file index.html, sau khi mở lên, 3 giây sau thì nó sẽ alert “App”. Bạn có thể dùng Chrome Develop Tool để xem tiến trình load này.

4. Nhúng file HTML vào trong file JS

Bằng việc sử dụng module html-loader, ta có thể load file HTML vào trong file JS.
Trước hết ta phải install html-loader

Source code:
https://github.com/phungnc/webpack-tut/tree/master/demo04

npm install html-loader --save-dev

Sau đó trong webpack.config.js file:

module.exports = {
  entry: {
    app: './app.js'
  },
  output: {
    path: __dirname + '/dist',
    filename: 'bundle.js'
  },
  module: {
      loaders: [
          { test: /\.html$/, loader: 'html-loader' },
      ]
  }
}

Thêm phần load module html-loader.

Về phần source code, chuẩn bị file HTML có nội dung nào đó như là:

container.html

<div id="container">
    <section>
        <h2>Web pack is Cool</h2>
          <p>webpack is a module bundler.</p>
          <p>webpack takes modules with dependencies and generates static assets representing those modules.</p>
    </section>
</div>

app.js

var container = require('./container.html');
document.body.innerHTML = container;

Và run WebPack command:

webpack

Với việc nhúng HTML từ bên ngoài vào trong JS, thì chúng ta sẽ dễ nghĩ đến hướng chia cấu trúc trang web thành các template, component. Phần này có lẽ tôi sẽ giới thiệu trong các bài viết sau.

5. Nhúng file CSS vào trong file JS

Tương tự như việc load file HTML vào trong file JS, ta cũng có thể sử dụng các module style-loadercss-loader để nhúng CSS vào trong file JS.

Source code:
https://github.com/phungnc/webpack-tut/tree/master/demo05

npm install style-loader css-loader --save-dev

css-loader: tạo chuỗi css và nhúng vào trong file JS.

style-loader: ghi style vào trong tag

webpack.config.js

module.exports = {
  entry: {
    app: './app.js'
  },
  output: {
    path: __dirname + '/dist',
    filename: 'bundle.js'
  },
  module: {
      loaders: [
          { test: /\.html$/, loader: 'html-loader' },
          { test: /\.css$/, loaders: ['style-loader', 'css-loader']},
      ]
  }
}

Sau đó bạn có thể chuẩn bị tùy ý style mà bạn thích trong file style.css như là:

style.css

#container {
  width: 800px;
  margin: 0 auto;
}
section {
  text-align: center;
}

Gọi css vào trong file app.js

require('./style.css');
var container = require('./container.html');
document.body.innerHTML = container;

Run webpack

webpack

6. Nhúng file image dạng base64 vào trong file JS

Sau cùng, chúng ta thử nhúng file hình vào trong file JS. Ta sử dụng url-loader

npm install url-loader --save-dev

Cũng tương tự như những phần trên, cho nên tôi sẽ không giới thiệu lại, các bạn có thể tham khảo source code bên dưới:

Source code
https://github.com/phungnc/webpack-tut/tree/master/demo06

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 đó.

Reactjs - Bài 14 - React router

Reactjs - router

[Update ngày 21/06/2016]
Nội dung bài viết bên dưới cách đây 1 năm, hiện tại react-router đã update rất nhiều, nên tôi khuyến khích các bạn hãy cập nhật nội dung về  react-router tại
https://github.com/reactjs/react-router-tutorial/tree/master/lessons

Tuy nhiên nội dung bên dưới vẫn có giá trị tham khảo, đặc biệt là khi có và không có react-router.

Goal

Giới thiệu react-router.

Giới thiệu

Nếu các bạn đã trải qua các bài viết từ bài đầu tiên đến giờ có lẽ bạn cũng đã nhận ra rằng React.js chỉ là thư viện để tạo các Component, nó không có Router.
react-router được dùng để giúp việc dẫn hướng UI đồng bộ với URL.
Để giúp các bạn hiểu rõ hơn react-router, chúng ta sẽ thử implement 2 tình huống như sau
- Without react-router
- With react-router
- Server rendering react-router
Cách này tôi dựa trên ý tưởng theo tài liệu của React Router nhưng được trình bày lại theo luồng source code demo từ những bài trước, cho nên nếu việc tham khảo trực tiếp thông qua tài liệu của React Router khiến bạn dễ hiểu hơn thì bạn nên tham khảo nó.
Ý tưởng demo như sau:
Chúng ta sẽ sử dụng source code Avatar list, và thêm trang detail cho từng Avatar. Có nghĩa là từ list Avatar, click vào link của từng Avatar thì nó chuyển sang trang Avatar detail của Avatar đó.

Without react-router

Thêm đoạn code xử lý load nội dung Avatar list hay Avatar Item theo route:
var App = React.createClass({
  render(){
    var Child;
    switch (this.props.route) {
      case 'avatars': Child = Avatars; break;
      case 'avatar': Child = Avatar; break;
      default: Child = Avatars;
    }
    return(
      <div>
        <Child />
      </div>
    )
  }
});

function render(){
  var route = window.location.hash.substr(1);
  React.render(<App route = {route}/>, document.body);
}
window.addEventListener('hashchange', render);
render();
App sẽ render các nội dung khác nhau tùy theo giá trị this.props.route. Nếu chỉ 2 trường hợp như trên thì còn khá là đơn giản nhưng trong trường hợp nhiều trang hơn, nhiều cấp cho route hơn thì vấn đề sẽ nhanh chóng trở nên phức tạp.
Source code minh họa:
https://github.com/phungnc/reactjs-tut-demo/tree/feature/route_without_react_router/route-without-react-router
Lưu ý: Vì tôi đã giới thiệu việc dùng browserify trong bài trước, nên từ bây giờ tôi sẽ dùng browserify cho việc require các module cần thiết. Cho bạn cần dùng câu lệnh sau để build file bundle.js
browserify -t reactify avatar.jsx > bundle.js

With react-router

Phần xử lý route sẽ trở thành:
var routes  = (
  <Route handler={App}>
    <DefaultRoute handler={Avatars} />
    <Route name="avatars" handler={Avatars} />
    <Route name="avatar" handler={Avatar} />
  </Route>
);

var App = React.createClass({
  render(){
    return(
      <div>
        <RouteHandler/>
      </div>
    )
  }
});
//Finally we need to listen to the url and render the application.
Router.run(routes, Router.HashLocation, function(Root) {
  React.render(<Root/>, document.body);
});
Nhớ là bạn cần ‘require’ react-router Router = require('react-router'); ở đầu file.
Source code:
https://github.com/phungnc/reactjs-tut-demo/tree/feature/route_with_react_router/route-with-react-router
Trong phần này, bạn cần đọc thêm các nội dung ở link này:
https://github.com/reactjs/react-router-tutorial/tree/master/lessons

Server-side rendering and react-router

React-router chẳng những có thể dùng ở phía client mà còn dùng được ngay cả khi render React App ở phía Server.
Nếu bạn hiểu sơ qua nguyên lý hoạt động React-router ở trên và đã đọc qua bài viết Server-side rendering thì việc modify lại code dùng React-router cho phía server sẽ không có vấn đề gì. Chỉ cần chú ý một chút là ở Sever chúng ta bundle up file browser.js
browserify -t reactify browser.js > bundle.js
Source code:
https://github.com/phungnc/reactjs-tut-demo/tree/feature/server_side_react_router/server-rendering-react-router

Reactjs - Bài 13 - server-side renderring

Goal

Trong các bài trước chúng ta chỉ sử dụng Reactjs ở phía Client, chúng ta có thể render HTML hoàn toàn ở phía Client. Trong bài này tôi sẽ giới thiệu cách ứng dụng Reactjs trong việc render HTML ở phía Server.

Client-side rendering

Trong các ví dụ trước, chúng ta render View hoàn toàn ở phía client nhờ vào method React.render(). Hãy xem hình minh họa bên dưới

Client-side-rendering

Để giúp các bạn hiểu rõ quá trình chuyển việc render từ phía client sang phía server cho nên tôi sẽ liệt kê các bộ phận cũng như từng bước của việc render ở phía Client như sau:

  1. File index.html: layout của view, chứa DOM để gắn ReactElement vào (trong các bài trước, tôi gắn React Element vào thẻ ).
  2. Include các file react.js, JSXTransformer.js,(ngoài ra còn có các file khác như superagent.js hay React-addon…)
  3. React.createClass() định nghĩa Virtual DOM
  4. React.createElement() tạo Virtual DOM
  5. React.render() render Virtual DOM vào thẻ
  6. Có một được một trang index.html hoàn chỉnh.

Source code mẫu cho Client-side rendering:

<!DOCTYPE html>
<html>
<head>
  <meta http-equiv='Content-type' content='text/html; charset=utf-8'>
  <title>Basic Example Props</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/superagent/0.15.7/superagent.min.js"></script>
  <script type="text/jsx">

    var Avatar = React.createClass({
      propTypes: {
        id: React.PropTypes.string.isRequired,
        name: React.PropTypes.string.isRequired,
        width: React.PropTypes.number.isRequired,
        height: React.PropTypes.number.isRequired,
        initialLike: React.PropTypes.bool.isRequired,
        // Thêm Interface onDelete()
        onDelete: React.PropTypes.func.isRequired,
      },
      getInitialState() {
        return {
          liked: this.props.initialLike
        };
      },
      onClick() {
        this.setState({liked: !this.state.liked});
      },
      // Ủy quyền cho Component cha xử lý.
      _onDelete() {
        this.props.onDelete(this.props.id);
      },
      render() {
        var textLike = this.state.liked ? 'like' : 'haven\'t liked';
        return (
          <li key={this.props.id}>
            <span>{this.props.id}</span>
            <img  src={this.props.src} width={this.props.width} height={this.props.height} alt="alt" />
            <span>{this.props.name}</span>
            <button onClick={this.onClick}>{textLike}</button>
            <button onClick={this._onDelete}>Delete</button>
          </li>
        );
    }
    });
    var request = window.superagent;
    var Avatars = React.createClass({

      getInitialState() {
        return {
          avatars: [
            {id: 1, name: "Avatar 1", height: 100, width: 100, like: false, src: "http://canime.files.wordpress.com/2010/05/mask-dtb.jpg"},
            {id: 2, name: "Avatar 2", height: 100, width: 100, like: true, src: "http://z4.ifrm.com/30544/116/0/a3359905/avatar-3359905.jpg"},
            {id: 3, name: "Avatar 3", height: 100, width: 100, like: false, src: "http://www.dodaj.rs/f/O/IM/OxPONIh/134.jpg"}
          ]
        }
      },
      // Thêm method deleteItem() set lại State (chứa các Component con) cho Component cha này
      deleteItem(id) {
        this.setState({
          avatars: this.state.avatars.filter(function(avatar){
            return avatar.id !== id;
          })
        });
      },
      componentDidMount: function() {
        var self = this;
        request.get('http://localhost:3000/api/employees', function(res) {
        console.log(res);
          self.setState({avatars: res.body});
        });
      },

      render() {  
        var avatars = this.state.avatars.map(function(avatar){
        // use below solution
        // map(function(){},this) 
        // or map(function(){}.bind(this)) 
        // or var that = this; onDelete = {that.deleteUser}
        // to pass this value to map function.
        // bind onDelete (event) to deleteUser.
          return <Avatar onDelete={this.deleteItem} id={avatar.id} name={avatar.name} width={avatar.width} height={avatar.height} src={avatar.src} initialLike={avatar.like} />;
        }, this);
        return (
          <ul>
            {avatars}
          </ul>
        );
      }
    });

    var AvatarsComponent = React.render(<Avatars />, document.body);
  </script>
</body>
</html>

Server-side rendering

Server-side rendering thực chất là việc tạo ra và trả về browser 1 trang index.html hoàn chỉnh.

Server-side-rendering

Để thực hiện điều đó chúng ta cũng cần các bước trên, nhưng sẽ có một số biến đổi tương đượng như sau:

  1. File index.html
    Chúng ta sẽ dùng ngôn ngữ template để gắn ReactElement vào vị trí DOM cần thiết (vì ở server thì chưa có Real DOM để chúng ta get và gắn vào). Ở đây tôi dùng ngôn ngữ template là jade, cho nên chúng ta cũng sẽ có file index.jade. File này cũng chứa layout của View

  2. Include các file react.js, JSXTransformer.js
    Tôi dùng nodejs, nên tôi sẽ dùng method require() để include các package tương ứng.

  3. React.createClass() không thay đổi

  4. React.createElement() không thay đổi
  5. React.render() ở phía client có 2 ý nghĩa là render Virtual DOM thành HTML (hay real DOM) và gắn vào thẻ . Điều đó, phía server sẽ thực hiện một cách tương tự như sau:

    • Dùng React.renderToString. (Vui lòng tham khảo method này để hiểu thêm) để trả về một HTML string (chứa nội dung HTML mà ta muốn render).
    • Truyền HTML string đó vào thông qua biến !{markup}. Jade engine sẽ render ra index.html hoàn chỉnh
  6. Trả về cho Browser trang index.html vừa tạo ra ở trên.

Server-side-rendering-static

Source code:

https://github.com/phungnc/reactjs-tut-demo/tree/feature/server_side_rendering_static/server-rendering-static

Lưu ý: bạn hãy tự cài những module còn thiếu nhé, vì có những module tôi dùng như là global module nên khi viết source này tôi không có cài lại.

Sau khi bạn clone về, bạn start cái server bằng lệnh:

node server

và vào link:

http://localhost:5000

Boom! bạn đã có được một View giống như khi render ở phía client.
Tuy nhiên khi bạn thử click và button như like hay là delete thì chẳng có điều gì xảy ra. Tại sao vậy?

Tại vì thực ra server chỉ trả về cho Browser 1 file HTML tĩnh có “gắn” phần HTML vừa được React tạo ra vào thẻ <body> thôi chứ không có phần source code JS đi kèm theo. Điều này khác với việc render ở Client là bản thân souce code JS đã include sẵn. Cho nên bây giờ ta tiếp tục “trả” về cho client source code JS đó.

Để thực hiện điều này, tôi dùng browserify để “write code that uses require in the same way that you would use it in Node”. Cho nên chúng ta sẽ tận dụng cách viết ở phía Node Server cho phía Client. Browserify sẽ bundle up (tập hợp) tất cả các module cần thiết định nghĩa trong file mà chúng ta tạm gọi là browser.js thành file bundle.js và gửi cho Client.

Hình mình họa:

server-side rendering dynamic

Bây giờ, chúng ta sẽ tạo một file là browser.js để thông qua nó Browserify tạo file bundle.js:

'use strict';
var React   = require('react'),
    Avatar = require('./avatar');
React.render(<Avatar />, document.body);

Vì ở đây chúng ta dùng JSX syntax cho nên chúng ta cũng cần reactify để giúp Browserify transform JSX.

Bạn có thể thử command như bên dưới để thử bundle up file bundle.js từ file browser.js ở trên:

browserify -t reactify browser.js > bundle.js

Khi bạn mở file bundle.js, bạn sẽ thấy file bundle.js này đã gộp Reactjs và file avatar.js lại thành một.

OK, chúng ta đã có bundle.js giờ chỉ cần gửi file này về cho browser bằng cách thêm đoạn code sau vào bên dưới body trong file index.jade

    |     
    script(src='./bundle.js')

Và thêm dòng code sau app.use(Express.static(__dirname + '/')); vào bên dưới dòng app.set('view engine', 'Jade'); ở file server.js để báo cho express.static biết là nơi chứa các file static.

Bây giờ bạn reload lại page, thử click các button thử xem, It’s work!

Source code:

https://github.com/phungnc/reactjs-tut-demo/tree/feature/server_side_rendering_dynamic/server-rendering

Lưu ý: bạn hãy tự cài những module còn thiếu nhé, vì có những module tôi dùng như là global module nên khi viết source này tôi không có install vào.

Việc sử dụng Javascript ở cả server và client người ta gọi là Isomorphic JavaScript (Javascript đồng hình)

!= Nodejs

  1. react-php-v8js
    https://github.com/reactjs/react-php-v8js

  2. react-rails
    https://github.com/reactjs/react-rails

  3. React.NET
    https://github.com/reactjs/React.NET

  4. react-python
    https://github.com/reactjs/react-python