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

Идеальная функция setCookie

Сравнение двух интересных библиотек (http://microbasejs.ru/, http://github.com/Kolyaj/CrossJS) привело меня к озарению и созданию совершенно особой, доселе невиданной функции по установке куки (с помощью javscript, ясно море). До сих пор (то есть примерно с неделю времени) я пользовался в последних практических разработках функцией из CrossJS. Потому что она принимала «простые» параметры: имя, значение куки и дату «сгорания» (expires). Причём, не дату окончания действия куки, а время действия в днях, что очень удобно и явилось решающим преимуществом (плюс к простоте параметров). Единственное, что пришлось добавить, – обработку значения escape'ом.

Но про вариант с microbasejs я тоже не забывал, он привлёк меня своей «правильностью», исчерпывающей полнотой. Там в параметре attrs (объект) содержится набор из всех (полагающихся по протоколу) path, domain и secure:

Эта идея – передавать параметры в объекте – хороша двумя свойствами: во-первых, удобно чисто зрительно (нет длинной строки параметров при вызове функции), и, во-вторых, именованные параметры защищают от глупых ошибок, вызванных не тем порядком параметров. То есть идея перспективная. Но функция вызывает желание кое-что доработать. Например, вместо проверки условия if (name) сделать наоборот. Или упростить проверку типа attrs. Ну, и раз уж мы используем join, то, может быть, и написать прямо в нём разделитель '; ' один раз, а не повторять в каждой строке, приблизительно так:

Проверка name и приведение attrs к объекту – это «сглаживающие» действия, позволяющие функции принимать до некоторой степени неправильные параметры. Нужно ли так делать – отдельный вопрос, зависящий от идеологии, наверное, уже не библиотеки, а конкретного проекта. Ну, в исходной функции они были (а во второй, кстати, библиотеке – CrossJS – нет, т.е. там можно передавать функции, например, пустой name, и функция спокойно создаст куки с именем "undefined"), а я их только немного переделал в духе минимализма, сокращения кода.

Далее, сделаем наконец «по максимуму» обработку параметра «время действия куки». Это единственный параметр, который надо обрабатывать в данной функции особо. Ведь очень удобно указывать время действия куки в днях. Но в идеале функция должна принимать и простой параметр «дата окончания действия в готовом формате», то есть строку вида "Sun, 02 Oct 2011 14:12:59 GMT". А ещё могут быть случаи, когда время действия появляется сразу в миллисекундах (в начальном варианте _0 допускается именно такой случай, без «дней»).

В любом случае ясно, что нужно рассматривать значение в качестве интервала (времени действия), если параметр является числом, иначе это дата окончания в готовом формате. Можно определять, что именно обозначает интервал – дни или миллисекунды – по значению: если число больше 5000, будем считать его миллисекундами (ведь вряд ли кому-то может понадобиться ставить куки меньше, чем на 5 секунд, или больше, чем на 5000 дней):

Разобравшись с этим «исключительным» атрибутом, мы можем наконец-то использовать во всю дурь применить по истинному назначению параметр-объект. Ведь улучшение внешнего вида функции и снижение риска мелких ошибок – это только побочные эффекты использования объекта в параметрах. Главное преимущество объекта в том, что он позволяет не перечислять явно свои элементы, вообще не писать строки вида '; domain=' + attrs.domain. Это принципиально упрощает код, и принципиально меняет идеологию получения параметров (в нашем случае – параметров куки):

И не нужно никаких проверок на существование свойств: если свойства (например, secure) нет в объекте, оно и так не попадёт в куки (secure не будет получено с помощью конструкции in attrs и не будет подрисовано в строку). И последний барьер: почему параметры name и value у нас отдельно, а не в общей куче? Я вижу сейчас из текста функции только одну причину – escape (value обрабатывается, а остальные параметры нет). С помощью маленькой хитрости можно в цикле получения свойств for (id in ...) особо обработать первую пару объекта, и функция станет предельно идеальной (если так можно сказать):

Да. Один массив на входе для функции установки куки – это супер круто. Но на практике работать с таким массивом (точнее, объектом) замучаешься. Именно потому что он не совсем массив – в нём нельзя поменять первую пару «имя – значение». Ведь в чём удобство набора параметров для функции? Правильно, в том, что его можно хранить.

Можно вообще определить заранее, для каких-то неизменных в текущем скрипте свойств (время жизни, путь...), создать в начале работы объект вида var c_params = {path:'/', expires:30}, а потом, перед конкретным вызовом уточнять (например, добавить domain). Но добавить новую пару «имя – значение» именно в начало объекта (как требует функция!) невозможно. Да и нужно будет удалять какое-то неизвестное в текущем контексте свойство (имя куки), оставшееся от предыдущего вызова. Поэтому формирование объекта-параметра в этом случае превратится в гемор, который уничтожит всю крастоту самой функции:

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

Напоследок пара пояснений. Можно вызывать функцию вообще без атрибута-объекта, если сохранить предварительно этот объект (с общими для проекта свойствами) в переменной window.c_attrs. И совершенно не обязательно здесь window, здесь может быть this или ещё что-то, по условной договорённости (window выбрано для простоты и наглядности).

Как удалять куки? Это больше уже философский вопрос – надо ли писать отдельную функцию deleteCookie. В библиотеке CrossJS такая функция есть, в microbasejs – нету. Мы присоединяемся ко второму. Если бы такая функция у нас была, она бы выглядела примерно так:

Так зачем, спрашивается, оборачивать строку setCookie(name, '', {expires:-1}) в ещё одну функцию? Такое действие предполагает, что разработчик не знает о принципах работы куки. Вот это и есть философский вопрос: должен ли разработчик знать об этих принципах? Отрицательный ответ ставит нас на путь jQuery, в конце которого находится идеал – максимально удобно, без всяких лишних усилий залезть в любую php-шную ж--у. Это не наш идеал, это идеология врагов коммунизма.

D.M., admin

Комментарии

лесник 05.10.2011 16:22:11

Всё-таки я кое-что упустил. Например, конечная дата действия куки может передаваться не в виде строки, а в виде объекта Date. Да и вообще, с этим 5000 как-то очень уж «условно» получается. Попробую ещё раз всё просчитать.

Для установки времени действия куки мы имеем (или они нас) следующие варианты: интервал (1), точка сгорания (2). Интервал может быть в днях (1.1) и, как я предполагал, в мс (1.2) – если надо куки, например, меньше дня. Точка сгорания (собсно expires) может быть как объект Date (2.1), или как строка, как я предполагал, только GMT (2.2), но на самом деле м.б. какая попало строка (2.3), или ещё примитивное значение Date, т.е. число мс (2.4). Да. И ещё какой-нибудь вредитель, использующий библиотеку, все числа может брать в кавычки.

Нормальная работа с объектом-параметром attrs требует конечного expires в виде строки – даты GMT, которую можно получить из объекта Date. То есть всё многообразие вариантов мы должны будем привести к Date -> GMDate. Какая попало строка вынуждает использовать Date.parse. Возможность примитивного значения (числа) требует «приводить» это значение через new Date(значение). При этом значение вполне может быть и объектом Date, но это, как увидим дальше, не имеет значения ;-).

Мы не можем проверять, строка ли в expires, с помощью typeof value = 'string' из-за предполагаемого придурка, взявшего все числа в кавычки. Наиболее простой способ, который я тут вижу, – Number(value) или isNaN(value), объект-дату он также корректно переведёт в число мс. Если оно есть (не false), пихаем его в new Date(). Если !Number(value), тогда Date.parse(value). Если число будет ноль, будет !Number(value), и Date.parse вернёт неправильную дату. Нефиг пихать в дату число ноль!

И это всё только о (2); а если придёт интервал, надо обрабатывать как-то по-другому. А главное, как теперь узнать, что это интервал, а не точка конечного срока? Видимо, с помощью специальных слов: days, sec... Если придут параметры с такими названиями, перерабатывать их для expires и потом уничтожать. Ну, sec или, допустим, часы или минуты – это уже тоже перебор. Это, в общем, несложно добавить в функцию по аналогии с attrs.days.

Объект attrs может выглядеть примерно так:

attrs = {path:'/', days:30}

или так:

attrs = {path:'/', domain:'sample.ru', expires:'Sun, 30 Oct 2011 14:12:59 GMT', secure:''}

Пример вызова функции:

setCookie ('a', 1, {days:30})