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

Оглавление

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

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

Ссылки на GitHub для этой главы: BrowseZipDiff.

Личные сообщения

Функция личных сообщений, которую я собираюсь внедрить, будет очень простой. Когда вы заходите на страницу профиля пользователя, там будет ссылка для отправки этому пользователю личного сообщения. Ссылка приведет вас на новую страницу, на которой веб-форма принимает сообщение. Чтобы прочитать отправленные вам сообщения, на панели навигации вверху страницы появится новая ссылка "Сообщения", которая приведет вас на страницу, похожую по структуре на страницы index или explore, но вместо сообщений в блоге на ней будут отображаться сообщения, отправленные вам другими пользователями.

В следующих разделах описаны шаги, которые я предпринял для реализации этой функции.

Поддержка базы данных для личных сообщений

Первая задача - расширить базу данных для поддержки личных сообщений. Вот новая модель Message:

app/models.py: Модель сообщений.

class Message(db.Model):
    id: so.Mapped[int] = so.mapped_column(primary_key=True)
    sender_id: so.Mapped[int] = so.mapped_column(sa.ForeignKey(User.id),
                                                 index=True)
    recipient_id: so.Mapped[int] = so.mapped_column(sa.ForeignKey(User.id),
                                                    index=True)
    body: so.Mapped[str] = so.mapped_column(sa.String(140))
    timestamp: so.Mapped[datetime] = so.mapped_column(
        index=True, default=lambda: datetime.now(timezone.utc))

    def __repr__(self):
        return '<Message {}>'.format(self.body)

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

app/models.py: Поддержка личных сообщений в пользовательской модели.

class User(UserMixin, db.Model):
    # ...
    last_message_read_time: so.Mapped[Optional[datetime]]

    # ...
    messages_sent: so.WriteOnlyMapped['Message'] = so.relationship(
        foreign_keys='Message.sender_id', back_populates='author')
    messages_received: so.WriteOnlyMapped['Message'] = so.relationship(
        foreign_keys='Message.recipient_id', back_populates='recipient')

    # ...

    def unread_message_count(self):
        last_read_time = self.last_message_read_time or datetime(1900, 1, 1)
        query = sa.select(Message).where(Message.recipient == self,
                                         Message.timestamp > last_read_time)
        return db.session.scalar(sa.select(sa.func.count()).select_from(
            query.subquery()))

В поле last_message_read_time будет указано время последнего посещения пользователем страницы сообщений, и оно будет использоваться для определения наличия непрочитанных сообщений, все они будут иметь временную метку новее, чем это поле. Вспомогательный метод unread_message_count() фактически использует это поле, чтобы вернуть количество непрочитанных сообщений у пользователя. К концу этой главы у меня будет эта цифра в виде красивого значка на панели навигации вверху страницы.

Две взаимосвязи возвращают сообщения, отправленные и полученные для данного пользователя. На стороне взаимосвязи Message я также включаю обратные связи, которые я назвал author и recipient:

app/models.py: Взаимосвязи в модели личных сообщений.

class Message(db.Model):
    # ...

    author: so.Mapped[User] = so.relationship(
        foreign_keys='Message.sender_id',
        back_populates='messages_sent')
    recipient: so.Mapped[User] = so.relationship(
        foreign_keys='Message.recipient_id',
        back_populates='messages_received')

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

На этом изменения базы данных завершены, так что теперь пришло время сгенерировать новую миграцию и обновить базу данных с ее помощью:

(venv) $ flask db migrate -m "private messages"
(venv) $ flask db upgrade

Отправка личного сообщения

Далее я собираюсь поработать над отправкой сообщений. Мне понадобится простая веб-форма, которая принимает сообщение:

app/main/forms.py: Класс формы личного сообщения.

class MessageForm(FlaskForm):
    message = TextAreaField(_l('Message'), validators=[
        DataRequired(), Length(min=0, max=140)])
    submit = SubmitField(_l('Submit'))

И мне также нужен HTML-шаблон, который отображает эту форму на веб-странице:

app/templates/send_message.html: HTML-шаблон для отправки личного сообщения.

{% extends "base.html" %}
{% import "bootstrap_wtf.html" as wtf %}

{% block content %}
    <h1>{{ _('Send Message to %(recipient)s', recipient=recipient) }}</h1>
    {{ wtf.quick_form(form) }}
{% endblock %}

Далее я собираюсь добавить новый маршрут /send_message/<recipient> для обработки фактической отправки личного сообщения:

app/main/routes.py: Способ отправки личных сообщений.

from app.main.forms import MessageForm
from app.models import Message

# ...

@bp.route('/send_message/<recipient>', methods=['GET', 'POST'])
@login_required
def send_message(recipient):
    user = db.first_or_404(sa.select(User).where(User.username == recipient))
    form = MessageForm()
    if form.validate_on_submit():
        msg = Message(author=current_user, recipient=user,
                      body=form.message.data)
        db.session.add(msg)
        db.session.commit()
        flash(_('Your message has been sent.'))
        return redirect(url_for('main.user', username=recipient))
    return render_template('send_message.html', title=_('Send Message'),
                           form=form, recipient=recipient)

Я думаю, логика в этой функции просмотра должна быть в основном понятной. Действие по отправке личного сообщения просто выполняется путем добавления нового экземпляра Message в базу данных.

Последнее изменение, связывающее все воедино, - это добавление ссылки на приведенный выше маршрут на странице профиля пользователя:

app/templates/user.html: Ссылка для отправки личного сообщения на странице профиля пользователя.

{% if user != current_user %}
<p>
    <a href="{{ url_for('main.send_message', recipient=user.username) }}">
    {{ _('Send private message') }}
    </a>
</p>
{% endif %}

Просмотр личных сообщений

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

app/main/routes.py: Просмотр личных сообщений.

@bp.route('/messages')
@login_required
def messages():
    current_user.last_message_read_time = datetime.now(timezone.utc)
    db.session.commit()
    page = request.args.get('page', 1, type=int)
    query = current_user.messages_received.select().order_by(
        Message.timestamp.desc())
    messages = db.paginate(query, page=page,
                           per_page=current_app.config['POSTS_PER_PAGE'],
                           error_out=False)
    next_url = url_for('main.messages', page=messages.next_num) \
        if messages.has_next else None
    prev_url = url_for('main.messages', page=messages.prev_num) \
        if messages.has_prev else None
    return render_template('messages.html', messages=messages.items,
                           next_url=next_url, prev_url=prev_url)

Первое, что я делаю в этой функции просмотра, это обновляю поле User.last_message_read_time текущим временем. По сути, это помечает все сообщения, которые были отправлены этому пользователю, как прочитанные. Затем я запрашиваю у модели Message список сообщений, отсортированных по временной метке от более новых к более старым. Я решил повторно использовать параметр конфигурации POSTS_PER_PAGE, поскольку страницы с записями и сообщениями будут очень похожи, но, конечно, если страницы будут расходиться, возможно, имеет смысл добавить отдельную переменную конфигурации для сообщений. Логика разбивки на страницы идентична той, которую я использовал для постов, так что все это должно быть вам знакомо.

Функция просмотра, описанная выше, завершается отображением страницы на основе нового шаблона /app/templates/messages.html, который вы можете увидеть ниже:

app/templates/messages.html: HTML-шаблон личных сообщений.

{% extends "base.html" %}

{% block content %}
    <h1>{{ _('Messages') }}</h1>
    {% for post in messages %}
        {% include '_post.html' %}
    {% endfor %}
    <nav aria-label="...">
        <ul class="pager">
            <li class="previous{% if not prev_url %} disabled{% endif %}">
                <a href="{{ prev_url or '#' }}">
                    <span aria-hidden="true">&larr;</span> {{ _('Newer messages') }}
                </a>
            </li>
            <li class="next{% if not next_url %} disabled{% endif %}">
                <a href="{{ next_url or '#' }}">
                    {{ _('Older messages') }} <span aria-hidden="true">&rarr;</span>
                </a>
            </li>
        </ul>
    </nav>
{% endblock %}

Здесь я прибегнул к еще одной маленькой хитрости. Я заметил, что экземпляры Post и Message имеют практически одинаковую структуру, за исключением того, что Message получает дополнительную взаимосвязь recipient (которую мне не нужно показывать на странице сообщений, поскольку это всегда текущий пользователь). Итак, я решил повторно использовать подшаблон app/templates/_post.html также для отображения личных сообщений. По этой причине в этом шаблоне используется странный цикл for for post in messages, так что все ссылки на post во вложенном шаблоне также работают с сообщениями.

Чтобы предоставить пользователям доступ к новой функции просмотра, на странице навигации появилась новая ссылка "Сообщения":

app/templates/base.html: Ссылка на личные сообщения в панели навигации.

{% if current_user.is_anonymous %}
...
{% else %}
<li class="nav-item">
    <a class="nav-link" aria-current="page"
            href="{{ url_for('main.messages') }}">
        {{ _('Messages') }}
    </a>
</li>
...
{% endif %}

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

(venv) $ flask translate update

Затем для каждого из языков в app/translations необходимо обновить файл messages.po с новыми переводами. Вы можете найти переводы на испанский язык для этого проекта в репозитории GitHub или в загружаемом zip-файле.

Статический значок уведомления о сообщениях

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

app/templates/base.html: Статический значок количества сообщений на панели навигации.

...
<li class="nav-item">
    <a class="nav-link" aria-current="page"
            href="{{ url_for('main.messages') }}">
        {{ _('Messages') }}
        {% set unread_message_count = current_user.unread_message_count() %}
        <span class="badge text-bg-danger">
            {{ unread_message_count }}
        </span>
    </a>
</li>
...

Здесь я вызываю метод unread_message_count(), который я добавил выше в модель User, непосредственно из шаблона и сохраняю это число в переменной шаблона с тем же именем. Затем, если эта переменная отлична от нуля, я просто добавляю значок с номером рядом со ссылкой на сообщения. Вот как это выглядит на странице:

Следующая глава => Динамический значок уведомления о сообщениях

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

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

app/templates/base.html: Значок непрочитанных сообщений, удобный для JavaScript.

<li class="nav-item">
    <a class="nav-link" aria-current="page" href="{{ url_for('main.messages') }}">
        {{ _('Messages') }}
        {% set unread_message_count = current_user.unread_message_count() %}
        <span id="message_count" class="badge text-bg-danger"
            style="visibility: {% if unread_message_count %}visible
            {% else %}hidden{% endif %};">
            {{ unread_message_count }}
        </span>
    </a>
</li>

В этой версии значка я всегда включаю его, но для CSS-свойства visibility установлено значение, visible когда unread_message_count не равно нулю, или hidden если оно равно нулю. Я также добавил атрибут id к элементу <span>, представляющему значок, чтобы упростить обращение к этому элементу с помощью document.getElementById('message_count').

Далее я могу закодировать короткую функцию JavaScript, которая обновит этот значок до нового значения:

app/templates/base.html: Статический значок количества сообщений на панели навигации.

...
{% block scripts %}
  <script>
    // ...

    function set_message_count(n) {
      const count = document.getElementById('message_count');
      count.innerText = n;
      count.style.visibility = n ? 'visible' : 'hidden';
    }
  </script>
{% endblock %}

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

Доставка уведомлений клиентам

Теперь осталось добавить механизм, с помощью которого клиент получает периодические обновления о количестве непрочитанных сообщений, имеющихся у пользователя. Когда произойдет одно из этих обновлений, клиент вызовет функцию set_message_count(), чтобы уведомить пользователя об обновлении.

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

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

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

Хорошей новостью является то, что независимо от используемого вами метода, в клиенте у вас будет функция обратного вызова, которая будет вызываться со списком обновлений. Итак, я мог бы начать с первого решения, которое намного проще реализовать, а позже, если я сочту его недостаточным, перейти на сервер WebSocket, который можно настроить для обратного вызова клиента. На мой взгляд, для приложений такого типа первое решение действительно приемлемо. Реализация на основе WebSocket была бы полезна для приложения, которому требуется доставка обновлений практически с нулевой задержкой.

На случай, если вам интересно, Twitter также использует первый подход для своих уведомлений на панели навигации. Facebook использует его разновидность, называемую длинным опросом, которая устраняет некоторые ограничения прямого опроса при сохранении использования HTTP-запросов. Stack Overflow и Trello - это два сайта, которые используют WebSocket для своих уведомлений. Вы можете узнать, какой тип фоновой активности происходит на любом сайте, заглянув на вкладку Сеть в отладчике браузера.

Итак, давайте продолжим и внедрим решение для опроса. Сначала я собираюсь добавить новую модель для отслеживания уведомлений для всех пользователей, а также взаимосвязь в модели пользователя.

app/models.py: Модель уведомлений.

import json
from time import time

# ...

class User(UserMixin, db.Model):
    # ...
    notifications: so.WriteOnlyMapped['Notification'] = so.relationship(
        back_populates='user')

    # ...

class Notification(db.Model):
    id: so.Mapped[int] = so.mapped_column(primary_key=True)
    name: so.Mapped[str] = so.mapped_column(sa.String(128), index=True)
    user_id: so.Mapped[int] = so.mapped_column(sa.ForeignKey(User.id),
                                               index=True)
    timestamp: so.Mapped[float] = so.mapped_column(index=True, default=time)
    payload_json: so.Mapped[str] = so.mapped_column(sa.Text)

    user: so.Mapped[User] = so.relationship(back_populates='notifications')

    def get_data(self):
        return json.loads(str(self.payload_json))

У уведомления должно быть имя, связанный пользователь, временная метка Unix и полезная нагрузка. Временная метка получает значение по умолчанию из функции time.time(). Полезная нагрузка будет разной для каждого типа уведомлений, поэтому я записываю ее в виде строки JSON, поскольку это позволит мне писать списки, словари или отдельные значения, такие как числа или строки. Я добавил метод get_data() для удобства, чтобы вызывающему не приходилось беспокоиться о десериализации JSON.

Эти изменения необходимо включить в новую миграцию базы данных:

(venv) $ flask db migrate -m "notifications"
(venv) $ flask db upgrade

Для удобства я собираюсь добавить новые модели Message и Notification в контекст командной строки, чтобы при запуске командной строки с помощью команды flask shell модель сообщений автоматически импортировалась для меня:

microblog.py: Добавляем модель сообщений в контекст оболочки.

# ...
from app.models import User, Post, Message, Notification

# ...

@app.shell_context_processor
def make_shell_context():
    return {'sa': sa, 'so': so, 'db': db, 'User': User, 'Post': Post,
            'Message': Message, 'Notification': Notification}

Я также собираюсь добавить вспомогательный метод add_notification() в пользовательскую модель, чтобы упростить работу с этими объектами:

app/models.py: Добавление уведомлений.


class User(UserMixin, db.Model):
    # ...

    def add_notification(self, name, data):
        db.session.execute(self.notifications.delete().where(
            Notification.name == name))
        n = Notification(name=name, payload_json=json.dumps(data), user=self)
        db.session.add(n)
        return n

Этот метод не только добавляет уведомление для пользователя в базу данных, но также гарантирует, что если уведомление с таким именем уже существует, оно будет удалено первым. Вы уже видели ранее, что отношения, доступные только для записи, могут быть запрошены путем вызова метода select(). Метод delete() возвращает запрос на удаление для отношения, который удаляет все элементы без их загрузки. Добавляя конструкцию where(), я точно указываю, какие элементы в отношениях я хочу удалить. Уведомление, с которым я собираюсь работать, будет называться unread_message_count. Если в базе данных уже есть уведомление с этим именем, например, со значением 3, когда пользователь получает новое сообщение и количество новых сообщений становится 4, я хочу заменить старое уведомление.

В любом месте, где изменяется количество непрочитанных сообщений, мне нужно вызватьadd_notification(), чтобы обновить мои уведомления для пользователя. Есть два места, где это меняется. Сначала, когда пользователь получает новое личное сообщение, в функции просмотра send_message():

app/main/routes.py: Обновление уведомлений пользователей.

@bp.route('/send_message/<recipient>', methods=['GET', 'POST'])
@login_required
def send_message(recipient):
    # ...
    if form.validate_on_submit():
        # ...
        user.add_notification('unread_message_count',
                              user.unread_message_count())
        db.session.commit()
        # ...
    # ...

Второе место, где мне нужно уведомить пользователя, - это когда пользователь переходит на страницу сообщений, и в этот момент количество непрочитанных сообщений возвращается к нулю:

app/main/routes.py: Маршрут просмотра сообщений.

@bp.route('/messages')
@login_required
def messages():
    current_user.last_message_read_time = datetime.now(timezone.utc)
    current_user.add_notification('unread_message_count', 0)
    db.session.commit()
    # ...

Теперь, когда все уведомления для пользователей хранятся в базе данных, я могу добавить новый маршрут, который клиент может использовать для получения уведомлений для вошедшего в систему пользователя:

app/main/routes.py: Функция просмотра уведомлений.

from app.models import Notification

# ...

@bp.route('/notifications')
@login_required
def notifications():
    since = request.args.get('since', 0.0, type=float)
    query = current_user.notifications.select().where(
        Notification.timestamp > since).order_by(Notification.timestamp.asc())
    notifications = db.session.scalars(query)
    return [{
        'name': n.name,
        'data': n.get_data(),
        'timestamp': n.timestamp
    } for n in notifications]

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

Я не хочу, чтобы клиенты получали повторяющиеся уведомления, поэтому я даю им возможность запрашивать уведомления только с определенного времени. Параметр since может быть включен в строку запроса URL-адреса запроса с меткой времени в виде числа с плавающей запятой. При включении этого аргумента будут возвращены только уведомления, которые произошли после указанного времени.

Завершающим этапом для завершения этой функции является реализация фактического запроса в клиенте. Лучшее место для этого - базовый шаблон, чтобы все страницы автоматически наследовали поведение:

app/templates/base.html: Запрос уведомлений.

...
{% block scripts %}
  <script>
    // ...

    {% if current_user.is_authenticated %}
    function initialize_notifications() {
      let since = 0;
      setInterval(async function() {
        const response = await fetch('{{ url_for('main.notifications') }}?since=' + since);
        const notifications = await response.json();
        for (let i = 0; i < notifications.length; i++) {
          if (notifications[i].name == 'unread_message_count')
            set_message_count(notifications[i].data);
          since = notifications[i].timestamp;
        }
      }, 10000);
    }
    document.addEventListener('DOMContentLoaded', initialize_notifications);
    {% endif %}
  </script>

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

Вы уже видели событие DOMContentLoaded в главе 20. Таким образом вы регистрируете функцию для выполнения после загрузки страницы. Для этой функции все, что мне нужно сделать при загрузке страницы, - это настроить обычный таймер, который получает уведомления для пользователя. Вы также видели функцию JavaScript setTimeout(), которая запускает функцию, указанную в качестве аргумента, по истечении определенного времени. Функция setInterval() использует те же аргументы, что и setTimeout(), но вместо того, чтобы запускать таймер только один раз, она продолжает вызывать функцию обратного вызова через регулярные промежутки времени. В этом случае мой интервал установлен равным 10 секундам (указывается в миллисекундах), поэтому я буду видеть обновление значка примерно шесть раз в минуту.

Функция, связанная с интервальным таймером, отправляет Ajax-запрос для нового маршрута уведомлений с помощью fetch(). Когда этот вызов возвращается, он выполняет итерацию по списку уведомлений. При получении уведомления с именем unread_message_count значок количества сообщений настраивается путем вызова функции, определенной выше, с количеством, указанным в уведомлении.

Способ, которым я обрабатываю аргумент since, может сбивать с толку. Я начинаю с инициализации этого аргумента равным 0. Аргумент всегда включается в URL запроса, но я не могу сгенерировать строку запроса с помощью url_for() Flask, как я делал раньше, потому что url_for() запускается на сервере один раз, и мне нужен аргумент since для динамического обновления. В первый раз запрос будет отправлен в /notifications?since= 0, но как только я получаю уведомление, я обновляю since его временную метку. Это гарантирует, что я не получу дубликатов, поскольку я всегда прошу получать уведомления, которые произошли с момента последнего уведомления, которое я видел. Также важно отметить, что я объявил переменную since вне интервальной функции, потому что я не хотел, чтобы это была локальная переменная, я хочу, чтобы одна и та же переменная использовалась во всех вызовах.

Самый простой способ попробовать это - использовать два разных браузера. Войдите в микроблог в обоих браузерах, используя разных пользователей. Затем из одного из браузеров отправьте одно или несколько сообщений другому пользователю. Панель навигации другого браузера должна обновиться, чтобы показывать количество сообщений, отправленных вами менее чем за 10 секунд. А когда вы нажимаете на ссылку Сообщения, количество непрочитанных сообщений сбрасывается до нуля.

Следующая глава =>

Комментарии (0)