Если бы я назвал одну вещь, с которой сталкивается большинство кодовых баз JavaScript, это была бы тесная связь вообще и связь с DOM в частности. Тесная связь вызывает головную боль разработчика и проблемы при модульном тестировании кода.
В этой серии из двух частей я дам вам несколько советов о том, как получить слабосвязанный код, и расскажу о том, как отсоединить ваш код от DOM. В этой первой части я познакомлю вас с проблемами, связанными с наличием тесно связанного кода, а также с реальной ситуацией, в которой мы можем применить обсуждаемые концепции: проверку формы.
Что такое муфта?
Во многих приложениях код взаимодействует с различными внешними API. В веб-приложениях мы взаимодействуем с DOM API, возможно, сетью (через XMLHttpRequest), JSON или XML для обмена данными и многими другими. На концептуальном уровне эти проблемы строго отделены друг от друга.
Если REST API, с которым взаимодействует ваше приложение, вносит несколько структурных изменений, разумно, что вам нужно обновить код, взаимодействующий со службой REST. Это не разумно, что это требует изменений в коде рендеринга пользовательского интерфейса. Тем не менее, очень часто это происходит. И когда это происходит, у вас есть то, что называется «тесная связь».
Слабая связь противоположна жесткой связи. В слабосвязанной системе изменение требований к сети не приводит к изменениям в коде рендеринга. Обновленная таблица стилей CSS и новые правила для имен классов не вызывают изменений в коде сериализации данных. Это означает меньше проблем и кодовую базу, о которой легче рассуждать.
Теперь, когда я дал вам некоторый контекст, давайте посмотрим, что это означает на практике.
Проверка формы
Проверка формы — возможно, самая лучшая лошадь, которую вы когда-либо могли победить с помощью флешки JavaScript. Это один из старейших сценариев использования JavaScript, который решался библиотеками с открытым исходным кодом раз в миллион раз, не говоря уже о введении таких атрибутов HTML5, как required
и pattern
. Тем не менее, новые библиотеки все еще всплывают, указывая, что:
- Мы не создаем правильные абстракции, что приводит к постоянной необходимости переписывать.
- Разработчикам JavaScript действительно нравится изобретать велосипед (и публиковать результат в виде программного обеспечения с открытым исходным кодом).
Я не могу помочь со вторым, но я надеюсь пролить свет на первое, даже если я сам внес свой вклад в беспорядок, который уже существует.
Проверка формы «близка» к DOM во многих отношениях. Мы тестируем набор ожиданий относительно текущего состояния form
, а затем сообщаем пользователю, внося изменения в DOM. Однако, если мы сделаем шаг назад, мы можем легко представить некоторые соответствующие варианты использования, которые в меньшей степени включают DOM:
- Отправка отчетов о проверке в систему аналитики, чтобы понять, как улучшить дизайн сайта.
- Проверка данных, полученных по сети
- Проверка данных из файлов, перетаскиваемых в браузер
- Вывод проверочных сообщений с использованием таких библиотек, как React
Даже если DOM активно задействован, существует множество факторов, которые различаются:
- Когда проверка запускается? Когда
onsubmit
событиеonsubmit
?onblur
?onchange
? Программно через код JavaScript? - Сообщение об ошибке по всей форме или по полю? И то и другое?
- Сведения о разметке отчетов об ошибках могут сильно различаться
- Отчеты об ошибках могут отличаться в зависимости от контекста
Тесное связывание цикла ввода-проверки-вывода затруднит учет всех мыслимых комбинаций этих вещей. Если вы планируете действительно хорошо, вы можете сделать довольно гибкое решение, но я гарантирую вам, что кто-то появится с вариантом использования, который сломает спину верблюда. Поверьте мне, я уже прошел эту дорогу и попал в каждую канаву по пути.
Как будто этого было недостаточно, учтите тот факт, что многие виды правил проверки зависят от более чем одного поля. Как мы решаем эти ситуации? Ответ можно найти, сначала проанализировав, что нам нужно сделать, а затем решив, как лучше всего это сделать:
- Чтение данных из формы (DOM-ориентированных)
- Проверка данных по набору правил (чистая бизнес-логика)
- Вывод результатов проверки (возможно, DOM-ориентированных)
Кроме того, нам понадобится тонкий слой кода, который объединяет фрагменты и запускает проверку в нужное время. Может быть, есть и другие аспекты, которые необходимо учитывать, но пока мы можем реализовать их как ортогональные проблемы, мы должны иметь возможность относительно легко наложить на эту абстракцию.
Проверка данных
Ядром любой библиотеки валидации является ее набор функций валидации. Эти функции должны быть применимы к любым данным, а не только к элементам формы. В конце концов, единственное, что отличает принудительное использование поля name
в форме от обязательного обеспечения наличия свойства имени объекта, — это то, как мы получаем доступ к значению. Сама логика проверки идентична. По этой причине было бы разумно спроектировать функции валидатора для работы с чистыми данными, а затем предоставить различные механизмы для извлечения значений для прохождения через валидатор отдельно. Это также означает, что наши модульные тесты могут использовать простые объекты JavaScript, что приятно и легко сделать.
Какой вклад должны ожидать наши валидаторы? Нам нужно будет указать правила для отдельных полей (а также составные правила, подробнее об этом позже), и будет очень полезно связывать контекстные сообщения об ошибках с каждой проверкой. Так что-то вроде:
var checkName = required("name", "Please enter your name");
required
функция возвращает функцию, которая проверяет все данные и ищет name
. Это можно назвать так:
var result = checkName({name: 'Chris'});
Если данные, предоставленные функции, проходят проверку, она возвращает undefined
. Если это не удается, функция возвращает объект, описывающий проблему:
// returns {id: "name", msg: "Please enter your name"} checkName({});
Эти данные можно использовать «на другом конце», например, для вывода сообщений на форму.
Чтобы реализовать эту функцию, давайте сформулируем тест:
describe('required', function () { it('does not allow required fields to be blank', function () { var rule = required('name', 'Name cannot be blank'); assert.equals(rule({}), { id: 'name', msg: 'Name cannot be blank' }); }); });
Функция проверяет непустое значение:
function required(id, msg) { return function (data) { if (data[id] === null || data[id] === undefined || data[id] === '' ) { return {id: id, msg: msg}; } }; }
Хотя вызов отдельных функций проверки аккуратен, наш основной вариант использования заключается в проверке полной формы. Для этого мы будем использовать другую функцию, которая примет набор правил (созданных различными функциями валидатора) и сопоставит их с набором данных. Результатом будет массив ошибок. Если массив пуст, проверка прошла успешно. Итак, у нас может быть что-то вроде этого:
var rules = [ required("name", "Please enter your name"), required("email", "Please enter your email") ]; var data = {name: "Christian"}; // [{id: "email", messages: ["Please enter your email"]}] var errors = enforceRules(rules, data);
Обратите внимание, что результирующее свойство messages
является массивом, так как enforceRules
может встретиться с несколькими правилами, не выполненными для одного свойства. Поэтому мы должны учитывать несколько сообщений об ошибках для каждого имени свойства.
Это выглядит как разумный дизайн: он прост, не имеет внешних зависимостей и не делает никаких предположений о том, откуда поступают данные или куда идет результат. Давайте попробуем реализацию. Начнем с теста:
describe('required', function () { it('does not allow required fields to be blank', function () { var rules = [required('name', 'Name cannot be blank')]; assert.equals(enforceRules(rules, {}), [ {id: 'name', messages: ['Name cannot be blank']} ]); }); });
Этот тест хорошо описывает дизайн, который мы запланировали. Есть массив правил, объект с данными и массив ошибок в результате. Функция не имеет побочных эффектов. Это тот тип дизайна, который может выдержать меняющиеся требования.
После еще нескольких тестов у вас может получиться реализация enforceRules
которая выглядит следующим образом:
function enforceRules(rules, data) { var tmp = {}; function addError(errors, error) { if (!tmp[error.id]) { tmp[error.id] = {id: error.id}; tmp[error.id].messages = []; errors.push(tmp[error.id]); } tmp[error.id].messages.push(error.msg); } return rules.reduce(function (errors, rule) { var error = rule(data); if (error) { addError(errors, error); } return errors; }, []); }
На данный момент у нас есть система, в которой реализация новых валидаторов довольно проста. Например, тесты регулярных выражений довольно распространены в валидаторах форм, и один из них может быть реализован так:
function pattern(id, re, msg) { return function (data) { if (data[id] && !re.test(data[id])) { return {id: id, msg: msg}; } }; }
Важно отметить, что этот валидатор предназначен для прохождения, если рассматриваемые данные пусты / не существуют. Если в этом случае мы потерпим неудачу, валидатор также будет неявной проверкой. Поскольку у нас это уже есть в автономной версии, лучше позволить пользователю API комбинировать их в соответствии со своими потребностями.
Если вы хотите увидеть код, созданный до сих пор, в действии и поиграть с ним, взгляните на этот код .
Вывод
В этой первой части мы обсудили проблему, общую для многих библиотек проверки форм: тесно связанный код. Затем я описал недостатки, которые связаны с тесно связанным кодом, а также показал, как создавать функции проверки, которые не демонстрируют эту проблему.
В следующей части я познакомлю вас с составными валидаторами и другими ортогональными проблемами: сбором данных из HTML-форм и сообщением об ошибках пользователю. Наконец, я соберу все это вместе, чтобы получить полный визуальный пример, с которым вы можете играть.