Постановка задачи

Прежде чем углубиться, давайте определимся с нашей целью. Вместо того, чтобы писать один и тот же шаблонный шаблон CRUD (создание, чтение, обновление, удаление) для каждой из наших потенциальных служб модели данных:

this.ProductService.readProducts();
this.ProductService.createProduct(newObj);
this.ProductService.updateProduct(updateObj);
this.ProductService.deleteProduct(deleteObj);

Вместо этого давайте создадим многоразовую динамическую службу CRUD, чтобы мы могли позволить ей обрабатывать наши HTTP-запросы из центра и избегать создания HTTP-методов для каждой модели данных:

this.DataService.read(Product);
this.DataService.create(Product, newObj);
this.DataService.update(Product, updateObj);
this.DataService.delete(Product, deleteObj);

Устали от различных шаблонных решений для тяжелых магазинов в Angular? Не забывайте основы!

Кажется, что в настоящее время все заняты попытками реализовать шаблон Redux в своем простом веб-приложении, потому что, если вы не используете Redux, NGRX, Akita и т. Д., Вы вообще настоящий программист? Однако, если вы сделаете шаг назад и посмотрите на инструменты и концепции, которые предоставляют вам современные фреймворки, то вы сможете придумать гибко настраиваемое динамическое решение для удовлетворения вашего варианта использования!

Введите концепцию динамической службы CRUD (создание, чтение, обновление, удаление) в Angular. Во многих учебных пособиях вам будет предложено создать службу для конкретной модели для выполнения основных функций CRUD с этими данными, например, печально известный вариант использования списка дел. В руководствах вы постепенно узнаете, как выполнить CRUD для модели Post, но никогда не углубляйтесь в то, как вы можете разработать повторно используемый сервис для выполнения этих запросов.

Проблемы возникают у новичков в программировании, когда они пытаются взять эти концепции и масштабировать их для создания реального приложения с несколькими таблицами. Не дай бог вам нужно ввести в смесь некоторые базовые реляционные таблицы связей, и вы получите службу для каждой модели данных, компоненты, выполняющие случайные HTTP-вызовы и, как правило, потерявшие данные во всем приложении.

Вместо того, чтобы обращаться к раздутому решению по управлению состоянием для вашего классного списка дел с приложением для отслеживания целей или создавать сервис для каждой из ваших моделей данных, вы можете вместо этого применить принципы D.R.Y. (не повторяйтесь!) и используйте тип Generics для создания единой службы данных с использованием возможностей RxJS Subjects и Observables!



Определите свою модель данных

Чтобы начать с самого начала, большинство знакомо с концепцией интерфейса или класса модели в Typescript:

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

Кроме того, мы определяем интерфейс для свойств вымышленного IProduct, который расширяет этот базовый интерфейс. Наконец, мы предоставляем класс Product, который реализует этот интерфейс, определяя статическое tableName для использования во всем приложении и сохраняя все другие соответствующие свойства для продукта. Обратите внимание на конструктор этой модели, так как мы ожидаем получить объект props типа IProduct. Мы используем Object.keys для перебора этого входящего IProduct, чтобы распространить его на наш объект Product.

Создание CRUD-сервиса для продукта

Вместо того, чтобы писать все утомительные методы CRUD для передачи и запроса данных конкретной модели «Продукт» и всех других моделей данных для моего приложения, что, если бы у меня была общая служба для выполнения этих вызовов для всех моих различных типов моделей? Прежде чем перейти к динамическому решению, вот пример того, как вы можете выполнить CRUD для своей таблицы Product с помощью RxJS Observables:

Это немного надуманный пример, поскольку в нем отсутствуют многие основные функции. Вы оставляете на усмотрение своих компонентов, как обрабатывать наблюдаемые CRUD, у вас нет центрального хранилища для ваших данных / состояния и отсутствуют заголовки HTTP (среди других требований для выполнения HTTP-вызовов).

Создание подобного сервиса для каждого из ваших типов моделей быстро станет утомительным, поскольку ваше приложение возрастает и вы начинаете иметь несколько таблиц или коллекций. Чтобы обойти это, мы можем использовать концепцию Generics в TypeScript для создания многоразовой службы CRUD, которая поддерживает Observables, Subject subscription и даже старые добрые обещания через ES6 fetch!

Дженерики спешат на помощь

Прежде чем погрузиться во всю службу CRUD, давайте посмотрим, как мы можем обобщить предыдущую службу продуктов:

Главное, что вы заметите, - это либеральное использование буквы T.

Помимо шуток, здесь мы определили множество общих типов в наших методах CRUD, чтобы обеспечить возможность передачи любого потенциального типа модели данных. Это станет очевидным вскоре, когда мы сможем считывать данные из базы данных, а затем динамически создавать «новая модель ()» по отношению к поступающим данным.

Вы также заметите, что я добавил параметр «модель» к каждому из методов. Это позволит нашим компонентам вызывать эти методы, просто передавая ссылку на конкретную модель данных, которая нас интересует. Для варианта использования продукта мы теперь можем читать все продукты, выполнив:

this.DS.read(Product).subscribe(res => console.log(res));

Мы просто передаем ссылку на класс Product, и наша общая служба будет ожидать, что во входящей модели будет tableName для создания URL-адреса конечной точки REST, а затем по конвейеру поступающие данные для преобразования их в модель продукта.

Создание динамического сервиса CRUD!

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

Подобно хранилищу в стиле Redux, мы, вероятно, захотим иметь какой-то механизм «кеширования» в нашем сервисе для хранения всех наших данных, для которых мы выполняем CRUD. Этого легко добиться с помощью локального свойства в нашей службе, называемого просто «кеш», и это простой старый объект JavaScript.

Затем мы хотим разрешить компонентам подписываться на определенные модели данных, чтобы получать уведомления при изменении этих данных. Опять же, объект JavaScript - наш друг, поскольку мы создаем другое локальное свойство под названием «subjectMap», которое будет отображением имени таблицы в субъект, на который наши компоненты могут подписаться. Кроме того, у нас будет две темы, определенные для каждого имени таблицы, где компонент может подписаться на получение уведомлений об «одном» или «многих» обновлениях. Это ценно, поскольку наш компонент может не захотеть получать уведомления обо всем массиве модели данных каждый раз, когда что-то меняется, а вместо этого интересуется только одним (одним) обновляемым объектом.

Наконец, приятно иметь возможность для компонента определять, работаем ли мы в настоящее время с определенной моделью данных. Обычно, когда вы выполняете операцию CRUD с базой данных, вам нужно уведомить пользователя о том, что приложение работает, с помощью какого-то счетчика загрузки или индикатора выполнения. Мы предоставляем свойство loadingMap, которое представляет собой простой логический флаг, который будет изменен на основе внутренних методов CRUD. Например, перед выполнением чтения мы установим для свойства loadMap для имени таблицы значение true, пока чтение не будет завершено, когда мы вернем его в значение false.

Убрав некоторые из этих определений, можно дополнить суть нашего CRUD DataService. Как уже упоминалось, мы хотим, чтобы компоненты могли выполнять операции CRUD любым удобным для них способом; будь то через возвращаемый Observable, уведомление Subject или выполнение ручного вызова ES6 Fetch через Promises и, возможно, async / await.

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

Это общая структура нашего DataService, вы можете увидеть эту реализацию здесь:

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

Во-первых, мы предполагаем, что каждый из наших классов модели данных имеет определение TableName. Это должно быть определено в отдельном классе «TableMap», который представляет собой просто статическое отображение всех имен наших таблиц. Это позволяет нам изменять имена наших таблиц из центральной точки и позволяет продвигаться по всему коду, поскольку наши модели будут ссылаться на это значение.

export class TableMap {    
    static Products = 'Products';    
    static Companies = 'Companies';    
    static Sellers = 'Sellers';
}

Внизу файла вы также заметите некоторые определения частного интерфейса для использования нашей службой DataService. Эти определения используются для обеспечения типизации того, что мы ожидаем от наших локальных свойств (cache, subjectMap и loadingMap).

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

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

Создав DataService верхнего уровня, мы можем теперь погрузиться в каждый из наших подклассов CRUD.

Структура подкласса CRUD

Наши подклассы должны учитывать некоторые конструктивные особенности.

Обычная тема, которую вы здесь увидите, - это передача ссылки на родительский DataService в этот подкласс. Есть и другие способы сделать это, но пока вам нужно отправить ссылку на DataService на эти подклассы из самого DataService.

Затем вы заметите, что у меня есть обычный метод (например, «create»), затем метод Observable (например, «createObs») и, наконец, метод на основе Promise (например, «createPromise»). Они предназначены для различения различных способов CRUD-обработки наших данных, где «create» осуществляется через уведомление Subject, «createObs» возвращает и Observable, чтобы компонент мог выбирать, когда подписываться, и, наконец, «createPromise» позволяет более детально контролировать выполнение CRUD. операция.

В этих методах также есть добавление оператора pipe (), где это необходимо. Это позволяет нам указать многоразовый обработчик HTTP, а также выполнить операцию tap () для преобразования входящего результата из типа «any []» в определенный тип модели «model []».

Каждый из этих подклассов содержит частный метод cacheAndNotify. Как следует из названия метода, этот метод кэширует результат операции CRUD в нашем интерфейсе через свойство «cache» DataService. Затем он будет уведомлять внешний интерфейс об обновленном состоянии данных, передавая весь массив модели данных субъекту .many, а также только объект, с которым выполнялась операция, субъекту .one.

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

СОЗДАЙТЕ

Задача класса create - создать новую запись в базе данных для определенного типа модели. Хотя это и не требуется, при желании он принимает «objToCreate», чтобы предварительно заполнить базу данных значениями из внешнего интерфейса.

Основная цель здесь - заставить базу данных генерировать новое значение UUID / PrimaryKey для нашего внешнего интерфейса, чтобы «привязать» объект внешнего интерфейса к записи базы данных.

Метод cacheAndNotifyCreated отличается от других тем, что он просто помещает новый объект в кеш DataService с помощью Object.assign. Затем он уведомляет субъекты внешнего интерфейса о новом состоянии кеша, а также о том, какой объект был только что создан.

ЧИТАТЬ

Чтение - интересный случай, поскольку у нас есть дополнительное свойство указывать запрос к данным. Если мы не предоставим запрос, мы просто хотим вернуть все доступные данные.

Поскольку необходимо указать запрос, для createSearchParams предоставляется дополнительный метод. Это позволяет несколькими способами определять параметры поиска, используя простую строку:

query = 'id=123&name=joe';

Через определение объекта:

query = { id: '123', name: 'joe' };

Или используя класс Angular HTTPParams:

let queryParams = new HTTPParams();
queryParams = queryParams.set('id', '123);
queryParams = queryParams.set('name', 'joe');

В сочетании с параметром tableName модели это соответствующим образом создаст запрос REST для конечной точки.

Здесь метод cacheAndNotifyRead полностью очистит кеш, поскольку мы только что прочитали новые данные. Затем он будет уведомлять внешний интерфейс только через тему .many, если это необходимо.

ОБНОВИТЬ

Обновление довольно простое, поскольку мы уже видели шаблон в двух предыдущих подклассах. Однако cacheAndNotifyUpdated немного отличается. Сначала мы находим локальный объект для обновления путем поиска в нашем кэше. Если он у нас есть, мы используем Object.assign для обновления кеша переданным новым объектом. Затем мы уведомляем интерфейсную часть о новом кэше, а также о том, какой объект был обновлен.

УДАЛЯТЬ

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

Примеры

PHEW! Это было довольно много кода и объяснений. Теперь, когда все это решено, мы можем использовать нашу службу CRUD по-разному. Для краткости, давайте продолжим читать.

Предположим, вы вводите CRUD DataService в конструктор следующим образом: мы сохраняем некоторые локальные массивы для наших ожидаемых данных и, конечно же, сохраняем тему отказа от подписки, чтобы очистить наши наблюдаемые подписки, когда мы уничтожаем этот компонент:

private unsub: Subject<void> = new Subject<any>();  
 
products: Product[] = [];  
companies: Company[] = [];  
sellers: Seller[] = [];
constructor(
    private DS: DataService
) {}

Вы заметите, что я не использую кеш DataService. Это сделано намеренно, чтобы показать несколько быстрых примеров. Я мог бы легко пропустить настройку результатов чтения в локальные массивы и вместо этого просто сослаться на кеш DataService, например:

this.DS.cache[Product.tableName];

Чтение через тему

Это, вероятно, самый подробный пример, но он дает вам максимальную гибкость, поскольку ваш компонент всегда будет отслеживать любые изменения этой модели данных в приложении. Мы определяем тематические подписки и определяем, что делать при получении новых данных. Затем мы просто вызываем this.DS.read (MODEL) для выполнения операции HTTP.

Чтение через Observable

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

В этом примере я использую this.DS.readObs для создания некоторых наблюдаемых для каждого из моих типов данных. Затем я могу использовать операторы RxJS для выполнения этих наблюдаемых различными способами.

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

Чтение через обещание

От старых привычек трудно избавиться, поэтому я обязательно включил в DataService подход, основанный на обещаниях. Если вы просто хотите получить свои данные самым простым способом и вам не нужно разбираться в RxJS, тогда мы можем вернуться к хорошему "ol async / await with fetch!"

Это похоже на наблюдаемый подход в том, что мы сначала создаем наши обещания в локальных переменных, используя this.DS.readPromise (MODEL).

С их помощью мы снова хотим запросить все данные и дождаться их, прежде чем использовать результаты. Для этого мы используем await Promise.all ([]). Await заблокирует продолжение выполнения кода до тех пор, пока не будут выполнены все обещания. Как только это будет сделано, мы присваиваем нашему компоненту локальные данные на основе этого.

Будущие примеры

В будущем я приведу примеры использования оставшихся методов «C-U-D», но процесс аналогичен этим чтениям.

ЗАКЛЮЧЕНИЕ

Надеюсь, эта статья поможет вам осознать некоторые возможности дженериков и снизить потребность в использовании библиотеки тяжелого управления состоянием только для управления состоянием. При создании этого сервиса было сделано много вольностей, и, в конце концов, ни один код не идеален. Однако он выполняет свою работу, и это все, что имеет значение!

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

Несмотря на большой объем этой статьи, здесь есть еще много вопросов, которые нужно осветить. Мы почти не коснулись концепций HTTP-заголовков и аутентификации, и есть много дизайнерских решений, которые можно утверждать как лучше, так и хуже. Однако эта статья представляет собой попытку предоставить простой в использовании сервис CRUD, который соответствует 90% ваших вариантов использования.

Дайте мне знать в комментариях, если здесь есть ошибки или явные дыры в логике!

Полный код, который я использую для своих приложений, можно найти в моем репозитории по адресу: