Разработка

Cookies: с ними нужно быть внимательным

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

Вообще чем кукиса характеризуется:

  • имя
  • значение
  • путь
  • время истечения
  • домен
  • секурность

Здесь и далее нас будет интересовать домен, поскольку именно он является причиной множества странный и часто трудноуловимых багов.

Возьмем пример https://github.com/RussianPenguin/blogSamples/blob/master/cookies.php и запустим его на локальном сервере пхп (примем за аксиому то, что у нас домен localhost.localdomain резолвится на локалхост).

Пример cookie.php на локальном сервереТеперь зайдем в браузер по нашему скрипту

Cookies.php в firebugВидно, что скрипт выставляет две кукисы для одного и того же домена.

Стандарт предписывает, что кукиса с точкой перед именем домена и без точки доступна в пределах всех поддоменов, а точка оставлена лишь для совместимости с более ранними браузерами.

Казалось бы, мы ставим кукисы на одном и том же домене. Кстати, вот код:

// Конфигурация (домен на котором запускаем)
$domain = 'localhost.localdomain';
$cookieName = 'test';

setcookie($cookieName, 'empty host', 3600+time(), '/');
setcookie($cookieName, 'host: ' . $domain, 3600+time(), '/', $domain);

тут я оставил конфигурацию домена руками так как запуск сервера идет на нестандартном порту.

Так вот. Думалось, что браузер должен поставить одну печеньку и в ней должно быть значение, которое передано последним.

Но нет. Если хидер установки кукиса содержит домен, то кукису будет присвоено имя домена с точкой в начале. А если имени домена не указывать, то кукис будет поставлен на текущий домен без точки в начале.

И когда мы попытаемся считать на стороне клиента (или сервера) наш поставленый кукис, то какой из них вернется — это уже немного рандом (хотя нет, все зависит от того, в какой последовательности печеньки ставились).

И то же самое происходит на клиенте: если мы при установки кукиса указываем домен, то браузер поставит кукис на домен с точкой в начале, а если не укажем, то без точки.

Попробуем прописать в тестовом скрипте какое-то значение для печеньки, но не указывать домен. А потом нажмем кнопку «поставить кукис».

cookies.php после setCookieЧто мы видим? А видим мы то, что браузер считал значение для текущего домена (первое) и перезаписал его новым. Которое мы выбрали. А второй кукис до перезагрузки остался нетронутым. Круто. А значит нажмем «прочитать печеньку» и увидим наше новое значение.

Однако если мы так думаем, то ошибаемся.

cookies.php getCookieДаже FireCookie что-то показывает — это не значит, что значение печенья поменялось.

Огнелись ничего не поставил.

Но как только мы домен укажем при установке

cookie.php - правильный результатТак сразу все работает как надо — устанавливается значение и при нажатии на кнопку «прочитать» мы его получаем.

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

Учтите, что если вы заходите повторить этот эксперимент, то на доменах верхнего уровня подобное не работает. Т.е. если мы будем использовать localhost, то подобное поведение не воспроизведется.

Разработка

Google JS API для AngularJS

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

Или в bower

 $ bower install angularjs-gapi

Поключение

angular.module('app', ['gapi']).config(['gapiProvider', '$routeProvider', function(gapiProvider, $routeProvider) {
   gapiProvider.apiKey(YOU_API_KEY) // апи-ключ можно создать в консоли разработчика
        .clientId(YOU_APP_CLIENT_ID) // берем в консоли разработчика
        .apiScope(SCOPES_FOR_APP); // скоупы, которые нужны для работы приложения
}])

Если вам не требуется работать с пользовательскими данными и не требуется разрешение пользователя, то достаточно использования только YOU_API_KEY.

Авторизация

angular.module('app').controller('tstController', ['$scope', 'gapi', function($scope, gapi) {
  gapi.login().then(function() {
    $scope.login = 'success';
  }, function() {
    $scope.login = 'fail';
  });
}])

Выполнение запросов не требующих авторизации

angular.module('app').controller('tstController', ['$scope', 'gapi', function($scope, gapi) {
  // we can't make requests while api is not ready
  if (gapi.isApiReady()) {
    gapi.call("youtube", "v3", "search", "list", {
      query: "search term"
      part: "snippet"
      type: "video"
    }).then(function(response) {
      // work with response
    })
  }
}]);

Выполнение запросов требующих авторизации

angular.module('app').controller('tstController', ['$scope', 'gapi', function($scope, gapi) {
  // we can't make requests while api is not ready and user is not logged in
  if (gapi.isApiReady() && gapi.isLoggedIn()) {
      gapi.call("youtube", "v3", "playlists", "list", {
      part: "snippet",
      type: "video";
    }).then(function(response) {
      // work with response
    })
  }
}]);

Вроде все.

Разработка

Grunt для самых маленьких

На проекте есть папка с с js и стилями css.

Нужно все это минифицировать.

1 — нам нужен грант (установленный локально).

Предполагается, что глобально грант уже установлен.

Если нет, то

 $ sudo yum install nodejs-grunt*

Ставим нужное

 $ npm install grunt
$ npm-install grunt-contrib-unglify grunt-contrib-watch grunt-contrib-cssmin grunt-contrib-concat

Если не поставить модули локально, то получим ошибку

Unable to find local grunt

2 — создаем файл с описанием проекта package.json

{
  "name": "<project name>",
  "version": "0.1.0",
  "devDependencies": {
    "grunt": "~0.4.5",
    "grunt-contrib-concat": "^0.4.0",
    "grunt-contrib-cssmin": "^0.10.0",
    "grunt-contrib-uglify": "^0.5.0",
    "grunt-contrib-watch": "*"
  },
  "dependencies": {
    "grunt": "^0.4.5",
    "grunt-ts": "^1.11.13"
  }
}

3 — создаем сценарий для работы

module.exports = function (grunt) {
    // 1 - Описываем все выполняемые задачи
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
        concat: {
            css: {
                src: ['src/**/*.css'],
                dest: 'dist/app.css'
            },
            js: {
                src: ['src/js/**/*.js'],
                dest: 'dist/app.js'
            }
        },
        cssmin: {
            css: {
                src: 'dist/app.css',
                dest: 'dist/app.min.css'
            }
        },
        uglify: {
            js: {
                src: 'dist/app.js',
                dest: 'dist/app.min.js'
            }
        },
        watch: {
            css: {
                files: ['src/css/**/*.css'],
                tasks: ['concat:css', 'cssmin:css']
            },
            js: {
                files: ['src/js/**/*.js'],
                tasks: ['concat:js', 'uglify:js']
            }
        }
    });

    // 2 - Загружаем нужные плагины
    grunt.loadNpmTasks('grunt-contrib-concat');
    grunt.loadNpmTasks('grunt-contrib-cssmin');
    grunt.loadNpmTasks('grunt-contrib-uglify');
    grunt.loadNpmTasks('grunt-contrib-watch');

    // 3 - Говорим grunt, что мы хотим сделать, когда напечатаем grunt в терминале.
    grunt.registerTask('default', ['concat', 'cssmin', 'uglify']);

};

4 — печатаем grunt в терминали

$ grunt
Running "concat:js" (concat) task
File gapi.js created.

Running "uglify:js" (uglify) task
>> 1 file created.

Done, without errors.
Разработка

Удаленная отладка в Google Chrome/Chromium

Пока рассмотрим только удаленную отладку комп-комп. Иногда это бывает очень необходимо.

На подопытном запускаем

$ chromium --remote-debugging-port=9222

На машине с отдадчиком заходим (ессно в хроме) на урл

http://target-machine:9999

Выбираем страницу — отлаживаем что надо.

К сожалению нельзя осуществлять взаимодействие с подопытной страницей напрямую из отладчика (конечно же можно, если мы будем делать что-то вроде $(‘element’).trigger(‘event-name’) в консоли), но можно перезагружать страницу.

Поэтому на подопытном запускаем тимвьювер или внц и отлаживаем чего надо.

Еще можно добавить параметр —user-data-dir=<тут путь>. Эта опция нужна если для отладки вы хотите пользовать как-то особым образом сконфигурированный профиль.

Удаленная отладка с Chrome

Разработка

Несколько советов по оптимизации приложений на Angular JS

Что нам потребуется.

AngularJS

Версия < 1.3

В версии 1.3 появились существенные отличия. Включая однонаправленный биндинг — :: (два двоеточия), но она несовместима с bindonce.

Хотя по скорости ничем не уступает оптимизированному варианту. Если вы используете >= 1.3, то применяйте готовый однонаправленный биндинг вместо bo-*

Bindonce

url: https://github.com/Pasvaz/bindonce

Эта библиотека позволяет делать однонаправленный биндинг данных в шаблоны.
Если вам не требуется, чтобы какая-то переменная после своего отображения меняла значение (т.е. она не будет его менять в процессе работы), то это однозначно для вас.

Например у нас есть список книг, который приходит с сервера и обновляется целиком (опять же с сервера).
Что мы делаем в коде

<div ng-repeat="book in books"><span>{{book.name}}</span><div>

Имя книги не меняется и мы можем записать так

<div ng-repeat="book in books"><span bo-text="book.name"></span><div>

Так как при перезагрузке книг список books будет изменен полностью, то дерево будет перестроено заново и нам не нужно никак заботиться о том, что имя изменится.

Однако, если вы используете sly-repeat (ниже), то его нельзя сочетать с bindonce из-за агрессивного кеширования дерева дом этим самым слайрепитом.

Scalyr

url: https://github.com/scalyr/angular

post: http://blog.scalyr.com/2013/10/31/angularjs-1200ms-to-35ms/

Эта библиотека позволит вам минимизировать количество одновременно работающих вотчеров путем отключения ненужных. А так же реализовать умный ng-repeat с кешированием созданных dom-элементов (который, увы, не сильно дружит с bindonce. т.е. не дружит вовсе).

Оптимизации

Много скрытых элементов на странице

Это одна из ключевых оптимизаций для проекта. Так как нам часто приходится скрывать/показывать разные элементы.

Представим, что у нас есть какой-то код типа нижеследующего.

<div ng-show="condition"><!-- а тут много разных строчек с данными, которые выводятся на базе данных из ангуляра --></div>

Если не предпринять дополнительных действий, то все то, что написано внутри скрытого дива будет выполняться (!), а это лишние такты процессора и нервные подергивания пользователя.

Что же мы можем сделать?

На помощь нам приходит sly-show+sly-prevent-evaluation-when-hidden. Эта убойная комбинация не будет запускать интерпретацию скрытых элементов.

<div sly-show="condition" sly-prevent-evaluation-when-hidden><!-- а тут много разных строчек с данными, которые выводятся на базе данных из ангуляра --></div>

Переписав код таким образом мы получим тот самый профит: снижение нагрузки на клиент-сайд.

Но стоит заметить одну особенность директивы: она в своем решении интерпретировать или нет вложенный код опирается на наличие класса ng-hide. При инициализации приложения или перерисовке элементов этого класса нет. Поэтому мы можем увидеть что-то вроде этого:

Trace angular application

Причина проста: у элементов при инициализации еще нет класса ng-hide. Он появится только после того, как отработают нужны контроллеры, а значит, что отрисовка элемента с нуля всегда будет отрисовывать и интерпретировать все скрываемые элементы (даже если они действительно скрыты).

Решение проблемы: принудительно добавить класс ng-hide всем элементам, которые находятся под связкой sly-show+sly-prevent-evaluation-when-hidden.

<div class="ng-hide"><!-- а тут много разных строчек с данными, которые выводятся на базе данных из ангуляра --></div>

На этом оптимизация неиспользуемого кода закончена.

Оптимизация биндингов

Под этим подразумевается однократное добавление в шаблон переменной.

Как в примере bindonce с книгами (выше) нам может не требоваться функционал двунаправленного отображения.

Поэтому все подобные участки мы заменяем однонаправленным биндингом.

{{var}} -> <div bo-text=”var”></div>

Путь оптимизации: вместо элемента создается контейнер, на который выполняется отображение текста элемента. Так как в биндванс не существует шаблонной конструкции для биндинга. Естественно, что там может быть любой тег и не обязательно div. Самое главное — наличие контейнера.

ng-href="text{{var}}" -> bo-href="’text’ + var"

А так мы оптимизировали ссылки.
Важно заметить, что ng-* и bo-* работают с биндингами различным образом: если первый работает по внутриангуляровским правилам интепретации, то второй просто выполняет .eval() на строку. Поэтому в bo-* мы работаем с обычными строками и переменными.

Аналогичным образом работаем и с другими атрибутами. Если есть аналог биндинга.

Если аналога нет, то используем конструкцию bo-attr-<имя кастомного атрибута>

<div bo-attr bo-attr-isbn="’{{book.isbn}}’"></div>

Здесь кроется еще одна важная особенность обработки атрибутов библиотекой: они рассматриваются как строки. И если в атрибуте текст, что его надо представить в виде текста (то же характерно для любого юиндига bo-*). Мы привыкли чаще всего работать с числами, но не стоит забывать, что есть еще буквы.

Поэтому если не уверены в содержимом переменной — пишите строковое предствление: ‘{{var}}’, а не var.

Вложенность элементов

На картинке профайлера (выше) вы можете увидеть гребенку из кучи вложенных вызовов функций. Если мы посмотрим на эти вывовы под микроскопом отладчиком, то увидим, что это рекурсивный обход дерева dom.

Чем меньше у вас уровней вложенности, тем лучше. Особенно это касается кода, который расположен в циклах.

Так что старайтесь сократить вложенности.

Дополнительная оптимизация при обработке скрытых элементов

У sly-prevent-evaluation-when-hidden есть одна большая проблема: она опирается только на наличие класса ng-hide, что не совсем уместно если видимость элемента контролируется каким-то другим классом.

В таком случае можно (вообще нужно всегда) использовать следующее расширение директивы (подключаем после подключения scalyr).

/**
* Extended version of slyPreventEvaluationWhenHidden from scalyr.
* This version prevent evaluation not only when element has class ng-hide.
* If element is hidden with "display: none" evaluation is prevented.
*/
angular.module('sly')
 .directive('slyPreventEvaluationWhenHidden', function () {
   return {
     restrict: 'A',
     // We create a new scope just because it helps segment the gated watchers
     // from the parent scope.  Unclear if this is that important for perf.
     scope: true,
     compile: function compile(tElement, tAttrs) {
       return {
         // We need a separate pre-link function because we want to modify the scope before any of the
         // children are passed it.
         pre: function preLink(scope, element, attrs) {
           scope.$addWatcherGate(function hiddenChecker() {
             // Should only return true if the element is not hidden.
             return !element.hasClass('ng-hide') && element.is(":visible") ;
           }, function hiddenDecider(watchExpression, listener, equality, directiveName) {
             // Make an exception for slyShow.. do not gate its watcher.
             if (isDefined(directiveName) && (directiveName == 'slyShow'))
               return false;
             return true;
           });
         },
       };
     },
   };
 });
Найдено в сети

Интересное в сети

Алгоритмы

JavaScript

Ruby

Python

Инструменты

Linux

Всякое

Разработка, HowTo

Управление зависимостями проекта при помощи bower+composer

Отлично. У нас есть новая интересная идея для нового проекта. Садимся мы значит за клавиатуру и… Эм. А что «и»? Наш проект зависит от кучи библиотек и фреймворков: yii, angularjs, doctrine, twitter bootstrap и т.д.

Ок. Открываем браузер или папочку на диске (в которой все нам нужное скачано и рассортировано) и начинаем неспешно скачивать/копировать/распаковывать.

Мы моложцы, справились. Все скопировали, но только злобные разработчики пофиксили баги и выкатили новую версию своей библиотеки, фреймворка.

Пофикшенные баги — это всегда хорошо. Начинаем процесс скачивания/распаковки заново.

Страшный сон? Кошмар разработчика? Нет-нет, что вы! Стандартная процедура, через которую проходят много-много разработчиков. И даже не задумывыются, что все это можно автоматизировать, упростить и дать себе возможность наконец-то реализовать задуманный проект, а не заниматься tar -xf или zip -u, или что у них там еще.

А ответ-то простой — это bower в связке с любым другим менеджером для вашего любимого языка (composer для php, npm для nodejs и т.п.). Больше всего мне приходится иметь дело с php, поэтому опишу на примере composer.

Опять же. Почему bower и composer. Потому что первый — это менеджер для фронтенд-составляющей, а второй — для бекенда. Композером не слишком удобно пользоваться для управления зависимостями морды приложения.

Начнем-с.

Читать далее