Fake chat - garevna/js-course GitHub Wiki

💼 Fake chat

📋 db.json

Внесем некоторые изменения в базу данных db.json

✏️ lastUpdate

Добавим запись lastUpdate с двумя полями:

"lastUpdate": {
    "data": "26.10.2018",
    "time": "12:38:01"
}

Мы будем использовать эти данные для обновления чата
после того, как данные в db.json были обновлены
( операции POST | PUT | DELETE )

✏️ posts

В каждую запись базы данных postsдобавим свойстваdateиtime`

"posts": [
    {
      "id": 0,
      "date": "05.08.2018",
      "time": "10:30:15",
      "userId": 2,
      "title": "My first post here",
      "body": "It's really wonder!"
    },
    ...
}

📋 json-server

Запускаем json-server

json-server --watch db.json

Получаем endpoints:

Resources
        http://localhost:3000/lastUpdate
        http://localhost:3000/users
        http://localhost:3000/posts
        http://localhost:3000/comments

Home
        http://localhost:3000

  • Открываем в браузере страницу http://localhost:3000
  • заходим в Chrome DevTools
  • Создаем snippet

vars functions
lastUpdate getData
chat appelem
posts buildChat
users initChat
currentUser updateChat
chatInput Запуск

Объявляем переменную lastUpdate,
в которой будем хранить дату и время модификации загруженных данных

let lastUpdate
getData

Объявляем переменную getData,
в которой будет ссылка на функцию, загружающую данные с сервера
по имени ресурса ( lastUpdate, users, posts, comments )

let getData = function ( ref ) {
    return fetch ( 'http://localhost:3000/' + ref )
        .then ( response => response.json () )
}
appElem
⤴️

Объявляем переменную appElem
В этой переменной будет ссылка на анонимную стрелочную функцию
Функция получает в первом аргументе имя тега tagName,
и создает элемент DOM
Второй ( опциональный ) аргумент container может содержать ссылку
на элемент-контейнер, куда нужно вставить созданный элемент
Если такой аргумент отсутствует, то функция вставляет элемент
в document.body

let appElem = ( tagName, container ) => 
    ( container ? container : document.body )
        .appendChild (
            document.createElement ( tagName )
        )

chat
⤴️

ссылка на элемент DOM, который будет контейнером для сообщений чата

posts & users

В переменные posts и users будем получать данные из базы данных на сервере

currentUser

объект активного пользователя чата ( от лица которого мы будем писать в чат )

let chat
let posts
let users

let currentUser
chatInput
⤴️

Создаем элемент input ( поле для ввода текста сообщения )
и стилизуем элемент:

let chatInput = appElem ( 'input' )
chatInput.style = `
    position: fixed;
    left: 20px;
    width: 80%;
    bottom: 10px;
    border: inset 1px;
    background-color: #af9;
    overflow: auto;
`
buildChat
⤴️

Ссылка на функцию, создающую элемент section ( это будет чат )

let buildChat = function () {
    chat = appElem ( 'section' )
    chat.style = `
        position: fixed;
        top: 30px;
        left: 20px;
        right: 20px;
        bottom: 70px;
        border: inset 1px;
        overflow: auto;
        padding: 10px;
    `
}
initChat
⤴️

Ранее мы уже объявили переменную chat

После вызова функции buildChat
в этой переменной ( chat) будет ссылка на элементsection,
который будет контейнером для сообщений в чате

Асинхронная функция initChatбудет итерировать массивpost
с помощью метода forEach
заполнять контейнер chatданными из массиваpost
( данные к моменту вызова функции должны быть уже получены от сервера )

В первую очередь контейнер chat освобождается:

chat.innerHTML = ""

На каждой итерации по значению post.userId
находится соответствующий элемент массива users
( с помощью метода filter )

На каждой итерации создается элемент div,
который будет контейнером для очередной записи
из массива post

Для создания новых элементов DOM и вставки их на страницу
используем асинхронную функцию appElem

let initChat = async function () {
    chat.innerHTML = ""
    posts.forEach ( post => {
        let user = users.filter (
            x => x.id === post.userId 
        )[0]
        chat.appendChild (
            ( function () {
                let cont = appElem ( 'div' )
                let ava = appElem ( 'img', cont )
                ava.src = user.photoURL
                ava.width = "40"
                ava.title = ` ${user.name} ${user.lastName}`
                appElem ( 'span', cont ).innerHTML = 
                    ` <small> ${post.date} ${post.time}</small>`
                appElem ( 'p', cont ).innerText = post.body
                return cont
            }) ( user )
        )
    })
}
updateChat
⤴️

Объявляем переменную updateChat,
в которую помещаем ссылку на асинхронную анонимную функцию updateChat

✋ Используем функцию getData для получения даты и времени
последнего обновления базы данных
в переменную updated,
используя ключевое слово await,
чтобы дождаться результата асинхронной операции

✋ Сравниваем дату и время обновления загруженных данных
с датой и временем последнего обновления базы данных
Если загруженные данные актуальны
( после их загрузки обновлений базы данных не было ),
то завершаем выполнение функции updateChat(return )

✋ В противном случае формируем массив промисов:

[ 
    getData ( "users" ).then ( x => users = x ) , 
    getData ( "posts" ).then ( x => posts = x ) 
]

и передаем его методу Promise.all,
который также вызываем с ключевым словом await

✋ Когда Promise.all будет разрешен, проверяем, есть ли значение
у объявленной ранее переменной currentUser
( пользователь, от лица которого будут добавляться сообщения в чат )
Если активный пользователь еще не определен,
выбираем его случайным образом из числа всех зарегистрированных пользователей ( users )

✋ Вызываем функцию initChat ()

let updateChat = async function () {
    let updated = await getData ( "lastUpdate" )
    if ( lastUpdate && updated.data === lastUpdate.data && 
        updated.time === lastUpdate.time ) return

    let scrollValue = chat.scrollTop

    await Promise.all ( [ 
        getData ( "users" ).then ( x => users = x ) , 
        getData ( "posts" ).then ( x => posts = x )
    ] )

    if ( !currentUser ) {
        currentUser = users [
            Math.floor ( Math.random () * users.length )
        ]
        currentUserId = currentUser.id
    }

    initChat ()
    chat.scrollTop = scrollValue
}

Свойство scrollTop можно использовать для управления прокруткой чата
Если установить

chat.scrollTop = chat.offsetTop

то элемент chat будет прокручен до конца
( мы будем видеть последние сообщения в чате )


📋 Запуск

⤴️
  • вызваем buildChat (), чтобы создать контейнер для чата
  • вызваем updateChat (), чтобы заполнить контейнер данными
  • устанавливаем интервал обновления чата ( setInterval )
через заданные интервалы времени 
мы будем вызывать updateChat (),
чтобы проверить, было ли за это время
обновление базы данных на серевере,
и если да - обновлять 
содержимое чата на клиенте
  • вешаем обработчика события change элемента chatInput
если клиент введет сообщение, это сообщение нужно
отправить на сервер для добавления в базу данных

Итак:

buildChat ()
updateChat ()

setTimeout ( function () {
    chat.scrollTop = chat.scrollHeight
}, 100 )

let interval = setInterval ( function () {
    updateChat ()
}, 1000 )

chatInput.onchange = function ( event ) {
    let postTime = new Date().toLocaleString ().split ( ', ' )
    fetch ( 'http://localhost:3000/posts', {
        method: 'POST',
        body: JSON.stringify ({
            date: postTime [0],
            time: postTime [1],
            userId: currentUserId,
            body: event.target.value
        }),
        headers: {
            "Content-type": "application/json"
        }
    })
}

⤴️ 📃 Полный код сниппета
⚠️ **GitHub.com Fallback** ⚠️