Что делает холодным воскресным утром нормальный человек? Любой вам ответит: холодным воскресным утром человек спит. Потому что всю неделю он работал и хочет отдохнуть.
Что делает холодным воскресным утром программист? Холодным воскресным утром программист пьёт горячий чай и пишет код. Чай он пьёт, потому что утро холодное, да и проснулся ещё не до конца, а код пишет, потому что хочется. Программисту всегда хочется писать код, только в будни он пишет код за деньги и от этого очень устаёт, а в выходные для себя, поэтому отдыхает.
Этим утром мы будем писать наше первое приложение для Ocsigen. Желающим неплохо бы сначала ознакомиться с официальным мануалом, впрочем, на многое надеяться не стоит, потому что мануал недописан, пестрит недоуменными строками а-ля "??????" и нецензурной речью на французском. Поэтому основным мануалом буду я.
Как вы возможно помните, когда-то мы писали интерпретатор языка Йоба. С тех пор интерпретатор был незначительно улучшен, выделен в отдельный класс, стал принимать строку на вход, отдавать строку на выход (вместо работы с консолью). Теперь нашей задачей станетвнедрение Йобы в качестве основного языка компании Google превращение интерпретатора Йобы в веб-приложение, да не простое — а клиентское. Хоть я и добавил в класс счётчик операций, чтобы нельзя было слишком обнаглеть, но всё равно — пусть пользователь на своём компьютере вычислительные мощности тратит, а не на сервере.
Для начала, нам надо установить сервер ocsigen. Поскольку 2.0 для дистрибутивов собрать ещё не успели, мы последуем вот этой инструкции и установим сервер бандлом в наш домашний каталог. Чтобы бандл получился правильным и вкусным, перед запуском make отредактируем Makefile.config и пропишем там:
Ocaml и Findlib собирать не будем, они и так есть в репозиториях. O'Closure нам в этот раз не понадобится, но мы его на всякий случай соберём — чтобы потом не пересобирать оксиген, если вдруг вы сами заинтересуетесь им или захотите от меня статью.
Следующим пунктом сразу отредактируем файл ${HOME}/bin/ocsigen/etc/ocsigenserver/ocsigenserver.conf: убедимся, что там прописаны подходящие порт, а также имя и группа пользователя, от которого стартовать. Теперь пришло время подготовить конфиг для будущего сайта. Создадим ${HOME}/bin/ocsigen/etc/ocsigenserver/conf.d/yoba.conf и наполним его содержимым:
Кратко о содержимом:
Ура! Переходим к написанию кода.
Создаём пресловутую папку /home/username/yoba/ и скачиваем архив с языком Йоба — сам язык мы уже когда-то написали, а как его чуть-чуть подпилить и превратить в класс, нам неинтересно, потому возьмём сразу готовое. Распаковываем в ту же папку и качаем туда же стандартные Makefile.config и Makefile.rules — сборка проекта дело непростое, с нуля мэйкфайл фиг напишешь.
Настало время кое-что поправить и сразу узнать о первых синтаксических новшествах: в Eliom код можно размещать в секции. Секция {server{… }} (то же самое, что код просто без секции) компилируется и исполняется на сервере, секция {client{… }} — на клиенте, а {shared{… }} — доступна и там, и там.
Поскольку наш интерпретатор будет работать на клиенте, мы переименовываем файл yobaLang.ml в yobaLang.eliom, открываем его, и в самом начале добавляем строку "{client{", а в самом конце заменяем ";;" (две точки с запятой — это конец инструкции только на верхнем уровне кода, внутри секции их использовать уже нельзя) на "}}". Генерируемый ocamllex и ocamlyacc код мы аналогично будем править в мэйкфайле, когда его напишем.
А пока давайте напишем файл, который будет делать всё. Обзовём его, скажем, home.eliom.
В начале файла пооткрываем модулей на будущее и сразу создадим строку с образцовым кодом, который будет предлагаться посетителю.
Дальше создадим модуль нашего приложения — для того чтобы клиент-серверное взаимодействие корректно работало, все сервисы регистрируются от имени какого-либо приложения. К счастью, это несложно:
Добавим функцию, которая будет выполнять код на Йобе и возвращать результат, функция будет чисто клиентская, сервер о ней даже не узнает
Создадим сервис, который будет обрабатывать пользовательские запросы. Сервисы в ocsigen создавать необычайно просто и удобно, каждый сервис характеризуется путём и набором строго типизированных(!) GET/POST параметров. Соответственно, сервер принимает решение о том, каким сервисом обрабатывать запрос, на основании пришедшего запроса. Можно создать дефолтный сервис, который будет обрабатывать запросы без параметров, второй сервис по тому же адресу, который будет обрабатывать запросы с одним GET-параметром, и третий сервис с одним POST-параметром. И они не будут путаться. Но пока нам нужен только один сервис:
Путь символизирует то, что сервис будет отдаваться по дефолтному пути для сайта (как индексная страница в апаче), а в ~get_params мы указали, что параметров сервис не принимает.
Пора написать шаблон страницы:
Как можно заметить, все элементы страницы являются функциями, которые принимают на вход строго определенные параметры, за счёт чего и осуществляется статическая типизация создаваемой HTML-страницы. Так, функция html принимает на вход два параметра — один типа `Head, а второй — типа `Body. А div — принимает на вход список разрешенных к нахождению внутри div элементов.
Но остановимся мы поподробнее на другом. Во-первых, наша функция page_template принимает на вход два параметра — код и счётчик посетителей. Первое помещается в textarea, а второе — во внутренности тэга <p> в самом низу. Во-вторых, названия функций raw_textarea и raw_button такие «сырые» — неспроста. Существуют аналогичные простые не-«raw_» функции, но они предназначены для создания элементов внутри замечательных строго типизированных форм, которые обязательно ссылаются на какой-то сервис (проще говоря, создавая форму, мы сразу проверяем, что она будет отсылать куда нужно строго требуемый список параметров). А наши textarea и button (это не тэг <input>, а самый натуральный тэг <button> из HTML5) слать ничего никуда не будут, а будут резвиться внутри страницы, поэтому и формы им не положено. В-третьих, мы создали специальный <pre>, в котором будут храниться результаты работы нашего интерпретатора.
Кстати, я упомянул счётчик посетителей? Совсем забыл, давайте его напишем. Для этого сразу познакомимся с двумя новыми модулями: Lwt и Ocsipersist. Первый отвечает за работу с кооперативными потоками, а второй — за персистентное хранилище.
Система потоков в оксигене кооперативная. Это значит, что вместо традиционных тредов, требующих создания нового процесса, стека вызовов и прочей лабуды мы получаем очень легковесные потоки (настолько легковесные, что они используются практически для каждого вызова). Вместо того, чтобы заниматься созданием всякими глупостями, мы, обращаясь к потокам, создаём в коде т.н. точки кооперативности, на основании которых компилятор сам делает всё, что нужно, минимизируя вероятность дедлока.
Если вы непривычны к OCaml (как я поначалу), то можно заметить, что функция у нас хитрая. Сам get_count — это «объект», хранящий в себе мьютекс и объект хранилища. Когда мы пишем в коде «get_count», нам возвращается функция, принимающая на вход () и только тогда выполняющая всю работу. Залезем теперь внутрь функции. Сразу видим хитрый оператор ">>=" — это специальный оператор, который передаёт результат работы первого аргумента — треда — на вход второму аргументу — функции, создающей новый тред. Если говорить формально, то сигнатура оператора такая:
А функция return в самом конце возвращает результат работы треда.
С Ocsipersist всё ясно, даже рассказывать нечего.
Откуда будем вызывать наш get_count и генерировать шаблон страницы? А вот и она, функция interpret:
Эта функция знакомит нас с ещё несколькими интересными возможностями. Оксиген, увы, пишет в лог исключительно краткую информацию о запросах — кто, какой юзерагент, на какой хост, за какой страницей, когда. А мне захотелось получить ещё и referer. Ну что же, мы получим информацию о запросе, из неё вытащим реферер. Он устроен опять хитро — это значение, которого может и не быть (типа Null в других языках), и которое обернуто ещё и в ленивое вычисление, то есть, пока я его не затребовал, оно нигде и не хранилось.
Ещё один новый оператор ">|=" похож на ">>=" — с той лишь разницей, что результат работы треда передаётся на вход функции, которая новый тред возвращать вовсе не планирует.
Всё, подошли к завершению. Пора регистрировать наш сервис и учить код интерпретироваться:
Блок {{… }} это как бы клиентская функция — мы её регистрируем в обработчике onload страницы.
В клиентском коде наш синтаксис немножко отличается. Для обращения к методам Js-объектов, вместо одиночного диеза используется двойной, таким образом, Dom_html.document##getElementById соответствует простому «document.getElementById».
Многочисленные Js.Opt.iter, которые можно здесь наблюдать, обусловлены тем, что функция getElementById вовсе не обязательно нам что-либо вернёт. Соответственно Js.Opt.iter выполнит над результатом действие только в том случае, если результат действительно есть. Поэтому для трёх объектов на странице, которые мы искали, нам потребовалось четыре Js.Opt.iter. Четыре — потому что по умолчанию функция getElementById возвращает нам объект типа element, который имеет только самые общие свойства. А чтобы докопаться до свойства value в textarea, мы пытаемся скастовать (Dom_html.CoerceTo) наш объект в тип textarea, что вовсе не гарантирует результат в общем случае.
Таким образом, зарегистрированный обработчик сервиса делает всего две вещи — вызывает Js-код при старте страницы и возвращает нам наш шаблон, упомянутый выше.
Внимательный читатель мог уже заметить и задаться вопросом, зачем я в самом начале объявил code_example как ссылку на строку (ref), а потом везде её разыменовываю. А всё дело в том, что мне в какой-то момент пришло в голову, что наша Йоба должна быть действительно коллективной. Давайте сохранять, то что пользователь пытался интерпретировать, и показывать другим.
Для этого рядом с первым сервисом создадим специально обученный второй сервис, который будет принимать на вход строку с кодом и класть её в code_example. Чтобы вызывать сервис из яваскрипта, создадим его от имени Eliom_output.Caml:
Теперь изменим нашу клиентскую функцию (которая самая внутренняя), добавив всего одну строку:
Voila! Теперь каждый, пришедший к нам на страницу, увидит код, который исполнялся последним. Правда при рестарте сервера на место всё равно вернётся наш пример.
Наконец, пишем Makefile:
Поскольку нам необходимо генерировать yobaLexer.eliom и yobaParser.eliom, мы написали для этого соответствующие правила. Аналогично, увы, дефолтный генератор зависимостей не справляется с определением, в каком порядке компилировать наши лексер и парсер, поэтому мы помогли ему парой правил.
Теперь можно запустить:
Первое сгенерирует порядок компиляции, второе скомпилирует серверный код, а третье — сгенерирует яваскрипт-файл, который автоматически будет вызываться серверной частью, хоть мы этот вызов и не прописали в шаблоне страницы. Если раскомментировать вызов yui-compressor и последующего mv, то можно немножко сжать js-код (в моём случае с 400кб до 209кб).
Выполнять все инструкции надо, прописав тот export PATH, про который вам говорила сборка ocsigenserver в самом конце.
После этого переходим в $HOME/bin/ocsigen/bin и говорим ./ocsigenserver
Открываем браузер и идём пробовать интерпретировать. А если самому лень, то можно порезвиться у меня: sorokdva.net
Что делает холодным воскресным утром программист? Холодным воскресным утром программист пьёт горячий чай и пишет код. Чай он пьёт, потому что утро холодное, да и проснулся ещё не до конца, а код пишет, потому что хочется. Программисту всегда хочется писать код, только в будни он пишет код за деньги и от этого очень устаёт, а в выходные для себя, поэтому отдыхает.
Этим утром мы будем писать наше первое приложение для Ocsigen. Желающим неплохо бы сначала ознакомиться с официальным мануалом, впрочем, на многое надеяться не стоит, потому что мануал недописан, пестрит недоуменными строками а-ля "??????" и нецензурной речью на французском. Поэтому основным мануалом буду я.
Как вы возможно помните, когда-то мы писали интерпретатор языка Йоба. С тех пор интерпретатор был незначительно улучшен, выделен в отдельный класс, стал принимать строку на вход, отдавать строку на выход (вместо работы с консолью). Теперь нашей задачей станет
Для начала, нам надо установить сервер ocsigen. Поскольку 2.0 для дистрибутивов собрать ещё не успели, мы последуем вот этой инструкции и установим сервер бандлом в наш домашний каталог. Чтобы бандл получился правильным и вкусным, перед запуском make отредактируем Makefile.config и пропишем там:
LOCAL := ${HOME}/bin/ocsigen
DEV := YES
OCAMLDUCE := YES
OCLOSURE := YES
OTHERS := YES
Ocaml и Findlib собирать не будем, они и так есть в репозиториях. O'Closure нам в этот раз не понадобится, но мы его на всякий случай соберём — чтобы потом не пересобирать оксиген, если вдруг вы сами заинтересуетесь им или захотите от меня статью.
Следующим пунктом сразу отредактируем файл ${HOME}/bin/ocsigen/etc/ocsigenserver/ocsigenserver.conf: убедимся, что там прописаны подходящие порт, а также имя и группа пользователя, от которого стартовать. Теперь пришло время подготовить конфиг для будущего сайта. Создадим ${HOME}/bin/ocsigen/etc/ocsigenserver/conf.d/yoba.conf и наполним его содержимым:
<ocsigen>
<server>
<charset>utf-8</charset>
<extension findlib-package="ocsigenserver.ext.staticmod"/>
<extension findlib-package="ocsigenserver.ext.ocsipersist-sqlite">
<database file="ocsidb"/>
</extension>
<extension findlib-package="ocsigenserver.ext.deflatemod" />
<extension findlib-package="eliom.server"/>
<host charset="utf-8" hostfilter="*">
<site path="" charset="utf-8">
<static dir="/home/username/yoba" />
<eliom module="/home/username/yoba/_build/server/yoba.cmo">
<cache-size>10000</cache-size>
</eliom>
</site>
<deflate compress="only">
<type>application/x-javascript</type>
</deflate>
</host>
</server>
</ocsigen>
Кратко о содержимом:
- staticmod позволяет серверу отдавать статические данные (в нашем случае это будет скомпилированный .js файл
- ocsipersist-sqlite — позволяет нам работать с персистентными данными, потом увидим, зачем это надо
- deflatemod, как и ожидается, жмёт данные при отправке. Ниже можно заметить в настройках сайта секцию deflate, которая говорит, что жмём мы только js. На заметку: выбирать способ сжатия — gzip или deflate — нельзя, он выбирается автоматически на основе ожиданий клиента.
- eliom.server предоставляет собственно всю серверную часть фреймворка
- В параметре hostfilter нашего хоста мы говорим, что отвечаем на всех доменах
- А указывая пустой path для сайта, мы говорим, что сайт располагается в корне домена. Заменив пустую строку на, скажем, «yoba», мы заставим наш сайт вместе со всем статическим контентом отдаваться по адресу hostname/yoba — однозначный профит, можно держать несколько сайтов на одном домене и тасовать их как вздумается
- /home/username/yoba будет нашим каталогом с кодом (и скомпилированным файлом yoba.js), а в _build/server будет лежать скомпилированный серверный модуль. Разумеется, в идеале надо компилировать, потом код переносить в отдельную папочку, да ещё и не хранить статический контент в одной папке со скомпилированным модулем, но сейчас мы так сделаем, чтобы побыстрее проверить накоденное
Ура! Переходим к написанию кода.
Создаём пресловутую папку /home/username/yoba/ и скачиваем архив с языком Йоба — сам язык мы уже когда-то написали, а как его чуть-чуть подпилить и превратить в класс, нам неинтересно, потому возьмём сразу готовое. Распаковываем в ту же папку и качаем туда же стандартные Makefile.config и Makefile.rules — сборка проекта дело непростое, с нуля мэйкфайл фиг напишешь.
Настало время кое-что поправить и сразу узнать о первых синтаксических новшествах: в Eliom код можно размещать в секции. Секция {server{… }} (то же самое, что код просто без секции) компилируется и исполняется на сервере, секция {client{… }} — на клиенте, а {shared{… }} — доступна и там, и там.
Поскольку наш интерпретатор будет работать на клиенте, мы переименовываем файл yobaLang.ml в yobaLang.eliom, открываем его, и в самом начале добавляем строку "{client{", а в самом конце заменяем ";;" (две точки с запятой — это конец инструкции только на верхнем уровне кода, внутри секции их использовать уже нельзя) на "}}". Генерируемый ocamllex и ocamlyacc код мы аналогично будем править в мэйкфайле, когда его напишем.
А пока давайте напишем файл, который будет делать всё. Обзовём его, скажем, home.eliom.
В начале файла пооткрываем модулей на будущее и сразу создадим строку с образцовым кодом, который будет предлагаться посетителю.
{shared{
open Eliom_pervasives
open Lwt
open HTML5.M
open Eliom_parameters
open Eliom_request_info
open Eliom_output.Html5
open Ocsigen_extensions
let code_example = ref "
чо люблю сэмки йоба
чо люблю пиво йоба
чо люблю яга йоба
чо люблю итерации йоба
чо пиво это 1 йоба
чо яга это 2 йоба
усеки результат это
чо покажь итерации йоба
чо покажь сэмки йоба
йоба
усеки фибоначчи это
чо сэмки это пиво и яга йоба
чо пиво это яга йоба
чо яга это сэмки йоба
чо итерации это итерации и 1 йоба
чо есть итерации 50 тада хуйни результат или хуйни фибоначчи йоба
йоба
чо хуйни фибоначчи йоба"
}}
Дальше создадим модуль нашего приложения — для того чтобы клиент-серверное взаимодействие корректно работало, все сервисы регистрируются от имени какого-либо приложения. К счастью, это несложно:
module My_appl =
Eliom_output.Eliom_appl (
struct
let application_name = "yoba"
end)
Добавим функцию, которая будет выполнять код на Йобе и возвращать результат, функция будет чисто клиентская, сервер о ней даже не узнает
{client{
let yoba_execute str = (
let yparser = new YobaLang.yoba_interpretator () in
yparser#parse str;
yparser#get_output)
}}
Создадим сервис, который будет обрабатывать пользовательские запросы. Сервисы в ocsigen создавать необычайно просто и удобно, каждый сервис характеризуется путём и набором строго типизированных(!) GET/POST параметров. Соответственно, сервер принимает решение о том, каким сервисом обрабатывать запрос, на основании пришедшего запроса. Можно создать дефолтный сервис, который будет обрабатывать запросы без параметров, второй сервис по тому же адресу, который будет обрабатывать запросы с одним GET-параметром, и третий сервис с одним POST-параметром. И они не будут путаться. Но пока нам нужен только один сервис:
let empty_service = Eliom_services.service
~path:[""]
~get_params:(Eliom_parameters.unit)
();;
Путь символизирует то, что сервис будет отдаваться по дефолтному пути для сайта (как индексная страница в апаче), а в ~get_params мы указали, что параметров сервис не принимает.
Пора написать шаблон страницы:
let page_template code_input counter_value =
html
(head
(title (pcdata "Yoba interpreter")) []
)
(body [
h1 [pcdata "Yoba! For human beings"];
p [pcdata "Вас приветствует Йоба! Йоба — это чотко!"];
div [
raw_textarea ~a:[a_id "clientcode"] ~name:"clientcode" ~rows:25 ~cols:60 ~value:code_input ();
raw_button ~button_type:`Button ~name:"clientbutton" ~a:[a_id "clientbutton"] ~value:"Кликай!" [pcdata "Кликай!"];
];
pre ~a:[a_id "clientoutput"] [];
hr ();
p [pcdata "Йоба-скриптов хуйнули уже: "; b [pcdata (string_of_int counter_value)]]
]);;
Как можно заметить, все элементы страницы являются функциями, которые принимают на вход строго определенные параметры, за счёт чего и осуществляется статическая типизация создаваемой HTML-страницы. Так, функция html принимает на вход два параметра — один типа `Head, а второй — типа `Body. А div — принимает на вход список разрешенных к нахождению внутри div элементов.
Но остановимся мы поподробнее на другом. Во-первых, наша функция page_template принимает на вход два параметра — код и счётчик посетителей. Первое помещается в textarea, а второе — во внутренности тэга <p> в самом низу. Во-вторых, названия функций raw_textarea и raw_button такие «сырые» — неспроста. Существуют аналогичные простые не-«raw_» функции, но они предназначены для создания элементов внутри замечательных строго типизированных форм, которые обязательно ссылаются на какой-то сервис (проще говоря, создавая форму, мы сразу проверяем, что она будет отсылать куда нужно строго требуемый список параметров). А наши textarea и button (это не тэг <input>, а самый натуральный тэг <button> из HTML5) слать ничего никуда не будут, а будут резвиться внутри страницы, поэтому и формы им не положено. В-третьих, мы создали специальный <pre>, в котором будут храниться результаты работы нашего интерпретатора.
Кстати, я упомянул счётчик посетителей? Совсем забыл, давайте его напишем. Для этого сразу познакомимся с двумя новыми модулями: Lwt и Ocsipersist. Первый отвечает за работу с кооперативными потоками, а второй — за персистентное хранилище.
Система потоков в оксигене кооперативная. Это значит, что вместо традиционных тредов, требующих создания нового процесса, стека вызовов и прочей лабуды мы получаем очень легковесные потоки (настолько легковесные, что они используются практически для каждого вызова). Вместо того, чтобы заниматься созданием всякими глупостями, мы, обращаясь к потокам, создаём в коде т.н. точки кооперативности, на основании которых компилятор сам делает всё, что нужно, минимизируя вероятность дедлока.
let get_count =
let counter_store = Ocsipersist.open_store "counter_store" in
let cthr = Ocsipersist.make_persistent counter_store "countpage" 0 in
let mutex = Lwt_mutex.create () in
(fun () ->
cthr >>= (fun c ->
Lwt_mutex.lock mutex >>= (fun () ->
Ocsipersist.get c >>= (fun oldc ->
let newc = oldc + 1 in
Ocsipersist.set c newc >>= (fun () -> Lwt_mutex.unlock mutex; return newc)
)
)
)
)
;;
Если вы непривычны к OCaml (как я поначалу), то можно заметить, что функция у нас хитрая. Сам get_count — это «объект», хранящий в себе мьютекс и объект хранилища. Когда мы пишем в коде «get_count», нам возвращается функция, принимающая на вход () и только тогда выполняющая всю работу. Залезем теперь внутрь функции. Сразу видим хитрый оператор ">>=" — это специальный оператор, который передаёт результат работы первого аргумента — треда — на вход второму аргументу — функции, создающей новый тред. Если говорить формально, то сигнатура оператора такая:
val (>>=) : 'a t -> ('a -> 'b t) -> 'b t
А функция return в самом конце возвращает результат работы треда.
С Ocsipersist всё ясно, даже рассказывать нечего.
Откуда будем вызывать наш get_count и генерировать шаблон страницы? А вот и она, функция interpret:
let interpret code =
let req = Eliom_request_info.get_ri () in
let ref = match Lazy.force_val req.ri_referer with | None -> "" | Some x -> x in
Ocsigen_messages.accesslog ("Referer: " ^ ref);
get_count() >|= (page_template code);;
Эта функция знакомит нас с ещё несколькими интересными возможностями. Оксиген, увы, пишет в лог исключительно краткую информацию о запросах — кто, какой юзерагент, на какой хост, за какой страницей, когда. А мне захотелось получить ещё и referer. Ну что же, мы получим информацию о запросе, из неё вытащим реферер. Он устроен опять хитро — это значение, которого может и не быть (типа Null в других языках), и которое обернуто ещё и в ленивое вычисление, то есть, пока я его не затребовал, оно нигде и не хранилось.
Ещё один новый оператор ">|=" похож на ">>=" — с той лишь разницей, что результат работы треда передаётся на вход функции, которая новый тред возвращать вовсе не планирует.
Всё, подошли к завершению. Пора регистрировать наш сервис и учить код интерпретироваться:
My_appl.register empty_service
(fun () () ->
Eliom_services.onload
{{
Js.Opt.iter (Dom_html.document##getElementById (Js.string "clientbutton")) (
fun clntbutton ->
clntbutton##onclick <- Dom_html.handler (fun _ ->
Js.Opt.iter (Dom_html.document##getElementById (Js.string "clientcode")) (
fun cdinput ->
Js.Opt.iter (Dom_html.document##getElementById (Js.string "clientoutput")) (
fun cdoutput ->
let cdinputarea = Dom_html.CoerceTo.textarea cdinput in
Js.Opt.iter cdinputarea (fun x ->
let i = Js.to_string x##value in
cdoutput##innerHTML <- Js.string (yoba_execute i)
)
)
);
Js._true
)
)
}};
interpret !code_example);;
Блок {{… }} это как бы клиентская функция — мы её регистрируем в обработчике onload страницы.
В клиентском коде наш синтаксис немножко отличается. Для обращения к методам Js-объектов, вместо одиночного диеза используется двойной, таким образом, Dom_html.document##getElementById соответствует простому «document.getElementById».
Многочисленные Js.Opt.iter, которые можно здесь наблюдать, обусловлены тем, что функция getElementById вовсе не обязательно нам что-либо вернёт. Соответственно Js.Opt.iter выполнит над результатом действие только в том случае, если результат действительно есть. Поэтому для трёх объектов на странице, которые мы искали, нам потребовалось четыре Js.Opt.iter. Четыре — потому что по умолчанию функция getElementById возвращает нам объект типа element, который имеет только самые общие свойства. А чтобы докопаться до свойства value в textarea, мы пытаемся скастовать (Dom_html.CoerceTo) наш объект в тип textarea, что вовсе не гарантирует результат в общем случае.
Таким образом, зарегистрированный обработчик сервиса делает всего две вещи — вызывает Js-код при старте страницы и возвращает нам наш шаблон, упомянутый выше.
Внимательный читатель мог уже заметить и задаться вопросом, зачем я в самом начале объявил code_example как ссылку на строку (ref), а потом везде её разыменовываю. А всё дело в том, что мне в какой-то момент пришло в голову, что наша Йоба должна быть действительно коллективной. Давайте сохранять, то что пользователь пытался интерпретировать, и показывать другим.
Для этого рядом с первым сервисом создадим специально обученный второй сервис, который будет принимать на вход строку с кодом и класть её в code_example. Чтобы вызывать сервис из яваскрипта, создадим его от имени Eliom_output.Caml:
let update_code_service = Eliom_output.Caml.register_service
~path:["update code"]
~get_params:(string "f")
(fun f () -> code_example := f; return ());;
Теперь изменим нашу клиентскую функцию (которая самая внутренняя), добавив всего одну строку:
let i = Js.to_string x##value in
ignore(Eliom_client.call_caml_service ~service:%update_code_service i ());
cdoutput##innerHTML <- Js.string (yoba_execute i)
Voila! Теперь каждый, пришедший к нам на страницу, увидит код, который исполнялся последним. Правда при рестарте сервера на место всё равно вернётся наш пример.
Наконец, пишем Makefile:
MODULE = yoba
APP = yoba
include Makefile.config
SERVERFILES := home.eliom
CLIENTFILES := yobaType.ml yobaLexer.eliom yobaParser.eliom yobaLang.eliom home.eliom
SERVERLIB := -package eliom.server,ocsigenserver,lwt
CLIENTLIB := -package js_of_ocaml
INCLUDES =
EXTRADIRS =
include Makefile.rules
yobaParser.eliom:
ocamlyacc yobaParser.mly
echo '{client{' >yobaParser.eliom
cat yobaParser.ml >>yobaParser.eliom
echo '}}' >>yobaParser.eliom
sed -i 's/;;//' yobaParser.eliom
rm yobaParser.ml yobaParser.mli
yobaLexer.eliom: yobaParser.eliom
ocamllex yobaLexer.mll
echo '{client{' >yobaLexer.eliom
cat yobaLexer.ml >>yobaLexer.eliom
echo '}}' >>yobaLexer.eliom
sed -i 's/;;//' yobaLexer.eliom
rm yobaLexer.ml
_build/client/yobaLexer.cmo: _build/client/yobaParser.cmo
_build/client/yobaLang.cmo: _build/client/yobaLexer.cmo _build/client/yobaParser.cmo
$(STATICDIR)/$(APP).js: _build/client/${MODULE}.cmo
${JS_OF_ELIOM} -jsopt -pretty -verbose ${CLIENTLIB} -o $@ $^
#yui-compressor --charset utf-8 $@ > $@_min
#mv $@_min $@
pack: $(STATICDIR)/$(APP).js
Поскольку нам необходимо генерировать yobaLexer.eliom и yobaParser.eliom, мы написали для этого соответствующие правила. Аналогично, увы, дефолтный генератор зависимостей не справляется с определением, в каком порядке компилировать наши лексер и парсер, поэтому мы помогли ему парой правил.
Теперь можно запустить:
make depend
make
make pack
Первое сгенерирует порядок компиляции, второе скомпилирует серверный код, а третье — сгенерирует яваскрипт-файл, который автоматически будет вызываться серверной частью, хоть мы этот вызов и не прописали в шаблоне страницы. Если раскомментировать вызов yui-compressor и последующего mv, то можно немножко сжать js-код (в моём случае с 400кб до 209кб).
Выполнять все инструкции надо, прописав тот export PATH, про который вам говорила сборка ocsigenserver в самом конце.
После этого переходим в $HOME/bin/ocsigen/bin и говорим ./ocsigenserver
Открываем браузер и идём пробовать интерпретировать. А если самому лень, то можно порезвиться у меня: sorokdva.net