Zotonic:Erlang 内容管理系统

作者:Michael Connors

Zotonic 被其作者描述为一个实用且现代的 CMS,它确实如此,甚至远不止于此。当我开始使用 Zotonic 时,是因为它的效率以及我能够将多个客户的 CMS 站点打包到一台资源有限的机器上。然而,我很快发现,Zotonic 不仅是一个 CMS,还是一个 Web 框架,它使我能够在比使用更传统的语言和框架所需时间少得多的时间内创建非常复杂的 Web 站点。Zotonic 不会在遇到错误时崩溃,也不需要在每次请求进来时用棍子戳一下才能唤醒。

Zotonic 是用 Erlang 编写的,Erlang 是一种为编程电话交换机而设计的函数式语言。在 Web 开发中使用 Erlang 背后的逻辑是,现代 Web 站点及其来自用户和机器人的大量连接,正开始越来越像电话交换机。“我从来没有用函数式语言编程过,Erlang 对我来说就像荷兰语一样!”,我听到你这样说。好吧,Zotonic 的作者精通 Erlang(顺便说一句,也精通荷兰语),他们在创建一个开箱即用、无论你是否了解 Erlang 的软件方面做得很好,Zotonic 可能正是你深入学习 Erlang 所需的杀手级应用。

Zotonic 的另一个吸引人的特点是它的 PostgreSQL 数据库(参见侧边栏)。对于像我这样已经尝试学习 Erlang 一段时间的人来说,可能最大的障碍之一是,除了学习一种全新的编程范式之外,我还必须学习一种新的数据库,即 mnesia。Zotonic 使用 PostgreSQL 意味着少学一样新东西,至少让我在设计数据时感到身处熟悉的领域。

注意

目前 Zotonic 仅提供 PostgreSQL 数据库;但是,有计划创建“弹性 Zotonic”,它将使用分布式存储。虽然 Zotonic 确实使用了关系数据库,但在大多数情况下,您插入数据库的内容将是一个 Zotonic 资源,其中包含一个键(ID)和一个文档(Props)。资源之间的关系使用谓词定义,例如 has_relation 和 has_part。对于许多 Web 开发,这已经足够了;但是,如果您确实需要,关系数据库的强大功能仍然可供您使用。

更新

目前,默认的 Zotonic 分支正在进行大量开发,安装说明与本文发布时相比略有变化。您可以改为克隆 0.6 版本,使用hg clone -r release-0.6.x https://zotonic.googlecode.com/hg/ zotonic。如果您想体验最新版本,请参阅默认分支的新说明:code.google.com/p/zotonic/source/browse/doc/INSTALL

如果您需要进一步的帮助,也可以查看非常活跃的 Zotonic Google 群组。

依赖项

我正在运行最新版本的 Ubuntu,它预装了 Erlang。您可以通过输入以下命令来测试您是否安装了 Erlangerl在命令行中。如果您得到 Erlang shell,那么您就可以开始了。按 Ctrl-c,然后按字母 a 和回车键退出 Erlang。如果您的系统上没有 Erlang,您可以从 Erlang 网站下载或使用您的发行版的软件包管理器安装它。

另一个依赖项是 ImageMagick;要检查是否已安装,请运行

convert -version

当然,您需要安装 PostgreSQL,并且需要安装 Mercurial 才能从 Google 代码站点获取最新版本的 Zotonic。

安装和配置 Zotonic

获取 Zotonic 源代码并构建它

hg clone https://zotonic.googlecode.com/hg/ zotonic
cd zotonic
make

现在,为 Zotonic 创建一个数据库

CREATE USER zotonic WITH PASSWORD 'yourdbpassword';
CREATE DATABASE zotonic 
    WITH OWNER = zotonic ENCODING = 'UTF8';
GRANT ALL ON DATABASE zotonic TO zotonic;
\c zotonic
CREATE LANGUAGE "plpgsql";
默认站点

Zotonic 完整地附带了一个示例站点,该站点实现了一个简单的博客。您可以在 priv/sites/default/ 目录中找到此默认站点的代码,您可以通过创建一个配置文件并启动 Zotonic 来运行此默认站点。

在 priv/sites/default/config.in 中找到示例配置文件,并重命名它或创建一个没有扩展名的副本

cp priv/sites/default/config.in priv/sites/default/config

在您喜欢的文本编辑器中打开 config,并修改它以使用您刚刚创建的数据库

% Hostname for virtual host support
{hostname, "127.0.0.1:8000"},
{hostalias, "localhost:8000"},
% PostgreSQL database connection
{dbhost, "127.0.0.1"},
{dbport, 5432},
{dbuser, "zotonic"},
{dbpassword, "yourdbpassword"},
{dbdatabase, "zotonic"},

现在,使用 start.sh 在调试模式下启动 Zotonic

./start.sh

您应该在控制台上看到文本飞快闪过,表明正在创建一些表。在浏览器中访问 127.0.0.1:8000,您应该会看到您的新博客。

使用 Zotonic 进行内容管理

Zotonic 首先是一个内容管理系统——这就是它所宣称的。现在您已经有了一个正在运行的 Zotonic 版本,您可以尝试内容管理功能了。

在浏览器中访问 http://127.0.0.1:8000/admin。您将看到一个登录屏幕,由于这是您的首次登录,您需要设置密码。

使用登录名“admin”,密码字段留空,然后单击“登录”。您将看到创建密码表单。

设置密码后,您将看到 Zotonic 管理仪表板。左侧下方是管理菜单。其中大部分都很简单明了,但这里更有趣的项目之一是模块。

Zotonic: the Erlang Content Management System

图 1. Zotonic 仪表板和广播对话框

在“模块”菜单项中,有一个可用模块列表,其中一些已激活,另一些未激活。激活 comments 模块以查看博客文章底部出现的评论表单。

您可以通过创建类别为“文章”的新页面来创建新的博客文章。

在页面列表中,找到主页。您也可以使用管理页面右上角的搜索框搜索“home”来找到它。

打开此页面进行编辑,向下滚动直到看到高级选项。展开高级选项部分,并注意主页已设置唯一的名称 page_home。这对于以后在代码中引用此页面很有用。

自定义前端

Zotonic 使用修改版的 Erlydtl 进行模板化。Erlydtl 是 Django 模板语言的 Erlang 实现。

查看默认 Zotonic 站点的模板文件夹(priv/sites/default/templates)。在这里,您将找到 .tpl 文件的集合,这些文件是定义站点的模板。以 underscore 开头的模板不打算单独呈现,而是可以包含在另一个模板中。

此目录中的大多数模板都继承自 base.tpl,其中包括站点的页眉、菜单和页脚。此站点使用 article.tpl 显示类别为“文章”的页面,并使用 home.tpl 显示主页。

调度规则将 URI 映射到资源。查看文件 priv/sites/default/dispatch/dispatch。定义了以下两个调度规则

{
     home,  [],
     resource_page, 
     [ {template, "home.tpl"}, {id, page_home} ]
 },
{
     article, ["article", id, slug],
     resource_page,
     [ {template, "article.tpl"}, {cat, article} ]
},

第一条规则规定,您使用模板 home.tpl 呈现唯一名称为 page_home 的页面。

第二条规则规定,您希望将类别为 article 的所有页面呈现为 article.tpl。您还可以在此规则中定义 URL 的结构。每篇文章的地址将采用以下形式:/article/id/page-name。

在这两个示例中,您都使用 resource_page 来执行实际的呈现。这会将资源呈现为 HTML 页面,并允许您从模板访问页面的 ID 和类别。

您创建的其他文本页面将默认呈现为 page.tpl。

默认 Zotonic 站点中已经存在一个唯一名称为 page_about 的关于页面;它当前呈现为 page.tpl。让我们尝试创建我们自己的模板来显示关于页面。

在 templates 目录中创建一个名为 about.tpl 的模板,并将以下代码放入其中

{% extends "base.tpl" %}
    
{% block title %}
    {{ m.rsc[id].title }}
{% endblock %}
    
{% block content %}
    
    <h1>{{ m.rsc[id].title }}
             -- {{ m.rsc[id].summary }}</h1>
   <h2>Hello, this is my about page!</h2>
    {{ m.rsc[id].body|show_media }}
    
{% endblock %}

将以下内容添加到您的调度规则中

{about,      ["about"],
                  resource_page,
                  [ 
                      {template, "about.tpl"}, 
                      {id, page_about}
                  ]
}

停止 Zotonic,并运行make.

再次启动 Zotonic。现在,如果您在浏览器中访问 http://127.0.0.1:8000/about,您将看到使用您创建的新模板呈现的默认关于页面中的文本。

通过从模板访问 about 资源的 ID,您可以调用数据库以检索其他信息进行显示。正如您从上面的模板中看到的,我使用了标题 (m.rsc[id].title)、摘要 (m.rsc[id].summary) 和正文 (m.rsc[id].body)。我还使用了一个名为 show_media 的“过滤器”来将正文文本中的图像标记转换为实际的图像标签以进行显示。

其他一些前端工具的摘要

您已经看到了上面的 show_media 过滤器,并且存在许多其他过滤器来转换数据以进行输出。除了过滤器之外,Zotonic 中的前端开发还得到了标签和 scomps 的帮助。

在上面的示例中,我使用了 block 标签来替换我正在扩展的模板中的内容区域。我经常使用的其他标签是 if、for 和 lib

{% if id == 1 %}
    <p>The ID is 1</p>
{% endif %}
    
{% for color in ["bleu", "blanc", "rouge"] %}
    <p>{{ color }}</p>
{% endfor %}

lib 标签可用于导入样式表或脚本的聚合,以减少对服务器的请求数量

{% lib 
        "css/zp-menu.css"
        "css/zp-project.css"
    
%}

当标签不够强大并且需要更多逻辑时,可以使用 Scomps 或屏幕组件。我最常用的 scomps 是 menu 和 validate。

Menu 用于将标准 Zotonic 菜单插入到您的站点中

{% menu id=id %}

Validate 用于在前端和后端验证表单字段

<input 
    type="password" 
    id="password" 
    name="password" value="" />
<input 
    type="password" 
    id="password2" 
    name="password2" value="" />
{% validate id="password" 
    type={confirmation match="password2"} %}

扩展后端

如果您愿意编写一些 Erlang 代码,Zotonic 可以不仅仅是一个内容管理系统。您可以使用模块扩展 Zotonic。模块可以存储在站点的 modules 子目录中。

要创建一个模块,请在您的站点中创建一个 modules 目录(如果它尚不存在)

mdkir priv/sites/default/modules

让我们创建一个简单的模块,该模块实现一个网站留言簿。用户将能够看到现有的留言簿帖子并添加新帖子。

在 modules 目录中创建一个名为 mod_guestbook 的目录

mkdir priv/sites/default/modules/mod_guestbook

使用您喜欢的文本编辑器,在此目录中创建一个名为 mod_guestbook.erl 的文件,并将以下代码放入此文件中

%% @author Michael Connors
%% @doc A guestbook module. 
-module(mod_guestbook).
-author("Michael Connors <michael@bring42.net>").
-mod_title("Guestbook").
-mod_description("A simple guestbook module.").
-mod_prio(500).
    
%% interface functions
-export([
    init/1,
    datamodel/0
]).
    
-include_lib("zotonic.hrl").
    
%% @doc Initiates the server.
init(Context) ->
    %% Manage our data model
    z_datamodel:manage(?MODULE, 
                       datamodel(), 
                       Context).
datamodel() ->
    [{categories,
      [
       {gp,
        text,
        [{title, <<"Guestbook Post">>}]}
      ]
     }
    ].

停止 Zotonic 并再次运行make。这将构建您的新模块。现在,重新启动 Zotonic,并登录到管理界面。转到模块页面,并观察现在有一个名为 Guestbook 的新模块。

您可以在此处看到在代码中为 author、mod_title、mod_description 和 mod_prio 定义的值。Prio 值指示模块的重要性——最高为 1,默认值为 500。具有较高优先级的模块会首先检查模板和 scomps。

Erlang 中的百分号表示注释,因此代码中任何以百分号开头的行都会被编译器忽略。前两行虽然是注释,但也包含特殊符号,用于记录程序。

在这里,我导出了 init/1。这是因为它必须被外部模块调用;init 的 arity 为 1,这意味着它接受 1 个参数

-export([
    init/1
]).

如果我有一个接受两个参数的函数,我会像这样导出它

-export([
    itsname/2
]).

您不需要导出 datamodel,因为它仅在此模块中的 init 函数中使用。

Init 将在每次加载模块时调用,并且第一次调用时,它将在 Zotonic 中创建一个名为 guestbook_post 的新类别。这将是“text”的子类别,并将具有显示名称 Guestbook Post。

对于每个留言簿帖子,您应该有一个标题和摘要——幸运的是,Zotonic 中的所有页面都已经有标题和摘要,因此无需执行任何其他操作,您可以通过创建类别为 Guestbook Post 的页面来向留言簿添加帖子。现在创建一些留言簿帖子,确保您填写标题和摘要。另外,不要忘记勾选“已发布”;否则,未登录的用户将看不到它们。您可以使用这些来测试您的留言簿的显示,我将在接下来讨论。

模板

在 mod_guestbook 中创建一个名为 templates 的新子目录

mkdir priv/sites/default/mod_guestbook/templates

使用您喜欢的文本编辑器,创建以下名为 guestbook.tpl 的文件

{% extends "base.tpl" %}
    
{% block content %}
<h1>Guestbook</h1>
<ul id="guestbook-posts" class="guestbook-posts">
{% with 
    m.search[
        {query cat='gp' sort='-publication_start'}
    ] as posts %}
    {% for post in posts %}
        {% include "_guestbook_post.tpl" %}
    {% endfor %}
{% endwith %}
</ul>
{% include "_guestbook_form.tpl" %}
{% endblock %}

此模板按发布日期顺序获取类别为 guestbook_post 的页面;它扩展了它所使用的站点的基本模板,并覆盖了该基本模板的“内容块”。

我还包括了另外两个模板,_guestbook_post.tpl 和 _guestbook_form.tpl。我稍后会创建这些模板。

接下来,您需要一个调度规则。在 mod_guestbook 中创建一个名为 dispatch 的新子目录

mkdir priv/sites/default/mod_guestbook/dispatch

使用文本编辑器,在 dispatch 文件夹中创建一个名为 dispatch(没有扩展名)的文件。它应该包含以下调度规则

[
    {guestbook, ["guestbook"],    
                resource_template,   
                [ {template, "guestbook.tpl"}]}
].

上面的第一个参数是规则的名称。接下来是一个列表,其中包含 URI 方案;在这种情况下,它只是 /guestbook。让我们使用一个预制的 Zotonic 资源,称为 resource_template 来进行呈现,而您实际要呈现的模板称为 guestbook.tpl。

保存所有内容,运行make然后重新启动 Zotonic。当您导航到 127.0.0.1:8000/guestbook 时,您将看到一个只包含标题 Guestbook 的页面。

在上面的模板中,我包含了一个名为 _guestbook_post.tpl 的另一个模板,但我尚未创建它。此模板将包含每个留言簿帖子的详细信息,并为每个留言簿帖子呈现一次。现在在 mod_guestbook 的 templates 子目录中创建它

    <li class="guestbook-post">
        <p>{{ post.title }}-{{post.summary}}</p>
    </li>

运行make并重新加载 Zotonic。您现在应该看到您先前在管理界面中创建的留言簿帖子。

下一步是允许用户通过创建 _guestbook_form.tpl 模板来签署留言簿

{% wire id="guestbook-form" 
            type="submit" 
            postback={np} 
            delegate="mod_guestbook" %}
<form id="guestbook-form" 
            method="post" action="postback">
  <div>
    <div class="form-item">
      <label for="title">Title</label>
        <input type="text" name="title" id="title" />
        {% validate id="title" type={presence} %}
    </div>
    <div class="form-item">
      <label for="summary">Summary</label>
      <input type="text" name="summary" id="summary" />
      {% validate id="summary" type={presence} %}
    </div>
    <div class="form-item button-wrapper">
      <button type="submit">{_ Post _}</button>
    </div>
  </div>
</form>

您可以使用 “wire” scomp 来指定带有id="guestbook-form"的表单将由 mod_guestbook 的 event 函数处理。您还可以使用 “validate” scomp 来检查所需字段是否存在。如果您希望摘要字段是可选的,则可以省略摘要字段的 validate scomp。在这里,您只使用 presence 验证器,但还有其他验证器,例如 numericality、length、confirmation(用于确保两个字段匹配)以及非常有用的 format 验证器,它接受正则表达式。

现在,您需要实现 mod_guestbook 的 event 函数来处理此 post

%% @doc Handle the submit event of guestbook
event({submit, {np, _}, _TriggerId, _TargetId}, C) ->
    T = z_context:get_q_validated("title", C),
    S = z_context:get_q_validated("summary", C),
    CatId = 
    m_category:name_to_id_check(gp, C),
    AC = z_acl:sudo(C),
    Props = [
         {category_id, CatId},
         {title, T},
         {summary, S},
         {is_published, true}],
     {ok, RscID} = m_rsc:insert(Props, AC),
     Post = m_rsc:get(RscId, C),
     TemplateProps = [
         {post, Post}
     ],
     Html = z_template:render("_guestbook_post.tpl",
                                  TemplateProps, AC),
     z_render:insert_top("guestbook-posts", 
                                  Html, AC).

不要忘记导出 event/2。现在您正在编写 Erlang 代码,并使用 Zotonic 附带的一些支持函数。

如果您是 Erlang 的新手,首先要注意的是,一旦变量绑定到值,就无法更改它。这可能看起来很奇怪,但其目的是避免副作用,以便您可以编写分布式应用程序。一个不错的副作用(我知道)是它使 Erlang 更容易调试。

在 Erlang 中,您使用列表和元组来存储数据聚合。列表用方括号括起来,元组用花括号括起来。Erlang 中的变量以大写字母 (Props) 开头,您还使用原子,它们是小写的。原子本身没有任何关联的值;本质上,它们就是值。

与访问控制相关的函数可以在 z_acl 中找到,在本例中,您使用z_acl:sudo来获得超级用户权限。z_context:get_q_validated允许您从 post 中获取已验证字段的内容;z_template:render返回呈现的模板,以及z_render:insert_top在具有给定 ID 的 HTML 元素的顶部插入一些文本。更多支持函数可以在 src/support 中找到。

访问数据库的代码可以在 src/models 中找到。在这里,我访问了数据库以检查类别的 ID (m_category:name_to_id_check) 并插入了一个新资源 (m_rsc:insert)。

留言簿尚未完全完成。您仍然需要添加签名者的姓名。但这很容易,而且您无需接触数据库即可完成。只需在表单模板中添加一个新字段,修改您的 event 函数以处理该字段,您的留言簿就完成了。

Michael Connors 是一位来自爱尔兰的自由软件开发人员,但他目前居住在法国诺曼底。如今,他主要使用 Erlang 开发软件。

加载 Disqus 评论