Статьи

Создание приложения списка Todo с Node.js и Geddy

В этом уроке из трех частей мы углубимся в создание приложения для управления списками дел в Node.js и Geddy . Это вторая часть серии, в которой мы создадим простое приложение для управления списками.


В качестве быстрого обновления, в прошлый раз мы установили Node и Geddy, сгенерировали новое приложение и узнали, как запустить сервер. В этом уроке мы будем опираться на то, что мы делали в прошлый раз, поэтому убедитесь, что вы его завершили, прежде чем продолжить.


У Гедди есть встроенный генератор ресурсов; это позволит нам автоматически генерировать модель, контроллер, представления и маршруты для конкретного ресурса. В нашем списке дел будет только один ресурс: todo . Чтобы сгенерировать его, просто cd в каталог вашего приложения ( cd path/to/your/todo_app ) и запустите:

1
geddy resource todo

Теперь вы должны добавить эти файлы в ваше приложение:

  • приложение / модели / todo.js
  • приложение / контроллеры / todos.js
  • приложение / просмотров / Todos /
    • index.html.ejs
    • show.html.ejs
    • edit.html.ejs
    • add.html.ejs

Ваш config/router.js также должен иметь следующее:

1
router.resource(‘todos’);

Если вы новичок в MVC, все это может показаться вам немного сложным. Не волнуйтесь, это действительно просто, как только вы это выясните.

models / todo.js : в этом файле мы определим нашу модель todo . Мы определим ряд свойств, которые есть во всех todo . Мы также напишем некоторые проверки данных здесь.

controllers / todos.js : в этом файле заканчиваются все /todos/ маршруты. Каждое действие в этом контроллере имеет соответствующий маршрут:

1
2
3
4
5
6
7
GET /todos/ => index
POST /todos/ => create
GET /todos/:id => show
PUT /todos/:id => update
DELETE /todos/:id => remove
GET /todos/:id/add => add
GET /todos/:id/edit => edit

views / todos / : Каждый файл здесь соответствует одному из маршрутов GET который мы показали вам выше. Это шаблоны, которые мы используем для создания внешнего интерфейса приложения. Гедди использует EJS (встроенный JavaScript) в качестве языка шаблонов. Это должно выглядеть знакомо, если вы когда-либо использовали PHP или ERB. По сути, вы можете использовать любой JavaScript, который вы хотите в своих шаблонах.

Теперь, когда мы сгенерировали кучу кода, давайте проверим, что у нас есть все маршруты, которые нам нужны. Запустите приложение снова ( geddy ) и укажите в браузере http: // localhost: 4000 / todos. Вы должны увидеть что-то вроде этого

Попробуйте и для других маршрутов GET :

  • HTTP: // локальный: 4000 / Todos / что-то
  • HTTP: // локальный: 4000 / Todos / добавить
  • HTTP: // локальный: 4000 / Todos / что-то / редактировать

Все хорошо? Хорошо, давайте продолжим.


В Geddy (и большинстве других сред MVC) вы используете модели для определения типа данных, с которыми будет работать ваше приложение. Мы только что сгенерировали модель для наших todo , поэтому давайте посмотрим, что это дало нам:

1
2
3
4
5
6
7
var Todo = function () {
  // Some commented out code
};
 
// Some more commented out code
 
Todo = geddy.model.register(‘Todo’, Todo);

Модели довольно просты в Geddy. Мы просто создаем новую функцию конструктора для наших todo и регистрируем ее как модель в geddy. Давайте определим некоторые свойства для наших todo . Удалите весь закомментированный код и добавьте его в функцию contructor:

1
2
3
4
5
6
7
var Todo = function () {
  this.defineProperties({
    title: {type: ‘string’, required: true}
  , id: {type: ‘string’, required: true}
  , status: {type: ‘string’, required: true}
  });
};

Наши todo будут иметь заголовок, идентификатор и статус, и все три будут необходимы. Теперь давайте установим некоторые проверки для наших todo .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
var Todo = function () {
 
  this.defineProperties({
    title: {type: ‘string’, required: true}
  , id: {type: ‘string’, required: true}
  , status: {type: ‘string’, required: true}
  });
 
  this.validatesPresent(‘title’);
  this.validatesLength(‘title’, {min: 5});
 
  this.validatesWithFunction(‘status’, function (status) {
    return status == ‘open’ ||
  });
 
};

Мы проверяем, присутствует ли заголовок, имеет ли он минимальную длину 5 символов, и мы используем функцию для проверки того, что статус open или done . Есть довольно много встроенных функций проверки, попробуйте и проверьте проект на http://github.com/mde/geddy, чтобы узнать больше о них.


Теперь, когда мы настроили нашу модель todo, мы можем создать место для хранения наших моделей. Для целей этого урока мы просто будем хранить данные в памяти. Мы geddy массив todos к нашему глобальному объекту geddy чтобы вставить данные. В следующей части этой серии мы начнем geddy их в базе данных.

Откройте файл config/init.js Все, что должно быть там сейчас, — это глобальный обработчик необработанных исключений:

1
2
3
4
5
6
// Add uncaught-exception handler in prod-like environments
if (geddy.config.environment != ‘development’) {
  process.addListener(‘uncaughtException’, function (err) {
    geddy.log.error(JSON.stringify(err));
  });
}

Сразу после этого блока кода давайте geddy наш массив с глобальным geddy :

1
geddy.todos = [];

Там, теперь у нас есть место для хранения наших todo . Помните, что это в вашей памяти приложений, поэтому оно исчезнет, ​​когда вы перезапустите сервер.

Модель-адаптер обеспечивает базовое save , remove , load и all методы, необходимые модели. Наш источник данных довольно прост (просто массив!), Поэтому написание нашего адаптера модели также должно быть довольно простым.

Создайте каталог в lib именем model_adapters и создайте файл в lib/model_adapters именем todo.js Давайте откроем этот файл и добавим некоторый шаблонный код:

1
2
3
var Todo = new (function () {
})();
exports.Todo = Todo;

Все, что мы здесь делаем, это устанавливаем новый пустой объект для экспорта во все, что в итоге требует этот файл. Если вы хотите узнать немного больше о том, как работает метод require в Node, у этой статьи есть довольно хороший обзор. В этом случае наш файл init.js выполнит необходимые требования.

Итак, мы создали новый объект-адаптер Todo. Сейчас это довольно бесплодно, но мы скоро к этому вернемся. Сейчас нам нужно вернуться к init.js и добавить некоторый код, чтобы он загружался в наше приложение при запуске. После geddy.todos = []; в config/init.js добавьте эти две строки:

1
2
geddy.model.adapter = {};
geddy.model.adapter.Todo = require(process.cwd() + ‘/lib/model_adapters/todo’).Todo;

Мы создали пустой объект модели-адаптера и добавили к нему адаптер модели Todo.


Теперь, когда у нас есть модель и адаптер модели, мы можем начать с логики приложения. Давайте начнем с добавления элементов в список дел.

При работе с данными, первое место, на которое вы должны пойти — это адаптер модели. Нам нужно сохранить экземпляр нашей модели Todo в нашем массиве geddy.todos. Итак, откройте lib/model_adapters/todo.js и добавьте метод сохранения:

01
02
03
04
05
06
07
08
09
10
11
12
13
var Todo = new (function () {
  this.save = function (todo, opts, callback) {
 
    if (typeof callback != ‘function’) {
      callback = function(){};
    }
 
    todo.saved = true;
    geddy.todos.push(todo);
    return callback(null, todo);
 
  }
})();

Все, что нам нужно сделать, это установить для сохраненного свойства экземпляра значение true и вставить элемент в массив geddy.todos. В Node лучше выполнять все операции ввода-вывода неблокирующим образом, поэтому рекомендуется использовать обратные вызовы для передачи данных. Для этого урока это не так важно, но позже, когда мы начнем сохранять вещи, это пригодится. Вы заметите, что мы убедились, что обратный вызов является функцией. Если мы не сделаем это и используем сохранение без обратного вызова, мы получим ошибку. Теперь давайте перейдем к действию контроллера.

app/controllers/todos.js посмотрим на действие create в app/controllers/todos.js :

1
2
3
4
this.create = function (req, resp, params) {
  // Save the resource, then display index page
  this.redirect({controller: this.name});
};

Довольно просто, правда? Гедди заглушил это для тебя. Итак, давайте немного его изменим:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
this.create = function (req, resp, params) {
  var self = this
    , todo = geddy.model.Todo.create({
        title: params.title
      , id: geddy.string.uuid(10)
      , status: ‘open’
      });
  todo.save(function (err, data) {
    if (err) {
      params.errors = err;
      self.transfer(‘add’);
    }
    else {
      self.redirect({controller: self.name});
    }
  });
};

Сначала мы создаем новый экземпляр модели Todo с помощью geddy.model.Todo.create , передавая заголовок, который наша форма отправит нам, и устанавливая значения по умолчанию для идентификатора и статуса.

Затем мы вызываем метод save, который мы создали для адаптера модели, и перенаправляем пользователя обратно на маршрут / todos. Если он не прошел проверку или мы получили ошибку, мы используем метод переноса контроллера для передачи запроса обратно в действие add .

Теперь пришло время настроить шаблон добавления. Взгляните на app/views/todos/add.html.ejs , это должно выглядеть так:

1
2
3
4
5
6
7
8
<div class=»hero-unit»>
  <h3>Params</h3>
  <ul>
  <% for (var p in params) { %>
    <li><%= p + ‘: ‘ + params[p];
  <% } %>
  </ul>
</div>

Нам это не понадобится

    для нашего варианта использования, так что давайте пока избавимся от него. Сделайте так, чтобы ваш add.html.ejs выглядел так:

    1
    2
    3
    <div class=»hero-unit»>
      <%= partial(‘_form’, {params: params});
    </div>

    Частичные функции упрощают обмен кодами между шаблонами.

    Вы заметите, что мы используем частичное в этом шаблоне. Частичные функции упрощают обмен кодами между шаблонами. Наши шаблоны добавления и редактирования будут использовать одну и ту же форму, поэтому давайте создадим эту форму частично. Создайте новый файл в каталоге views/todos/ именем _form.html.ejs . Мы используем подчеркивание, чтобы легко определить, является ли этот шаблон частичным. Откройте его и добавьте в этот код:

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    <%
      var isUpdate = params.action == ‘edit’
        , formTitle = isUpdate ?
        , action = isUpdate ?
        , deleteAction = isUpdate ?
        , btnText = isUpdate ?
        , doneStatus = isUpdate ?
        , titleValue = isUpdate ?
        , errors = params.errors;
    %>
    <form id=»todo-form» class=»form-horizontal» action=»<%= action %>» method=»POST»>
      <fieldset>
        <legend><%= formTitle %></legend>
        <div class=»control-group»>
          <label for=»title» class=»control-label»>Title</label>
          <div class=»controls»>
            <input type=»text» class=»span6″ placeholder=»enter title» name=»title» value='<%= titleValue %>’/>
            <% if (errors) { %>
              <p>
              <% for (var p in errors) { %>
                <div><%= errors[p];
              <% } %>
              </p>
            <% } %>
          </div>
        </div>
        <% if (isUpdate) { %>
          <div class=»control-group»>
            <label for=»status»>Status</label>
            <div class=»controls»>
              <select name=»status»>
                <option>open</option>
                <option>done</option>
              </select>
            </div>
          </div>
        <% } %>
        <div class=»form-actions»>
          <input type=»submit» class=»btn btn-primary» value=»<%= btnText %>»/>
          <% if (isUpdate) { %>
            <button type=»submit» formaction=»<%= deleteAction %>» formmethod=»POST» class=»btn btn-danger»>Remove</button>
          <% } %>
        </div>
      </fieldset>
    </form>

    Вау, там много кода! Посмотрим, сможем ли мы пройти через это. Так как два разных шаблона будут использовать этот частичный элемент, мы должны убедиться, что форма выглядит правильно в обоих. Большая часть этого кода на самом деле является образцом из Twitter Bootstrap. Это то, что позволяет этому приложению выглядеть так хорошо сразу же (и на мобильных устройствах тоже!).

    Чтобы приложение выглядело еще лучше, вы можете использовать CSS-файл, предоставленный при загрузке демонстрационного приложения.

    Первое, что мы сделали, — это установили некоторые переменные, которые мы могли бы использовать. В действии add мы передаем объект params в шаблон при вызове метода reply. Это дает нам несколько вещей — это говорит нам, на какой контроллер и действие был направлен этот запрос, и дает нам любые параметры запроса, которые были переданы в URL. Мы устанавливаем переменную isUpdate чтобы увидеть, в данный момент мы находимся на действии обновления, а затем мы настраиваем еще несколько переменных, чтобы помочь очистить наш код представления.

    Оттуда все, что мы сделали, это сделали форму. Если мы находимся на действии добавления, мы просто визуализируем форму как есть. Если мы находимся на действии редактирования, мы заполняем форму, чтобы позволить пользователю обновить поля.

    Обратите внимание, что форма отправит запрос POST в /todos/ с параметром _method=PUT . Geddy использует стандартный параметр переопределения метода, чтобы вы могли отправлять запросы PUT и DELETE из браузера без использования JavaScript. (по крайней мере на переднем конце!)

    Последняя маленькая деталь, на которую мы должны обратить внимание, это кнопка «Удалить». Мы используем атрибут formaction html5, чтобы изменить действие для этой формы. Вы заметите, что formaction этой кнопки отправляет запрос POST до маршрута /todos/:id с параметром _method=DELETE . Это ударит действие remove на контроллере, к которому мы вернемся позже.

    Перезагрузите ваш сервер ( geddy ) и посетите http: // localhost: 4000 / todos / add, чтобы увидеть ваш шаблон в действии. Создайте элемент To Do, пока вы на нем.


    Теперь, когда у нас есть пользовательский ввод элементов To Do, добавляемых в наш массив geddy.todos, мы, вероятно, должны где-то перечислить их. Давайте начнем со all метода в модели-адаптере.

    Давайте снова откроем lib/model_adapters/todo.js и добавим all method right above the методом save`:

    1
    2
    3
    this.all = function (callback) {
      callback(null, geddy.todos);
    }

    Вероятно, это самый простой метод адаптера модели, который мы создадим сегодня. Все, что он делает, это принимает обратный вызов и вызывает его с ошибкой (которая сейчас всегда равна нулю, мы обновим этот метод в следующем уроке), и geddy.todos .

    /app/controllers/todos.js откройте /app/controllers/todos.js и посмотрите на действие index . Это должно выглядеть примерно так:

    1
    2
    3
    this.index = function (req, resp, params) {
      this.respond({params: params});
    };

    Эта часть очень проста, мы просто используем метод all который мы только что определили на модели-адаптере, чтобы получить все todo и отобразить их:

    1
    2
    3
    4
    5
    6
    this.index = function (req, resp, params) {
      var self = this;
      geddy.model.adapter.Todo.all(function(err, todos){
        self.respond({params: params, todos: todos});
      });
    };

    Вот и все для контроллера, теперь на виду.

    Посмотрите на /app/views/todos/index.html.ejs, это должно выглядеть так:

    1
    2
    3
    4
    5
    6
    7
    8
    <div class=»hero-unit»>
      <h3>Params</h3>
      <ul>
      <% for (var p in params) { %>
        <li><%= p + ‘: ‘ + params[p];
      <% } %>
      </ul>
    </div>

    Очень похоже на то, что шаблон add.html.ejs не так. Опять же, здесь нам не понадобится шаблон параметров, поэтому возьмите это и сделайте так, чтобы ваш шаблон index.html.ejs выглядел так:

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    <div class=»hero-unit»>
      <h2>To Do List</h2>
      <a href=»/todos/add» class=»btn pull-right»>Create a new To Do</a></p>
    </div>
    <% if (todos &amp;&amp; todos.length) { %>
      <% for (var i in todos) { %>
      <div class=»row todo-item»>
        <div class=»span8″><h3><a href=»/todos/<%= todos[i].id; %>/edit»><%= todos[i].title;
        <div class=»span4″><h3><i class=»icon-list-alt»></i><%= todos[i].status;
      </div>
      <% } %>
    <% } %>

    Это тоже довольно просто, но на этот раз у нас есть цикл в нашем шаблоне. В заголовке мы добавили кнопку для добавления новых задач. Внутри цикла мы генерируем строку для каждой todo , отображая ее заголовок (как ссылку на страницу edit ) и его статус.

    Чтобы проверить это, перейдите по адресу http: // localhost: 4000 / todos .


    Теперь, когда у нас есть ссылка на страницу edit , мы, вероятно, должны заставить ее работать!

    Снова /lib/model_adapters/todo.js адаптер модели ( /lib/model_adapters/todo.js ). Мы собираемся добавить метод load чтобы мы могли загрузить конкретную todo и использовать ее на нашей странице редактирования. Неважно, куда вы его добавляете, но пока давайте поместим его между методом all методом save :

    1
    2
    3
    4
    5
    6
    7
    8
    this.load = function (id, callback) {
      for (var i in geddy.todos) {
        if (geddy.todos[i].id == id) {
          return callback(null, geddy.todos[i]);
        }
      }
      callback({message: «To Do not found»}, null);
    };

    Этот метод загрузки принимает идентификатор и обратный вызов. Он просматривает элементы в geddy.todos и проверяет, совпадает ли id текущего элемента с переданным в id . Если это так, он вызывает обратный вызов, передавая элемент todo обратно. Если он не находит соответствия, он вызывает обратный вызов с ошибкой. Теперь нам нужно использовать этот метод в действии show контроллера todos.

    Снова откройте свой контроллер todos и посмотрите на его действие edit . Это должно выглядеть примерно так:

    1
    2
    3
    this.edit = function (req, resp, params) {
      this.respond({params: params});
    };

    Давайте используем метод load, который мы только что создали:

    1
    2
    3
    4
    5
    6
    this.edit = function (req, resp, params) {
      var self = this;
      geddy.model.Todo.load(params.id, function(err, todo){
        self.respond({params: params, todo: todo});
      });
    };

    Все, что мы делаем здесь, это загружаем todo и отправляем его в шаблон для визуализации. Итак, давайте посмотрим на шаблон.

    Откройте /app/views/todos/edit.html.ejs . Еще раз, нам не понадобится шаблон параметров, поэтому давайте удалим его. Сделайте так, чтобы ваш edit.html.ejs выглядел так:

    1
    2
    3
    <div class=»hero-unit»>
      <%= partial(‘_form’, {params: params, todo: todo});
    </div>

    Это должно выглядеть очень похоже на файл add.html.ejs который мы только что отредактировали. Вы заметите, что на этот раз мы отправляем объект todo как частичному, так и params. Круто то, что, поскольку мы уже написали партию, это все, что нам нужно сделать, чтобы страница редактирования отображалась правильно.

    Перезагрузите сервер, создайте новую todo и нажмите на ссылку, чтобы увидеть, как это работает. Теперь давайте заставим эту кнопку обновления работать!

    Снова откройте модель-адаптер и найдите способ save . мы собираемся добавить немного к нему, чтобы мы могли сохранить поверх существующих todo . Сделайте так, чтобы это выглядело так:

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    15
    16
    17
    this.save = function (todo, opts, callback) {
      if (typeof callback != ‘function’) {
        callback = function(){};
      }
      var todoErrors = null;
      for (var i in geddy.todos) {
        // if it’s already there, save it
        if (geddy.todos[i].id == todo.id) {
          geddy.todos[i] = todo;
          todoErrors = geddy.model.Todo.create(todo).errors;
          return callback(todoErrors, todo);
        }
      }
      todo.saved = true;
      geddy.todos.push(todo);
      return callback(null, todo);
    }

    Это перебирает все geddy.todos в geddy.todos и, если id уже существует, заменяет эту todo новым экземпляром todo . Мы делаем некоторые вещи здесь, чтобы убедиться, что наши проверки работают как на обновление, так и на создание — для этого нам нужно извлечь свойство errors из нового экземпляра модели и передать его обратно в обратном вызове. Если он прошел валидацию, он просто будет неопределенным, и наш код его проигнорирует. Если это не прошло, todoErrors будет массивом ошибок проверки.

    Теперь, когда у нас это есть, давайте поработаем над действием update нашего контроллера.


    Затем снова откройте контроллер и найдите действие «обновление», оно должно выглядеть примерно так:

    1
    2
    3
    4
    this.update = function (req, resp, params) {
      // Save the resource, then display the item page
      this.redirect({controller: this.name, id: params.id});
    };

    Вы захотите отредактировать его так, чтобы оно выглядело так:

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    15
    16
    this.update = function (req, resp, params) {
      var self = this;
      geddy.model.adapter.Todo.load(params.id, function (err, todo) {
        todo.status = params.status;
        todo.title = params.title;
        todo.save(function (err, data) {
          if (err) {
            params.errors = err;
            self.transfer(‘edit’);
          }
          else {
            self.redirect({controller: self.name});
          }
        });
      });
    };

    Здесь мы загружаем запрошенную todo , редактируем некоторые ее свойства и снова сохраняем todo . Код, который мы только что написали в модели-адаптере, должен обрабатывать все остальное. Если мы получим ошибку назад, это означает, что новые свойства не прошли проверку, поэтому мы передадим запрос обратно в действие edit . Если мы не вернули ошибку, мы просто перенаправим запрос обратно на действие index .

    Идите и попробуйте. Перезапустите сервер, создайте новую todo , нажмите на ссылку для ее редактирования, измените статус на done и убедитесь, что он обновлен в index . Если вы хотите убедиться, что у вас работают проверки, попробуйте изменить title на более чем 5 символов.

    Теперь давайте сделаем так, чтобы кнопка «Удалить» работала.


    К настоящему времени у нас есть приложение для работы со списком работ, но если вы начнете использовать его некоторое время, будет сложно найти todo элемент на этой странице индекса. Давайте сделаем так, чтобы кнопка «Удалить» работала, чтобы мы могли сохранить наш список красивым и коротким

    Давайте снова откроем нашу модель-адаптер, на этот раз мы захотим добавить туда метод remove . Добавьте это сразу после метода save :

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    this.remove = function(id, callback) {
      if (typeof callback != ‘function’) {
        callback = function(){};
      }
      for (var i in geddy.todos) {
        if (geddy.todos[i].id == id) {
          geddy.todos.splice(i, 1);
          return callback(null);
        }
      }
      return callback({message: «To Do not found»});
    }

    Этот довольно простой, он должен выглядеть как метод загрузки. Он просматривает все geddy.todos в geddy.todos чтобы найти id который мы ищем. Затем он склеивает этот элемент из массива и вызывает обратный вызов. Если он не находит его в массиве, он вызывает обратный вызов с ошибкой.

    Давайте использовать это в нашем контроллере сейчас.

    Снова откройте свой контроллер и нажмите « remove . Это должно выглядеть примерно так:

    1
    2
    3
    this.remove = function (req, resp, params) {
      this.respond({params: params});
    };

    Отредактируйте его, чтобы он выглядел так:

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    this.remove = function (req, resp, params) {
      var self = this;
      geddy.model.adapter.Todo.remove(params.id, function(err){
        if (err) {
          params.errors = err;
          self.transfer(‘edit’);
        }
        else {
          self.redirect({controller: self.name});
        }
      });
    }

    Мы передаем id который мы получили из параметров в записи формы, в метод remove который мы только что создали. Если мы получим ошибку назад, мы перенаправим обратно к действию edit (мы предполагаем, что в форме размещена неверная информация). Если мы не вернули ошибку, просто отправьте запрос в действие index .

    Это оно! Были сделаны.

    Вы можете протестировать функцию удаления, перезапустив сервер, создав новый элемент todo , щелкнув по его ссылке, а затем нажав кнопку «Удалить». Если вы все сделали правильно, вы должны вернуться на страницу индекса с удаленным этим элементом.


    В следующем уроке мы будем использовать потрясающий модуль mongodb-wrapper http://i.tv, чтобы сохранить наши todo в MongoDB. С Гедди это будет легко; все, что нам нужно изменить, это модель-адаптер.

    Если у вас есть какие-либо вопросы, пожалуйста, оставьте комментарий здесь или откройте вопрос на github .