Создаём одинаковое приложение 5 раз

На написание этой статьи меня вдохновил YouTube-канал Fireship, записывающий отличные видео о веб-разработке, крайне рекомендую их посмотреть, если вам интересна эта тема.

Вот видео с канала, в котором в 10 фреймворках создают todo-приложение:

https://youtu.be/cuHDQhDhvPE

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

Создаём приложение

jQuery

Чтобы упростить создание приложения без фреймворка, я буду использовать jQuery. Начал я с создания базовой структуры файлов и открытия index.html. Если вам любопытно, структура файлов выглядит так:

По сути, у меня есть таблица стилей в SCSS, которую я скомпилирую в CSS, и на этом пока всё. Разметка html выглядит пока так, но в дальнейшем я её расширю:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="https://habr.com/ru/company/itsoft/blog/575416/./css/styles.css" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <title>Notes App</title>
</head>

<body>
    <div class="container">
        <header>
            <h1>Notes App</h1>
        </header>
        <main>
            <div class="note">
                <form>
                    <input required type="text" id="note-title" placeholder="Note Title" />
                    <textarea id="note-body" placeholder="Note Body"></textarea>
                    <input type="submit" id="note-submit" title="Add Note" />
                </form>
            </div>
        </main>
    </div>
</body>

</html>

Таблица стилей выглядит так:

body {
    height: 100%;
    width: 100%;
    margin: 0;
}

.container {
    width: 100%;
    height: auto;
    margin: 0;
    display: flex;
    flex-direction: column;

    header {
        display: flex;
        align-items: center;

        width: 100%;
        height: 56px;
        background-color: #4e78b8;
        color: white;

        h1 {
            margin-left: 6px;
        }
    }

    main {
        margin: 10px;
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
        grid-gap: 1rem;
        align-items: center;

        .note {
            display: flex;
            flex-direction: column;

            padding: 10px;
            background-color: #a15fbb;
            border-radius: 5px;

            form {
                display: flex;
                flex-direction: column;

                textarea {
                    resize: none;
                }
            }
        }
    }
}

Далее я компилирую код при помощи sass scss/styles.scss:css/styles.css, после чего мы готовы писать JavaScript. По сути, нам достаточно добавить в DOM новый div с парой дочерних элементов для отправки формы и сохранение в локальное хранилище. Вот что у меня в результате получилось:

let notes = [];

$(document).ready(function () {
    if (localStorage.getItem("notes")) notes = JSON.parse(localStorage.getItem("notes"));
    setNotes();
});

$("#note-submit").click(function (e) { 
    let noteTitle = $("#note-title").val();
    let noteDesc = $("#note-body").val();
    let note = {
        title: noteTitle,
        desc: noteDesc
    }
    notes.push(note);
    console.log(notes);
    localStorage.setItem("notes", JSON.stringify(notes));
    setNotes();
});

function setNotes() {
    notes.forEach((note) => {
        $("main").prepend(`
            <div class="note">
                <h4>${note.title}</h4>
                <span>${note.desc}</span>
            </div>
        `);
    });
}

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

Angular

Забавен контраст между тем, сколько можно сделать с помощью Angular, и тем, что делаем мы. Вопреки впечатлению, которое могло сложиться по предыдущим постам, мне очень нравится Angular, просто я недолюбливают его недостаток модульности по сравнению с чем-то наподобие React. Впрочем, ладно, давайте сгенерируем проект:

$ ng new angular

И это буквально всё, что нужно для начала. Разве CLI Angular не великолепен? Для базовой структуры приложения я напишу, по сути, тот же код:

<div class="container">
  <header>
    <h1>Notes App</h1>
  </header>
  <main>
    <div class="note" *ngFor="let note of [0, 1, 2, 3]">
      <h4>Note Title</h4>
      <span>Note Body</span>
    </div>
    <div class="note">
      <form>
        <input required type="text" #noteTitle placeholder="Note Title" ngModel />
        <textarea #noteBody placeholder="Note Body" ngModel></textarea>
        <input type="submit" #noteSubmit title="Add Note" />
      </form>
    </div>
  </main>
</div>

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

.container {
  width: 100%;
  height: auto;
  margin: 0;
  display: flex;
  flex-direction: column;

  header {
      display: flex;
      align-items: center;

      width: 100%;
      height: 56px;
      background-color: #4e78b8;
      color: white;

      h1 {
          margin-left: 6px;
      }
  }

  main {
      margin: 10px;
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
      grid-gap: 1rem;
      align-items: center;

      .note {
          display: flex;
          flex-direction: column;

          padding: 10px;
          background-color: #a15fbb;
          border-radius: 5px;

          form {
              display: flex;
              flex-direction: column;

              textarea {
                  resize: none;
              }
          }
      }
  }
}

Теперь мы просто можем написать код, похожий на тот, что писали раньше:

import { Component } from '@angular/core';

type Note = {
  title: string;
  desc: string;
}

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  notes: Array<Note> = [];
  title!: string;
  body?: string;

  constructor() {
    const data = localStorage.getItem("notes");
    if (data) this.notes = JSON.parse(data);
  }

  submitForm() {
    let note: Note = {
      title: this.title,
      desc: this.body || ""
    }
    this.notes.push(note);
    localStorage.setItem("notes", JSON.stringify(this.notes));
  }
}

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

<div class="container">
  <header>
    <h1>Notes App</h1>
  </header>
  <main>
    <div class="note" *ngFor="let note of notes">
      <h4>{{note.title}}</h4>
      <span>{{note.desc}}</span>
    </div>
    <div class="note">
      <form #addNoteForm="ngForm">
        <input required type="text" placeholder="Note Title" [(ngModel)]="title" name="Title" />
        <textarea placeholder="Note Body" [(ngModel)]="body" name="Body"></textarea>
        <input type="submit" #noteSubmit title="Add Note" (click)="submitForm()" />
      </form>
    </div>
  </main>
</div>

Вот и всё, ребята!

React

Думаю, здесь всё будет сложнее необходимого из-за природы React. React спроектирован так, чтобы быть более модульным и лёгким, чем другие фреймворки, но в каком-то смысле из-за своей структуры для мелких приложений он сложнее. Я начал генерацию своего react-приложения с собственного шаблона sammy-libraries:

$ yarn create react-app react-app --template sammy-libraries

У меня время от времени возникает баг: node sass (который я использую обычно, потому что dart sass, по моему опыту, имеет более медленное время компиляции для React) отказывается компилировать мой код sass, поэтому я просто удалил node_modules и yarn.lock, а затем снова запустил yarn, и всё заработало идеально. Вот что я сделал: я начал с создания index.scss, аналогичного styles.scss из первого приложения, а затем в своём компоненте App воссоздал базовую структуру приложения:

import React, { useEffect, useState } from "react";
import NotesList from "components/NotesList";
import { NoteType } from "components/Note";
//import "scss/App.scss";

function App() {
    const [notesList, setNotesList] = useState<NoteType[]>([]);

    const [noteTitle, setNoteTitle] = useState<string>("");
    const [noteDesc, setNoteDesc] = useState<string>("");

    useEffect(() => {
        const data = localStorage.getItem("notes");
        if (data) {
            setNotesList(JSON.parse(data));
        }
    }, []);

    useEffect(() => {
        localStorage.setItem("notes", JSON.stringify(notesList));
    }, [notesList])

    const addNote = (event: React.FormEvent<HTMLFormElement>) => {
        let note: NoteType = {
            title: noteTitle,
            desc: noteDesc,
        };
        setNotesList([...notesList, note]);
        event.preventDefault();
    };

    const changeTitle = (event: React.ChangeEvent<HTMLInputElement>) => {
        setNoteTitle(event.currentTarget.value);
    };

    const changeDesc = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
        setNoteDesc(event.currentTarget.value);
    };

    return (
        <div className="container">
            <header>
                <h1>Notes App</h1>
            </header>
            <NotesList addNote={addNote} changeTitle={changeTitle} changeDesc={changeDesc} notes={notesList} />
        </div>
    );
}

export default App;

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

NotesList.tsx:

import React from "react";
import AddNote from "components/AddNote";
import Note, { NoteType } from "components/Note";

type NotesListProps = {
    notes: NoteType[];
    addNote: (event: React.FormEvent<HTMLFormElement>) => void;
    changeTitle: (event: React.ChangeEvent<HTMLInputElement>) => void;
    changeDesc: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
};

function NotesList({ notes, addNote, changeTitle, changeDesc }: NotesListProps) {
    return (
        <main>
            {notes.map((note) => {
                return (
                    <Note
                        note={{
                            title: note.title,
                            desc: note.desc,
                        }}
                    />
                );
            })}
            <AddNote addNote={addNote} changeTitle={changeTitle} changeDesc={changeDesc} />
        </main>
    );
}

export default NotesList;

Note.tsx:

import React from "react";

export type NoteType = {
    title: string;
    desc: string;
}

interface NoteProps {
    note: NoteType;
}

function Note(props: NoteProps) {
    return (
        <div className="note">
            <h4>{props.note.title}</h4>
            <span>{props.note.desc}</span>
        </div>
    );
}

export default Note;

И AddNote.tsx:

import React from "react";

interface AddNoteProps {
    changeTitle: (event: React.ChangeEvent<HTMLInputElement>) => void;
    changeDesc: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
    addNote: (event: React.FormEvent<HTMLFormElement>) => void;
}

function AddNote(props: AddNoteProps) {
    return(
        <div className="note">
            <form onSubmit={props.addNote}>
                <input type="text" placeholder="Note Title" onChange={props.changeTitle} />
                <textarea placeholder="Note Body" onChange={props.changeDesc}></textarea>
                <input type="submit" value="Add Note" />
            </form>
        </div>
    );
}

export default AddNote;

Не самое сложное из того, что я делал, но определённо кажется более сложным, чем работа с jQuery или Angular, по крайней мере, мне. Я очень люблю React и считаю его любимым фреймворком, просто не знаю, что любить в нём, работая над подобными проектами. Если бы мне пришлось выбирать, то я бы сказать, что самый чистый из всех — это Angular, а JQuery кажется самым логичным (по крайней мере, для этого проекта). React — самый неуклюжий, с ним удобно работать, но он не кажется особо подходящим.

Vue

С этим фреймворком я работал один раз, и кому-то это может показаться оскорблением, но я действительно не видел смысла в возне с ним. Я могу использовать и Angular, и React. Мне кажется, что они покрывают почти все мои потребности (а оставшееся решают библиотеки), поэтому Vue никогда не казался мне полезным. Что ж, давайте создадим свой проект на Vue.

$ vue ui

По сути, я использовал всё по умолчанию, но с TypeScript и SCSS (в основном с dart sass, чтобы не ломать зависимости), потому что мне нравится применять их в своих проектах. Единственная реальная причина того, что я не использовал TypeScript в первом проекте, заключалась в том, что мне не хотелось возиться с обеспечением совместной работы jQuery и TS, но если вам интересно, то это возможно.

Как я подошёл к созданию приложения? Сначала я удалил почти всё, что было сгенерировано автоматически, и заменил код App вот на это:

<template>
  <div class="container">
    <header>
      <h1>Notes App</h1>
    </header>
    <main>
      <Note
        v-for="(note, index) in notes"
        :key="index"
        :title="note.title"
        :body="note.body"
      />
      <div class="note">
        <form @submit="submitForm()">
          <input type="text" placeholder="Note Title" v-model="title" />
          <textarea placeholder="Note Body" v-model="body"></textarea>
          <input type="submit" value="Add Note" />
        </form>
      </div>
    </main>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import Note from "./components/Note.vue";

type NoteType = {
  title: string;
  body: string;
};

@Component({
  components: {
    Note,
  },
})
export default class App extends Vue {
  notes: Array<NoteType> = [];
  title!: string;
  body?: string;

  constructor() {
    super();
    const data = localStorage.getItem("notes");
    if (data) this.notes = JSON.parse(data);
  }

  submitForm(): void {
    let note: NoteType = {
      title: this.title,
      body: this.body || "",
    };
    this.notes.push(note);
    localStorage.setItem("notes", JSON.stringify(this.notes));
  }
}
</script>

<style lang="scss">
body {
  height: 100%;
  width: 100%;
  margin: 0;
}

.container {
  width: 100%;
  height: auto;
  margin: 0;
  display: flex;
  flex-direction: column;

  header {
    display: flex;
    align-items: center;

    width: 100%;
    height: 56px;
    background-color: #4e78b8;
    color: white;

    h1 {
      margin-left: 6px;
    }
  }

  main {
    margin: 10px;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    grid-gap: 1rem;
    align-items: center;

    .note {
      display: flex;
      flex-direction: column;

      padding: 10px;
      background-color: #a15fbb;
      border-radius: 5px;

      form {
        display: flex;
        flex-direction: column;

        textarea {
          resize: none;
        }
      }
    }
  }
}
</style>

А компонент Note выглядел вот так:

<template>
  <div class="note">
    <h4>{{ this.title }}</h4>
    <span>{{ this.body }}</span>
  </div>
</template>

<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";

@Component({
  components: {},
})
export default class App extends Vue {
  @Prop() title!: string;
  @Prop() body?: string;
}
</script>

И на этом всё.

Svelte

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

Потратив десять минут на поиск несуществующего CLI для Svelte yarn create-*, я решил настроить проект с бойлерплейтом фреймворка. Я преобразовал проект в TypeScript, потому что я фанат языков со строгой типизацией, а потом приступил к работе. Занявшись стилями, я пошёл на принцип и отказался от SCSS. Под этим я подразумеваю, что не мог возиться с настройкой SCSS, какой бы лёгкой она ни была, поэтому просто скомпилировал его вручную. Вот с таким компонентом я начал работу:

<script lang="ts">
import Note from "./components/Note.svelte";

type NoteType = {
    title: string;
    body: string;
};

let notes: Array<NoteType> = [];

const data = localStorage.getItem("notes");
if (data) notes = JSON.parse(data);

let title: string = "";
let body: string = "";

function onSubmit() {
    let note: NoteType = {
        title: title,
        body: body
    };
    notes.push(note);
    localStorage.setItem("notes", JSON.stringify(notes));
}
</script>

<div class="container">
    <header>
        <h1>Notes App</h1>
    </header>
    <main>
        {#each notes as note}
            <Note title={note.title} body={note.body} />
        {/each}
        <div class="note">
            <form on:submit={onSubmit}>
                <input type="text" placeholder="Note Title" bind:value={title} />
                <textarea placeholder="Note Body" bind:value={body}></textarea>
                <input type="submit" value="Add Note" />
            </form>
        </div>
    </main>
</div>

<style>
body {
  height: 100%;
  width: 100%;
  margin: 0;
}

.container {
  width: 100%;
  height: auto;
  margin: 0;
  display: flex;
  flex-direction: column;
}
.container header {
  display: flex;
  align-items: center;
  width: 100%;
  height: 56px;
  background-color: #4e78b8;
  color: white;
}
.container header h1 {
  margin-left: 6px;
}
.container main {
  margin: 10px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  grid-gap: 1rem;
  align-items: center;
}
.container main .note {
  display: flex;
  flex-direction: column;
  padding: 10px;
  background-color: #a15fbb;
  border-radius: 5px;
}
.container main .note form {
  display: flex;
  flex-direction: column;
}
.container main .note form textarea {
  resize: none;
}
</style>

А вот компонент Note:

<script lang="ts">
    export var title: string;
    export var body: string;
</script>

<div class="note">
    <h4>{title}</h4>
    <span>{body}</span>
</div>

Я обнаружил проблему, решения которой не знаю, и на данном этапе не хочу её решать: стили работают, только если вставить их в bundle.css, который затем сбрасывается при обновлении кода и ресурсов. Это не будет проблемой в полностью собранном приложении, но очень раздражает при тестировании. Не думаю, что буду решать эту проблему в ближайшее время, но, вероятно, со временем устраню её.

Заключение

Выше я говорил, что буду пробовать создавать при помощи Svelte и другие приложения, но не знаю, насколько решительно я готов выполнять это обещание. Хотя мне понравились многие аспекты Svelte, в нём слишком много препятствий, чтобы использовать его чаще. Учитывая проект, над которым я работал, React, по моему мнению, подвергся несправедливой критике. Angular по-прежнему кажется мне самым чистым, Vue — самым интересным, а jQuery — самым лучшим, что меня сильно удивило. Думаю, если бы мне пришлось выбирать фреймворк для будущих проектов, то это определённо бы зависело от проекта, но сам я вполне могу представить, что использую все пять, даже с учётом сложностей Svelte. Тем не менее, вероятно, основную свою работу я буду выполнять в Angular и React, а jQuery и Vue будут следующими по приоритету. Наверно, я дам Svelte ещё один шанс, но не думаю, что буду создавать в нём особо много проектов, вне зависимости от того, была ли справедлива критика в этом проекте. В любом случае, мне кажется, что любой из этих фреймворков станет отличным выбором для множества способов применения. Теперь я вижу, почему людям нравится Vue, но не могу сказать, что моё мнение очень сильно изменилось.

Код

Весь код выложен на github: https://github.com/jackmaster110/five-apps

Источник 📢