А еще мне нужно было сделать так, чтобы долгоиграющий Rake-скрипт не запускался повторно (по крону). Я это сделал с помощью PID-файла, хотя не исключаю, что есть варианты и попроще.
В начале задачи – проверяем, запущена ли она уже, и если нет, создаем PID-файл.
pid_filename = "#{RAILS_ROOT}/tmp/#{task.name.gsub ':', '.'}.pid" # или любой другой
if File.exists?(pid_filename) && system("kill -0 #{File.read(pid_filename).to_i}")
fail "Task already running"
else
File.open(pid_filename,'w') {|f| f.write(Process.pid) }
end
В конце задачи – удаляем PID-файл:
File.unlink(pid_filename) rescue nil
Я тут занялся оптимизированием RentFeed, где большую часть страниц можно положить в кеш и обновлять несколько раз в день. Самый тупой и быстрый кеш в виде статических файлов, отдаваемых непосредственно апачем.
Рельсы умеют такое делать «из коробки», практически одной строчкой:
class SomeController < ApplicationController
caches_page :index
def index
end
end
Все! Кеш уже работает. Правда, почему-то рельсы не предусматривают две вещи. Кеш не работает с GET-параметрами – раз. Хранится прямо в public – два.
GET-параметры
Тут все просто: хочешь, чтобы для разного набора параметров кешировались разные страницы – помещай параметры в путь. Кстати, к таким ссылкам и Google лучше относится.
Для меня это в первую очередь касалось постраничной разбивки. Если ты используешь will_paginate, то тебе достаточно вынести URL с номером страницы в таблицу роутов:
map.resources :items
map.paged_items '/items/page/:page', :controller => 'items', :action => 'index'
Перемещаем кеш в более удобное место (например, в public/cache)
Путь к кешу указывается в конфиге:
# config/environments/production.rb
config.action_controller.page_cache_directory = RAILS_ROOT+"/public/cache/"
Осталось научить Apache забирать кеш из нужного каталога. Решается это довольно простыми реврайтами, не считая того, что отдавать нужно только файлы, которые точно лежат в кеше – иначе Rails станет получать неправильные пути и перестанет работать.
В общем, нужен вот такой .htaccess:
# public/.htaccess
RewriteEngine On
# убираем слеш в конце пути
RewriteCond %{REQUEST_URI} ^([^.]+)/$
RewriteRule ^[^.]+/$ /%1 [QSA,L]
# перенаправляем в кеш, только если там есть файл
RewriteCond %{THE_REQUEST} ^(GET|HEAD)
RewriteCond %{DOCUMENT_ROOT}/cache/%1 -f
RewriteRule ^.+$ /cache/%1 [QSA,L]
# то же для страниц без расширения (к ним Rails приписывает .html)
RewriteCond %{THE_REQUEST} ^(GET|HEAD)
RewriteCond %{REQUEST_URI} ^([^.]+)$
RewriteCond %{DOCUMENT_ROOT}/cache/%1.html -f
RewriteRule ^[^.]+$ /cache/%1.html [QSA,L]
# то же для пустого пути
RewriteCond %{THE_REQUEST} ^(GET|HEAD)
RewriteCond %{DOCUMENT_ROOT}/cache/index.html -f
RewriteRule ^$ /cache/index.html [QSA,L]
Теперь можно очищать весь кеш вместе обыкновенным rm -rf public/cache/*.
В очередной раз набирая те шесть строчек, которыми – по соглашению – нужно описать таблицу для связи «многие ко многим», я подумал: а почему в Rails нет готового метода для этого? Почему каждый раз надо переписывать один и тот же код, попутно вспоминая, как сделать таблицу без ID – ведь используются они только для HABTM?
Подумал и написал:
# config/initializers/create_habtm_table.rb
class ActiveRecord::Migration
# create_habtm_table :posts, :tags
def self.create_habtm_table(first_model,second_model)
table_name = [first_model.to_s,second_model.to_s].sort.join('_').to_sym
create_table table_name, :id => false do |t|
t.belongs_to first_model.to_s.singularize.to_sym
t.belongs_to second_model.to_s.singularize.to_sym
end
add_index table_name, (first_model.to_s.singularize+'_id').to_sym
add_index table_name, (second_model.to_s.singularize+'_id').to_sym
end
# drop_habtm_table :posts, :tags
def self.drop_habtm_table(first_model, second_model)
table_name = [first_model.to_s,second_model.to_s].sort.join('_').to_sym
drop_table table_name
end
end
Вообще задача такая:
class Book < ActiveRecord::Base
has_many :ratings
has_many :tags, :through => :ratings
end
class Rating < ActiveRecord::Base
belongs_to :book
belongs_to :tag
validates_presence_of :score
end
Надо возвращать для каждой книжки не только все ее теги, но и сумму оценок, поставленных для каждого тега.
Ущербные решения
Затык был не в том, как закодить саму выборку суммы, а в каком виде возвращать результат. Первая мысль – массивом хешей типа {:tag => tag, :score => score}. Мало того, что это попахивает PHP, так с такими хешами еще и неудобно работать. Потом я хотел сделать total_score методом Tag:
class Tag < ActiveRecord::Base
def total_score(book)
return score_computed_some_way
end
end
@book.tags.first.total_score(@book)
Но это тоже бредовая тавтология, как по мне.
Расширение Ruby-объектов на уровне объекта
Итак. Есть такой метод Object#instance_eval – он позволяет запихнуть в объект практически все, что угодно. (Если бы что-то угодно было чем-то более сложным, чем возвращение объекта, можно было бы вынести его в модуль и подключать через Object#extend)
# scores = {tag_id => score, ...}
def inject_scores_into_tags(tags, scores)
tags.each do |tag|
tag.instance_eval do
@total_score = scores[tag.id]
def total_score
@total_score
end
end
end
end
После этого у тегов появляется метод total_score.
Вот, давно хотел выложить, но она раньше выглядела еще ужаснее, чем сейчас.
Идея проста: ты даешь ей список прокси-серверов в текстовом файле, она отвечает, какие из них вообще доступны (и отдают правильную страницу), какие анонимные (не открывают твой IP), какие – неанонимные.
Легким прикосновением напильника можно довести ее до нужного в каждом конкретном случае результата.
http://github.com/leonid-shevtsov/proxytools
Продолжаю цикл статей о взаимодествии с архаичными сайтами и их же поддержке.
Пришел к выводу, что удобнее всего производить изменения (крупные, ибо мелкие удобнее в GEdit по gvfs) посредством rsync. То есть, слил код сайта себе на машину, доработал, залил обратно. Когда-то я это делал вручную, по одному файлу, что было сложно и опасно. Тем, кто делает так до сих пор, посвящается статья.
Нужен будет ssh-доступ на искомый сайт.
читать дальше →
Для меня одним из самых проблемных мест Javascript было управление переменными. Излагаю простым русским языком.
Области видимости переменных
Переменные в Javascript бывают глобальными и локальными. Глобальная переменная доступна везде, локальная – только в текущей области видимости.
Технически, глобальные переменные – всего лишь свойства объекта window, поскольку весь код выполняется в его контексте.
<script>
alert(location); // сообщит window.location
</script>
Из этого следует, что глобальные переменные могут затирать свойства window (я уже молчу о том, что они зло, нарушают инкапсуляцию и все такое).
Объявление переменных
При присвоении значения неопределенной локальной переменной используется или создается глобальная переменная.
function foo() {
a = 2;
b = 3;
return a+b;
}
alert(a); // undefined
a = 'очень важное значение';
alert(a); // очень важное значение
foo();
alert(a); // 2
Таким образом можно легко затереть лишнего. По-моему такое поведение абсолютно нелогично, но, что ж, это не самое странное место яваскрипта. В любом случае неявного определения переменных стоит избегать.
Явно объявлять переменные можно и нужно ключевым словом var.
Такая строка всегда создает новую локальную переменную. Если объявление происходит вне функций, то она будет глобальной, что вполне логично.
function foo() {
var a = 2;
var b = 3;
return a+b;
}
alert(a); // undefined
var a = 'очень важное значение';
alert(a); // очень важное значение
foo();
alert(a); // очень важное значение
Как объявить глобальную переменную из функции? Как обратиться к глобальной переменной, если есть локальная с таким же именем? Очень просто – нужно обратиться к ней как к свойству window:
function foo() {
var location = 'location';
alert(location); // вернет 'location'
alert(window.location); // вернет window.location
window.a = 'переменная из функции';
}
alert(a); // undefined
foo();
alert(a); // переменная из функции
Наследование области видимости
Меня всегда смущало то, что в Javascript можно определять функции внутри функций, а использовать их потом где угодно.
Переменные при этом передаются очень просто: если на момент определения функции переменная существовала, то она будет существовать и внутри функции. Откуда бы ее не вызывали.
function alertOnTimeout(message, timeout) {
return setTimeout(function() {
// message будет доступен в безымянной функции, переданной таймауту
alert(message);
}, timeout);
}
Передача кода по старинке – строкой, которая прогоняется через eval() – не попадает под это правило, код исполняется в той области видимости, где и определен.
Поскольку объекты в Javascript – это тоже типа функции, то свойство объекта определяется точно так же, как и переменная.
function myObject() {
var property = 0;
// Cамо собой, property будет доступно только внутри объекта.
}
А еще в Javascript область видимости переменной ограничивается только функциями, а не блоками типа if (привет, Паскаль). Потому удобнее всего объявлять переменные в начале функции.
this
А что this? А то, что эта переменная автоматически появляется в методах объектов и затирает значение this из предыдущей области видимости. Решение простое – переприсваивать ее значение другой переменной.
$('div.with-links').click(function() {
var theDiv = this; //сохраняем значение this
$(this).find('a').click(function() {
alert($(this).attr('href')); // this - это ссылка
theDiv.remove(); // а theDiv - это все еще дивак
});
});
Отдельно замечу, что при оборачивании какой-то поведенческой логики в объект надо помнить, что в создаваемых DOM-событиях значение this самого объекта теряется.
function myObject() {
var _this = this; // сохраняем ссылку на родительский объект
var linkRemoved = false;
$('a').click(function() {
$(this).remove(); // this - это объект ссылки
_this.linkRemoved = true; // _this - это родительский объект
});
}
Во-первых, я добрался до сессий в виме – теперь не приходится переходить в каталог проекта вручную. Пригодился плагин SessionMan.
Во-вторых, раз такое дело, надоело открывать vim и терминал двумя командами. Придумал открывать терминал из вима. Делается это так:
" open new terminal on keystroke
map <F5> <esc><esc>:silent !/usr/bin/x-terminal-emulator<CR>
Кроме того, зачем открывать терминал чтоб набрать там script/console (который у меня сам по себе сокращен до sc)?
" open script/console on keystroke
map <F6> <esc><esc>:silent !/usr/bin/x-terminal-emulator -e script/console<CR>
Само собой, таким образом можно открывать что угодно.
silent не просит после выполнения команды нажать ENTER
/usr/bin/x-terminal-emulator, в отличие от sh, который в gvim практически не работает, открывает терминал в новом окне.
- ключ
-e выполняет в терминале какую-нибудь команду и закрывается после ее завершения.
Если на работу не берут – твоя проблема не в том, что на дворе кризис, рабочих мест мало, что у тебя плохое резюме, что ты мало вращался в каких-то там «кругах», прочитал мало книжек или плохо учился (ничему не учили?) в универе.
Тебе просто нужен практический опыт. Практический. Опыт.
Бери свою фантазию в руки, сам придумай себе проект и реализуй его самостоятельно. Или клонируй чужой, если фантазии не хватает (а вообще я не представляю, куда без нее в программисты).
Просто привыкай проектировать и писать код. А потом иди на собеседование другим человеком. С опытом.
Не надейся, что подвернется случай или что опыт придет сам собой. Не ищи помощи на фрилансе – там на начальном уровне проекты скучные и унылые. Не участвуй в опенсорс-проектах – только тебя там не хватало.
Удачи.
UPD: Мое плохое отношение к опенсорсу основывается на большом количестве людей, которые делают из программирования не за деньги, а ради удовольствия какую-то религию, а из себя, соответственно, мучеников за идею и борцов за свободу.
До недавних времен первым делом после создания git-репозитария я добавлял в .gitignore разнообразные служебные и резервные файлы, которыми IDE и редакторы засоряют проект.
Так вот, сделать это можно раз и навсегда, поскольку git поддерживает глобальный .gitignore.
Сотоварищей он не спасет. По-моему это вполне законно – инструментарий у каждого свой, поэтому и резервные копии у каждого свои.
Так вот, сначала сообщаем гиту о наличии глобального файла .gitignore:
git config --global core.excludesfile ~/.gitignore
…а потом отправляем в ~/.gitignore список ненавистных файлов. У меня такой:
*.*.sw*
*~
*.log
Thumbs.db
nbproject
Подробнее о gitignore