Как сделать сокращатель URL с помощью SvelteKit.

Читайте, если:

  • Причины не нужны

Ресурсы

  • Исходный код : Github
  • Живой пример : shrink.theether.live
  • Вдохновение : @prajyu Как сделать сокращатель URL.

Введение
Это руководство по проекту, я предлагаю вам следовать ему шаг за шагом самостоятельно. В этой статье вы узнаете, как сделать сократитель URL, используя Svelte, SvelteKit, Redis и TailwindCSS.

Инициализация
Мы начнем со стартового проекта sveltekit. Затем мы настроим TailwindCSS (если вы не знаете, как это сделать, прочитайте мою статью Использование TailwindCSS в SvelteKit для создания системы проектирования: Часть первая).

  • Требуемый пакет, кроме вышеуказанного (npm i package-name)
   1. redis
   2. dotenv
Вход в полноэкранный режим Выйти из полноэкранного режима
  • Создайте файл .env для переменных окружения. В этом проекте мы будем использовать Redis, а я использую Railway, который является PaaS-провайдером для БД.
// .env

REDIS_URL=redis://localhost:6379 // Use your own it's for example.
Войдите в полноэкранный режим Выйдите из полноэкранного режима
  • Создайте новый файл в каталоге src с именем hooks.ts.
// hooks.ts
import dotenv from 'dotenv';

dotenv.config();
Войдите в полноэкранный режим Выйти из полноэкранного режима

Это загрузит ваши переменные окружения в svelteKit.

Подключение сервера к Redis

Убедитесь, что вы установили его в свой проект, если нет, то выполните эту команду в терминале

npm i redis
  • Создайте папку lib в каталоге src и в папке lib создайте файл redisConnection.ts. В этом файле мы будем обрабатывать наше соединение Redis и добавим несколько основных функций для set или get значений в/из Redis.
// redisConnection.ts

import { createClient } from 'redis';
import log from './log';

const client = createClient({ url: process.env.REDIS_URL as string });

let connectPromise: Promise<void> | undefined;
let errorOnce = true;
async function autoConnect(): Promise<void> {
    if (!connectPromise) {
        errorOnce = true;
        connectPromise = new Promise((resolve, reject) => {
            client.once('error', (err) => reject(new Error(`Redis: ${err.message}`)));
            client.connect().then(resolve, reject);
        });
    }
    await connectPromise;
}
client.on('error', (err) => {
    if (errorOnce) {
        log.error('Redis:', err);
        errorOnce = false;
    }
});
client.on('connect', () => {
    log('Redis up');
});
client.on('disconnect', () => {
    connectPromise = undefined;
    log('Redis down');
});
async function get<T>(key: string): Promise<T | undefined>;
async function get<T>(key: string, fallback: T): Promise<T>;
async function get<T>(key: string, fallback?: T): Promise<T | undefined> {
    await autoConnect();
    const value = await client.get(key);
    if (value === null) {
        return fallback;
    }
    return JSON.parse(value);
}
async function set(
    key: string,
    value: unknown,
    options?: { ttl: number } // TTL in seconds
): Promise<void> {
    const data = JSON.stringify(value);
    const config = options ? { EX: options.ttl } : {};
    await autoConnect();
    await client.set(key, data, config);
    await client.publish(key, data);
}
const storage = {
    get,
    set
};
export default storage;
Вход в полноэкранный режим Выход из полноэкранного режима

Здесь я тоже использую логгер, поэтому пока пропустим логгер. Если вы хотите узнать, как я это сделал, посмотрите Multiplayer Dice Game от bfanger на github. Он использует socket.io, redis и многое другое, вы многому научитесь.

  • Объяснение

— Создайте клиент для redis в node. Мы будем использовать createClient из redis. Для этого понадобится ваш REDIS_URL, который мы добавили в .env.

const client = createClient({ url: process.env.REDIS_URL as string });
Вход в полноэкранный режим Выход из полноэкранного режима

— Мы добавим функцию autoConnect, которая будет подключаться к Redis, когда мы будем set или get значение в redis.

let connectPromise: Promise<void> | undefined;
let errorOnce = true;
async function autoConnect(): Promise<void> {
    if (!connectPromise) {
        errorOnce = true;
        connectPromise = new Promise((resolve, reject) => {
            client.once('error', (err) => reject(new Error(`Redis: ${err.message}`)));
            client.connect().then(resolve, reject);
        });
    }
    await connectPromise;
}
Вход в полноэкранный режим Выход из полноэкранного режима

— Теперь мы добавили три инициатора, которые проверяют наше соединение с redis и отвечают в соответствии с этим, например, error, connected и disconnected.

client.on('error', (err) => {
    if (errorOnce) {
        log.error('Redis:', err);
        errorOnce = false;
    }
});
client.on('connect', () => {
    log('Redis up');
});
client.on('disconnect', () => {
    connectPromise = undefined;
    log('Redis down');
});
Вход в полноэкранный режим Выход из полноэкранного режима

— Теперь мы собираемся добавить нашу функциональную и необходимую функцию для получения данных из redis.

async function get<T>(key: string): Promise<T | undefined>;
async function get<T>(key: string, fallback: T): Promise<T>;
async function get<T>(key: string, fallback?: T): Promise<T | undefined> {
    await autoConnect();
    const value = await client.get(key);
    if (value === null) {
        return fallback;
    }
    return JSON.parse(value);
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Здесь мы сначала определили типы функций. Функция принимает один параметр key, который необходим для нахождения значения в redis. Сначала мы соединимся с redis с помощью нашей функции автоматического соединения, затем получим значение, используя предоставленный нами key, и вернем разобранный JSON.

— Теперь мы добавим функцию set, которая поможет нам добавить значение в redis. Функция set принимает два параметра key и value. Ключ должен быть уникальным, что поможет нам получить элемент из redis.

async function set(
    key: string,
    value: unknown,
    options?: { ttl: number } // TTL in seconds
): Promise<void> {
    const data = JSON.stringify(value);
    const config = options ? { EX: options.ttl } : {};
    await autoConnect();
    await client.set(key, data, config);
    await client.publish(key, data);
}
Вход в полноэкранный режим Выйти из полноэкранного режима

— Наконец, мы собираемся экспортировать их, чтобы мы могли использовать их в любом месте нашего проекта.

const storage = {
    get,
    set
};
export default storage;
Войти в полноэкранный режим Выйти из полноэкранного режима

Это все, что нам нужно добавить в redis для работы в нашем проекте.

Добавление конечных точек Frontend и Shadow

Здесь мы добавим наши html и tailwindcss, чтобы сделать наше поле ввода и кнопки.

// index.svelte

<script lang="ts">
    import { page } from '$app/stores';
    import Clipboard from '$lib/Clipboard.svelte';
    let url: string;
    let isURLGenerated: boolean = false;
    let isInvalidURL: boolean = false;

    function isValidHttpUrl(string) {
        let url;

        try {
            url = new URL(string);
        } catch (_) {
            return false;
        }

        return url.protocol === 'http:' || url.protocol === 'https:';
    }

    async function getURL() {
        if (isValidHttpUrl(url)) {
            const r = (Math.random() + 1).toString(36).substring(7);
            const redirectURL = `${$page.url}${r}`;
            isInvalidURL = false;
            const data = { key: r, url: url };
            await fetch('/', {
                method: 'POST',
                headers: {
                    accept: 'application/json'
                },
                body: JSON.stringify(data)
            });
            url = redirectURL;
            isURLGenerated = true;
        } else isInvalidURL = true;
    }
</script>

<svelte:head>
    <title>Shrink | Home</title>
</svelte:head>
<div class="bg-white w-screen h-screen flex flex-col justify-center text-center">
    <h1 class="text-6xl p-4 text-fuchsia-500 font-bold">Shrink Me, Web.</h1>
    <div class="flex justify-center w-full p-10">
        <div class="mb-3 xl:w-2/4">
            {#if isInvalidURL}
                <div
                    id="alert-border-2"
                    class="flex p-4 mb-4 bg-red-100 border-t-4 border-red-500 dark:bg-red-200"
                    role="alert"
                >
                    <svg
                        class="flex-shrink-0 w-5 h-5 text-red-700"
                        fill="currentColor"
                        viewBox="0 0 20 20"
                        xmlns="http://www.w3.org/2000/svg"
                        ><path
                            fill-rule="evenodd"
                            d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
                            clip-rule="evenodd"
                        /></svg
                    >
                    <div class="ml-3 text-sm font-medium text-red-700">You have typed wrong URL.</div>
                </div>
            {/if}
            <div class="input-group relative flex items-stretch w-full mb-4">
                <input
                    type="text"
                    class="form-control relative flex-auto block w-full border-b-2 px-3 py-1.5 text-base font-normal text-gray-700 bg-white bg-clip-padding transition ease-in-out m-0 focus:outline-none duration-300 focus:text-gray-700 focus:bg-white {isURLGenerated
                        ? 'border-emerald-600 focus:border-emerald-600'
                        : 'border-fuchsia-600 focus:border-fuchsia-600'}"
                    placeholder="Paste or type your URL"
                    aria-label="url"
                    aria-describedby="url"
                    bind:value={url}
                />
                <Clipboard
                    text={url}
                    let:copy
                    on:copy={() => {
                        console.log('Has Copied');
                    }}
                >
                    {#if isURLGenerated}
                        <button
                            on:click={copy}
                            class="inline-block px-6 py-2 border-2 border-emerald-600 bg-emerald-600 text-white font-medium text-xs leading-tight uppercase hover:bg-white hover:text-emerald-600 transition duration-300 ease-in-out"
                            type="button"
                            id="button-copy">Copy</button
                        >
                        <button
                            on:click={() => {
                                url = '';
                                isURLGenerated = false;
                            }}
                            class="inline-block px-6 py-2 border-2 border-rose-600 bg-rose-600 text-white font-medium text-xs leading-tight uppercase hover:bg-white hover:text-rose-600 transition duration-300 ease-in-out"
                            type="button"
                            id="button-reset">Reset</button
                        >
                    {:else}
                        <button
                            on:click={getURL}
                            class="inline-block px-6 py-2 border-2 border-fuchsia-600 bg-fuchsia-600 text-white font-medium text-xs leading-tight uppercase hover:bg-white hover:text-fuchsia-600 transition duration-300 ease-in-out"
                            type="button"
                            id="button-addon3">Shrink</button
                        >
                    {/if}
                </Clipboard>
            </div>
            <div class="flex justify-center items-center">
                <div class="spinner-grow inline-block w-8 h-8 bg-fuchsia-600 rounded-full  opacity-0" />
            </div>
        </div>
    </div>
</div>

Вход в полноэкранный режим Выход из полноэкранного режима
  • ОбъяснениеЗдесь я собираюсь объяснить основные функциональные возможности и создание запросов к конечной точке shadow. Пожалуйста, поймите часть html и tailwind из моих предыдущих сообщений.

— Сначала мы сосредоточимся на теге pur script.

<script lang="ts">
    import { page } from '$app/stores';
    import Clipboard from '$lib/Clipboard.svelte';
    let url: string;
    let isURLGenerated: boolean = false;
    let isInvalidURL: boolean = false;

    function isValidHttpUrl(string) {
        let url;

        try {
            url = new URL(string);
        } catch (_) {
            return false;
        }

        return url.protocol === 'http:' || url.protocol === 'https:';
    }

    async function getURL() {
        if (isValidHttpUrl(url)) {
            const r = (Math.random() + 1).toString(36).substring(7);
            const redirectURL = `${$page.url}${r}`;
            isInvalidURL = false;
            const data = { key: r, url: url };
            await fetch('/', {
                method: 'POST',
                headers: {
                    accept: 'application/json'
                },
                body: JSON.stringify(data)
            });
            url = redirectURL;
            isURLGenerated = true;
        } else isInvalidURL = true;
    }
</script>
Вход в полноэкранный режим Выход из полноэкранного режима

Здесь я должен определить несколько параметров. Это URL : который предоставляется пользователем, isURLGenerated : проверяет, сгенерирован ли url или нет (нам это нужно, чтобы изменить кнопки с generate на copy), isInvalidURL : определяется для html, чтобы активировать предупреждение, если url не действителен.

isValidHttpUrl : Эта функция помогает проверить наш URL, предоставленный пользователем, является ли он действительным или нет.

getURL : Эта функция генерирует короткие url. Сначала я добавил проверку на isValidHttpUrl, если он действителен, мы продолжим, иначе мы вернем isInvalidURL = true. Если URL действителен, то будет сгенерирована случайная строка, после чего мы сделаем запрос к нашей теневой конечной точке/api для сохранения нашей сгенерированной строки key и url, предоставленных пользователем.

  • Конечная точка Shadow или API
// index.ts

import storage from '$lib/redisConnection';
import type { RequestHandler } from '@sveltejs/kit';

export const POST: RequestHandler = async ({ request }) => {
    const data = await request.json();
    await storage.set(data.key, data.url);
    return {
        body: {
            status: 200,
            error: null
        }
    };
};
Вход в полноэкранный режим Выйти из полноэкранного режима

— Здесь я добавил обработку метода POST, который принимает параметр запроса, из которого мы получаем данные, предоставленные нами при запросе из index.svelte. Здесь мы сохраняем эти данные в redis с помощью функции set.

Это все, что нам нужно для создания правильного короткого URL и сохранения в redis. Теперь мы собираемся как перенаправить пользователя на URL при посещении сайта, используя сгенерированный вами короткий URL.

Получение исходного URL и перенаправление на него
В этом разделе мы узнаем, как получить параметр из URL и получить данные из redis, а затем перенаправить на исходный URL.

— Добавьте новый файл в директории routes [slug].ts и добавьте следующие строки кода

// [slug].ts

import storage from '$lib/redisConnection';
import type { RequestHandler } from '@sveltejs/kit';

export const GET: RequestHandler = async ({ params }) => {
    if (params.slug.length > 3) {
        return {
            headers: {
                Location: await storage.get(params.slug)
            },
            status: 302
        };
    } else
        return {
            headers: {
                Location: '/'
            },
            status: 302,
            error: new Error('Short URL doesn't exist')
        };
};
Войти в полноэкранный режим Выйти из полноэкранного режима

Здесь мы добавили метод GET, который будет вызываться при посещении любого короткого url. Мы используем {params}, который является встроенным словарем, содержащим наш slug страницы.

Я добавил условие для проверки длины slug (сгенерированная строка в index.svelte) и, используя его, мы запросим наш redis с помощью функции get и получим значение, которое является нашим сохраненным оригинальным URL. Я добавил Location в заголовки, которые помогут нам перенаправить на этот URL и добавил статус или перенаправление. Если условие не сработает, он будет перенаправлен на домашний адрес нашего URL Shortner.

Таким образом мы получим и перенаправим исходный URL.

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

Это я пишу для вас. Если вы хотите что-то спросить или предложить, пожалуйста, напишите это в комментариях.

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