Блог /

Laravel + Nuxt.js: отличный тех. стек для веб-приложений

Зачем вообще разрабатывать CRM-систему?

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

Действительно, какой смысл разрабатывать что-то своё, если на рынке уже есть готовые решения? Это извечный вопрос, ответ на который не так однозначен и очевиден.

Лидером среди готовых решений по праву является Битрикс24. Читайте нашу статью на эту тему: Добро пожаловать в новую цифровую эпоху вместе с Битрикс24.

Первоначальная стоимость вложений во внедрение готового продукта намного ниже чем в разработку своего собственного. Но дальнейшая работа с таким продуктом может с лихвой «перебить» сэкономленные средства, если:

  1. Нет чёткого представления о том, как работает приобретенное ПО. Как с помощью имеющихся в нём инструментов решить задачи бизнеса;
  2. Нет гибкости в построении бизнес-процессов. С готовым решением точно возникнут моменты, когда придётся «подвинуться», подстроить свои действия под его возможности;
  3. Персонал недостаточно «продвинут» и одновременно с этим не открыт к новому, не хочет переучиваться и воспринимает нововведения «в штыки».

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

С индивидуальной разработкой CRM всё наоборот. Вы получаете продукт, который тесно интегрирован именно с вашими процессами, не имеет никакого «лишнего» функционала. Работа будет доставлять радость. Клиенты будут довольны быстрым и качественным обслуживанием. Но, ничего не получится если:

  1. Разработчик не погружён в отрасль, не изучил специфику вашего бизнеса;
  2. Бизнес недостаточно серьёзно отнёсся к процессу проектирования и составления ТЗ, не консультирует разработчика должным образом;
  3. Персонал воспринимает нововведения «в штыки» и отсутствует безапеляционная директива руководства на использование нового разработанного ПО.

Своя "рубашка" ближе к телу, но ухаживать за ней придётся самостоятельно. Принимая решение разрабатывать свою CRM-систему, нужно быть готовым к тому, что её придётся поддерживать, обновлять и развивать за свой счёт. Финансирование "по остаточному принципу" здесь точно не подойдёт. Система станет вашим незаменимым сотрудником, которому тоже нужна "зарплата". 

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

Что нужно сделать?

Требуется создать CRM-систему, которая покрывала бы все потребности сети косметологических салонов по работе с собственниками, сотрудниками и клиентами. Тезисно и в очень простом варианте задачу можно сформулировать пунктами:

  1. В системе должны присутствовать типы пользователей: супер-админ, собственник салона, сотрудник салона, клиент;
  2. Супер-админ создаёт салоны;
  3. В салоне может быть назначен один собственник. Один и тот же пользователь может быть собственником нескольких салонов;
  4. Собственник создает сотрудников;
  5. Сотрудник может работать в нескольких салонах;
  6. Собственник составляет календарный график работы по каждому сотруднику;
  7. Собственник создает список услуг, группируя их по типам (разделам);
  8. Собственник создает список доп. товаров, которые будут предлагаться клиентам при посещении салонов;
  9. Сотрудник-менеджер создаёт запись Клиента на определенный набор процедур, на определенное время и длительность к определенному Сотруднику-мастеру. При этом учитывается доступность желаемого времени с поправкой на предполагаемую длительность;
  10. Есть процедуры, которые могут проходить параллельно с двумя клиентами;
  11. Есть абонементы, где первая и последняя процедуры бесплатно;

Выбор технологического стека

Как видно из заголовка мы остановились на связке Laravel + Nuxt.js. Нам было важно построить независимый отдельный API чтобы его можно было использовать на фронтэнде, в мобильном приложении и в виджете записи на сайте или ещё где-то. Основывать выбор инструментов для разработки можно на следующих принципах:

  1. Опыт использования. Чем лучше команда разработчиков владеет инструментом, тем более предсказуемо будет происходить сам процесс разработки;
  2. Актуальность. Буквально с каждым днём становится всё больше и больше новых библиотек и фреймворков. В момент старта проекта есть определенный набор трендов, которые очень бы хотелось попробовать. И тут может быть две крайности: взять новые и популярные инструменты, которые плохо изучены командой или довериться старым но проверенным решениям. Время от времени всё-таки нужно обновлять свой арсенал. Поэтому здесь требуется та самая «золотая» середина;
  3. Устойчивость. Это про то, насколько хорошо командой изучен тот или иной инструмент в плане потребления ресурсов. Адекватны ли доступные серверные мощности заявленному стеку. Есть ли мониторинг и какой-то алгоритм действий, когда вдруг что-то пойдёт не так и возникнут проблемы с доступностью разрабатываемого сервиса;
  4. Удобство разработки. Одну и ту же задачу можно выполнить на абсолютно разных стеках и связках. Чем лучше будет чувствовать себя кодовая база тем проще будет её развивать и обслуживать;

Истина находится где-то на стыке всех четырёх утверждений, пропущенных через призму доступных ресурсов и обозначенных ограничений. Если сильно увлечься новыми крутыми фреймворками, не имея запаса по срокам и бюджету, легко можно выйти за рамки взаимовыгодного сотрудничества с заказчиком. Будет сложно доказать что, к примеру, из-за использования сыроватого но классного инструмента пришлось немного отвлечься на рефакторинг и нарушить согласованный дедлайн. Ну и дальше сами знаете, взаимообмен любезностями и подпорченные отношения с бизнесом. А хотели как лучше.

Поэтому очень важна согласованная тонкая настройка по каждому из пунктов.

Вернёмся к нашему проекту по созданию CRM. Взвесив все за и против, мы выбрали:

  1. Laravel для создания API;
  2. Nuxt.js для создания фронтэнда;
  3. Twitter Bootstrap для построения интерфейсов;
  4. PUG для разметки HTML;
  5. SASS для стилей;

Структура системы

Структурно у нас всё просто:

  1. API (Laravel) на поддомен api._DOMAIN_.com;
  2. Фронтэнд размещаем на crm._DOMAIN_.com;
  3. Виджет живёт там, куда мы его встроим (если будет нужно разработать);
  4. Приложения для iOS и Android (если будет нужно разработать);
  5. При необходимости можно будет завязать на API ещё что-нибудь. Например, приложение ВКонтакте. 

Фронтэнд, виджет и приложения будут общаться с API посредством GET / PUT / POST / DELETE запросов, получая и отправляя данные.

По железу у нас есть виртуальный выделенный сервер с 4гб оперативной памяти. На нём установлено веб-окружение от 1С Битрикс. Создаём новый сайт api._DOMAIN_.com по модели kernel для нашего API на Laravel. Все необходимые PHP модули уже есть, ничего дополнительно ставить не придётся. Единственный нюанс с веб окружением от Битрикса состоит в том, что в корневой директории сайта должна находиться папка bitrix, иначе в консольном меню данный сайт отобразится с ошибкой. Но это не проблема, оставляем папку пустой и едем дальше.

С нашим фронтендом ситуация чуть сложнее и одновременно проще. Для создания crm._DOMAIN_.com будем использовать только Nginx, где настроим проксирование на запущенное node js приложение, предварительно скомпилированное и задеплоеное в продакшн.

Конфиг Nginx достаточно простой:

server {
	listen 443 http2;
  
	server_name crm._DOMAIN_.com www.crm._DOMAIN_.com;
  
	access_log /var/log/nginx/crm_access.log main;
	error_log  /var/log/nginx/crm_error.log warn;

	server_name_in_redirect off;

	include bx/conf/ssl_options.conf;

	ssl_certificate   /_PATH_TO_CERT_/cert.pem;
	ssl_certificate_key  /_PATH_TO_CERT_/privkey.pem;
	ssl_trusted_certificate /_PATH_TO_CERT_/fullchain.pem;

	proxy_set_header  X-Forwarded-Proto https;

	location / {
		proxy_pass http://localhost:3000;
		proxy_http_version 1.1;
		proxy_set_header Upgrade $http_upgrade;
		proxy_set_header Connection 'upgrade';
		proxy_set_header Host $host;
		proxy_cache_bypass $http_upgrade;
	}
}

API на Laravel

Вообще мы любим фреймворк CakePHP, выпустили на нём в продакшн немало проектов, от простых админок до нагруженных сайтов и веб-приложений. Прошли весь путь версий, от 1 до 4 (на момент написания статьи). И очень довольны. Фреймворк развивается, у него очень хорошая концепция, близкая нам по духу :) и достаточно «чистые» и понятные conventions.

Но не Кейком единым. Пришло время попробовать в продакшне фреймворк из мэйнстримовых. И знаете что? Laravel по-своему хорош, есть приятные фичи, хорошо развита сущностная составляющая, ORM весьма и весьма удобен.

База данных, роутинг и авторизация

Прежде чем приступить к непосредственному написанию API, следует спроектировать структуру БД и endpoints. В Laravel есть простой и удобный механизм миграций, с помощью которого можно создавать и модифицировать таблицы в БД. Это удобно, поскольку вы централизованно храните структуру БД и историю её изменений и можете «развернуть» проект в любой момент с чистого листа. Например, файл миграции для создания таблицы для хранения записей клиентов:

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateRecordsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('records', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->integer('org_id');
            $table->integer('client_id');
            $table->integer('user_id');
            $table->dateTime('date');
            $table->decimal('summ', 9, 3);
            $table->decimal('summ_minus', 9, 3);
            $table->string('pay_type');
            $table->boolean('payed')->default(false);
            $table->timestamps();

            $table->index('org_id');
            $table->index('client_id');
            $table->index('user_id');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('records');
    }
}

Файлы для указания правил роутинга находятся в папке /routes проекта. Здесь мы оставили пустым web.php, поскольку используем только API правила. А живут они в файле api.php. Роуты удобно группируются. Например, для работы с записями клиентов на процедуры нам понадобится следующая группа правил:

// Records
Route::group(['prefix' => 'records'], function () {
    Route::get('/{org}/{from}/{to}', 'RecordController@index')->middleware('auth:api');
    Route::post('/make/{org}', 'RecordController@make')->middleware('auth:api');
    Route::post('/edit', 'RecordController@edit')->middleware('auth:api');
    Route::get('/clients', 'RecordController@clients')->middleware('auth:api');
    Route::post('/add/{org}', 'RecordController@add')->middleware('auth:api');
    Route::delete('/{record}', 'RecordController@destroy')->middleware('auth:api');
});

Для доступа к прописанным эндпоинтам мы используем middleware, где реализуем логику по проверке доступа. Авторизация пользователей происходит по стандарту JWT (Json Web Token). При работе с API самое то.

Когда приходит запрос к API, проверяется наличие и актуальность токена доступа. Далее, в зависимости от роли пользователя и endpoint’а, происходит уточнение его роли и возможность доступа к запрашиваемому действию при помощи механизма policies. Сам файл /app/Policies/UserPolicy.php очень простой:

<?php

namespace App\Policies;

use App\User;
use Illuminate\Auth\Access\HandlesAuthorization;

class UserPolicy
{
    use HandlesAuthorization;

    /**
     * Create a new policy instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    public function index(User $user)
    {
        return $user->isAdmin();
    }

    public function owner(User $user)
    {
        return $user->isOwner();
    }

    public function worker(User $user)
    {
        return $user->isWorker();
    }
}

Здесь происходит проверка на соответствие трём ролям: супер-админ, собственник и сотрудник. Функции-обёртки обращаются к соответствующим методам класса User.

Не можем не поделиться тем, насколько красивый ORM в Laravel, и как здорово реализуются связи «Многие-ко-многим» с дополнительной информацией. Например, когда собственник салона создаёт сотрудника-мастера, у него есть возможность указать процент, который получит мастер с определённой услуги. Причём один и тот же человек может работать в разных салонах и оказывать разные услуги с разным процентом.

Данные хранятся в табличке serv_user с простой структурой:

id
serv_id (id услуги)
user_id (id пользователя)
percent (процент)

А связь «Мастер» — «Салон» формируется путём прикрепления услуги к определенному салону.

В итоге, чтобы получить список услуг мастера с процентами, мы используем красивую ORM конструкцию, обёрнутую в функцию, которая регламентирует связь «Пользователь» — «Услуга»:

public function servs()
{
	return $this->belongsToMany('App\Serv', 'serv_user')->withPivot('percent');
}

При добавлении или обновлении сущностей через API-запросы, удобно использовать кастомизированные Request и Recource. Например, функция добавления услуги:

public function add(ServAddRequest $request)
{
	$userInstance = new User();
	$this->authorize('owner', $userInstance);

	$serv = new Serv;
	$serv->name = $request->name;
	$serv->servtype_id = $request->servtype_id;
	$serv->price = $request->price;
	$serv->price_first = $request->price_first;
	$serv->price_last = $request->price_last;
	$serv->procedures = $request->procedures;
	$serv->time = $request->time;
	$serv->can_parallel = $request->can_parallel;
	$serv->desc = $request->desc;

	$serv->save();

	if (!empty($request->orgs)) {
		$orgsIdsToAttach = [];

		foreach ($request->orgs as $tmp) {
			$orgsIdsToAttach[] = $tmp['id'];
		}

		if (!empty($orgsIdsToAttach)) {
			$serv->orgs()->attach($orgsIdsToAttach);
		}
	}

	return new ServResource($serv);
}

ServAddRequest при этом выглядит так:

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class ServAddRequest extends FormRequest
{
	/**
	 * Determine if the user is authorized to make this request.
	 *
	 * @return bool
	 */
	public function authorize()
	{
		return true;
	}

	/**
	 * Get the validation rules that apply to the request.
	 *
	 * @return array
	 */
	public function rules()
	{
		return [
			'name' => 'required',
			'price' => 'required',
			'time' => 'required',
			'servtype_id' => 'required',
			'orgs' => 'required',
		];
	}

	/**
	 * Get the error messages for the defined validation rules.
	 *
	 * @return array
	 */
	public function messages()
	{
		return [
			'name.required' => 'Введите название типа услуги',
			'price.required' => 'Введите цену услуги',
			'time.required' => 'Введите длительность услуги',
			'servtype_id.required' => 'Выберите тип услуги',
			'orgs.required' => 'Укажите организации, которые могут использовать тип услуги',
		];
	}
}

Особенно здорово писать правила валидации и сообщения об ошибках.

В целом очень здорово и удобно разрабатывать API на Laravel. Иии.. Переходим посмотреть что там у нас на фронтэнде с Nuxt.js.

Фронтэнд на Nuxt.js

Nuxt.js — это фреймворк на Vue.js. Из коробки вы получаете продвинутый набор утилит, роутинг, модульность и набор best practices для построения вашего приложения. Подробности на официальном сайте https://nuxtjs.org

Мы создали проект Nuxt.js в режиме SPA. Этот режим лучше всего подходит для веб-сервисов. Если же вы разрабатываете сайт, где требуется быть СЕО-фрэндли, нужно воспользоваться режимом Server Side Rendering. Тогда поисковые роботы без проблем смогут проиндексировать всё содержимое.

Конфигурация

Конфигурация проекта находится в файле nuxt.config.js в корне приложения. Здесь указывается набор параметров как для приложения целиком, так и для подключаемых модулей. Для построения интерфейса мы решили использовать старый добрый проверенный Twitter Bootstrap. Красивый шрифт берём в Google Fonts, набор иконок FontAwesome. Всё это подгружаем из CDN, указывая в конфигурационном файле в секции head в виде:

script: [
…
	{
		src: 'https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js',
		type: 'text/javascript'
	},
	{
		src: 'https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js',
		type: 'text/javascript'
	},
…
]

При переходе между роутами Nuxt.js уже из коробки снабжён красивым прогресс-баром. Меняем стандартный цвет на свой:

loading: {
	color: '#9858bf',
	height: '5px'
},

Подключаем необходимые модули:

modules: [
	'@nuxtjs/axios', // делать запросы к API с axios удобно
	'@nuxtjs/auth', // реализуем авторизацию JWT
	'cookie-universal-nuxt', // работа с печеньками
	['@nuxtjs/moment', {locales: ['ru'], defaultLocale: 'ru'}], // работа с датой / временем
	'nuxt-vue-multiselect', // удобный мультиселект для связей многие-ко-многим
	'@nuxtjs/toast', // а это in-app пуши
],

В качестве указания настроек модуля в конфигурационном файле:

axios: {
	baseURL: 'https://api._DOMAIN_.ru/api'
},

Здесь мы говорим Axios о том, чтобы он добавил нам базовый урл до API, чтобы каждый раз не писать адрес полностью. Мелочь, а приятно.

И настраиваем параметры авторизации, указывая нужные endpoints:

auth: {
	strategies: {
		local: {
			endpoints: {
				login: {
					url: 'login',
					method: 'post',
					propertyName: "meta.token"
				},
				user: {
					url: 'user',
					method: 'get',
					propertyName: 'data'
				},
				logout: {
					url: 'logout',
					method: 'post'
				}
			}
		}
	}
},

На этом с конфигурацией всё, двигаемся дальше.

Авторизация

Как происходит авторизация JWT на стороне нашего фронтенда? Когда пользователь попадает на страницу, которая предполагает авторизацию, мы просто подключаем middleware под названием auth из одноименного модуля:

…
export default {
	middleware: [
		'auth'
	],
	data() {
…

Если же нам требуется дополнительная проверка роли пользователя, оформляем это отдельным middleware в /middleware/owner.js:

export default function (context) {
	let user = context.store.getters["user"];
	if (user && !user.is_owner) {
		return context.redirect('/');
	}
}

и в самой странице, где необходимо такая проверка, указываем после middleware auth наш ‘owner’:

…
export default {
	middleware: [
		'auth’,
		‘owner’
	],
	data() {
…

Ошибки валидации

Для обработки ошибок валидации данных будем использовать серверное решение, никаких клиентских проверок. Чтобы всё было централизованно и монолитно. Происходит это следующим образом: если API получает некорректные данные, то возвращаемый ответ будет содержать статус ошибки и ассоциативный массив самих ошибок, где ключи это поля, а значения это сами сообщения. На стороне Nuxt.js мы будем хранить эти ошибки в store, и чтобы каждый раз не заниматься складыванием / очисткой данных об ошибках непосредственно на странице с формой, сделаем один middleware для этой работы:

router: {
	middleware: ['clearValidationErrors']
},

И сам код middleware /middleware/clearValidationErrors.js:

export default function ({store}) {
    store.dispatch('validation/clearErrors');
}

который в свою очередь цепляет store в /store/validation.js:

export const state = () => ({
    errors: {}
});

// getters
export const getters = {
    errors(state) {
        return state.errors;
    }
};

// mutations
export const mutations = {
    SET_VALIDATION_ERRORS(state, errors) {
        state.errors = errors;
    }
};

// actions
export const actions = {
    setErrors({commit}, errors) {
        commit("SET_VALIDATION_ERRORS", errors);
    },
    clearErrors({commit}) {
        commit("SET_VALIDATION_ERRORS", {});
    },
};

Ошибки заносятся в store каждый раз, когда приходят от API в запросе со статусом 422. Это легко и удобно сделать при помощи плагина, который дополнит стандартный Axios небольшим хуком:

export default function ({$axios, store}) {
	$axios.onError(error => {
		if (error.response.status === 422) {
			store.dispatch('validation/setErrors', error.response.data.errors)
		}
	});
	
	$axios.onRequest(() => {
		store.dispatch('validation/clearErrors');
	});
}

Для наглядности приводим листинг страницы добавления услуги собственником салона /pages/servs/add/index.vue:

<template lang="pug">
	.container-fluid
		.row.mb-4
			.col
				h1 Добавление услуги
		
		.row.mb-4
			.col-lg-6
				form.p-3.bg-purple-light.border-0(@submit.prevent="submit")
					.form-group
						label Тип услуги
						multiselect(
							v-model="form.servtypes"
							:options="additional.servtypes"
							placeholder="Выберите"
							label="name"
							track-by="id"
							selectedLabel="Выбран"
							deselectLabel="убрать"
							selectLabel=""
							@input="opt => form.servtype_id = opt.id"
						)
						
						small.form-text.text-danger(v-if="errors.servtype_id") {{errors.servtype_id[0]}}
					
					
					.form-group
						label Может использоваться в организациях
						multiselect(
							v-model="form.orgs"
							:options="additional.orgs"
							:multiple="true"
							placeholder="Выберите"
							label="name"
							track-by="id"
							selectedLabel="Выбран"
							deselectLabel="убрать"
							selectLabel=""
						)
						
						small.form-text.text-danger(v-if="errors.orgs") {{errors.orgs[0]}}
						
					.form-group
						label Название
						input.form-control.form-control-app(v-model.trim="form.name" type="text" autofocus)
						small.form-text.text-danger(v-if="errors.name") {{errors.name[0]}}
					
					.form-group
						label Стоимость
						input.form-control.form-control-app(v-model.trim="form.price" type="number")
						small.form-text.text-danger(v-if="errors.price") {{errors.price[0]}}
					
					.form-group
						label Стоимость первой процедуры (если это комплекс)
						input.form-control.form-control-app(v-model.trim="form.price_first" type="number")
						small.form-text.text-danger(v-if="errors.price_first") {{errors.price_first[0]}}
					
					.form-group
						label Стоимость последней процедуры (если это комплекс)
						input.form-control.form-control-app(v-model.trim="form.price_last" type="number")
						small.form-text.text-danger(v-if="errors.price_last") {{errors.price_last[0]}}
					
					.form-group
						label Количество процедур (если это комплекс)
						input.form-control.form-control-app(v-model.trim="form.procedures" type="text")
						small.form-text.text-danger(v-if="errors.procedures") {{errors.procedures[0]}}
					
					.form-group
						label Длительность (минут)
						input.form-control.form-control-app(v-model.trim="form.time" type="text")
						small.form-text.text-danger(v-if="errors.time") {{errors.time[0]}}
					
					.form-group.form-check
						input.form-check-input(v-model="form.can_parallel" type="checkbox" id="userCanParallel")
						label(for="userCanParallel") Можно записывать параллельно
					
					.form-group
						label Описание
						textarea.form-control.form-control-app(v-model.trim="form.desc" type="text" autofocus)
						small.form-text.text-danger(v-if="errors.desc") {{errors.desc[0]}}
						
					.d-flex.justify-content-between.align-items-center
						nuxt-link.btn.btn-secondary.btn-sm(to="/servs") к списку
						button.btn.btn-purple(type="submit") Добавить

</template>

<script>
	export default {
		middleware: [
			'auth',
			'owner'
		],
		data() {
			return {
				form: {
					name: '',
					servtype_id: '',
					price: '',
					time: '',
					can_parallel: false,
					desc: '',
					orgs: []
				},
				additional: {
					orgs: []
				}
			}
		},
		async asyncData({$axios, params}) {
			const {data} = await $axios.$get(`/servs/additional_data`);
			return data;
		},
		methods: {
			async submit() {
				try {
					await this.$axios.$post('servs/add', this.form);

					// redirect
					this.$router.push({
						'path': this.$route.query.redirect || "/servs"
					});
				} catch (e) {
					console.log(e);
				}
			}
		}
	}
</script>

Здесь код

small.form-text.text-danger(v-if="errors.name") {{errors.name[0]}}

выводит текст ошибки для поля «Название», если он присутствует в store

Деплоймент

В режиме разработки Nuxt.js приложение запускается на локальной машине веб-мастера. На сервер же выгружается продакшн версия, предварительно собранная командой npm run build

Для автоматизации данного процесса мы написали deployment скрипт. Единственное условие для его работы: на сервер должен быть добавлен ssh-ключ.

const consola = require('consola');
const config = require('./config');

// Проверяем, есть ли данные для подключения
if (!config.deploy) {
    consola.error('Создайте данные для подключения в config.js');
    return false;
}

consola.start('Начинаю deploy...');

const exe = require('exe');
const appIngoreFiles = ['.git', '.gitignore', '.prettierrc'];
const connect = `${config.deploy.user}@${config.deploy.ip}`;
const projectFolder = config.projectName.toLowerCase();

const appExclude = appIngoreFiles
    .map(file => {
        return `--exclude="${file}"`
    })
    .join(' ');
exe(`rsync -avm ${config.deploy.port ? `--rsh="ssh -p${config.deploy.port}" ` : ''}--delete ${appExclude} . ${connect}:/home/bitrix/node/${projectFolder}/`);

console.log(`***`);
consola.success('--- Файлы скопированы ---');
console.log(`***`);

consola.start('Перезапускаю проект');
exe(`ssh ${connect} ${config.deploy.port ? `-p ${config.deploy.port}` : ''} 'cd /home/bitrix/node/${projectFolder}/ && pm2 reload apps.config.js'`);
console.log(`***`);

const date = new Date().toISOString();
consola.ready(`--- Проект перезапущен: ${date} ---`);

Вывод

В процессе работы над проектом мы получили огромное удовольствие от написания кода. Laravel прекрасно подходит для создания API с JWT авторизацией. Был только единственный нюанс с трудоёмкостью и непонятностью настройки этого самого JWT, в том числе и из-за несовместимости версий Laravel, которую использовали мы и которая была использована при составлении мануала разработчиками JWT плагина. Но это решаемо.

Nuxt.js — это настоящий подарок для разработчика. Прекрасный инструмент для того, быстро, просто и понятно реализовывать сложные интерфейсы и нестандартную логику.

Здесь вы можете предложить тему для следующих статей
Заказать разработку