factor/contrib/cont-responder/tutorial.txt

416 lines
14 KiB
Plaintext
Raw Normal View History

Overview
========
The 'cont-responder' library is a continuation based web server
for writing web applications in Factor. Each 'web application' is a
standard Factor httpd responder.
This document outlines how to write simple web applications using
'cont-responder' by showing examples. It does not attempt to go into
the technical details of continuation based web applications or how it
is implemented in Factor. Instead it uses a series of examples that
can be immediately tried at the Factor prompt to get a feel for how
things work.
Getting Started
===============
To get started you will first need to load the 'cont-responder'
code. You will need the following as a minimum:
"cont-responder.factor" run-file
"cont-utils.factor" run-file
USE: cont-responder
USE: cont-utils
The responders that you will be writing will require an instance of
the httpd server to be running. It will be run in a background thread
to enable the interactive development of the applications. The
following is a simple function to start the server on port 8888 and
restart it if an error occurs:
USE: httpd
USE: threads
: start-httpd [ 8888 httpd ] [ dup . flush [ start-httpd ] when* ] catch ;
[ start-httpd ] in-thread
Responders
==========
A 'responder' is a word that is registered with the httpd server that
gets run when the client accesses a particular URL. When run that word
has 'standard output' bound in such a way that all output goes to the
clients web browser.
In the 'cont-responder' system the word used to set output to go to the web
browser and display a page is 'show'. Think of it as 'show a page to
the client'. 'show' takes a single item on the stack and that is a
'page generation' quotation.
A 'page generation' quotation is a quotation with stack effect (
string -- ). For now we'll ignore the string it receives on the
stack. Its purpose will be explained later.
Hello World 1
=============
A simple 'hello world' responder would be:
: hello-world1 ( -- )
[
drop
"<html><head><title>Hello World</title></head>" write
"<body>Hello World!</body></html>" write
] show drop ;
When installed this will show a single page which is simple HTML to
display 'Hello World!'. The 'show' word returns a namespace, the
purpose of which will also be explained later. For now we ignore it
and drop it. Notice we also drop the 'URL' that the quotation passed
to 'show' receives on the stack.
The responder is installed using:
"helloworld1" [ hello-world1 ] install-cont-responder
The 'install-cont-responder' word has stack effect ( name quot --
). It installs a responder with the given name. When the URL for that
responder is accessed the 'quot' quotation is run. In this case it is
'hello-world1' which displays the single HTML page described
previously.
Accessing the above responder from a web browser is via an URL like:
http://localhost:8888/responder/helloworld1
This should display an HTML page showing 'Hello World!".
HTML Generation
===============
Generating HTML by writing strings containing HTML can be a bit of a
chore. Especially when the content is dynamic requiring concatenation
of many pieces of data.
The 'cont-responder' system uses 'html', a library that allows writing
HTML looking output directly in factor. This system developed for
'cont-responder' has recently been made part of the standard 'html'
library of Factor.
'html' basically allows you to write HTML-like output in a factor word
and it will be automatically output. It can be tested at the console
very easily:
USE: html
<p> "This is a paragraph" write </p>
=> <p>This is a paragraph</p>
You can write open and close tags like orginary HTML and anything sent
to standard output in between the tags will be enclosed in the
specified tags. Attributes can also be used:
<p style= "text-align: center" p> "More text" write </p>
=> <p style='text-align: center'>More text</p>
The attribute must be seperated from the value of that attribute via
whitespace. If you are using attributes the tag must be closed with a
'[tagname]>' where the [tagname] is the name of the tag used. See the
'<p p>' example above.
You can use any factor code at any point:
"text-align: " "red"
<p style= 2dup cat2 p>
"Using style " write swap write write
</p>
=> <p style='text-align: red'>Using style text-align: red</p>
Tags that are not normally closed are written using an XML style
(ie. with a trailing slash):
"One" write <br/> "Two" write <br/> <input type= "text" input/>
=> One<br>Two<br><input type='text'>
Hello World 2
=============
Using the HTML generation library makes writing responders more
readable. Here is the hello world example perviously using this
system:
: hello-world2 ( -- )
[
drop
<html>
<head> <title> "Hello World" write </title> </head>
<body> "Hello World!" write </body>
</html>
] show drop ;
Install it using:
"helloworld2" [ hello-world2 ] install-cont-responder
Dynamic Data
============
Adding dynamic data to the page is relatively easy. This example pulls
a value from the 'room' word which displays the amount of available
and free memory in the system.
: memory-stats1 ( -- )
[
drop
<html>
<head> <title> "Memory Statistics" write </title> </head>
<body>
<table border= "1" table>
<tr>
<td> "Total Data Memory" write </td>
<td> room unparse write </td>
</tr>
<tr>
<td> "Free Data Memory" write </td>
<td> unparse write </td>
</tr>
<tr>
<td> "Total Code Memory" write </td>
<td> unparse write </td>
</tr>
<tr>
<td> "Free Code Memory" write </td>
<td> unparse write </td>
</tr>
</table>
</body>
<pre> room. </pre>
</html>
] show drop ;
"memorystats1" [ memory-stats1 ] install-cont-responder
Accessing this page will show a table with the current memory
statistics. Hitting refresh will update the page with the latest
information.
The HTML output can be refactored into different words. For example:
: memory-stats-table ( free total -- )
#! Output a table containing the given statistics.
<table border= "1" table>
<tr>
<td> "Total Data Memory" write </td>
<td> unparse write </td>
</tr>
<tr>
<td> "Free Data Memory" write </td>
<td> unparse write </td>
</tr>
</table> ;
: memory-stats2 ( -- )
[
drop
<html>
<head> <title> "Memory Statistics 2" write </title> </head>
<body> room memory-stats-table 2drop </body>
</html>
] show drop ;
"memorystats2" [ memory-stats2 ] install-cont-responder
Some simple flow
================
The big advantage with continuation based web servers is being able to
write a web application in a standard procedural flow and have it
correctly served up in the HTTP request/response model.
This example demonstates a flow of three pages. Clicking an URL on the
first page displays the second. Clicking an URL on the second displays
the third.
When a 'show' call is executed the page generated by the quotation is
sent to the client. The computation of the responder is then
'suspended'. When the client accesses a special URL computiation is
resumed at the point of the end of the 'show' call. In this way
procedural flow is maintained.
This brings us to the 'URL' stack item that is available to the 'page
generation' quotation passed to 'show'. This URL is a string that
contains a URL that can be embedded in the page. When the user access
that URL computation is resumed from the point of the end of the
'show' call as described above:
: flow-example1 ( -- )
[
<html>
<head> <title> "Flow Example 1" write </title> </head>
<body>
<p> "Page 1" write </p>
<p> <a href= a> "Press to continue" write </a> </p>
</body>
</html>
] show drop
[
<html>
<head> <title> "Flow Example 1" write </title> </head>
<body>
<p> "Page 2" write </p>
<p> <a href= a> "Press to continue" write </a> </p>
</body>
</html>
] show drop
[
drop
<html>
<head> <title> "Flow Example 1" write </title> </head>
<body>
<p> "Page 3" write </p>
</body>
</html>
] show drop ;
"flowexample1" [ flow-example1 ] install-cont-responder
The 'flow-example1' word contains three 'show' calls in a row. The
first two display simple pages with an anchor link to the URL received
on the stack. This URL when accessed resumes the computation. The
final page just drops the URL.
When you display this example in the browser you'll be able to click
the URL to navigate. You can use the back button to retry the URL's,
you can clone the browser window and navigate them independantly, etc.
The similarity of the functions above could so with some
refactoring. The pages are almost exactly the same so we seperate this
into a seperate word:
: show-flow-page ( n bool -- )
#! Show a page in the flow, using 'n' as the page number
#! to display. If 'bool' is true display a link to the
#! next page.
[ ( n bool url -- )
<html>
<head> <title> "Flow Example 1" write </title> </head>
<body>
<p> "Page " write rot unparse write </p>
swap [
<p> <a href= a> "Press to continue" write </a> </p>
] [
drop
] ifte
</body>
</html>
] show 3drop ;
: flow-example2 ( n -- )
#! Display the given number of pages in a row.
dup pred [ succ t show-flow-page ] times*
f show-flow-page ;
"flowexample2" [ 5 flow-example2 ] install-cont-responder
In this example the 'show-flow-age' pulls the page number off the
stack. It also gets whether or not to display the link to the next
page.
Notice that after the show that a '3drop' is done whereas
previously we've only done a single 'drop'. This is due to a side
effect of 'show' using continuations.
After the 'show' call returns there will be one item on the stack
(which we've been dropping and will explain later what it is). The
stack will also be set as it was before the show call. So in this case
the 'n' and 'bool' remain on the stack even though they were removed
during the page generation quotation. This is because we resumed the
continuation which, when captured, had those items on the stack. The
general rule of thumb is you will need to account for items on the
stack before the show call.
This example also demonstrates using the 'times*' combinator to
sequence the page shows. Any Factor code can be called and the
continuation based system will sequentially display each page. The
back button, browser window cloning, etc will all continue to work.
You'll notice the URL's in the browser have a number at the end of
them. This is the 'continuation identifier' which is like a session id
except that it identifies not just the data you have stored but your
location within the responder as well.
Forms and POST data
===================
The web pages we've generated so far don't accept input from the
user. I've mentioned previously that 'show' returns a value on the
stack and we've been dropping it in our examples.
The value returned is a namespace containing the field names and
values of any POST data in the request. If no POST data exists then it
is the boolean value 'f'.
To process input from the user just put a form in the HTML with a
method of 'POST' and an action set to the URL passed in to the page
generation quotation. The show call will then return a namespace
containing this data. Here is a simple example:
: accept-users-name ( -- name )
#! Display an HTML requesting the users name. Push
#! the name the user input on the stack..
[
<html>
<head> <title> "Please enter your name" write </title> </head>
<body>
<form action= method= "post" form>
<p>
"Please enter your name:" write
<input type= "text" size= "20" name= "username" input/>
<input type= "submit" value= "Ok" input/>
</p>
</form>
</body>
</html>
] show [
"username" get
] bind ;
: post-example1 ( -- )
[
drop
<html>
<head> <title> "Hello!" write </title> </head>
<body>
<p> accept-users-name write ", Good to see you!" write </p>
</body>
</html>
] show drop ;
"post-example1" [ post-example1 ] install-cont-responder
The 'accept-users-name' word displays an HTML form allowing input of
the name. When that form is submitted the namespace containing the
data is returned by 'show'. We bind to it and retrieve the 'username'
field. The name used here should be the same name used when creating
the field in the HTML.
'post-example1' then does something a bit tricky. Instead of first
calling 'accept-users-name' to push the name on the stack and then
displaying the resulting page we call 'accept-users-name' from within
the page itself when we actually need it. The magic of the
continuation system causes the 'accept-users-name' to be called when
needed displaying that page first. It is certainly possible to do it
the other way though:
: post-example2 ( -- )
accept-users-name
[ ( name url -- )
drop
<html>
<head> <title> "Hello!" write </title> </head>
<body>
<p> write ", Good to see you!" write </p>
</body>
</html>
] show 2drop ;
"post-example2" [ post-example2 ] install-cont-responder
Either way works. Notice that in the 'post-example2' we had to do a
'2drop' instead of a 'drop' at the end of the show to remove the
additional 'name' that is on the stack. This wasn't needed in
'post-example1' because the 'name' was not on the stack at the time of
the 'show' call.