В начале весны ABBYY LS совместно с Xerox запустили сервис для перевода документов Xerox Easy Translator Service. Изюминкой этого сервиса является приложение, запускаемое на МФУ Xerox и позволяющее отсканировать необходимое количество документов, дождаться перевода на один из выбранных 38 языков, произвести печать перевода — и все это не отходя от МФУ.
Приложение запускается на определенной серии МФУ Xerox на основе технологии Xerox ConnectKey с сенсорным экраном 800x480 точек. Аппаратная начинка МФУ зависит от конкретной модели, например, наша тестовая малютка Xerox WorkCentre 3655 имеет на борту 1Ghz Dual Core процессор и 2Gb оперативной памяти. Как ни удивительно, но МФУ имеет встроенный webkit-браузер, а наше приложение — это обычное html-приложение, разработанное на AngularJS 1.3.15.
О самом проекте мы писали в блоге раньше, а эта статья посвящена одному из увлекательных этапов проекта, а именно оптимизации AngularJS под работу на МФУ Xerox. Как оказалось на деле, платформа МФУ практически не накладывает никаких серьезных ограничений на разработку приложений, и они работают практически так же, как и на десктопных webkit-браузерах, за исключением одного НО — html-приложению для исполнения JS выделятся всего 3.5 Mb оперативной памяти (на данный момент Xerox уже выпустили обновление для своей платформы, подняв порог выделяемой памяти до 10 Mb). AngularJS эти 3.5 Mb съедал за несколько минут работы в приложении, а сборщик мусора встроенного браузера МФУ не успевал за такой прожорливостью и просто выбивал наше приложение на главный экран МФУ. Вдобавок у Xerox нет средств для анализа и дебага приложений, запущенных на МФУ.
Сначала казалось, что сделать ничего не получится (тем более непонаслышке зная прожорливость современных браузеров), но грамотно оценив ситуацию, мы все же решились попробовать укротить AngularJS и заставить приложение потреблять минимально возможное количество памяти. Начав с 220kb скомпилированного (минимизированного, не gzip) JS кода приложения, мы закончили 97kb (AngularJS занимает 56kb, все остальное – наш код), по максимуму удалив весь незадействованный код, либо видоизменив его для наименьшего потребления памяти. Результат – стабильная работа приложения в течение нескольких десятков минут на платформе с 3.5 Mb памяти и полная неубиваемость на новой платформе с 10 Mb. Что же мы сделали?
Http-запросы
Основная проблема, с которой мы столкнулись сразу же, — это “тяжелые” http-запросы. Их “тяжесть” измеряется не в количествах или объемах передаваемых данных, а в создаваемом при каждом запросе новом объекте XmlHttpRequest под капотом $http сервиса AngularJS. Официальная информация в секции рекомендаций SDK Xerox указывала на то, что крайне желательно использовать в приложении всего один объект XmlHttpRequest и все запросы последовательно выполнять, используя только один объект.
Примеры из SDK носили весьма простой характер — буквально пару запросов на все приложение, что, в принципе, никак не усложняет использование одного объекта XmlHttpRequest в голом виде с применением нативных коллбеков этого объекта. В нашем же приложении организована весьма хитрая логика синхронизации заказов пользователя, oauth авторизация, запросы к soap-сервисам МФУ для запуска сканирования или печати. К тому же, запросы к МФУ выполнялись с использованием кода из SDK Xerox, который создавал свой объект XmlHttpRequest, тянул за собой методы для работы с xml-ответом soap-сервисов и в целом создавал дополнительную сложность при парсинге этого xml-ответа и приводил к ситуации написания не Angular-way кода.
Таким образом, мы столкнулись с действительно серьезными проблемами: отсутствие нормальных примеров реального использования одного объекта XmlHttpRequest, широкого спектра использования запросов и полу-legacy кодом из SDK. Несмотря на всю сложность, выход из ситуации оказался прост – написать свой $http сервис, отказаться от кода из Xerox SDK и написать свои Angular-сервисы для поддержки сканирования и печати.
Одной из главных трудностей было еще и то, чтобы наш кастомный сервис должен иметь такой же программный интерфейс, как и ангуларовский $http сервис, чтобы сохранить уже работающий и протестированный код наших контроллеров и зависимых от $http сервисов. Так как в приложении использовались только get и post запросы, в простой аннотации $http.get(...) и $http.post(...), то сам сервис выглядит вот так:
function ($q) {
var queue = [];
// execute request
function query() {
var request = queue[0];
var defer = request.defer;
xhr.open(request.method, request.url, true);
// set headers
var headers = request.headers;
for (var i in headers) {
xhr.setRequestHeader(i, headers[i]);
}
// load callback
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && !defer.promise.$$state.status) {
var status = xhr.status;
var data = JSON.parse(xhr.response);
(200 <= status && status < 300 ? defer.resolve : defer.reject)({
data: data,
status: status
});
queue.shift();
if (queue.length) {
query();
}
}
};
// send data
xhr.send(request.data);
}
// add request to queue
function push(method, url, data, headers) {
var defer = $q.defer();
queue.push({
data: typeof data === "string" ? data : JSON.stringify(data),
defer: defer,
headers: headers,
method: method,
url: url
});
if (queue.length == 1) query();
return defer.promise;
}
return {
// get request
get: function (url, data, headers) {
return push("GET", url, data, headers);
},
// post request
post: function (url, data, headers) {
return push("POST", url, data, headers);
}
};
}
Это минимальный вид нашего сервиса, который, используя один объект XmlHttpRequest, способен выполнять последовательно любое количество http-запросов без угрозы агрессивного потребления памяти МФУ. В конечном результате данный сервис содержит функциональность http interceptor’ов (без возможности внесения изменений в конечный ответ запроса, правильнее было бы назвать http listeners, используем для логирования ошибок), отмены очереди запросов $http.cancel(), плюс дополнительные свойства результирующего объекта, которые позволяют понять, что запрос был отменен пользователем или отвалился по таймауту (30 секунд на запрос), например:
$http.get(...).catch(function (response) { if (response.canceled) { ... } });
Следующий этап – обернуть вызовы soap-сервисов МФУ в соответствующие Angular-сервисы. Основная проблема здесь состоит в том, что ответ от МФУ мы получаем в виде громоздкой soap’овской xml, а реально необходимые данные занимают всего несколько байт. Чтобы упростить этот этап, из исходной xml (которая нам пришла в виде строки) мы с помощью регулярного выражения “вынимаем” только тот тег, который нам интересен:
var parser = new DOMParser();
function toXml (xml, tag) {
if (tag) {
var node = new RegExp('((<|<)[\\w:]*' + tag + '(>|>|\\s).*\/[\\w:]*' + tag + '(>|>))', 'g').exec(xml);
return node && node.length ? parse(node[1]) : null;
} else {
return parse(xml);
}
}
function parse(xml) {
return parser.parseFromString(xml
.replace(/amp;/g, '')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/<\w+:/g, '<')
.replace(/<\/\w+:/g, '<\/'), 'text/xml').documentElement;
}
В результате мы получаем DOM-дерево, забрать данные из которого уже не составляет труда. К тому же, по DOM-дереву можно искать интересующие нас теги, используя возможности querySelector. Изначально код из SDK Xerox всегда парсил xml ответ целиком, а поиск по DOM-дереву выполнялся путем кастомного обхода дерева до нахождения нужного элемента (что-то вроде куцего самописного XPath в JS). Действительно сложно ответить, какой из подходов лучше и меньше потребляет памяти и системных ресурсов, но лично мы почему-то больше доверяем нативным функциям браузера DomParser.parseFromString, querySelector (querySelectorAll) по работе с DOM деревом, чем ручной обход.
Итого:
Разработана своя функциональность для выполнения http-запросов и простого парсинга xml, в уменьшенном виде занимающие 2.3kb. Из приложения удален весь зависимый код SDK Xerox, в минифицированном виде занимавший 17kb.
Из AngularJS были удалены сервисы $http и $httpBackend.
Роутинг
Изначально в проекте использовался всем известный ui-router версии 0.2.13. Это действительно замечательное, разностороннее и уникальное в своем роде решение для AngularJS. Используя его, мы сделали вполне обычный роутинг приложения, для модальных окон использовались вложенные состояния.
Разумеется, есть менее функциональное и легковесное решение непосредственно от самих разработчиков AngularJS, которое изначально не подходило в своем чистом виде и требовало доработок для модальных окон. Но именно исходный код этого модуля был активно использован для разработки собственного решения. В процессе оптимизации приложения нами было обнаружено, что вся функциональность модуля ui-router нам не нужна, а именно – у нас не было необходимости в url-роутинге (приложение на МФУ открывается на весь экран и нет доступа к адресной строке), вложенных состояниях, resolve и пр. Все, что нам нужно от роутинга это:
1. Возможность простого конфигурирования состояний (экранов и модальных окон) приложения.
2. Сопутствующие директивы и сервисы для кэширования и навигации между экранами и (или) модальными окнами.
3. Корректная подстановка и удаление из DOM-дерева html-шаблонов посещаемых экранов, а также отображение модальных окон поверх исходного экрана (аналог вложенных состояний ui-router, но нам нужен всего один уровень вложенности).
Первый пункт реализуется весьма легко:
xerox.provider("$route", function () {
...
var base = "/";
var routes = {};
var start;
var self = this;
// add new route
function add(name, templateUrl, controller, modal) {
routes[name] = {
name: name,
modal: modal,
controller: controller,
templateUrl: base + templateUrl + ".html"
};
return self;
}
// set start state
self.start = function (name) {
start = name;
return self;
};
// add modal
self.modal = function (name, templateUrl, controller) {
return add(name, templateUrl, controller, true);
};
// add state
self.state = function (name, templateUrl, controller) {
return add(name, templateUrl, controller, false);
};
self.$get = [...];
});
На стадии конфигурирования:
xerox.config(["$routeProvider", function ($routeProvider) {
$routeProvider
// default state
.start("settings")
// modals
.modal("login", "login/login", "login")
.modal("logout", "login/logout", "logout")
.modal("processing", "new-order/processing", "processing")
// states
.state("settings", "new-order/settings", "settings")
.state("languages", "new-order/languages", "languages");
}]);
Второй пункт реализуется посредством сервисов:
$view
xerox.factory("$view", ["$http", "$locale", "$q", function ($http, $locale, $q) {
var views = {};
return {
// get view
get: function (url) {
var self = this;
if (views[url]) {
return $q.when(views[url]);
} else {
return $http.get(url).then(function (response) {
var template = response.data;
self.put(url, template);
return template;
});
}
},
// put view
put: function (url, text) {
views[url] = text;
}
};
}]);
и $route
return {
// route history
var history = [];
// $route interface
var $route = {
// current route
current: null,
// history back
back: function () {
if ($route.current.modal) {
$rootScope.$broadcast("$routeClose");
} else {
$route.go(history.pop() && history.pop());
}
},
// goto route
go: function (name, params) {
prepare(name, params);
}
};
// prepare and load route
function prepare(name, params) {
var route = routes[name];
$view.get(route.templateUrl).then(function (template) {
route.template = template;
commit(route, params);
});
}
// commit route
function commit(route, params) {
route.params = params || {};
if (!route.modal) {
history.push(route.name);
}
$route.current = route;
$rootScope.$broadcast("$routeChange");
}
// routing start
prepare(start);
return $route;
}];
А также директив xrx-back:
xerox.directive("xrxBack", ["$route", function ($route) {
return {
restrict: "A",
link: function (scope, element) {
element.on(xrxClick, $route.back);
}
};
}]);
xrx-sref:
xerox.directive("xrxSref", ["$route", function ($route) {
return {
restrict: "A",
link: function (scope, element, attr) {
element.on(xrxClick, function () {
$route.go(attr.xrxSref);
});
}
}
}]);
и scriptDirective (для кэширования text/ng-template):
xerox.directive("script", ["$view", function ($view) {
return {
restrict: "E",
terminal: true,
compile: function(element, attr) {
if (attr.type == "text/ng-template") {
$view.put(attr.id, element[0].text);
}
}
};
}]);
В сервисе $route мы организуем дополнительную логику для модальных окон, а именно: 1) не помещаем их в историю состояний и 2) при попытке вызвать $route.back при открытом модальном окне триггерим событие, что необходимо закрыть модальное окно. На событие подписана директива xrx-view, которая и реализует пункт 3:
xerox.directive("xrxView", ["$compile", "$controller", "$route", function ($compile, $controller, $route) {
return {
restrict: "A",
link: function (scope, element) {
var stateScope;
var modalScope;
var modalElement;
var targetElement;
// destroy scope
function $destroy(scope) { scope && scope.$destroy(); }
// on route change
scope.$on("$routeChange", function () {
var current = $route.current;
var newScope = scope.$new();
// prepare scopes and DOM element
$destroy(modalScope);
if (current.modal) {
modalScope = newScope;
// find or create modal container
modalElement = element.find(".modals");
if (!modalElement.length) {
modalElement = xrxElement("<div class=modals>");
element.append(modalElement);
}
targetElement = modalElement;
} else {
$destroy(modalScope);
$destroy(stateScope);
modalScope = null;
stateScope = newScope;
targetElement = element;
}
// append controller and inject { $scope, $routeParams }
if (current.controller) {
targetElement.data("$ngControllerController", $controller(current.controller, {
$routeParams: current.params,
$scope: newScope
}));
}
// append Template to DOM and compile
targetElement.html(current.template);
$compile(targetElement.contents())(newScope);
});
// on modal close
scope.$on("$routeClose", function () {
$destroy(modalScope);
modalScope = null;
modalElement.remove();
});
}
};
}]);
На этом всё. Роутинг является максимально легковесным, поддерживает работу как с реальными html-шаблонами, так и их аналогами из <script type=text/ng-template>...</script>, реализует необходимую нам логику модальных окон. Дополнительно имеет схожий с ui-router синтаксис для работы и конфигурирования состояний приложения.
Итого:
Из приложения был исключен ui-router размером 28kb и разработана своя функциональность в минимальном виде занимающий всего 1.8kb.
Из AngularJS были удалены следующие сервисы и директивы:
- ng-controller
- ng-include
- scriptDirective (помещает в $templateCache скрипты c типом text/ng-template)
- $anchorScroll
- $location
- $cacheFactory
- $templateCache
Локализация приложения
К тому моменту, когда мы начали проводить полномасштабную оптимизацию приложения, мы уже имели почти полностью локализованное приложение на шести языках – английском, немецком, французском, итальянском, испанском и португальском. Тексты языков хранились по типу ключ-значение в JSON, а подставлялись в приложении с помощью одностороннего биндинга {{::locale.HELLO_HABR}}. В загрузке локализации из JSON все довольно просто и оптимизировать больше нечего:
angular.element(document).ready(function () {
window.$locale(function () {
angular.bootstrap(document.body, ["xerox"]);
});
});
Внутри функции $locale идет определение языка интерфейса и подгружается наиболее подходящий язык из JSON с помощью глобального xhr.
Но вот стадию реалтаймовой локализации приложения можно и нужно оптимизировать, хотя она и использует односторонний биндинг, но все равно это дополнительная работа внутри digest цикла при каждом заходе на страницу. К тому же в локализации есть тексты с версткой, которые требуют применения ng-bind-html, а тот в свою очередь влечет и еще дополнительные проверки сервисом $sanitize. Решение далеко не из лучших, но ничего более удобного сделать, собственно, практически и нельзя было до того момента, пока не был разработан свой роутинг. С появлением собственного сервиса загрузки и кэширования html-шаблонов $view, несомненно, пришла идея использовать его для локализации приложения.
Что же нам для этого пришлось сделать? В принципе совсем немного:
1. Во всех html шаблонах места, требующие локализации, обернуть двойными квадратными скобками, а-ля, было {{::locale.HELLO_HABR}}, стало — [[HELLO_HABR]]
2. Так как такое сочетание квадратных скобок в приложении уникально, то мы можем с помощью регулярного выражения сделать обычный replace, минуя стадию готового DOM и в целом digest цикл, а если быть точнее – производить локализацию до того, как шаблон будет скомпилирован и вставлен в DOM:
2. xerox.factory("$view", ["$http", "$locale", "$q", function ($http, $locale, $q) {
var views = {};
// locale inject RegExp
var localeRegExp = /\[\[(\w+)\]\]/mg;
// template localization
function localization(template) {
var match;
while (match = localeRegExp.exec(template)) {
template = template.replace(match[0], $locale[match[1]]);
}
return template;
}
return {
...
// put view
put: function (url, text) {
views[url] = localization(text);
}
};
}]);
Таким образом, локализация срабатывает однократно в момент старта Angular-приложения и в памяти мы уже храним локализованные html-шаблоны.
Итого:
Локализация приложения вынесена из цикла digest на стадию загрузки приложения.
Из AngularJS были удалены следующие сервисы и директивы:
- ng-bind
- ng-bind-html
- ng-bind-template
- ng-pluralize
- $$sanitizeUri
- $sce
- $sceDelegate
ng-model
Директива ng-model (и остальные директивы для работы с html формами, связанные с ней,) – одна из жемчужин AngularJS, это невероятный инструмент, в который влюбляешься с первого знакомства. Но мало кто знает, что скрыто под капотом ng-model. Это в действительности весьма тяжеловесный код, отслеживающий события на элементе (cut, paste, change, keydown), синхронизирующий реальное значение модельки с отображаемым значением на экране, проверяющий при каждом изменении нашу модельку, предоставляющий интерфейс-контроллер для работы с моделькой в наших директивах.
На деле оказалось, что нам все эти возможности и не нужны. Например, не нужна валидация, так как даже форма авторизации, согласно всем гайдлайнам, выводит ошибку в модальном окне только после неудачного серверного запроса. Кастомные чекбоксы, селектбоксы и списки тоже не требуют проверки, а директивы, которые их реализуют, работают с моделькой в режиме read-write-watch. То есть директива checkbox выглядит как-то так:
xerox.directive("checkbox", function () {
return {
restrict: "E",
scope: {
xrxModel: "="
},
link: function (scope, element) {
var icon = xrxElement("<div class=checkbox-icon>");
element.prepend(icon);
icon.on(xrxClick, function () {
if (!element.attr(xrxDisabled)) {
scope.$apply(function () {
scope.xrxModel = !scope.xrxModel;
});
}
});
scope.$watch("xrxModel", function (value) {
element[value ? "addClass" : "removeClass"]("checked");
});
}
};
});
Единственное – у нас есть форма авторизации, на которой мы используем текстовые инпуты. Поэтому директива клавиатуры, как и ng-model, отслеживает события cut, change, paste, но в более облегченном виде, не запуская маховик валидации и остальных вкусняшек AngularJS при работе с модельками.
Пару слов о клавиатуре, раз ее затронули, вот ее реальный вид:
Вся верстка строится на стороне JS (директива не имеет html шаблона), из интересных оптимизаций – событие клика навешивается на общий контейнер, а не на каждую кнопочку. Тем самым достигается небольшая, но все же экономия на обработчиках событий, а следовательно, и занимаемой приложением памяти.
Итого:
Из AngularJS были удалены следующие директивы:
ng-model
ng-list
ng-change
pattern
required
minlength
maxlength
ng-value
ng-model-options
ng-options
ng-init
ng-form
input
form
select
Скролл
Немало масла в огонь подлили нам и скроллируемые списки:
Оптимизируя потребление памяти, мы отказались от ng-repeat (который создает свой scope для каждого элемента), написали свое легковесное решение и думали, что на этом все, но вот рендеринг списка из 38 языков изрядно притормаживал на МФУ. Дополнительно дело ухудшало еще и то, что МФУ не рисует в браузере системный скролл и приходится его рисовать собственными средствами. Мы перепробовали множество ухищрений, начиная от использования -webkit-scrollbar и заканчивая кастомным скроллингом через element.scrollTop или -webkit-transform: translate(x, y) с использованием overflow: hidden. Попытки понять принцип рендеринга браузера тоже не увенчались успехом. Либо тормозил сам скролл, либо перестроение списка (пользователь выбрал другой Source язык и нужно перестроить список Target языков, который в себе не содержит выбранный Source язык).
Уже практически потеряв надежду, в один из очередных экспериментов, мы заметили, что если в список вставить несколько элементов и менять только их innerHTML, рендеринг не тормозит, а скролл осуществляется плавно и без задержек. Этим нелегким путем в приложении появилась директива для скролла, принцип ее работы прост и хитер одновременно:
1. В контейнер вставляется необходимое количество элементов, чтобы заполнить всю его высоту, например, 7 элементов списка.
2. На основе значения offset'а (отступ от начала массива данных) и html шаблона меняются innerHTML наших элементов.
3. Отлавливаем события нажатия на стрелочки скролла или «таскание» (mouseDown-mouseMove-mouseUp) ползунка, высчитываем offset, меняем позицию ползунка и возвращаемся к пункту 2.
Таким образом, создается ощущение скролла данных, хотя на деле меняется только внутреннее содержимое всех тех же 7 элементов списка.
Итого:
Из AngularJS была удалена директива ng-repeat, так как в ней больше не было ни какого смысла, а всю нужную нам работу выполняла новая директива скролла.
Дополнительно
Дополнительно над AngularJS был произведен еще ряд шаманств:
- полностью удален функционал анимации ($animate, $$rAF);
- удалены директивы для работы с классами и атрибутами (ng-class, ng-class-even и пр.);
- ng-if и ng-switch заменены их оптимизированными аналогами – устанавливается display: none для html-элементов, не удовлетворяющих условию, минуя создание своего scope, как в оригинальных директивах, а ng-show и ng-hide удалены совсем;
- удалены $filter и $locale, а все необходимые данные подготавливаются непосредственно в наших сервисах на основе небольших самописных решений.
В итоге из AngularJS были удалены все директивы из коробки, а список сервисов приобрел следующий вид:
- $compile
- $browser
- $controller
- $exceptionHandler
- $interpolate
- $log
- $parse
- $rootScope
- $q
Мы не вмешивались в инициализацию и цикл работы AngularJS, но немного доработали jqLite.
Выводы
Современная разработка web-приложений идет путем включения довольно больших сторонних решений ради одной небольшой функции или фичи, тем самым как на дрожжах увеличивается прожорливость приложения. Хотя, возможно, эта прожорливость не столь заметна на десктопах и ноутбуках, девайсы со слабой аппаратной начинкой такие приложения переваривают с трудом, дополнительно изрядно нагреваясь.
Мы на своем опыте убедились, что, когда наступает время необходимой оптимизации, её вполне по силам сделать практически любой творческой команде разработчиков. Затраченное время на все описанные оптимизации составило порядка 12-15% от общего времени разработки проекта, что, в принципе, более чем достаточно и мы остались очень довольны достигнутыми результатами.
AngularJS действительно модульный фреймворк, и, хотя он не позволяет сконфигурировать необходимый набор функций и скачать его (как например можно сделать с jQuery UI), мы не испытывали никакого дискомфорта при исключении не нужных нам директив и сервисов из AngularJS. Именно модульный подход разработки приложений позволяет практически безболезненно производить широкомасштабную оптимизацию и рефакторинг.
И все же хочется верить, что такая оптимизация производятся не только вынужденно, но и в рамках заботы о конечных пользователях системы и всей команды разработки, да и, возможно, просто на волне безудержного энтузиазма.
Спасибо специалистам СимбирСофт за активное участие в работе над проектом.
Комментарии (24)
alek0585
22.06.2016 00:26+6Проще указать что же осталось в итоге от ангуляра. Кажется, что только сама структура: возможность написания сервисов, директив и контроллеров.
Было бы интересно почитать о том, как происходил выбор либ и прочего.
Статья супер!
nazarpc
22.06.2016 00:38+1Можно уточнить смысл
(<|<)
,(>|>|\\s)
и(>|>)
в регулярках? Выглядит как бесполезное усложнение.zatey
22.06.2016 09:18Спасибо за замечание, исправили.
Выглядит так:
var node = new RegExp('((<|<)[\\w:]*' + tag + '(>|>|\\s).*\/[\\w:]*' + tag + '(>|>))', 'g').exec(xml);
Некоторые методы soap-сервиса МФУ возвращают в ответ xml со вложенной экранированной xml.
napa3um
22.06.2016 00:42+10Не рассматривали angularlight? Ну или knockout, vue или что-то ещё компактное и обеспечивающее только биндинги с DOM (остальное вы всё равно отрезали от оригинального ангуляра, насколько я понял).
zatey
22.06.2016 09:59+1Разумеется рассматривали и Angular Light и Vue.js и даже были маниакальные идеи переписать все на чистый JS.
Но отправной точкой послужило почти готовое и протестированное приложение на AngularJS.
С точки зрения рисков, мы оценили, что переписать приложение на другой JS фреймворк против доработать (зарефачить) готовую кодовую базу значительно проигрывает по трудозатратам.
В любом случае, код для http-запросов, роутингу, скролу и т.д. пришлось бы писать свой, а «кастрация» AngualrJS до состояния более легковесных фреймворков занимает, уж поверьте, очень мало времени.
Если же за отправную точку взять старт проекта, то тут разумеется выбор бы пал на Vue.js, сообщество Angular Light показалось нам каким-то полумертвым, а готовых модулей для http-запросов и роутинга днем с огнем не сыщешь.
youROCK
22.06.2016 01:09+2Интересно, что вы описали, сколько времени это у вас заняло: около 10%. По моим ощущениям, для оптимизации программ до «приемлемого» состояния у меня обычно уходит процентов 30 времени, но все равно это намного меньше, чем люди обычно себе представляют, когда им предлагают оптимизировать свой код.
zatey
22.06.2016 10:28Вы несомненно правы, но в нашем случае описанная оптимизация действительно заняла 12-15% от всего времени разработки проекта. А это не может не радовать как глаз, так и бюджет проекта.
Sayonji
22.06.2016 02:21+4Делать пул xhr объектов целесообразно сейчас для обычных браузеров? Пусть, например, веб-сайт собирается посылать по запросу раз в 10 секунд и работать в таком режиме пару дней на компьютере. Если реализовать переиспользование xhr, получится ли выигрыш по памяти, занимаемой вкладкой? Или gc всё сделает хорошо, и приём в статье нужен чисто потому, что оперативки ксерокса хватаешь лишь на считанные минуты, и gc не успевает всё подмести?
zatey
22.06.2016 10:20+2В обычной веб-разработке целесообразность не то чтобы нулевая, а скорее отрицательная, так как все параллельные запросы выстроятся в очередь и приложение станет менее отзывчивым. Про потребление памяти XHR тоже не беспокойтесь, а вот контент оптимизируйте.
Если же ваше приложение работает на специфичной платформе с ограниченным количеством ОЗУ, то тут вам повезло — мы собрали грабли и поделились кодом.Sayonji
22.06.2016 12:08+1Насчет отзывчивости — я понимаю, да. Но, во-первых, для борьбы с неконтролируемым распуханием вкладки, отзывчивостью можно частично пожертвовать, а во-вторых, всегда можно дселать пул такого размера, чтобы вероятность простаивания была крайне мала. Но спасибо за ответ, похоже, что вы правы, и xhr не входит в основные причины утечек.
Radjah
22.06.2016 16:31+2> из исходной xml (которая нам пришла в виде строки) мы с помощью регулярного выражения “вынимаем” только тот тег, который нам интересен
А это не является грязным хаком?zatey
22.06.2016 19:25Есть запрос к soap-сервисам, который возвращают несколько десятков килобайт данных о самом МФУ, но из этого набора данных нужна всего лишь информация, поддерживает МФУ листы формата А3 или нет. Такая же история и в момент постановки документа на печать — интересен только статус задачи, а не килобайты балластных для нас данных.
Разумеется это грязный хак, но вынужденный — приоритет находится на стороне потребления памяти, а не красоты и универсальности решения.Radjah
22.06.2016 23:21Я к тому, что если в следующей версии прошивки устройства производитель что-то там подправит, то есть ненулевая вероятность, что приложение может отвалиться. Или такова плата за уменьшение потребления памяти?
zatey
23.06.2016 06:35В таком случае отвалится не только этот хак, но и сам обход xml-дерева до нужной ноды, так как имя самой или одной из родительских нод может измениться.
Ребята из Xerox версифицируют свои soap-сервисы, аля /v0/print или /v1/scan, все изменения новой платформы не затрагивают старые методы.
Antelle
На этой платформе каждое приложение должно сделать свою клавиатуру? Интересно, почему так.
zatey
Необязательно, можно использовать нативную. Но так как мы не смогли ее полноценно кастомизировать под свои нужды и стили — вынесли в виде отдельной директивы.