Недавно начал использовать триггеры в БД. Полезная штука.
Первый случай использования - валидация данных. Позволяет на уровне БД сохранять транзитивную целостность, т.е. более глубокую проверку, чем просто существующий foreign key.
Вот простейший пример. Есть три таблички - site, category, article. При добавлении нового ряда в article, мы хотим проверить что categoryID использует тот же siteID что и article. Забудем сейчас про нормализацию, допустим siteID нужен и там и там. Триггером валидация решается так..
DROPTRIGGERIFEXISTS trigger_article_insert; CREATETRIGGER trigger_advert_insert BEFORE INSERTON article FOR EACH ROW BEGIN IF NEW.siteID <> (SELECT category.siteID FROM category WHERE category.ID=NEW.categoryID) THEN SIGNAL SQLSTATE '46000' SET MESSAGE_TEXT = 'Cannot insertrow: site ID is different for article and category ENDIF;END;
Аналогично мы можем решать и вопрос обновления данных.
В обычных случаях мы могли делать сразу JOIN или подзапрос, либо уже создать VIEW, но при высоких нагрузках лучше иметь триггер..
DROPTRIGGERIFEXISTS trigger_article_insert2; CREATETRIGGER trigger_article_insert2 AFTERINSERTON advert FOR EACH ROWUPDATE category c SET size =(SELECTcount(a.id)FROM article a WHERE a.categoryID=NEW.categoryID AND a.status='published') WHERE c.id=NEW.campaignID;
Хождение по воде и разработка по спецификации легки, если и обе заморожены
Эдвард В. Берард
С самого начала моей карьеры, спецификация была больной темой. В маленькой веб-студии написание «спеки» сводилось в лучшем случае к двум-страничкам A4 с описанием нового модуля для уже работающей CMSки. Со временем я повидал и спеки как на 80 страниц от сторонних компаний в фиксированном pdf виде, так и в режиме постоянного scrumа где спека размазана по десяткам задачам в issue tracker'е.
Есть разница между спецификацией (SRS, техническим заданием), документацией и руководству по техническому обслуживанию, она в цели.
Если вы так же озабочены тестированием как и я, то вы возможно сталкивались с проблемами дедлоков при транзакциях. Транзакции в БД дело хорошее, особенно на все REST-запросы, т.к. он становится атомарной операцией. Однако если вы вместе с этим затрагиваете часто используемую таблицу или операция происходит на файлах или с другими сетевыми запросами, то вы можете вызвать дедлок.
MySQL/InnoDB дедлок возникает из-за двух транзакций пытающихся в разном порядке изменить одни и те же данные. Если вы используете триггеры, то вам в коде даже не надо явно объявлять о транзакции - любой UPDATE с триггером потенциально опасен дедлоком. При этом уровень изоляции транзакции не влияет на вероятность его получения
#dont update unless you want CentOS 6.8 upgrade #yum update -y #install basic tools yum install -y htop nano #reboot for kernel updates to take place shutdown -r now
Систему надо настроить для вагранта, если мы хотим её автоматически выкатывать под разные проекты..
Сначала - настроить пользователя
sudo visudo -f /etc/sudoers.d/vagrant #adding to this file: vagrant ALL(ALL) NOPASSWD:ALL #setup vagrant public key access mkdir -p /home/vagrant/.ssh chmod 0700 /home/vagrant/.ssh wget --no-check-certificate \ https://raw.github.com/mitchellh/vagrant/master/keys/vagrant.pub \ -O /home/vagrant/.ssh/authorized_keys chmod 0600 /home/vagrant/.ssh/authorized_keys chown -R vagrant /home/vagrant/.ssh nano /etc/ssh/sshd_config #uncomment AuthorizedKeysFile %h/.ssh/authorized_keys #enable public key authentication service sshd restart #start ssh on reboot chkconfig sshd on visudo #comment out requiretty option
Теперь для вагранта нужны Guest Additions. Это iso файл который у virtual box'а уже есть. Надо смонтировать его на диск и запустить установку пакетов
yum install -y gcc build-essential perl kernel-devel #mount VBoxGuestAdditions.iso on CD in Virtualbox and run these: sudo mount /dev/cdrom /mnt cd /mnt sudo ./VBoxLinuxAdditions.run echo export KERN_DIR=/usr/src/kernels/`uname -r`/ >> ~/.bashrc
Теперь, у вас вероятно стоит надстройка для безопасности - SELinux. Если вы будете разрабатывать локально, то лучше его отключить
setenforce Permissive nano /etc/sysconfig/selinux #set to: #SELINUX=disabled
Теперь когда всё готово, выходим из виртуалки и упаковываем её для vagrant'а. Для этого надо зайти в папку где лежит виртуалка и запустить комманду которая сгенерит package.box файлик на который в дальнейшем будет ссылаться Vagrantfile в нашем проекте:
#package box and move it to desktop vagrant package --base CentosEmpty && mv package.box ~/Desktop/ #clean vagrant cache vagrant global-status --prune #cd to your project that references new box from desktop vagrant up
Одна из проблем с которой сталкивается backend разработчик разрабатывая приложение с отложенными вычислениями это управление параллельными процессами. Например, если вы делаете загрузку картинок которые необходимо в дальнейшем обработать (уменьшить, вырезать, передвинуть на другой сервер), то для масштабирования такого решения под N -> infinity пользователей, каждый этап имеет смысл делать в отдельном процессе, число которых тоже можно масштабировать вместе с ростом N.
Я по-прежнему использую php демонов, которые рождаются кроном (если на nohup полагаться не хочется). Но поскольку мне хочется одновременно делать ресайз картинки в 5 разных размеров одновременно, я хочу управлять количеством instance'ов php процессов. Это решается простой bash-коммандой:
Заметьте что ключевая комманда -lt. Если коротко, то мы ищем процессы (ps) по их имени (grep), исключая самих себя и считаем количество найденных строк (wc), количество которых должно быть меньше (lt) количества заданных мною инстансов. Как вариант вместо -lt можно использовать -eq 0, если хочется только одного демона
При постройке качественного single-page приложения рано или поздно встаёт вопрос синхронизации данных. Это тем более актуально, когда дело касается работы разных пользователей. Приложения типа google wave, google docs, push-notifications в IOS, подняли планку ожидания пользователей и от обычных веб-систем
Увеличивает реактивность, поддерживая данные в реальном времени без обновления страницы. Типичная ситуация когда вы работаете в нескольких табах и данные не обновляются потому что закешировались
Улучшает безопасность — если пользователь выходит из одного таба, его можно выкинуть из остальных автоматически
Снижает нагрузку на сервер — можно высылать оповещение о состоянии файла, если он обрабатывается в реальном времени, подобно ютубу без какого-либо взаимодействия с пользователем или поллинга
Если вы решили что это must-have фича, прежде чем начать реализовывать её у себя, задумайтесь о последствиях:
Вам понадобится транспортный механизм (см. ниже), который обычно ограничен количеством подключённых пользователей (макс. 10 тыс), размером сообщения и объёмом траффика
Безопасность. Транспортный механизм и источник оповещения должен точечно высылать данные только нужному пользователю (без broadcast-а).
Лучше всего источник оповещения ставить на backend, сразу после закрытия транзакции. В этом случае полезно привязываться к сессии на бэкэнде
Я советую не использовать datasync-сервис в качестве источника данных. Это должен быть легковесный вестник адреса по которому можно прочитать детали. Более того, формат этого сообщения должен быть стандартизирован. Например {action:'file.added', id:1345}
Весь front-end код должен хранить данные в сыром виде в сервисах/моделях, которые будут точечно обновляться.
Соответсвенно все события будут делится на прямое вмешательство пользователя и на datasync-действия
Код при этом будет запускать похожие функции над данными
Все use-кейсы усложняются из-за ситуаций
что делать если A удалил данные который B просматривает (редиректить B, блокировать A, показывать оповещение B?)
что делать если A и B одновременно сохраняют изменения над одним и тем же обьектом (провести обе операции, кто последний тот и победил? провести первую и заблокировать последующие до окончания операции?)
что делать если A составляет новый обьект 3, ссылающийся на обьект 2, который удаляет B, а потом происходит сохранение (удалять include при удалении или при сохранении?)
Если вы используете систему привилегий, то надо дополнительно иметь оповещения о присвоении или изъятия обьекта из видимости пользователей
Если ваш сервис синхронизации вдруг упадёт, то при поднимании надо сделать так что-бы сообщения не потерялись из-за ещё неподключившихся пользователей (у socket.io 5 сек)
Не ушёл в небытие, я по-прежнему вижу как некоторые сайты настойчиво стучат в бэкэнд. Например движение общественного транспорта в Таллинне. Видимо это проще всего реализовать и проще всего использовать вместо API. Что может быть проще для сторонних разработчиков - просто читай .json файл так часто как тебе это надо. А он в свою очередь может выплёвываться из кэша-в-памяти
Pusher
Пушер — сервис который вобщем-то следующий по сложности для разработчиков. Внедряете скрипт, подписываетесь на события в js. Высылаете события, которые будут получать все пользователи.
Лучше всего это делать на уровне backend'а, иначе захламите frontend и могут возникнуть редкие баги. Например — поставил заливаться файл и закрыл браузер, а клиент X не получил итогового оповещения. Для более тонкой настройки прийдётся повозиться с интеграцией авторизации и токенами
Наконец, ещё один сервис, купленный гуглом, который больше позиционируется как nosql-база данных с синхронизацией между клиентами. Для того что-бы настроить её, как систему оповещений, но при этом оставить хранение данных у себя, прийдётся повозиться. Изначальный setup такой же простой как у pusher'а, да и авторизация по токену такая же, но поскольку это БД, то подписываться надо не на события оповещений, а на изменения json-схемы. Саму json-схему вы делаете сами, но я советую для этого случая использовать что-то типа..
{ notifications:[ 2345:[ 'file X added by 2346', 'file Y deleted by 2346' ], 2346:[ 'file X added by 2345' ] ] }
Socket.io вы должны хостить сами и для этого нужен nodejs. Socket.io реализует браузерные веб-сокеты, протокол которых менялся несколько раз. Из-за этого могут быть трудности с подключением со стороны php к нему. Я использую elephant.io, пока полёт нормальный. Самый простой начальный вариант — socket.io принимает подключение и делает broadcast всем подключённым клиентам. Для более тонкой настройки прийдётся повозиться с куки, php сессиями и желательно переносом их хранения в БД, что-бы backend мог высылать безопасно сообщение конкретному подключённому пользователю, после чего контролировать по session_id + socket кто что будет получать.
У socket.io есть альтернативные библиотечки — socky (любит руби), sockJs, вроде как очень быстрый ws, engine.io, data.io. Faye я сам не пробовал, но судя по описанию растёт ещё со времени когда терминология с кометами была на слуху. Видимо теперь тоже работает на веб-сокетах
Микросервисы в современной веб-разработке это архитектурный подход по разделению изначального монолитного приложения на независимые системные (linux) процессы. Необходимость в таком разделении возникает когда монолит становится слишком медленным для одного синхронного процесса, когда код тянет слишком много зависимостей и когда повышается риск что-то сломать в этой длинной цепочке обработки данных.
Микросервисы — не панацея и тоже усложняют всё приложение в плане транзактивности, логов, и обработки ошибок, управления конфигураций, версионирования, деплоя, возникает дублирование bootstrap-кода из-за изоляции сервисов. Поэтому стоит осторожно подходить к тому, что вы хотите выделить в микросервис и что он даст по характеристикам приложения (performance, стабильность, масштабирование, разделение нагрузки). Как правило, веб-приложения создаются сначала монолитом, а потом разделяются на сервисы — так эффективней и проще эволюционировать.
Синхронно — сервис вызывают явно как веб-сервис по HTTP/REST, делая работу синхронно по-старинке, подобно веб-серверу, тогда как клиент ждёт ответа (подобно ajax)
Асинхронно — cервис вызывают явно по HTTP/REST, регистрируется задача и клиент тут же получает job ID. Клиент обязан сам проверить состояние задачи, периодически спрашивая у сервиса (polling, aws transcoder)
Асинхронно — сервис как работник (демон) без публичного доступа. Слушает какой-то ресурс и генерирует новые вызовы. Это может быть
хранилище с локами (файлы, память, Memcache, Redis)
другие ресурсы (процессы, сеть, железо)
Асинхронно, не используя MQ, регистрирует слушателей в себе и сам знает к кому куда стучать
Главное что-бы подход был единый, с предсказуемым форматом данных (JSON, protobuf, thrift, messagepack).Для удобной конфигурации и дружбы между сервисами, надо ставить Consul, ETCD или ZooKeeper — они позволят абстрагироваться от конкретных IP адресов и PID процессов.
Например пользователь приложения имеет возможность загружать файлы.Обработка файла очень тяжёлая по CPU, генерирует несколько результатов и занимает много времени из-за долгих внутренних сетевых запросов по загрузке готовых обработанных файлов в хранилище. Допустим эта синхронная операция upload+resize+store занимает 20 секунд в монолитном приложении. В микросервисном приложении, вы решаете создать сервис обработки картинки, а для очередей сначала создаёте табличку в БД. Отлично, это даёт возможность сервис положить на другую машину и не нагружать основной сайт. Теперь возникают практические вопросы — как это всё сделать? Практически, если раньше в монолите разные слои приложения проверяли всё за вас, то теперь чуть ли не все значимые классы надо будет выделять отдельные сервисы авторизацию, логирование, менеджер картинок, менеджер транзакций и сам ресайз картинок.
Если ваш сервис должен иметь публичный веб-интерфейс, проще всего запустить php в качестве сервера под конкретную папку и она будет по запросу дёргаться. Тут пригодятся микро-фреймворки типа Silex, Slim или Lumen
#run me as: #php -S 0.0.0.0:80 -t /var/www/silex #install me with: #composer require silex/silex:~1.3 $loader = require __DIR__.'/vendor/autoload.php'; $app = new Silex\Application(); #синхронная работа $app->post('/resizeImage', function(Request $request) use($app) { #загрузить imagemagick, сделать resize, сохранить return "{success:true}"; }); #асинхронная работа $app->post('/resizeImageAsync', function(Request $request) use($app) { #зарегистрировать новую работу и передать её демону через RabbitMQ, см. ниже #клиента оповестим автоматом через websocketы return "{success:true}"; });
Хотя php изначально разрабатывался как интерпретатор html файлов, современный php может работать в постоянном режиме как сервис если его запустить в CLI режиме. В реальной жизни, php демоны как правило нестабильны из-за утечек памяти и не так эффективны по скорости как сервисы на nodejs или go. Запустить демон можно из коммандной строки..
php -f /var/www/app/daemon/myImageProcessor.php #let it live after terminal exit: nohup php -f myImageProcessor.php &
С демонами есть несколько особенностей
Работая в бесконечном цикле надо чистить ресурсы (DB-соединения, file handles)
Надо мониторить рост своей памяти что-бы не умереть от fatal error
Надо поддерживать graceful stop при получении SIGTERM и прочих сигналов что-бы не повредить данные на случай если кто-то остановит демона из консоли или перезапустит сервис
Надо ловить все исключения, делать альтернативную функциональность бэкапов и откатов, формировать красивый log
Перезапуск сервера должен запускать демоны заново — через крон или через регистрацию демона в виде системного сервиса
У вас может быть несколько экземпляров (instance) одного и того же демона в виде разных процессов. Тогда очень важно не попасть в проблемы многопоточности — дедлоки транзакций, запись в один log файл параллельно и обработка одного и того же задания. Обо всём этом чуть позже..
А пока, вот простой пример с бесконечным циклом и псевдо-кодом:
//myImageProcessor.php //set custom process name cli_set_process_title('php - daemon - myImageProcessor.php'); //enable process interruption handling declare(ticks = 1); $interrupted = false; function handleSignal(){ $interrupted = true; } pcntl_signal(SIGTERM, "handleSignal"); pcntl_signal(SIGHUP, "handleSignal"); pcntl_signal(SIGUSR1, "handleSignal"); pcntl_signal(SIGINT, "handleSignal"); while(true){ $db->connect(); $db->query("SELECT * FROM images WHERE processed = 0"); //do some work here with resize & upload $db->disconnect(); //memory cleanup & check gc_collect_cycles(); const MAX_MEMORY_EXTRA = 10 * 1024 * 1024; if (convertToBytes(ini_get('memory_limit')) < (memory_get_usage(true) + MAX_MEMORY_EXTRA)) { exit(); } //graceful shutdown if($interrupted){ exit(); } //wait for some time to decrease load on DB above sleep(10); }
Проверку на УЖЕ запущенного демона можно сделать как на уровне bash, так и на уровне самого php с использованием экзотических флагов в базе, флагов во временных файлах или же используя такой же доступ к shell. С такой проверкой, комманду можно смело класть в cron и демон будет запускаться автоматом (раз в минуту)
Для более могучего nodejs, существует отличный process manager, который может управлять и php-демонами! С его помощью можно сразу видеть только процессы демонов. Кроме того:
перезапускать демона сразу после его падения
следить за изменениями файла что-бы перезапустить демон (watch)
следить за памятью и CPU и перезапускать при их превышениях
Как только вы начали использовать демонов, вероятней всего вы станете использовать и паттерн Command, по которому микросервисы выполняют роль работников (workers). MQ-сервер выступает в роли брокера и шины, связывающей асинхронные процессы воедино. Он отвечает за надёжность передачи сообщения сервису и за то что несколько сервисов не получат одну и ту же комманду. Реализовывать это на стандартной БД было бы сложней
Для PHP-демонов, подключение rabbitMQ делается с помощью php-amqplib библиотеки и бесконечный цикл в этом случае начинает зависеть от неё..
$callback = function($msg){ //actual work here $data = json_decode($msg->body, true); //Ack message that everything is done $msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']); } //technical details.. $connection = new AMQPStreamConnection('localhost', 5672, 'root', 'pass'); $channel = $connection->channel(); //new queue if it was missing $channel->queue_declare($queue, false, true, false, false); //one entry at a time $channel->basic_qos(null, 1, null); $channel->basic_consume($queue, $receiverName, false, false, false, false, $callback); //almost infinite cycle while (count($channel->callbacks)) { $channel->wait(null); } $channel->close(); $connection->close();
Понятно что один сервис хорошо, но архитектура ценна именно тем что сервисы взаимодействуют между собой. Поэтому сообщения можно генерировать другому сервису. Например если обработалось изображение, значит надо оповестить пользователя в реальном времени и обновить UI.
Многие в курсе что при параллельных запросах от браузера к серверу, браузер старается ограничить число активных запросов (по ~5 на домен). Но та же проблема возникает и на бэкэнде с ресурсами
Одна проблема с БД решается транзакциями. Но вторая, более явная остаётся - сессии. В моём случае эта проблема вылезла при загрузке файлов в SPA-приложении. Делаете 20 POST запросов с файлами и параллельно ходите по сайту. Выглядит всё так, что покуда файл не загрузится и сервер не ответит, следующий GET не сработает и UI не обновится
Происходит это потому что php блокирует потоки где есть session_start(). В моём случае это все запросы, потому что есть авторизация
Решается очень просто:
session_write_close();
Закрываем явно запись в сессию. Читать мы всё ещё сможем оттуда, но зато следующие запросы смогут быстро исполняться. Я это делаю ещё до того как файл начнётся обрабатываться
Добавляете CORS-файл на S3, вбиваете в нужные места ключи — вот у вас готов аплоадер, поддерживающий multipart, параллельную загрузку файлов, необходимую для файлов в 100MB и выше
Я не так давно писал о protractor'е — он облегчает работу с selenium, хоть и завязан с jasmine. Недавно вышла 3я версия и я, заодно обновив jasmine до 2.4.1, решил двинуться дальше со сценарными тестами, а именно - лишиться их, разбив на изолированные UI-тесты. Зачем?
Когда вы пишете сценарные тесты, то довольно скоро сталкиваетесь с проблемой заготовки данных. В интеграционных тестах я уже решал эту проблему, используя отдельную базу данных на каждый тест. Это довольно тяжело и энергоёмко. Когда дело касается UI-тестов, это ещё сложней.
Иногда, когда пишешь терминальное приложение (миграции например), хочется кроме стандартного потока, создавать более красочные сообщения и прогресс-бары. Для этого надо использовать управляющие коды для шелла. Перепечатаю для себя шпаргалку на будущее..
echo "\033[s"; //Сохранить положение курсора echo "\033[u"; //Восстановить положение курсора echo "\033[<линия>;<колонка>H"; //Поставить курсор на линию и колонку echo "\033[<линий>A"; //Сдвинуть курсор вверх echo "\033[<линий>B"; //Сдвинуть курсор вниз echo "\033[<колонок>C"; //Сдвинуть курсор вправо echo "\033[<колонок>D"; //Сдвинуть курсор влево echo "\033[2J"; //Очистить экран и перейти на начало echo "\033[K"; //Очистить строку до конца echo "\e"; //Аналог \033 echo "\r"; //Вернуться на начало строки
/** * @deprecated Устаревшая функция * @see nameSpace.ClassName#methodName Note, there are no { } brackets. * @see namespace.ClassName Note, there are no { } brackets. **//** This is a comment of how {@link nameSpace.ClassName} is similar to this object. **/
/** * @param {paramType} paramName Basic parameter description. * @param {String|Number} paramName Can be one of these types. * @param {String} [paramName] Parameter is optional. * @param {String[]} paramName Parameter is an array of strings. * @param {function(string)} paramName Parameter is a function callback. * @param {String} [paramName="mathew"] Parameter has a default value of "mathew" via code. (must be optional). * @param objectName This parameter is an object with values. * @param objectName.property1 This is a property of above object. **/
/** * @class {namespace.ClassName} Defines a reference to a class. * @augments {namespace.ClassName} Define a derived base class. * @extends alias for @augments * @lends {namespace.ClassName} Declares a block to define part of a class. * @constructor Defines a function as the creator for a class. * @constructs Must be used with @lends to define the initialization function. * @private Highlights invalid references to a property in PHPStorm * @protected Highlights invalid references to a property in PHPStorm * @override Defines a function as overriding a function of the same name in a lower class. * @borrows namespace.ClassName#methodName Tells PHPStorm to use the documentation from another class/method for this method. * @memberOf namespace.ClassName Declares something outside of a class block as part of that class. **/
/** * @this {type} Defines what object "this" referrers to. * function(/**String**/arg1)Defines the parameters of an inline function. * @return{type} The return type. * @returns alias of above. **/
Backbone Model and Inheritance A typical example using a Backbone model to demonstrate how to declare inheritance.
/** * This is a model class what be will inherited. * * @class {gems.BaseModel} * @extends {Backbone.Model} **/ gems.BaseModel = Backbone.Model.extend( /** * @lends {gems.BaseModel} */ { /** * This is a property of the class. * * @type {string} **/ message: 'Hello world', /** * A constructor for this class. * * @param {Object} attributes * @param {Object} options * * @constructs */ initialize: function(attributes, options) { }, /** * This is a virtual method on the model. * * @param {string} arg An argument * @public * @returns {boolean} */ foo: function(arg) { return false; } });
Небольшой know-how.. javascript асинхронный как вы понимаете, а это налагает ряд ограничений и проблем. Например когда у вас несколько асинхронных запросов (т.е. вы незнаете когда они завершатся), а от всех их зависит последующая логика приложения, то обычное не-асинхронное приложение (например на php) решает это в лоб - последовательно выполняет операции и получается такая картинка..
Именно многопоточность даёт возможности ускорять backend на nodejs, масштабироваться на многоядерных процессорах, многомашинных обработках с map-reduce и тп. Для простого решения этой проблемы на frontend можно конечно написать костыль самому, но в jQuery уже есть Deferred объекты, с простым синтаксисом..
Набрёл на интересную бесплатную книжку Тамары Кулинкович по психологии (см. пост на хабре). Сама она занимается HR-процессами, визуализацией данных и тп. Суть в потребностях людей. Будете их удовлетворять - у вас будет хороший продукт
Я в последнее время всё больше люблю писать интеграционные (API) тесты — запускаю половину приложения, но не привязан к UI. Это золотая середина между очень медленными end-to-end тестами и очень быстрыми unit-тестами. Рассмотрим особый случай таких тестов, которые используют заготовленные данные под каждый тест.
Такие тесты приходится создавать, когда проект становится таких масштабов, что тесты с одной БД начинают конфликтовать между собой и становятся нестабильными. То список где-то постоянно растёт, то ID у ресурса требуется фиксированный а у нас autoincrement, то данные хочется удалить.
Это особенно ярко видно в e2e тестах, где приходится управлять всем жизненным циклом данных что-бы тесты оставались рабочими. Управление всем циклом из создания-операции-удаления, вынуждает тесты делать зависимыми друг от друга, а значит становится невозможно запустить тест сам по себе.
В продолжение поста про Радикальная честность, по личному опыту общения с одним любимым человечком, захотелось выписать присущие ему характеристики, а потом нашёл наглядный пример, социопата которого вы могли бы заметить в фильме Gone girl.
Социопат – это человек, лишенный совести. Возникает либо наследственно, либо из-за повреждения лобной доли мозга либо из-за психологической травмы детства (родители постарались). Это душевная инвалидность, не лечится, человека можно лишь адаптировать. Во многом схожи с нарциссами
Если вы заботитесь о качестве своего проекта и кода, то пишете unit-тесты. Но с ними всегда есть «особые случаи». Один из них - работа с файловой системой и ресурсами. Решение «в лоб» - параллельно создавать папку/дерево специально для тестов и надеяться что они не прыгнут на настоящие пути и ничего не удалят.
Более правильный подход - использование in-memory виртуальной файловой системы, vfs. А поскольку ресурсы это по сути потоки, то и название для этой мок-библиотеки - vfsStream. Ставим..
Я когда-то писал про то что в phpunit небыло возможности нормально протестировать внутренние методы класса и приходится обращаться к runkit. Незнаю, была ли это моя недоработка, либо с версии 3.8 прошло уже много времени, но в 4.5 эта возможность есть! Моки в 4.5 стали удобней — они умеют перезаписывать как весь класс, так и его части.
У меня подход к разработке эволюционный и к тестам прагматичный — тесты должны позволять постепенно улучшать качество приложения. В реальной жизни приложение уже существует в какой-то форме, а программисты ленивые и тесты не писали. Поэтому надо уметь минимальным рефакторингом добавлять автоматические тесты, постепенно приводя людей и покрытие к вере в Бога TDD. По-оправдывался, теперь к конкретике..
Jira от Atlassian — самый современный трекер задач и багов позволяющий гибко настроить workflow организации. Но если вы не доросли, не хотите использовать Bamboo, а скажем используете PHPCI для автоматического тестирования, то вам возможно будет полезно видеть результаты прогона тестов сгруппированных по фичам.
Это достаточно спорная тем, многие апологеты тестирования не понимают зачем надо связывать тесты с фичами. Такие разработчики либо придерживаются простого мировоззрения что «все тесты должны проходить всегда», либо считают это излишним усложнением не дающего ничего в итоге.
По-моему эта методика достаточно полезна для повышения прозрачности покрытия фич. Если разработчик пишет новую фичу, то это не значит что он сразу же покроет код тестами. И даже если код на 100% покрыт юнит-тестами, это не значит что ошибок нет. Надо писать интеграционные тесты. А интеграционные тесты как правило не генерируют покрытия. Поэтому видеть - написаны ли какие-то тесты для фичи полезны для уверенности. Это справедливо и если вы программист и не следуете методике TDD "test first" и если вы менеджер и для вас покрытие тестами недостаточно прозрачно, не гранулярно.
Кайдзен — по-японски означает «изменение к лучшему», это нечто типа трезвения и осознанности, полезная практика постоянного самосовершенствования (constant improvement) ради всеобщего блага. Популяризируется она благодаря тому что это фундамент, используемый на фабриках корпорации Toyota для эффективного производства.
Первый шаг это устранение потерь. Для этого надо беречь исходные материалы, время, место и работников — словно вы работаете в (после)военное время. Применимо к IT, практика так и называется — бережливая разработка, Lean (англ. скудный). Философски, это избавление от вредных зависимостей.
Современные приложения всё больше начинают напоминать полноценное desktop решение, где в итоге запускается build-процесс с помощью grunt и мы получаем один кешируемый js файл. Теперь встаёт вопрос, как бы его загрузить так что-бы показать % подгрузки?
Есть два отдельных скрипта — $script, который инжектит новые script-элементы с обратной связью и progress.js, который симпатично показывает степень загрузки. Проблема в том что оба они уже сами по себе тяжёлые, в итоговый загружаемый билд-файл их нельзя вставить, а внедрять в index.html слишком бы его раздуло.
К тому же $script не поддерживает степень загрузки, а progress.js не использует % от ширины страницы. Поэтому я написал более простенький вариант.
$script = function (path, callback, sizeInChars) { var head = document.getElementsByTagName('head')[0]; var body = document.getElementsByTagName('body')[0]; var div = document.createElement('div'); div.setAttribute('style', 'background-color:#3498db; width:0px;height:3px;transition: width 0.5s linear;'); body.insertBefore(div, body.firstChild); var src = path + (path.indexOf('?') === -1 ? '?' : '&'); var oReq = new XMLHttpRequest(); oReq.addEventListener("progress", function (e) { div.style.width = (100 * ( e.lengthComputable ? (e.loaded / e.total) : (e.loaded / sizeInChars))) + '%'; }, false); oReq.onreadystatechange = function (e) { if (oReq.readyState != 4) return; var el = document.createElement('script'); el.text = oReq.responseText; head.insertBefore(el, head.lastChild); callback(); window.setTimeout(function () { div.remove(); }, 500); }; oReq.open('GET', src, true); oReq.send(null); }
Итого — js файлы подгружаются как текст через ajax, внедряются (без eval), вызывается callback, при этом показывается div отражающий степень загрузки. Единственная проблема — вы должны знать размер загружаемого файла, что в случае nginx + gzip не приходит с заголовками. Поэтому приходится использовать например так..
$script('/js/app.js', function () { angular.bootstrap(document, ['myApp']); }, 4879435);
Порой надо написать какой-то установочный скрипт, который требует одновременный запуск нескольких задач, из которых некоторые достаточно долгие — сервер, билд проекта. Это не то же самое что последовательный запуск с «&&» или «;» разделителями. Shell-скрипты могут и это с помощью комманды trap.
Комманда работает на прерывании процесса (второй параметр). В данном случае это EXIT и TERM. Основная комманда - убиение запущенных этим терминалом в фоне процессов (jobs -p). Сами они перечисляются дальше с группировкой фигурными скобками.
Первый trap таким образом вызывает последующие вызовы. Заметьте что второй вызов с инициализацией базы mongo стоит с задержкой в 3 секунды, что-бы сервер успел подняться (который уже будет бежать бесконечно)
На практике получается впрочем так, что комманды бегут бесконечно, т.е. mongod запускается, вы нажимаете Ctrl+C, а он всё ещё работает.. jobs ничего не показывает, но если возникает соединение - контекст возвращается. Приходится использовать killall mongod
Бесконечный скролл, т.е. постепенный показ и подгрузка данных как реакция на действия пользователя, закрепился в SPA-приложениях. Я работаю с angular, но в целом это может и к backbone относиться..
Технически, данные на frontend хранятся в какой-то коллекции/сервисе. То что показывается пользователю — подмножество этих данных, т.е. может быть отфильтрованным, отсортированным и изменяемым на лету. Таких подмножеств в приложении может быть несколько, в зависимости от отображения (view).
В одном месте у вас товары одного бренда, в другом месте у вас заказанные товары, в третьем - товары конкретного пользователя.
Это значит, что вопрос подгрузки новых данных - нетривиальная задача.
Сначала я подумал что лучше всего запрашивать данные по времени. У каждого ряда данных будет timestamp, как у твиттера, соответсвенно запрос будет высылаться "подгрузи 20 рядов данных, старше X", а время будет браться у последнего ряда.
Но тут возникает несколько проблем.
Что если у данных одно и то же время? Если на backend добавить вторую сортировку по ID, и передавать ID последнего ряда вместе со временем, то это всё-равно не спасает положения, т.к. значительно усложняется вычисление OFFSET в SQL запросе.
Если запрос зависит от последнего показанного ряда, то получается что запрос формируется на основе подмножества, т.е. зависит от view. Это плохо, т.к. завязывает модель, контроллер и view в единое целое. Альтернатива - дублировать фильтрацию которая происходит для view в модели.
Что если данные обновились со времени последнего запроса? Надо ли их добавлять в начало списка?
Что делать если данные отсортированы не по времени, а по алфавиту? И они вдруг поменялись?
Вобщем приходишь к тому, что для универсального решения, надо по-прежнему работать с OFFSET и ORDER BY, как то было с постраничной навигацией. При этом offset высылается в зависимости от нужды view и он равен размеру подмножества.На случай обновившихся данных, я использую socket.io, который оповещает что надо обновить. Добавившиеся данные я сразу внедряю в список, а update происходит автоматически
ngInfiniteScroll - простенькая ангулярная директива для ангуляра, которая триггерит нашу функцию подгрузки если вы достаточно проскроллили вниз (scrollTop() близок к высоте элемента). Проблемы в ней две - она привязана ко всему окну (window). Обычно же у нас может быть несколько контейнеров на странице где надо подгружать данные. Есть и более простой пример
И вторая проблема следующая из неё - если вы запросили слишком мало данных за запрос (LIMIT 5), и/или их высота динамическая и меньше чем высота контейнера, то ваш scroll никогда не появится и не запустит постоянное дополнение списка. Пришлось это поисправлять, каждый раз расчитывая высоту содержания контейнера.
Тут главное не заDDOSить сервер из-за неправильного расчёта высоты элементов и рекурсии. По-хорошему вызов подгрузки надо начинать до достижения нижнего края.
Есть небольшая проблема, если у вас есть фильтрация коллекции с поиском. Я решаю эту проблему отдельным мини-кэшем и отдельными запросами. Если вы ищете что-то по ключевому слову, то результаты не добавляются в основную коллекцию списка, поскольку это повлияет на offset при нормальном режиме. Search и отфильтрованные данные у меня подгружаются без постраничной навигации, т.е. их количество ограничено
Бесконечный скролл напрямую зависит от скролла. Замечали сайты на которых открываешь в телефоне страничку с контактами, а там google map на всю высоту экрана? И не добраться до текста с телефонами ниже.. Бесконечный скролл, если он достаточно высокий, аналогично блокирует данные ниже себя.
Во-вторых, бесконечность дезориентирует и лишает контроля. Вы не знаете сколько вы уже проскроллили, дажа примерно глядя на размер бегунка, не можете понять сколько осталось до конца. Аналогично, вы не можете перепрыгнуть в "значительно более далёкие" версии данных.
В-третьих, вы не можете скинуть URL-ссылку на это место другому человеку и соответственно, если вы уйдёте с бесконечной ленты по ссылке и потом попробуете вернуться назад, вы попадёте на начало ленты. Т.е. такой скролл требует изменения поведения при навигации и пользователь должен открывать ссылки в новом окне.
Само собой, если у вас SPA, то могут быть проблемы с индексацией поисковиками. Гугл предлагает гибридный вариант, где данные таки будут доступны и по страницам тоже.
Наконец, если у вас что-то случилось с ajax-запросом и нет хорошей обработки ошибок, бесконечный скролл ломает глубину просмотра. А если сервер достаточно нестабильный, то риск что пользователь никогда не сможет добраться до нужных данных увеличивается с каждым запросом. Например если вероятность ответа сервера HTTP 500 оценивается в 5%, то после 15ти последовательных запросов вероятность что лента будет сломана уже (100 - 100 × 0.9515) = 55%.
Тем не менее, если вы делаете крутой динамический сайт, который может обновляется блоками с нестатичными размерами и с нестандартной навигацией вверх/вниз и вправо/влево, постепенная подгрузка musthave.
Interstellar, новый фильм Кристофера Нолана эпичен, масштабен, грандиозен и со своими чёрными дырами. В сущности фильм затрагивает три темы — любовь и эгоизм, судьбу человечества, время и смерть. Хорошо балансирует между космической одиссеей и опытом-чувствами, которые становятся большей ценностью для нашего общества. По качеству:
Хороший набор актёров
Отличный визуальный ряд
Классная, атмосферная музыка Ханса Циммера
Закрученный сюжет, хоть и со своими неровностями
Продуманная научная тема
Обязательно посмотрите, а теперь честность на 100% и сплошь спойлеры..
Часто сталкиваюсь с желанием поменять что-то на сайтах, добавив свой функционал, сделав их удобней или автоматизировав многие вещи. Во многих случаях это становится хорошим лайфхаком. К счастью для браузеров есть расширения, добавляющие возможность запуска user-скриптов. Для firefox это greasemonkey, а для chrome - tampermonkey.
Движки "обезьянок" позволяют подключить внешние библиотеки (например jquery) и напрямую манипулировать страницей. Это даёт широкий простор по изменению layoutа и взаимной интеграции сайтов. Например
Посмотрел вчера Lucy. Средне. Хорошие спец-эффекты, вставки как из Baraka под псевдо-философию, что-бы дать видимость глубины мысли, там где её нет. Дальше спойлеры.
Фильм об обожении, но как и большинство последних голливудских фильмов затрагивающих внутреннее развитие и трансцендентность, он слишком материалистичен. Главная героиня с одной стороны становится сверх-умна, теряет чисто человеческие качества ограниченности сознания (страх, боль, страдание), но при этом не отказывается от насилия и поглощения всего наркотика. Полное раскрытие разума даёт способности по управлению и пониманию всего вокруге, но не ведёт ни к чему продуктивному.
Если у вас есть веб-приложение и вы задались тем что-бы идеально его покрыть тестами, то вот что у вас должно быть:
unit-тесты бэкэнда — в основном покрываются модели, генерируется покрытие — получаете необходимость изолировать модели (заодно single responsibility principle выполняется)
unit-тесты frontend — карма + phantomjs прокрутят все ваши angular-сервисы и backbone-модели — тоже приходится изолировать код
e2e (сценарные, системные) тесты — наверняка основанный на selenium (protractor, selenide). Медленно тестируется функционал работающей системы из UI — приходится задумываться о том что пользователь вообще делает (use cases)
db/entity тесты миграций — "с нуля" запускают изменения в БД и когда всё готово - сравнивают с entity/record классами для синхронизации кода с БД (так находятся лишние свойства и недостающие )
Protractor — движок для запуска системных (end-to-end, браузерных) тестов. Внутри он использует selenium с драйверами для браузера (chromedriver), а сами тесты пишутся с синтаксисом jasmine. Про него и карму я уже писал, впрочем mocha и cucumber тоже поддерживаются.
Из особенностей - protractor имеет интеграцию с angular (находит модели и repeat-директивы) - отсюда и название слов (angle - угол, protractor - транспортир, т.е. угловая линейка), но может тестировать и любые другие сайты. Из недостатков -
Я когда-то писал про тестирование javascriptа с помощью jstestdriver , поскольку тот был первым интегрированным в PHPStorm, но инструменты развиваются и теперь для ангуляра есть простор для проверки качества кода.
Karma это консольный инструмент для запуска тестов согласно конфиг файлу. Тесты гоняются в разных браузерах, умеет следить за изменениями исходников и генерировать покрытие кода. Есть ещё protractor - близкий по смыслу но для системных тестов.
Jasmine это уже библиотека для тестирования, аналог Mocha.
Рамки (frames) всегда ограничивают. С Backbone и jQuery можно было писать код, который хочешь, где главное что-бы он работал. С Angular надо глубоко влазить в дебри настроек что-бы всё было по правилам. Это хорошо что приложение следует правилам. Плохо что документации слишком много. Почитайте описания ui-router или binding у директив!
Код связывающий DOM с логикой переместился полностью в html. Селекторы в стиле jquery практически не нужны. Это серьёзно ограничивает используемость фреймворка с существующими скриптами и работающими системами. Попытка соединить два мира и использовать из ng-контроллера каких-то jquery событий будет выглядеть как хак.
Все события вешаются на самом элементе. Это в чём-то хорошо (меньше кода в js), но с другой стороны это распыляет видимость всего приложения и сильней привязывает html к логике. Например теперь проще делать показывание элементов с ng-show, но когда у вас на элементе висит ng-class, ng-src, ng-repeat, ng-click и ещё с десяток директив, в том числе самописных, то становится уже сложно не то что читать html, но и понять состояния к которым элемент может прийти или порядок вызова событий.
Проблема обновления DOM никуда не пропала. Модели это обычные объекты в которых спрятан параметр $$hashKey, по которому и привязывается двустороннее обновление DOM. Проблема в том что своей логикой вы можете модели поломать так, что они лишатся этого ключа и тогда DOM останется жить своей жизнью, а вы не заметите этого. Такой баг отловить сложней всего.
Чаще всего потеря случается из-за того что у вас есть локальный кэш данных в памяти (модель), который асинхронно обновляется через $http, но вы используете присваивание для обновления.
Например - в контроллере вы привязали view к кешированным данным из сервиса
Это из-за того, что в javascript данные передаются по ссылке (by reference), но если вы присваиваете новое значение, то старое значение (которое присвоено во view) в памяти то останется, но создадутся и новые данные. Поэтому правильно использовать angular.copy:
angular.copy(response.user, _localUserCache);
Второй частый источник батхёрта с bindом - удаление элементов из коллекций. Их надо делать с помощью array.slice(i), а не с присваиванием undefined или delete ключевым словом
Escaping в ангуляре встроен - вы не можете из фильтра просто так выплюнуть HTML. Это с точки зрения безопасности хорошо, но с другой стороны - долпонительная проблема. Надо использовать $sce.trustAsHtml(). Но я даже не на это жалуюсь..
Допустим у вас есть переводы как фильтр. Вы хотите в переводы вставить ссылку.. то как вы её передадите?
Замечаете escaping кавычек? А это внешний, чистый JSON. В общем случае escapingа кавычек внутри реального HTML - беда. Т.е. я себе представляю это так.. но на самом деле оно работать не будет:
Наконец Angular никак не развивает язык программирования ради языка. Вам не нужны классы, прототипы, иерархии, наследования, публичные-приватные методы. Подключи зависимости, внедри scope и нужные сервисы и пиши контроллер. Это своего рода деградация ради эффективности.
Из плюсов - мне понравились директивы. То что ты можешь определить визуально изолированный элемент как кирпичик даёт удобство для компоновки всяких wysiwyg, комбобоксов, календариков и тп. Понравилась магия с автообновлением view-моделей, с переводом на лету и с dependency-injection компонентов просто по имени сервиса.