В наше время для разработки фронтенда существует много фреймворков и библиотек. Есть хорошие, есть не очень. Часто нам нравится только какая-то концепция, модуль или синтакс. Универсальных инструментов не существует. В статье я описываю фреймворк будущего – такой, которого ещё нет. Я собрал достоинства и недостатки известных фреймворков и мечтаю об идеальном решении.

Абстракция опасна


Всем нравится простота. Сложность убивает. Она усложняет работу и приводит к крутой кривой обучения. Программистом нужно понимать, что как работает – иначе они чувствуют себя неуверенно. При работе со сложной системой есть большое расстояние между «я её использую» и «я знаю, как это работает». К примеру, следующий код прячет сложность:

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', 'крутой', 'язык']);


Результат следующий:

image

Покажем, почему это плохо. Добавим ссылку на страницу и навесим отслеживание событий. Функция вызовет update уже с другими параметрами:

document.querySelector('a').addEventListener('click', function() {
	Framework.update(['Веб', 'крутая', 'штука']);
});


Мы отправляем очень похожие данные и меняем только первый элемент массива. Но из-за использования innerHTML каждый раз вызывается перерисовка всего списка. Давайте посмотрим на это через Opera’s DevTools.

image

После каждого клика перерисовывается всё содержимое. Это проблема.

Было бы лучше, если б мы работали только с узлами <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. Цикл в конце удаляет узлы, если в полученном массиве элементов меньше, чем в текущем.

Результат:

image

Браузер перерисовывает только изменившуюся часть.

Фреймворки типа 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)


  1. f0rmat1k
    01.06.2015 18:41
    +1

    Фреймворки типа React

    React — это не фреймворк. Впрочем, это не претензия к переводу.


    1. alist
      03.06.2015 00:00
      -1

      Мне кажется, что нет смысла сравнивать чисто фреймворки/библиотеки. Возьмем Angular и Ember. Ребята из Эмбера сразу начнут нахваливать свой крутой раутер. А мы добавим в проект на Angular их UI-router, и разница с Эмбером будет уже меньше (Эмбер-раутер все равно круче, но не намного).

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


      1. f0rmat1k
        03.06.2015 00:56
        +1

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


      1. sferrka
        03.06.2015 01:24

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


    1. Finom
      08.06.2015 09:50

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


  1. gwer
    01.06.2015 20:05
    +7

    Я вас, возможно, немного расстрою, но на странице оригинала большими буквами написано «Available in English, Russian» со ссылочкой на перевод и на гитхаб.


    1. pepelsbey
      01.06.2015 21:21
      +1

      В общем, да: спустя 2 недели после публикации статьи, там же появился хороший перевод.


    1. Jabher
      01.06.2015 22:39

      то-то у меня дежавю…


  1. cmdr
    01.06.2015 22:12
    -5

    бросил читать после фразы

    Программистом нужно понимать, что как работает – иначе они чувствуют себя неуверенно.

    p.s.
    (на самом деле нет, дочитаю :) )


  1. TelnovOleg
    02.06.2015 05:40
    +1

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


    1. poxu
      02.06.2015 09:57
      +2

      Вот зачем изобретать какое-то API, если потом нужно изобрести еще 100500 фреймворков, что бы можно стало нормально работать?

      Это состояние дел в индустрии. Сначала делаем, потом смотрим, как получилось, потом корректируем. 100500 фреймворков получается из-за того, что у всех своё мнение, как должен выглядеть идеал, плюс программисты таки решают разные задачи.


    1. KIVagant
      11.06.2015 01:27

      Так и работает коллективный разум. До появления Энштейнов в рамках конкретной науки, разрывающий замкнутый круг и расширяющих горизоны.