Pull to refresh

Динамические приложения с Ocsigen или Йоба возвращается

Reading time11 min
Views2.5K
Что делает холодным воскресным утром нормальный человек? Любой вам ответит: холодным воскресным утром человек спит. Потому что всю неделю он работал и хочет отдохнуть.
Что делает холодным воскресным утром программист? Холодным воскресным утром программист пьёт горячий чай и пишет код. Чай он пьёт, потому что утро холодное, да и проснулся ещё не до конца, а код пишет, потому что хочется. Программисту всегда хочется писать код, только в будни он пишет код за деньги и от этого очень устаёт, а в выходные для себя, поэтому отдыхает.

Этим утром мы будем писать наше первое приложение для Ocsigen. Желающим неплохо бы сначала ознакомиться с официальным мануалом, впрочем, на многое надеяться не стоит, потому что мануал недописан, пестрит недоуменными строками а-ля "??????" и нецензурной речью на французском. Поэтому основным мануалом буду я.

Как вы возможно помните, когда-то мы писали интерпретатор языка Йоба. С тех пор интерпретатор был незначительно улучшен, выделен в отдельный класс, стал принимать строку на вход, отдавать строку на выход (вместо работы с консолью). Теперь нашей задачей станет внедрение Йобы в качестве основного языка компании Google превращение интерпретатора Йобы в веб-приложение, да не простое — а клиентское. Хоть я и добавил в класс счётчик операций, чтобы нельзя было слишком обнаглеть, но всё равно — пусть пользователь на своём компьютере вычислительные мощности тратит, а не на сервере.

Для начала, нам надо установить сервер 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
Tags:
Hubs:
+45
Comments11

Articles

Change theme settings