DevGang
Авторизоваться

Начать работу со Svelte

В этой статье я объясню (словами и кодом), как настроить приложение Svelte, создав простое приложение электронной коммерции с помощью Svelte. Это приложение не делает ничего особенного, но оно познакомит вас с некоторыми концепциями, которые вам понадобятся для создания чего-то сложного с помощью Svelte.

Это будет просто базовый магазин электронной коммерции с тремя маршрутами: «Магазин», «Сведения о продукте» и «Корзина». Мы не будем подключаться к какому-либо API или серверной службе, вместо этого мы будем использовать устаревшие фиктивные данные для отображения продуктов и контекстного хранилища для нашей корзины. Но прежде чем мы перейдем к этому, вот обзор содержания этой статьи и того, что мы создаем.

Обзор

  1. Что такое Svelte
  2. Создание нашего приложения
  3. Как писать код в Svelte
  4. Маршрутизация в Svelte
  5. Давайте напишем код

Что такое Svelte

«Svelte — это радикально новый подход к созданию  пользовательских интерфейсов. В то время как традиционные фреймворки, такие как React и Vue, выполняют большую часть своей работы в браузере, Svelte переносит эту работу на этап компиляции, который происходит при создании приложения.

Вместо использования таких методов, как сравнение виртуальных DOM, Svelte пишет код, который хирургическим путем обновляет DOM при изменении состояния вашего приложения».

Это означает, что Svelte — это платформа, которая позволяет разработчикам создавать производительные приложения с помощью HTML, CSS и Javascript и обрабатывает обновления приложений во время компиляции, поэтому ваше приложение изменяется при изменении состояния вашего приложения, но это делается во время компиляции, когда ваше приложение построен вместо Runtime.

Создание нашего приложения

В этом уроке мы будем использовать Svelte-Kit — официальную платформу для Svelte. Svelte-Kit поддерживает рендеринг на стороне сервера и рендеринг на стороне клиента, что отлично подходит для нашего варианта использования. Чтобы начать работу (при условии, что у вас установлены Node JS и npm), просто запустите эти команды в своем терминале.

setup-svelte.sh размещен на GitHub
npm create svelte@latest my-svelte-app
cd my-svelte-app
npm install
npm run dev --open

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

Как писать код в Svelte

Синтаксис Svelte прост в освоении и не требует большого количества кода для запуска и работы. Компоненты Svelte записываются в файл «.svelte» и в основном представляют собой просто HTML-файлы с ограниченной областью действия Javascript и CSS, см. пример ниже.

Одна важная вещь, которую нужно знать о Svelte, заключается в том, что каждый файл «example-component.svelte» может иметь только один компонент, то есть каждый файл является отдельным компонентом и по умолчанию экспортируется по умолчанию, поэтому вам не нужно делать «module.exports», но компромисс заключается в том, что вы не можете иметь подкомпоненты в одном файле (в отличие от декларативной природы React, которая позволяет вам иметь несколько компонентов в одном файле).

Пример синтаксиса файла компонента svelte
<script type="text/javascript">
  import Layout from '../components/layout/+page.svelte'; // the file is exported as a default component
  
  export let name; // a prop
  
  export outterFunc(){
    // body
    // can be imported from another file as > import Component, {outterFunc} from '../components/file/+page.js
   }

</script>

<div> <!-- This is a markup native to html -->
  <h1> Hello {name} </h1>
  <Layout /> <!-- This is a svelte component (always starts with an upper case -->
</div>

<style type="text/css"> <!-- also, text/scss, text/sass -->

</style>

Маршрутизация в Svelte

Прежде чем мы начнем программировать, давайте создадим несколько каталогов и файлов, соответствующих этой структуре папок, в my-svelte-app/src.

показывая ваш-проект/src/*<br>
показывая ваш-проект/src/*

Я уверен, что вы заметили странные соглашения об именах, используемые в этом проекте, позвольте мне объяснить их значение.

Таким образом, Svelte использует маршрутизатор на основе файловой системы, который сопоставляет маршруты в вашем приложении с папками в области каталога вашего приложения. Эти маршруты помещаются в каталог с удобным названием «routes» в «src/routes», что означает, что routes/profile/ и routes/settings будут создавать два маршрута — профиль и маршрут настроек в нашем приложении.

+page.svelte — это компонент маршрута (то, что визуализируется, когда пользователь посещает маршрут в нашем приложении), а 
+page.js — это функция, которая запускается перед визуализацией каждого связанного маршрута и заполняет каждый маршрут любыми данными . Может потребоваться при загрузке (аналогично getStaticProps в Next.js).

Теперь, когда у нас есть файловая структура и несколько файлов-компонентов — CartItem, Navbar и ProductCard для нашего простого приложения для электронной коммерции, теперь давайте приступим к созданию.

Для ясности отныне любой файл с расширением +page.js будет называться маршрутом, а файлы, оканчивающиеся на .svelte, — компонентами.

Для ясности отныне любой файл с расширением +page.js будет называться маршрутом, а файлы, оканчивающиеся на .svelte, — компонентами.

Давайте напишем код

Во-первых, давайте создадим наш макет. Мы хотим, чтобы отображение нашего приложения имело согласованный макет на разных маршрутах.

src/маршруты/+layout.svelte
<svelte:head>
	<link rel="stylesheet" type="text/css" href="/index.css">
</svelte:head>

<script type="text/javascript">
	import Navbar from '../components/Navbar.svelte';
	import GlobalStore from '../store';
	import {useContext} from '../utils';
	
	let store = useContext(GlobalStore);
</script>

<Navbar />

<div class="page">
	{#if store.notification.display}
		<div class="alert mb-2">
			<span> {store.notification.message} </span>
		</div>
	{/if}

	<slot> </slot>

</div>

<style type="text/css">
	.page{
		padding: 1.25rem;
	}

	.alert{
		background-color: teal;
		color:  #fff;
		font-size: 16px;
		padding: 15px;
		border-radius: 2px;
		position: fixed;
		top: 50px;
		z-index: 20;
		right: 10px;
		animation: fadeIn .5s forwards;
	}

	@keyframes fadeIn{
		from{
			opacity: 0;
		}
		to{
			opacity: 1;
		}
	}
</style>

Нам нужно создать несколько вспомогательных функций, чтобы облегчить нашу разработку и упростить наш синтаксис.

src/utils/index.js
import GlobalStore from '../store';

export function notify(message){
	const state = useContext(GlobalStore)
	function showAlert(){
		GlobalStore.update((newValue) => {
			return {
				...state,
				notification:{
					message,
					display: true,
				}
			}
		})
	}

	function hideAlert(){
		GlobalStore.update((newValue) => {
			return {
				...state,
				notification:{
					message,
					display: false,
				}
			}
		})
	}

	showAlert();
	setTimeout(() => hideAlert(), 2500)
}

export function useContext(ctx){
	let data;
	ctx.subscribe(val => {data = val});
	return data;
}

export function setContext(ctx, key, value){
	// use as 
	// ctx = setContext(ctx, 'user', 'johndoe@gmail.com')
	ctx.update(() => {
		return {
			...ctx,
			key: value
		}
	})
}

Мы ссылались на GlobalStore, но еще не создали наш магазин, для этого нам нужно использовать API магазина Svelte для создания доступного для записи хранилища, в котором будут храниться данные о наших продуктах, сохранять данные нашей корзины на протяжении всего жизненного цикла нашего приложения в глобальном масштабе, и разрешите нам отображать уведомления.

Svelte имеет три типа магазинов

  1. Хранилища для чтения — только для чтения (например, сведения о пользователе).
  2. Хранилища с возможностью записи — чтение и запись
  3. Производные магазины.
источник/магазин.
import {writable} from 'svelte/store';
import img from './sample.jpg';


export const GlobalStore = writable({
	products: [
	    {
	      id: 1,
	      name: 'Christian Dior TShirt',
	      price: 200.99,
	      image: img
	    },
	    {
	      id: 2,
	      name: 'Jordans 96',
	      price: 620.25,
	      image: img
	    },
	    {
	      id: 3,
	      name: 'Alven Cruella Jeans',
	      price: 120.00,
	      image: img
	    },
	    {
	      id: 4,
	      name: 'Jordans 96',
	      price: 620.25,
	      image: img
	    },
	    {
	      id: 5,
	      name: 'Christian Dior TShirt',
	      price: 200.99,
	      image: img
	    }],
	cart: [],
	notification: {
		display: false,
		message: null
	}
})

export default GlobalStore;

Вставьте это в свой src/routes/+page.svelte. Причина для этого файла в том, что Svelte ожидает, что индексный маршрут будет соответствовать базовому пути «https://example.com/», и у нас нет целевой страницы, поэтому мы просто отображаем StorePage как индексный маршрут, т.е. «/src/routes/+page.svelte».


<script type="text/javascript">
	import StorePage from './store/+page.svelte';
</script>

<StorePage />

Теперь давайте создадим наш компонент StorePage, который будет служить нашей целевой страницей и витриной. Вставьте этот фрагмент кода в «/src/routes/store/+page.svelte».

<script>
	import {useContext} from '../../utils';
	import GlobalStore from '../../store';
	import ProductCard from '../../components/ProductCard.svelte';
	
	let products = useContext(GlobalStore).products;
</script>

<div>
	<h2 class="page-title"> {"Storefront"} </h2>

	<div class="slider"></div>

	<div class="item-list">
		{#each products as item}
			<ProductCard {item} />
		{/each}
	</div>	
</div>

<style type="text/css">
	.slider{
		height: 250px;
		width: 100%;
		border-radius: 10px;
		background-color: lavender;
	}

	.item-list{
		margin: 10px 0px;
		display: flex;
		flex-wrap: wrap;
		justify-content: center;
		width: 100%;
	}
</style>

Теперь для нашей страницы сведений о продукте нам нужно создать пару файлов «src/routes/store/[itemId]/+page.js» и «src/routes/store/[itemId]/+page.svelte» соответственно.

Теперь, в чем разница между этими файлами? «+page.js» — это специальный файл, который svelte запускается перед любым +page.svelte и позволяет нам анализировать параметры маршрута и передавать данные фактическому +page.svelte — то, что видит пользователь.

Функция с именем «load» должна быть экспортирована из +page.js, и вы возвращаете объект Javascript, который затем доступен в +page.svelte в виде переменной с именем «data». Svelte также передает функции загрузки несколько других параметров, таких как url, fetch и другие.

Если вы хотите получить данные из серверной службы или API, вы должны делать это только в своей функции загрузки +page.js, а не в +page.svelte.

Внимание — «load» и «data» — зарезервированные имена, поэтому не используйте их для своих переменных и функций, чтобы не столкнуться с ошибками в будущем.

import GlobalStore from '../../../store';
import {useContext} from '../../../utils';


export function load({params}){
	const itemId = Number(params.itemId);
	const item = useContext(GlobalStore)
				.products
				.find(it => it.id === itemId)
	return {
		item
	}
}
src/routes/store/[itemId]/+page.js &amp; src/routes/store/[itemId]/+page.svelte
<script>
	import GlobalStore from '../../../store';
	import {notify, useContext, setContext} from '../../../utils';
	export let data; // data loading from '+page.js'
	
	// app state
	let 
	quantity = 1,
	loading=false,
	ctx = useContext(GlobalStore),
	cart = ctx.cart,
	item = data.item;

	function addToCart(){
		freezBuy()
		let cartItem = {
			id: cart.length + 1,
			item,
			quantity,
		}
		cart = [...cart, cartItem]
		GlobalStore.update((newValue) => {
			return {
				...ctx,
				cart
			}
		})
		notify("Item has been added to your cart")
	}

	// click rate limiting
	function freezBuy() {
		loading = true;
		setTimeout(() => loading = false, 1500)
	}

	function increase(){
		quantity += 1
	}

	function reduce(){
		if (quantity > 1) quantity -= 1
	}
</script>

<div>
	<h2 class="page-title"> {"Item Detail"} </h2>

	<div class="wrapper">
		<div class="image">
			<img alt={item.name} src={item.image} class="w-100">
		</div>

		<div class="details">
			<h2 class=""> {item.name} </h2>
			<h3 class=""> $ {item.price} </h3>
			<h3 class="">
			 {
			 		data.item.about || 
					` There is currently no description for this item. However, this is a
					 placeholder value that can be replaced at any time. Beware!
					`
			} </h3>

			<div>
				<div class="control mb-2">
					<button class="quantity-control" on:click={reduce}> &minus; </button>
					<input disabled type="number" class="input" name="quantity" bind:value={quantity}>
					<button class="quantity-control" on:click={increase}> &plus; </button>
				</div>

				<button on:click={addToCart} disabled={loading} class="cta"> {
					loading ? "Please Wait..." : "Add to cart"
				} </button>
			</div>
		</div>
	</div>
</div>

<style type="text/css">
	.control{
		display: flex;
	}

	.wrapper{
		display: flex;
		flex-wrap: wrap;
		justify-content: space-between;
	}

	.input{
		padding: 12px;
		width: 100%;
		max-width: 100px;
	}

	.quantity-control{
		padding: 12px 16px;
		font-weight: 900;
	}

	.wrapper .image{
		width: 100%;
		max-width: 350px;
		height: 300px;
	}

	.wrapper .image img{
		width: 100%;
		height: 100%;
	}

	.wrapper .details{
		padding: 1.5rem;
		flex-grow: 1;
		width: 50%;
		max-width: 100%;
	}

	.cta{
		padding: 15px 20px;
		width: 100%;
		max-width: 250px;
		background-color: #05081fba;
		font-size: 17px;
		font-weight: 600;
		color: #fff;
		border: none;
		transition: .3s;
		border-radius: 3px;
	}

	.cta:disabled{
		background-color: #9e9e9e;
	}

	.btn:hover{
		opacity: .8;
	}
</style>

В строке 4 мы видим оператор «export let data;» это данные, которые передаются нашему компоненту функцией загрузки +page.js. Синтаксис немного странный, но именно так мы объявляем реквизиты в Svelte, поэтому, если вы хотите передать реквизит компоненту следующим образом: «<NameTag name=»John Doe» />» в вашем файле NameTag.svelte, вы бы экспортировали имя переменной, т.е. «export let name».

Кроме того, когда вы хотите реализовать некоторую логику в своем коде, например, условное выражение или цикл, Svelte имеет специальные разметки для этого — это чем-то похоже на язык шаблонов Django и Handlebars.


<!-- If Condition -->
{#if loading}
<p> Loading... </p>
{:else}
<!-- Render something -->
{/if}

<!-- Looping -->
{# each list as listItem}
<!-- Render something with listItem -->
{/each}

Также при привязке обратных вызовов к событиям или состояния приложения к элементам ввода мы используем этот шаблон для: <event≥={<action>} и bind:value={<state>}, см. пример ниже.

<script type="text/js">
  let name;
  
  function handleClick(){
    console.log(name);
  }
</script>

<div>
  <input bind:value={name} /> <!-- Automatically updates name when input value changes -->
  
  <button on:click={handleClick}> Click Me </button>
</div>

Теперь мы собираемся создать наши компоненты. В папке «src/components» создайте три файла компонентов, т. е. CartItem.svelte, Navbar.svelte и ProductCard.svelte. Вставьте эти коды в аналогичном порядке (см. имена файлов ниже)

CartItem.svelte, Navbar.svelte, ProductCard.svelte
<script type="text/javascript">
	export let item, onRemove;
</script>

<div class="item">
	<div class="wrapper">
		<div class="image">
			<img alt="product" src={item.item.image} />
		</div>

		<button class="cta-remove" on:click={onRemove(item.id)}> Remove </button>
	</div>

	<div>
		<p> <span>(x{item.quantity})</span> {item.item.name}  </p>
		<p>Sub Total: ${item.item.price * item.quantity}  </p>
	</div>	
</div>

<style type="text/css">
	.item{
		border-left: 1px solid black;
		border-right: 1px solid black;
		border-top: 1px solid black;
		padding:  10px;
	}

	.item p{
		margin-bottom: 0px;
	}

	.item:last-child{
		border-bottom: 1px solid black;
	}

	.wrapper{
		display: flex;
		align-items: center;
		justify-content: space-between;
	}

	.image{
		width: 70px;
		height: 70px;
		border: 1px solid black;
	}

	.image img{
		width: 100%;
		height: 100%;
	}

	.cta-remove{
		padding: 10px;
		background-color: #263238;
		color: #fff;
		border-radius: 5px;
		border: none;
	}

	button:hover{
		opacity: .79;
	}
</style>
<script type="text/javascript"></script>

<div>
	<nav class="navbar">
		<h4 class="brand"> Svelte - Com</h4>

		<ul class="nav">
			<li> <a class="nav-link" href="/"> Store </a> </li>
			<li> <a class="nav-link" href="/cart"> Cart </a> </li>
		</ul>
	</nav>
</div>

<style type="text/css">
	.brand{
		margin: 0px 10px;
		color: #fff;
	}
	.navbar{
		top: 0;
		padding: 5px;
		display: flex;
		position: sticky;
		align-items: center;
		background-color: #252e2e;
		justify-content: space-between;
	}

	.nav{
		list-style: none;
		display: flex;
		flex-grow: 1;
		padding: 0px;
		max-width: max-content;
	}

	.nav li{
		margin: 0px 5px;
	}

	.nav .nav-link{
		text-decoration: none;
		color: #fff;
		transition: .3s;
		font-weight: 600;
		padding: 10px 25px;
		border-radius: 3px;
	}
	.nav-link:hover{
		background-color: #80808054;
	}	
</style>
<script type="text/javascript">
	export let item;
</script>

<div class="list-item">
	<div class="card-header">
		<a href="/store/{item.id}" class="w-100">
			<img src={item.image} alt="{item.name}">
		</a>
	</div>
	<div class="card-body">
		<a href="/store/{item.id}">
			<h3 class="m-1">{item.name}</h3>
		</a>
		<p class="m-1">$ {item.price}</p>
	</div>
</div>

<style type="text/css">
	.card-header{
		height: 170px;
		width: 100%;
	}

	a{
		text-decoration: none;
		color: inherit;
	}

	.card-header img{
		width: 100%;
		height: 100%;
		border-radius: 5px 5px 0 0;
	}

	.list-item{
		box-shadow: 10px 10px #80808030;
		width: 100%;
		max-width: 300px;
		margin: 10px 10px;
		border-radius: 3px;
		background-color: #fbfbfbf5;
	}

	.card-body{
		padding: 10px;
	}
</style>

Наше приложение почти готово, но нам не хватает последней страницы, поэтому теперь мы создадим страницу корзины, где пользователи могут видеть свои товары в корзине. Создайте +page.svelte в src/routes/cart/ и вставьте его в свой редактор.

src/маршруты/корзина/+page.svelte
<script type="text/javascript">
	import CartItem from '/src/components/CartItem.svelte';
	import {useContext, notify} from '../../utils';
	import GlobalStore from '../../store';
	
	const ctx = useContext(GlobalStore);
	let 
	idx,
	cart = ctx.cart.reverse(),
	cartTotal = get_cart_total();

	function onRemove(id){
		idx = cart.findIndex(elem => elem.id === id)
		cart.splice(idx, 1)
		cart = cart.reverse()
		GlobalStore.update((val) => {
			return {
				...ctx,
				cart
			}
		})
		notify("Item was removed from your cart")
		cartTotal = get_cart_total()
	}

	function get_cart_total() {
		return cart.length
	}
</script>

<div class="container">
	
	<h2 class="w-100">Your Cart 
		{#if count < 1}
			is empty
		{:else}
			<span class="badge"> {cartTotal} </span>
		{/if}
	</h2>

	{#if cart.length > 0}
		{#each cart as item}
			<CartItem {item} {onRemove} />
		{/each}
	{/if}
</div>

<style type="text/css">

	.container{
		max-width: 900px;
		margin: auto;
	}
</style>

Мы закончили создание нашего приложения. Теперь, если вы запустите «npm run dev», вы увидите свое приложение в реальном времени по адресу «http://localhost:5173».

#Svelte
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу