Это 2 часть цикла статей о сервисной архитектуре во Vue 2. В 1 части я рассказала о том, какие способы выноса логики популярны на данный момент, почему они меня не устраивали, и чего я хотела достичь.
UPD к 1 части
В 1 части не все меня поняли, и думали, что я решаю какую-то конкретную задачу и просили пример или уточняющие данные. Эта серия статей не о конкретной задаче и конкретном примере.
Эти статьи для тех, у кого проект дорос до того момента, когда текущих решений становится недостаточно, и ты начинаешь чувствовать, как будто все глубже и глубже себя закапываешь, а твои способы распределять данные или выносить логику начали вызывать сложные баги, и становится все больше костылей.
Я не приветствую заменять все сервисами и больше не использовать vuex, миксины и т.д., я хотела донести мысль, что каждый инструмент хорош для своей цели. Но если вы чувствуете, что этого инструмента вам недостаточно для какой-то задачи, и что минусы достаточно существенны для какого-то конкретного случая, то возможно вам стоит вынести эту логику в класс и оформить его как сервис для определенной задачи.
Сервис в моем понимании — это отдельный архитектурный слой, который выполняет конкретную задачу (например, работает с товарами: запрашивает их с определенными условиями, обрабатывает, производит поиск).
Сервисная архитектура — это когда в проекте на каждую отдельную задачу (или сущность) выделены отдельные сервисы, которые с ними работают.
О чем мои статьи? О том, как можно использовать классы во Vue 2. Какое это отношение имеет к сервисной архитектуре? Прямое, это один из способов, как организовать сервис. С этого все и начинается, без практического удобного решения, как эти сервисы встраивать, они не будут появляться в проекте.
В этой части мы поговорим о том, как работать с объектами и примитивами, как их защищать. Статья получилась достаточно длинной, так что работу с массивами и вычисляемыми значениями я решила вынести в отдельную статью.
О встройке класса во Vue 2
Эта статья сосредоточена конкретно на проектировании класса, так что встройка будет показана условно с помощью singleton. Во время своего исследования я нашла решение, которое мне нравится больше, я покажу его в 4 части.
Сейчас уже можно посмотреть на те функции, которые я создала, дабы упростить использование класса в компонентах вот в этих файлах.
Примитив
Я решила начать с того, чтобы попытаться встроить примитивное свойство из класса в компонент. Логика простая, сам примитив не передашь, он запишется и все, связи нет. Тогда я вспомнила про функцию ref из Vue 3 (docs), где все примитивы они предлагают обернуть в объект по структуре:
{ value: <some_val> }
Итак, пробуем создать класс
class Example { someString = { value: 'someString', }; } const example = new Example();
Пытаемся встроить свойство в пару компонентов как-то так
<template> <div> <span>Value: {{ someString.value }}</span> </div> </template>
export default { data() { return { someString: example.someString, }; }, }
Нам же еще нужно это свойство изменить, добавляем кнопку и метод
<template> ... <button @click="changeValue">Change value</button> ... </template>
... methods: { changeValue() { this.someString.value = 'someAnotherString'; }, }, ...
И наконец, проверяем

Я понимаю, что сейчас можно подумать что-то вроде «Ну это же очевидно, что оно сработает». И когда я попробовала, и оно сработало, я подумала абсолютно то же самое. Но почему-то до этого я никогда не пыталась так сделать, ровно также как и многие другие на самом деле.
Вся эта реактивность во Vue с его геттерами и сеттерами была покрыта некоторой мистикой, хоть я и смотрела видео до этого с объяснениями этой технологии, но все равно не задумывалась о том, что если мы передадим ссылку на объект, то связь будет, ей некуда будет деваться. Хотя если бы в фреймворке сделали бы хотя бы shallow copy, то пришлось бы как-то вертеться.
Хорошо, а если бы мы хотели менять через метод класса? Как бы мы могли сделать это?
Давайте добавим метод
class Example { ... changeValue() { this.someString.value = 'anotherString'; } }
В компонент мы могли бы встроить его как-то так
methods: { changeValue() { example.changeValue(); }, },
И это бы сработало, но мне не нравится лишний код, проксирование там где не нужно, мы ведь хотим просто вызвать метод, правильно? Путем проб и ошибок, я начала встраивать его так
export default { data() { return { ... changeValue: example.changeValue.bind(example), }; }, }
Подробнее о том, почему метод встраивается в секцию data я буду говорить в 3-ей части, когда разговор будет идти о разных экземплярах и их уничтожении.
Давайте проверим

Где смотреть полные примеры?
В дальнейшем я буду указывать ссылки, где можно будет посмотреть полный код примера для той или иной ситуации. Всю документацию можно посмотреть у меня в репозитории. Для запуска надо выполнить командуyarn docs:dev.
Ссылки будут приведены на локально поднятую документацию.
Хорошо, мы успешно использовали свойство из класса, смогли поменять его из компонента и из метода. Но если мы на этом остановимся, мы никогда не сможем обеспечить защищенность данных и найти, в каком месте эти данные были изменены, будет сложновато.
Давайте сделаем наше свойство для компонента в формате read-only и заставим производить изменения только через методы.
Создадим класс с псевдо-приватным свойством (покажем это визуально) и методом для его изменения
class Example { _privateString = { value: 'I\'m private', } changePrivateString() { this._privateString.value = 'My value is changed from method'; } } const example = new Example();
Но как нам ограничить его изменения внешне, из компонента? Я реализовала это через геттер с Proxy (docs). Добавим геттер
class Example { ... get privateString() { return new Proxy(this._privateString, { // Запрещаем изменение set() { throw new Error('This property is read-only'); }, }); } }
Суть в том, что компонент забирает именно публичный геттер, а не внутреннее приватное свойство. О том, как разрешать компоненту получать только публичные свойства, я буду говорить в 4 части.
В set необязательно кидать exception, достаточно вернуть из сеттера false, но таким образом мы получаем ошибку без нормального описания


В двух компонентах заберем публичный геттер
data() { return { privateString: example.privateString, }; },
Но в первом компоненте попытаемся изменить с компонента через присваивание
methods: { changeValue() { this.privateString.value = 'Should cause error'; }, },
А во втором компоненте изменим через метод класса
data() { return { ... changeValue: example.changePrivateString.bind(example), }; },
Давайте проверим

Полный пример
Окей, с read-only разобрались, а что если мы хотим, чтобы данные менялись с компонента, но нам нужна дополнительная валидация? Для этого нам нужно изменить наш Proxy, чтобы он не запрещал изменения, а валидировал их.
Предположим, нам нужно проверить, что в наше свойство можно записать только строку, тогда сеттер будет выглядеть так
set(obj, prop, value) { // Запрещаем добавление новых полей if (prop !== 'value') { throw new Error('Only accesible property is "value"'); } // Запрещаем записывать НЕ строку if (typeof value !== 'string') { throw new TypeError('Value must be string'); } // Проводим операцию присваивания obj[prop] = value; // Сигнализируем, что ошибки не возникло return true; },
Я сделала тестовый компонент для проверки, куда добавила несколько присваиваний, вот результат

Полный пример
Объект
Мы оборачивали примитив в объект, так что работа с остальными объектами естественно будет схожа. Но когда мы разговариваем про обычный объект, то у нас появляются дополнительные кейсы, которые нужно учесть.
В случае, когда у нашего объекта статичная структура, т.е. мы изначально его инициализировали, и добавление/удалений свойств не требуется, а лишь изменение существующих, то действуют те же правила, которые мы рассматривали в секции с примитивами.
Пример для объекта со статичной структурой
А что если нам нужно добавить поле? Документация Vue подсказывает выход — использовать Vue.set. Работает ли это, если мы вызовем это в классе, а не в компоненте? Давайте проверим.
Сделаем класс со свойством-объектом, импортируем туда Vue, и сделаем метод, где мы добавим новое свойство с помощью set
import Vue from 'vue'; class Example { testObject = { oldField: 'I was here from the beginning!' } addNewField() { Vue.set(this.testObject, 'newField', 'I was added recently!'); } } const example = new Example();
Сделаем пару тестовых компонентов, где мы получим наше свойство и метод. Для теста давайте добавим туда еще дополнительный метод, который будет добавлять новое свойство из компонента с помощью this.$set
export default { data() { return { testObject: example.testObject, addNewField: example.addNewField.bind(example), }; }, methods: { addField() { this.$set(this.testObject, 'fieldFromComponent', 'I was added from component!'); } } };
Итак, время истины

Полный пример
К этому моменту у меня складывается ощущение, что если мы и столкнемся с какой-то проблемой с реактивностью, то она скорее будет связана с изначальной архитектурой фреймворка, чем с нашими изысканиями.
Окей, а что если наш объект приходит с сервера, и мы хотим сначала инициализировать его как null, а потом уже записать значение? Перезаписать наше свойство в классе мы не можем, так что воспользуемся тем же механизмом, который использовали для работы с примитивом. Т.е. обернем наш объект в еще один объект.
class Example { obj = { value: null, } fillObj() { this.obj.value = { field: 'field' }; } }
Выведем в паре компонентов, вызовем метод и проверим

Полный пример
Если мы хотим запретить изменения в объекте из компонента, то тактика такая же, как и с примитивом. Делаем геттер, где отдаем наш объект, обернутый в Proxy, из set кидаем ошибку.
Но во время тестирования я обнаружила любопытную особенность, связанную с this.$set. Так как это достаточно узкий кейс, в статье я на этом останавливаться не буду, welcome на страничку в моей документации, там я это описала.
Пример readonly объекта
С валидацией объекта все абсолютно также, как мы делали для примитива. Но в документации я привела пример того, как можно использовать эту технологию на примере валидации значений в форме. Покажу под катом, какой компонент в результате у меня получился.
Компонент формы
<template> <div> <p> <input v-model="form.firstName" placeholder="Имя" :class="{ 'error': errors.firstName }" /><br/> <span v-if="errors.firstName" class="error"> {{ errors.firstName }} </span> </p> <p> <input v-model="form.age" placeholder="Возраст" :class="{ 'error': errors.age }" /><br /> <span v-if="errors.age" class="error"> {{ errors.age }} </span> </p> </div> </template>
import ValidatedForm from '@example-services/ValidatedForm'; export default { data() { return { form: ValidatedForm.form, errors: ValidatedForm.errors, }; }, }
Обратите внимание, насколько чистый стал компонент. Вся логика связанная с хранением и валидацией ушла в класс, а компонент стал заниматься тем, чем он и должен был — отображением. Т.е. единственное, что решает компонент — это как и когда показывать ошибку пользователю.
Полный пример (и в том числе, как выглядит класс, для того чтобы компонент выглядел так), смотрите в моей документации.
Пример валидации объекта
Напоминаю, что документация разворачивается локально из моего репозитория с помощью команды yarn docs:dev. Ссылка приведена на поднятую документацию.
Это получилась довольно насыщенная статья, я хотела расписать все детали работы с классом, но в эту часть влезла только работа с примитивами и объектами.
План примерно такой: в 3 части поговорим о массивах и вычисляемых свойствах, в 4 части поговорим об экземплярах класса и удобных функциях, чтобы можно было встраивать класс в компонент проще.
ссылка на оригинал статьи https://habr.com/ru/post/700964/
Добавить комментарий