forked from pcl-ru/pcl-ru
-
Notifications
You must be signed in to change notification settings - Fork 0
/
chapter-26.tex
1149 lines (961 loc) · 79.2 KB
/
chapter-26.tex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
\chapter{Практика. Веб-программирование с помощью AllegroServe}
\label{ch:26}
\thispagestyle{empty}
В~этой главе вы ознакомитесь с одним из способов разработки веб-приложений на Common Lisp:
используя AllegroServe~-- веб-сервер с открытым исходным кодом. Это не означает, что вы
найдёте здесь исчерпывающее введение в AllegroServe. И я определённо не собираюсь
описывать ничего более, чем небольшую часть огромной темы веб-программирования. Моей целью
является описание достаточного количества базовых вещей по части использования
AllegroServe, которые позволят нам в главе~\ref{ch:29} разработать приложение для
просмотра библиотеки MP3-файлов и проигрывания их на MP3-клиенте. Кроме того, данная глава
может служить кратким введением в веб-программирование для новичков.
\section{30-секундное введение в веб-программирование на стороне сервера}
Хотя в настоящее время веб-программирование обычно означает использование одного из
доступных программных каркасов (frameworks) и различных протоколов, основы
веб-программирования не особо изменились с момента их появления в начале 1990-х. Для
простых приложений, таких как мы напишем в главе~\ref{ch:29}, вам необходимо понять только
несколько основных концепций, так что в этом разделе я сделаю их быстрый обзор. Опытные
веб-программисты могут лишь просмотреть, а то и вовсе пропустить этот
раздел\pclfootnote{Новичкам в веб-программировании, вероятно, понадобится дополнить это
введение информацией из одного-двух учебников с более глубоким охватом. Вы можете найти
хорошую подборку доступных online учебников по адресу
\url{http://www.jmarshall.com/easy/}.}.
Для начала вам необходимо понимание ролей веб-браузера и веб-сервера в
веб-программировании. Хотя современные браузеры поставляются с кучей свистелок и дуделок,
основной функциональностью веб-браузера являются запрос веб-страниц с веб-сервера и их
отображение. Обычно эти страницы пишутся на Hypertext Markup Language (HTML, язык разметки
гипертекста), который указывает браузеру, как отображать страницу, включая информацию о
том, где вставить изображения и ссылки на другие страницы. HTML состоит из текста,
\textit{размеченного} с помощью \textit{тегов}, которые структурируют текст, а эту
структуру браузер использует при отображении страницы. Например, простой HTML-документ
выглядит вот так:
\begin{lstlisting}[language=HTML]
<html>
<head>
<title>Hello</title>
</head>
<body>
<p>Hello, world!</p>
<p>This is a picture: <img src="some-image.gif"></p>
<p>This is a <a href="another-page.html">link</a> to another page.</p>
</body>
</html>
\end{lstlisting}
Рис.~\ref{fig:26-1}, показывает как браузер отображает эту страницу.
\begin{figure}[htb]
\centering
\includegraphics[scale=0.7]{images/new-sample.jpg}
\caption{Пример веб-страницы}
\label{fig:26-1}
\end{figure}
Браузер и сервер общаются между собой, используя протокол, называемый Hypertext Transfer
Protocol (HTTP, протокол передачи гипертекста). Хотя вам не нужно беспокоиться
относительно деталей протокола, полезным будет понимание того, что он полностью состоит из
последовательности запросов, инициированных браузером, и ответов, сгенерированных
сервером. Таким образом, браузер подключается к веб-серверу и посылает запрос, который
включает в себя, как минимум, адрес желаемого ресурса (URL) и версию протокола HTTP,
используемую браузером. Браузер также может включать в запрос дополнительные данные; таким
образом браузер отправляет HTML-формы на сервер.
Для ответа на запрос сервер отправляет ответ, состоящий из наборов заголовков и тела
ответа. Заголовки содержат информацию о теле, такую как тип его данных (например, HTML,
обычный текст или изображение), а тело ответа содержит сами данные, которые затем
отображаются браузером. Сервер также может отправлять ответ-ошибку, который сообщит
браузеру о том, что его запрос не может быть обработан по некоторой причине.
И это почти все. После того как браузер получил завершённый ответ от сервера, между
сервером и браузером не происходит никакого общения до тех пор, пока браузер не решит
запросить страницу у сервера в следующий раз\pclfootnote{Загрузка отдельной веб-страницы
может привести к выполнению множества запросов~-- для отображения HTML-страницы,
содержащей изображения, браузер должен выполнить отдельный запрос для каждого из них, а
затем вставить их на соответствующие места страницы.}. Это основное ограничение
веб-программирования~-- для кода, выполняемого на сервере, не существует способа
воздействовать на то, что пользователь увидит в браузере, до тех пор, пока браузер не
сделает новый запрос к серверу\pclfootnote{Большая часть сложности веб-программирования
заключается в попытках обойти это основное ограничение, чтобы предоставить пользователю
больше возможностей, таких как интерактивность приложений, выполняющихся на компьютере
пользователей.}.
Некоторые веб-страницы, называемые \textit{статическими} (\textit{static}) страницами,
являются просто файлами с разметкой на языке HTML, хранимыми на веб-сервере и считываемыми,
когда приходит соответствующий запрос. \textit{Динамические} (\textit{dynamic}) страницы,
с другой стороны, состоят из HTML, генерируемого при каждом запросе страницы
браузером. Например, динамическая страница может быть сгенерирована путём осуществления
запроса к базе данных, а затем конструирования HTML для представления результатов этого
запроса\pclfootnote{К сожалению, слово \textit{динамичный} (\textit{dynamic}) имеет много
значений в мире веб. Фраза <<динамичный HTML>> (\textit{Dynamic HTML}) относится к HTML,
который содержит встроенный код, обычно на языке JavaScript, который может выполняться
браузером без дополнительного общения с сервером. При осторожном использовании
динамичный HTML может улучшить работу веб-приложения, поскольку даже при использовании
высокоскоростных соединений выполнение запроса к серверу, получение результата и
отображение новой страницы может занять заметное количество времени. Что ещё более
запутывает дело, динамически генерируемые страницы (страницы, генерируемые на сервере)
могут также содержать динамичный HTML (код для выполнения на клиенте). В~этой книге мы
столкнёмся только с динамически генерируемым обычным, нединамичным HTML.}.
При генерации ответа на запрос код, выполняемый на сервере, получает четыре основные
части информации, на основании которой он работает. Первой является запрошенный адрес
(URL). Но обычно URL используется самим веб-сервером для определения того, какой код
ответствен за генерацию ответа. Затем если URL содержит знак вопроса, то все, что
следует за ним, рассматривается как \textit{строка запроса} (\textit{query string}),
которая обычно игнорируется самим веб-сервером и передаётся им коду, который будет
генерировать ответ. В~большинстве случаев строка запроса содержит набор пар
имя/значение. Запрос от браузера может также содержать \textit{POST-данные}, которые также
обычно состоят из пар имя/значение. POST-данные обычно используются для передачи
содержимого форм HTML. Пары имя/значение, переданные либо через строку запроса, либо через
дополнительные данные, обычно называют \textit{параметрами запроса} (\textit{query
parameters}).
В~заключение, для того чтобы связать между собой последовательность отдельных запросов от
одного и того же браузера, код, выполняющийся на сервере, может установить \textit{cookie}
путём отправки специального заголовка в своём ответе браузеру; этот заголовок будет
содержать некий набор данных. После установки cookie браузер будет слать его при каждом
запросе, отправляемом этому серверу. Браузер не заботится о данных, хранимых в cookie,~--
он просто отправляет их обратно на сервер, и код, работающий там, может обрабатывать их
так, как захочет.
Это все базовые элементы, на основании которых основано 99 процентов кода, выполняемого на
веб-сервере. Браузер отправляет запрос, сервер находит код, который будет обрабатывать
запрос, и запускает его, а код использует параметры запроса и cookies для определения
того, что именно нужно сделать.
\section{AllegroServe}
Вы можете отдавать веб-страницы с помощью Common Lisp разными способами; существует, по
крайней мере, три реализации веб-серверов с открытым исходным кодом, написанных на Common
Lisp, а также подключаемые модули, такие как
\pclURL{http://www.fractalconcept.com/asp/html/mod\_lisp.html}{mod\_lisp} и
\pclURL{http://lisplets.sourceforge.net/}{Lisplets}\hspace{-0.25em}, которые позволяют веб-серверу Apache
или любому контейнеру Java Servlet делегировать обработку запросов серверу Lisp,
работающему в отдельном процессе.
В~этой главе мы будем использовать веб-сервер с открытым исходным кодом AllegroServe,
изначально написанный John Foderaro из Franz Inc. AllegroServe включён в версию Allegro,
доступную с сайта Franz для использования с этой книгой. Если вы не используете Allegro,
то вы можете использовать PortableAllegroServe, ответвление (fork) кода AllegroServe,
которое включает в себя уровень (layer) совместимости с Allegro, что позволяет
PortableAllegroServe работать почти на всех реализациях Common Lisp. Код, который мы
напишем в этой главе и в главе~\ref{ch:29}, должен работать как на стандартном
AllegroServe, так и на PortableAllegroServe.
AllegroServe реализует модель программирования, сходную по духу с Java Servlets~-- каждый
раз, когда браузер запрашивает страницу, AllegroServe разбирает запрос и ищет объект,
называемый \textit{сущностью} (\textit{entity}), который будет обрабатывать
запрос. Некоторые классы сущностей, предоставляемые как часть AllegroServe, умеют
обрабатывать статическое содержимое: либо отдельные файлы, либо содержимое
каталога. Другие, которые я буду обсуждать большую часть главы, запускают произвольный код
на Lisp для генерации ответа\pclfootnote{AllegroServe также предоставляет каркас
(framework), названный \textit{Webactions}, который аналогичен JSP в Java~-- вместо
написания кода, который генерирует HTML, с помощью Webactions вы можете писать страницы,
которые являются HTML, но с небольшим количеством специального кода, разворачивающегося
в настоящий код при обработке страницы. В~этой книге я не буду описывать Webactions.}.
Но, перед тем как начать, вам необходимо знать, как запускать AllegroServe и как настроить
его для обработки файлов. Первым шагом является загрузка кода AllegroServe в ваш образ
Lisp. В~Allegro вы можете просто набрать \lstinline{(require :aserve)}. В~других реа\-ли\-за\-циях
Lisp (а также в Allegro) вы можете загрузить PortableAllegroServe путём загрузки файла
\lstinline{INSTALL.lisp}, находящегося в корне каталога \lstinline{portableaserve}. Загрузка
AllegroServe создаст три новых пакета: \lstinline{NET.ASERVE}, \lstinline{NET.HTML.GENERATOR} и
\lstinline{NET.ASERVE.CLIENT}\pclfootnote{Загрузка PortableAllegroServe также создаст
дополнительные пакеты для библиотек, обес\-пе\-чи\-ваю\-щих совместимость, но нас в основном
интересуют три вышеперечисленных пакета.}.
После загрузки сервера вы можете запустить его с помощью функции \lstinline{start} из пакета
\lstinline{NET.ASERVE}. Чтобы иметь простой доступ к символам, экспортированным из пакета
\lstinline{NET.ASERVE}, из пакета \lstinline{COM.GIGAMONKEYS.HTML} (который мы скоро обсудим), а
также остальных частей Common Lisp, нам нужно создать новый пакет:
\begin{myverb}
CL-USER> (defpackage :com.gigamonkeys.web
(:use :cl :net.aserve :com.gigamonkeys.html))
#<The COM.GIGAMONKEYS.WEB package>
\end{myverb}
Теперь переключитесь на этот пакет с помощью следующего выражения \lstinline{IN-PACKAGE}:
\begin{myverb}
CL-USER> (in-package :com.gigamonkeys.web)
#<The COM.GIGAMONKEYS.WEB package>
WEB>
\end{myverb}
Теперь мы можем использовать имена, экспортированные из \lstinline{NET.ASERVE}, без указания
квалификатора. Функция \lstinline{start} запускает сервер. Она принимает множество именованных
параметров, но единственный нужный нам~-- \lstinline{:port}, который указывает номер порта, на
котором сервер будет принимать запросы. Возможно, вам понадобится использовать большой
номера порта, такой как 2001, вместо порта по умолчанию для HTTP-серверов, 80, поскольку в
Unix-подобных операционных системах только администратор (root) может использовать порты с
номером меньше 1024. Для запуска AllegroServe на порту 80 под Unix вам необходимо
запустить Lisp с правами администратора (root), а затем использовать параметры
\lstinline{:setuid} и \lstinline{:setgid} чтобы заставить \lstinline{start} переключить пользовательский
контекст после открытия этого порта. Вы можете запустить сервер на порту 2001 с помощью
следующей команды:
\begin{myverb}
WEB> (start :port 2001)
#<WSERVER port 2001 @ #x72511c72>
\end{myverb}
Теперь сервер выполняется в вашей среде Lisp. Возможно, что при попытке запуска сервера вы
получите ошибку вида <<port already in use>>. Это означает, что данный порт уже
используется каким-то сервером на вашей машине. В~таком случае самым простым решением
будет использование другого порта, передав другой аргумент функции \lstinline{start}, а затем
использование нового значения во всех адресах, встречаемых на протяжении данной главы.
Вы можете продолжить взаимодействие с Lisp с помощью REPL, поскольку AllegroServe
запускает отдельные нити для обработки запросов браузеров. Это означает, среди прочего,
что вы можете использовать REPL для того, чтобы заглянуть во <<внутренности>> сервера во
время его работы, что делает тестирование и отладку намного более лёгкой, по сравнению с
тем, когда сервер представляет собой <<чёрный ящик>>.
Предполагая, что вы запустили Lisp на той же машине, где находится и ваш браузер, вы
можете проверить, что сервер запущен, путём перехода в браузере по следующему адресу:
\url{http://localhost:2001/}. В~данный момент вы получите в браузере сообщение об ошибке
\lstinline{page-not-found} (страница не найдена), поскольку вы пока ничего не опубликовали. Но
сообщение об ошибке придёт от AllegroServe; это видно по строке внизу страницы. С другой
стороны, если браузер отображает ошибку, сообщающую что-то вроде <<The connection was
refused when attempting to contact localhost:2001>>, то это означает, что сервер не
запущен, или вы запустили его на порту с номером, отличным от 2001.
Теперь мы можем публиковать файлы. Предположим, что у нас есть файл \lstinline{hello.html} в
каталоге \lstinline{/tmp/html} со следующим содержимым:
\begin{lstlisting}[language=HTML]
<html>
<head>
<title>Hello</title>
</head>
<body>
<p>Hello, world!</p>
</body>
</html>
\end{lstlisting}
Вы можете опубликовать его с помощью функции \lstinline{publish-file}.
\begin{myverb}
WEB> (publish-file :path "/hello.html" :file "/tmp/html/hello.html")
#<NET.ASERVE::FILE-ENTITY @ #x725eddea>
\end{myverb}
Аргумент \lstinline{:path} задаёт путь, который будет использоваться в URL, запрашиваемом
браузером, а аргумент \lstinline{:file} является именем файла на файловой системе. После
вычисления выражения \lstinline{publish-file} вы можете задать в браузере адрес
\url{http://localhost:2001/hello.html}, и он должен отобразить что-то наподобие того, что
изображено на рис.~\ref{fig:26-2}.
\begin{figure}[htb]
\centering
\includegraphics[scale=0.7]{images/hello-world.jpg}
\caption{\url{http://localhost:2001/hello.html}}
\label{fig:26-2}
\end{figure}
Вы также можете опубликовать целый каталог с помощью функции \lstinline{publish-directory}. Но
сначала давайте избавимся от уже опубликованной сущности с помощью следующего вызова
\lstinline{publish-file}:
\begin{myverb}
WEB> (publish-file :path "/hello.html" :remove t)
NIL
\end{myverb}
Теперь вы можете опубликовать каталог \lstinline{/tmp/html/} целиком (включая его подкаталоги)
с помощью функции \lstinline{publish-directory}.
\begin{myverb}
WEB> (publish-directory :prefix "/" :destination "/tmp/html/")
#<NET.ASERVE::DIRECTORY-ENTITY @ #x72625AA2>
\end{myverb}
В~данном случае аргумент \lstinline{:prefix} указывает начало пути адресов URL, которые будут
обрабатываться данной сущностью. Так что если сервер получает запрос
\url{http://localhost:2001/foo/bar.html}, то путь будет \lstinline{/foo/bar.html}, который
начинается с \lstinline{/}. Затем этот путь транслируется в имя файла путём замены префикса
(\lstinline{/}) на аргумент \lstinline{:destination} (\lstinline{/tmp/html/}). Так что URL
\url{http://localhost:2001/hello.html} будет преобразован в запрос файла
\lstinline{/tmp/html/hello.html}.
\section{Генерация динамического содержимого с помощью AllegroServe}
Публикация сущностей, генерирующих динамическое содержимое, практически так же проста, как
и публикация статического содержимого. Функции \lstinline{publish} и \lstinline{publish-prefix}
являются <<динамическими>> аналогами \lstinline{publish-file} и
\lstinline{publish-directory}. Основная их идея заключается в том, что вы публикуете функцию,
которая будет вызываться для генерации ответа на запрос либо к определённому адресу
(URL), либо к любому адресу с заданным префиксом. Эта функция будет вызвана с двумя
аргументами: объектом, представляющим запрос, и опубликованной сущностью. Большую часть
времени нам не нужно будет ничего делать с опубликованной сущностью, за исключением её
передачи набору макросов, которые вскоре будут описаны. С другой стороны, мы будем
использовать объект запроса для получения информации, переданной браузером: параметров
запроса, переданных в строке URL, или данных, посланных формами HTML.
В~качестве простого примера использования функции для генерации динамического
содержимого давайте напишем функцию, которая будет генерировать страницу с различными
случайными числами при каждом обращении к ней.
\begin{myverb}
(defun random-number (request entity)
(with-http-response (request entity :content-type "text/html")
(with-http-body (request entity)
(format
(request-reply-stream request)
"<html>~@
<head><title>Random</title></head>~@
<body>~@
<p>Random number: ~d</p>~@
</body>~@
</html>~@
"
(random 1000)))))
\end{myverb}
Макросы \lstinline{with-http-response} и \lstinline{with-http-body} являются частью
AllegroServe. Первый из макросов начинает процесс генерации ответа HTTP и может быть
использован, так же как в нашем примере, для указания таких вещей, как тип возвращаемых
данных. Он также обрабатывает различные требования, указанные в стандарте HTTP, такие как
обработка запросов \lstinline{If-Modified-Since}. Макрос же \lstinline{with-http-body} фактически
отправляет заголовки HTTP-ответа, а затем вычисляет своё тело, которое должно содержать
код, генерирующий содержимое ответа. Внутри \lstinline{with-http-response}, но перед
\lstinline{with-http-body} вы можете добавить или изменить значение заголовков HTTP, которые
будут отправлены в ответе. Функция \lstinline{request-reply-stream} также является частью
AllegroServe и возвращает поток, в который вы должны записывать данные, предназначенные для
отправления браузеру.
Как видно из этой функции, вы можете просто использовать \lstinline{FORMAT} для вывода HTML в
поток, возвращённый вызовом \lstinline{request-reply-stream}. В~следующем разделе я покажу вам
более удобные способы программной генерации HTML\footnote{Спецификатор \lstinline{~@}, за
которым следует знак новой строки, заставляет \lstinline{FORMAT} игнорировать пробельные
знаки после этого знака новой строки, что позволяет вам красиво отформатировать код без
фактического добавления пробельных знаков в HTML. Поскольку пробельные знаки обычно не
являются значимыми в HTML, это никак не влияет на работу браузера, но делает
сгенерированный код HTML более удобным для чтения людьми.}\hspace{\footnotenegspace}.
Теперь мы готовы к публикации данной функции.
\begin{myverb}
WEB> (publish :path "/random-number" :function 'random-number)
#<COMPUTED-ENTITY @ #x7262bab2>
\end{myverb}
Так же как и в функции \lstinline{publish-file}, аргумент \lstinline{:path} указывает <<путевую
часть>> (path part) адреса, указание которой будет приводить к вызову данной функции.
Аргумент \lstinline{:function} указывает либо имя функции, либо сам функциональный объект.
Использование имени функции, как показано в этом примере, позволяет вам в дальнейшем
переопределить функцию, не выполняя заново процесса публикации, для того чтобы AllegroServe
стал использовать новое определение. После вычисления данного вызова вы можете указать в
браузере адрес http://localhost:2001/random-number для получения страницы со случайным
числом, как это показано на рис.~\ref{fig:26-3}.
\begin{figure}[htb]
\centering
\includegraphics[scale=0.7]{images/random-number.jpg}
\caption{\url{http://localhost:2001/random-number}}
\label{fig:26-3}
\end{figure}
\section{Генерация HTML}
Хотя использование \lstinline{FORMAT} для генерации HTML вполне приемлемо для
генерации простых страниц наподобие приведённой выше, по мере того как вы начнёте
создавать более сложные, было бы лучше иметь более краткий способ генерации HTML. Для
генерации HTML из представления в виде s-выражений существует несколько библиотек,
включая \lstinline{htmlgen}, которая поставляется вместе с AllegroServe. В~этой главе мы будем
применять библиотеку FOO\pclfootnote{FOO~-- это рекурсивный тавтологический
акроним для <<FOO Outputs Output>>.}, которая использует примерно ту же модель, что и
\texttt{htmlgen}, и чью реализацию мы рассмотрим более подробно в главах~\ref{ch:30}
и~\ref{ch:31}. Сейчас, однако, нам нужно лишь знать, как использовать FOO.
Генерирование HTML из Lisp вполне естественно, так как s-выражения и HTML по своему
существу изоморфны. Мы можем представить HTML-элементы с помощью s-выражений, рассматривая
каждый элемент HTML как список, <<помеченный>> соответствующим первым элементом, таким как
ключевым символом с таким же именем, что имеет тег HTML. Таким образом, HTML
\lstinline!<p>foo</p>! представляется s-выражением \lstinline{(:p "foo")}. Так как элементы
HTML, так же как и списки в s-выражениях, могут быть вложенными, эта схема распространяется
и на более сложный HTML. Для примера вот такой HTML:
\begin{lstlisting}[language=HTML]
<html>
<head>
<title>Hello</title>
</head>
<body>
<p>Hello, world!</p>
</body>
</html>
\end{lstlisting}
\noindent{}может быть представлен в виде следующего s-выражения:
\begin{myverb}
(:html
(:head (:title "Hello"))
(:body (:p "Hello, world!")))
\end{myverb}
Элементы HTML с атрибутами несколько усложняют дело, но не создают непреодолимых
проблем. FOO предоставляет два способа включения атрибут в тег. Одним из них
является добавление после первого элемента списка пар ключ/значение. Первый же элемент,
который следует за парами ключ/значение и который сам не является ключевым символом,
обозначает начало содержимого элемента. Таким образом, такой HTML:
\begin{lstlisting}[language=HTML]
<a href="foo.html">This is a link</a>
\end{lstlisting}
\noindent{}будет представляться следующим s-выражением:
\begin{myverb}
(:a :href "foo.html" "This is a link")
\end{myverb}
Другой предоставляемый FOO синтаксис заключается в группировке имени тега и его
атрибутов в отдельный список следующим образом:
\begin{myverb}
((:a :href "foo.html") "This is link.")
\end{myverb}
FOO может использовать представление HTML в виде s-выражений двумя
способами. Функция \lstinline{emit-html} получает представляющее HTML s-выражение и выводит
соответствующий HTML.
\begin{myverb}
WEB> (emit-html '(:html (:head (:title "Hello")) (:body (:p "Hello, world!"))))
<html>
<head>
<title>Hello</title>
</head>
<body>
<p>Hello, world!</p>
</body>
</html>
T
\end{myverb}
Однако \lstinline{emit-html} не всегда является наиболее эффективным способом генерации
HTML, так как её аргументом должно быть законченное s-выражение, предоставляющее HTML,
который нужно сгенерировать. Хотя генерировать такое представление легко, действовать так
не всегда эффективно. Например, представим, что нам нужно сгенерировать страницу HTML,
содержащую список из 10~000 случайных чисел. Мы можем построить соответствующее
s-выражение, используя шаблон квазицитирования, а затем передать его в функцию
\lstinline{emit-html}:
\begin{myverb}
(emit-html
`(:html
(:head
(:title "Random numbers"))
(:body
(:h1 "Random numbers")
(:p ,@(loop repeat 10000 collect (random 1000) collect " ")))))
\end{myverb}
Однако этот код должен построить дерево, содержащее 10000-элементный список, перед тем как
он сможет даже начать генерировать HTML, и все это s-выражение станет мусором, как только
HTML будет сгенерирован. Для избежания такой неэффективности FOO также
предоставляет макрос \lstinline{html}, позволяющий вам встраивать произвольный код Lisp в
середину s-выражения, по которому будет генерироваться HTML.
Литеральные значения, такие как строки и числа, входа \lstinline{html} будут подставлены в
генерируемый HTML. Также символы интерпретируются как ссылки на переменные, которые они
именуют, и сгенерированный код будет брать их значения во время выполнения. Таким образом,
оба следующих выражения:
\begin{myverb}
(html (:p "foo"))
(let ((x "foo")) (html (:p x)))
\end{myverb}
\noindent{}сгенерируют следующее:
\begin{lstlisting}[language=HTML]
<p>foo</p>
\end{lstlisting}
Формы списков, которые не начинаются с ключевых символов, рассматриваются как код и
встраиваются в генерируемый код. Любые значения, возращаемые встроенным кодом, будут
проигнорированы, но такой код сам может генерировать HTML путём вызова
\lstinline{html}. Например, для генерации содержимого списка в виде HTML мы можем написать
следующее:
\begin{myverb}
(html (:ul (dolist (item (list 1 2 3)) (html (:li item)))))
\end{myverb}
\noindent{}что сгенерирует следующий HTML:
\begin{lstlisting}[language=HTML]
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
\end{lstlisting}
Если вы захотите выдать значение формы, вы должны обернуть его в псевдотег
\lstinline{:print}. Таким образом, выражение
\begin{myverb}
(html (:p (+ 1 2)))
\end{myverb}
\noindent{}сгенерирует такой HTML после вычисления и отбрасывания значения \lstinline{3}:
\begin{lstlisting}[language=HTML]
<p></p>
\end{lstlisting}
Для выдачи \lstinline{3} вы должны написать такой код:
\begin{myverb}
(html (:p (:print (+ 1 2))))
\end{myverb}
Или же вы можете вычислить значение и сохранить его в переменной вне вызова \lstinline{html}
следующим образом:
\begin{myverb}
(let ((x (+ 1 2))) (html (:p x)))
\end{myverb}
Таким образом вы можете использовать макрос \lstinline{html} для генерации списка случайных
чисел следующим образом:
\begin{myverb}
(html
(:html
(:head
(:title "Random numbers"))
(:body
(:h1 "Random numbers")
(:p (loop repeat 10 do (html (:print (random 1000)) " "))))))
\end{myverb}
Версия, использующая макрос \lstinline{html}, будет эффективнее, чем использующая
\lstinline{emit-html}. И не только из-за того, что теперь не нужно генерировать s-выражение,
представляющее страницу целиком, но и из-за того, что большая часть работы по
интерпретации s-выражения, которую \lstinline{emit-html} осуществляет во время выполнения,
будет сделана однократно, во время раскрытия макроса, а не каждый раз при выполнении кода.
Вы можете управлять тем, куда будет отправлен вывод \lstinline{html} и \lstinline{emit-html}, с
помощью макроса \lstinline{with-html-output}, который также является частью библиотеки
FOO. Вы можете использовать макросы \lstinline{with-html-output} и \lstinline{html} для
переписывания \lstinline{random-number} следущим образом:
\begin{myverb}
(defun random-number (request entity)
(with-http-response (request entity :content-type "text/html")
(with-http-body (request entity)
(with-html-output ((request-reply-stream request))
(html
(:html
(:head (:title "Random"))
(:body
(:p "Random number: " (:print (random 1000))))))))))
\end{myverb}
\section{Макросы HTML}
Другой возможностью FOO является то, что он позволяет вам определять <<макросы>>
HTML, которые могут преобразовывать произвольные формы в представляющие HTML s-выражения,
которые понимает макрос \lstinline{html}. Например, предположим, что вы заметили, что часто
создаёте страницы следующего вида:
\begin{myverb}
(:html
(:head (:title "Some title"))
(:body
(:h1 "Some title")
... stuff ...))
\end{myverb}
Вы можете определить макрос HTML, представляющий этот образец, следующим образом:
\begin{myverb}
(define-html-macro :standard-page ((&key title) &body body)
`(:html
(:head (:title ,title))
(:body
(:h1 ,title)
,@body)))
\end{myverb}
Теперь вы можете использовать <<тег>> \lstinline{:standard-page} в ваших представляющих HTML
s-выражениях, и он будет раскрыт перед интерпретацией или компиляцией этих выражений.
Например, следующий код:
\begin{myverb}
(html (:standard-page (:title "Hello") (:p "Hello, world.")))
\end{myverb}
\noindent{}сгенерирует такой HTML:
\begin{lstlisting}[language=HTML]
<html>
<head>
<title>Hello</title>
</head>
<body>
<h1>Hello</h1>
<p>Hello, world.</p>
</body>
</html>
\end{lstlisting}
\section{Параметры запроса}
Конечно, генерация HTML является только половиной темы web-программирования. Ещё одной
вещью, которую вы должны уметь делать, является получение ввода от пользователя. Как я
рассказывал в разделе <<30-секундное введение в web-программирование на стороне сервера>>,
когда браузер запрашивает страницу у web-сервера, он может послать параметры запроса в
адресе URL или POST-данные, и оба этих источника рассматриваются как ввод для кода на
стороне сервера.
AllegroServe, как и большинство других каркасов web-программирования, берёт на себя заботу
о разборе обоих этих источников ввода для вас. В~то время когда ваша опубликованная
функция вызывается, все пары ключ/значение из строки запроса и/или POST-данных уже
декодированы и помещены в ассоциативный список (alist), который вы можете получить из
объекта запроса с помощью функции \lstinline{request-query}. Следующая функция возвращает
страницу, отображающую все полученные ею параметры запроса:
\begin{myverb}
(defun show-query-params (request entity)
(with-http-response (request entity :content-type "text/html")
(with-http-body (request entity)
(with-html-output ((request-reply-stream request))
(html
(:standard-page
(:title "Query Parameters")
(if (request-query request)
(html
(:table :border 1
(loop for (k . v) in (request-query request)
do (html (:tr (:td k) (:td v))))))
(html (:p "No query parameters.")))))))))
(publish :path "/show-query-params" :function 'show-query-params)
\end{myverb}
Если вы укажете своему браузеру URL со строкой запроса, подобной такой:
\begin{myverb}
http://localhost:2001/show-query-params?foo=bar&baz=10
\end{myverb}
\noindent{}вы получите страницу, подобную показанной на рис.~\ref{fig:26-4}.
\begin{figure}[htb]
\centering
\includegraphics[scale=0.7]{images/show-query-params-1a.jpg}
\caption{\url{http://localhost:2001/show-query-params?foo=bar&baz=10}}
\label{fig:26-4}
\end{figure}
Для генерации POST-данных нам нужна форма HTML. Следующая функция генерирует простую
форму, которая посылает свои данные show-query-params:
\begin{myverb}
(defun simple-form (request entity)
(with-http-response (request entity :content-type "text/html")
(with-http-body (request entity)
(let ((*html-output* (request-reply-stream request)))
(html
(:html
(:head (:title "Simple Form"))
(:body
(:form :method "POST" :action "/show-query-params"
(:table
(:tr (:td "Foo")
(:td (:input :name "foo" :size 20)))
(:tr (:td "Password")
(:td (:input :name "password" :type "password" :size 20))))
(:p (:input :name "submit" :type "submit" :value "Okay")
(:input ::type "reset" :value "Reset"))))))))))
(publish :path "/simple-form" :function 'simple-form)
\end{myverb}
Перейдите в вашем браузере по адресу \url{http://localhost:2001/simple-form}~-- вы должны
увидеть страницу, подобную изображённой на рис.~\ref{fig:26-5}.
\begin{figure}[htb]
\centering
\includegraphics[scale=0.7]{images/simple-form.jpg}
\caption{\url{http://localhost:2001/simple-form}}
\label{fig:26-5}
\end{figure}
Если вы заполните форму значениями <<abc>> и <<def>>, щёлкните на кнопку \texttt{Okay}, то
получите страницу, сходную с изображённой на рис.~\ref{fig:26-6}.
\begin{figure}[htb]
\centering
\includegraphics[scale=0.7]{images/show-query-params-2.jpg}
\caption{Результат посылки простой формы}
\label{fig:26-6}
\end{figure}
Однако чаще всего вам не нужно проходить по всем параметрам запроса: обычно вам нужно
просто получить определённый параметр. Например, вы можете захотеть модифицировать
\lstinline{random-number} так, чтобы предельное значение, передаваемое в функцию
\lstinline{RANDOM}, предоставлялось как параметр запроса. В~таком случае используется функция
\lstinline{request-query-value}, получающая объект запроса и имя параметра, значение которого
вы хотите получить. Она возвращает либо значение параметра в виде строки, либо
\lstinline{NIL}, если такой параметр не предоставлен. <<Параметризованная>> версия
\lstinline{random-number} может выглядеть следующим образом:
\begin{myverb}
(defun random-number (request entity)
(with-http-response (request entity :content-type "text/html")
(with-http-body (request entity)
(let* ((*html-output* (request-reply-stream request))
(limit-string (or (request-query-value "limit" request) ""))
(limit (or (parse-integer limit-string :junk-allowed t) 1000)))
(html
(:html
(:head (:title "Random"))
(:body
(:p "Random number: " (:print (random limit))))))))))
\end{myverb}
Так как \lstinline{request-query-value} может возвращать как \lstinline{NIL}, так и пустую
строку, мы должны обрабатывать оба этих случая при разборе параметра и его преобразования
в число, которое будет передано \lstinline{RANDOM}. Мы можем обработать значение
\lstinline{NIL} при связывании переменной \lstinline{limit-string}, связывая её с
пустой строкой (\lstinline{""}), если параметра \lstinline{"limit"} нет. Затем мы можем использовать
аргумент функции \lstinline{PARSE-INTEGER} \lstinline{:junk-allowed} для гарантии того,
что она вернёт либо \lstinline{NIL} (если не сможет разобрать целое число из переданной
строки), либо целое число. В~разделе <<Небольшой каркас приложений>> мы разработаем
несколько макросов для облегчения работы по получению параметров запроса и преобразованию
их в различные типы.
\section{Cookies}
В~AllegroServe вы можете послать заголовок \lstinline{Set-Cookie}, который укажет браузеру
сохранить cookie и посылать его со всеми последующими запросами, путём вызова функции
\lstinline{set-cookie-header} внутри тела \lstinline{with-http-response}, но перед вызовом
\lstinline{with-http-body}. Первый аргумент этой функции должен быть объектом запроса, а
остальные аргументы~-- ключевые аргументы, используемые для установки различных свойств
cookie. Обязательными являются лишь два аргумента: \lstinline{:name} и \lstinline{:value}, оба из
которых являются строками. Остальные возможные аргументы, влияющие на посылаемый браузеру
cookie: \lstinline{:expires}, \lstinline{:path}, \lstinline{:domain} и \lstinline{:secure}.
Из этих аргументов нам следует обратить внимание лишь на \lstinline{:expires}. Он управляет
тем, как долго браузер должен сохранять cookie. Если \lstinline{:expires} равен \lstinline{NIL}
(по умолчанию), браузер сохранит cookie только до завершения своей работы. Другие
возможные значения: \lstinline{:never}, что означает, что cookie должен сохраняться навсегда,
или всемирное (universal) время, как возвращается \lstinline{GET-UNIVERSAL-TIME} или
\lstinline{ENCODE-UNIVERSAL-TIME}. Значение \lstinline{:expires}, равное нулю, указывает клиенту
немедленно удалить существующие cookie\pclfootnote{За информацией о смысле остальных
параметров обращайтесь к документации AllegroServe и RFC 2109, разъясняющей механизм
cookie.}.
После того как вы установили cookie, вы можете использовать функцию
\lstinline{get-cookie-values} для получения ассоциативного списка (alist), содержащего по паре
имя/значение на каждый cookie, посланный браузером. Из этого списка вы можете получить
значения отдельных cookie с помощью \lstinline{ASSOC} и \lstinline{CDR}.
Следующая функция отображает имена и значения всех cookie, посланных браузером:
\begin{myverb}
(defun show-cookies (request entity)
(with-http-response (request entity :content-type "text/html")
(with-http-body (request entity)
(with-html-output ((request-reply-stream request))
(html
(:standard-page
(:title "Cookies")
(if (null (get-cookie-values request))
(html (:p "No cookies."))
(html
(:table
(loop for (key . value) in (get-cookie-values request)
do (html (:tr (:td key) (:td value)))))))))))))
(publish :path "/show-cookies" :function 'show-cookies)
\end{myverb}
При первой загрузке страницы \lstinline{http://localhost:2001/show-cookies} она должна
отобразить сообщение \lstinline{"No cookies"}, как показано на рис.~\ref{fig:26-7}, так как
вы ещё ничего не установили.
\begin{figure}[htb]
\centering
\includegraphics[scale=0.7]{images/show-cookies-no-cookies.jpg}
\caption{\url{http://localhost:2001/show-cookies} без установленных cookie}
\label{fig:26-7}
\end{figure}
Для установки cookie нам понадобится другая функция, такая как эта:
\begin{myverb}
(defun set-cookie (request entity)
(with-http-response (request entity :content-type "text/html")
(set-cookie-header request :name "MyCookie" :value "A cookie value")
(with-http-body (request entity)
(with-html-output ((request-reply-stream request))
(html
(:standard-page
(:title "Set Cookie")
(:p "Cookie set.")
(:p (:a :href "/show-cookies" "Look at cookie jar."))))))))
(publish :path "/set-cookie" :function 'set-cookie)
\end{myverb}
Если вы откроете в браузере \url{http://localhost:2001/set-cookie}, он должен отобразить
страницу, показанную на рис.~\ref{fig:26-8}. Сервер также пошлёт заголовок
\lstinline{Set-Cookie} с cookie по имени <<MyCookie>> со значением <<A cookie value>>. Если вы
нажмёте на ссылку //Look at cookie jar//, то попадёте на страницу \lstinline{/show-cookies},
где увидите новый cookie, как показано на рис.~\ref{fig:26-9}. Так как вы не задали
аргумент \lstinline{:expires}, браузер продолжит посылать cookie при каждом запросе, пока вы не
выйдете из него.
\begin{figure}[htb]
\centering
\includegraphics[scale=0.7]{images/set-cookie.jpg}
\caption{\url{http://localhost:2001/set-cookie}}
\label{fig:26-8}
\end{figure}
\begin{figure}[htb]
\centering
\includegraphics[scale=0.7]{images/show-cookies-one-cookie.jpg}
\caption{\url{http://localhost:2001/show-cookies} после установки cookie}
\label{fig:26-9}
\end{figure}
\section{Небольшой каркас приложений}
Хотя AllegroServe предоставляет достаточно прямой доступ ко всем базовым возможностям,
необходимым для написания кода, выполняющегося на стороне сервера (доступ к параметрам
запроса как из строки запроса, так и из POST-данных; возможность установки cookie и
получения их значений; и, конечно же, возможность генерации ответа для посылки
браузеру), приходится писать некоторое количество раздражающе повторяющегося кода.
Например, каждая генерирующая HTML-функция, которую вы будете писать, будет получать в
качестве аргументов запрос и сущность, а затем будет содержать вызовы
\lstinline{with-http-response}, \lstinline{with-http-body} и, если вы будете использовать FOO
для генерирации HTML, \lstinline{with-html-output}. Далее функции, которым нужно получать
параметры запроса, будут содержать множество вызовов \lstinline{request-query-value} и ещё
больше кода для преобразования получаемых строк к нужным типам. И наконец, вам нужно не
забыть опубликовать функции.
Для уменьшения повторяющегося кода, который вам нужно писать, мы можем реализовать
небольшой каркас поверх AllegroServe в целях облегчения определения функций,
обрабатывающих запросы по определённым URL.
Базовым приближением будет определение макроса \lstinline{define-url-function}, который мы будем
использовать для определения функций, которые будут автоматически публиковаться с помощью
\lstinline{publish}. Этот макрос будет раскрываться в \lstinline{DEFUN}, содержащий
соответствующий шаблонный код, а также в код публикации функции под URL с таким же, как у
функции, именем. Он также возьмёт на себя заботу по генерации кода извлечения значений
параметров запроса и cookies и связывания их с переменными, объявленными в списке
параметров функции. Таким образом, базовой формой определения \lstinline{define-url-function}
будет такая:
\begin{myverb}
(define-url-function name (request query-parameter*)
body)
\end{myverb}
\noindent{}где \lstinline{body} будет кодом, выдающим код HTML-страницы. Он будет обернут в вызов макроса
\lstinline{html} из FOO и поэтому для простых страниц может не содержать ничего, кроме
представляющего HTML s-выражения.
Внутри тела переменные параметров запроса будут связаны со значениями параметров запроса с
такими же именами или значениями cookie. В~простейшем случае значением параметра запроса
будет строка, полученная из параметра запроса или поля POST-данных с таким же именем. Если
же параметр запроса задаётся списком, вы также можете задать автоматическое преобразование
типа, значение по умолчанию и то, нужно ли получать значение параметра из cookie и сохранять его
в cookie. Полный синтаксис для параметра запроса выглядит так:
\begin{myverb}
name | (name type [default-value] [stickiness])
\end{myverb}
\lstinline{type} должен быть именем, распознаваемым \lstinline{define-url-function}. Мы скоро
обсудим, как определять новые типы. \lstinline{default-value} должно быть значением данного
типа. И наконец, \lstinline{stickness}, если предоставлен, указывает, что значение параметра
должно быть взято из cookie с соответствующим именем в случае, если параметр запроса не
предоставлен, а также что в ответе должен быть послан заголовок \lstinline{Set-Cookie}, который
сохранит значение cookie с этим именем. Таким образом, сохраняемый параметр (sticky
parameter), после явного предоставления значения в параметре запроса, сохранит это
значение при последующих запросах страницы, даже если параметр запроса не предоставляется.
Имя используемого cookie зависит от значения \lstinline{stickness}: при значении \lstinline{:global}
cookie будет иметь то же имя, что и параметр. Таким образом, различные функции,
использующие глобальные сохраняемые параметры с одинаковым именем, будут разделять
значение. Если \lstinline{stickness} равно \lstinline{:package}, то имя cookie будет сконструировано
из имён параметра и пакета, в котором находится имя функции; это позволит функциям одного
пакета разделять значения, не заботясь о возможных конфликтах с параметрами функций других
пакетов. И наконец, параметр со значением \lstinline{stickness}, равным \lstinline{:local}, будет
использовать имя cookie, составленное из имён параметра, пакета, в котором находится имя
функции, и самого имени функции, что делает его уникальным для этой функции.
Например, вы можете использовать \lstinline{define-url-function} для замены предыдущего
11-строчного определения \lstinline{random-page} 5-строчной версией:
\begin{myverb}
(define-url-function random-number (request (limit integer 1000))
(:html
(:head (:title "Random"))
(:body
(:p "Random number: " (:print (random limit))))))
\end{myverb}
Если вы хотите, чтобы аргумент \lstinline{limit} сохранялся, то должны изменить объявление
\lstinline{limit} следующим образом: \lstinline{(limit integer 1000 :local)}.
\section{Реализация}
Я разъясню реализацию \lstinline{define-url-function} <<сверху вниз>>. Сам макрос выглядит
следующим образом:
\begin{myverb}
(defmacro define-url-function (name (request &rest params) &body body)
(with-gensyms (entity)
(let ((params (mapcar #'normalize-param params)))
`(progn
(defun ,name (,request ,entity)
(with-http-response (,request ,entity :content-type "text/html")
(let* (,@(param-bindings name request params))
,@(set-cookies-code name request params)
(with-http-body (,request ,entity)
(with-html-output ((request-reply-stream ,request))
(html ,@body))))))
(publish :path ,(format nil "/~(~a~)" name) :function ',name)))))
\end{myverb}
Давайте рассмотрим её по шагам, начиная с первых строк.
\begin{myverb}
(defmacro define-url-function (name (request &rest params) &body body)
(with-gensyms (entity)
(let ((params (mapcar #'normalize-param params)))
\end{myverb}
До настоящего момента мы только готовились к генерации кода. Мы генерируем с помощью
\lstinline{GENSYM} символ для дальнейшего использования в качестве имени параметра сущности в
\lstinline{DEFUN}. Затем мы нормализуем параметры, преобразуя обычные символы в списочную
форму с помощью следующей функции:
\begin{myverb}
(defun normalize-param (param)
(etypecase param
(list param)
(symbol `(,param string nil nil))))
\end{myverb}
Другими словами, объявления параметра как просто символа~-- это то же самое, что и
объявление несохраняемого строкового параметра без значения по умолчанию.
Затем идёт \lstinline{PROGN}. Мы должны раскрывать макрос в \lstinline{PROGN}, так как нам нужно
сгенерировать код, осуществляющий две вещи: определение функции с помощью \lstinline{DEFUN} и
вызов \lstinline{publish}. Определение функции должно идти первым: таким образом, если в её
определении будет ошибка, то функция не будет опубликована. Первые две строки
\lstinline{DEFUN} являются уже привычным нам шаблонным кодом:
\begin{myverb}
(defun ,name (,request ,entity)
(with-http-response (,request ,entity :content-type "text/html")))
\end{myverb}
Теперь мы можем приступить к настоящей работе. Следующие две строки генерируют привязки
параметров, заданных в \lstinline{define-url-function} (кроме \lstinline{request}), а также код,
вызывающий \lstinline{set-cookie-header} для сохраняемых параметров. Конечно же реальная работа
осуществляется во вспомогательных функциях, которые мы вскоре увидим\footnote{Нам нужно
использовать \lstinline{LET* } вместо \lstinline{LET}, чтобы позволить формам значений
параметров по умолчанию ссылаться на параметры, идущие в списке ранее. Например, вы
можете написать такое:
\begin{myverb}
(define-url-function (request (x integer 10) (y integer (* 2 x))) ...)
\end{myverb}
\noindent{}и значение \lstinline{y}, не будучи предоставлено, будет удвоенным значением \lstinline{x}.}\hspace{\footnotenegspace}.
\begin{myverb}
(let* (,@(param-bindings name request params))
,@(set-cookies-code name request params)
\end{myverb}
Оставшаяся часть кода более шаблонна: мы помещаем тело из определения
\lstinline{define-url-function} в соответствующий контекст \lstinline{with-http-body},
\lstinline{with-html-output} и макроса \lstinline{html}. Затем идёт вызов \lstinline{publish}.
\begin{myverb}
(publish :path ,(format nil "/~(~a~)" name) :function ',name)
\end{myverb}