Введение

Данное руководство составлено на основе некоторого опыта, который был получен из книг и официальной документации. Вашему вниманию будет представлено 2 варианта написания простых в поддержке сайтов на Vue.js (с использованием backend систем и без). 

В первой части будет изложена инструкция по экспресс созданию сайта - галереи с использованием статичных JSON файлов для хранения данных сайта. 

Во второй части мы рассмотрим более сложный вариант, с использованием опен сорс CMS решения для управления данными сайта. Весь код в данном руководстве представлен в синтаксисе javascript/hml/css и bash.

Часть 1. Создаем просто сайт

Для быстрой установки шаблона Vue.js мы будем использовать инструмент Vite

npm create vite@latest

или

yarn create vite

Далее следуем подсказкам установщика:

✔ Project name: … vue_simple

✔ Select a framework: › Vue

✔ Select a variant: › JavaScript

Переходим в директорию проекта

cd vue-simple

Пример списка файлов в директории после установки шаблона
Пример списка файлов в директории после установки шаблона

Устанавливаем зависимости 

npm install

Запускаем в режиме разработки

npm run dev

В терминале выводится информация об успешном запуске локального сервера node js

Успешный запуск Vue в dev режиме
Успешный запуск Vue в dev режиме

Открываем браузер по указанному адресу:

Стартовая страница в браузере
Стартовая страница в браузере

Отлично, с “ctrl+c” и “ctrl+v” разобрались, далее начинаем продумывать свойства элементов нашей будущей галереи. Для этого в директории public создадим директорию json, и сохраним внутри файл gallery_1.json

mkdir public/json

touch public/json/gallery_1.json

Для упрощения, будем использовать 3 свойства:

  1. id - порядковый идентификатор картины

  2. name - наименование картины

  3. imgSrc - имя файла изображения

[
    {
        "id": 1,
        "name": "Иван Грозный",
        "imgSrc": "7cbed231a5d52ccbb1ee8a7c7bb753f2.jpg"
    },
    {
        "id": 2,
        "name": "Медный всадник",
        "imgSrc": "7cbed231a5d52ccbb1ee8a7c7bb753f2.jpg"
    },
    {
        "id": 3,
        "name": "Утро в сосновом лесу",
        "imgSrc": "7cbed231a5d52ccbb1ee8a7c7bb753f2.jpg"
    },
    {
        "id": 4,
        "name": "Девятый вал",
        "imgSrc": "7cbed231a5d52ccbb1ee8a7c7bb753f2.jpg"
    },
    {
        "id": 5,
        "name": "Апофеоз войны",
        "imgSrc": "7cbed231a5d52ccbb1ee8a7c7bb753f2.jpg"
    },
    {
        "id": 6,
        "name": "Черный квадрат",
        "imgSrc": "7cbed231a5d52ccbb1ee8a7c7bb753f2.jpg"
    },
    {
        "id": 7,
        "name": "Грачи прилетели",
        "imgSrc": "7cbed231a5d52ccbb1ee8a7c7bb753f2.jpg"
    },
    {
        "id": 8,
        "name": "Парижское кафе",
        "imgSrc": "7cbed231a5d52ccbb1ee8a7c7bb753f2.jpg"
    }
]

Для управлением состояния внутри Vue.js рекомендуется использовать хранилище Vuex. Это позволяет работать с изменением состояния ожидаемо.

Создадим хранилище для изображений. 

Для начал установим пакет

npm install vuex@next --save

или

yarn add vuex@next --save

Добавляем в main.js 

import { createStore } from 'vuex'

Мы будем использовать простой вариант реализации кода для Vuex в main.js (в реальном проекте лучше перенести его и подключать из отдельного js файла)

const store = createStore({
	state() {
    	return {
        	gallery: []
    	}
	},
	mutations: {
    	load(state, data) {
        	state.gallery = data;
    	}
	},
	actions: {
    	downloadGallery(context) {
        	return new Promise((resolve, reject) => {
            	axios.get("/json/gallery_1.json").
                	then((response) => {
                    	var array = response.data;
                    	context.commit("load", array);
                	}).catch((err=>{
                    	alert(err.message);
                	}));
        	})
    	}
	}
})

Действие downloadGallery будет вызываться из компонента App.vue. Внутри действия мы выполняем GET запрос (используя http клиент Axios), записывая полученные данные их через мутацию в хранилище Vuex.  В случае ошибки вызываем простое оповещение через встроенную в браузер функцию alert. 

В компоненте App.vue импортируем необходимые зависимости:

import {  onMounted } from "vue";
import { useStore } from "vuex";
import { reactive, computed } from "vue";

Подключаем хранилище

const store = useStore();

Создаем объект, в котором будем хранить статус загрузки данных:

const status = reactive({

  downloaded:false
  })

Создаем заголовок страницы

const h1 = reactive("Картинная галерея");

В хуке жизненного цикла onMounted инициируем загрузку данных в хранилище.

onMounted(() => {
	store
		.dispatch("downloadGallery")
		.then(() => {
			console.log("Галерея загружена.");
			status.downloaded = true;
		})
		.catch((err) => console.log(err));
});

Создаем computed свойство gallery, в котором будет храниться галерея из хранилища

const gallery = computed(() => {
  return store.state.gallery;
});

Далее, переходим к описанию шаблона страницы. Используя синтаксис Vue передаем данные в шаблон страницы. 

Заголовок выводится как есть, без каких-либо дополнительных операций над ним. Элемент p (абзац) отображается только в случае если downloaded возвращает true (для достижения такого поведения используется директива v-if).

Информацию о количестве картин в галерее, так-же получаем динамически, обращаясь к длине массива с картинами.

С помощью директивы v-for в цикле создается динамический список с нашими картинами, формируя простую галерею.

<template>
  {{ h1 }}
  <p v-if="status" class="downloaded">Последнее обновление: {{ new Date() }}</p>
<p>
  Количество картин в нашей галерее: {{ gallery.length }}
  <ol>
	<li v-for="item in gallery" :key="item.id">
  	<span>&nbsp{{ item.data.name }} </span>
  	<div>
    	<img class="gallery-img" :src="item.data.imgSrc" :alt="item.data.name">
  	</div>
	</li>
  </ol>
</p>
</template>

С помощью нехитрых стилей, выравниваем положение на странице.

<style scoped>
.downloaded{
  font-size: 0.8em;
}

ol {
  display: flex;
  flex-direction: column;
  justify-content: space-evenly;
  align-items: center;
}

li{
  text-align: center;
}

.gallery-img{
  width: 50%;
  height: auto;
 
}
</style>
Полный листинг App.vue
<script setup>
import {  onMounted } from "vue";
import { useStore } from "vuex";
import { reactive, computed } from "vue";

const store = useStore();

const status = reactive({
  downloaded:false
})

const h1 = reactive("Картинная галерея");

onMounted(() => {
 
  store
	.dispatch("downloadGallery")
	.then(() => {
  	console.log("Галерея загружена.");
  	status.downloaded=true;
	})
	.catch((err) => console.log(err));
});

const gallery = computed(() => {
  return store.state.gallery;
});
</script>

<template>
  {{ h1 }}
  <p v-if="status" class="downloaded">Последнее обновление: {{ new Date() }}</p>
<p>
  Количество картин в нашей галерее: {{ gallery.length }}
  <ol>
	<li v-for="item in gallery" :key="item.id">
  	<span>&nbsp{{ item.data.name }} </span>
  	<div>
    	<img class="gallery-img" :src="item.data.imgSrc" :alt="item.data.name">
  	</div>
	</li>
  </ol>
</p>
</template>

<style scoped>
.downloaded{
  font-size: 0.8em;
}

ol {
  display: flex;
  flex-direction: column;
  justify-content: space-evenly;
  align-items: center;
}

li{
  text-align: center;
}

.gallery-img{
  width: 50%;
  height: auto;
 
}
</style>

Больше никаких изменений в vue шаблоне от vite вносится не будет - запускаем проект в  dev режиме. Вернемся в браузер, и посмотрим на результат.

При открытии страницы, заглянем во вкладку “сеть” и обнаружим успешно выполненный запрос

Во вкладке “консоль” обнаружим запись об успешной загрузке данных

Ну и наконец, сама страница (наш компонент App.vue):

В настоящем (не обучающем) проекте на Vue рекомендуется разносить в разные файлы компоненты и страницы. Страница это глобальный компонент для страницы SPA, компонент это какая-то его деталь. 

Например, в нашем случае, можно было бы вынести элемент li в отдельный компонент (например, GalleryElement.Vue) в папке components, а единственную страницу в отдельный файл в папке pages (например, index.vue).

Часть 2. Подключаем CMS к галерее

В первой части, мы реализовали простой сайт на Vue, используя хранение данных в статичном файле с данными в json формате внутр. Через какое-то время наш проект может вырасти, и нам потребуется более простой доступ для контент менеджера и более сложный формат хранения данных (например типизация, связи между свойствами и сущностями). Для таких целей мы будем использовать CMS Strapi, которая из коробки создает интуитивно понятную систему для хранения контента, и получения его через REST API.

Данная система хорошо задокументирована, имеет хорошее сообщество на популярных ресурсах. Для быстрого старта, будем использовать официальное руководство.

Создадим отдельную директорию для нашего проетка

mkdir gallery-cms

Запустим процесс установки quickstart версии. По-умолчанию в качестве базы данных будет использоваться Sqlite3.

npx create-strapi-app@latest my-project --quickstart

Need to install the following packages:

  create-strapi-app@latest

Ok to proceed? (y) y

Дожидаемся окончания установки, запуск выполнится автоматически:

Экран терминала после успешного старта Strapi
Экран терминала после успешного старта Strapi

Открываем в браузере http://localhost:1337/ (порт по-умолчанию 1337, при желании его можно изменить в конфигурации config/server.js ), регистрируем учетную запись администратора.

Переходим в раздел управления коллекциями (Сontent-Type Builder), и создаем коллекцию для нашей галереи. 

Я не буду составлять пошаговую инструкцию по настройке коллекции и самой системы, но опишу базовые моменты, без которых не получится сделать минимальную конфигурацию, позволяющую управлять составом галереи и получать ее через REST API.

Создание коллекции
Создание коллекции

Создадим необходимые нам свойства для элементов галереи (свойство id будет создано автоматически):

  1. name - Текст

  2. imgSrc - Текст (В идеальном варианте, необходимо использовать тип “Media”, который позволяет загружать медиа файлы с устройства прямо в хранилище Strapi, данную функциональность системы можно попробовать самостоятельно, выбрав тип “Media” для свойства imgSrc)

    Создание свойства name
    Создание свойства name

    Заканчиваем настройку, сохраняем конфигурацию - системе после таких изменений необходимо перезапуститься, это может занять некоторое время.

    После перезапуска, переходим в раздел управления контентом, и создаем первый элемент галереи. Так как мы не стали тип Media для imgSrc, просто вписываем в поле адрес картинки, как в json файле.

    Создание элемента галереи
    Создание элемента галереи

    Перенесем список картин из json файла базу данных Strapi:

    В данном примере картины создаются вручную через пользовательский интерфейс, если данных накопилось слишком много, и переносить их вручную может показаться сумасшествием, можно воспользоваться REST API методами, которые создаются автоматически, при создании коллекции. Например после создания коллекции gallery, мы уже можем использовать GET запросы, для получения данных из системы. 

    curl --location 'http://localhost:1337/api/galleries'

    По-умолчанию все методы публичного API отключены, их надо включить в соответствующем разделе:

    Настройка доступа к API
    Настройка доступа к API
В ответ сервер вернет нам список картин в галерее:
{
	"data": [
		{
			"id": 1,
			"attributes": {
				"name": "Иван Грозный",
				"imgSrc": "7cbed231a5d52ccbb1ee8a7c7bb753f2.jpg",
				"createdAt": "2023-03-15T21:48:57.486Z",
				"updatedAt": "2023-03-15T21:49:06.317Z",
				"publishedAt": "2023-03-15T21:49:06.315Z"
			}
		},
		{
			"id": 2,
			"attributes": {
				"name": "Медный всадник",
				"imgSrc": "7cbed231a5d52ccbb1ee8a7c7bb753f2.jpg",
				"createdAt": "2023-03-15T21:49:27.326Z",
				"updatedAt": "2023-03-15T21:49:27.867Z",
				"publishedAt": "2023-03-15T21:49:27.864Z"
			}
		},
		{
			"id": 3,
			"attributes": {
				"name": "Утро в сосновом лесу",
				"imgSrc": "7cbed231a5d52ccbb1ee8a7c7bb753f2.jpg",
				"createdAt": "2023-03-15T21:49:41.684Z",
				"updatedAt": "2023-03-15T21:49:42.393Z",
				"publishedAt": "2023-03-15T21:49:42.391Z"
			}
		},
		{
			"id": 4,
			"attributes": {
				"name": "Девятый вал",
				"imgSrc": "7cbed231a5d52ccbb1ee8a7c7bb753f2.jpg",
				"createdAt": "2023-03-15T21:49:51.564Z",
				"updatedAt": "2023-03-15T21:49:52.311Z",
				"publishedAt": "2023-03-15T21:49:52.309Z"
			}
		},
		{
			"id": 5,
			"attributes": {
				"name": "Апофеоз войны",
				"imgSrc": "7cbed231a5d52ccbb1ee8a7c7bb753f2.jpg",
				"createdAt": "2023-03-15T21:50:02.978Z",
				"updatedAt": "2023-03-15T21:50:09.993Z",
				"publishedAt": "2023-03-15T21:50:09.990Z"
			}
		},
		{
			"id": 6,
			"attributes": {
				"name": "Черный квадрат",
				"imgSrc": "7cbed231a5d52ccbb1ee8a7c7bb753f2.jpg",
				"createdAt": "2023-03-15T21:50:21.597Z",
				"updatedAt": "2023-03-15T21:50:22.871Z",
				"publishedAt": "2023-03-15T21:50:22.868Z"
			}
		},
		{
			"id": 7,
			"attributes": {
				"name": "Грачи прилетели",
				"imgSrc": "7cbed231a5d52ccbb1ee8a7c7bb753f2.jpg",
				"createdAt": "2023-03-15T21:50:32.727Z",
				"updatedAt": "2023-03-15T21:50:33.433Z",
				"publishedAt": "2023-03-15T21:50:33.431Z"
			}
		},
		{
			"id": 8,
			"attributes": {
				"name": "Парижское кафе",
				"imgSrc": "7cbed231a5d52ccbb1ee8a7c7bb753f2.jpg",
				"createdAt": "2023-03-15T21:50:42.297Z",
				"updatedAt": "2023-03-15T21:50:43.046Z",
				"publishedAt": "2023-03-15T21:50:43.043Z"
			}
		}
	],
	"meta": {
		"pagination": {
			"page": 1,
			"pageSize": 25,
			"pageCount": 1,
			"total": 8
		}
	}
}

Теперь, нам необходимо переключить источник данных в нашем Vue хранилище.

actions: {
	downloadGallery(context) {
		return new Promise((resolve, reject) => {
			axios.get("http://localhost:1337/api/galleries").
				then((response) => {
					var array = response.data.data;
					context.commit("load", array);
					resolve();
				}).catch((err=>{
					alert(err.message);
					reject(err.message);
				}));
		})
	}
}

Изменим шаблон, так как структура объектов-картин изменилась

  <ol>
	<li v-for="item in gallery" :key="item.id">
  	<span>&nbsp{{ item.attributes.name }} </span>
  	<div>
    	<img class="gallery-img" :src="item.attributes.imgSrc" :alt="item.attributes.name">
  	</div>
	</li>
  </ol>

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

Пример простого скрипта на nodejs для подобной загрузки
//подключаем библиотеку fs (filesystem) для доступа к локальным файлам
var fs = require('fs');

//подключаем библиотеку для выполнения синхронных запросов к API
const request = require('sync-request');

// опции для выполнения запроса 
const options = {
    hostname: 'http://localhost:1337',
    path: '/api/galleries',
    headers: {
        'Content-Type': 'application/json',
        'Accept':'application/json'
    }
};

// С помощью readFile() читаем файл
fs.readFile('json/gallery_1.json', 'utf-8', function (err, data) {

    //парсим строку в объект
    var gallery = JSON.parse(data);

    //запускаем цикл вызовов API
     for (var i = 0; i < gallery.length; i++) {

        //подготовка тела запроса
        var data = {
            data: {
                name: gallery[i].name ,
                imgSrc: gallery[i].imgSrc
            }
        }
       
        //выводим подготовленное тело запроса в консоль
        console.log(data)
        

        //выполняем запрос на загрузку данных
         var res = request('POST', options.hostname + options.path, {
            json: data,
            headers:options.headers
        }); 

        //парсим ответ от сервера 
        //var res = JSON.parse(res.getBody('utf8'));

        //выводим ответ от сервера в консоль
        console.log(res.getBody('utf8'));
    } 
});

После запуска скрипта, в логах Strapi видны запросы на загрузку данных:

Возвращаемся на сайт и проверяем работоспособность загрузки и отображения данных.

Если запрос выполнен успешно, все данные отобразятся так же, как при получении в первой части.

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

Заключение

Strapi возможно не самый популярный CMS среди прочих, но он очень прост в установке и имеет много предустановленных функций, которые позволяют быстро развернуть интерфейс для пользователя и всю “подкапотную” магию для работ с БД и REST API. 

Вариант сайта, который мы рассматривали в первой части, хорошо подойдет для простых хостингов, где у владельца хостинга нет ничего кроме ftp папки для статических сайтов. Второй вариант, требует более низкого доступа к операционной системе, по сути полного. На серверах VDS/VPS данная схема работает отлично, но требует определенный минимум по ресурсам

Так же, при использовании методов API для изменения данных, рекомендуется использовать доступ по токену (не публичный) и применять дополнительные меры безопасности (Например, использование http авторизации для доступа к административной панели, помимо авторизации в самом Strapi). 

Клиентский код можно посмотреть тут.

Для удобства, при работе с Vue рекомендую использовать расширения для браузера VueDevtools 

Схематично процесс по получению данных для Vue можно изобразить на простой диаграмме последовательностей. 

Шаг 1 - Vue приложение запустилось в браузере

Шаг 2 - В хуке onMounted запускается процесс получения галереи

Шаг 3 - Выполняется запрос в действии хранилища

Шаг 4-5 - Работает Strapi (принимает запрос, получает данные из БД) и возвращает их на сайт

Шаг 6 - Применение данных в шаблоне

Шаг 7 - Отрисовка html в браузере

В заключение, для понимания, как это все связано между собой, набросал небольшую упрощенную схему в C4.

Надеюсь, чтиво Вам понравилось! Данная статья - это моя первая серьезная попытка на Хабре. Буду рад Вашим комментариям и критике.

Комментарии (7)


  1. little-brother
    00.00.0000 00:00
    +2

    const h1 = reactive("Картинная галерея");

    Я только начал разбираться с Vue 3, но вроде так не делают, а используют ref для примитивов.

    Для управлением состояния внутри Vue.js рекомендуется использовать хранилище Vuex.

    Уже pinia

    Надо попробовать такую схему API.

    C CORS проблем нет (dev сервер Vue будет же на другом порту чем CMS)?


    1. evoq
      00.00.0000 00:00

      Тоже интересно про CORS


    1. Spaceoddity
      00.00.0000 00:00

      Я только начал разбираться с Vue 3, но вроде так не делают, а используют ref для примитивов.

      Не делают. И даже ref-ссылки нет никакой нужды тут использовать, это "реактовщина" какая-то)) Во Vue же специальный html-образный синтаксис шаблонов как раз для этого - хоть хардкодь какой-то контент, хоть реактивные данные подключай. А не писать половину представления в шаблоне, а другую половину через константы развешивать...


  1. enkryptor
    00.00.0000 00:00
    +1

    Почему Vuex, а не рекомендуемая в текущей версии Pinia?


  1. interprise
    00.00.0000 00:00

    А какие альтернативы еще есть из серверных CMS на nodejs?


  1. Vadiok
    00.00.0000 00:00
    +1

    Ещё одна статья за короткое время, где стор отвечает за общение с сервером, а ответ хранится в сторе.

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


    1. doanonimit
      00.00.0000 00:00

      Ага, ещё и юзает страпи, который кроме примаков больше никаких ключей и индексов не поддерживает - миграции придумывайте сами, если какой уникальный надо нацепить или внешку