svelte-draft-with-router-stores-dotenv

Svelte Draft With Router Stores Dotenv

Черновик Svelte 3 с настроенным роутером, примером использования хранилища, примером запроса к REST API, примером использования файлов .env и настроенным ESLint (Standard).

Черновик Svelte 3

Черновик Svelte 3 с настроенным роутером на базе Page.js, примером использования хранилища (Stores), примером запроса к REST API (JSONPlaceholder), примером использования переменных из файлов .env и настроенным ESLint (стиль Standard).

Вступление

Я разрабатывал на Vue веб-приложение для мебельной компании, когда решил попробовать Svelte. Сначала я хотел вести разработку приложения на Vue и Svelte параллельно, чтобы сравнить их возможности в бою, но к сожалению, время поджимало и мне пришлось сосредоточиться на чём-то одном. Приложение на Vue готово, но теперь я не вижу смысла в дописывании его на Svelte и выкладывании для сообщества, тем более оно завязано на непубличный API. Я решил, что для сообщества будет полезнее создать черновик с настроенным роутером и получением данных с Fake REST API. Возможно в этом черновике вы найдёте полезные для себя части кода или информацию. Ниже я постарался подробно описать как что работает.

При копировании содержания данного .md целиком, большая просьба указывать источник 👍.

Содержание

Структура проекта

Из коробки 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 – хранилища с данными которые нужны по всему проекту и постоянно используются.

Роутер через Page.js

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>

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

Хранилища (Stores)

Для хранилищь используются возможности 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>.

.env (environment variables)

Есть одна проблема, если при разработке клиента вы работаете с локальным бэкендом, то скорее всего вы прописываете в файлах 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}">

Top categories

svelte logo

Want a Svelte site built?

Hire a Svelte developer
Loading Svelte Themes