Obstawiam, że 90% aktualnie tworzonych aplikacji opiera się na podział frontend, backend i przesył danych pomiędzy nimi w postaci JSON’a. Frontend prosi o dane i gdy je otrzymuje, to odpowiednio modyfikuje wygląd strony. A może można przesyłać coś innego niż JSON?
Co przesyłamy między przeglądarką a serwerem?
Frontend i backend muszą się jakoś komunikować. Jak to wyglądało na przestrzeni lat? Na początku backend i frontend były jednym organizmem. Backend tworzył pliki HTML i wysyłał do przeglądarki. Proste. Tylko od strony UI/UX było słabe, ponieważ każde zapytanie było równoznaczne z odświeżeniem strony.
Wtedy zaczęto rozdzielać frontend od backendu. Ciężar odświeżania danych jest teraz na frontendzie. Frontend tworzy UI, wysyła zapytania o dane i odświeża UI strony. Backend musi tylko zwrócić dane w postaci JSON’a. Nie interesuje go już, co zostanie z tymi danymi zrobione. Od strony UI/UX jest już spoko, ale są inne problemy - SEO, duplikowanie stanu, konieczność synchronizacji itp.
Teraz zaczynamy próbować podejścia hybrydowego. Mamy backend, który tworzy część widoków, a część zwraca jako kod JS’a by sam się sobą zajął. W Ruby pojawił się ostatnio inne podejście oparte o wysyłanie do frontendu fragmentów HTML’a, które trzeba zaktualizować. Oto Hotwire.
Czym jest Hotwire?
Hotwire (HTML over the wire) jest nowym pomysłem na przesyłanie danych pomiędzy frontendem a backendem. Został stworzony przez Basecamp i polega na wysyłaniu fragmentów HTML zamiast JSON’a. Dzięki temu, że strona jest budowana na backendzie, to ładuje się szybko i cały stan aplikacji jest trzymany w jednym miejscu. Co więcej, dzięki przesyłaniu fragmentów HTML’a nie musimy przeładować całego widoku, tylko pojedyncze elementy - tak jak w SPA.
Jak Hotwire wygląda w praktyce?
Do zabawy z Hotwire wykorzystałem Fastify. Aby to dodać wykorzystałem wtyczkę fastify-hotwire, która umożliwia prostą implementację tego rozwiązania.
npm i fastify-hotwire --save
Teraz zostaje kwestia dodania wtyczki do Fastify
fastify
    .register(import('fastify-hotwire'), {
        templates: join(__dirname, 'views'),
        filename: join(__dirname, 'worker.js')
    })
    .register(import('fastify-formbody'))
Drugą wtyczkę dodałem do poprawnej obsługi formularzy i odczytu danych z formularza. To, co musimy podać przy rejestracji wtyczki fastify-hotwire, to jest folder, gdzie będziemy trzymać widoki oraz plik odpowiedzialny za renderowanie plików. Mamy tutaj pełną dowolność, jeśli chodzi o wybór narzędzia do tworzenia widoków. Ja skorzystałem z ejs, który jest dosyć popularny, ale możesz skorzystać z dowolnej biblioteki.
import ejs from 'ejs'
export default async ({ file, data, fragment }) => {
   const body = await ejs.renderFile(file,data)
   return body
}
Cała aplikacja to bardzo podstawowa wersja TODO list - czyli jest możliwość dodawania zadań i ustawianie ich jako ukończonych. Jak to wygląda w praktyce?
Wyświetlanie głównego widoku
Na początek wyświetlenie wszystkich elementów. Aby to zrobić musimy obsłużyć zapytanie GET na adresie /.
fastify.get('/', async (req, reply) => {
    return reply.render('index.ejs', { tasks: tasks })
})
Dzięki zarejestrowaniu fastify-hotwire mamy dostępne w obiekcie reply dodatkowe metody. Podstawową z nich jest render, która  renderuje całą stronę. Plik index.ejs prezentuje się następująco.
//index.ejs
<head>
    <script src="https://unpkg.com/@hotwired/[email protected]/dist/turbo.es5-umd.js"></script>
</head>
<form action="/task" method="POST" style="margin-top: 40px">
    <input name="content" type="text" />
    <button type="submit">Create New Task</button>
</form>
<%- include('tasks'); %>
To, co najważniejsze, to skrypt hotwire w nagłówku. Reszta plików prezentuje się następująco. Są odpowiedzialne za wyświetlenie listy zadań oraz wyświetlenie pojedynczego elementu.
//tasks.ejs
<table id='tasks'>
        <% tasks.forEach(function(task){ %>
                <%- include('task', {task: task}); %>
        <% }); %>
</table>
// task.ejs
<tr id="task_<%= task.id %>">
    <td style="width: 200px">
        <%= task.name %>
    </td>
    <td style="width: 200px">
        <%= task.status %>
    </td>
    <td style="width: 200px">
            <form action="/finish-task" method="POST" style="margin-bottom: 0;">
                <input value="<%= task.id %>" name='id' style="visibility: hidden; width: 0">
                <button type="submit">Finish</button>
            </form>
    </td>
</tr>
Dodawanie nowych zadań
Aby dodać nowe zadania, bez konieczności przeładowania strony, skorzystałem z Turbo Streams. Jest to dodatkowo wspierane przez użytą przeze mnie bibliotekę. Sprowadza się to do następującego kodu.
fastify.post('/task', async (req, reply) => {
    const newTask = { id: tasks.length, name: req.body.content, status: 'TODO' };
    tasks.push(newTask)
    return reply.turboStream.append(
        'task.ejs',
        'tasks',
        { task: newTask }
    )
})
Cały kod sprowadza się do wyrenderowania nowego zadania przy pomocy pliku task.ejs, oraz dodanie tego do elementu HTML, który ma id="tasks".
Zmiana statusu zadania
Tutaj jest podobnie. W pliku task.ejs jest mały formularz, który umożliwia zmianę statusu zadania. Kod na backendzie, który to umożliwia, wygląda następująco.
fastify.post('/finish-task', async (req, reply) => {
    const id = req.body.id;
    tasks[id] = { ...tasks[id], status: 'FINISHED' }
    return reply.turboStream.replace(
        'task.ejs',
        `task_${id}`,
        { task: tasks[id] }
    )
})
Tutaj wykorzystałem metodę replace. Wyszukuje ona element HTML o podanym id i zamienia zawartość.
Ogólnie koncept mi się podoba. Jest to kolejna próba połączenia frontendu i backendu do jednego projektu, ale z lepszym UX dla użytkownika. Myślę, że podobne koncepty będą się pojawiały coraz częściej. A ty co o tym sądzisz?
