转载译文: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
的知识。
首先使用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
打开浏览器,访问http://localhost:8080
,你会看到一行信息:“greeting running.
” 标题栏显示“It Worked
”,这说明运行成功了
回到终端,按回车后将出现一个Erlang Shell>
,你可以使它来跟你的app
交互,这对app
的调试非常有用.
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 = 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
,应该不难理解这段代码的作用。它从请求中提取路径,如果该请求的Method
是GET
或HEAD
(默认将"/
"映射至/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
文件,你会看到一个条目指向MochiWeb
的git
仓库,让我们添加另外一个指向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
。
至此,你的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
调度器来实现对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/
都可以正常工作了。
你收到了许许多多的反馈,有些反馈是希望可以在下次再访问/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),
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
的代码通常比其他语言更容易理解,可能是因为它的函数式特性和简约。
启动脚本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/3
及greeting_web:loop/2
处理请求。