Статьи

Умные интерфейсы и тупые фоны: сохраняющееся состояние в AngularJS

Состояние — это то, что вы генерируете, когда вы взаимодействуете с веб-сайтом, например, нажимая кнопку или вводя текст в текстовое поле. Это состояние находится в оперативной памяти вашего браузера и состоит из объектов JavaScript, таких как массивы, строки и объекты.

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

Начиная

Допустим, у нас есть массив объектов, и у каждого объекта есть «временные» ключи. Мы не хотим, чтобы эти ключи были постоянными при отправке массива объектов в постоянное хранилище ( localStorage или Ajax).

В примере, который я собираюсь показать вам, мы будем работать с приложением AngularJS. В нем мы помещаем вещи в каждый объект, которые не должны быть постоянными. Все, что не важно быть настойчивым, начинается с подчеркивания.

Вот HTML-код, с которого мы начинаем:

 <div ng-repeat="thing in things track by thing.id" ng-click="thing._expanded=!thing._expanded"> <div ng-if="thing._expanded"> EXPANDED VIEW </div> <div ng-if="!thing._expanded"> collapsed view </div> </div> 

и это небольшой код JavaScript, который обеспечивает его:

 angular .module('app', []) .controller('Ctrl', ($scope) => { $scope.things = [ {id: 1, key: 'Value'}, {id: 2, key: 'Value2'}, {id: 3, key: 'Value3'}, ] }); 

Если вы хотите увидеть этот пример вживую, посмотрите на эту демонстрацию .

Довольно мило, верно? Это действительно практично.

Думая о сохранении этого настойчиво

Давайте теперь предположим, что мы хотим сохранить $scope.things . Это может быть список Todo, к которому мы хотим вернуться после закрытия вкладки. И когда мы вернемся, многие из этих мелких атрибутов (например, _expanded или _dateAsString ) — это вещи, которые мы не хотим загромождать в постоянном хранилище. Мы хотим сделать это, потому что они могут быть легко сгенерированы или сброшены без потери того, что ценно. Или, возможно, мы беспокоимся о памяти, и некоторые из этих временных состояний тяжелы.

Допустим, мы хотим сохранить это в localStorage . То, что нам нужно сделать, выглядит следующим образом:

 localStorage.setItem('mystuff', JSON.stringify($scope.things)); 

Тем не менее, это сохранит кучу других вещей в объектах, которые мы не хотим помнить (опять же, потому что они могут быть восстановлены или потому что их просто не стоит сохранять).

Итак, первое, что нам нужно сделать, это клонировать их. Чтобы клонировать массив, вы должны написать такой код JavaScript (здесь я использую ECMAScript6 ):

 let copy = Array.from(myArray) 

Но если массив заполнен объектами (то есть словарями), мы должны сделать немного больше магии:

 let copy = Array.from( myArray, (item) => Object.assign({}, item) ); 

Таким образом, мы получаем глубокую копию .

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

 let copy = Array.from(myArray, (item) => { let obj = Object.assign({}, item) for (let key of Object.keys(obj)) { if (key.startsWith('_') || key === '$$hashKey') { delete obj[key] } } return obj }); 

Если вы хотите увидеть этот пример вживую, посмотрите на эту демонстрацию .

Что с этим $$hashKey Thing?

AngularJS помещает ключ с именем $$hashKey в каждый $$hashKey объект (если он считает, что это необходимо). Таким образом, он может внутренне знать, что изменилось, а что нет. В AngularJS есть встроенная утилита, которую мы можем использовать для удаления:

 angular.toJson(myObject); 

Вы можете найти документацию об этом здесь .

Если мы повторяем как ng-repeat="thing in things" , AngularJS вставит эти ключи. Но чтобы избежать этого, мы можем использовать ключ отслеживания, например:

 ng-repeat="thing in things track by thing.id" 

И это не будет вставлено.

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

Положить его вместе

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

Давайте сначала построим это так, чтобы у нас работали кости. Ниже приведен HTML-код, который нам нужен:

 <div ng-app="app"> <div ng-controller="Ctrl"> <div ng-repeat="week in weeks track by week.date"> <h3 ng-click="week._expanded=!week._expanded"> Week of {{ week._date | date: 'EEEE MMM d' }} - {{ week._end | date: 'EEEE d, yyyy' }} <span ng-if="week._expanded">(click to close)</span> <span ng-if="!week._expanded">(click to edit)</span> </h3> <div ng-if="week._expanded" class="expanded"> <table> <tr ng-repeat="day in week._days"> <td>{{ day.name }}</td> <td><input type="text" ng-model="day.text" ng-blur="saveWeeks()"></td> </tr> </table> </div> <div ng-if="!week._expanded" class="collapsed"> <table> <tr ng-repeat="day in week._days"> <td><b>{{ day.name }}</b></td> <td>{{ day.text }}</td> </tr> </table> </div> </div> <hr> <p> Click to edit the week entries. After reloading the page you get back what you typed last. </p> <p> A useful extension of this would be to be able to add new weeks. And make it infinitely prettier. </p> </div> </div> 

И это код JavaScript:

 angular.module('app', []) .controller('Ctrl', ($scope) => { const weekdays = [ [0, 'Monday'], [1, 'Tuesday'], [2, 'Wednesday'], [3, 'Thursday'], [4, 'Friday'], [5, 'Saturday'], [6, 'Sunday'], ] let dressUp = (weeks) => { // add internally needed things weeks.forEach((week) => { week._date = Date.create(week.date) week._end = week._date.clone() week._end.addDays(6) week._days = []; weekdays.forEach((pair) => { week._days.push({ index: pair[0], name: pair[1], text: week.days[pair[0]] || '' }) }) }) } let dressDown = (weeks) => { // week.days is an object, turn it into an array weeks.forEach((week) => { week._days.forEach((day) => { week.days[day.index] = day.text || '' }) }) } // try to retrieve from persistent storage $scope.weeks = JSON.parse( localStorage.getItem('weeks') || '{"weeks":[]}' ).weeks if (!$scope.weeks.length) { // add a first default let monday = Date.create().beginningOfISOWeek().format('{yyyy}-{MM}-{dd}') $scope.weeks.push({date: monday, days: {}}) } // when retrieved it doesn't have the internal // stuff we need for rendering, so dress it up dressUp($scope.weeks) $scope.saveWeeks = () => { // copy from _days to days dressDown($scope.weeks) // make a deep copy clone let copy = Array.from($scope.weeks, (item) => { let obj = Object.assign({}, item) for (let key of Object.keys(obj)) { if (key.startsWith('_') || key === '$$hashKey') { delete obj[key] } } return obj }) // actually save it persistently localStorage.setItem('weeks', JSON.stringify({weeks: copy})) } }); 

Живое демо показано здесь .

Там много чего происходит.

Демонстрационное приложение использует localStorage для постоянства. После загрузки приложения оно извлекает прошлые сохраненные данные и, если там ничего нет, создает первый образец. Затем он отображает недели, и вы можете редактировать записи. Как только вы стираете любое поле ввода, начинается процесс сохранения. Во-первых, он немного изменяет состояние (копирование из списка week._days в объект week.days ), а во-вторых, создает клон глубокого копирования, а когда он это делает, он удаляет все ключи, которые мы не считаем необходимыми для сохранения. , Наконец, он хранит его в localStorage .

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

Стойкость и дальше

Кто-то может посмеяться над тем, что localStorage в браузере является постоянным. Он хранится только на вашем устройстве, и если вы потеряете устройство или полностью сотрете свой профиль, память исчезнет. Более устойчивое решение — правильная база данных в облаке. Тот, который копируется и реплицируется и еще много чего. Тем не менее, это выходит за рамки данной статьи и расширение, чтобы сделать это действительно просто. Поскольку мы храним JSON в localStorage , это означает, что Ajax отправит только то, что мы хотим сохранить, и вам не придется беспокоиться о столбцах или типах.

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

Выводы

Это реалистично? Да, это так! Это масштабируется? Да.

Мы вступаем в эру, когда внешние интерфейсы становятся умнее, а внутренние — тупее. Это означает, что вы вкладываете большую часть бизнес-логики в свой код переднего плана и просите, чтобы сервер просто выполнил действительно базовые задачи (например, «Просто сохраните этот BLOB-объект!»). Если вы хотите создать одно из этих модных приложений, работающих в автономном режиме, вам нужно подумать о сохранении всех состояний в веб-приложении, и вы не можете зависеть от того, всегда ли сервер доступен. В конце концов, так и должно быть.

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

Кроме того, использование бизнес-логики во внешнем интерфейсе и представление о бэк-энде как о «немом» означает, что вы можете стать гораздо более агностиком в отношении своих серверных технологий. Эти серверные технологии заняты тем, что они доступны, их резервное копирование и быстрое. Упомянутое выше демонстрационное приложение использует localStorage но его очень легко заменить на Kinto , PouchDB или Firebase . Это все очень масштабируемые платформы. Итак, опять же, эта модель действительно масштабируется.