Создание почтовой формы

В двух предыдущих статьях мы настроили и развернули проект, который может получать данные из KV-хранилища Cloudflare Workers. Теперь мы создадим форму для создания новых постов.

В Rakkas есть встроенная поддержка работы с формами. Мы начнем с создания самой формы, добавив следующие строки в src/routes/index.page.tsx, сразу после закрывающего тега </ul> списка постов и перед закрывающим тегом </main>:

<form method="POST">
    <p>
        <textarea name="content" rows={4} />
    </p>
    <button type="submit">Submit</button>
</form>
Вход в полноэкранный режим Выход из полноэкранного режима

Пока все довольно просто. Самое интересное — это обработчик действий. Если вы экспортируете функцию с именем action из файла страницы, Rakkas будет вызывать ее, когда форма будет отправлена по этому адресу. Код в функции action всегда будет выполняться на стороне сервера, аналогично коду в обратном вызове useServerSideQuery. Давайте добавим его в нижнюю часть файла:

// ActionHandler type is defined in the `rakkasjs` package.
// Add it to your imports.
export const action: ActionHandler = async (ctx) => {
    // Retrieve the form data
    const data = await ctx.requestContext.request.formData();
    const content = data.get("content");

    // Do some validation
    if (!content) {
        return { data: { error: "Content is required" } };
    } else if (typeof content !== "string") {
        // It could be a file upload!
        return { data: { error: "Content must be a string" } };
    } else if (content.length > 280) {
        return { data: { error: "Content must be less than 280 characters" } };
    }

    await ctx.requestContext.locals.postStore.put(generateKey(), content, {
        metadata: {
            // We don't have login/signup yet,
            // so we'll just make up a user name
            author: "Arden Eberhardt",
            postedAt: new Date().toISOString(),
        },
    });

    return { data: { error: null } };
};

function generateKey() {
    // This generates a random string as the post key
    // but we'll talk more about this later.
    return Math.random().toString(36).slice(2);
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Если вы запустите сервер dev, то увидите, что теперь вы можете добавлять новые посты!

Улучшение пользовательского опыта

Круто, но у нас есть несколько проблем с UX. Во-первых, мы не показываем пользователю ошибки валидации.

Если обработчик действия возвращает объект с ключом data, эти данные будут доступны компоненту страницы в реквизите actionData. Он будет неопределен, если не было отправлений формы. Поэтому мы изменим сигнатуру компонента HomePage следующим образом:

// PageProps type is defined in the `rakkasjs` package.
// Add it to your imports.
export default function HomePage({ actionData }: PageProps) {
    // ...
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь мы добавим сообщение об ошибке прямо над кнопкой отправки:

<form method="POST">
    <p>
        <textarea name="content" rows={4} />
    </p>

    {actionData?.error && <p>{actionData.error}</p>}

    <button type="submit">Submit</button>
</form>
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь вы сможете увидеть сообщение об ошибке, если попытаетесь отправить пустой пост или если содержимое слишком длинное. Но все же не очень удобно, что форма очищается при возникновении ошибки. Одно из решений — вернуть данные формы в возвращаемое значение обработчика действия и затем использовать их для заполнения формы. Поэтому мы изменим часть, которая возвращает ошибку «слишком длинная», следующим образом:

-   return { data: { error: "Content must be less than 280 characters" } };

+   return {
+       data: {
+           error: "Content must be less than 280 characters",
+           content, // Echo back the form data
+       },
+   };
Войти в полноэкранный режим Выйти из полноэкранного режима

А затем мы используем его для инициализации значения по умолчанию нашего элемента textarea:

<textarea name="content" rows={4} defaultValue={actionData?.content} />
Войти в полноэкранный режим Выйти из полноэкранного режима

Если вы повторите попытку и отправите слишком длинный пост, вы увидите, что форма не будет очищена, и вы сможете отредактировать содержимое до 280 символов, чтобы отправить его повторно.

Сортировка постов

Вы могли заметить, что вновь созданные посты вставляются в список в случайном месте. Было бы лучше, если бы они отображались в порядке «самый новый-первый». В магазине KV нет метода сортировки по содержанию или метаданным. Но оно всегда возвращает элементы в алфавитном порядке ключей. Вместо случайных ключей мы могли бы использовать время создания, но это будет прямо противоположно тому, что мы хотим, поскольку 2022-08-01T00:00:00.000Z идет после 2020-08-01T00:00:00.000Z при сортировке по алфавиту.

Поэтому здесь нам придется проявить изобретательность. Экземпляры JavaScript Date имеют метод getTime(), который возвращает временную метку, представляющую собой количество миллисекунд, прошедших с 1 января 1970 года. Вы также можете создать дату из временной метки, например, с помощью new Date(0). Какова дата для временной метки 9,999,999,999,999? new Date(9_999_999_999_999) возвращает 20 ноября 2286 года. Я уверен, что ublog не будет существовать так долго. Поэтому моя идея состоит в том, чтобы использовать 9_999_999_999_999 - new Date().getTime() в качестве ключа.

Чтобы убедиться, что ключи маленькие, мы будем использовать кодировку base-36, а чтобы обеспечить сортировку по алфавиту, мы будем добавлять нули в левую строку. В кодировке base-36 число 9,999,999,999,999 равно 3jlxpt2pr, что составляет 9 символов. Поэтому мы будем нажимать левую кнопку клавиатуры до тех пор, пока ключ не будет содержать не менее 9 символов:

function generateKey() {
    return (9_999_999_999_999 - new Date().getTime())
        .toString(36)
        .padStart(9, "0");
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Ключи должны быть уникальными, но что если два пользователя создают посты одновременно? Мы можем свести вероятность столкновения ключей практически к нулю, добавив в конец случайную строку:

function generateKey() {
    return (
        (9_999_999_999_999 - new Date().getTime()).toString(36).padStart(9, "0") +
        Math.random().toString(36).slice(2).padStart(6, "0")
    );
}
Войти в полноэкранный режим Выйти из полноэкранного режима

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

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

Вот и все! Теперь мы можем добавлять новые сообщения в список и просматривать их в порядке «новые-первые».

Тестирование с Miniflare

Поскольку теперь любой может создавать посты, лучше пока не разворачивать это на Cloudflare Workers. Но мы можем протестировать наш рабочий пакет с Miniflare, собрав его с помощью npm run build и запустив с помощью npm run local. Miniflare имеет встроенную поддержку KV store, так что все должно работать как ожидалось.

Что дальше?

В следующей статье мы реализуем аутентификацию (вход/регистрация) с помощью GitHub OAuth API.

Прогресс до этого момента вы можете найти на GitHub.

Оцените статью
devanswers.ru
Добавить комментарий