Абстракция опасна
Всем нравится простота. Сложность убивает. Она усложняет работу и приводит к крутой кривой обучения. Программистом нужно понимать, что как работает – иначе они чувствуют себя неуверенно. При работе со сложной системой есть большое расстояние между «я её использую» и «я знаю, как это работает». К примеру, следующий код прячет сложность:
var page = Framework.createPage({
'type': 'home',
'visible': true
});
Допустим, это реальный фреймворк. createPage где-то создаёт новый класс Вида, загружающий html-шаблон home. Основываясь на параметре visible мы добавляем созданный DOM-элемент к дереву. С точки зрения разработчика мы не знаем, как это всё работает в деталях, потому, что это – абстракция.
У некоторых фреймворков есть не один, а много уровней абстракции. Иногда нам нужно знать детали его работы. Абстракция – инструмент мощный, поскольку она делает обёртки для функциональностей, инкапсулирует решения по поводу дизайна. Но её надо использовать с умом, потому что она приводит к процессам, которые трудно отслеживать.
Если мы поменяем пример на следующий:
var page = Framework.createPage();
page
.loadTemplate('home.html')
.appendToDOM();
Теперь становится ясно, что происходит. Загрузка шаблона и присоединение указаны в качестве методов API. То есть, мы можем разобраться в процессе и контролировать его.
Возьмём Ember.js. Фреймворк прекрасный. В несколько строк мы можем создать одностраничное приложение. Но этому есть цена. Он определяет классы «за кулисами». К примеру:
App.Router.map(function() {
this.resource('posts', function() {
this.route('new');
});
});
Фреймворк создаёт три пути, к каждому из которых присоединён контроллер. Их можно использовать или не использовать, но они есть. Они нужны фреймворку для работы.
Часто в проекте требуется собственная функциональность. Нет фреймворка, рассчитанного на все случаи. И мы встречаемся с задачами, не имеющими простых решений. Нам приходится разбираться, как всё работает, чтобы найти правильный способ решения задач. И часто то, что нам надо сделать, больше похоже на хакинг фреймворка.
К примеру, Backbone.js имеет несколько предварительно заданных объектов. У них есть основная функциональность, а её реализация ложится на программиста. Класс DocumentView расширяет Backbone.View. И всё. У нас есть только один уровень между нашим кодом и базовыми функциями фреймворка.
var DocumentView = Backbone.View.extend({
'tagName': 'li',
'events': {
'mouseover .title .date': 'showTooltip',
'click .open': 'render'
},
'render': function() { … },
'showTooltip': function() { … }
});
Мне больше нравится фреймворк, у которого нет множества уровней абстракции – такой, который получается прозрачным.
Исчезнувший конструктор
Некоторые фреймворки принимают от нас определения классов, но не создают конструкторов. Фреймворк сам решает, где и когда создать экземпляр объекта. Я бы хотел увидеть фреймворк, который бы позволял нам самим это делать. К примеру, в Knockout:
function ViewModel(first, last) {
this.firstName = ko.observable(first);
this.lastName = ko.observable(last);
}
ko.applyBindings(new ViewModel("Планета", "Земля"))
Мы определяем модель и инициализируем её. В AngularJS всё немного по-другому:
function TodoCtrl($scope) {
$scope.todos = [
{ 'text': 'Учи angular', 'done': true },
{ 'text': 'Делай приложение на angular', 'done': false }
];
}
Мы определяем класс, но не запускаем его. Мы просто говорим, что это – наш контроллер а фреймворк решает, как с ним работать. Это может сбивать с толку, т.к. мы теряем ключевые точки в коде, которые позволяют ориентироваться в работе приложения.
Манипуляции DOM
Нам в любом случае необходимо взаимодействовать с DOM. И нам надо точно знать, как это происходит – обычно, каждое действие с узлами страницы приводит к её перерисовке, что может быть довольно затратным. К примеру, рассмотрим следующий класс:
var Framework = {
'el': null,
'setElement': function(el) {
this.el = el;
return this;
},
'update': function(list) {
var str = '<ul>';
for (var i = 0; i < list.length; i++) {
var li = document.createElement('li');
li.textContent = list[i];
str += li.outerHTML;
}
str += '</ul>';
this.el.innerHTML = str;
return this;
}
}
Этот фреймворк создаёт ненумерованный список из заданных данных. Мы отправляем элемент DOM, в котором будет содержаться список, и вызываем update, которая показывает данные на экране.
Framework
.setElement(document.querySelector('.content'))
.update(['JavaScript', 'крутой', 'язык']);
Результат следующий:
Покажем, почему это плохо. Добавим ссылку на страницу и навесим отслеживание событий. Функция вызовет update уже с другими параметрами:
document.querySelector('a').addEventListener('click', function() {
Framework.update(['Веб', 'крутая', 'штука']);
});
Мы отправляем очень похожие данные и меняем только первый элемент массива. Но из-за использования innerHTML каждый раз вызывается перерисовка всего списка. Давайте посмотрим на это через Opera’s DevTools.
После каждого клика перерисовывается всё содержимое. Это проблема.
Было бы лучше, если б мы работали только с узлами
<li>
. Тогда мы будем менять не весь список, а только его потомков. Первое, что нужно поменять – это setElement:setElement: function(el) {
this.list = document.createElement('ul');
el.appendChild(this.list);
return this;
}
Теперь мы не будем ссылаться на внешний элемент. Нужно лишь создать
<ul>
и один раз его добавить.Логика, улучшающая быстродействие, находится в методе update:
'update': function(list) {
for (var i = 0; i < list.length; i++) {
if (!this.rows[i]) {
var row = document.createElement('LI');
row.textContent = list[i];
this.rows[i] = row;
this.list.appendChild(row);
} else if (this.rows[i].textContent !== list[i]) {
this.rows[i].textContent = list[i];
}
}
if (list.length < this.rows.length) {
for (var i = list.length; i < this.rows.length; i++) {
if (this.rows[i] !== false) {
this.list.removeChild(this.rows[i]);
this.rows[i] = false;
}
}
}
return this;
}
Первый цикл for проходит данные и создаёт элементы
<li>
. this.rows содержит созданные элементы. Если по определённому индексу есть узел, фреймворк обновляет его свойство textContent. Цикл в конце удаляет узлы, если в полученном массиве элементов меньше, чем в текущем.Результат:
Браузер перерисовывает только изменившуюся часть.
Фреймворки типа React корректно работают с манипуляциями DOM. Браузеры становятся умнее и стараются уменьшить количество перерисовок. Но всегда неплохо иметь это в виду и проверять работу фреймворка.
Надеюсь, в будущем нам не придётся думать о таких вещах.
Обработка событий DOM
Приложения JavaScript общаются с пользователями через события DOM. Элементы страницы отправляют сообщения, а код их обрабатывает. Вот кусок кода Backbone.js, обрабатывающего взаимодействие пользователя со страницей:
var Navigation = Backbone.View.extend({
'events': {
'click .header.menu': 'toggleMenu'
},
'toggleMenu': function() {
// …
}
});
Должен быть элемент, соответствующий селектору .header.menu, и когда пользователь по нему кликает, мы переключаем меню. Проблема в том, что мы привязываем объект к определённому элементу DOM. Если мы поменяем код и переименуем .menu. в .main-menu, нам придётся поменять JS-код. Я считаю, что контроллеры должны быть независимыми, и их надо отвязать от DOM.
Определяя функции, мы передаём таски классам JS. Если эти таски – хэндлеры событий DOM, то имеет смысл включить их в HTML.
Мне нравится обработка событий в AngularJS:
<a href="#" ng-click="go()">жмакай меня</a>
go — функция, зарегистрированная в контроллере. И тогда нам не надо думать про селекторы DOM. Мы просто назначаем поведение узлам HTML. И пропускаем скучный этап взаимодействия с DOM.
Хотелось бы видеть такую логику внутри HTML. Мы годами приучали разработчиков к разделению содержимого (HMTL) и поведения (JS). А теперь я вижу, что их объединение могло бы сэкономить нам массу времени и добавить гибкости. Но я не имею в виду код вроде:
<div onclick="javascript:App.doSomething(this);">и тут текст</div>
Я имею в виду описательные атрибуты, управляющие поведением элемента. К примеру:
<div data-component="slideshow" data-items="5" data-select="dispatch:selected">
…
</div>
Это должно быть похоже не на включение кода в HTML, а на указание настроек.
Управление зависимостями
При разработке очень важно правильно управлять зависимости. Мы обычно полагаемся на внешние библиотеки и функции. И постоянно сами создаём зависимости – мы ведь не пишем всё в один метод. Мы разбиваем приложение на функции и связываем их. В идеале нам надо инкапсулировать логику в модули, которые работают как «чёрные ящики». Они знают только то, что им нужно для их работы.
RequireJS – популярный инструмент для работы с зависимостями. Идея в том, чтобы обернуть код в замыкание, принимающее необходимые нам модули:
require(['ajax', 'router'], function(ajax, router) {
// …
});
В примере нашей функции нужны модули ajax и router. Волшебный метод require обрабатывает массив и вызывает функцию с нужными аргументами. Определение router выглядит так:
// router.js
define(['jquery'], function($) {
return {
'apiMethod': function() {
// …
}
}
});
Тут у нас есть ещё одна зависимость – jQuery. Важно упомянуть, что нам необходимо вернуть публичное API нашего модуля – иначе код, который включает наш модуль, не сможет получить доступ к нужной функциональности.
AngularJS идёт ещё дальше и предоставляет нечто под названием factory (фабрика). Мы регистрируем там зависимости, и они волшебным образом становятся доступны в контроллерах:
myModule.factory('greeter', function($window) {
return {
'greet': function(text) {
alert(text);
}
};
});
function MyController($scope, greeter) {
$scope.sayHello = function() {
greeter.greet('Всем привет!');
};
}
Обычно это упрощает работу – нам не надо использовать функцию require для доступа к зависимостям. Надо только вписать нужные слова в список аргументов.
Но эти техники завязаны на особый стиль кода. В будущем я хотел бы увидеть фреймворк, устраняющий это ограничение. Было бы проще добавлять метаданные при определении переменных. В языке пока нет таких возможностей, но было бы круто сделать нечто вроде:
var router:<inject:Router>;
Это бы означало, что мы сделаем инъекцию только по необходимости. RequireJS и AngularJS работают с функциями, и вы можете использовать модуль достаточно редко, но инициализация будет проходить каждый раз, и зависимости необходимо определять в жёстко заданных местах.
Шаблоны
Шаблоны используются для разделения данных и разметки HTML. Как это делается на сегодняшний день? Вот самые популярные подходы.
Шаблон определяется в <script>
<script type="text/x-handlebars">
Hello, <strong> </strong>!
</script>
Шаблон сидит в HTML, выглядит естественно, браузер не рендерит содержимое тега в
<script>
.Шаблон грузится через Ajax
Backbone.View.extend({
'template': 'my-view-template',
'render': function() {
$.get('/templates/' + this.template + '.html', function(template) {
var html = $(template).tmpl();
});
}
});
Код размещён во внешних HTML-файлах и не надо пользоваться лишними тегами
<script>
. Но при этом совершаются лишние HTTP-запросы, что иногда не очень хорошо.Шаблон включён в страницу – фреймворк читает его из DOM. HTML уже сгенерился, нам не надо делать лишних HTTP-запросов, создавать файлы или использовать теги
<script>
.Шаблон является частью JavaScript
var HelloMessage = React.createClass({
render: function() {
// следующая строчка не будет допустимой в языке JS:
return <div>Здоровелло, {this.props.name}</div>;
}
});
Такой подход используется в React, и там используется встроенный парсер, который преобразовывает недопустимые с точки зрения синтаксиса строки в нормальный код.
Шаблоны не-HTML
Некоторые фреймворки вообще не используют HTML. Они используют JSON или YAML.
Итоги по шаблонам
Что мы можем улучшить? Фреймворк будущего должен настроить нас только на мысли о данных и о разметке. Ничего в промежутке. Не надо нам возиться с загрузкой HTML-строк или передавать данные в специальные функции. Нам надо назначать переменным их значения и обновлять DOM. Двусторонняя связь данных должна быть не возможностью, а необходимой основной функцией.
AngularJS находится близко к идеалу. Он читает шаблон из содержимого страницы и волшебным образом связывает данные. Но он не идеален. Иногда изображение мигает – когда браузер рендерит HTML, а загрузочные механизмы AngularJS ещё не подгрузились. Надеюсь, что Object.observe скоро будет поддерживаться всеми браузерами, и тогда мы получим более удобное связывание данных.
Модульность
Мне нравится возможность включать и выключать функциональность. Если мы что-то не используем, его не должно быть в коде. Хорошо бы иметь в фреймворке билдер, который бы создавал версию кода, содержащего только те модули, которые мы используем. Как, к примеру, YUI, у которого есть конфигуратор. Мы выбираем нужные модули и получаем минифицированный JS-файл, готовый к использованию.
Сейчас у некоторых фреймворков есть некое «ядро». Также мы можем использовать разные плагины или модули. Но это можно улучшить – сделать так, чтобы не нужно было скачивать файлы или включать их в код страницы вручную. Это всё должно происходить внутри фреймворка.
После необходимых настроек, среда разработки должна быть расширяемой. Мы должны уметь писать свои модули и делиться ими с другими. То есть, должна быть удобная среда для создания модулей. Нельзя создать хорошее сообщество, не имея хорошей среды разработки.
Публичный API
API текущих фреймворков даёт нам доступ только к тем их частям, к которым почитали нужным дать доступ разработчики. В иных случаях нам приходится заниматься хакингом. Нам что-то нужно, но у нас нет нужных инструментов, и мы используем всякие обходные пути и подпорки:
var Framework = function() {
var router = new Router();
var factory = new ControllerFactory();
return {
'addRoute': function(path) {
var rData = router.resolve(path);
var controller = factory.get(rData.controllerType);
router.register(path, controller.handler);
return controller;
}
}
};
var AboutCtrl = Framework.addRoute('/about');
У фреймворка есть встроенный роутер. Мы определили путь, и наш контроллер проинициализирован. Когда пользователь идёт по нужному URK, роутер запускает хэндлер контроллера. Хорошо, но что, если нам надо выполнить простую функцию в ответ на совпадение URL? И нам неохота создавать новый контроллер? Это невозможно с текущим API.
Можно было бы использовать другой дизайн:
var Framework = function() {
var router = new Router();
var factory = new ControllerFactory();
return {
'createController': function(path) {
var rData = router.resolve(path);
return factory.get(rData.controllerType);
}
'addRoute': function(path, handler) {
router.register(path, handler);
}
}
}
var AboutCtrl = Framework.createController({ 'type': 'about' });
Framework.addRoute('/about', AboutCtrl.handler);
Мы не выставляем роутер напоказ, он не виден – но мы контролируем два процесса, создание контроллера и регистрацию пути. Конечно, этот дизайн подходит к частному случаю. Может быть, он покажется более сложным из-за необходимости создавать контроллеры вручную. При разработке API приходится думать о принципе единственной обязанности, и о том, что нам надо сделать одну вещь и сделать её правильно. Насколько я вижу, фреймворки децентрализовывают свою функциональность, разделяют сложные методы на мелкие куски. Это хорошо, и надеюсь, что этот процесс продолжится.
Тестируемость
Нужно не только писать тесты кода, но и писать код, который можно протестировать. Иногда это отнимает много времени. Уверен, что если мы для чего-нибудь не напишем тест, то именно в этом месте и получим ошибку. Особенно это касается клиентской части кода. Разные браузеры, операционки, и т.д. – слишком много причин для использования TDD.
При таком подходе мы не только обеспечиваем сиюминутную работоспособность приложения, мы заботимся о том, чтобы оно работало завтра и послезавтра. Если мы вводим новую функцию, мы делаем для неё тест. И новый тест, и все старые тесты должны успешно проходить – и тогда мы получим гарантию, что мы ничего не сломали.
Хотелось бы видеть более стандартизированные инструменты и методы для тестов. Хотелось бы иметь один инструмент для тестирования всех фреймворков. Необходимо обратить особое внимание на сервисы вроде Travis. Они удобны не только для программистов, но и для других людей, участвующих в проекте.
Я ещё работаю c PHP, мне необходимо работать с фреймворками типа WordPress. Меня часто спрашивают – как я тестирую свои приложения? А никак – нет у меня такой возможности. Не могу я делать юнит-тесты в отсутствие юнитов. И у некоторых JS-фреймворков та же проблема – нет юнитов. Разработчики не только должны выдавать нам умный, элегантный и работающий код, он ещё должен быть и тестируемый.
Документация
Без хорошей документации проект умирает. Документация – это первое, что видит разработчик. Никто не хочет тратить часы на поиски подробностей работы функции. Недостаточно перечислить только основную функциональность – особенно у большого фреймворка.
Хорошую документацию я бы разделил на три части:
- что можно сделать. Обучающая часть. Неважно, какой фреймворк крутой и навороченный – для него должна быть хорошая объясняющая документация. Некоторые любят смотреть видео, некоторые – читать статьи. Разработчик должен провести своих пользователей от простых вещей к сложным
- документация по API. Обычно мы видим только это. Список всех методов, параметров, возвратов и примеры.
- как это работает. Обычно такого нигде нет. Принцип работы фреймворка, схема, диаграмма, связи между частями. Это сделало бы код прозрачным и помогло тем, кто пытается вносить изменения в работу.
Итог
Будущее предсказывать трудно, но о нём можно мечтать. Важно обсуждать то, что мы хотим увидеть и что нам нужно от фреймворков на JavaScript.
Комментарии (12)
gwer
01.06.2015 20:05+7Я вас, возможно, немного расстрою, но на странице оригинала большими буквами написано «Available in English, Russian» со ссылочкой на перевод и на гитхаб.
pepelsbey
01.06.2015 21:21+1В общем, да: спустя 2 недели после публикации статьи, там же появился хороший перевод.
cmdr
01.06.2015 22:12-5бросил читать после фразы
Программистом нужно понимать, что как работает – иначе они чувствуют себя неуверенно.
p.s.(на самом деле нет, дочитаю :) )TelnovOleg
02.06.2015 05:40+1Никак не могу отделаться от ощущения, что что-то в этом мире все время идет не так. Вот зачем изобретать какое-то API, если потом нужно изобрести еще 100500 фреймворков, что бы можно стало нормально работать? Почему бы сразу не делать фреймворк элементом архитектуры, как была спроектирована например BeOS? Понятно что кому-то покажется что фреймворк неправильный и надо делать по другому, но вдруг таких будет меньшинство?
Или может быть правы считающие излишним переусложнение программ в попытках сделать их компоненты сверхуниверсальными на все случаи жизни и надо решать только конкретную проблему стоящую перед задачей, на что вполне хватает и API?poxu
02.06.2015 09:57+2Вот зачем изобретать какое-то API, если потом нужно изобрести еще 100500 фреймворков, что бы можно стало нормально работать?
Это состояние дел в индустрии. Сначала делаем, потом смотрим, как получилось, потом корректируем. 100500 фреймворков получается из-за того, что у всех своё мнение, как должен выглядеть идеал, плюс программисты таки решают разные задачи.
KIVagant
11.06.2015 01:27Так и работает коллективный разум. До появления Энштейнов в рамках конкретной науки, разрывающий замкнутый круг и расширяющих горизоны.
f0rmat1k
React — это не фреймворк. Впрочем, это не претензия к переводу.
alist
Мне кажется, что нет смысла сравнивать чисто фреймворки/библиотеки. Возьмем Angular и Ember. Ребята из Эмбера сразу начнут нахваливать свой крутой раутер. А мы добавим в проект на Angular их UI-router, и разница с Эмбером будет уже меньше (Эмбер-раутер все равно круче, но не намного).
Но чтобы вот так стеки сравнивать, нужно быть очень осведомленным обо всем, что вокруг того или иного фреймворка иди библиотеки делается — это гораздо сложнее, чем пойти на официальные сайты и сравнить по документации и примерам.
f0rmat1k
И эмбер и ангуляр — фреймворки. Библиотеки вам не навязывают какой-то mv-паттерн для вашего сайта/приложения. Это ключевая разница. Можно использовать фреймворк и библиотеку вместе, но не два фреймворка.
sferrka
Мне кажется, в теории, критерии можно выделить. Количество сущностей на один и тот же функционал, количество кода, количество сложных конструкций и т.д. Другое дело, что никто этого не делает, только поверхностные «интуитивные» обзоры.
Finom
Не впервые вижу такое замечание и не могу согласиться. Реакт может быть использован, как DOM шаблонизатор, но, в то же время, он может работать и как фреймворк, навязывая собственную структуру кода. jQuery, например — это набор функций, не навязывающий ничего, поэтому его называют баблиотекой.