Черновик Svelte 3 с настроенным роутером на базе Page.js, примером использования хранилища (Stores), примером запроса к REST API (JSONPlaceholder), примером использования переменных из файлов .env
и настроенным ESLint (стиль Standard).
Я разрабатывал на Vue веб-приложение для мебельной компании, когда решил попробовать Svelte. Сначала я хотел вести разработку приложения на Vue и Svelte параллельно, чтобы сравнить их возможности в бою, но к сожалению, время поджимало и мне пришлось сосредоточиться на чём-то одном. Приложение на Vue готово, но теперь я не вижу смысла в дописывании его на Svelte и выкладывании для сообщества, тем более оно завязано на непубличный API. Я решил, что для сообщества будет полезнее создать черновик с настроенным роутером и получением данных с Fake REST API. Возможно в этом черновике вы найдёте полезные для себя части кода или информацию. Ниже я постарался подробно описать как что работает.
При копировании содержания данного .md
целиком, большая просьба указывать источник 👍.
Структура проекта – Что и в каких папках лежит
Роутер через Page.js – Веб-приложение многостраничное, значит нужно реагировать на изменения адреса в строке URL
Запросы на сервер – Делаем запросы к API
Хранилища (Stores) – Использование общих данные между компонентами
.env (environment variables) – Переменные окружения
Из коробки Svelte 3 не предлагает определённой структуры проекта. Мой вариант:
📁 node_modules
📁 public
📁 src
📁 pages
📁 components
📁 modules
📁 routes
📁 stores
🗋 App.svelte
🗋 main.js
node_modules
, public
, src
, App.svelte
, main.js
и некоторые другие файлы – минимум идущий в базовом шаблоне Svelte.
https://github.com/sveltejs/template
📁pages
– папка с компонентами .svelte
представляющими из себя отдельные страницы, переключаемые роутером.
📁components
– папка с небольшими и переиспользуемыми компонентами .svelte
, например шапка навигации, карточка поста и т.д.
📁modules
– .js
файлы, переиспользуемые и вынесенные для облегчения кода JS модули.
📁routes
– внутри index.js
файл с массивом объектов для роутинга. Возможно целая папка излишнее решение, но оставлю так, вдруг сильно разрастётся. А ещё это очень наглядно в редакторах кода в которых иконки папки отталкиваются от их названия (например VSCode + Material Icon Theme)
📁stores
– хранилища с данными которые нужны по всему проекту и постоянно используются.
https://visionmedia.github.io/page.js/
Добавляем модуль в свой проект:
npm i page
Сначало работаем с main.js
. Подключаем здесь page:
// main.js
import page from 'page'
А также импортируем компоненты которые считаются страницами:
import MyPage from './pages/MyPage.svelte'
Добавляем роуты:
page('/my-page', ctx => {
app.$set({ ctx, page: MyPage })
})
И не забыть активировать Page.js после ваших роутов:
page.start()
Лично я вынес роуты в отдельную папку и файл. Можете посмотреть в коде как это сделано, там просто.
Выводить в шаблоне компоненты по измению адреса нам поможет <svelte:component>
. Если директиве this
этого тега передать компонент, он будет реактивно отрисован. Если потом передать другой компонент, предыдущий сменится новым.
В App.svelte
добавим:
<!-- App.svelte -->
<script>
export let page = null
export let ctx = null
</script>
{#await page}
<p>Загрузка...</p>
{:then component}
<svelte:component this = {component} page = {ctx} />
{/await}
Что тут происходит? Мы подсоеденяемся к page
и ctx
объявленым ранее в main.js
.
export let page = null
export let ctx = null
При смене адреса Page.js в page
попадает компонент указанный в соответствующем роуте, а в ctx
различная полезная информация от самого Page.js.
Ожидаем когда переменная page
получит данные:
{#await page}
<p>Loading...</p>
Данные пришли, обозвали их component
(на самом деле назвать можно как угодно):
{:then component}
И как выше говорили, если присвоить дериктиве this
компонент, он будет выведен:
<svelte:component this = {component} page = {ctx} />
Не забываем закрыть {/await}
.
В <svelte:component>
мы также создали ещё одну дериктиву page
*, эти данные будут доступны в подключаемом компоненте. Мы передаём туда ctx
из Page.js. Это пригодятся в динамических адресах.
К примеру, если в роутер добавить адрес /posts/:id
, то в соответствующем компоненте мы сможем извлечь этот id
<!-- Какой-то компонент использующий динамический адрес -->
<script>
export let page
const { id } = page.params
</script>
Если перейти по адресу /posts/5
, то id
будет равно '5'
.
* – возможно вас запутали столько сущностей с именем page
, можно выбирать иные имена.
Для управления навигацией программно, используется станартный для современных браузеров History API
https://developer.mozilla.org/ru/docs/Web/API/History_API
Например кнопка назад:
<script>
const back = () => window.history.back()
</script>
<button on:click={back}>Назад</button>
Может пригодится в редиректах. Например перебрасывать на /login
при отсутствии авторизации.
Опять стандартные возможности JS:
window.location = '/login'
Я например, при переходах проверяю действительность токена в хранилище user.js
*.
* – Этот файл не используется в черновике, я его оставил в качестве примера. См. комментарии в файле.
Приложению постоянно нужны актуальные данные c сервера и чтобы не заграможнать код компонентов методами Fetch API, всё вынесено в отдельный модуль, который импортируется в нужный компонент.
/src/modules/request.js
Вот так виглядит GET запрос через Fetch API. Функция вызывается с параметром router, который содержит строку в какую конечную точку обращаться (например '/posts'
):
// request.js
const apiUrl = 'https://jsonplaceholder.typicode.com'
const get = async (router) => {
console.log(`GET /${router}`)
const url = `${apiUrl}/${router}`
const method = 'GET'
const headers = {
'Content-Type': 'application/json',
}
const response = await fetch(url, { method, headers })
const json = await response.json()
return json
}
export default {
get
}
В черновике для демонстрации задействован JSONPlaceholder – это fake REST API с фейковыми данными для быстрого теста или прототипирования на фронтенде, без оглядки на настоящие данные.
Аналогично с POST, PUT...
На этапе разработки хорошо бы видеть сколько запросов делается. Например можно случайно поместить запрос к серверу в какой-нибудь компонент который вызывается перебором тысячи раз. Потом можно будет это удалить или закоментить:
console.log(`GET /${router}`)
Теперь получать данные с сервера внутри компонента мы можем так:
<!-- Posts.svelte -->
<script>
import { onMount } from 'svelte'
import request from './request'
let posts = []
onMount(async () => {
posts = await request.get('posts')
})
</script>
Такой способ уместен, если полученные данные нужны только в одном компоненте. Если одни и теже данные используются в разных компонентах, удобнее завести хранилища.
Для хранилищь используются возможности Svelte из коробки (svelte/store).
https://ru.svelte.dev/docs#svelte_store
Модули складываются в папку /stores
/src/stores/
Пример хранилища которое заполняется постами:
// posts.js
import { writable } from 'svelte/store'
import request from '../helpers/request'
const posts = writable([])
const getPosts = async () => {
const json = await request.get('posts')
posts.set(json)
}
export {
posts,
getPosts
}
Создаётся хранилище с начальными данными переданными методу writable()
и присваивается переменной posts
. Если бы хранили числа, можно поставить 0
, если строку – ''
, если объект – пустой объект, если массив – пустой массив и т.д. У меня массив:
const posts = writable([])
Начальное значение хранилища предотвратит от некоторых дальнейших ошибок. Например $posts.length
* вернёт ошибку, а не длину массива, если хранилище не успеет наполниться.
* – про символ $
см. далее.
Создаётся функция для наполнения хранилища. По вызову этой функции совершается запрос на сервер (об этом рассказывал выше) и через метод set()
полученные данные присваиваются хранилищу:
const getPosts = async () => {
const json = await request.get('posts')
posts.set(json)
}
По хорошему нужно добавить обработчиков ошибок и систему уведомлений.
Ну а теперь мы можем получить доступ к хранилищу в нужном нам компоненте. Просто импортируем хранилище в соответсвующий компонент. Также предварительно вызываем getPosts()
чтобы оно наполнилось.
Так как у нас в переменной posts
не совсем данные, нам нужно как-то подписаться на них, в Svelte для этого перед именем переменной добавляется префикс $
(в компанентах .svelte
символ $
зарезервирован, не следует применять его в объявлении переменных):
<!-- Posts.svelte -->
<script>
import { posts, getPosts } from '../stores/posts'
getPosts()
</script>
<ul>
{#each $posts as { title }, i}
<li>
Пост №{i+1}. Заголовок: "{title}"
</li>
{/each}
</ul>
{#each $posts as { title }, i}
...
{/each}
Так как у нас массив, мы его перебераем. Всё что находится внутри {#each}{/each}
будет выведено столько раз, сколько длина у массива (если нет иных условий).
{#each $posts as { title }, i}
$posts
наш массив с данными полученый из хранилища. Элементы массива объекты. Мы можем сразу извлекать из них нужное значение нужного свойства, в данной случаи title
. i
– это номер элемента массива.
$posts
будет работать не только в шаблоне, но и внутри <script></script>
.
Есть одна проблема, если при разработке клиента вы работаете с локальным бэкендом, то скорее всего вы прописываете в файлах url-ы типа http://localhost:3000/api
или http://127.0.0.1:3000/api
. Но не менять же эти пути вручную когда решим публиковать на сервер.
Помогут Dotenv
и @rollup/plugin-replace
.
npm i dotenv --save-dev
npm i @rollup/plugin-replace --save-dev
Создадим в корне проекта пару файлов: .env.development
и .env.production
. Хорошим тоном будет добавить их в .gitignore
.
В .env.development
мы можем перенеси точку входа в API, чтобы не хранить её в коде:
API=https://jsonplaceholder.typicode.com
Создадим dotenv.js
:
// dotenv.js
import dotenv from 'dotenv'
const production = !process.env.ROLLUP_WATCH
if (production) dotenv.config({ path: '.env.production' })
else dotenv.config({ path: '.env.development' })
const __API__ = process.env.API
export {
__API__
}
В переменную production
возвращается false
при работе в dev
окружении и true
при сборке build
.
Dotenv добавляет переменные в окружение Node.js из файла .env
, описанному по схеме КЛЮЧ=значение
. Остаётся только получить интересующую переменную через process.env.КЛЮЧ
.
Далее работаем с rollup.config.js
. Тут задаются параметры для сборщика Rollup.
Ранее мы добавили плагин @rollup/plugin-replace
позволяющий заменять строки в собираемом проекте. Подключаем плагин replace
и модуль dotenv.js
, извлекая нужную переменную __API__
. Передаём эту переменную в параметры плагина replace
.
// rollup.config.js
...
import replace from '@rollup/plugin-replace'
import { __API__ } from './dotenv'
export default {
...
plugins: [
...
replace({
__API__
}),
...
],
...
}
Если в каком-нибудь компоненте или модуле мы добавим такую переменную:
const API = '__API__'
При сборке проекта любая строка '__API__'
заменится на значение из соответствующего текущему окружению .env
файла. Как раз этим обуславливается нестандартное написание, чтобы случайно не заменить то чего не надо.
Теперь можно смело делать так:
const API = '__API__'
const response = await fetch(`${API}/posts`)
const json = await response.json()
И использовать в шаблонах для ссылок и изображений с бэкенда:
<!-- Svelte компонент -->
<script>
const API = '__API__'
</script>
<img src="{API}/{file.path}" alt="{file.name}">