Pull to refresh

В поисках идеального фреймворка для JavaScript

Reading time 11 min
Views 20K
Original author: Krasimir Tsonev
В наше время для разработки фронтенда существует много фреймворков и библиотек. Есть хорошие, есть не очень. Часто нам нравится только какая-то концепция, модуль или синтакс. Универсальных инструментов не существует. В статье я описываю фреймворк будущего – такой, которого ещё нет. Я собрал достоинства и недостатки известных фреймворков и мечтаю об идеальном решении.

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


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

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

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

Было бы лучше, если б мы работали только с узлами
. Тогда мы будем менять не весь список, а только его потомков. Первое, что нужно поменять – это setElement:

setElement: function(el) { this.list = document.createElement('ul'); el.appendChild(this.list); return this; }


Теперь мы не будем ссылаться на внешний элемент. Нужно лишь создать
 и один раз его добавить.

Логика, улучшающая быстродействие, находится в методе 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 проходит данные и создаёт элементы
. 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 type="text/x-handlebars">
	Hello, <strong> </strong>!
</script>


Шаблон сидит в HTML, выглядит естественно, браузер не рендерит содержимое тега в
.

Шаблон грузится через Ajax

Backbone.View.extend({ 'template': 'my-view-template', 'render': function() { $.get('/templates/' + this.template + '.html', function(template) { var html = $(template).tmpl(); }); } });


Код размещён во внешних HTML-файлах и не надо пользоваться лишними тегами
. Но при этом совершаются лишние HTTP-запросы, что иногда не очень хорошо.

Шаблон включён в страницу – фреймворк читает его из DOM. HTML уже сгенерился, нам не надо делать лишних HTTP-запросов, создавать файлы или использовать теги .

Шаблон является частью 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.
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+7
Comments 12
Comments Comments 12

Articles