В 2013 году Senfino закрыла сделку по предоставлению настольного приложения для пациентов после операции по реконструкции коленного сустава. Предполагается, что он поможет им вернуться в форму, ежедневно давая рекомендации по комплексу упражнений, которые можно выполнять дома. Эти упражнения такие же, как в реабилитационной клинике. Весь процесс должен занять около трех месяцев. Однако, в зависимости от прогресса пациента, он может быть автоматически продлен.

Приложение изначально должно было быть мультиплатформенным. Клиент не заботился о конкретных инструментах, которые мы использовали, только о конечном результате. Но он должен был работать на Windows и OSX. Вместо создания двух нативных реализаций мы решили, что будет намного проще использовать обертку для каждой операционной системы. Таким образом, мы могли бы совместно использовать как можно больше кода, работающего внутри них. Они не должны были выглядеть как родные. Более важной особенностью была простота использования. Программа должна была показать пациенту, что делать дальше, и дать ему дополнительную информацию, когда он в ней нуждался. Выбор инструментов быстро стал ясен. У нас было время и силы для экспериментов с JavaScript, поэтому мы пошли по пути Adobe Air и его преемников.

Adobe Air существует уже давно, но ему так и не удалось получить достаточное признание. Я видел приложения, работающие с его использованием. Хотя все они были довольно просты. По умолчанию приложение требует установки Adobe Air на клиентском компьютере. В более новых развертываниях можно использовать Adobe Air, чтобы упростить процесс установки. Было несколько проблем, которые мешали нам использовать его. В то время как разработка Flash поддерживалась довольно хорошо, разработка JavaScript сводилась к использованию командной строки и простому указанию, где находится проект. Его нужно было отделить от среды выполнения, что означало множество распакованных файлов. Но самой большой проблемой было заставить работать AngularJS. Я рекомендовал этот фреймворк в самом начале. Мы не знали, как будет выглядеть проект, и со временем в его функционировании должны были произойти некоторые изменения. Это был гораздо более безопасный подход, который позже доказал свою эффективность на практике. Adobe Air работает в песочнице, что кажется хорошей идеей, пока вы не заметите, что это меняет работу веб-приложения. Самой большой проблемой здесь была перезапись путей к файлам. Иногда это срабатывало, иногда нет. У Angular были проблемы с этим. Он также полностью отказался запускаться из-за некоторых ограничений безопасности. В Интернете был пост, объясняющий, как пропустить уровень безопасности, но это было похоже на взлом. Кроме того, выполнение приложения в этой модифицированной среде означало, что его нельзя было протестировать непосредственно в браузере или, по крайней мере, без слоя абстракции, скрывающего различия. С точки зрения разработчиков это было не так уж плохо, но клиенты хотели иметь простой способ удаленно проверять ход разработки и качество взаимодействия. Итак, поиск продолжился.

Нам посчастливилось найти альтернативу Air. Первым был TideSDK. Это новый проект и, к сожалению, не удовлетворил нашим требованиям. Он эмулирует Chromium, что является отличной идеей. Основная проблема заключалась в том, что он не мог хорошо обрабатывать видео. При воспроизведении на экране были артефакты. Он не получал новейшие версии Chrome достаточно быстро, чтобы быть готовым к серьезным приложениям.

Последним протестированным инструментом был node-webkit, созданный Intel. Он использует обновленный Chromium, но поверх Node, поэтому его инструменты также присутствовали. У него нет ограничительной песочницы, такой как Adobe Air, и он запускает веб-приложения с графическим ускорением. Angular работает без каких-либо модификаций. Доступны инструменты, которые могут аккуратно упаковать все в один файл, готовый для определенных операционных систем. Видео WebM работает, но есть библиотеки для других форматов, которые можно включать отдельно. Еще одной полезной функцией стала поддержка Grunt. grunt-node-webkit-builder позволяет развертывать несколько версий для разных операционных систем и пакет приложения (с расширением .nw) отдельно, если это необходимо, и в целом имеет удовлетворительное количество настроек. Так что это то, что мы в конечном итоге решили использовать.

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

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

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

Детали реализации

Этого уровня сложности было достаточно, чтобы замедлить работу приложения в нескольких ключевых областях, особенно когда пациент выполняет упражнение, что происходит большую часть времени. Это связано с тем, как AngularJS обрабатывает изменения. Он переоценивает все наблюдаемые значения и делает это снова, когда что-то изменяется во время этой переоценки, подход, аналогичный новому Object.observe в ECMAScript Harmony. В большинстве случаев этот подход очень эффективен и прост в реализации. В отличие от KnockoutJS, модели данных могут быть просто объектами. Однако, когда многие значения изменяются за короткие промежутки времени, нагрузка становится слишком большой. Именно эта проблема возникла при реализации таймера упражнений в виде отдельной директивы. Предполагалось, что оно будет обновляться каждые 18 миллисекунд, что позволяет анимации работать плавно. Теперь я знаю, что есть решения получше, чем использование setTimeout, например requestAnimationFrame, но это не главная проблема. Виновником стал результат звонка, который заставил провести повторную оценку. Единственная оптимизация, сделанная здесь, заключалась в задержке. Существуют способы изолировать область оценки с помощью пользовательских директив или $digest (хорошее объяснение можно найти здесь и более хакерский способ здесь). К сожалению, многие части приложения реагируют на таймер. Они могут приостановить, сбросить его или действовать по завершении. Другая возможность — использовать ng-if (появившийся после Angular 1.0.4) или ng-include. Они удаляют целые деревья DOM и наблюдателей. Вероятно, это самый простой вариант, который эффективно работает в большинстве ситуаций. Проблема в нашем случае, которая ограничивала их использование, снова заключалась во взаимосвязи различных частей приложения и задержке инициализации. Часто макет прыгал на секунду, прежде чем каждое поле заполнялось данными из моделей, что снижало качество взаимодействия с пользователем.

Другим ключевым решением было окружить AngularJS модулями AMD, в частности RequireJS. AMD использовалась во многих предыдущих проектах, что делает разработку более организованной. При этом будущие приложения будут основаны на объединении файлов в реальном времени с использованием Yeoman. Yeoman в значительной степени полагается на Grunt, может запустить сервер, который следит за изменениями в файлах и автоматически выполняет множество оптимизаций. В обсуждаемом случае Grunt также используется, но более простым способом и сжимает код только для производства.

Большая часть кода выполняется на стандартных структурах AngularJS: контроллерах и директивах. Однако был момент, когда количество различных результатов от использования приложения разными способами делало дальнейшую разработку сложной и медленной. Например, когда пациент пытается проверить свой прогресс в информационной панели, если он еще не настроил приложение, он будет перенаправлен на экран регистрации. Другой пример — когда пользователь завершает последний этап тренировки. Вместо того, чтобы возвращать его обратно к экрану прогресса, который происходит при завершении нижних этапов, ему показывают видео, рассказывающее о его текущей ситуации и о том, что ему следует делать дальше.

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

Соединить конечный автомат с остальной частью приложения было непросто. Необходимо было выбрать определенный уровень детализации. Когда алгоритм изменял приложение по одному полю ввода за раз, это было слишком специфично, но выполнение одного действия, изменяющего десять разных вещей, обычно означало создание двух других, которые лишь немного отличались от оригинала. Кроме того, операторы $watch нужно было использовать с осторожностью, потому что они могли изменить приложение в конфликте с конечным автоматом. Каждое состояние обрабатывает только те сигналы, которые оно хочет. Другие проверяются на соответствие набору действий по умолчанию, например, сигналу тактовых импульсов, который можно обрабатывать одинаково независимо от того, что происходит. Наличие значений по умолчанию значительно уменьшило объем кода.

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

Уникальный подход был создан с точки зрения хранения данных. В начале была проблема с выбором места для каждого значения: в памяти, в локальном хранилище (поддерживаемом браузерами) или в файлах. Для хранения требовался уровень абстракции, но этого было недостаточно. Хранение данных в разных контейнерах должно быть простым и прозрачным. Остальной части приложения нужно было знать не то, где все сохранено, а то, как изменять и извлекать значения.

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

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

Сейчас приложение тестируют как пациенты, так и их тренеры. Пока результаты многообещающие.

Подводя итог, вот ключевые вещи, которые показал этот проект:

  • Клиентам нравится иметь простой способ проверки текущего состояния своего приложения на своих компьютерах и в одиночку.
  • Модули AMD решают организационные проблемы, но автоматическая конкатенация кода с помощью инструментов более гибка.
  • Циклы дайджеста Angular могут значительно замедлить работу больших приложений, поэтому рекомендуется с самого начала разрабатывать их с учетом этой проблемы.
  • Angular отлично подходит для быстрой разработки, но оставляет место для изменения функциональности по ходу дела.
  • Grunt — очень универсальный инструмент, который поддерживает множество различных потребностей.
  • Сотрудничество с клиентами и ведение заметок во время встреч могут решить многие проблемы до того, как они возникнут.
  • node-webkit — самая продвинутая оболочка для веб-приложений.
  • Хранение больших исходных файлов в Git невозможно. Базовая структура папок, где каждая из них помечена датой, лучше.
  • Старые исходные файлы могут потребоваться в любой момент, хорошо иметь их под рукой.
  • Во время реализации будут происходить оптимизации. Не все из них оправдывают огромные изменения в коде. Код никогда не будет таким хорошим, каким его хотят видеть программисты. Какая реализация победит, всегда зависит от предпочтений.

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

Мы объединяем аналитиков, разработчиков, дизайнеров и исследователей. Посетите нас на senfino.com, чтобы реализовать свою следующую идею.