fpWeb Tutorial/ru

From Free Pascal wiki
Jump to navigationJump to search

العربية (ar) English (en) español (es) русский (ru)

 Первоначально основано на учебнике fcl-web (в формате PDF) пользователя форума Leledumbo.

Введение

fpWeb - это фреймворк веб-приложений, поставляемый FPC в своем дистрибутиве по умолчанию как часть пакета fcl-web. Сам фреймворк построен на основе функций fcl-web. Фреймворк построен с мыслью о RAD, чтобы эффективно использовать компонентность при создании динамического контента. Предоставляется пакет Lazarus, который может использовать фреймворк методом перетаскивания для управления сеансами и создания контента. В этом руководстве мы попытаемся охватить основные функции fpWeb, чтобы с его помощью можно было создать обычное веб-приложение. Обратите внимание, что это руководство не пытается научить вас HTTP-протоколу, HTML, CSS, JavaScript или манипуляциям с базой данных, поскольку владение протоколом и клиентскими языками должно быть предварительным условием работы для каждого программиста веб-приложений, а манипуляции с базой данных не отличаются от десктопной реализации.


Архитектура (ПОЖАЛУЙСТА, прочтите)

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

Приложение

Под приложением здесь понимается протокол, который будет реализовывать ваше приложение. fpWeb с радостью переключится с CGI, FCGI, с модуля Apache на встроенный сервер и т.д., если fcl-web реализует еще что- нибудь в будущем. Каждое приложение реализовано в своем собственном модуле, поэтому для переключения с одного приложения на другое, за исключением модуля Apache, нужно просто изменить соответствующий идентификатор в разделе uses. В настоящее время (по состоянию на 3.0.0 / 3.1.1) это:

  • fpCGI -> CGI
  • fpFCGI -> FastCGI
  • fpApache (также требуется httpd) -> модуль Apache
  • fpHttpApp -> встроенный сервер
  • microhttpapp -> встроенный сервер с использованием библиотеки GNU libmicrohttp.
  • fphttpsys -> поддержка системой Windows протокола HTTP.

В этом руководстве мы будем использовать встроенный сервер для простоты, потому что вам не придется иметь дело с настройкой виртуального сервера и путаницей со сложным файлом конфигурации и управлением службами. Ваше приложение будет единым переносным бинарным веб-приложением! Другая причина может заключаться в том, что существует больше, чем одно приложение веб-сервера, и каждое имеет свой способ настройки. Было бы излишним пытаться охватить их всех, пока их документация уже выполняет свою работу. Модуль Apache реализован как (динамическая) библиотека, тогда как другие протоколы являются обычным приложением. Каждое приложение может иметь определенные свойства (например, порт), доступные и значимые только для этого приложения. Вот почему, если вы посмотрите на примеры fcl-web, пары .lpi / .lpr для каждого протокола помещаются в свои собственные каталоги, только веб-модули являются общими.

Веб-модули

fpWeb-overview.png

Приложения fpWeb состоят из веб-модулей, которые фактически создают контент. Веб-модуль может содержать веб-действия, которые могут еще больше разделить функциональность. Например, веб-модуль аутентификации может иметь веб-действия входа и выхода. Хотя модуль about web может вообще не нуждаться в действии и обслуживает только один контент. Веб-модуль интегрирован с fpTemplate, который можно использовать для создания динамического контента из файла шаблона. Это примерно похоже на то, что делает PHP, только разрыв между логикой и представлением скорее вынужденный, чем предполагаемый. Некоторые говорят, что fpTemplate реализует пассивное представление, в то время как PHP по умолчанию реализует шаблон проектирования активного представления.

Установка

Пакет fpWeb для Lazarus по умолчанию не установлен (но идет в стандартной поставке). Чтобы включить fpWeb:

  1. Откройте Lazarus и выберите Package->Install/Uninstall Package
  2. В списке Available (Доступно) для установки найдите weblaz и нажмите Install selection (Установить выбранное). Нажмите Save and rebuild IDE(Сохранить и пересобрать IDE) и подтвердите, нажав Continue(Продолжить).
  3. Позвольте IDE пересобраться и перезапуститься. Если все пойдет хорошо, у вас должна появиться вкладка fpWeb на палитре компонентов, как показано ниже:
Installed weblaz package

Специализированные модули

Класс*TFPWebModule* (используемый ниже) - это простой пример модуля fpWEB, который можно использовать для всех видов HTTP-запросов.

Однако fpWEB поставляется с некоторыми специализированными модулями, которые имеют дополнительную поддержку для специализированных задач:

  • Класс TSimpleFileModule в модуле fpwebfile.pp можно использовать для отправки файлов. Вы указываете ему каталог, и он делает все остальное.
  • Класс TFPHTMLModule в модуле fphtml.pp можно использовать для создания HTML.
  • Класс TProxyWebModule в модуле fpwebproxy.pp - это готовый прокси для пересылки.
  • Класс TFPWebProviderDataModule в модуле fpwebdata.pp обслуживает данные в формате JSON, которые могут использоваться хранилищами ExtJS.
  • Класс TSQLDBRestModule в модуле sqldbrestmodule.pp реализует полноценный сервер REST, поддерживаемый SQLDB. См. дополнительную информацию в SQLDBRestBridge.
  • Класс TJSONRPCModule в модуле webjsonrpc.pp реализует службу JSON-RPC.
  • Класс TExtDirectModule в модуле fpextdirect.pp реализует вариант Ext.Direct службы JSON-RPC.

Hello, World!

Создадим простое веб-приложение. Как обычно преподают при изучении программирования: "Hello, World!" будет нашим первым приложением.

1. Откройте Lazarus и выберите Project->New Project, затем выберите HTTP server Application

Создать новое приложение HTTP-сервера

2. Появится другое диалоговое окно для обслуживания статических файлов, выбора порта и многопоточности. Просто используйте порт по умолчанию 8080.

You may skip the static files serving (go to tips and tricks section if you want to know it more).

(Вы можете пропустить обслуживание статических файлов (перейдите в раздел советов и рекомендаций, если хотите узнать больше))

Static files, port selection and multithreading options


ВАЖНО!:
Если вы решите использовать потоки на *nix, не забудьте добавить cthreads в качестве первого модуля в разделе uses .lpr, в противном случае будет создан RTE 232. При запуске с консоли должно отображаться сообщение:
This binary has no thread support compiled in. Recompile the application with a thread-driver in the program uses clause before other units using thread.

(В этом двоичном файле не скомпилирована поддержка потоков. Перекомпилируйте приложение с драйвером потока в программе using предложение перед другими модулями, использующими поток)

3. С 14 января 2017г. (или FPC 3.0.4) вам может потребоваться открыть .lpr и добавить следующую строку в основной текст, если ее еще там нет:

Application.LegacyRouting := true;
причина будет объяснена в главе Маршрутизация.


4. Что бы вы ни выбрали, нажмите "OK", и вы будете представлены в приложении fpWeb с одним модулем по умолчанию.

5. Переместите фокус на модуль и перейдите в Object Inspector. Не стесняйтесь переименовывать модуль, если хотите.

6. Выберите вкладку Events и нажмите кнопку справа от второго столбца в строкеOnRequest, чтобы создать обработчик событий.

Создание обработчика OnRequest веб-модуля в object inspector
Вы будете перенаправлены в редактор исходного кода со следующим кодом:
procedure TFPWebModule1.DataModuleRequest(Sender: TObject; ARequest: TRequest;
AResponse: TResponse; var Handled: Boolean);
begin
  |
end;
Заполните событие:
procedure TFPWebModule1.DataModuleRequest(Sender: TObject; ARequest: TRequest;
AResponse: TResponse; var Handled: Boolean);
begin
  AResponse.Content := 'Hello, World!';
  Handled := true;
end;

7. Теперь запустите приложение (или нажмите F9).

8. Откройте браузер и введите:

http://localhost:8080/

9. Вы должны увидеть отображающееся "Hello, World!".

Если это не так, проверьте ниже:

  • Фреймворк выполняет много операций по обработке исключений, и отладчик IDE может их поймать и прервать работу вашего приложения. Можно добавить большинство исключений в список игнорирования, чтобы вы могли больше сосредоточиться на потоке приложения. Продолжайте пропускать и продолжайте, пока не перестанет появляться диалоговое окно и браузер не покажет результат.
  • Handled := true - это способ, которым мы сообщаем фреймворку, что запрос был обработан. Если вы не установите его (или установите для него значение false), вместо этого будет отображаться страница с ошибкой. На данный момент это не влияет на поток запросов, но будет позже. Так что держите это так, пока не придет время, чтобы использовать его с пользой.
  • другой трек: тест без брандмауэра, загруженного в ОЗУ (как приложение, как сервис или демон, или как оба).

Чтение данных GET и POST

Динамический контент, скорее всего, будет запускаться из пользовательского ввода, либо через формы, предоставляя значения в URL-адресе, и т. Д. Эти данные отправляются вместе с запросом, который представлен в методе как ARequest параметр типа TRequest.

Чтение GET

Данные GET предоставляются как ARequest.QueryFields, который является потомком TStrings. Короче говоря, все, что вы обычно делаете с TStrings, применимо здесь, например, доступ к данным в стиле карты через свойство Values.

Повторно используя приведенный выше код, замените тело метода на:

procedure TFPWebModule1.DataModuleRequest(Sender: TObject; ARequest: TRequest;
 AResponse: TResponse; var Handled: Boolean);
var
  LName: String;
begin
  LName := ARequest.QueryFields.Values['Name'];
  if LName = EmptyStr then
    with AResponse.Contents do
    begin
      Add('<form action="' + ARequest.URI + '" method="GET"');
      Add('<label for="name">Please tell me your name:</label>');
      Add('<input type="text" name="name" id="name" />');
      Add('<input type="submit" value="Send" />');
      Add('</form>');
    end
  else
    AResponse.Content := 'Hello, ' + LName + '!';
  Handled := true;
end;

ARequest.URI - это просто ссылка на текущий URI, поэтому даже когда вы меняете зарегистрированный модуль или имя действия, этот код остается прежним.

Обратите внимание, что, как и в Паскале, обращение к данным осуществляется без учета регистра.

Теперь вы можете попробовать запросить /, который отобразит

 Please tell me your name

и /?name=<напишите здесь что угодно, например: Bob>, который отобразит

 Hello, Bob!

Чтение POST

POST на самом деле не сильно отличается от GET, отличается только тем, к какому свойству получить доступ. Если доступ к GET осуществляется через ARequest.QueryFields, доступ к POST осуществляется через ARequest.ContentFields. Стиль POST предыдущего кода:

procedure TFPWebModule1.DataModuleRequest(Sender: TObject; ARequest: TRequest;
 AResponse: TResponse; var Handled: Boolean);
var
  LName: String;
begin
  LName := ARequest.ContentFields.Values['Name'];
  if LName = EmptyStr then
    with AResponse.Contents do
    begin
      Add('<form action="' + ARequest.URI + '" method="POST"');
      Add('<label for="name">Please tell me your name:</label>');
      Add('<input type="text" name="name" id="name" />');
      Add('<input type="submit" value="Send" />');
      Add('</form>');
    end
  else
    AResponse.Content := 'Hello, ' + LName + '!';
  Handled := true;
end;

Чтение загружаемых файлов

Единственное исключение - чтение полей multipart/form-data, то есть файлов. Оно доступно в ARequest.Files как экземпляр TUploadedFiles, который является потомком TCollection. Ниже приведен общедоступный интерфейс TUploadedFiles, который вы можете использовать для доступа к файлам:

TUploadedFiles = Class(TCollection)
...
public
  Function First: TUploadedFile;
  Function Last: TUploadedFile;
  Function IndexOfFile(AName: String) : Integer;
  Function FileByName(AName: String) : TUploadedFile;
  Function FindFile(AName: String) : TUploadedFile;
  Property Files[Index: Integer] : TUploadedFile read GetFile Write SetFile; default;
end;

Каждый TUploadedFile сам по себе имеет несколько свойств:

TUploadedFile = Class(TCollectionItem)
...
Public
  Destructor Destroy; override;
  Property FieldName: String Read FFieldName Write FFieldName;
  Property FileName: String Read FFileName Write FFileName;
  Property Stream: TStream Read GetStream;
  Property Size: Int64 Read FSize Write FSize;
  Property ContentType: String Read FContentType Write FContentType;
  Property Disposition: String Read FDisposition Write FDisposition;
  Property LocalFileName: String Read FLocalFileName Write FLocalFileName;
  Property Description: String Read FDescription Write FDescription;
end;

Они должны быть достаточно информативными, за исключением FileName и LocalFileName. FileName - это name исходного файла, загружаемого с клиента, LocalFileName - это путь к файлу на сервере, где файл временно хранится. Обратите внимание на разницу, выделенную жирным шрифтом выше.

Опять же, повторное использование того же обработчика запросов:

procedure TFPWebModule1.DataModuleRequest(Sender: TObject; ARequest: TRequest;
 AResponse: TResponse; var Handled: Boolean);
var
  n: Integer;
  f: TUploadedFile;
  i: Integer;
begin
  n := ARequest.Files.Count;
  if n = 0 then
    with AResponse.Contents do
    begin
      Add('<form id="form" action="' + ARequest.URI + '" method="POST" enctype="multipart/form-data">');
      Add('<label for="name">Drag n drop or click to add file:</label>');
      Add('<input type="file" name="input" />');
      Add('<input type="submit" value="Send" />');
      Add('</form>');
    end
  else
  begin
    f := ARequest.Files[0];
    AResponse.Contents.LoadFromStream(f.Stream);
  end;
  Handled := true;
end;

перетащите и бросьте файл (желательно текстовый, так как он будет отображаться как текст) в поле входного файла (или нажмите соответствующую кнопку), затем нажмите кнопку Send' (Отправить). Должно отобразиться содержимое файла.

Cookies

Отправка

Концепция «cookie», изобретенная Netscape в 1994 году, позволяет HTTP-серверу идентифицировать всех своих клиентов.


Файлы cookie - небольшой фрагмент данных, отправленный веб-сервером и хранимый на компьютере пользователя. Веб-клиент (обычно веб-браузер) всякий раз при попытке открыть страницу соответствующего сайта пересылает этот фрагмент данных веб-серверу в составе HTTP-запроса (Wiki). AResponse.Cookies содержит список отправляемых файлов cookie. Это потомок TCollection, соответственно содержащийся TCookie является потомком TCollectionItem. Следовательно, вы можете использовать TCollection как способ управления элементами для управления ими.

Вот пример:

procedure TFPWebModule1.DataModuleRequest(Sender: TObject; ARequest: TRequest;
 AResponse: TResponse; var Handled: Boolean);
var
  C: TCookie;
begin
  C := AResponse.Cookies.Add;
  C.Name := 'mycookie';
  C.Value := 'somevalue';
  Handled := true;
end;

Вы не увидите никаких результатов в своем браузере. Но если вы используете какие-то инструменты разработчика (в Chrome есть встроенный) можно увидеть заголовок ответа:

Заголовок ответа Set-Cookie в инструментах разработчика Chrome

Обратите внимание, что файл cookie имеет атрибуты, поэтому вы можете установить не только имя и значение. Просмотрите интерфейс TCookie, чтобы узнать, какие свойства поддерживаются.

Получение

Как только вы укажете выше заголовок Set-Cookie, последующий запрос на ваш сайт будет содержать дополнительный заголовок, содержащий значение, которое вы просили установить ранее:

Заголовок запроса cookie в инструментах разработчика Chrome

К счастью, способ чтения не отличается от данных GET и POST. Связанное свойство - ARequest.CookieFields. Чтобы прочитать ранее отправленный файл cookie:

procedure TFPWebModule1.DataModuleRequest(Sender: TObject; ARequest: TRequest;
 AResponse: TResponse; var Handled: Boolean);
begin
  AResponse.Contents.Add('<p>Cookie get: ' + ARequest.CookieFields.Values['mycookie'] + '</p>');
  Handled := true;
end;

Сессии

TFPWebModule является потомком TSessionHTTPModule, поэтому имеет возможность управления сессией. Сессия основана на модулях, поэтому каждый модуль может выбирать, использовать или не использовать управление сессией.

Сессия реализована абстрактно. По умолчанию реализация не предусмотрена. Один пример реализации с использованием файлов .ini приведен в модуле iniwebsession'. Вы должны иметь этот модуль в своем проекте или реализовать его, чтобы управление сессией работало. Если вы решите реализовать один из них, в основном вам необходимо расширить и реализовать абстрактные методы в классах TCustomSession и TSessionFactory' .

Активация

Чтобы активировать управление сессией, установите для свойства CreateSession в значение true. Сессия будет запущена до обработки запроса. В случае новой сессией будет вызвано OnNewSession. Здесь инициализируйте переменные сессии.

Управление переменными сессии

Переменные сессии представлены как Session.Variables (примечание: объект сессии является эквивалентом массива $ _SESSION, используемого в Php). Это строка для сопоставления строк, подобная структуре, поэтому вы можете читать / писать так:

Session.Variables['myvar'] := myvar; // записываем
...
myvar := Session.Variables['myvar']; // читаем

Задание переменной пустой строки НЕ удаляет ее. Если вы действительно хотите удалить переменную, вместо этого вызовите Session.RemoveVariable.

Завершение

Вызывайте Session.Terminate всякий раз, когда вы хотите завершить сессию (например, выход пользователя из системы). Сессия также автоматически истечет, если следующий запрос поступит после Session.TimeOutMinutes с момента последнего запроса. Когда сессия завершается, будет вызван OnSessionExpired. Сделайте там все, что вам нужно.

Маршрутизация

Начиная с FPC 3.0.4, был реализован новый механизм маршрутизации. Вместо сохранения обратной совместимости решено, что по умолчанию будет использоваться новая маршрутизация. Таким образом, любой старый код (или новый код в зависимости от старой маршрутизации) необходимо перенести, добавив:

Application.LegacyRouting := true;

в *.lpr файле.

Старый способ

Использование нескольких модулей

В вашем приложении может быть несколько модулей. Щелкните меню "File", затем щелкните "New...". В появившемся диалоговом окне выберите "Web Module" в дереве выбора модулей.

Add new web module

затем нажмите OK.

Поскольку в вашем приложении существует несколько модулей, вы больше не можете делать запрос (request) только с использованием /. Платформа не сможет волшебным образом выбрать, какой модуль должен обслуживать ответ (response), поэтому есть два способа указать, какой модуль вы хотите вызвать:

  • /<module name>
  • /?module=<module name>

Во втором формате вы можете изменить "module" (который является значением по умолчанию) на любой допустимый ключ строки запроса, изменив Application.ModuleVariable.

С использованием Actions

До сих пор мы использовали только веб-модули с обработчиком единого запроса. Это не сильно масштабируется, поскольку ваше веб-приложение становится все более и более сложным. Более того, некоторые функции могут иметь общие свойства и могут быть лучше логически сгруппированы, например:

  • Account module
    • Login action
    • Logout action
    • Register action
  • Product module
    • Create action
    • Update action
    • Delete action
    • Details action
Поток обработки запросов (request)

Прежде, чем использовать действие (action), важно знать, как обрабатывается запрос fpWeb. В противном случае ваше действие (action) может оказаться бесполезным, потому что ваш модуль данных всегда обрабатывает запрос. Как такое могло случится? Возвращаясь немного назад, вспомните Handled := true, которое мы всегда делали раньше? Теперь в игру вступает параметр Handled.

Каждый запрос сначала будет проходить через модуль OnRequest, независимо от запрошенного действия. Только если для этого не установлено значение Handled, выполняется OnRequest веб-действия.

В общем, поток запросов:

fpWeb request flow

Обратите внимание на окошко "Our Concern" (наша озабоченность), это то, на что мы собираемся обратить наше внимание.

Добавление действий в веб-модули

Чтобы добавить действие, выберите веб-модуль и перейдите в инспектор объектов. На вкладке свойств выберите "Actions" и нажмите кнопку во втором столбце.

Manage actions button in object inspector

Появится всплывающее окно, в котором вы можете добавлять, удалять и изменять порядок действий.

Manage actions button in popup window

Нажмите "Add", в списке появится новое действие. Выберите его, затем перейдите в инспектор объектов. В настоящее время он будет показывать свойства и события этого вновь созданного действия. Переименуйте свойство Name (это будет имя, которое вы напишете в URL-адрес, поэтому дайте ему короткое, простое, но описательное имя) как вы желаете, я выберу "Hello". Двигаемся дальше на вкладку событий, сделайте то же самое, что и OnRequest для модуля, нажмите кнопку справа от строки OnRequest, чтобы создать обработчик запросов.

Creating web action's OnRequest handler in the object inspector

Вы будете представлены в том же интерфейсе OnRequest, но этот обрабатывает веб-действие вместо веб-модуля. Все, что вы можете сделать в OnRequest веб-модуля, можно сделать и здесь. Скопируйте тело метода из 'Hello, World!' раздела.

Не забудьте удалить Handled := true из тела OnRequest предыдущего веб-модуля (или полностью удалить событие), чтобы действие позаботилось об обработке запроса.

Запустите свой проект и запустите браузер. Теперь, поскольку обработка запросов делегирована веб-действию, вы больше не можете просто запрашивать /, но вам нужно / <action name> или <свойство ActionVar модуля>=<action name>. Обратите внимание, что <свойство ActionVar модуля> по умолчанию имеет значение пустой строки, в отличие от Application.ModuleVariable, для которого "модуль" является значением по умолчанию. Итак, по умолчанию вы можете использовать только /<action name> формы.

Если у вас несколько модулей, то у вас есть множество вариантов:

  • /<module name>/<action name>
  • /<module name>?action=<action name>
  • /<action name>?module=<module name>
  • /?module=<module name>&action=<action name>

Обратите внимание, что как только у модуля есть хотя бы одно действие, /<module или action name> по умолчанию сопоставляется с /<action name>. Чтобы изменить поведение таким образом, чтобы по умолчанию оно отображалось как /<module name>, установите для параметра Application.PreferModuleName значение true. В случае нескольких модулей, если имя модуля не указано, модуль по умолчанию будет обрабатывать данное действие. Чтобы изменить поведение таким образом, что имя модуля задавалось явно, установите для Application.AllowDefaultModule значение false.

Следующие таблицы обобщают то, что произойдет на основе двух свойств:

/<module or action name> Application.PreferModuleName
true false
Application.AllowDefaultModule true /<module name> /<default module>/<action name>
false /<module name> ERROR
Действие по умолчанию

Помните предыдущую диаграмму, "Delegate request handling to actions" (Делегировать обработку запросов действиям) на самом деле не так просто, но если мы расширим эту диаграмму, изображение будет слишком большим, чтобы поместиться. Итак, вот схема этой части:

Request delegation to action flow

Две важные вещи из потока: DefActionWhenUnknown и действие по умолчанию. Первое является свойством веб-модуля, а второе соответствует свойству Default действия. В последнем случае, если имеется более двух действий, для которых свойство Default установлено в значение true, порядок действий (то, как он отображается во всплывающем окне управления действиями) будет учитываться для определения того, какое действие является действием по умолчанию. Эти два свойства определяют, что должно делать приложение, если для данного запроса не найдено подходящего действия.

Следующие таблицы обобщают то, что произойдет на основе двух свойств:

Запрос с недопустимым названием действия DefActionWhenUnknown
true false
Action.Default true Запрос обрабатывается действием по умолчанию Ошибка: действие не найдено для действия: <action name>
false Ошибка: неверное имя действия и нет действия по умолчанию Ошибка: действие не найдено для действия: <action name>


Запрос без имени действия, т.е.: / DefActionWhenUnknown
true false
Action.Default true Запрос обрабатывается действием по умолчанию Запрос обрабатывается действием по умолчанию
false Ошибка: нет имени действия и действия по умолчанию. Ошибка: нет имени действия и действия по умолчанию.

В случае приведенного выше ответа об ошибке должна следовать трассировка стека с информацией о номере строки, если вы создаете свое приложение с параметром -gl. Позже мы увидим, как создать собственный обработчик для этого (трассировка стека не годится для производства). Но пока убедитесь, что вы понимаете концепцию веб-модуля и веб-действия, особенно поток запросов. Играйте, пока не решите, что готовы к следующему разделу.

Новый механизм

Новый механизм чрезвычайно гибок и работает даже без модулей данных (старый механизм работает только с модулями данных).

Выделенный модуль для этой маршрутизации предоставляется как httproute (добавьте его в пункт использования программы/модуля, где вы хотите зарегистрировать маршруты). Модуль содержит функцию HTTPouter, которая возвращает одноэлементный объект, отвечающий за управление маршрутом приложения, и имеет метод RegisterRoute для регистрации вашего маршрута.

Синтаксис маршрута

Первый параметр HTTPouter.RegisterRoute — это маршрут, который будет сопоставляться с входящим запросом. Это может быть как простое * выше, что означает 0 или более путей или просто любые пути, так и сложное, как /api/v1/:resource/*, что означает, что заголовок REQUEST_URI должен начинаться с /api/v1/, за которым следует что-то еще, что будет привязано к переменной с именем resource и заканчиваться 0 или более путями. Он будет соответствовать:

  • /api/v1/products
  • /api/v1/products/1
  • /api/v1/products/1/clone
  • /api/v1/products/something/else/that/is/really/long/and/silly

но не:

  • /api/v1
  • /excuse/me/api/v1/products

В основном есть только 3 специальных символа:

  • *  обозначает 0 или более путей
  • :param  обозначающий часть
  • /  обозначающий разделитель частей

ваш маршрут будет состоять из этих 3 символов, а также всего остального, из чего состоит маршрут. 

Регистрация маршрута

2-й, 3-й или 4-й параметр (в зависимости от того, хотите ли вы обрабатывать конкретный метод HTTP и/или передавать ему дополнительные данные) HTTPRouter.RegisterRoute перегружен несколькими возможностями:

  • Процедура обратного вызова
TRouteCallback = Procedure(ARequest: TRequest; AResponse);
  • Событие обратного вызова
TRouteEvent = Procedure(ARequest: TRequest; AResponse) of object;
  • Объект, удовлетворяющий интерфейсу (CORBA)
IRouteInterface = Interface ['{10115353-10BA-4B00-FDA5-80B69AC4CAD0}']
  Procedure HandleRequest(ARequest: TRequest; AResponse: TResponse);
end;
  • Объект, расширяющий абстрактный класс маршрутизатора
TRouteObject = Class(TObject, IRouteInterface)
Public
  Procedure HandleRequest(ARequest: TRequest; AResponse: TResponse); virtual; abstract;
end;

TRouteObjectClass = Class of TRouteObject;

По умолчанию, если второй параметр не является TRouteMethod, будут совпадать все методы HTTP. Используйте один из rmUnknown, rmAll, rmGet, rmPost, rmPut, rmDelete, rmOptions, rmHead, rmTrace, чтобы соответствовать только определенному методу HTTP.

Порядок регистрации имеет значение. Если есть два или более маршрута, соответствующих текущему запросу, его обработает ранее зарегистрированный.

В этом новом механизме автономнаяй "Hello, World!"-программа может быть такой простой, как:

uses
  fphttpapp, httpdefs, httproute;
procedure DoHello(ARequest:TRequest; AResponse : TResponse);
begin
  AResponse.Content:='<html><body><h1>Hello,World!</h1></body></html>'
end;

begin
  HTTPRouter.RegisterRoute('*', @DoHello);
  Application.Port := 9000;
  Application.Initialize;
  Application.Run;
end.

Пример веб-сервера

Это пример простого кроссплатформенного многопоточного веб-сервера.

program webserver;
 
{$mode objfpc}{$H+}
 
uses
  {$ifdef UNIX}
    cthreads, cmem,
  {$endif} 
  fphttpapp, httpdefs, httproute;
 
procedure route1(aReq: TRequest; aResp: TResponse);
begin
  aResp.content:='<html><body><h1>Route 1 The Default</h1></body></html>'
end;
 
procedure route2(aReq: TRequest; aResp: TResponse);
begin
  aResp.content:='<html><body><h1>Route 2</h1></body></html>'
end;
 
begin
  HTTPRouter.registerRoute('/', @route1, true);
  HTTPRouter.registerRoute('/route2', @route2);
  Application.port := 8080;
  Application.threaded := true;
  Application.initialize;
  Application.run;
end.

Использование шаблонов

fpWeb имеет встроенную поддержку FPTemplate, универсального движка шаблонов Free Pascal. Его не обязательно использовать из контекста веб-приложения, но с интегрированной поддержкой все будет немного проще. По крайней мере, управление памятью можно игнорировать, так как об этом позаботится модуль.

Есть два уровня, на которых можно использовать шаблоны: действие и модуль. Полная поддержка RAD до конца не реализована, поэтому в некоторых моментах вам нужно перейти к ручному кодированию.

Существует два режима работы: непараметризованный и параметризованный. Активный режим управляется свойством AllowTagParams, которое должно быть достаточно очевидным, какое значение относится к какому режиму.

Строка шаблона может быть передана из файла через свойство FileName или прямая строка через свойство Template. Template.Template - я знаю, это звучит странно :) Если оба заполнены, то FileName будет иметь приоритет.

Два свойства: StartDelimiter и EndDelimiter определяют, как механизм должен распознавать тег шаблона. Например, если у вас есть:

  • StartDelimiter = '{+'
  • EndDelimiter = '+}'

затем строка '{+title+}' определяет тег шаблона с именем 'title'. Обратите внимание, что пробелы важны, поэтому '{+ title +}' определяет тег шаблона с именем ' title ', а не просто 'title'.

Специальные для параметризованного режима дополнительные три свойства: ParamStartDelimiter, ParamEndDelimiter и ParamValueSeparator определяют, как движок должен распознавать параметр тега шаблона. Например, если у вас есть:

  • ParamStartDelimiter = '[-'
  • ParamEndDelimiter = '-]'
  • ParamValueSeparator = '='

тогда строка '{+data [-p=v-][-a=b-] +}' определяет тег шаблона с именем 'data' с параметром 'p' значения 'v' и параметром 'a' значения ' б'. Это можно использовать для передачи параметра уровня шаблона, такого как ожидаемый формат даты, заголовок-строка-нижний колонтитул для настраиваемого выходного представления, имя файла и т. д., как вы решите.

Как следствие другого способа работы, основное событие, в котором работает шаблон, также отличается. Непараметризованный будет использовать OnGetParam, а параметризованный будет использовать OnReplaceTag. У них, конечно, другой интерфейс:

Type
  // OnGetParam: только для поддержки простого тега шаблона (напр: {Name})
  TGetParamEvent = Procedure(
    Sender: TObject;
    Const ParamName: String;
    Out AValue: String
  ) Of Object;
  // OnReplaceTag: для тегов с поддержкой параметров
  TReplaceTagEvent = Procedure(
    Sender: TObject;
    Const TagString: String;
    TagParams: TStringList;
    Out ReplaceText: String
  ) Of Object;

В OnGetParam вы проверяете ParamName, а затем соответственно назначаете AValue, т. е. если вы хотите, чтобы тег 'title' был заменен на 'My App', тогда заполните метод следующим образом:

// используйте Trim(), если вы хотите, чтобы пробелы вокруг тега были незначительными
case Trim(ParamName) of
  'title': AValue := 'My App';
else
  AValue := 'UNKNOWN';
end;

В OnReplaceTag вы проверяете TagString и, возможно, TagParams, а затем соответственно назначаете ReplaceText, т.е., если вы хотите, чтобы тег 'datetime' был заменен текущим временем с параметром 'datetimeformat', чтобы указать, как следует форматировать дату и время, заполните метод следующим образом:

// используйте Trim(), если вы хотите, чтобы пробелы вокруг тега были незначительными
case Trim(TagString) of
  'datetime': AValue := FormatDateTime(TagParams.Values['datetimeformat'],Now);
else
  AValue := 'UNKNOWN';
end;

На уровне действий

Создайте/выберите действие, затем перейдите в инспектор объектов. Вы увидите свойство подкомпонента с именем Template(Шаблон). Этот шаблон является обычным экземпляром TFPTemplate. Разверните его и заполните свойства, как описано выше. Теперь перейдите на вкладку Events(События), снова разверните Template, вы увидите два события. Заполните тот, который основан на вашем значении свойства AllowTagParams.

Light bulb  Примечание: Если ваш Lazarus не может автоматически заполнять событие, попробуйте ввести имя вручную в поле редактирования, а затем нажмите кнопку "...". Это ошибка в текущем Lazarus, которая может быть исправлена ​​в будущем.

На этом уровне шаблон с содержимым не устанавливается автоматически в качестве обработчика запроса. Это может измениться в будущем, но давайте разберемся с его текущим состоянием. Заполните событие OnRequest действия, затем заполните его:

with Actions.CurrentAction as TFPWebAction do
begin
  AResponse.Content := Template.GetContent;
end;
Handled := true;

Приведение необходимо, поскольку CurrentAction имеет тип TCustomWebAction вместо TFPWebAction. Без него мы не сможем получить доступ к свойству Template.

На уровне модуля

На уровне модуля вам в настоящее время придется делать это вручную, поскольку поддержка RAD не реализована. Связанным свойством является ModuleTemplate. Однако это не обычный экземпляр TFPTemplate, а специальный класс TFPWebTemplate, являющийся его потомком.

Идея здесь состоит в том, чтобы модуль предоставлял макет, а действия предоставляли контент с возможностью предоставления дополнительных переменных. Поэтому рекомендуется оставить AllowTagParams как есть и назначить событие OnGetParam только **модуля**. НЕ назначайте OnGetParam для ModuleTemplate, так как он никогда не будет вызываться.

Тег шаблона с именем 'content' будет автоматически заменен содержимым, созданным действием, все остальное заменяется либо из внутренних переменных шаблона, либо из OnGetParam.

Использование отдельного шаблона

Несмотря на несколько неполную интеграцию, ничто не мешает вам использовать fpTemplate (или любые другие решения для шаблонов) вручную, вне встроенной поддержки. В некоторых случаях это может быть даже лучше, поскольку оно будет модульным.

Использольвание html Producer object

Предполагая, что существует TWebAction с именем act_get_my_html, вы должны попросить написать в AResponse возврат внутреннего потока байтов: при ответе с помощью решения html Producer objects мы должны использовать методы писателя. Он управляет потоком памяти с целью ускорения. Здесь нет строкового типа, как в приведенном выше решении fpTemplate, которое использует обработку текста строки, как обычно в Php. Producer записывает в AResponse рекурсивную итерацию " foreach ", которая проходит по дереву полиморфных объектов HTML Dom, состоящему из иерархии классов THtmlCustomElement = Class(TDOMElement)(см. модуль htmlelements.pp). Итак, AResponse пишется параметром aWriter, со следующим вызовом:

procedure TFPWebModule1.act_get_my_htmlRequest(Sender: TObject; ARequest: TRequest; AResponse: TResponse; var Handled: Boolean);
begin
  (* спрашиваем у HTMLEntityProducer1 содержимое своих html-элементов DOM: *)
  HTMLEntityProducer1.HandleRequest(ARequest, AResponse, Handled);
  Handled := True;
end;

Повторим еще раз, что работа Producer заключается в преобразовании потока внутренней памяти в текст в объекте ответа: именно так был разработан этот шаблон, хотя существует метод рендеринга text-html с именем ProduceContent только для целей отладки. Вы можете переопределить этот метод, если вы пишете или отлаживаете компонент Producer.

Вот пример с html-объектом Producer, позволяющий создать веб-приложение «способом RAD», то есть с помощью перетаскивания компонента HTMLEntityProducer из палитры:

procedure TFPWebModule1.HTMLEntityProducer1WriteEntity(Sender: THTMLContentProducer; aWriter: THTMLWriter);
begin

  aWriter.startHeader;
    aWriter.Meta('','Content-Type','text/html; charset=UTF-8');
    aWriter.title('My web page');
    aWriter.link('stylesheet','stylesheet.css','text/css','screen');
  aWriter.EndHeader;

  aWriter.Startbody;
    aWriter.Startparagraph;
      aWriter.heading2('Hello, world from inside §1:');
      aWriter.Text('Here is text written inside the current paragraph.');
    aWriter.Endparagraph;
    aWriter.paragraph('This is another text written inside a self "started and ended" paragraph.');
    aWriter.Startparagraph;
      aWriter.heading2('Hello, world from inside §2:');
      aWriter.Text('Here is the final text.');
      aWriter.Image.src := 'logo.png';
      AnotherProducer.WriteContent(aWriter);
    aWriter.Endparagraph;
  aWriter.Endbody;

end;

Свойство TWebAction.ContentProducer позволяет связать THTTPContentProducer с его веб-действием, предоставляемым через URI в сети.

[ToDo: нет официальной документации о компонентах «способ RAD» fpWeb (html producer, html provider, html adapter, html formatter и т.д.)]

Tips and Tricks

Returning Different HTTP Response Code

By default, fpWeb will return HTTP 200 OK to indicate successful request handling. This surely is not always the case, as user input might not be as what we expected. To do so, set AResponse.Code in your request handler to the code you want to return.

Redirect Request to Different URL

A common flow after a successful login is to redirect user to his account page. This can be done by calling AResponse.SendRedirect in your request handler, supplying the URL to redirect request to.

Serving Static Files (Embedded Web Server)

Remember the dialog in the #Hello, World! section after you select HTTP server Application? If you tick "Register location to serve files from" you can fill "Location" (the URI segment, must not contain any slashes) and "Directory" (physical directory in your computer, must exist at runtime) and the wizard will simply add:

RegisterFileLocation('<Location>','<Directory>');

to the beginning of your .lpr and add the unit fpwebfile to the uses clause. You can actually do this by hand anytime and also register multiple times for different locations / directories. After this you can request /<Location>/<any filename under Directory> and it will be served automatically. Note that the mimetype of the file is determined by fpmimetypes. Call MimeTypes.LoadFromFile with your mime.types file in order to give correct mimetype based on its extension. Otherwise, the file will always be served as application/octet-stream which means the browser will download it instead of interpreting it (especially important for JavaScript and CSS files).

You can grab a complete mime.types here http://svn.apache.org/viewvc/httpd/httpd/trunk/docs/conf/mime.types?&view=co

In Lazarus 2.0.6 or newer you must add at the top of your program the full path of the mime.types file

begin
  MimeTypesFile := Application.Location + 'mime.txt';

Take into consideration that the default path coming with the project is lib\$(TargetCPU)-$(TargetOS)

For example httpproject\lib\i386-win32\mime.txt

Centralize Management of Configuration and Modules

By default, the program file (.lpr) is the one that contains protocol unit. This limits the ability to use Application object from other contexts such as from web modules. Fortunately, it's not difficult to refactor to have what we want. We remove RegisterHTTPModule calls from web modules' units and left out the .lpr to empty main block with single unit identifier in the uses clause, we name it: brokers. The unit contains:

unit Brokers;

{$mode objfpc}{$H+}

interface

{ $define cgi}
{ $define fcgi}
{$define httpapp}

uses
  CustWeb;

function GetApp: TCustomWebApplication; inline;

implementation

uses
  {$ifdef cgi}fpcgi{$endif}
  {$ifdef fcgi}fpfcgi{$endif}
  {$ifdef httpapp}fphttpapp{$endif}
  ,webmodule1
  ,webmodule2
  ;

function GetApp: TCustomWebApplication;
begin
  Result := Application;
end;

initialization
  RegisterHTTPModule('wm1', TWebModule1);
  RegisterHTTPModule('wm2', TWebModule2);
  {$ifndef cgi}
  Application.Port := 2015;
  {$endif}
  Application.Initialize;
  Application.Run;
end.

This way, we can control over web module registration and also provide an API to get Application object (casted as TCustomWebApplication), while still easily switch between protocol implementations, in a single place.

Terminating Gracefully (FastCGI / Embedded Web Server)

Instead of Ctrl+C-ing your app, there is a way for your app to terminate gracefully, doing whatever cleanup it needs, by calling Application.Terminate. You might need to use previous trick to easily access the Application object. A common implementation is to provide a specific password protected module / action that calls the Terminate method. You may choose whatever way you want, though.

Custom Exception Handler

[edit the May 1, 2020 => moved from a method pointer to a simple procedure.]

To override the default exception handler, which prints stacktrace whenever an exception is raised (i.e.: on HTTP 404 or 500), and thus not good for production, you must assign Application.OnShowRequestException.

This is a method so you will need to provide your procedure that implements the method and assign it by using the object. i.e.: if you have MyExceptionHandler as an object of TMyExceptionHandler which has MyShowRequestException method, you can assign it by:

Application.OnShowRequestException := @MyExceptionHandler.MyShowRequestException;

don't forget to .Create() MyExceptionHandler BEFORE assigning above or you will get an EAccessViolation!

You must provide your global procedure that implements your own exception handler (in production, it is advisable to replace the call stack by an HTTP status code and its explanation). Then, you can override the default exception handler, by assigning it like this:

Application.OnShowRequestException := @MyShowRequestException;

Pure Hand Coding (No Form Designer Required)

It's not a must to use Lazarus' form designer to write an fpWeb application. You can use pure hand coding technique to write it. The secret lies in the 3rd parameter of RegisterHTTPModule : SkipStreaming. When this parameter is set to true, fpWeb will not search for .lfm resource. Therefore everything must be manually handled: property settings, event handlers, action registration, etc.

Note that it's logical to do what's usually done through object inspector in an overriden constructor. Inside it, call the inherited constructor supplying both AOwner and CreateMode as parameters. After that you can set properties, assign event handlers, etc. Example:

type
  THelloWorldModule = class(TFPWebModule)
    constructor CreateNew(AOwner: TComponent; CreateMode: Integer); override;
    procedure Request(Sender: TObject; ARequest: TRequest;
      AResponse: TResponse; var Handled: Boolean);
  end;

constructor THelloWorldModule.CreateNew(AOwner: TComponent; CreateMode: Integer);
begin
  inherited CreateNew(AOwner,CreateMode);
  OnRequest := @Request;
end;

procedure THelloWorldModule.Request(Sender: TObject; ARequest: TRequest;
 AResponse: TResponse; var Handled: Boolean);
begin
  AResponse.Content := 'Hello, World!';
  Handled := true;
end;


See also

  • Lazarus for the web (tutorial)
  • Lazarus for the web: Sessions and Templates (tutorial) This article explains a TSQLDBSession Class that manages a SQL connection, that inherits from a web session (in order to manage the histories of web session variables, within a connection-session to a database having row-locking capabilities).
  • the examples - read the text files - in your directory ...\fpc\x.x.x\source\packages\fcl-web\examples.

Screenshot copy of one of the examples

  • XML or JSON, on ExtJS: if you want to use a more or less complex engine that is very "accomplished" regarding the rendering aspect of Javascript objects on the browser side, in addition to their functionalities (like ExtJS, for example, to display a db-aware grid, dynamic graphics, etc), this kind of solution very very often expects an XML or a JSON file. There are Adapter and Formatter classes (e.g. the classes TExtJSXMLWebdataInputAdaptor, TExtJSJSONDataFormatter).

Overall, the Adapter classes adapt the nodes of an incoming jSon or XML to a mapping of their TField database fields. And Formatter classes are mappers of each TField of a record to its node in jSon or XML format, before sending it out towards the browser.