- PVSM.RU - https://www.pvsm.ru -
Evershop https://evershop.io [1] — опенсорсная платформа для создания интернет-магазинов на Node.js. Платформа включает необходимый функционал: интеграции с платежными системами (Stripe, PayPal), админ-панель для управления заказами и контентом, систему промокодов и аналитику продаж.
Официальная документация Evershop не покрывает многие практические аспекты разработки. Данная статья — практическое руководство по созданию интернет-магазина на основе Evershop, включающее:
Пошаговую настройку проекта
Создание тем и расширений
Решение типичных проблем и необходимые патчи
Описание внутренней архитектуры для разработки кастомных решений
Важно: Для корректной работы некоторых функций (например, PayPal) и избежания ошибок при оформлении заказов необходимо применить патчи, описанные в статье. Рекомендуется прочитать материал полностью перед началом разработки, это сильно облегчит вам жизнь, если вы решите создавать Evershop приложение.
Создание Evershop проекта с нуля по инструкции в документации [2]
Написание themes [4]
Переводы [11]
Миграции данных [12]
Патчи [13]
Настройка PayPal [19]
Способы доставки [20]
Полезные ресурсы [21]
Если вы используете данную статью, как руководство, тогда читайте параллельно документацию, т.к. тут я лишь комментирую моменты, на которые следует обратить внимание и которые документация оставляет неясными.
Если вы запускаете приложение не через Docker, тогда вам нужно иметь пустую PostgreSQL базу данных.
Затем запускаете команду npx create-evershop-app [название проекта]
npx create-evershop-app new-clear-evershop-app
Далее нужно будет указать хост базы данных, порт, название БД, имя пользователя и его пароль. А также данные администратора.
Часть скриптов в package.json добавляется сразу на этапе create-evershop-app, но часть почему-то нужно добавлять вручную.
Причем скрипт dev нас, получается, просят удалить? Вместо этого предлагается скрипт start:debug, который тоже запускается на порту 3000 и, вероятно, выполняет те же функции. Зачем два скрипта, делающих одно и то же, и почему в ходе первоначальной настройки нам говорят заменить один другим — непонятно.
Мою настройку скриптов можно посмотреть по ссылке https://github.com/Luca324/BuildMeister/blob/master/package.json [22]
Далее в документации нас просят запустить скрипт setup. Он снова просит указать информацию о БД и создать пользователя. Скорее всего, он нужен на тот случай, если во время создания проекта эти этапы выполнить не удалось.
Далее сборка и запуск проекта. Сборка длится довольно долго, может занимать минут 5-10. Поэтому при разработке настоятельно рекомендую использовать dev режим. При открытии каждой страницы будет заново запускаться процесс пересборки, он тоже занимает ощутимое время, иногда до минуты, но это уже лучше, чем пересобирать проект полностью после каждого изменения.
npm start и приложение запустится.
Также важно создать правильные файлы конфигурации для dev и production режимов. Если вы разрабатываете на localhost, то homeUrl должен быть localhost:3000 и localhost соответственно.
Это необходимо для корректной работы ссылок: чтобы с localhost:3000/home при переходе по ссылке на /admin вы попали на localhost:3000/admin, а не на localhost/admin.
Если сайт размещен на домене, тогда вместо localhost нужно указать ваш домен: http://yourdomain.ru:3000 [23] и http://yourdomain.ru [24] соответственно.
Тема в Evershop — это набор компонентов, стилей и статических файлов, которые переопределяют внешний вид и поведение магазина.
В системе может быть активна только одна тема в момент времени.
Evershop предоставляет тему по умолчанию sample для демонстрационных целей. Но для настройки интерфейса под себя необходимо создать новую тему.
Базовая структура темы выглядит так:
themes/my-theme/
├── package.json
├── tsconfig.json
├── src/
│ ├── pages/ # Компоненты страниц (организованы по роутам)
│ │ ├── all/ # Компоненты для всех страниц
│ │ ├── homepage/ # Пример: только для главной
│ │ ├── categoryView/# Пример: для страницы категории
│ │ └── productView/ # Пример: для страницы товара
│ ├── components/ # Переопределения компонентов
│ │ └── frontStore/ # Компоненты фронтенда
│ │ └── admin/ # Компоненты админки
│ └── css/ # Глобальные стили
│ └── global.scss
└── public/ # Статические файлы
├── logo.png
└── favicon.ico
Можно просто скопировать содержимое темы по умолчанию sample в качестве основы.
Этот шаг желателен для того, чтобы переопределять стили сразу по всему сайту. Достаточно создать src/css/global.scss для глобальных стилей темы и импортировать его в Layout компоненте.
Я переопределила Layout.tsx в том числе для того, чтобы поменять местами меню и лого в хедере.
Для этого в themes/tech/src/pages/all вставила копию файла Layout.jsx из исходного кода и внесла необходимые изменения.
Мой Layout компонент смотри здесь: Layout.jsx [25]
1. src/pages/ — компоненты страниц
Компоненты организованы по роутам Evershop. Структура папок определяет, на каких страницах компонент будет отображаться:
all/ — компоненты отображаются на всех страницах
homepage/ — только на главной странице
categoryView/ — страница категории товаров
productView/ — страница отдельного товара
catalogSearch/ — результаты поиска
Пример: Компонент логотипа для всех страниц
// themes/my-theme/src/pages/all/Logo.jsx
import React from "react";
export default function Logo() {
return (
<div>
<a href="/">
<img src="/logo.png" alt="logo" style={{ maxHeight: "50px" }} />
</a>
</div>
);
}
export const layout = {
areaId: "header",
sortOrder: 5,
};
2. src/components/ — переопределения компонентов
Вы можете переопределить стандартные компоненты Evershop, создав их копии в теме:
src/components/frontStore/catalog/product/list/List.jsx
Путь компонента должен точно соответствовать пути в исходном коде Evershop. Тема автоматически заменит стандартный компонент на ваш.
Пример: Переопределение списка товаров
// themes/my-theme/src/components/frontStore/catalog/product/list/List.jsx
import React from 'react';
export default function ProductList({ products = [], countPerRow = 3 }) {
// Ваша кастомная реализация
return (
<div className="my-custom-product-list">
{products.map(product => (
<ProductCard key={product.productId} product={product} />
))}
</div>
);
}
3. src/css/ — глобальные стили
Тут можно положить файл global.scss с общими стилями, который можно импортировать, например, в Layout.tsx, либо файлы стилей для отдельных страниц, и импортировать их оттуда.
4. public/ — статические файлы
Файлы из папки public/ доступны по корневому пути сайта (например, так они указываются в config/default.json):
public/logo.png → доступен как /logo.png
public/favicon.ico → доступен как /favicon.ico
Перед использованием темы необходимо скомпилировать TypeScript код:
cd themes/my-theme
npm run tsc
Активируйте тему в config/default.json:
{
"system": {
"theme": "my-theme"
}
}
Это создаст необходимые файлы для работы темы.
Расширение — это модуль, который добавляет новую функциональность в магазин. В отличие от темы, расширения не меняют внешний вид, а добавляют новые возможности: виджеты, фильтры, страницы, GraphQL типы и т.д.
Примеры расширений:
Виджет для отображения категорий
Кастомные фильтры для товаров
Дополнительные страницы (например, статусы заказов)
Интеграции с внешними сервисами
Разберем создание нового расширения на примере создания виджета категорий. У меня он находится на главной странице и показывает список категорий, которые мы выберем в админ-панели.
Подробнее про создание расширений: Extensions Documentation [26]. (прим.: тут в структуре файлов не упомянута папка components, содержащая widgets, но она содержится в другой статье документации — о создании виджетов (об этом читай дальше))
Шаг 1: Создание структуры папок
Базовая структура расширения:
extensions/my-extension/
├── package.json
├── tsconfig.json
└── src/
├── bootstrap.ts # Точка входа, регистрация функциональности
├── components/ # React компоненты
│ └── widgets/ # Компоненты виджетов
├── services/ # Сервисы (регистрация фильтров, процессоров)
├── pages/ # Дополнительные страницы (опционально)
└── types/ # GraphQL типы (опционально)
└── MyType/
└── MyType.graphql
Примечание: GraphQL типы (src/graphql/ или src/types/) нужны только в редких случаях, когда требуется кастомная обработка данных на сервере. В большинстве случаев достаточно стандартных GraphQL запросов Evershop (products, categories, collection).
Виджет — это переиспользуемый компонент, который можно добавить на любую страницу через админ-панель.
Подробнее: Widgets Documentation [27]
Bootstrap функция — точка входа расширения
Вызывается при загрузке расширения Evershop
Здесь регистрируется вся функциональность расширения
// extensions/my-extension/src/bootstrap.ts
import path from "path";
import { fileURLToPath } from "url";
import { registerWidget } from "@evershop/evershop/lib/widget";
import registerCategoryIdFilter from "./services/registerCategoryIdFilter.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default () => {
// регистрация процессоров (опционально)
// …
// Регистрация виджета
registerWidget({
type: "categories_widget", // уникальный идентификатор виджета (используется в БД)
name: "Categories Widget", // название в админке
description: "Display selected categories", // описание
settingComponent: path.resolve( // компонент настроек для админки
__dirname,
"components/widgets/CategoriesWidgetSetting.js"
),
component: path.resolve( // основной компонент виджета (отображается на фронтенде)
__dirname,
"components/widgets/CategoriesWidget.js"
),
enabled: true, // включен ли виджет
defaultSettings: { categories: [] }, // настройки по умолчанию (сохраняются в БД при создании)
});
};
Компонент виджета:
// extensions/categories_widget/src/components/widgets/CategoriesWidget.tsx
import React from 'react';
import PropTypes from 'prop-types';
export default function CategoriesWidget({ categories }: CategoriesWidgetProps) {
const categoriesList = categories?.items || [];
return (
<div className="categories__widget">
<div>
<h3>Kategorier</h3>
<div>
{categoriesList.map((category) => (
<a key={category.uuid} href={category.url || `#category-${category.uuid}`}>
{/* ... отображение изображения категории и названия ... */}
</a>
))}
</div>
</div>
</div>
);
}
/**
* GraphQL запрос для получения данных виджета
* Использует стандартный запрос Evershop categories с фильтром category_id.
* Фильтр работает благодаря процессору registerCategoryIdFilter().
*/
export const query = `
query Query($categoryIds: [String]) {
categories(filters: [
{key: "category_id", operation: in, value: $categoryIds}
]) {
items {
categoryId
uuid
name
url
image { url alt }
}
}
}
`;
export const variables = `{
categoryIds: getWidgetSetting("categories", [])
}`;
Полный код компонента: CategoriesWidget.tsx [28]
Компонент настроек виджета:
Компонент настроек позволяет администратору выбирать категории для отображения в виджете через админ-панель. Использует стандартный компонент CategorySelector из Evershop и модальное окно для выбора категорий.
// extensions/categories_widget/src/components/widgets/CategoriesWidgetSetting.tsx
export default function CategoriesWidgetSetting({
categoriesWidget: { categories = [] },
}) {
const [selectedCategoryIds, setSelectedCategoryIds] = React.useState<number[]>(categories);
// ... логика выбора категорий через модальное окно ...
return (
<div>
{/* ... UI для отображения выбранных категорий и их выбора ... */}
{/* Скрытые поля для сохранения выбранных категорий */}
{selectedCategoryIds.map((categoryId, index) => (
<input
key={categoryId}
type="hidden"
name={`settings[categories][${index}]`}
value={categoryId}
/>
))}
</div>
);
}
export const query = `
query Query($settings: JSON) {
categoriesWidget(settings: $settings) {
categories
}
}
`;
/**
* getWidgetSetting() - получает настройки виджета из БД (таблица WIDGET.settings)
* В компоненте настроек это текущие сохраненные настройки виджета
*/
export const variables = `{
settings: getWidgetSetting()
}`;
Полный код компонента: CategoriesWidgetSetting.tsx [29]
Процессоры позволяют модифицировать поведение системы на разных этапах выполнения. Они используются для добавления новых фильтров для коллекций (товаров, категорий и т.д.).
Зачем нужны процессоры:
Виджет использует фильтр в GraphQL запросе (например, category_id)
Без процессора этот фильтр не будет работать
Процессор добавляет поддержку фильтра в SQL запросы
Когда НЕ нужны процессоры:
Виджет использует только стандартные фильтры Evershop
Виджет не использует фильтрацию в GraphQL запросах
Пример: Регистрация фильтра для коллекций
В данном примере используется процессор registerCategoryIdFilter, который добавляет поддержку фильтрации категорий по category_id. В репозитории также присутствует registerCategoryUuidFilter.ts — аналогичный процессор для фильтрации по uuid категорий, но он не используется в данном примере виджета.
// extensions/my-extension/src/services/registerCategoryIdFilter.ts
// @ts-ignore - EverShop resolves these modules at runtime
import { OPERATION_MAP } from '@evershop/evershop/lib/util/filterOperationMap';
// @ts-ignore - EverShop resolves these modules at runtime
import { addProcessor } from '@evershop/evershop/lib/util/registry';
// @ts-ignore - types resolved at runtime
import { value } from '@evershop/postgres-query-builder';
/**
* Регистрация процессора (фильтра) для категорий
* Как это работает:
* 1. Виджет делает GraphQL запрос с фильтром category_id
* 2. Evershop вызывает все процессоры для 'categoryCollectionFilters'
* 3. Наш процессор добавляет SQL условие: WHERE category.category_id IN (1, 2, 3)
* 4. Запрос выполняется с фильтром
*
* См. также: src/components/widgets/CategoriesWidget.tsx - использование фильтра
*/
export default () => {
/**
* Регистрация процессора для фильтров коллекции категорий
*
* 'categoryCollectionFilters' - имя точки расширения (hook point)
* (filters: any[]) => {...} - функция-обработчик
* 10 - приоритет выполнения (меньше = выполняется раньше)
*/
addProcessor('categoryCollectionFilters', (filters: any[]) => {
/**
* Добавляем новый фильтр в список доступных фильтров
*/
filters.push({
key: 'category_id', // Имя фильтра (используется в GraphQL)
operation: ['in', 'eq'], // Поддерживаемые операции: IN (массив) и = (равно)
/**
* Callback функция вызывается при использовании фильтра
*
* @param query - объект SQL запроса (для добавления условий WHERE)
* @param operation - операция фильтра ('in' или 'eq')
* @param val - значение фильтра (может быть массивом или одним значением)
* @param currentFilters - массив текущих фильтров (для отображения в UI)
*/
callback: (query: any, operation: string, val: string | string[] | number | number[], currentFilters: any[]) => {
if (operation === 'in') {
// Операция IN - фильтр по нескольким значениям
// Преобразуем все значения в числа
const ids = Array.isArray(val)
? val.map((v) => {
const num = typeof v === 'string' ? parseInt(v, 10) : v;
return isNaN(num) ? null : num;
}).filter((v) => v !== null)
: [typeof val === 'string' ? parseInt(val, 10) : val].filter((v) => !isNaN(v));
if (ids.length > 0) {
// Добавляем SQL условие: WHERE category.category_id IN (1, 2, 3)
query.andWhere('category.category_id', 'IN', value(ids));
// Сохраняем фильтр для отображения в UI
currentFilters.push({
key: 'category_id',
operation,
value: val,
});
}
} else if (operation === 'eq') {
// Операция = (равно) - фильтр по одному значению
const num = typeof val === 'string' ? parseInt(val, 10) : (typeof val === 'number' ? val : null);
if (num !== null && !isNaN(num)) {
// Добавляем SQL условие: WHERE category.category_id = 1
query.andWhere('category.category_id', OPERATION_MAP[operation], num);
// Сохраняем фильтр для отображения в UI
currentFilters.push({
key: 'category_id',
operation,
value: val,
});
}
}
},
});
return filters;
}, 10);
};
Регистрация в bootstrap:
// extensions/my-extension/src/bootstrap.ts
import registerCategoryIdFilter from "./services/registerCategoryIdFilter.js";
export default () => {
registerCategoryIdFilter();
// далее регистрация виджетов (опционально)
};
cd extensions/my-extension
npm run tsc
Добавьте расширение в config/default.json:
{
"system": {
"extensions": [
{
"name": "my-extension",
"resolve": "extensions/my-extension",
"enabled": true,
"priority": 20
}
]
}
}
Area — это система композиции компонентов в Evershop, которая позволяет автоматически размещать компоненты в нужных местах страницы без ручной регистрации. Компоненты объявляют, где они должны отображаться, а система Area автоматически их находит и рендерит.
Area — это React компонент, который рендерит все компоненты, зарегистрированные для определенной области страницы:
import Area from '@components/common/Area';
// В Layout компоненте
<Area id="header" /> // Рендерит все компоненты с areaId: "header"
<Area id="content" /> // Рендерит все компоненты с areaId: "content"
<Area id="footer" /> // Рендерит все компоненты с areaId: "footer"
Подробнее: The View System [30]
Чтобы компонент автоматически появился в Area, нужно экспортировать объект layout.
Покажу на примере переопределения компонента Logo.jsx, который присутствует на всех страницах в области header.
// themes/your-theme/src/pages/all/Logo.jsx
import React from "react";
export default function Logo() {
return (
<div>
<a href="/">
<img src="/logo.png" alt="logo" />
</a>
</div>
);
}
// Регистрация в системе Area
export const layout = {
areaId: "header", // В какой Area отображать
sortOrder: 5 // Порядок отображения (меньше = выше)
};
areaId определяет, в какую область макета будет вставлен компонент.
Его необходимо указать при создании компонента в экспорте layout. При создании виджета layout не экспортируется (т.к. элемент добавляется на страницы вручную из админки), поэтому и areaId указывать не нужно.
Примеры области:
body — корневой макет (Layout компонент)
header — шапка сайта
content — основная область контента
footer — подвал
rightColumn — правая колонка (например, в категориях)
leftColumn — левая колонка
productPageMiddleRight — область на странице товара
oneColumn — одна колонка (для страниц поиска)
Вы можете использовать любые кастомные areaId в своих компонентах.
Где можно получить полный список возможных areaId? А нигде. В исходном коде. Но в официальном репозитории почему-то присутствуют не все файлы, тут уж я разбираться не стала. Мой способ увидеть все возможные areaId: 1) скопировать папку @evershop [31]/evershop из node_modules и сделать поиск по строке 'React.createElement(Area, { id: "'.
Можно заметить, что areaId зачастую повторяются из-за того, что area с одинаковыми id встречаются на разных страницах. Поэтому важно создать компонент в правильной директории, соответствующей названию страницы, на которой вы хотите, чтобы он был отображен. Скоро поговорим об этом подробнее.
sortOrder определяет порядок компонентов в одной области. Компоненты сортируются по возрастанию:
// Компонент A
export const layout = {
areaId: "header",
sortOrder: 5 // Отобразится первым
};
// Компонент B
export const layout = {
areaId: "header",
sortOrder: 10 // Отобразится вторым
};
Структура папок src/pages/ соответствует роутам Evershop:
all/ — компоненты, отображаемые на всех страницах
homepage/ — только на главной странице
categoryView/ — страница категории
productView/ — страница товара
catalogSearch/ — результаты поиска
Пример: Компонент в pages/all/ будет отображаться на всех страницах, а в pages/homepage/ — только на главной.
Подробнее: Theme Structure [32]
// themes/your-theme/src/pages/all/EveryWhere.tsx
import React from 'react';
export default function EveryWhere() {
return (
<div>
<p>Этот компонент виден на всех страницах</p>
</div>
);
}
export const layout = {
areaId: 'content',
sortOrder: 20
};
// themes/your-theme/src/pages/homepage/OnlyHomePage.tsx
import React from 'react';
export default function OnlyHomePage() {
return (
<div>
<p>Этот компонент виден только на главной странице</p>
</div>
);
}
export const layout = {
areaId: 'content',
sortOrder: 10 // Отобразится перед EveryWhere (sortOrder: 20)
};
// themes/your-theme/src/pages/categoryView/Products.jsx
import React from 'react';
export default function Products({ products }) {
return (
<div>
{products.items.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
export const layout = {
areaId: 'rightColumn',
sortOrder: 25
};
Build Time (Сборка): Webpack loader (AreaLoader) сканирует все компоненты страницы
Парсинг: Находит export const layout в каждом компоненте
Группировка: Группирует компоненты по areaId
Регистрация: Автоматически регистрирует их в Area.defaultProps.components
Runtime: Компонент Area использует эту информацию для рендеринга
Подробнее: Templating [33]
Вы можете создавать собственные области в Layout:
// themes/your-theme/src/pages/all/Layout.jsx
import Area from '@components/common/Area';
export default function Layout() {
return (
<>
<Area id="header" />
<main>
<Area id="content" />
{/* Ваша кастомная область */}
<aside>
<Area id="sidebar" />
</aside>
</main>
<Area id="footer" />
</>
);
}
Теперь компоненты с areaId: "sidebar" будут автоматически отображаться в этой области.
Theme Overview [34] — общий обзор системы тем
The View System [30] — подробно о системе Area и композиции
Templating [33] — система шаблонов и компонентов
Theme Structure [32] — структура темы и организация
В Evershop используется сложная для понимания система GraphQL запросов, напоминающая очень костыльное решение. Для каждого роута (страницы) все GraphQL запросы компонентов этой страницы объединяются в один запрос и сохраняются в файл query.graphql.
Проблема в том, что некоторые значения для запросов (например, фильтры из URL или настройки виджетов) доступны только в runtime, когда существует объект request с контекстом HTTP-запроса. Поэтому Evershop использует обходное решение: вместо реальных значений в запросах сохраняются маркеры-заглушки с аргументами функций в base64-кодировании.
Конкретно: когда в компоненте указано getContextValue("filtersFromUrl") или getWidgetSetting("collection"), на этапе сборки эти функции не могут быть выполнены (объекта request еще нет). Поэтому аргументы этих функций кодируются в base64 и сохраняются как маркеры вида "getContextValue_ImZpbHRlcnNGcm9tVXJsIg==". Эти маркеры становятся частью структуры запроса и сохраняются в файле query.graphql внутри поля variables.*.source (где * - это уникальный ключ компонента). При выполнении запроса (runtime) файл query.graphql загружается, маркеры извлекаются из этого поля, декодируются из base64, и функции выполняются с реальным объектом request, получая актуальные значения из контекста запроса. Затем маркеры заменяются на реальные значения, и финальный GraphQL запрос выполняется.
Если пишете расширение (или тему), которое хотите опубликовать — хорошим тоном будет предусмотреть интернационализацию. В Evershop предусмотрен i18n. Жаль только, что по интернационализации в расширениях 0 документации. Я изучила исходный код и обнаружила, что все слова обернуты в функции перевода. Функций две: одна __(), другая translate(). Насколько я поняла, первая применяется на этапе сборки webpack. А вторая как-то по-другому. Я использую функцию __(): экспортирую ее из @evershop/evershop/lib/locale/translate/ и оборачиваю в нее исходные фразы на английском.
Сами переводы размещаются в папке translations/язык/, как и сказано в документации https://evershop.io/docs/development/knowledge-base/translation [35], в виде .csv файлов с двумя колонками: первая — фразы на английском, вторая — их перевод на целевой язык. Если разместить все и в одном файле, то должно тоже работать, но принято разделить на 5: account, catalog, checkout, general, paypal.
Я лично писала JS скрипты. В теории можно было использовать сервисы для PostgreSQL миграций, однако я выбрала импорт из CSV файлов, чтобы было удобнее генерировать данные с нейросетью.
Мои скрипты можно посмотреть в репозитории https://github.com/Luca324/BuildMeister/tree/master/scripts [36]
Некоторый базовый функционал не работал, пока я не изменила исходный код.
Патч файла evershop/dist/lib/webpack/createBaseConfig.js, без которого у меня не работал перевод для некоторых компонентов. Может, тут я что-то не доглядела.
test: /[\/][A-Z][^\/]*.(js|jsx)$/
Патч можно посмотреть здесь: [https://github.com/Luca324/BuildMeister/blob/master/patches/%40evershop%2Bevershop%2B2.0.1.patch [37]]
Почему-то просто без этого патча у меня не все переводы через _() применялись.
Написала патч для расширения s3_file_storage, чтобы он работал с Yandex S3. Yandex S3 был выбран потому что там есть достаточный объем и бесплатное количество запросов.
Настройка Yandex S3:
Для начала необходимо в console.yandex.cloud создать бакет. Затем по этой инструкции https://yandex.cloud/ru/docs/iam/operations/authentication/manage-access-keys#create-access-key [38] получить ID ключа доступа и секретный ключ доступа.
И добавить следующие переменные окружения в файл .env:
AWS_ACCESS_KEY_ID=your_access_key_id # ID ключа доступа (в интерфейсе Yandex S3)
AWS_SECRET_ACCESS_KEY=your_secret_access_key # Секретный ключ доступа
AWS_BUCKET_NAME=your_bucket_name # Имя бакета
AWS_REGION=ru-central1 # Регион
AWS_ENDPOINT=https://storage.yandexcloud.net # Endpoint для Yandex S3 (`https://storage.yandexcloud.net`)
AWS_S3_FORCE_PATH_STYLE=true
Патч можно посмотреть здесь: [https://www.npmjs.com/package/@luca324/yandex-evershop-s3 [39]]
Патч, который делает кнопку "Continue to payment" disabled, если заполнены не все поля. Подробнее описано в разделе "Способы доставки" [40].
Патч можно посмотреть здесь: https://github.com/Luca324/BuildMeister/blob/master/patches/%40evershop%2Bevershop%2B2.0.1.patch [37]
Патч, который делает поле city обязательным для оплаты. Подробнее описано в разделе "Настройка PayPal" [41].
Патч можно посмотреть здесь: https://github.com/Luca324/BuildMeister/blob/master/patches/%40evershop%2Bevershop%2B2.0.1.patch [37]
Это патч, который мне пришлось сделать, т.к. разработчики не предусмотрели, что клиент тоже должен видеть статус заказа.
Для реализации создается расширение order_status_display, которое переопределяет компонент списка заказов и добавляет отображение статусов через стандартный компонент Badge из Evershop (@components/common/Badge).
Структура расширения:
extensions/order_status_display/
├── package.json
├── tsconfig.json
└── src/
├── bootstrap.ts
├── pages/frontStore/account/OrderHistory.tsx
└── components/frontStore/customer/detail/OrderWithStatuses.tsx
Переопределение страницы OrderHistory:
Страница переопределяется для добавления полей shipmentStatus и paymentStatus в GraphQL запрос:
// extensions/order_status_display/src/pages/frontStore/account/OrderHistory.tsx
import OrderWithStatuses from '../../../components/frontStore/customer/detail/OrderWithStatuses.js';
export default function OrderHistory({ customer }) {
const orders = customer?.orders || [];
return (
<div className="order-history">
{orders.map((order) => (
<div key={order.orderId}>
<OrderWithStatuses order={order} />
</div>
))}
</div>
);
}
export const layout = {
areaId: 'accountPageOrderHistory',
sortOrder: 10,
};
export const query = `
query Query {
customer: currentCustomer {
orders {
orderId
orderNumber
createdAt { text }
shipmentStatus {
name
code
badge
progress
}
paymentStatus {
name
code
badge
progress
}
grandTotal { text }
items {
productName
thumbnail
productPrice { text }
productSku
qty
}
}
}
}
`;
2. Компонент OrderWithStatuses:
Компонент переопределяет стандартный Order и добавляет отображение статусов:
// extensions/order_status_display/src/components/frontStore/customer/detail/OrderWithStatuses.tsx
import Badge from '@components/common/Badge';
import { _ } from '@evershop/evershop/lib/locale/translate/_';
export default function OrderWithStatuses({ order }) {
const { shipmentStatus, paymentStatus } = order;
// Функция для определения progress по коду статуса
const getProgress = (statusCode, statusType) => {
if (statusType === 'shipment') {
return statusCode === 'delivered' ? 'complete' : 'incomplete';
} else {
return statusCode === 'paid' ? 'complete' : 'incomplete';
}
};
return (
<div className="order">
<div className="order-inner grid grid-cols-1 md:grid-cols-3 gap-8">
{/* ... отображение товаров заказа и другой информации ... */}
{/* Отображение статусов */}
{(shipmentStatus || paymentStatus) && (
<div className="order-statuses flex gap-12 mt-4">
{shipmentStatus && (
<div className="shipment-status">
<span>{_('Shipment')}:</span>
<Badge
title={_(shipmentStatus.name)}
variant={shipmentStatus.badge}
progress={shipmentStatus.progress || getProgress(shipmentStatus.code, 'shipment')}
/>
</div>
)}
{paymentStatus && (
<div className="payment-status">
<span>{_('Payment')}:</span>
<Badge
title={_(paymentStatus.name)}
variant={paymentStatus.badge}
progress={paymentStatus.progress || getProgress(paymentStatus.code, 'payment')}
/>
</div>
)}
</div>
)}
</div>
</div>
);
}
Полный код расширения: order_status_display [42]
Чтобы привязать оплату PayPal, нужно в админ-панели сайта указать два значения: Client Id, client secret. Эти значения должен будет получить заказчик. У PayPal есть исчерпывающая статья на этот счет:
Также можно посмотреть видео https://youtu.be/povhVRdCExc?si=drIiPodSv8QiJMnj [44]
Важный момент: для того, чтобы оплата прошла, обязательно должно быть заполнено поле city, которое в Evershop не является обязательным. Эту проблему я тоже решила при помощи патча (см. раздел "Патчи" [45]).
Доставка в Evershop организована следующим образом:
1. Создание shipping zone
Вы создаете shipping zone. Нужно указать страну и перечислить провинции/города (из выпадающего списка), на которые распространяется эта зона. Если не указывать провинции, то зона будет охватывать всю страну. Если не указывать страну, то зона будет охватывать весь мир.
2. Добавление методов доставки
Добавляете методы доставки для этой зоны. Это может быть Самовывоз, Почта и т.д. Есть гибкая система условий для формирования цены в зависимости от стоимости заказа и его веса.
Если пользователю не доступен ни один способ доставки, например, потому что пользователь находится в другой географической зоне, тогда в списке методов доставок будет сообщение «Sorry, there is no available method for your address», и пользователь не сможет выбрать метод доставки, однако кнопка оформления заказа все равно будет активной, и при ее нажатии Evershop будет кидать ошибку «Cannot read properties of undefined (reading 'code')», но не скажет, что проблема в том, что поле «способ доставки» не может быть пустым. Поэтому будьте осторожны и всегда имейте минимум один возможный способ доставки для всей территории страны. Пользователь, увидев «Cannot read properties of undefined», решит, что сайт сломался.
Я решила эту проблему, написав патч, который делает кнопку disabled, если заполнены не все поля формы (в т.ч. метод доставки). См. раздел "Патчи" [45].
ВАЖНО:
Всегда должен быть хотя бы один метод доставки для страны.

Автор: Luca324
Источник [48]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ecommerce/441373
Ссылки в тексте:
[1] https://evershop.io: https://evershop.io
[2] Создание Evershop проекта с нуля по инструкции в документации: #sozdanie-proekta
[3] Файлы конфигурации: #fayly-konfiguracii
[4] Написание themes: #napisanie-themes
[5] Написание extensions: #napisanie-extensions
[6] Создание нового расширения: #sozdanie-rasshireniya
[7] Регистрация функциональности: #registraciya-funkcionalnosti
[8] Компиляция и активация расширения: #kompilyaciya-rasshireniya
[9] Система Area в Evershop: #sistema-area
[10] Для любопытных: система GraphQL запросов: #graphql-zaprosy
[11] Переводы: #perevody
[12] Миграции данных: #migracii-dannyh
[13] Патчи: #patchi
[14] Патч файла evershop/dist/lib/webpack/createBaseConfig.js: #patch-webpack
[15] Yandex S3 storage: #yandex-s3
[16] Патч кнопки "Continue to payment": #patch-knopki
[17] Патч обязательного поля city: #patch-city
[18] Добавление статусов заказов на стороне клиента: #statusy-zakazov
[19] Настройка PayPal: #nastroyka-paypal
[20] Способы доставки: #sposoby-dostavki
[21] Полезные ресурсы: #poleznye-resursy
[22] https://github.com/Luca324/BuildMeister/blob/master/package.json: https://github.com/Luca324/BuildMeister/blob/master/package.json
[23] http://yourdomain.ru:3000: http://yourdomain.ru:3000
[24] http://yourdomain.ru: http://yourdomain.ru
[25] Layout.jsx: https://github.com/Luca324/BuildMeister/blob/master/themes/tech/src/pages/all/Layout.jsx
[26] Extensions Documentation: https://evershop.io/docs/development/module
[27] Widgets Documentation: https://evershop.io/docs/development/widget
[28] CategoriesWidget.tsx: https://github.com/Luca324/BuildMeister/blob/master/extensions/categories_widget/src/components/widgets/CategoriesWidget.tsx
[29] CategoriesWidgetSetting.tsx: https://github.com/Luca324/BuildMeister/blob/master/extensions/categories_widget/src/components/widgets/CategoriesWidgetSetting.tsx
[30] The View System: https://evershop.io/docs/development/theme/view-system
[31] @evershop: https://www.pvsm.ru/users/evershop
[32] Theme Structure: https://evershop.io/docs/development/theme/theme-overview
[33] Templating: https://evershop.io/docs/development/theme/templating
[34] Theme Overview: https://evershop.io/docs/development/theme
[35] https://evershop.io/docs/development/knowledge-base/translation: https://evershop.io/docs/development/knowledge-base/translation
[36] https://github.com/Luca324/BuildMeister/tree/master/scripts: https://github.com/Luca324/BuildMeister/tree/master/scripts
[37] https://github.com/Luca324/BuildMeister/blob/master/patches/%40evershop%2Bevershop%2B2.0.1.patch: https://github.com/Luca324/BuildMeister/blob/master/patches/%40evershop%2Bevershop%2B2.0.1.patch
[38] https://yandex.cloud/ru/docs/iam/operations/authentication/manage-access-keys#create-access-key: https://yandex.cloud/ru/docs/iam/operations/authentication/manage-access-keys#create-access-key
[39] https://www.npmjs.com/package/@luca324/yandex-evershop-s3: https://www.npmjs.com/package/@luca324/yandex-evershop-s3
[40] в разделе "Способы доставки": #%D1%81%D0%BF%D0%BE%D1%81%D0%BE%D0%B1%D1%8B-%D0%B4%D0%BE%D1%81%D1%82%D0%B0%D0%B2%D0%BA%D0%B8
[41] в разделе "Настройка PayPal": #%D0%BD%D0%B0%D1%81%D1%82%D1%80%D0%BE%D0%B9%D0%BA%D0%B0-paypal
[42] order_status_display: https://github.com/Luca324/BuildMeister/tree/master/extensions/order_status_display
[43] https://developer.paypal.com/api/rest/?_ga=2.1033913.984222656.1766907053-1493702913.1765184715#link-getclientidandclientsecret: https://developer.paypal.com/api/rest/?_ga=2.1033913.984222656.1766907053-1493702913.1765184715#link-getclientidandclientsecret
[44] https://youtu.be/povhVRdCExc?si=drIiPodSv8QiJMnj: https://youtu.be/povhVRdCExc?si=drIiPodSv8QiJMnj
[45] см. раздел "Патчи": #%D0%BF%D0%B0%D1%82%D1%87%D0%B8
[46] Официальная документация Evershop: https://evershop.io/docs
[47] Репозиторий Evershop на GitHub: https://github.com/evershopcommerce/evershop
[48] Источник: https://habr.com/ru/articles/983870/?utm_source=habrahabr&utm_medium=rss&utm_campaign=983870
Нажмите здесь для печати.