当前位置: 首页 > 工具软件 > MochiWeb > 使用案例 >

A practical introduction to MochiWeb

慕容雅珺
2023-12-01

notice : origin article from : http://alexmarandon.com/articles/mochiweb_tutorial/#basic-request-handling


A practical introduction to MochiWeb

Published on 11 July 2011, updated on 11 July 2011, 34 Comments

Introduction

Bob Ippolito , creator or  MochiWeb , describes it as  “an Erlang library for building lightweight HTTP servers” . It’s not a framework: it doesn’t come with URL dispatch, templating or data persistence. Despite not having an official website or narrative documentation, MochiWeb is a popular choice to build web services in Erlang. The purpose of this article is to help you to get started by gradually building a microframework featuring templates and URL dispatch. Persistence will not be covered.

I assume you’re familiar with basic sequential Erlang. If it’s not the case, I’d suggest you study the first few chapters of this guide. No knowledge of concurrent and distributed Erlang is needed to follow this tutorial.

If you get stuck, you can get the code corresponding to this tutorial. Each commit corresponds to a section of the tutorial so you can easily see the code for a specific step by checking out the corresponding commit.

Getting started

Start by getting a copy of MochiWeb using Git:

$ git clone git://github.com/mochi/mochiweb.git

MochiWeb source tree contains a README file that tells you how to create a new project. Let’s create a project called greeting:

$ cd mochiweb
$ make app PROJECT=greeting

We can now compile the code for our new app and start it:

$ cd ../greeting/
$ make
$ ./start-dev.sh

You should see a bunch of PROGRESS REPORT messages. Among those message you should see something like {port,8080} which tells you that your app is running on port 8080. You can now point your browser at http://localhost:8080 and you should see a message telling you that it’s successfully running.

If you go back to the terminal and press return you’ll get an Erlang shell that you can use to interact directly with your app. It will come very handy for debugging or experimenting with libraries, similarly to the ./script/console of Ruby on Rails or the manage.py shell of Django.

Documentation

When I was first looking for MochiWeb documentation, most of the search results I got were messages from people looking for documentation. This is one of the reasons why I decided to write this tutorial.

Acutually, MochiWeb contains some API reference documentation. You can generate it with:

$ cd ../mochiweb
$ make edoc

You should now find doc/index.html in your mochiweb directory. Open it with your browser and you’ll be able to navigate the API documentation generated from source code. It’s useful to get an overview of modules and functions available and to find details about specific functions.

There’s also a good video tutorial by BeeBole which shows an interesting approach to building a fully AJAX app with MochiWeb.

In this tutorial I present a more traditional approach allowing you to map regular browser requests to Erlang functions.

Basic request handling

When we made our first request to our new app, the message we got came from a static index.html file located in greeting/priv/www/. This is where you can put static content such as CSS, images, etc. but for now it’s probably more interesting to start creating a request handler taking some user input.

Our starting point to plug our request handling code is going to be the file src/greeting_web.erl, which contains a function loop/2:

loop(Req, DocRoot) ->
    "/" ++ Path = Req:get(path),
    try
        case Req:get(method) of
            Method when Method =:= 'GET'; Method =:= 'HEAD' ->
                case Path of
                    _ ->
                        Req:serve_file(Path, DocRoot)
                end;
            'POST' ->
                case Path of
                    _ ->
                        Req:not_found()
                end;
            _ ->
                Req:respond({501, [], []})
        end
    catch
        %% ... exception handling code ...
    end.

If you can read Erlang, it shouldn’t be too hard to understand what this does. It extracts the path of the request, serves a corresponding static file if it’s a GET or HEAD request (implicitly mapping / to /index.html), returns a 404 if it’s a POST and an error if it’s another HTTP verb. Any exception is caught and displayed on the terminal. I don’t show exception handling code here for brevety, but it does not mean you should get rid of it!

What we’re going to do now is add some code to handle a request to /hello, get the user name from the query string and display a greeting to the user. In the conditional branch that handles GET requests, we’re going to add this clause:

"hello" ->
  QueryStringData = Req:parse_qs(),
  Username = proplists:get_value("username", QueryStringData, "Anonymous"),
  Req:respond({200, [{"Content-Type", "text/plain"}],
               "Hello " ++ Username ++ "!\n"});

First we use mochiweb_request:parse_qs/0 to get a proplist of query string parameters. We then useproplist:get_value/3 to get the username parameter, providing a default value if the parameter is missing. Finally we call mochiweb_request:respond/1, passing it a tuple containing the HTTP code, a proplist of headers and the body content. Here is what our new loop/2 function looks like:

loop(Req, DocRoot) ->
    "/" ++ Path = Req:get(path),
    try
        case Req:get(method) of
            Method when Method =:= 'GET'; Method =:= 'HEAD' ->
                case Path of
                    "hello" ->
                        QueryStringData = Req:parse_qs(),
                        Username = proplists:get_value("username", QueryStringData, "Anonymous"),
                        Req:respond({200, [{"Content-Type", "text/plain"}],
                                     "Hello " ++ Username ++ "!\n"});
                    _ ->
                        Req:serve_file(Path, DocRoot)
                end;
            'POST' ->
                case Path of
                    _ ->
                        Req:not_found()
                end;
            _ ->
                Req:respond({501, [], []})
        end
    catch
        %% ... exception handling code ...
    end.

Compile your project with make and you should be able to go to http://localhost:8080/hello?username=Mike and see the famous quote from Erlang the movieHello Mike!

Rendering templates

Your new exciting greeting feature works, but its presentation might be a bit too simple. Let’s see if we can enhance the user experience using cutting edge HTML technology. We’re going to use ErlyDTL, an Erlang implementation of Django Template Language written by Evan Miller. If you’re not familiar with Django templates, you might want to take a look at its documentation but basically I can tell you variable substitution looks like {{my_variable}} and control is achieved using tags with a syntax such as {% tagname param %}here comes some content{% endtagname %}.

Installing ErlyDTL

First let’s add ErlyDTL to our application. Mochiweb uses Rebar, a build and packaging tool for Erlang applications, which we can use to add a dependency to our project. Open the configuration filerebar.config. You’ll see an entry pointing to MochiWeb git repository. Let’s add another entry for ErlyDTL so that our Rebar config file now looks like:

%% -*- erlang -*-
{erl_opts, [debug_info]}.
{deps, [
  {erlydtl, ".*",
   {git, "git://github.com/evanmiller/erlydtl.git", "master"}},
  {mochiweb, ".*",
   {git, "git://github.com/mochi/mochiweb.git", "master"}}]}.
{cover_enabled, true}.
{eunit_opts, [verbose, {report,{eunit_surefire,[{dir,"."}]}}]}.

You should now be able to fetch and compile ErlyDTL by just typing make:

$ make
==> mochiweb (get-deps)
==> greeting (get-deps)
Pulling erlydtl from {git,"git://github.com/evanmiller/erlydtl.git","master"}
Initialized empty Git repository in /home/al/dev/projects/greeting/deps/erlydtl/.git/
==> erlydtl (get-deps)
==> erlydtl (compile)
Compiled src/erlydtl_parser.yrl
[...]

In order for the new library to be taken into account, you will need to restart the application by typingq(). in the Erlang shell and execute ./start-dev.sh again.

Of course, this method is not limited to ErlyDTL. You can add other dependencies the same way using Rebar.

Template compilation

ErlyDTL compiles Django Template source code into Erlang bytecode. While it’s perfectly possible to manage ErlyDTL templates compilation in our code, Rebar happens to provide support for it, so let’s use it.

We’ll create a templates directory, where Rebar looks for templates by default:

$ mkdir templates

Now create a file templates/greeting.dtl with a content similar to:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>MochiWeb Tutorial</title>
    <link rel="stylesheet" type="text/css" href="/style.css" media="screen">
  </head>
  <body>
    <p>
        Hello {{ username }}!
    </p>
  </body>
</html>

If you make again you’ll see that Rebar (which is invoked by make) has created an Erlang moduleebin/greeting_dtl.beam. Note that Rebar provides options to customize location and file names of template source files and compiled template modules.

You can now use your new template in the request handler using this code:

QueryStringData = Req:parse_qs(),
Username = proplists:get_value("username", QueryStringData, "Anonymous"),
{ok, HTMLOutput} = greeting_dtl:render([{username, Username}]),
Req:respond({200, [{"Content-Type", "text/html"}],
        HTMLOutput});

Run make, refresh your browser and be in awe of the fine piece of web design you’ve created.

As you might have noticed by now, you need to run make when you change your code. You might want to configure a shortcut in your text editor to start make easily. From now on, I won’t tell you when you need to run make.

POST requests

Your app is already getting popular but some users are complaining that they can’t remember the query string syntax and would like to be able to enter their name using a form. You edit your template and add an HTML form with a text field for the user name:

<form method="POST">
    <p>Username: <input type="text" name="username"></p>
    <input type="submit">
</form>

You also change your request handler to support POST requests, so that it now looks like:

loop(Req, DocRoot) ->
    "/" ++ Path = Req:get(path),
    try
        case Req:get(method) of
            Method when Method =:= 'GET'; Method =:= 'HEAD' ->
                case Path of
                    "hello" ->
                        QueryStringData = Req:parse_qs(),
                        Username = proplists:get_value("username", QueryStringData, "Anonymous"),
                        {ok, HTMLOutput} = greeting_dtl:render([{username, Username}]),
                        Req:respond({200, [{"Content-Type", "text/html"}],
                                HTMLOutput});
                    _ ->
                        Req:serve_file(Path, DocRoot)
                end;
            'POST' ->
                case Path of
                    "hello" ->
                        PostData = Req:parse_post(),
                        Username = proplists:get_value("username", PostData, "Anonymous"),
                        {ok, HTMLOutput} = greeting_dtl:render([{username, Username}]),
                        Req:respond({200, [{"Content-Type", "text/html"}],
                                HTMLOutput});
                    _ ->
                        Req:not_found()
                end;
            _ ->
                Req:respond({501, [], []})
        end
    catch
        % ... exception handling code ...
    end.

It seems to work very well but something bothers you. Seeing "hello" in two places in not a good sign. Repetition of code that renders the template and returns a response isn’t great either. We also notice that if we visit /hello/ with a final slash, we get a page not found error. It’s time for some refactoring.

A simple URL dispatcher

We’re going to create a minimalistic URL dispatcher that will allow you to map regular expressions to Erlang functions. Here is what URL configuration will look like:

[
  {"^hello/?$", hello}
]

This says that any request to /hello or /hello/ should be routed to a function named hello.

Here is the code for the dispatcher:

% Iterate recursively on our list of {Regexp, Function} tuples
dispatch(_, []) -> none;
dispatch(Req, [{Regexp, Function}|T]) -> 
    "/" ++ Path = Req:get(path),
    Method = Req:get(method),
    Match = re:run(Path, Regexp, [global, {capture, all_but_first, list}]),
    case Match of
        {match,[MatchList]} -> 
            % We found a regexp that matches the current URL path
            case length(MatchList) of
                0 -> 
                    % We didn't capture any URL parameters
                    greeting_views:Function(Method, Req);
                Length when Length > 0 -> 
                    % We pass URL parameters we captured to the function
                    Args = lists:append([[Method, Req], MatchList]),
                    apply(greeting_views, Function, Args)
            end;
        _ -> 
            dispatch(Req, T)
    end.

Add the dispatcher code somewhere in greeting_web.erl and modify loop/2 to make use of it:

loop(Req, DocRoot) ->
    "/" ++ Path = Req:get(path),
    try
        case dispatch(Req, greeting_views:urls()) of
            none -> 
                % No request handler found
                case filelib:is_file(filename:join([DocRoot, Path])) of
                    true -> 
                        % If there's a static file, serve it
                        Req:serve_file(Path, DocRoot);
                    false ->
                        % Otherwise the page is not found
                        Req:not_found()
                end;
            Response -> 
                Response
        end
    catch
        % ... exception handling code ...
    end.

Now we’re going to create a module that contains URL configuration and request handlers. Create a filesrc/greeting_views.erl that contains this code:

-module(greeting_views).
-compile(export_all).
-import(greeting_shortcuts, [render_ok/3]).

urls() -> [
      {"^hello/?$", hello}
    ].

hello('GET', Req) ->
    QueryStringData = Req:parse_qs(),
    Username = proplists:get_value("username", QueryStringData, "Anonymous"),
    render_ok(Req, greeting_dtl, [{username, Username}]);
hello('POST', Req) ->
    PostData = Req:parse_post(),
    Username = proplists:get_value("username", PostData, "Anonymous"),
    render_ok(Req, greeting_dtl, [{username, Username}]).

We make use of a function render_ok/3 to avoid a bit of code duplication when returning responses. Let’s define this function in src/greeting_shortcuts.erl:

-module(greeting_shortcuts).
-compile(export_all).

render_ok(Req, TemplateModule, Params) ->
    {ok, Output} = TemplateModule:render(Params),
    % Here we use mochiweb_request:ok/1 to render a reponse
    Req:ok({"text/html", Output}).

Now you’ve got a more generic way to handle requests. We removed some duplication and our code is also more organized because we’ve now defined a place where to put our request handlers and a place where to put utility functions.

This is all good but one of your friend tells you that she likes being able to get a greeting with a GET request, but she finds using the query string rather ugly. Instead she says it would be great if she could get a greeting just by visiting /hello/Alice or /hello/Alice/. Luckily our URL dispatcher makes it easy to add this innovative functionality.

Add a second entry to your URL configuration so now it looks like:

urls() -> [
      {"^hello/?$", hello},
      {"^hello/(.+?)/?$", hello}
    ].

And create the request handler (in greeting_views.erl) that will accept a URL parameter:

hello('GET', Req, Username) ->
    render_ok(Req, greeting_dtl, [{username, Username}]);
hello('POST', Req, _) ->
  % Ignore URL parameter if it's a POST
  hello('POST', Req).

Voila, /hello/Alice or /hello/Alice/ should now work too.

Handling cookies

You’re getting more and more feedback about your application and some users tell you it would be great if next time they visit /hello/ it could remember their name. Let’s use a cookie for that. Editgreeting_shortcuts.erl and add a function that returns a cookie value or a default value if the cookie is not present. Also create a new function render_ok/4, similar to our existing render_ok/3 except that it takes an extra Headers parameter that we’ll use to send the cookie header. Modify render_ok/3 so that it now just calls render_ok/4 with an empty list of headers.

-module(greeting_shortcuts).
-compile(export_all).

render_ok(Req, TemplateModule, Params) ->
    render_ok(Req, [], TemplateModule, Params).

render_ok(Req, Headers, TemplateModule, Params) ->
    {ok, Output} = TemplateModule:render(Params),
    Req:ok({"text/html", Headers, Output}).

get_cookie_value(Req, Key, Default) ->
    case Req:get_cookie_value(Key) of
        undefined -> Default;
        Value -> Value
    end.

Now edit your view module to make use of these new functions. We’ll also remove some duplication while we’re at it.

-module(greeting_views).
-compile(export_all).
-import(greeting_shortcuts, [render_ok/3, render_ok/4, get_cookie_value/3]).

urls() -> [
      {"^hello/?$", hello},
      {"^hello/(.+?)/?$", hello}
    ].

% Return username input if present, otherwise return username cookie if
% present, otherwise return "Anonymous"
get_username(Req, InputData) ->
    proplists:get_value("username", InputData,
        get_cookie_value(Req, "username", "Anonymous")).

make_cookie(Username) ->
    mochiweb_cookies:cookie("username", Username, [{path, "/"}]).

handle_hello(Req, InputData) ->
    Username = get_username(Req, InputData),
    Cookie = make_cookie(Username),
    render_ok(Req, [Cookie], greeting_dtl, [{username, Username}]).

hello('GET', Req) ->
    handle_hello(Req, Req:parse_qs());
hello('POST', Req) ->
    handle_hello(Req, Req:parse_post()).

hello('GET', Req, Username) ->
    Cookie = make_cookie(Username),
    render_ok(Req, [Cookie], greeting_dtl, [{username, Username}]);
hello('POST', Req, _) ->
  hello('POST', Req).

When users set their username, it should now be stored in a cookie and displayed when they visit/hello/.

Conclusion

That’s it for this tutorial. Now that you know how to add a library to your project, get user input, render templates and set cookies, you should have the building blocks to add further functionality such as authentication/authorization, global template context, persistence, etc. You could also modify the URL dispatcher to allow specifying a module as well as a function, or take a different approach, maybe based on conventions rather than configuration.

I hope you’re now slightly more familiar with MochiWeb so that you can come up with the way that best suits your needs. Browse the API documentation to find out more about what MochiWeb has to offer and by all means don’t hesitate to look at the source code. This is something I found to be generally true when working with Erlang libraries: the source code often speaks more that the documentation. Fortunately, if you’re like me, you might find that Erlang code is often easier to understand than other languages, probably because of its functional nature and its simplicity, so use the source Luke!


 类似资料:

相关阅读

相关文章

相关问答