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

MochiWeb入门

方长卿
2023-12-01

mochiweb入门

转载译文:https://www.cnblogs.com/ken-zhang/archive/2011/10/24/2222202.html
原文出处:http://alexmarandon.com/articles/mochiweb_tutorial/

简介

  MochiWeb由Bob Ippolito创建,其描述为:“一个创建轻量级http服务器的Erlang库”。它不是框架:不附带URL调度、模版引擎、数据持久等。尽管没有官方网站和文档,但仍然是Erlang构建web服务的热门选择。这篇随笔将带您逐步入门并构建一个支持URL调度和模版引擎的迷你型框架。(不包含数据持久)

  我假设您已经具备一些Erlang语言的基础,否则,建议您先学习这本指南的前面部分章节,本教程不需要具备对并发和分布式Erlang的知识。

入门

首先使用Gitgithub获取MochiWeb源代码:

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

接下来,我们创建一个项目,叫做 greeting

$ cd mochiweb
$ make app PROJECT=greeting

很简单,现在我们可以编译和运行我们的app了:

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

打开浏览器,访问http://localhost:8080,你会看到一行信息:“greeting running.” 标题栏显示“It Worked”,这说明运行成功了

回到终端,按回车后将出现一个Erlang Shell>,你可以使它来跟你的app交互,这对app的调试非常有用.

文档

MochiWeb本身包含一些参考文档,你可以这样生成它:

$ cd ../mochiweb
$ make edoc

之后,可以在mochiweb所在目录的找到doc/index.html,用浏览器打开即可。它有助于了解模块概述、可用函数和函数的具体说明。

这里有一个很棒的视频教程展示了一种有趣的方法搭建完全基于MochiWebAJAX应用程序。

本篇教程中,使用了一种更传统的方法来实现按规则将请求映射到相应的Erlang函数。(类似于Django中的url pattern 映射相应的views方法)

基本的请求处理

当我们首次请求app时,页面上显示的信息来自于greeting/priv/www/下的index.html文件,此目录将供我们放置一些静态文件,如css、图片等。现在,可能更有趣的事应该是开始创建一个请求处理程序,得到一些用户输入。

我们将在src/greeting_web.erl中插入一些代码来处理请求,该模块中包含一个函数 loop/2:

loop(Req, DocRoot) -> 
    "/" ++ Path = mochiweb_request:get(path, Req), 
    try 
        case mochiweb_request:get(method, Req) of 
            Method when Method =:= 'GET' ; Method =:= 'HEAD' -> 
                case Path of 
                    "hello_world" -> 
                        mochiweb_request:respond({200, [{"Content-Type", "text/plain"}], "Hello world1!\n"}, Req); 
                    _ -> 
                        mochiweb_request:serve_file(Path, DocRoot, Req) 
                end; 
            'POST' ->
                     _ -> 
                        mochiweb_request:not_found(Req) 
                end; 
            _ -> 
                mochiweb_request:respond({501, [ ], [ ]}, Req) 
        end 
    catch 
        ?CAPTURE_EXC_PRE(Type, What, Trace) -> 
            Report = ["web request failed", {path, Path}, {type, Type}, {what, What}, {trace, ?CAPTURE_EXC_GET(Trace)}], 
            error_logger:error_report(Report), 
            mochiweb_request:respond({500, [{"Content-Type", "text/plain"}], "request failed, sorry\n"}, Req) 
    end.

如果你能阅读Erlang,应该不难理解这段代码的作用。它从请求中提取路径,如果该请求的MethodGETHEAD(默认将"/"映射至/index.html),如果是POST或者其他HTTP动作,将返回404和错误提示。任何异常将被捕获并显示在终端。

我们现在要做的是添加一些代码来处理访问路径为/hello 的请求,从QueryString中获得用户名,并显示欢迎词,在上面代码中对GET请求处理的条件分支处添加下面的字句:

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

首先我们使用 mochiweb_request:parse_qs/1 方法得到包含query string参数的proplist ,然后用proplist:get_value/3 方法得到username的参数值,如果不存在则默认为"Anonymous"。

最后我们调用mochiweb_request:respond/2方法,它需要传递一个元组参数,其中包含:HTTP状态码、头信息proplist、主体信息. 下面是我们的新loop/2函数:

loop(Req, DocRoot) -> 
    "/" ++ Path = mochiweb_request:get(path, Req), 
    try 
        case mochiweb_request:get(method, Req) of 
            Method when Method =:= 'GET' ; Method =:= 'HEAD' -> 
                case Path of 
                    "hello_world" -> 
                        mochiweb_request:respond({200, [{"Content-Type", "text/plain"}], "Hello world1!\n"}, Req);
                        "hello" ->
						    QueryStringData = mochiweb_request:parse_qs(Req),
						    Username = proplists:get_value("username", QueryStringData, "Anonymous"),
						    mochiweb_request:respond({200, [{"Content-Type", "text/plain"}],
						               "Hello " ++ Username ++ "!\n"}, Req);
                    _ -> 
                        mochiweb_request:serve_file(Path, DocRoot, Req) 
                end; 
            'POST' ->
                     _ -> 
                        mochiweb_request:not_found(Req) 
                end; 
            _ -> 
                mochiweb_request:respond({501, [ ], [ ]}, Req) 
        end 
    catch 
        ?CAPTURE_EXC_PRE(Type, What, Trace) -> 
            Report = ["web request failed", {path, Path}, {type, Type}, {what, What}, {trace, ?CAPTURE_EXC_GET(Trace)}], 
            error_logger:error_report(Report), 
            mochiweb_request:respond({500, [{"Content-Type", "text/plain"}], "request failed, sorry\n"}, Req) 
    end.

使用make 编译你的项目,然后你可以访问http://localhost:8080/hello?username=Mike 将会看到《Erlang: The Movie》中的名言:Hello Mike!

渲染模版

接下来让我们使用先进的HTML来提升下用户体验,我们使用ErlyDTL ,由Evan Miller 编写的Django模版语法的Erlang版实现。如果你还不了解Django的模版引擎语法,可以看其文档 ,不过我可以告诉你一些基本的,变量看起来像这样{{my_variable}},控制语句是用这样的标签语法来实现{% tagname param %}这里是一些内容{% endtagname %}

安装 ErlyDTL

首先添加ErlyDTL到我们的项目,MochiWeb使用rabar ,一个用于Erlang应用程序构建和打包的工具。我们可以用它来为项目添加依赖。打开rebar.config文件,你会看到一个条目指向MochiWebgit仓库,让我们添加另外一个指向ErlyDTL的条目,此时配置文件应该是这样:

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

你仅仅只需要输入make,即可获取和编译ErlyDTL

由于使用了新的库,要使其生效,需重启应用程序。终端输入q(). ,然后再执行./start-dev.sh

当然,这个方法不仅仅用于ErlyDTL,你也可以用rabar,同样的方法添加其他的依赖。

模版编译

ErlyDTL会把Django模版编译为Erlang字节码,rabar恰恰完美支持在我们的代码中管理编译ErlyDTL模版,所以我们使用它。

我们将创建一个 templates 目录,它是rabar编译时默认的模版目录:

$ mkdir templates

现在创建一个模版文件 templates/greeting.dtl ,内容大概如下:

<!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>

再次make,你会看到rabar创建了一个Erlang模块 ebin/greeting_dtl.beam。 注意,rabar提供了一些选项来自定义模版源文件和编译文件的路径和名称。

现在你可以用下面的代码在处理请求时使用新的模版:

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

运行make,刷新浏览器,你会被你的设计佳作吓到。

正如您看到的,当代码改动后,你需要执行make使之生效。从现在起,我将不再重申需要make

POST请求处理

至此,你的app已经很受欢迎了,但是很多用户会抱怨他们记不住QueryString的语法,并希望能够通过页面填写表单的形式来提交用户名。现在编辑模版,为username添加一个文本框表单:

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

你还需要更改一下请求处理程序,使其支持POST请求,它看起来应该像这样:

loop(Req, DocRoot) -> 
    "/" ++ Path = mochiweb_request:get(path, Req), 
    try 
        case mochiweb_request:get(method, Req) of 
            Method when Method =:= 'GET' ; Method =:= 'HEAD' -> 
                case Path of 
                    "hello_world" -> 
                        mochiweb_request:respond({200, [{"Content-Type", "text/plain"}], "Hello world1!\n"}, Req); 
                    "hello" ->
                        QueryStringData = mochiweb_request:parse_qs(Req),
                        Username = proplists:get_value("username", QueryStringData, "Anonymous"),
                        {ok, HTMLOutput} = greeting_dtl:render([{username, Username}]),
                        mochiweb_request:respond({200, [{"Content-Type", "text/html"}], HTMLOutput}, Req);
                        % mochiweb_request:respond({200, [{"Content-Type", "text/plain"}],
                        %            "Hello " ++ Username ++ "!\n"}, Req);
                    _ -> 
                        mochiweb_request:serve_file(Path, DocRoot, Req) 
                end; 
            'POST' -> 
                case Path of
                    "hello" ->
                        PostData = mochiweb_request:parse_post(Req),
                        Username = proplists:get_value("username", PostData, "Anonymous"),
                        {ok, HTMLOutput} = greeting_dtl:render([{username, Username}]),
                        mochiweb_request:respond({200, [{"Content-Type", "text/html"}], HTMLOutput}, Req);
                     _ -> 
                        mochiweb_request:not_found(Req) 
                end; 
            _ -> 
                mochiweb_request:respond({501, [ ], [ ]}, Req) 
        end 
    catch 
        ?CAPTURE_EXC_PRE(Type, What, Trace) -> 
            Report = ["web request failed", {path, Path}, {type, Type}, {what, What}, {trace, ?CAPTURE_EXC_GET(Trace)}], 
            error_logger:error_report(Report), 
            mochiweb_request:respond({500, [{"Content-Type", "text/plain"}], "request failed, sorry\n"}, Req) 
    end.

它看起来工作得非常好,但还是有一些问题。可见"hello"出现在了2个地方,不是个好兆头。重复代码来渲染模版和返回Response也不太好。我们注意到,当我们访问/hello/ (末尾加斜杠)的地址时会得到一个页面未找到的错误。是时候做一些重构了。

简单的URL调度器(Dispatcher)

我们将创建一个简约的URL调度器来实现对URL规则与Erlang函数的的映射,URL规则配置看起来像这样:

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

这是说所有以/hello 或者 /hello/ 的请求都会被路由到名为hello的函数。下面给出调度器的代码:

% Iterate recursively on our list of {Regexp, Function} tuples
dispatch(_, []) -> none;
dispatch(Req, [{Regexp, Function}|T]) -> 
    "/" ++ Path = mochiweb_request:get(path, Req),
    Method = mochiweb_request:get(method, Req),
    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.

将调度器代码插入到 greeting_web.erl 的适当位置,并修改loop/2 函数来使用它:

loop(Req, DocRoot) -> 
    "/" ++ Path = mochiweb_request:get(path, Req), 
    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
                        mochiweb_request:serve_file(Path, DocRoot, Req);
                    false ->
                        % Otherwise the page is not found
                        mochiweb_request:not_found(Req) 
                end;
            Respond ->
                Respond
        end
    catch 
        ?CAPTURE_EXC_PRE(Type, What, Trace) -> 
            Report = ["web request failed", {path, Path}, {type, Type}, {what, What}, {trace, ?CAPTURE_EXC_GET(Trace)}], 
            error_logger:error_report(Report), 
            mochiweb_request:respond({500, [{"Content-Type", "text/plain"}], "request failed, sorry\n"}, Req) 
    end.

现在我们来创建一个模块,其包含所需的URL规则配置和请求处理程序。新建 src/greeting_views.erl 文件,并输入以下代码:

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

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

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

我们再使用一个函数 render_ok/3 来防止返回Response时的重复代码。让我们把这个函数放到 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
    mochiweb_request:ok({"text/html", Output}, Req).

现在,你已经有了一些通用方式来处理请求了;我们删除了一些重复代码,使得看起来更加有条理了,并且也定义了专门放置请求处理程序和工具函数的地方。

一切都挺不错,但是你的朋友告诉你,他想能通过GET请求获得一个欢迎辞,但他却觉得用QueryString的方式(?username=alice)很难看。而他希望通过这样的访问地址/hello/Alice 或/hello/Alice/ 即可得到一个欢迎辞页面。幸运的是,我们的URL调度器已经能很容易的实现该新功能。

添加第二个URL配置项,现在配置看起来是这样:

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

再创建一个请求处理函数 (在 greeting_views.erl 中),该函数可接收URL中的参数:

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).

瞧,现在/hello/Alice 或 /hello/Alice/ 都可以正常工作了。

处理COOKIES

你收到了许许多多的反馈,有些反馈是希望可以在下次再访问/hello/ 的时候能记住之前他们的名字就更好了。我们使用cookie来实现它,编辑greeting_shortcuts.erl文件,并添加一个函数返回一个cookie值,若不存在则返回默认值。还需要创建一个新函数 render_ok/4 ,它基本上很像我们已有的 render_ok/3,除了它需要一个额外的参数 Headers用于发送Cookie头。修改render_ok/3 让其直接调用 render_ok/4 ,传递一个空的listHeaders参数。

-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),
    mochiweb_request:ok({"text/html", Headers, Output}, Req).

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

现在编辑你的视图模块,使用以上新函数,而我们还需要删除一些重复的东西:

-module(greeting_views).
-compile(export_all).
% -import(greeting_shortcuts, [render_ok/3]).
-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, mochiweb_request:parse_qs(Req));
hello('POST', Req) ->
    handle_hello(Req, mochiweb_request:parse_post(Req)).

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

当用户设置过他们的用户名时,用户名将被存储为cookie,且下次访问/hello/时将被显示。

结论

本教程就到这里,现在你已经知道了如何添加库到项目、获取用户输入、渲染模版和设置cookies,你需要积累更多的功能,如用户认证、全局模版上下文、数据持久等等。还需要修改URL调度器使其能映射到指定模块的指定方法,或用其他方法,或许采用“约定大于配置”会比较好。

我希望你稍微多熟悉一下MochiWeb,那样你才可以用最合适的方法来实现你的需求。浏览API文档,了解更多MochiWeb所提供的功能,毫不犹豫的阅读其源代码。这里有一些我使用Erlang库工作的一些“真理”:源代码通常比文档讲得更多;有幸你跟我一样的话,你会发现Erlang的代码通常比其他语言更容易理解,可能是因为它的函数式特性和简约。

mochiweb应用greeting启动及请求处理简要流程

启动脚本start-dev.sh调用greeting:start()启动greeting项目。然后在greeting:start()中去调用greeting_deps:ensure()加载项目依赖文件的路径到代码搜索路径;调用ensure_started(crypto)启动crypto应用;调用application:start(greeting)启动应用greeting

根据greeting.app中的配置,会去调用greeting_app:start()启动应用程序greeting,在此去调用greeting_sup:start()去启动监控树的主进程。监控树启动后,会通过greeting_web:start/1调用mochiweb_http:start/1启动http服务器。

mochiweb_http:start/1会去启动mochiweb_clock进程,用于更新格式化日期缓存;调用mochiweb_socket_server:start/1通过mochiweb_socket:listen/4创建Socket并监听端口,通过mochiweb_acceptor:start_link/4创建一个进程调用mochiweb_socket:transport_accept/1接受请求。

当有请求来的时候,就会调用mochiweb_http:loop/3greeting_web:loop/2处理请求。

 类似资料: