Проектирование и создание прогрессивного веб-приложения без фреймворка (часть 2)
Опубликовано: 2022-03-10Смысл этого приключения заключался в том, чтобы немного подтолкнуть вашего скромного автора в дисциплинах визуального дизайна и кодирования JavaScript. Функциональность приложения, которое я решил создать, не отличалась от приложения «что делать». Важно подчеркнуть, что это не было упражнением в оригинальном мышлении. Пункт назначения был гораздо менее важен, чем путь.
Хотите узнать, чем закончилось приложение? Откройте в браузере телефона https://io.benfrain.com.
Вот краткое изложение того, что мы рассмотрим в этой статье:
- Настройка проекта и почему я выбрал Gulp в качестве инструмента сборки;
- Шаблоны проектирования приложений и что они означают на практике;
- Как хранить и визуализировать состояние приложения;
- как CSS был привязан к компонентам;
- какие тонкости UI/UX были использованы, чтобы сделать вещи более похожими на приложения;
- Как область ответственности изменилась в ходе итерации.
Начнем с инструментов сборки.
Инструменты сборки
Чтобы настроить и запустить мои базовые инструменты TypeScipt и PostCSS и создать достойный опыт разработки, мне понадобится система сборки.
На моей основной работе в течение последних пяти лет или около того я создавал прототипы интерфейсов на HTML/CSS и, в меньшей степени, на JavaScript. До недавнего времени я использовал Gulp с любым количеством плагинов почти исключительно для удовлетворения моих довольно скромных потребностей в сборке.
Обычно мне нужно обрабатывать CSS, конвертировать JavaScript или TypeScript в более широко поддерживаемый JavaScript, а иногда выполнять связанные задачи, такие как минимизация вывода кода и оптимизация ресурсов. Использование Gulp всегда позволяло мне решать эти проблемы с апломбом.
Для тех, кто не знаком, Gulp позволяет вам писать JavaScript, чтобы делать «что-то» с файлами в вашей локальной файловой системе. Чтобы использовать Gulp, у вас обычно есть один файл (называемый gulpfile.js
) в корне вашего проекта. Этот файл JavaScript позволяет вам определять задачи как функции. Вы можете добавить сторонние «Плагины», которые по сути являются дополнительными функциями JavaScript, решающими определенные задачи.
Пример задачи Gulp
Примером задачи Gulp может быть использование плагина для использования PostCSS для обработки в CSS при изменении таблицы стилей разработки (gulp-postcss). Или компилировать файлы TypeScript в ванильный JavaScript (gulp-typescript) по мере их сохранения. Вот простой пример того, как вы пишете задачу в Gulp. Эта задача использует плагин gulp del для удаления всех файлов в папке с именем build:
var del = require("del"); gulp.task("clean", function() { return del(["build/**/*"]); });
require
назначает плагин del
переменной. Затем вызывается метод gulp.task
. Мы называем задачу строкой в качестве первого аргумента («очистить»), а затем запускаем функцию, которая в данном случае использует метод del для удаления папки, переданной ей в качестве аргумента. Символы звездочки — это шаблоны «шариков», которые, по сути, говорят «любой файл в любой папке» папки сборки.
Задачи Gulp могут стать намного сложнее, но, по сути, это механика того, как все обрабатывается. Правда в том, что с Gulp вам не нужно быть волшебником JavaScript, чтобы обойтись; Навыки копирования и вставки 3 класса — это все, что вам нужно.
Все эти годы я придерживался Gulp в качестве инструмента сборки/исполнителя задач по умолчанию с политикой «если он не сломался; не пытайтесь исправить это».
Тем не менее, я беспокоился, что застрял на своем пути. В эту ловушку легко попасть. Во-первых, вы начинаете отдыхать в одном и том же месте каждый год, затем отказываетесь перенимать какие-либо новые модные тенденции, прежде чем, в конце концов, и упорно отказываетесь опробовать какие-либо новые инструменты для сборки.
Я слышал много болтовни в Интернете о «Webpack» и подумал, что это мой долг — попробовать проект, использующий новомодный тост крутых ребят-разработчиков интерфейса.
Вебпак
Я отчетливо помню, как с большим интересом перешел на сайт webpack.js.org. Первое объяснение того, что такое Webpack, начиналось так:
import bar from './bar';
Чего-чего? По словам Доктора Зла, «Брось мне сюда чертову кость, Скотт».
Я знаю, что это мое собственное зависание, но у меня появилось отвращение к любым объяснениям кодирования, в которых упоминаются «foo», «bar» или «baz». Это плюс полное отсутствие краткого описания того, для чего на самом деле был Webpack, заставило меня заподозрить, что, возможно, это не для меня.
Если немного углубиться в документацию Webpack, было предложено несколько менее туманное объяснение: «По своей сути, webpack — это сборщик статических модулей для современных приложений JavaScript».
Хм. Сборщик статических модулей. Этого ли я хотел? Я не был убежден. Я читал дальше, но чем больше я читал, тем менее я был понятен. В то время такие понятия, как графы зависимостей, горячая перезагрузка модуля и точки входа, были для меня практически потеряны.
Спустя пару вечеров изучения Webpack я отказался от любой идеи его использования.
Я уверен, что в правильной ситуации и в более опытных руках Webpack будет чрезвычайно мощным и подходящим, но для моих скромных потребностей он казался излишним. Объединение модулей, встряхивание деревьев и перезагрузка модулей в горячем режиме звучали великолепно; Я просто не был уверен, что они нужны мне для моего маленького «приложения».
Итак, вернемся к Gulp.
На тему отказа от изменений ради изменений я хотел оценить еще одну технологию — Yarn over NPM для управления зависимостями проекта. До этого момента я всегда использовал NPM, а Yarn рекламировался как лучшая и более быстрая альтернатива. Мне нечего сказать о Yarn, за исключением того, что если вы в настоящее время используете NPM и все в порядке, вам не нужно пытаться использовать Yarn.
Один инструмент, который появился слишком поздно для оценки этого приложения, — это Parceljs. С нулевой конфигурацией и поддержкой BrowserSync, такой как перезагрузка браузера, я с тех пор нашел в нем большую полезность! Кроме того, в защиту Webpack мне сказали, что для Webpack v4 и выше не требуется файл конфигурации. Как ни странно, в недавнем опросе, который я провел в Твиттере, из 87 респондентов более половины выбрали Webpack, а не Gulp, Parcel или Grunt.
Я запустил свой файл Gulp с базовой функциональностью, чтобы приступить к работе.
Задача «по умолчанию» просматривает «исходные» папки таблиц стилей и файлов TypeScript и компилирует их в папку build
вместе с базовым HTML и связанными исходными картами.
У меня BrowserSync тоже работает с Gulp. Я мог не знать, что делать с конфигурационным файлом Webpack, но это не означало, что я был каким-то животным. Необходимость вручную обновлять браузер во время итерации с HTML/CSS — это оооочень 2010 год, а BrowserSync дает вам короткую обратную связь и цикл итераций, которые так полезны для внешнего кодирования.
Вот базовый файл gulp по состоянию на 11.06.2017.
Вы можете видеть, как я настроил Gulpfile ближе к концу поставки, добавив минификацию с помощью ugilify:
Структура проекта
В результате моего выбора технологии некоторые элементы организации кода для приложения определялись сами собой. gulpfile.js
в корне проекта, папка node_modules
(где Gulp хранит код плагина), папка preCSS
для таблиц стилей разработки, папка ts
для файлов TypeScript и папка build
для скомпилированного кода.
Идея состояла в том, чтобы иметь index.html
, содержащий «оболочку» приложения, включая любую нединамическую структуру HTML, а затем ссылки на стили и файл JavaScript, которые заставят приложение работать. На диске это будет выглядеть примерно так:
build/ node_modules/ preCSS/ img/ partials/ styles.css ts/ .gitignore gulpfile.js index.html package.json tsconfig.json
Настройка BrowserSync для просмотра этой папки build
означала, что я мог указать свой браузер на localhost:3000
, и все было хорошо.
Имея базовую систему сборки, упорядоченную организацию файлов и некоторые базовые проекты, с которых можно было начать, у меня закончились корма для прокрастинации, которые я мог законно использовать, чтобы помешать мне на самом деле построить эту вещь!
Написание приложения
Принцип работы приложения был такой. Будет хранилище данных. Когда JavaScript загрузится, он загрузит эти данные, переберет каждого игрока в данных, создав HTML-код, необходимый для представления каждого игрока в виде строки в макете, и поместит их в соответствующий раздел входа/выхода. Затем действия пользователя переводили игрока из одного состояния в другое. Простой.
Когда дело дошло до написания приложения, необходимо было понять две большие концептуальные проблемы:
- Как представить данные для приложения таким образом, чтобы их можно было легко расширять и манипулировать ими;
- Как заставить пользовательский интерфейс реагировать на изменение данных, введенных пользователем.
Один из самых простых способов представить структуру данных в JavaScript — это нотация объекта. Это предложение читается как компьютерная наука. Проще говоря, «объект» на жаргоне JavaScript — это удобный способ хранения данных.
Рассмотрим этот объект JavaScript, назначенный переменной с именем ioState
(для состояния входа/выхода):
var ioState = { Count: 0, // Running total of how many players RosterCount: 0; // Total number of possible players ToolsExposed: false, // Whether the UI for the tools is showing Players: [], // A holder for the players }
Если вы не очень хорошо знаете JavaScript, вы, вероятно, можете хотя бы понять, что происходит: каждая строка внутри фигурных скобок представляет собой пару свойства (или «ключа» на языке JavaScript) и значения. Вы можете установить всевозможные вещи для ключа JavaScript. Например, функции, массивы других данных или вложенные объекты. Вот пример:
var testObject = { testFunction: function() { return "sausages"; }, testArray: [3,7,9], nestedtObject { key1: "value1", key2: 2, } }
Конечным результатом является то, что с помощью такой структуры данных вы можете получить и установить любой из ключей объекта. Например, если мы хотим установить счетчик объекта ioState равным 7:
ioState.Count = 7;
Если мы хотим установить фрагмент текста в это значение, нотация работает следующим образом:
aTextNode.textContent = ioState.Count;
Вы можете видеть, что получение значений и установка значений для этого объекта состояния просты с точки зрения JavaScript. Однако отражение этих изменений в пользовательском интерфейсе не так важно. Это основная область, в которой фреймворки и библиотеки пытаются абстрагироваться от боли.
В общих чертах, когда дело доходит до обновления пользовательского интерфейса на основе состояния, предпочтительнее избегать запросов к DOM, так как это обычно считается неоптимальным подходом.
Рассмотрим интерфейс входа/выхода. Обычно это список потенциальных игроков для игры. Они перечислены вертикально, один под другим, вниз по странице.
Возможно, каждый игрок представлен в DOM с помощью label
, обертывающей input
флажка. Таким образом, щелчок по проигрывателю переключал бы проигрыватель в состояние «В игре» благодаря метке, делающей ввод «отмеченным».
Чтобы обновить наш интерфейс, у нас может быть «слушатель» для каждого элемента ввода в JavaScript. При щелчке или изменении функция запрашивает DOM и подсчитывает, сколько входных данных нашего игрока проверено. На основе этого подсчета мы могли бы обновить что-то еще в DOM, чтобы показать пользователю, сколько игроков проверено.
Давайте рассмотрим стоимость этой базовой операции. Мы прослушиваем несколько узлов DOM для щелчка/проверки ввода, затем запрашиваем DOM, чтобы увидеть, сколько экземпляров определенного типа DOM проверено, затем записываем что-то в DOM, чтобы показать пользователю, с точки зрения пользовательского интерфейса, количество игроков. мы просто посчитали.
Альтернативой может быть сохранение состояния приложения в виде объекта JavaScript в памяти. Щелчок кнопки/ввода в DOM может просто обновить объект JavaScript, а затем, на основе этого изменения в объекте JavaScript, выполнить однопроходное обновление всех необходимых изменений интерфейса. Мы могли бы не запрашивать DOM для подсчета игроков, поскольку объект JavaScript уже содержит эту информацию.
Так. Использование структуры объекта JavaScript для состояния казалось простым, но достаточно гибким, чтобы инкапсулировать состояние приложения в любой момент времени. Теория того, как этим можно управлять, тоже казалась достаточно разумной — должно быть, это то, о чем говорят такие фразы, как «односторонний поток данных»? Тем не менее, первый реальный трюк заключается в создании некоторого кода, который будет автоматически обновлять пользовательский интерфейс на основе любых изменений в этих данных.
Хорошая новость заключается в том, что более умные люди, чем я, уже разобрались с этим ( слава богу! ). Люди совершенствовали подходы к решению подобных задач с момента появления приложений. Эта категория проблем — хлеб с маслом «шаблонов проектирования». Прозвище «шаблон проектирования» поначалу звучало для меня эзотерически, но после того, как я немного покопался, все это стало звучать не столько как компьютерная наука, сколько как здравый смысл.
Шаблоны проектирования
Шаблон проектирования в лексиконе информатики — это заранее определенный и проверенный способ решения общей технической задачи. Думайте о шаблонах проектирования как о программном эквиваленте кулинарного рецепта.
Возможно, самая известная литература по шаблонам проектирования — это «Шаблоны проектирования: элементы многоразового объектно-ориентированного программного обеспечения», вышедшая еще в 1994 году. Что касается JavaScript, книга Эдди Османи «Изучение шаблонов проектирования JavaScript» охватывает аналогичную тему. Вы также можете прочитать его онлайн бесплатно здесь.
Шаблон наблюдателя
Обычно шаблоны проектирования делятся на три группы: творческие, структурные и поведенческие. Я искал что-то поведенческое, что помогло бы справиться с обменом информацией об изменениях в различных частях приложения.
Совсем недавно я видел и читал отличное подробное описание реализации реактивности внутри приложения Грегга Поллака. Здесь есть как пост в блоге, так и видео для вашего удовольствия.
Прочитав вступительное описание шаблона «Наблюдатель» в книге « Learning JavaScript Design Patterns
, я был почти уверен, что это шаблон для меня. Это описывается так:
Наблюдатель — это шаблон проектирования, в котором объект (известный как субъект) поддерживает список объектов, зависящих от него (наблюдателей), автоматически уведомляя их о любых изменениях состояния.
Когда субъекту необходимо уведомить наблюдателей о том, что происходит что-то интересное, он передает наблюдателям уведомление (которое может включать конкретные данные, относящиеся к теме уведомления).
Ключом к моему волнению было то, что это, казалось, предлагало какой-то способ обновления вещей, когда это необходимо.
Предположим, что пользователь щелкнул игрока по имени «Бетти», чтобы отметить, что она участвует в игре. В пользовательском интерфейсе может потребоваться несколько вещей:
- Добавьте 1 к счету игр
- Удалить Бетти из пула игроков «Out»
- Добавьте Бетти в пул игроков
Приложению также потребуется обновить данные, представляющие пользовательский интерфейс. Чего я очень хотел избежать, так это:
playerName.addEventListener("click", playerToggle); function playerToggle() { if (inPlayers.includes(e.target.textContent)) { setPlayerOut(e.target.textContent); decrementPlayerCount(); } else { setPlayerIn(e.target.textContent); incrementPlayerCount(); } }
Цель состояла в том, чтобы иметь элегантный поток данных, который обновлял бы то, что было необходимо в DOM, когда и если бы центральные данные были изменены.
С помощью шаблона Observer можно было довольно лаконично отправлять обновления состояния и, следовательно, пользовательского интерфейса. Вот пример фактической функции, используемой для добавления нового игрока в список:
function itemAdd(itemString: string) { let currentDataSet = getCurrentDataSet(); var newPerson = new makePerson(itemString); io.items[currentDataSet].EventData.splice(0, 0, newPerson); io.notify({ items: io.items }); }
Часть, относящаяся к шаблону Observer, представляет собой метод io.notify
. Поскольку это показывает, как мы модифицируем items
состояния приложения, позвольте мне показать вам наблюдателя, который прослушивал изменения в «элементах»:
io.addObserver({ props: ["items"], callback: function renderItems() { // Code that updates anything to do with items... } });
У нас есть метод уведомления, который вносит изменения в данные, а затем наблюдатели к этим данным, которые отвечают, когда интересующие их свойства обновляются.
При таком подходе приложение может иметь наблюдаемые объекты, отслеживающие изменения в любом свойстве данных, и запускать функцию всякий раз, когда происходит изменение.
Если вас интересует шаблон Observer, который я выбрал, я более подробно опишу его здесь.
Теперь появился подход для эффективного обновления пользовательского интерфейса на основе состояния. Персиковый. Тем не менее, это все еще оставило меня с двумя вопиющими проблемами.
Одним из них было то, как сохранить состояние при перезагрузке страницы/сеансах, и тот факт, что, несмотря на то, что пользовательский интерфейс работает, визуально он просто не очень похож на приложение. Например, если была нажата кнопка, пользовательский интерфейс мгновенно изменился на экране. Просто это не было особенно убедительно.
Давайте сначала разберемся со стороной хранения вещей.
Сохранение состояния
Мой основной интерес со стороны разработчиков был сосредоточен на понимании того, как можно создавать интерфейсы приложений и делать их интерактивными с помощью JavaScript. Как хранить и извлекать данные с сервера или решать вопросы аутентификации пользователей и входа в систему, было «выходом за рамки».
Поэтому вместо того, чтобы подключаться к веб-сервису для хранения данных, я решил хранить все данные на клиенте. Существует ряд методов веб-платформы для хранения данных на клиенте. Я выбрал localStorage
.
API для localStorage невероятно прост. Вы устанавливаете и получаете данные следующим образом:
// Set something localStorage.setItem("yourKey", "yourValue"); // Get something localStorage.getItem("yourKey");
LocalStorage имеет метод setItem
, которому вы передаете две строки. Первая — это имя ключа, с которым вы хотите сохранить данные, а вторая строка — это фактическая строка, которую вы хотите сохранить. Метод getItem
принимает строку в качестве аргумента, который возвращает вам все, что хранится под этим ключом в localStorage. Красиво и просто.
Однако среди причин не использовать localStorage является тот факт, что все должно быть сохранено как «строка». Это означает, что вы не можете напрямую хранить что-то вроде массива или объекта. Например, попробуйте запустить эти команды в консоли браузера:
// Set something localStorage.setItem("myArray", [1, 2, 3, 4]); // Get something localStorage.getItem("myArray"); // Logs "1,2,3,4"
Несмотря на то, что мы пытались установить значение «myArray» как массив; когда мы его извлекли, он был сохранен в виде строки (обратите внимание на кавычки вокруг «1,2,3,4»).
Вы, конечно, можете хранить объекты и массивы с помощью localStorage, но вы должны помнить, что они должны преобразовываться из строк туда и обратно.
Итак, чтобы записать данные о состоянии в localStorage, они были записаны в строку JSON.stringify()
следующим образом:
const storage = window.localStorage; storage.setItem("players", JSON.stringify(io.items));
Когда данные необходимо было извлечь из localStorage, строка была преобразована обратно в пригодные для использования данные с помощью JSON.parse()
следующим образом:
const players = JSON.parse(storage.getItem("players"));
Использование localStorage
означало, что все было на клиенте, и это означало отсутствие проблем со сторонними службами или хранением данных.
Данные теперь сохраняли обновления и сеансы — ура! Плохая новость заключалась в том, что localStorage не выдерживает очистки данных браузера пользователем. Когда кто-то это сделает, все его данные о входе/выходе будут потеряны. Это серьезный недостаток.
Нетрудно понять, что `localStorage`, вероятно, не лучшее решение для "правильных" приложений. Помимо вышеупомянутой проблемы со строками, он также замедляет серьезную работу, поскольку блокирует «основной поток». Появляются альтернативы, такие как KV Storage, но на данный момент обратите внимание на то, чтобы предостеречь его от использования, исходя из его пригодности.
Несмотря на хрупкость локального сохранения данных на пользовательском устройстве, подключение к службе или базе данных встречало сопротивление. Вместо этого проблему решили обойти, предложив опцию «загрузить/сохранить». Это позволит любому пользователю In/Out сохранить свои данные в виде файла JSON, который при необходимости можно будет загрузить обратно в приложение.
Это хорошо работало на Android, но гораздо менее элегантно для iOS. На iPhone это привело к тому, что на экране появилось такое количество текста:
Как вы понимаете, я был далеко не единственным, кто ругал Apple через WebKit за этот недостаток. Соответствующая ошибка была здесь.
На момент написания этой статьи для этой ошибки уже было найдено решение и патч, но она еще не появилась в iOS Safari. Предположительно, iOS13 исправляет это, но пока я пишу, это бета-версия.
Итак, для моего минимально жизнеспособного продукта это было адресовано для хранения. Теперь пришло время попытаться сделать вещи более похожими на приложения!
App-I-Ness
Оказывается, после многих дискуссий со многими людьми точно определить, что означает «приложение похожее», довольно сложно.
В конце концов, я остановился на том, что слово «приложение» является синонимом визуальной гладкости, обычно отсутствующей в сети. Когда я думаю о приложениях, которые приятно использовать, все они поддерживают движение. Не беспричинное, а движение, которое дополняет историю ваших действий. Это могут быть переходы страниц между экранами, способ появления меню. Это трудно описать словами, но большинство из нас узнают это, когда видят.
Первым необходимым визуальным элементом было смещение имен игроков вверх или вниз с «Вход» на «Выход» и наоборот при выборе. Заставить игрока мгновенно переходить из одного раздела в другой было просто, но, конечно, не «как в приложении». Мы надеемся, что анимация щелчка по имени игрока подчеркнет результат этого взаимодействия — переход игрока из одной категории в другую.
Как и многие из этих видов визуального взаимодействия, их кажущаяся простота противоречит сложности, связанной с тем, чтобы на самом деле заставить их работать хорошо.
Потребовалось несколько итераций, чтобы получить правильное движение, но основная логика была такова:
- Как только «игрок» щелкнут, зафиксируйте, где этот игрок находится на странице в геометрической прогрессии;
- Измерьте, насколько далеко находится верхняя часть области, до которой игрок должен двигаться, если идет вверх («Вход»), и как далеко находится нижняя часть, если он идет вниз («Выход»);
- При движении вверх необходимо оставить пространство, равное высоте ряда игроков, когда игрок движется вверх, а игроки выше должны падать вниз с той же скоростью, что и время, необходимое игроку, чтобы подняться и приземлиться в пространстве. освобождается существующими игроками In (если таковые существуют);
- Если игрок выходит из игры и движется вниз, все остальные должны переместиться вверх на оставшееся место, а игрок должен оказаться ниже любых текущих игроков «вне игры».
Фу! С английским было сложнее, чем я думал — не говоря уже о JavaScript!
Были дополнительные сложности, которые нужно было рассмотреть и опробовать, например, скорость перехода. Вначале не было очевидно, что лучше будет выглядеть: постоянная скорость движения (например, 20 пикселей за 20 мс) или постоянная продолжительность движения (например, 0,2 с). Первый был немного сложнее, так как скорость нужно было вычислять «на лету» в зависимости от того, как далеко игроку нужно было пройти — большее расстояние требовало большей продолжительности перехода.
Однако оказалось, что постоянная длительность перехода не просто проще в коде; это действительно произвело более благоприятный эффект. Разница была тонкой, но это тот выбор, который вы можете определить только после того, как увидите оба варианта.
Время от времени при попытке добиться этого эффекта в глаза бросался визуальный сбой, но его было невозможно разобрать в реальном времени. Я обнаружил, что лучшим процессом отладки является создание записи анимации в формате QuickTime, а затем просмотр ее по кадрам. Неизменно это выявляло проблему быстрее, чем любая отладка на основе кода.
Глядя на код сейчас, я понимаю, что в чем-то помимо моего скромного приложения эту функциональность почти наверняка можно было бы написать более эффективно. Учитывая, что приложение будет знать количество игроков и фиксированную высоту планок, должно быть вполне возможно выполнить все расчеты расстояния только в JavaScript, без какого-либо чтения DOM.
Дело не в том, что то, что было отправлено, не работает, просто это не то кодовое решение, которое вы бы продемонстрировали в Интернете. О, подожди.
Другие взаимодействия, похожие на приложения, реализовать было намного проще. Вместо того, чтобы меню просто появлялись и исчезали с помощью чего-то такого простого, как переключение свойства отображения, было получено много преимуществ, просто раскрывая их с немного большей утонченностью. Он по-прежнему запускался просто, но CSS делал всю тяжелую работу:
.io-EventLoader { position: absolute; top: 100%; margin-top: 5px; z-index: 100; width: 100%; opacity: 0; transition: all 0.2s; pointer-events: none; transform: translateY(-10px); [data-evswitcher-showing="true"] & { opacity: 1; pointer-events: auto; transform: none; } }
Там, когда data-evswitcher-showing="true"
был переключен на родительском элементе, меню исчезало, возвращалось в положение по умолчанию, а события указателя снова включались, чтобы меню могло получать клики.
Методология таблиц стилей ECSS
Вы заметите в предыдущем коде, что с точки зрения автора переопределения CSS вложены в родительский селектор. Именно так я всегда предпочитаю писать таблицы стилей пользовательского интерфейса; единый источник правды для каждого селектора и любые переопределения для этого селектора, заключенные в один набор фигурных скобок. Это шаблон, который требует использования процессора CSS (Sass, PostCSS, LESS, Stylus и др.), но я считаю, что это единственный положительный способ использовать функциональность вложения.
Я закрепил этот подход в своей книге Enduring CSS, и, несмотря на то, что существует множество более сложных методов, доступных для написания CSS для элементов интерфейса, ECSS служит мне и большим группам разработчиков, с которыми я хорошо работаю, с тех пор, как этот подход был впервые задокументирован. еще в 2014 году! В данном случае он оказался столь же эффективным.
Разделение TypeScript
Даже без процессора CSS или надмножества языков, таких как Sass, CSS имеет возможность импортировать один или несколько файлов CSS в другой с помощью директивы import:
@import "other-file.css";
Когда я начинал с JavaScript, я был удивлен, что не было эквивалента. Всякий раз, когда файлы кода становятся длиннее экрана или выше, всегда кажется, что было бы полезно разделить их на более мелкие части.
Еще одним преимуществом использования TypeScript было то, что он имеет очень простой способ разделения кода на файлы и их импорта при необходимости.
Эта возможность предшествовала нативным модулям JavaScript и была очень удобной функцией. Когда TypeScript был скомпилирован, он сшивал все обратно в один файл JavaScript. Это означало, что можно было легко разбить код приложения на управляемые частичные файлы для авторской разработки, а затем легко импортировать их в основной файл. Верх основного inout.ts
выглядел так:
/// <reference path="defaultData.ts" /> /// <reference path="splitTeams.ts" /> /// <reference path="deleteOrPaidClickMask.ts" /> /// <reference path="repositionSlat.ts" /> /// <reference path="createSlats.ts" /> /// <reference path="utils.ts" /> /// <reference path="countIn.ts" /> /// <reference path="loadFile.ts" /> /// <reference path="saveText.ts" /> /// <reference path="observerPattern.ts" /> /// <reference path="onBoard.ts" />
Эта простая хозяйственная и организационная задача очень помогла.
Несколько событий
Вначале я чувствовал, что с точки зрения функциональности одного события, такого как «Футбол во вторник вечером», будет достаточно. В этом сценарии, если вы загрузили вход/выход, вы просто добавляли/удаляли или перемещали игроков внутрь или наружу, и все. Не существовало понятия множественности событий.
Я быстро решил, что (даже при выборе минимально жизнеспособного продукта) это приведет к довольно ограниченному опыту. Что, если бы кто-то организовал две игры в разные дни с другим составом игроков? Конечно, In/Out может/должен удовлетворить эту потребность? Потребовалось не так много времени, чтобы изменить форму данных, чтобы сделать это возможным, и изменить методы, необходимые для загрузки в другом наборе.
Изначально набор данных по умолчанию выглядел примерно так:
var defaultData = [ { name: "Daz", paid: false, marked: false, team: "", in: false }, { name: "Carl", paid: false, marked: false, team: "", in: false }, { name: "Big Dave", paid: false, marked: false, team: "", in: false }, { name: "Nick", paid: false, marked: false, team: "", in: false } ];
Массив, содержащий объект для каждого игрока.
После учета нескольких событий он был изменен, чтобы выглядеть следующим образом:
var defaultDataV2 = [ { EventName: "Tuesday Night Footy", Selected: true, EventData: [ { name: "Jack", marked: false, team: "", in: false }, { name: "Carl", marked: false, team: "", in: false }, { name: "Big Dave", marked: false, team: "", in: false }, { name: "Nick", marked: false, team: "", in: false }, { name: "Red Boots", marked: false, team: "", in: false }, { name: "Gaz", marked: false, team: "", in: false }, { name: "Angry Martin", marked: false, team: "", in: false } ] }, { EventName: "Friday PM Bank Job", Selected: false, EventData: [ { name: "Mr Pink", marked: false, team: "", in: false }, { name: "Mr Blonde", marked: false, team: "", in: false }, { name: "Mr White", marked: false, team: "", in: false }, { name: "Mr Brown", marked: false, team: "", in: false } ] }, { EventName: "WWII Ladies Baseball", Selected: false, EventData: [ { name: "C Dottie Hinson", marked: false, team: "", in: false }, { name: "P Kit Keller", marked: false, team: "", in: false }, { name: "Mae Mordabito", marked: false, team: "", in: false } ] } ];
Новые данные представляли собой массив с объектом для каждого события. Затем в каждом событии было свойство EventData
, которое, как и прежде, представляло собой массив с объектами игрока.
Потребовалось гораздо больше времени, чтобы переосмыслить, как интерфейс лучше всего справляется с этой новой возможностью.
С самого начала дизайн всегда был очень стерильным. Учитывая, что это также должно было быть упражнением в дизайне, я не чувствовал себя достаточно смелым. Поэтому было добавлено немного визуального чутья, начиная с заголовка. Вот что я смоделировал в Sketch:
Он не собирался завоевывать награды, но он определенно был более захватывающим, чем то, с чего он начинался.
Помимо эстетики, только когда кто-то другой указал на это, я оценил, что большой значок плюса в заголовке очень сбивает с толку. Большинство людей думали, что это способ добавить еще одно событие. На самом деле он переключился на режим «Добавить игрока» с причудливым переходом, который позволял вам вводить имя игрока в том же месте, где в данный момент было название события.
Это был еще один случай, когда свежий взгляд был неоценим. Это также был важный урок отпускания. Честно говоря, я придерживался перехода режима ввода в заголовке, потому что чувствовал, что это круто и умно. Однако на самом деле это не соответствовало дизайну и, следовательно, приложению в целом.
Это было изменено в живой версии. Вместо этого заголовок просто имеет дело с событиями — более распространенный сценарий. Между тем, добавление игроков осуществляется из подменю. Это дает приложению гораздо более понятную иерархию.
Другой урок, извлеченный здесь, заключался в том, что всегда, когда это возможно, очень полезно получать откровенные отзывы от коллег. Если они хорошие и честные люди, они не дадут тебе пройти мимо!
Резюме: мой код воняет
Верно. Пока что нормальная техно-приключенческая ретроспектива; эти вещи стоят десять копеек на Medium! Формула выглядит примерно так: разработчик подробно описывает, как он разрушил все препятствия, чтобы выпустить в Интернет отлаженную часть программного обеспечения, а затем прошел собеседование в Google или был где-то нанят. Тем не менее, правда в том, что я был новичком в этой махинации по созданию приложений, поэтому код, который в конечном итоге был отправлен как «законченное» приложение, вонял до небес!
Например, использованная реализация паттерна Observer работала очень хорошо. С самого начала я был организован и методичен, но этот подход «пошел наперекосяк», поскольку я все больше отчаянно пытался довести дело до конца. Как серийный человек, сидящий на диете, вернулись старые знакомые привычки, и впоследствии качество кода упало.
Looking now at the code shipped, it is a less than ideal hodge-bodge of clean observer pattern and bog-standard event listeners calling functions. In the main inout.ts
file there are over 20 querySelector
method calls; hardly a poster child for modern application development!
I was pretty sore about this at the time, especially as at the outset I was aware this was a trap I didn't want to fall into. However, in the months that have since passed, I've become more philosophical about it.
The final post in this series reflects on finding the balance between silvery-towered code idealism and getting things shipped. It also covers the most important lessons learned during this process and my future aspirations for application development.