Генератор вкусных рецептов на React.js

November 27, 2014

Чтобы продемонстрировать библиотеку React.js, которая мне очень нравится, я сделал простой интерфейс для создания вкусных пошаговых рецептов.

По ссылке находится живая дема , исходники на Гитхабе и ниже можно посмотреть результат работы:

В React.js интересно обрабатываются действия пользователя. У объекта Item что-то меняется (в нашем случае пользователь нажал на кнопку удаления). Item рапортует об изменении родителю, который в свою очередь передает информацию главному объекту, и он уже помечает изменение в состоянии с помощью this.setState. Далее реактивная магия обновляет виртуальный и реальный DOM без нашего участия в удобное для себя время.

Хотя каждый компонент может содержать в себе состояние, у меня почему-то вся изменяемая часть всегда собирается в главном объекте. Он из-за этого распухает, но все равно мне кажется, что код на React.js гораздо легче писать и поддерживать, чем аналогичное решение, допустим, на jQuery.

Как работает be_some в RSpec?

November 24, 2014

Как-то раз я решил разобраться какая именно магия используется в RSpec-предикаторах be_some, так как на первый взгляд они напоминают undefined local variable or method.

class Sun
  def available?
    false
  end
end

describe Sun do
  it "sad and cold" do
    expect(Sun.new).not_to be_available
  end
end

Конечно же это method_messing. Продираться через все хитросплетения RSpec мне не захотелось, хотя код там очень основательный и щедро документированный, я нашел лишь несколько ключевых точек.

describe Sun превращается в класс RSpec::ExampleGroups::Sun, наследованный от RSpec::Core::ExampleGroup, в который при инициализации подмешивается модуль RSpec::Matchers , отлавливающий be_* с помощью method_messing .

module RSpec
  # ...
  module Matchers
  # ...

    BE_PREDICATE_REGEX = /^(be_(?:an?_)?)(.*)/
    HAS_REGEX = /^(?:have_)(.*)/

    def method_missing(method, *args, &block)
      case method.to_s
      when BE_PREDICATE_REGEX
        BuiltIn::BePredicate.new(method, *args, &block)
      when HAS_REGEX
        BuiltIn::Has.new(method, *args, &block)
      else
        super
      end
    end

Отлично, теперь можно спать спокойно.

Неведомый Array#find

November 20, 2014

Полагаясь на интуицию, как делаю в 90% случаев, программируя на Руби, я написал проверку наличия элемента в массиве array.find(4) ? 'success' : 'fail' и с удивлением узнал, что это не работает.

Документация нам подсказывает , что find вернет Enumerator если блок не задан.

[1, 2, 3].find(4)  # =>  #<Enumerator: [1, 2, 3]:find(4)>

Любой объект в руби - это true, поэтому конструкция из вступления будет всегда возвращать success. Есть несколько способов исправить ситуацию.

Использовать блочную версию find (не очень изящно, но работает):

[1, 2, 3].find { |x| x == 4 }  # =>  nil

Использовать метод index или include? (я так делаю, когда пишу на чистом руби):

[1, 2, 3].index(4)     # =>  nil
[1, 2, 3].include?(4)  # =>  false

В Рейлс (или когда просто подключен ActiveSupport) еще есть метод #in? , позволяющий иногда сделать код более читаемым:

array = [1, 2, 3]
4.in?(array)       # =>  false

На один неправильный вариант использования find, есть много правильных.

Почему я не меняю схему и данные в одной миграции

November 17, 2014

В Рейлс есть метод #reset_column_information, который я больше никогда не использую.

История

Допустим мы добавили колонку в таблицу users и тут же решили ее посчитать. А пользователей у нас много, и расчеты сложны (миграция написана как предлагается в документации ):

class SlowMigration < ActiveRecord::Migration
  def up
    add_column :users, :some_awesome_column, :string

    User.reset_column_information

    User.find_each do |user|
      user.some_awesome_column = ... # Сложные вычисления
    end
  end

  # ...
end

Далее деплоим новую версию приложения на хероку git push heroku master, юникорны начали перегружаться, и следом запускаем миграции heroku run rake db:migrate. Каждая миграция выполняется в отдельной транзакции, add column превратится в ALTER TABLE. Опуская детали, процесс выполнения миграции выглядит так:

SQL: BEGIN      # <-- начало транзакции

== 20141112130023 SlowMigration: migrating =========================
-- add_column(:users, :some_awesome_column, :string)

# новая колонка
SQL: ALTER TABLE "users" ADD COLUMN "surname" character varying(255)

   -> 0.0024s

# <-- здесь мы долго обновляем всех пользователей

Теперь на наш свежеперезапущенные юникорны приходят пользователи, у которых мы проверяем авторизацию User.find.... Чтобы работала магия ActiveRecord при первом обращении к модели запрашивается схема таблицы users. В Postgresql используется следующий запрос :

SELECT a.attname, format_type(a.atttypid, a.atttypmod),
  pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod
FROM pg_attribute a LEFT JOIN pg_attrdef d
  ON a.attrelid = d.adrelid AND a.attnum = d.adnum
WHERE a.attrelid = '"users"'::regclass
  AND a.attnum > 0 AND NOT a.attisdropped
  ORDER BY a.attnum

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

Я промоделировал проблему в консоли, но рейлс приложение залипает абсолютно так же.

Выводы

Метод #reset_column_information сам по себе хороший, но шаблон использования, который он предлагает, может испортить выкатку на Хероку. Поэтому я больше не меняю схему и данные в одной транзакции.

Комментарии Дискуса на телефоне

November 13, 2014

Я люблю сайты, которые хорошо отображаются на телефоне. Когда они плотно вбиты в ширину экрана и вверх-вниз скролятся, а вправо-влево нет.

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

  <body onorientationchange="if(DISQUS) DISQUS.reset({reload: true});">

Не помню где этот рецепт подсмотрел, но теперь скролл на телефоне отличный.

Что означает расширение .ru для конфига rack-приложения

November 10, 2014

Я задумался, что означает расширение .ru у файла config.ru и почему приложение не запустится, если поменять расширение на .rb.

Christian Neukirchen, автор оригинального кода запуска rack-приложения, сказал, что это сокрашение от Rack Up. Отдельное расширение он завел, чтобы люди не пытались запустить конфиг с помощью обычного руби.

Так же, изучая исходники, я узнал несколько других секретов rack.

Параметры по умолчанию в config.ru

В файле config.ru можно указать параметры по умолчанию, начав первую строку с символов #\.

#\ -p 3000
run proc { |env| [ 200, {'Content-Type' => 'text/plain'}, ["hello"] ] }

Такое rack-приложение запустится на 3000-м порту, вместо 9292.

Что такое метод run в config.ru

#run это метод класса Rack::Builder , так как конфиг файла для rackup исполняется в контексте Rack::Builder c помощью eval .

Что будет если передать rackup обычный rb-файл

Rackup возьмет класс, который совпадает с именем файла и запустит его как rack-приложение .

# app.rb
# 
# Usage: rackup app.rb

class App
  def self.call(env)
    [ 200, {'Content-Type' => 'text/plain'}, ["hello"] ]
  end
end

Теперь файл config.ru, который лежит в корне каждого Рейлс проекта, больше не вызывает у меня вопросов.

Bash Prompt, проверенный временем

November 6, 2014

Подозреваю, что многие меняли внешний вид строки приглашения (так же переводится prompt, правильно?) в консоли много-много раз.

После долгих экспериментов я остановился на таком:

# http://stackoverflow.com/a/1862762
function timer_start {
  timer=${timer:-$SECONDS}
}
function timer_stop {
  timer_show=$(($SECONDS - $timer))
  unset timer
}
trap 'timer_start' DEBUG
PROMPT_COMMAND=${PROMPT_COMMAND}timer_stop;

## http://railstips.org/blog/archives/2009/02/02/bedazzle-your-bash-prompt-with-git-info/
function parse_git_branch {
  ref=$(git symbolic-ref HEAD 2> /dev/null) || exit
  echo "["${ref#refs/heads/}"]"
}

PS1="\[\e[0;33m\]\w\[\e[0m\]{\${timer_show}}(\$(ruby -v | cut -d' ' -f2))\$(parse_git_branch)$ "

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

В собранном виде это выглядит так:

Симпатично.

Object as a Service

October 30, 2014

Мне нравится концепция сервис объектов для гурманов, на работе мы её часто используем.

Оригинальный материал можно найти по ссылке , у нас же прижились два правила:

  1. У сервисного объекта есть метод call (обязательно).
  2. Название начинается с глагола (за этим не следим, но так само получается).

В проекте среднего размера сервисные объекты хорошо заменяют архитектуру, организуя код в компактные классы. Приведу пару примеров:

ООП:

class AuthenticateUser
  def self.call(*args)
    new(*args).call
  end

  # NOTE: пользуемся keyword arguments
  # http://brainspec.com/blog/2012/10/08/keyword-arguments-ruby-2-0/
  def initialize(cookies, user, permanent: true)
    @cookies = cookies
    @user = user
    @permanent = permanent
  end

  def call
    if @user == "admin"
      set_cookie("admin")
    elsif @user.is_a?(Customer)
      set_cookie("c-#{@user.id}")
    elsif @user.is_a?(Reseller)
      set_cookie("r-#{@user.id}")
    else
      raise "unknown user: #{@user.inspect}"
    end
  end

  private

  def set_cookie(value)
    if @permanent
      @cookies.signed.permanent[:uid] = value
    else
      @cookies.signed[:uid] = value
    end
  end
end

Простой сервисный объект:

class GetCurrentUser
  def self.call(cookies)
    if cookies.signed[:uid] == 'admin'
      'admin'
    else
      user_type, user_id = cookies.signed[:uid].to_s.split('-', 2)
      if user_type == 'c'
        Customer.find(user_id)
      elsif user_type == 'r'
        Reseller.find(user_id)
      end
    end
  end
end

Сервисными объектами легко думать. Когда я вижу запись AuthenticateUser.call(cookies, @user), сразу догадываюсь, что это сервисный объект, у которого одна единственная обязанность - логинить пользователя. Не заглядывая в код ясно, что там все в порядке.

Лок на запись

October 27, 2014

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

В PostgreSQL есть замечательный способ блокировки SELECT FOR UPDATE, который мне как-то подсказал Иван. Если запустить 2 транзакции и в обеих вызвать SELECT FOR UPDATE, то вторая транзакция будет ожидать окончания первой.

В Рейсл такие запросы удобно упакованы в методе with_lock .

class User < ActiveRecord::Base
  def do_some_tricky_calculations
    with_lock do
      self.name = "Tricky Dude"
      # update a lot different records
      save!
    end
  end
end

В логах мы увидем такой же FOR UPDATE, как мы это делали вручную:

Как и в любой хорошем приеме, здесь нужна мера. Если блокировать базу часто и надолго, то производительность упадет сильно.

Синатра инлайн-темплейты

October 23, 2014

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

Любой текст, который встретится в руби-скрипте за маркером __END__ проигнорируется во время выполнения, но будет доступен, через IO-объект DATA. В блоге Causis Theory можно найти несколько любопытных примеров использования этой фичи , самый приличный из которых, как мне кажется:

DATA.each_line.map(&:chomp).each do |url|
  `open "#{url}"`
end

__END__
http://google.com/
http://yahoo.com/

Во фрейморке Синатра пошли еще дальше и предложили использовать подвал скрипта для внедрения именованных вьюх. Этот прием называется Inline Templates и позволяет, ну вы сами видите, что он позволяет делать:

require 'rubygems'
require 'sinatra'

get '/' do
  erb :index
end

__END__

@@ layout
<html>
  <body>
    <%= yield %>
  </body>
</html>

@@ index
<div>Hello!</div>

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