原文出处:http://alexmarandon.com/articles/mochiweb_tutorial/
简介
MochiWeb由Bob Ippolito 创建,其描述为:“一个创建轻量级http服务器的Erlang库”。它不是框架:不附带URL调度、模版引擎、数据持久等。尽管没有官方网站和文档,但仍然是Erlang构建web服务的热门选择。这篇随笔将带您逐步入门并构建一个支持URL调度和模版引擎的迷你型框架。(不包含数据持久)
我假设您已经具备一些Erlang语言的基础,否则,建议您先学习这本指南的前面部分章节,本教程不需要具备对并发和分布式Erlang的知识。
如果您遇到问题,可以从这里获取本教程相应的代码。每次提交对应本教程相应部分,所以你可以很容易的找到某个步骤相关的代码。
入门
首先使用Git从github获取MochiWeb源代码:
$ git clone git://github.com/mochi/mochiweb.git
接下来,我们创建一个项目,叫做 greeting:
$ cd mochiweb
$ make app PROJECT=greeting
很简单,现在我们可以编译和运行我们的app了:
$ cd ../greeting/
$ make
$ ./start-dev.sh
此时,你会看到一堆进度报告的信息,其中有类似于{port, 8080}字样,这是说我们的app将运行在8080端口。此时,可以打开浏览器,访问http://localhost:8080,你会看到一行信息:“stat running.” 标题栏显示“It Worked”,这说明运行成功了。
这时回到终端,按回车后将出现一个Erlang Shell>,你可以使它来跟你的app交互,这对app的调试非常有用。(相当于ROR的./script/console 或者 Django的manage.py shell)
文档
当我第一次在网上找关于MochiWeb的文档时,大部分搜索结果都是一些人也在找文档的信息。这也是我写本教程的原因。
实际上,MochiWeb本身包含一些参考文档,你可以这样生成它:
$ cd ../mochiweb
$ make edoc
之后,你可以在mochiweb所在目录的找到doc/index.html,用浏览器打开即可。它有助于了解模块概述、可用函数和函数的具体说明。
这里有一个很棒的视频教程展示了一种有趣的方法搭建完全基于MochiWeb的AJAX应用程序.
本篇教程中,我使用了一种更传统的方法来实现按规则将请求映射到相应的Erlang函数.(译者注:类似于Django中的url pattern 映射相应的views方法)
基本的请求处理
当我们首次请求app时,页面上显示的信息来自于greeting/priv/www/下的index.html文件,此目录将供我们放置一些静态文件,如css,图片等。现在,可能更有趣的事应该是开始创建一个请求处理程序,得到一些用户输入。
我们将在src/greeting_web.erl中插入一些代码来处理请求,该模块中包含一个函数 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.
如果你能阅读Erlang,应该不难理解这段代码的作用。它从请求中提取路径,如果该请求的Method是GET或HEAD(默认将"/"映射至/index.html),如果是POST或者其他HTTP动作,将返回404和错误提示。任何异常将被捕获并显示在终端,此处我没有给出异常处理的代码,但并不意味着你可以不管它。
我们现在要做的是添加一些代码来处理访问路径为/hello 的请求,从QueryString中获得用户名,并显示欢迎词,在上面代码中对GET请求处理的条件分支处添加下面的字句:
"hello" ->
QueryStringData = Req:parse_qs(),
Username = proplists:get_value("username", QueryStringData, "Anonymous"),
Req:respond({200, [{"Content-Type", "text/plain"}],
"Hello " ++ Username ++ "!\n"});
首先我们使用 mochiweb_request:parse_qs/0 方法得到包含query string参数的proplist ,然后用
proplist:get_value/3 方法得到username的参数值,如果不存在则默认为"Anonymous".
最后我们调用
mochiweb_request:respond/1方法,它需要传递一个元组参数,其中包含:HTTP状态码、头信息proplist、主体信息. 下面是我们的新loop/2函数:
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.
使用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文件,你会看到一个条目指向MochiWeb的git仓库,让我们添加另外一个指向ErlyDTL的条目,此时配置文件应该是这样:
%% -*- 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,"."}]}}]}.
你仅仅只需要输入make,即可获取和编译ErlyDTL:
$ 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
[...]
由于使用了新的库,要使其生效,需重启应用程序。终端输入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 = 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});
运行make ,刷新浏览器,你会被你的设计佳作吓到。
正如您看到的,当代码改动后,你需要执行make使之生效。从现在起,我将不再重申需要make。
POST请求处理
至此,你的app已经很受欢迎了,但是很多用户会抱怨他们记不住QueryString的语法,并希望能够通过页面填写表单的形式来提交用户名。现在编辑模版,为username添加一个文本框表单:
<form method="POST">
<p>Username: <input type="text" name="username"></p>
<input type="submit">
</form>
你还需要更改一下请求处理程序,使其支持POST请求,它看起来应该像这样:
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.
它看起来工作得非常好,但还是有一些问题。可见"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 = 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.
将调度器代码插入到 greeting_web.erl
的适当位置,并修改loop/2 函数来使用它:
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.
现在我们来创建一个模块,其包含所需的URL规则配置和请求处理程序。新建 src/greeting_views.erl
文件,并输入以下代码:
-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}]).
我们再使用一个函数 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
Req:ok({"text/html", Output}).
现在,你已经有了一些通用方式来处理请求了;我们删除了一些重复代码,使得看起来更加有条理了,并且也定义了专门放置请求处理程序和工具函数的地方。
一切都挺不错,但是你的朋友告诉你,他想能通过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 ,传递一个空的list给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.
现在编辑你的视图模块,使用以上新函数,而我们还需要删除一些重复的东西:
-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).
当用户设置过他们的用户名时,用户名将被存储为cookie,且下次访问/hellp/时将被显示。
结论
本教程就到这里,现在你已经知道了如何添加库到项目、获取用户输入、渲染模版和设置cookies, 你需要积累更多的功能,如用户认证、全局模版上下文、数据持久等等。还需要修改URL调度器使其能映射到指定模块的指定方法,或用其他方法,或许采用“约定大于配置”会比较好。
我希望你稍微多熟悉一下MochiWeb,那样你才可以用最合适的方法来实现你的需求。浏览API文档,了解更多MochiWeb所提供的功能,毫不犹豫的阅读其源代码。这里有一些我使用Erlang库工作的一些“真理”:源代码通常比文档讲得更多;有幸你跟我一样的话,你会发现Erlang的代码通常比其他语言更容易理解, 可能是因为它的函数式特性和简约。