Читайте, если:
- Причины не нужны
Ресурсы
- Исходный код : 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.
На этом мы заканчиваем статью. Вы должны проверить все упомянутые ресурсы и ссылки, которые помогут вам понять эту статью намного лучше.
Это я пишу для вас. Если вы хотите что-то спросить или предложить, пожалуйста, напишите это в комментариях.