Cloudflare Workers предоставляет глобально распределенную бессерверную среду с высокой доступностью и масштабируемостью. В этой статье мы воспользуемся возможностями Cloudflare Workers и KV, распределенного хранилища ключей и значений с низкой задержкой, для создания простого инструмента Link in Bio, аналогичного Linktree и Lnk.Bio.
Предварительный просмотр
Вот так будет выглядеть готовый проект:
Инициализация нового проекта
Начнем с инициализации нового проекта с помощью Wrangler CLI. Идите вперед и запустите эту команду:
$ npx wrangler init link-in-bio
В этом уроке мы будем использовать Typescript, поэтому при появлении запроса ответьте yes
.
Создание KV-магазина
Выполните следующие команды, чтобы создать новый магазин KV:
$ npx wrangler kv:namespace create "LINK_IN_BIO" $ npx wrangler kv:namespace create "LINK_IN_BIO" --previews
Нам также потребуется обновить файл wrangler.toml
, чтобы включить привязки пространств имен.
... kv_namespaces = [{ binding = "LINK_IN_BIO", id = "xxxxxxxxx", preview_id = "xxxxxxxxx"}]
Запуск локально
Мы запустим сервер разработки для сборки и запуска проекта локально, выполнив следующую команду:
$ npx wrangler dev
Создание веб-шаблонов
Нам нужно три страницы для проекта. На главной странице будет форма. На странице профиля будут отображаться ссылки и информация о профиле пользователя. Наконец, нам понадобится страница успеха, которая будет отображаться после того, как пользователь успешно отправит форму. Он будет содержать ссылку, которая будет опубликована в профиле социальной сети.
Шаблоны представляют собой стандартные HTML-документы, обернутые вокруг функции. Они довольно длинные, чтобы включать их сюда, поэтому проверьте их на Github. В profile_page.ts
также есть некоторые интерфейсы TypeScript. ProfileContent
— это тип ввода для шаблона страницы профиля.
export interface Link { name: string; url: string; } export interface ProfileContent { name: string; picture: string; description?: string; links: Link[]; }
После создания этих файлов импортируйте их в index.js
.
import profilePage, { ProfileContent, Link } from "./profile_page"; import homePage from "./home_page"; import successPage from "./success_page";
Мы также деструктурируем LINK_IN_BIO
, который является нашим хранилищем KV, из объекта env
. Наша функция fetch
теперь выглядит примерно так:
export default { async fetch(request: Request, { LINK_IN_BIO }: Record<string, any>): Promise<Response> { ... } }
Обработка HTTP-запросов
Нам нужно обработать два запроса GET
. Если пути нет, мы просто отрендерим homePage
.
// handle get request if (request.method.toLowerCase() === "get") { const url = new URL(request.url); // home page route if (url.pathname === "/") { return new Response(homePage, { headers: { 'Content-Type': 'text/html' } }); } }
Если в URL-адресе есть путь, мы отобразим profilePage
с содержимым страницы, полученным из нашего хранилища KV. Сначала мы извлекаем ключ из URL-адреса, а затем получаем значение из KV, вызывая метод get
для LINK_IN_BIO
.
// handle get request if (request.method.toLowerCase() === "get") { const url = new URL(request.url); // home page route if (url.pathname === "/") { return new Response(homePage, { headers: { 'Content-Type': 'text/html' } }); } /** * Additional code to handle route with a path **/ // extract key from url const key = url.pathname.replaceAll("/", ""); // render profile page const content = await LINK_IN_BIO.get(key); if (content) { return new Response(profilePage(JSON.parse(content)), { headers: { 'Content-Type': 'text/html' } }); } // key is not in the store return new Response("Page does not exist", { status: 404 }); }
Следующее, что нам нужно обработать, это запрос POST
. Это вызывается при отправке формы на домашней странице. Первое, что нужно сделать, это получить данные формы из запроса. Мы также создадим переменную namedcontent
для хранения содержимого профиля, которое мы поместим в KV. Он будет иметь тип ProfileContent
, который совпадает с вводом для шаблона profilePage
.
// handle post request if (request.method.toLowerCase() === "post") { const formData = await request.formData(); const content: ProfileContent = { name: '', picture: '', links: [] }; }
Нам также нужны две вспомогательные функции. toBase64
будет использоваться для преобразования изображения профиля в строку base64 и generateId
простой генератор уникальных идентификаторов для генерации ключей для нашего хранилища KV.
// convert file to base64 const toBase64 = async (file: File) => { const buffer = await file.arrayBuffer(); var binary = ''; var bytes = new Uint8Array(buffer); var len = bytes.byteLength; for (var i = 0; i < len; i++) { binary += String.fromCharCode(bytes[i]); } const base64 = btoa(binary); return `data:${file.type};base64,${base64}` }; // generate unique id const generateId = () => { return btoa((Date.now() * Math.random() + "")); }
Форма на главной странице позволяет пользователям добавлять в форму бесконечное количество ссылок. С каждым новым вводом ссылки будет связан номер, например link_name_1
и link_url_1
. Посмотреть код можно на Github.
Мы напишем здесь некоторый код, чтобы получить все имена ссылок и URL-адреса из формы и сгруппировать их на основе их количества. Сначала мы отображаем их в объект, а затем сохраняем список ссылок в переменной content
.
// handle post request if (request.method.toLowerCase() === "post") { ... // map to store all links in the form const linkMap: Record<number, Link> = {} // iterate over all entries in the form data for (const [key, value] of formData) { // extract number from the field name const numberMatch = key.match(/\d+/) const number = numberMatch ? parseInt(numberMatch[0]) : undefined; // add link name to linkMap if (key.includes("link_name")) { if (number && typeof value === "string") linkMap[number] = { ...linkMap[number], name: value } } // add link url to linkMap else if (key.includes("link_url")) { if (number && typeof value === "string") linkMap[number] = { ...linkMap[number], url: value } } } // add links to content from linkMap content.links = Object.values(linkMap); }
Далее мы получим name
, description
и picture
из запроса, а также добавим их в нашу переменную content
.
// handle post request if (request.method.toLowerCase() === "post") { ... // map to store all links in the form const linkMap: Record<number, Link> = {} // iterate over all entries in the form data for (const [key, value] of formData) { ... // add name and description to content else if ((key === "name" || key === "description") && typeof value === "string") { content[key] = value; } // add profile picture to content else if (key === "picture" && typeof value !== "string") { content[key] = await toBase64(value); } } ... }
Хорошо, последнее, что нам нужно сделать, это добавить контент в наш магазин KV и отобразить страницу успеха.
// handle post request if (request.method.toLowerCase() === "post") { ... // generate a unique ID const id = generateId(); // add content to KV await LINK_IN_BIO.put(id, JSON.stringify(content)); // display success page return new Response(successPage(id), { headers: { 'Content-Type': 'text/html' } }); }
Издательский
Мы закончили с нашей реализацией, так что давайте развернем ее. Чтобы опубликовать наш Cloudflare Worker, нам нужно выполнить следующую команду:
$ npx wrangler publish
Теперь наши работники будут бегать по всему миру. Потрясающий!
Заключение
Есть еще некоторые модификации, которые мы могли бы сделать, такие как добавление аутентификации, настраиваемых идентификаторов и настраиваемых шаблонов профилей. Но я надеюсь, что это руководство дало вам представление о типах приложений, которые вы можете создавать с помощью Cloudflare Workers и KV.
Надеюсь, вам понравилось читать эту статью. Вы можете найти исходный код проекта на Github, а также ознакомиться с живой демонстрацией.