Một ứng dụng của prototype trong dự án Reactjs

Bài đã được updated, giải thích thêm về cách React xử lý prototype

Intro

Cách đây không lâu, dự án Homeup của chúng tôi, thuộc Basic Lab, đã rất mạnh dạn (giờ thì có chút hối hận :-s) áp dụng Reactjs và framework reflux. Vấn đề được gì và mất gì sau khoảng gần nửa năm làm reactjs sẽ không được nói đến ở đây, mà tôi đề cập về 1 bài học nhỏ hơn, khi áp dụng js prototype vào reactjs. Bài toán tôi phải giải quyết là thêm chức năng role-based authorization vào trang reactjs đã được xây dựng trong nửa năm qua (chắc cũng phải đến vài ngàn dòng code), logic khá phức tạp và code base lớn. Bước đầu tiên là search google xem có thư viện nào hay ho, hoặc tut nào áp dụng được không. Sau một hồi mò mẫm, chẳng có cái nào áp dụng được 😦, bài viết này thì giúp tôi có thêm ý tưởng nhưng nó lại khác dự án của chúng tôi ngay từ cách sử dụng reactjs (chúng tôi sử dụng nodejs để build ra file reactjs min, trong khi bài viết sử dụng react-rails gem!). Đành ngậm ngùi tự viết chức năng này từ đầu.

Để cho đơn giản thì coi như chúng ta có 2 roles: READABLE và WRITABLE. WRITABLE thì coi như trương hợp mặc định, vấn đề với READABLE là tôi sẽ phải chặn việc update các components, hiển thị thông báo lỗi, nếu chỗ nào có cho người dùng edit thì sau cùng sẽ không cho save và reset lại những sự thay đổi đó. Oke, đây là đoạn code thực thi:

var _current_role = "READ",
    _permision = {},
    UltiActions = require("./actions/UltiActions.js");

var Ability = {
  can: function(action, component) {
    if (typeof _permision[action] === "boolean"){
      return _permision[action];
    } else if (typeof _permision[action] === "object") {
      return _permision[action][component];
    }
    return false;
  },

  cannot: function(action, component){
    return !this.can(action, component);
  },

  setRole: function(role){
    if (role) {
      _current_role = role;
    }
    this.processRole();
  },

  processRole: function(){
    switch(_current_role) {
      case "READ":
        _permision = {
          read: true,
          update: false,
          drag: false
        }
        break;
      case "WRITE":
        _permision = {
          read: true,
          update: true,
          drag: true
        }
        break;
    }
  },

  currentRole: function(){
    return _current_role;
  },
}

module.exports = Ability;

Đến đây tôi chỉ việc import Ability và kiểm tra quyền cho từng chức năng, tuy nhiên, với hàng trăm methods, việc này chẳng khác gì tự sát nếu sau này có thay đổi (ví dụ đổi tên hàm cancannot). Giá mà có cái before_filter như của bên rails thì hay biết mấy. Lại mày mò google xem có cái lib nào tương tự before_filter hay gần giống không, nhưng no help *sign*. Vì bản chất các tương tác từ view truyền đến handler là event, và trước khi các event này được react truyền đến các handlers thì chúng ta phải đưa qua authorization filters. Tuy nhiên việc này dường như là không khả thi. Giải pháp chấp nhận được sau cùng là bằng cách nào đó, khi initialize các react components, chúng ta phải kiểm tra quyền ngay lập tức, nếu thoả mãn thì trả về handler mặc định, nếu không thì trả về một hàm thông báo lỗi.

Javascript Prototype

là một tính năng của javascript, được áp dụng trong implement inheritance trong javascript. Nói chung thì các hàm được định nghĩa trong class sẽ là immutable, qua đó được coi là "private", nhưng các hàm định nghĩa qua prototype có thể bị thay đổi trong runtime:

  Product = function(amount){
    this._amount = amount;
    this.price = function(){
      return this._amount * 1000;
    }
  }

  // giờ bạn không thể thay đổi được hàm `price`, đoạn code dưới sẽ không chạy
  Product.price = function(){
    return this_amount * 2000;
  }

  // nhưng nếu bạn định  nghĩa qua prototype thì moi chuyện hoàn toàn khác

  Product = function(amount){
    this._amount = amount;
  }

  Product.prototype.price = function(){
    return this._amount * 1000;
  }

  // Bạn có thể thay đổi được price

  Product.prototype.price = function() {
    return this._amount * 2000;
  }

  //hay thậm chí là xoá nó đi

  delete Product.prototype.price
  => true

React Authorization

Áp dụng prototype vào dự án của chúng tôi:

  var Ability = {
    ...
    applyPermissions: function(component, methods_to_check){
    var self = this;
    for (var key in component) {
      if (component.hasOwnProperty(key) && methods_to_check.indexOf(key) > -1) {
        if (self.cannot("update")) {
          delete component[key];
          component[key] = function(){
            UltiActions.ShowAlert({result: false, msg: "You are not allowed!"});
            return false;
          }
        }
      }
    }
  },
    ...
  }

Thay vì kiểm tra từng methods, tôi sẽ truyền vào 1 danh sách methods, loop qua nó và kiểm tra permission.

Đây là một component ví dụ:

...
Column.prototype.updateColumnWith = function() {
  ...
},

Column.prototype.deleteColumn = function(){
  ...
},

Column.prototype.handleDrop = function(prev_component_id, component) {
  ...
},

Column.prototype.saveColumn = function() {
  ...
},

methods_to_check = [
  "updateColumnWith",
  "deleteColumn",
  "handleDrop",
  "saveColumn"
]

Ability.applyPermissions(Column.prototype, methods_to_check);

Voila, vậy là cả 4 hàm đều đã được chạy qua bước filter permission!

Proxy

Còn một vấn đề nhỏ nữa, chúng tôi sử dụng react Drag and Drop để implement chức năng kéo thả vào trong page, nên tại hàm render, chúng ta sẽ có những connector như:

  return connectDragSource(
    //nội dung render
  );

nếu dùng if-else chúng ta lại lặp code, khi chỉ có thay đổi đúng 1 chỗ là dùng connectDragsource hoặc không, nên sau cùng chúng ta có thêm một hàm proxy sau:

  //Ability file
  sourceProxy: function(func){
    return func;
  },
  // Component
  render: {
    ...
    if (Ability.can("update")) {
      connectDragSource = // normal implementation
    } else {
      connectDragSource = Ability.sourceProxy;
    }
  }

Đến đây tôi chợt nghĩ nếu mình có thể change prototype của thư viện Drag and Drop thì hàm proxy không cần phải để ở Ability và cách xử lý đoạn render sẽ đơn giản hơn nhiều. Nhưng tôi sẽ tìm hiểu và cập nhật sau.


updated:

Tôi đã giành khá nhiều thời gian để đọc tìm hiểu cách Facebook implement hàm createClass. Trái với nhận định trước đo của, React trả về 1 Constructor, và bản thân nó đã đưa các hàm chúng ta định nghĩa vào Constructor.prototype rồi, thông qua arg spec. Điều đó làm tôi có ý tưởng ko cần phải chuyển các hàm ra bên ngoài hàm createClass và gán tường minh cho prototype như tôi đã làm bên trên. Nên tôi đã thử:

...
methods = [...]
Ability.applyPermissions(ClassName, methods);

Nhưng fail! nghĩ mãi, nghĩ mãi cũng ko hiểu vì sao, lại phải đọc lại source của react và tại đây các hàm được định nghĩa trong createClass được đưa vào 1 biến là autoBindPairs, khi được initialize các hàm đó được gán trở lại component, khiến cho việc applyPermissions của chúng ta trở lên vô nghĩa.

Do đó chỉ có 1 cách duy nhất là định nghĩa tường mình các hàm cần authorized ở bên ngoài reactClass và gán nó vào prototype 😉



All Rights Reserved