Что делает холодным воскресным утром нормальный человек? Любой вам ответит: холодным воскресным утром человек спит. Потому что всю неделю он работал и хочет отдохнуть.
Что делает холодным воскресным утром программист? Холодным воскресным утром программист пьёт горячий чай и пишет код. Чай он пьёт, потому что утро холодное, да и проснулся ещё не до конца, а код пишет, потому что хочется. Программисту всегда хочется писать код, только в будни он пишет код за деньги и от этого очень устаёт, а в выходные для себя, поэтому отдыхает.
Этим утром мы будем писать наше первое приложение для 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 := YESOcaml и 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
