Ошибки

9

На данный момент переведено

В этой главе вы:

  • Создадите лучший механизм для отображения ошибок и сообщений.
  • Узнаете как использовать `Template.rendered`, чтобы узнать когда пользователь увидел ошибку.
  • Используете фильтр для роутера, чтобы ошибки отображались только один раз.
  • Использование стандартного диалога alert() для уведомления пользователя об ошибках и проблемах вряд ли оставит о нашем приложении хорошее впечатление. Мы можем сделать все гораздо лучше.

    Давайте создадим универсальный механизм оповещения об ошибках, который будет уведомлять пользователей не слишком отвлекая их от приложения.

    Представляем Локальные Коллекции (Local Collections)

    Мы создадим простую систему, которая будет следить за тем, какие ошибки пользователь уже успел просмотреть, а также показывать новые ошибки в специально отведенной области на сайте под названием “flash”.

    Наша система будет похожа на сообщения ошибок в Ruby on Rails, но только более утонченную - она будет находиться на клиенте и знать, когда пользователь уже просмотрел сообщение.

    Для начала, мы создадим коллекцию для хранения ошибок. Учитывая то, что ошибки относятся только к текущей сессии и их вовсе не нужно сохранять на будущее, мы сделаем что-то новенькое. Мы создадим локальную коллекцию. Это означает, что коллекция Errors будет существовать только в браузере и не будет даже пытаться синхронизироваться с сервером.

    Для этого мы создадим объект ошибки в файле, доступном только на клиенте, с именем коллекции null. Мы создадим функцию throwError которая будет добавлять новую ошибку в нашу локальную коллекцию:

    // Локальная коллекция, доступна только на клиенте
    Errors = new Meteor.Collection(null);
    
    client/helpers/errors.js

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

    throwError = function(message) {
      Errors.insert({message: message})
    }
    
    client/helpers/errors.js

    Преимущество локальной коллекции для сохранения ошибок в том, что как и все коллекции она реактивна. Это позволит нам оперативно отражать ошибки в пользовательском интерфейсе, точно также как и любую другую коллекцию.

    Выводим ошибки

    Ошибки будут выводиться вверху нашего главного шаблона:

    <template name="layout">
      <div class="container">
        {{> header}}
        {{> errors}}
        <div id="main" class="row-fluid">
          {{yield}}
        </div>
      </div>
    </template>
    
    client/views/application/layout.html

    Давайте создадим шаблоны errors и error в файле errors.html:

    <template name="errors">
      <div class="errors row-fluid">
        {{#each errors}}
          {{> error}}
        {{/each}}
      </div>
    </template>
    
    <template name="error">
      <div class="alert alert-error">
        <button type="button" class="close" data-dismiss="alert">&times;</button>
        {{message}}
      </div>
    </template>
    
    client/views/includes/errors.html

    Шаблоны близнецы

    Вы наверное обратили внимание, что мы поместили два шаблона в один файл. До последнего момента мы придерживались правила “по одному шаблону в один файл”, но для Meteor все будет работать даже если мы поместим все шаблоны в один файл (хотя при этом наш main.html стал бы плохо читаем и огромен в размерах).

    В нашем случае оба шаблона довольно кратки. Мы сделаем исключение и поместим оба шаблона в один файл чтобы не засорять наш репозиторий обилием файлов.

    Нам осталось только добавить логику поиска ошибок в нашем методе шаблона (helper):

    Template.errors.helpers({
      errors: function() {
        return Errors.find();
      }
    });
    
    client/views/includes/errors.js

    Коммит 9-1

    Простой вывод ошибок.

    Создаем ошибки

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

    Вдобавок, если у нас будет ошибка 302 (означающая что пост с данным URL уже существует), мы перенаправим пользователя на страницу с существующим постом. Мы получим _id этого поста из error.details (если вы помните из главы 7, мы передаем _id этого поста как третий аргумент details нашего класса Error).

    Template.postSubmit.events({
      'submit form': function(e) {
        e.preventDefault();
    
        var post = {
          url: $(e.target).find('[name=url]').val(),
          title: $(e.target).find('[name=title]').val(),
          message: $(e.target).find('[name=message]').val()
        }
    
        Meteor.call('post', post, function(error, id) {
          if (error) {
            // показываем ошибку пользователю
            throwError(error.reason);
    
            if (error.error === 302)
              Router.go('postPage', {_id: error.details})
          } else {
            Router.go('postPage', {_id: id});
          }
        });
      }
    });
    
    client/views/posts/post_submit.js

    Коммит 9-2

    На самом деле используем систему вывода ошибок.

    Попробуйте создать пост и ввести URL http://meteor.com. Так как этот URL уже добавлен к посту из тестовых данных, вы скорее всего увидите:

    Преднамеренно вызываем ошибку
    Преднамеренно вызываем ошибку

    Убираем ошибки

    Если вы уже попробовали клинуть на кнопке закрытия ошибки, вы заметили что ошибка исчезает. Но если вы попробуете открыть другую страницу, ошибка вновь появится. Что же происходит?

    Кнопка закрытия ошибки вызывает скрипт Twitter Bootstrap, и не имеет ничего общего с Meteor! Происходит следующее - Bootstrap удаляет <div> с сообщением об ошибке из DOM, но не из коллекции Meteor. Само собой, ошибка тут же появится вновь, как только Meteor отрисует страницу заново.

    Если мы не хотим, чтобы ошибки восставали из мертвых, напоминая пользователям об их прошлых проступках и сводя их с ума, нам стоит добавить механизм удаления ошибок из коллекции.

    Для начала мы изменим функцию throwError, добавив в нее свойство seen. Это нам пригодится чуть позже для ведения учета, видел ли пользователь уже эту ошибку.

    Затем мы создадим простую фукцию clearErrors очищающую коллекцию от уже отображенных ошибок:

    // Local (client-only) collection
    Errors = new Meteor.Collection(null);
    
    throwError = function(message) {
      Errors.insert({message: message, seen: false})
    }
    
    clearErrors = function() {
      Errors.remove({seen: true});
    }
    
    client/helpers/errors.js

    Дальше, мы добавим очистку от ошибок в роутер, чтобы при открытии новой страницы старые ошибки удалялись автоматически:

    // ...
    
    Router.before(requireLogin, {only: 'postSubmit'})
    Router.before(function() { clearErrors() });
    
    lib/router.js

    Чтобы наша функция clearErrors() заработала, ошибки должны помечаться параметром seen. Для этого стоит учесть один особенный случай. Когда мы выдаем ошибку и перенаправляем пользователя на другую страницу (как в случае с уже существующей ссылкой), перенаправление происходит мгновенно. У пользователя не остается времени увидеть ошибку перед тем, как она будет убрана с экрана.

    Вот тут мы и воспользуемся параметром seen. Нам нужно установить его значение в true если пользователь успел увидеть ошибку.

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

    Мы просим Meteor установить параметр seen в true одной миллисекундой позже после того, как шаблон errors отрисован. Помните, как мы говорили что перенаправление на другую страницу осуществляется мгновенно? Это означает, перенаправление произойдет до того, как будет вызван обработчик defer.

    Это то что нам нужно - если обработчик не будет вызван, ошибка не будет отмечена как seen, и она не будет удалена из коллекции - соответственно, мы увидим ее на странице, куда был перенаправлен пользователь:

    Template.errors.helpers({
      errors: function() {
        return Errors.find();
      }
    });
    
    Template.error.rendered = function() {
      var error = this.data;
      Meteor.defer(function() {
        Errors.update(error._id, {$set: {seen: true}});
      });
    };
    
    client/views/includes/errors.js

    Коммит 9-3

    Следим за тем какие ошибки уже были показаны. Очищаем оши…

    Функция rendered будет вызвана, как только наш шаблон будет отрисован в браузере. Внутри этой функции this указывает на текущий объект шаблона, а this.data позволит нам обратиться к параметрам объекта, который мы отрисовываем (в данном случае, ошибке).

    Фух! Куча работы ради чего-то, что пользователи возможно никогда не увидят!

    Функция-обработчик rendered

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

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