В этом уроке вы научитесь создавать полностековое бессерверное приложение с помощью Svelte.js, GraphQL и Fauna. Вы создадите платформу для ведения блогов, подобную Dev.to, hashnode.com или Medium. Пользователи смогут входить в ваше приложение, создавать новые сообщения, редактировать и удалять свои посты.
Мы будем использовать следующий технологический стек.
- Svelte.js (Sveltekit)
- GraphQL
- Fauna для базы данных
- Развертывание (Vercel или Netlify)
🤖 Вы можете найти финальный код в следующей ссылке github.
Создание нового приложения Svelte
Сначала создадим новое приложение Svelte. Выполните следующие команды в терминале.
npm init svelte@next blogApp
Svelte CLI предоставит вам несколько опций для настройки нашего приложения. Выберите следующие опции.
✔ Which Svelte app template? › Skeleton project
✔ Use TypeScript? … No
✔ Add ESLint for code linting? Yes
✔ Add Prettier for code formatting? Yes
Запустите наше только что созданное приложение с помощью следующей команды.
cd blogApp
npm i
npm run dev
В этом руководстве мы сосредоточимся на функциональности нашего приложения. Мы не будем уделять много времени стилизации. Давайте продолжим и создадим простой компонент Navbar. Создайте новый файл src/lib/Nav.svelte
и добавьте в него следующий код.
// src/lib/Nav.svelte
<nav>
<a href="/">Home</a>
<a href="/login">Login</a>
<a href="/register">Register</a>
</nav>
Далее создадим файл макета. Создайте новый файл src/routes/__layout.svelte
и добавьте в него следующий код.
// src/routes/__layout.svelte
<script>
import Nav from '$lib/Nav.svelte';
</script>
<Nav />
<slot></slot>
Теперь при запуске приложения компонент Navbar
будет появляться на каждой странице.
Настройка клиента Svelte GraphQL
Ваше приложение Svelte будет потреблять бэкенд-сервис GraphQL. Существует множество популярных библиотек, которые вы можете использовать для потребления GraphQL в Svelte. Библиотека @urql/svelte
является одной из самых популярных. Давайте продолжим и установим ее.
Выполните следующую команду, чтобы добавить библиотеку в ваш проект.
npm i @urql/svelte --save
Далее создайте новый файл src/client.js
и добавьте следующий фрагмент кода.
// src/client.js
import { createClient } from '@urql/svelte';
export default createClient({
url: 'https://graphql.us.fauna.com/graphql',
// For DB in other zone use the following url
// EU: https://graphql.eu.fauna.com/graphql
// Classic: https://graphql.fauna.com/graphql
fetchOptions: () => {
const token = import.meta.env.VITE_PUBLIC_FAUNA_KEY;
return {
headers: { authorization: token ? `Bearer ${token}` : '' },
};
},
});
Теперь мы готовы запрашивать данные из бэкенда GraphQL. Давайте продолжим и настроим нашу базу данных.
Настройка базы данных
Создайте новую учетную запись в Fauna, если вы еще этого не сделали. Fauna — это распределенная бессерверная база данных, которая использует встроенный GraphQL API.
Перейдите на приборную панель Fauna и создайте новую базу данных.
Теперь вы готовы определить нашу схему GraphQL. Следующая ULM-диаграмма описывает, как моделировать данные в вашем приложении. В этом приложении у вас есть пользователи, и у каждого пользователя может быть много постов. Это отношение has_many
между User
и Post
.
Вернитесь к коду и создайте новый файл schema.graphql
в вашем корневом каталоге. Добавьте следующий код.
# schema.graphql
type User {
username: String!
email: String!
posts: [Post!] @relation
}
type Post {
title: String!
content: String!
author: User!
}
type Query {
listPosts: [Post]
}
Затем загрузите схему в базу данных Fauna. Перейдите на приборную панель Fauna, выберите GraphQL и импортируйте схему. Импортируйте файл schema.graphql
.
Обратите внимание, что после загрузки схемы перед вами откроется игровая площадка GraphQL. Вы можете добавлять, изменять и отлаживать свой GraphQL api с этой игровой площадки.
Давайте продолжим и добавим некоторые данные в нашу базу данных. Создайте нового пользователя, выполнив следующую мутацию на игровой площадке GraphQL.
mutation CreateNewUser {
createUser(data: {
username: "shadid"
email: "shadid120@email.com"
}) {
_id
username
email
}
}
Аналогично, создайте новый пост. Запустите следующую мутацию на игровой площадке GraphQL, чтобы создать новый пост.
mutation CreatePost {
createPost(data: {
title: "Hello worlds"
content: "Some content"
author: {
**connect: "321522241336508481"**
}
}) {
_id
title
content
author {
email
}
}
}
Обратите внимание, что мы использовали поле author > connect. Добавьте сюда идентификатор пользователя из предыдущей мутации. Это свяжет пользователя с постом. Поэтому автором этого поста будет пользователь, которого вы создали в первой мутации.
Запрос данных из приложения Svelte
Давайте продолжим и запросим данные из нашего приложения Svelte. Сначала нам нужно указать роль и сгенерировать ключ для связи нашего фронтенда с базой данных.
Перейдите на приборную панель Fauna. Выберите Security > Roles > New Custom Role.
Дайте вашей роли имя и предоставьте доступ на чтение к коллекциям User
и Post
. Также предоставьте доступ на чтение к индексу post_author_by_user
и индексу listPosts
.
Теперь перейдите в раздел Security > Keys > New Key.
Создайте новый ключ для вашей роли SvelteApp
.
Затем скопируйте созданный ключ. Создайте новый файл .env
в корне вашего приложения и добавьте ключ в качестве переменной окружения.
# .env
VITE_PUBLIC_FAUNA_KEY=<Your Key Here>
Обратите внимание, что этот ключ является открытым, и он будет открыт для внешнего интерфейса. Вот почему роль, связанная с этим ключом, имеет доступ только для чтения.
Теперь на главной странице давайте извлечем все посты из вашей базы данных. Добавьте следующий код в файл src/routes/index.js
.
<script lang="js">
import { operationStore, query, setClient} from '@urql/svelte';
import client from '../client'
setClient(client);
const allPosts = operationStore(`
query GetAllPosts($size: Int!, $cursor: String) {
listPosts(_size: $size, _cursor: $cursor) {
data {
_id
title
author {
email
}
}
}
}
`,
{ size: 100 },
{ requestPolicy: 'network-only' }
);
query(allPosts);
</script>
<h1>Posts</h1>
{#if $allPosts.fetching}
<p>Loading...</p>
{:else if $allPosts.error}
<p>Oh no... {$allPosts.error.message}</p>
{:else}
{#each $allPosts.data.listPosts.data as post}
<div class="post-wrap">
<a href={`/posts/${post._id}`}>
<div>{post.title}</div>
</a>
<span>by {post.author.email}</span>
</div>
{/each}
{/if}
<style>
.post-wrap {
margin-bottom: 1rem;
}
</style>
Перезапустите ваше приложение. Обратите внимание, что теперь вы получаете все посты в корневом URL вашего приложения.
Обратите внимание, что при выборе поста приложение переводит вас на маршрут /post/:id
. Вы сможете увидеть отдельные посты в этом маршруте. Давайте продолжим и создадим этот маршрут.
Создайте новый файл routes/posts/[id].svelte
и добавьте следующий код.
// routes/posts/[id].svelte
<script lang="js">
import { operationStore, query, setClient} from '@urql/svelte';
import { page } from '$app/stores';
import client from '../../client'
setClient(client);
const currentPost = operationStore(`
query GetPostById($id: ID!) {
findPostByID(id: $id) {
_id
title
content
author {
email
}
}
}
`,
{ id: $page.params.id }
)
query(currentPost)
</script>
{#if $currentPost.fetching}
<p>Loading...</p>
{:else}
<h2>{$currentPost.data.findPostByID.title}</h2>
<p>By <b>{currentPost.data.findPostByID.author.email}</b></p>
<p>{$currentPost.data.findPostByID.content}</p>
{/if}
Аутентификация и авторизация
Далее давайте добавим аутентификацию в наше приложение. Мы можем легко добавить аутентификацию, используя библиотеки fauna-gql-upload
и fauna-graphql-tool
. Для начала давайте добавим эти зависимости в наш проект.
npm i @fauna-labs/graphql-tool fauna-gql-upload --save-dev
Эти библиотеки являются скриптами автоматизации, и для запуска этих инструментов вам нужен ключ администратора от Fauna.
Перейдите на приборную панель Fauna.
Выберите Безопасность > Ключи > Новый ключ.
Создайте новый ключ Admin. Убедитесь, что роль установлена как admin.
Не передавайте этот ключ администратора никому и не развертывайте его вместе с вашим приложением. Ключ администратора должен использоваться только с инструментами автоматизации/миграции.
Добавьте ключ администратора в переменную .env
. Убедитесь, что ваш файл .env
находится в списке gitignore.
##.env
VITE_PUBLIC_FAUNA_KEY=<Fauna Public Key>
FGU_SECRET=<Your Admin Key>
Далее необходимо внести следующие изменения в схему GraphQL.
type User **@auth(primary: "email")** {
username: String!
email: String!
posts: [Post!] @relation
}
type Post **@protected(membership: "User", rule: ["read", "write", "create"])** {
title: String!
content: String!
author: User!
}
type Query {
listPosts: [Post]
}
Обратите внимание, что в предыдущем блоке кода мы добавили директиву @auth
к нашей коллекции User. Это означает, что мы будем использовать коллекцию User для аутентификации. Ключ primary
определяет, какие поля будут использоваться для регистрации и входа пользователей. В данном случае это email
. Поэтому пользователи могут войти в систему, используя свой email и пароль.
Обратите внимание, что в коллекцию Post добавлена директива @protected
.* Эта директива определяет шаблоны доступа. Вошедшим пользователям разрешено писать, создавать новые посты.
После добавления этих изменений в схему откройте файл package.json
и добавьте следующий фрагмент кода в раздел script.
// package.json
{
...
"script": {
...
"fgu": "fgu",
"fgt": "fgt"
}
}
Мы добавляем эти скрипты сюда, чтобы мы могли запускать fauna-graphql-tool
(fgt) и fauna-gql-upload
(fgu) из npm.
fgt
берет вашу схему GraphQL и компилирует схему в различные ресурсы базы данных (т.е. коллекции, функции, определяемые пользователем, правила аутентификации), а fgu
загружает ресурсы в Fauna.
Наконец, выполните следующую команду в терминале
npm run fgt && npm run fgu
Обратите внимание, что создается новая папка/fauna
со всеми ресурсами.
-
📗 Pro Tip:
Обратите внимание, что при запуске скриптов создается новая папка
/fauna
. Вы можете открыть эту папку и посмотреть на различные функции и роли, созданные сценариями автоматизации. Если вы хотите дополнительно настроить правила аутентификации, не стесняйтесь изменить здесь параметр logic.
Если вам интересно, как работают эти ресурсы, ознакомьтесь с документацией по драйверам Fauna JavaScript.
Теперь, когда вы вернетесь к GraphQL playground в Fauna, вы заметите, что вам доступны мутации register
и login
.
Наконец, перейдите в раздел Security > Roles > SvelteRole и предоставьте вашей роли привилегии вызова для этих недавно созданных функций. Не забудьте также предоставить доступ на чтение к индексу user_by_email
, поскольку этот индекс используется функцией login.
Форма регистрации пользователя
Далее, давайте продолжим создание формы регистрации пользователя. Создайте новый файл src/routes/register.svelte
и добавьте следующий код.
// src/routes/register.svelte
<script lang="js">
import { setClient, mutation } from '@urql/svelte';
import client from '../client'
import { goto } from '$app/navigation';
setClient(client);
const registerMutation = mutation({
query: `
mutation ($email: String!, $password: String!) {
register(email: $email, password: $password) {
email
_id
}
}
`,
});
async function onSubmit(e) {
const formData = new FormData(e.target);
const data = {};
for (let field of formData) {
const [key, value] = field;
data[key] = value;
}
const { email, password } = data;
const resp = await registerMutation({ email, password })
if (resp.data.register) {
goto('/');
}
if(resp.error) {
alert(resp.error.message);
console.log(resp.error);
}
}
</script>
<div class="wrap">
<h3>Register New User</h3>
<form on:submit|preventDefault={onSubmit}>
<div>
<label for="name">Email</label>
<input
type="text"
id="email"
name="email"
value=""
/>
</div>
<div>
<label for="name">Password</label>
<input
type="password"
id="password"
name="password"
value=""
/>
</div>
<button class="button is-light" type="submit">Register</button>
</form>
</div>
В предыдущем блоке кода у вас есть простой компонент формы. При отправке формы выполняется мутация register
и регистрируется новый пользователь.
Форма входа пользователя в систему
Далее давайте создадим форму входа пользователя в систему. Мы можем сохранить сессию пользователя в cookies браузера. Библиотека js-cookie
позволит нам сделать это легко. Добавьте эту библиотеку, выполнив следующую команду в терминале.
npm i js-cookie --save
Создайте новый файл src/routes/login.svelte
и добавьте следующий код.
<script>
import { setClient, mutation } from '@urql/svelte';
import client from '../client';
import Cookies from 'js-cookie';
import { goto } from '$app/navigation';
setClient(client);
const loginMutation = mutation({
query: `
mutation ($email: String!, $password: String!) {
login(email: $email, password: $password) {
secret
ttl
data {
_id
email
}
}
}
`,
});
async function onSubmit(e) {
const formData = new FormData(e.target);
const data = {};
for (let field of formData) {
const [key, value] = field;
data[key] = value;
}
const { email, password } = data;
const resp = await loginMutation({ email, password })
if(resp.data.login.data) {
Cookies.set(
'MY_BLOG_APP_TOKEN',
JSON.stringify({
id: resp.data.login.data._id,
secret: resp.data.login.secret
}),
{ expires: resp.data.login.data.ttl }
);
alert('Login Successful');
goto('/')
}
}
</script>
<div>
<h3>Login Form</h3>
<form on:submit|preventDefault={onSubmit} >
<div>
<label for="name">Email</label>
<input
type="text"
id="email"
name="email"
value=""
/>
</div>
<div>
<label for="name">Password</label>
<input
type="password"
id="password"
name="password"
value=""
/>
</div>
<button type="submit">Submit</button>
</form>
</div>
В предыдущем блоке кода у вас есть компонент простой формы. При отправке формы срабатывает мутация login
. При успешном входе в систему Fauna возвращает новый токен. Этот токен является токеном аутентифицированного пользователя. Мы используем js-cookie
для хранения этого токена в cookies браузера.
Создание нового поста
В нашем приложении авторизованные пользователи могут создавать новые посты. Создайте новую функцию clientWithAuthToken
в вашем файле client.js
. Вы можете передать токен аутентификации, полученный из cookies сессии, и эта функция установит клиент GraphQL с этим токеном сессии.
// src/client.js
export const clientWithAuthToken = token => createClient({
url: 'https://graphql.us.fauna.com/graphql',
fetchOptions: () => {
console.log('token', token);
return {
headers: { authorization: token ? `Bearer ${token}` : '' },
};
},
});
Далее давайте создадим страницу, на которой пользователи смогут публиковать новые сообщения.
Создайте новый файл src/routes/posts/new.svelte
и добавьте в него следующий код.
// src/routes/posts/new.svelte
<script lang="js">
import Cookies from 'js-cookie';
import { setClient, mutation } from '@urql/svelte';
import { clientWithAuthToken } from '../../client';
import { goto } from '$app/navigation';
let userSession = Cookies.get('MY_BLOG_APP_TOKEN');
let authorId;
if(userSession) {
const { secret, id } = JSON.parse(userSession);
authorId = id;
setClient(clientWithAuthToken(secret));
}
const newPost = mutation({
query: `
mutation CreatePost($title: String!, $content: String! $authorId: ID!) {
createPost(data: {
title: $title
content: $content
author: {
connect: $authorId
}
}) {
_id
title
content
}
}
`,
});
async function onSubmit(e) {
const formData = new FormData(e.target);
const data = {};
for (let field of formData) {
const [key, value] = field;
data[key] = value;
}
const { content, title } = data;
try {
console.log('authorId', authorId);
if(!authorId) {
alert('You must be logged in to create a post');
return;
}
const resp = await newPost({ title, content, authorId });
if(resp.data.createPost) {
alert('Post created successfully')
goto('/')
}
} catch (error) {
console.log(error);
}
}
</script>
<div>
<h3>New Post</h3>
{#if !userSession}
<p class="login-promt">You must be logged in to create a post</p>
{/if}
<form on:submit|preventDefault={onSubmit} >
<div class="input-blocks">
<label for="name">Title</label>
<input
type="text"
name="title"
value=""
/>
</div>
<div class="input-blocks">
<label for="name">Content</label>
<textarea
type="text"
name="content"
value=""
/>
</div>
<button type="submit">Submit</button>
</form>
</div>
<style>
.input-blocks {
display: flex;
flex-direction: column;
max-width: 300px;
margin-bottom: 1em;
}
.login-promt {
color: coral;
}
</style>
В предыдущем блоке кода, когда пользователь отправляет форму, срабатывает мутация createPost
. Обратите внимание, что мы используем clientWithAuthToken
для настройки вашего клиента GraphQL. Вы получаете токен сессии из cookies браузера и используете его для настройки клиента GraphQL. Если пользователь не вошел в систему или если срок действия токена сессии истек, то эта мутация не сработает.
Удаление сообщения
Давайте добавим функциональность для удаления поста. Создайте новый компонент src/lib/Delete.svelte
и добавьте следующий код.
// src/lib/Delete.svelte
<script lang="js">
import Cookies from 'js-cookie';
import { clientWithAuthToken } from '../client';
import { setClient, mutation } from '@urql/svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
let userSession = Cookies.get('MY_BLOG_APP_TOKEN');
if (userSession) {
setClient(clientWithAuthToken(userSession))
const {secret } = JSON.parse(userSession);
setClient(clientWithAuthToken(secret));
}
const deletePost = mutation({
query: `
mutation DeletePost($id: ID!) {
deletePost(id: $id) {
_id
title
}
}
`
})
async function handleDelete() {
const { data, error } = await deletePost({ id: $page.params.id });
if(error) {
console.log('error', error);
alert('error', error.message);
return;
}
if(data.deletePost) {
alert('Post deleted');
goto('/')
}
}
</script>
<button on:click|preventDefault={handleDelete} disabled={!userSession}>Delete</button>
Этот компонент отображает кнопку. Когда кнопка выбрана, она запускает мутацию deletePost
с токеном аутентифицированного пользователя.
Добавьте этот компонент на страницу src/routes/posts/[id].svelte
.
<script lang="js">
...
</script>
...
<Delete />
{/if}
Однако обратите внимание, что при выборе кнопки вы получите сообщение об отказе в разрешении. Это происходит потому, что мы не установили привилегию delete.
Снова перейдите в Fauna dashboard и выберите Security > Roles > UserRole.
В коллекции Post
отметьте галочкой пункт delete и выберите save.
🤔 Что если вы хотите, чтобы только владелец поста мог удалить его. Добавить такое правило очень просто. Из выпадающего списка постов выберите правило удаления.
Добавьте следующий фрагмент кода в правило предиката. Это правило предиката определяет, что удалять сообщение может только автор сообщения.
Lambda("ref", Equals(
Identity(), // logged in user
Select(["data", "author"], Get(Var("ref")))
))
Редактирование сообщения
Далее добавим функцию редактирования сообщения. Создайте новый компонент /src/lib/Edit.svelte
и добавьте следующий код.
// /src/lib/Edit.svelte
<script lang="js">
import { operationStore, query, setClient } from '@urql/svelte';
import { page } from '$app/stores';
import client from '../../client'
import Delete from '$lib/Delete.svelte';
import Edit from '$lib/Edit.svelte';
setClient(client);
const currentPost = operationStore(`
query GetPostById($id: ID!) {
findPostByID(id: $id) {
_id
title
content
author {
email
}
}
}
`,
{ id: $page.params.id },
{ requestPolicy: 'network-only' }
)
query(currentPost)
export let post = null;
currentPost.subscribe(({data}) => {
if(data) {
post = data.findPostByID;
}
})
</script>
{#if $currentPost.fetching}
<p>Loading...</p>
{:else}
<h2>{$currentPost.data.findPostByID.title}</h2>
<p>By <b>{currentPost.data.findPostByID.author.email}</b></p>
<p>{$currentPost.data.findPostByID.content}</p>
<Edit post={post}/>
<Delete />
{/if}
Этот компонент представляет собой базовый компонент формы, где данные предварительно заполняются из компонента posts/[id].svelte
. При отправке формы этот компонент запускает мутацию редактирования поста.
Добавьте этот компонент в файл src/routes/posts/[id].svelte
.
<script lang="js">
import Edit from '$lib/Edit.svelte';
...
export let post = null;
currentPost.subscribe(({data}) => {
if(data) {
post = data.findPostByID;
}
})
</script>
...
<Edit post={post}/>
{/if}
После внесения изменений код в вашем файле src/routes/posts/[id].svelte
должен выглядеть следующим образом.
// src/routes/posts/[id].svelte
<script lang="js">
import { operationStore, query, setClient } from '@urql/svelte';
import { page } from '$app/stores';
import client from '../../client'
import Delete from '$lib/Delete.svelte';
import Edit from '$lib/Edit.svelte';
setClient(client);
const currentPost = operationStore(`
query GetPostById($id: ID!) {
findPostByID(id: $id) {
_id
title
content
author {
email
}
}
}
`,
{ id: $page.params.id },
{ requestPolicy: 'network-only' }
)
query(currentPost)
export let post = null;
currentPost.subscribe(({data}) => {
if(data) {
post = data.findPostByID;
}
})
</script>
{#if $currentPost.fetching}
<p>Loading...</p>
{:else}
<h2>{$currentPost.data.findPostByID.title}</h2>
<p>By <b>{currentPost.data.findPostByID.author.email}</b></p>
<p>{$currentPost.data.findPostByID.content}</p>
<Edit post={post}/>
<Delete />
{/if}
Обновление шаблона для отражения состояния аутентификации пользователя
В настоящее время шаблон нашего приложения не изменяется, когда пользователь находится в состоянии входа в систему. Давайте изменим это.
Создайте новый файл src/store.js
. Создайте в этом файле новый записываемый магазин для хранения данных пользовательской сессии. Добавьте в этот файл следующий код.
import { writable } from 'svelte/store';
export const userSession = writable(null);
Далее, каждый раз, когда пользователь входит в систему, записывайте информацию о нем в это хранилище. Внесите следующие изменения в файл src/routes/login.svelte
.
<script>
...
import { userSession } from '../store';
...
async function onSubmit(e) {
...
if(resp.data.login.data) {
...
userSession.update(() => ({
email,
id: resp.data.login.data._id,
secret: resp.data.login.secret
}));
alert('Login Successful');
goto('/')
}
}
</script>
Наконец, обновите файл src/lib/Nav.svelte
следующим кодом. В следующем блоке кода мы слушаем любые изменения в магазине. Если пользователь вошел в систему, приложение отображает форму Logout, в противном случае оно отображает ссылку для входа и регистрации.
<script lang="js">
import { userSession } from '../store.js';
import Cookies from 'js-cookie';
let user;
userSession.subscribe(val => {
user = val;
});
function logout() {
userSession.update(() => null);
Cookies.remove('MY_BLOG_APP_TOKEN');
}
</script>
<nav>
<a href="/">Home</a>
{#if user}
<!-- svelte-ignore a11y-invalid-attribute -->
<a href="#" on:click={logout}>Logout</a>
{:else}
<a href="/login">Login</a>
<a href="/register">Register</a>
{/if}
<hr />
</nav>
Развертывание
Vercel
Теперь мы готовы к развертыванию нашего приложения. Вы можете легко развернуть приложение Svelte с помощью Vercel. Создайте новую учетную запись на Vercel, если вы еще не сделали этого. Затем выполните следующую команду и следуйте инструкциям.
npx vercel --prod
Netlify
Для установки Netlify следуйте приведенной ниже статье.
https://dev.to/danawoodman/deploying-a-sveltekit-app-to-netlify-5dc3
Вот и все. Надеюсь, эта статья была информативной и дала вам общее представление о разработке собственных полнофункциональных бессерверных приложений с помощью Svelte и GraphQL. Если у вас есть какие-либо замечания, не стесняйтесь оставить строчку в разделе комментариев. Если у вас есть вопросы, не стесняйтесь обращаться ко мне в Twitter @HaqueShadid