Работа с данными в JavaScript

в 8:39, , рубрики: javascript, архитектура web приложений, метки:

В этой публикации хоче поделиться одним из способов работы с данными. Началось все с того, что мне дали задачу сделать календарь, в котором каждая ячейка представляет собой некоторую форму, причем в зависимости от выбора пользователя. То есть, генерация шаблона ячейки должна была происходить на стороне клиента, в зависимости от выбора разных полей. Сложность заключалась в том, что поля каждой формы можно редактировать, а значит нужно где-то хранить все изменения.

От создания моделей на каждую форму я сразу отказался, поскольку при изменении одного поля, должно было каким-то образом меняться другое. Логика взаимодействия полей распространялась на 3 таблицы базы данных, из которых 2 хотя бы на половину заполнены данными и одна, в которую мне нужно передавать изменения. Поэтому я решил сделать собственное хранилище (навроде Store в Ext js). Суть заключалась в том, чтобы представить каждый узел объекта формата JSON в самостоятельную единицу. То есть принцип следующий: я получаю все 3 таблицы и создаю 3 дерева указателей, где каждый родитель является контекстом дочернего узла. Возможно, звучит, немного запутанно. Вот пример.

Заказчики

var Customers = [
  {
    "FirstName": "Customer first name 1",
    "LastName": "Customer second name 1",
    "Id": "1",
    "JobSpecificsList": [
      {
        "Id": "1",
        "Name": "Orders group 1"
      },
      {
        "Id": "2",
        "Name": "Orders group 2"
      }
    ],
    "WorkTypes": [
      "2",
      "0",
      "1"
    ],
    "PaymentTypes": [
      "1",
      "2",
      "3"
    ]
  },
  {
    "FirstName": "Customer first name 2",
    "LastName": "Customer second name 2",
    "Id": "2",
    "JobSpecificsList": [
      {
        "Id": "1",
        "Name": "Orders group 1"
      },
      {
        "Id": "61",
        "Name": "Orders group 3"
      },
      {
        "Id": "58",
        "Name": "Orders group 4"
      }
    ],
    "WorkTypes": [
      "2",
      "0"
    ],
    "PaymentTypes": [
      "1"
    ]
  }
]

Заказы

var orders = [
  {
    "Id":"1",
    "CustomerId": "1",
    "OrderName": "Order 1",
    "Start": "2015-04-12T11:22:00.0000000",
    "End": "2015-04-12T22:11:00.0000000"
  },
  {
    "Id":"2",
    "CustomerId": "2",
    "OrderName": "Order 2",
    "Start": "2015-04-12T11:22:00.0000000",
    "End": "2015-04-12T22:11:00.0000000"
  },
  {
    "Id":"3",
    "CustomerId": "1",
    "OrderName": "Order 3",
    "Start": "2015-04-12T11:22:00.0000000",
    "End": "2015-04-05T22:11:00.0000000"
  }
]

Расписание

var schedule = [
  {
    "CustomerId": "1",
    "StartWeek": "2015-04-13T00:00:00.000Z",
    "Schedule": [
      {
        "Id": "-1",
        "Start": "2015-04-15T17:00:00+06:00",
        "End": "2015-04-15T17:30:00+06:00",
        "WorkType": "0"
        "JobSpecificId": "-1",
        "PaymentType": "-1",
      },
      {
        "Id": "-1",
        "Start": "2015-04-16T19:00:00+06:00",
        "End": "2015-04-16T20:30:00+06:00",
        "WorkType": "2",
        "JobSpecificId": "-1",
        "PaymentType": "-1"
      },
      {
        "Id": "-1",
        "Start": "2015-04-19T00:00:00+06:00",
        "End": "2015-04-19T00:10:00+06:00",
        "WorkType": "-1",
        "JobSpecificId": "-1",
        "PaymentType": "-1"
      }
    ]
  },
  {
    "CustomerId": "2",
    "StartWeek": "2015-04-13T00:00:00.000Z",
    "Schedule": [
      {
        "Id": "-1",
        "Start": "2015-04-13T00:00:00+06:00",
        "End": "2015-04-13T00:00:00+06:00",
        "WorkType": "-1",
        "JobSpecificId": "-1",
        "PaymentType": "-1"
      }
    ]
  },
  {
    "CustomerId": "25",
    "StartWeek": "2015-04-13T00:00:00.000Z",
    "Schedule": [
      {
        "Id": "-1",
        "Start": "2015-04-19T00:00:00+06:00",
        "End": "2015-04-19T00:00:00+06:00",
        "WorkType": "-1",
        "JobSpecificId": "-1",
        "PaymentType": "-1"
      }
    ]
  }
]

Есть список заказчиков, список заказов и расписание отправки. В календаре происходит редактирование расписания. То есть курьер может просматривать назначенные на него заказы. В расписание так же можно добавить/удалить курьера. Добавление или удаление происходит понедельно.

Реализация

Объект Store отвечает за хранение, получение и отправку данных на сервер. Реализация примерно такая:

var Store = function(){
    var wrapSchedule= undefined;
    …
    this.loadScheduleList = function(){
        ...
        wrapSchedule = inMemory(serverResponse);
    };
    …
    this.getScheduleList = function(){ return wrapSchedule };
    …
    this.saveScheduleList = function(){
        var clientResponse= dumpAcc(wrapSchedule);
        ...
    };
}

Функция InMemory как раз занимается тем, что приводит объект или массив, полученный с сервера в дерево указателей:

Функция inMemory

   function inMemory(from) {
            return (function getNode() {
                var that = this;

                function facade(innerValue) {
                    function facadeArray(_innerValue) {
                        return (function(context) {
                            return {
                                get: function() {
                                    return context;
                                },
                                set: function(value) {
                                    context = clone(value);
                                },
                                indexOf: function(index) {
                                    return context[index];
                                },
                                push: function (v) {
                                    context.push(getNode.call(clone(v)));
                                },
                                splice: function(index, count) {
                                    context.splice(index, count);
                                },
                                orderBy: function (comparator) {
                                    context.sort(comparator);
                                },
                                filter: function (predicate) {
                                    return facadeArray(context.filter(predicate));
                                }
                            }
                        })(_innerValue);
                    }

                    function facadeObject(_innerValue) {
                        return (function(context) {
                            return {
                                get: function() {
                                    return context;
                                },
                                set: function(value) {
                                    context = clone(value);
                                },
                                update: function(obj) {
                                    var keys = Object.keys(obj);
                                    for (var prop_i = 0; prop_i < keys.length; prop_i++) {
                                        if (context[keys[prop_i]]) {
                                            context[keys[prop_i]] = clone(obj[keys[prop_i]]);
                                        }
                                    }
                                },
                                append: function(obj) {
                                    var keys = Object.keys(obj);
                                    for (var prop_i = 0; prop_i < keys.length; prop_i++) {
                                        context[keys[prop_i]] = clone(obj[keys[prop_i]]);
                                    }
                                }
                            }
                        })(_innerValue);
                    }

                    function facadePrimitive(_innerValue) {
                        return (function(context) {
                            return {
                                get: function() {
                                    return context;
                                },
                                set: function(value) {
                                    context = value;
                                }
                            }
                        })(_innerValue);
                    }

                    if (innerValue instanceof Array) {
                        return facadeArray(innerValue);
                    }

                    if (isPrimitive(innerValue)) {
                        return facadePrimitive(innerValue);
                    }

                    return facadeObject(innerValue);
                }

                if (that instanceof Array) {
                    for (var i = 0; i < that.length; i++) {
                        if (!isPrimitive(that[i]))
                            that[i] = getNode.call(that[i]);
                        else {
                            that[i] = facade(that[i]);
                        }
                    }
                }
                if (!(that instanceof Array)) {
                    for (var prop_i = 0; prop_i < Object.keys(that).length; prop_i++) {
                        var field = that[Object.keys(that)[prop_i]];
                        if (!isPrimitive(field)) {
                            that[Object.keys(that)[prop_i]] = getNode.call(field);
                        } else {
                            that[Object.keys(that)[prop_i]] = facade(field);
                        }
                    }
                }
                that = facade(that);
                return that;
            }).call(from);
    };

Доступ к каждому уровню осуществляется через get, set или любую другую функцию узла. Например в facadeArray можно реализовать метод first, тогда получение расписания определенного курьера за определенную неделю выглядело бы так:

var firstOnWeekPredicate = function(x){
    return x.get().CustomerId.get() == "1" && x.get().StartWeek.get() == '2015-04-13T00:00:00.000Z';
}

var scheduleByСourierIdAndWeek =  store.getScheduleList().get().first(firstOnWeekPredicate).get().Schedule

Для тех, кто еще не знает про такие удобные функции как where, first, last, union, map и т.д., советую посетить underscorejs.org

Теперь во внутреннее значение (массив) scheduleByСourierIdAndWeek можно добавить новый день недели или изменить существующий.

Соответственно:

scheduleByСourierIdAndWeek.push({
    "Id": "-1",
    "Start": "2015-04-15T00:00:00+06:00",
    "End": "2015-04-15T00:00:00+06:00",
    "WorkType": "0"
    "JobSpecificId": "-1",
    "PaymentType": "-1"
})

scheduleByСourierIdAndWeek.get()[0].update({WorkType: 2})

Весь «фасад» в функции getNode может быть переработан под собственные нужды.

Ну и что же мы получили в итоге? Мы разделили большой объект, полученный с сервера на связанные узлы. Раздали узлы в участки кода, где с ними происходит работа. Преобразовали данные так, как захотел пользователь. Теперь нужно все это собрать в объект и отправить обратно на сервер. Для этого есть функция dumpAcc.

   function dumpAcc(storeAcc) {
        return clone((function getNodeValue(acc) {
            if (typeof (acc.get ? acc.get() : acc) === "boolean"
                || typeof (acc.get ? acc.get() : acc) === "number"
                || typeof (acc.get ? acc.get() : acc) === "string") {
                return acc.get ? acc.get() : acc;
            }
            else if (acc.get() instanceof Array) {
                var _a = [];
                for (var _a_i = 0; _a_i < acc.get().length; _a_i++) {
                    _a.push(getNodeValue(acc.get()[_a_i]));
                }
                return _a;
            } else {
                var _o = {};
                for (var prop in acc.get()) {
                    if (acc.get().hasOwnProperty(prop))
                        _o[prop] = getNodeValue(acc.get()[prop]);
                }
                return _o;
            }
        })(storeAcc));
    }

То есть для того, чтобы получить clientResponse, нужно выполнить JSON.stringify(dumpAcc(store.getScheduleList())).
И напоследок функции clone, isPrimitive, которые я использовал.

    function clone(a) {
        return JSON.parse(JSON.stringify(a));
    }

    function isPrimitive(value) {
        return typeof (value) === "string" || typeof (value) === "number" || typeof (value) === "boolean";
    }

Автор: Karkat

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js