在本系列文章的第一篇中,您将了解如何使用 Smarty 模板为 Ajax 请求生成 JSON、XML 和 HTML 响应。这些技术允许您在开发 PHP 代码时关注于应用逻辑,而这些应用逻辑是与 Ajax 客户端和服务器之间通信所使用的数据格式分离的。
您还将了解如何创建两个版本的表单,其中一个提供输入域让用户输入数据,另一个使用隐藏域并以不可编辑形式显示数据。通过单击一个按钮,用户能够切换两个版本的表单,使用 Ajax 向服务器提交数据并获取用于更新页面的 HTML 内容。此外,这个表单在 Web 浏览器禁用 JavaScript 时仍然能够使用。
本文的最后一部分包含配置 Smarty 和示例应用的说明。如果您的服务器或工作站启用了 SELinux,这个过程会有些复杂。这些信息对于需要修改公共文件的 Web 应用是很有用的,如内容管理系统和允许用户上传内容的网站。不管您使用的是 Smarty、流行 CMS 或定制系统,在您的代码尝试修改 Web 文件时,您都会遇到相同的与 SELinux 有关的配置问题。本文阐述了如何使用 Linux 的 restorecon
、chcon
和 setsebool
命令解决这些问题。
在本节中,我们将了解如何创建用来为 Ajax 请求生成响应的 Smarty 模板。您可以使用任何一种通用格式,如 JSON、XML 或 HTML。Smarty 语法主要是面向 HTML 设计的,这使它也非常适合于 XML。然而,使用 Smarty 创建 JSON 响应会有一些困难,因为模板构建的语法使用了 {
和 }
,这意味着如果 JSON 使用了这两个字符,您需要对它们进行转义。然而,您会发现可以修改 Smarty 的分隔符以避免这个语法冲突。您还将了解如何创建自定义修饰符和函数,并将它们注册到 Smarty 框架,这样您就能够在您的模板中使用它们了。
让我们从一个简单的例子(如清单 1 所示)开始,它将生成一个可以被 Ajax 客户端使用的 XML 响应。首先,PHP 代码应该设置内容类型以及 no-cache
响应头,它可以保证 Web 浏览器不会缓存 Ajax 响应。考虑到您以前可能没有使用过 Smarty,我们在这里简要介绍一下 demo_xml.php 文件的作用:它通过 require
包含了 Smarty 类,创建了这个类的一个实例,设置了它的编译和调试标记,使用 assign()
创建两个名为 root_attr
和elem_data
的变量,然后使用 display()
调用一个 Smarty 模板,这样就将生成了 XML 响应。
清单 1. demo_xml.php 示例
<?php header("Content-Type: text/xml"); header("Cache-Control: no-cache"); header("Pragma: no-cache"); require 'libs/Smarty.class.php'; $smarty = new Smarty; $smarty->compile_check = true; $smarty->debugging = false; $smarty->force_compile = 1; $smarty->assign("root_attr", "< abc & def >"); $smarty->assign('elem_data', array("111", "222", "333")); $smarty->display('demo_xml.tpl'); ?> |
demo_xml.tpl 模板(见清单 2)生成了一个 <root>
元素,它有一个属性,它的值是从 demo_xml.php 文件的变量 root_attr
查询的。字符 <
、>
、"
、'
和 &
分别使用 Smarty 的 escape
修饰词替换成<
、>
、"
、'
和 &
。在 root
元素中,模板使用 Smarty 的 {section}
遍历 elem_data
数组的元素,这个数组是 demo_xml.php 文件中定义的第二个变量。demo_xml.tpl 会为数组中的每一个元素生成包含从数组查询到的值的 XML 元素。
清单 2. demo_xml.tpl 模板
<root attr="{$root_attr|escape}"> {section name="d" loop=$elem_data} <elem>{$elem_data[d]|escape}</elem> {/section} </root> |
清单 3 包含了由 demo_xml.php 文件和 demo_xml.tpl 模板生成的 XML 输出。
清单 3. XML 输出
<root attr="< abc & def >"> <elem>111</elem> <elem>222</elem> <elem>333</elem> </root> |
demo_json.php 文件(如清单 4 所示)设计了 no-cache 头,并且如清单 3 所示例子一样创建和配置了 Smarty 对象。此外,它还定义了两个名为 json_modifier()
和 json_function()
的函数,它们会调用 json_encode()
PHP 函数。这两个函数被注册到 Smarty 上,这样它们就可以在模板中使用了,您将在本节的后面看到如何使用。在这之后,demo_json.php 文件创建了一些以下类型的 Smarty 变量:字符串型、数字型、布尔型和数组。然后,PHP 例子执行 demo_json.tpl 模板生成 JSON 响应。
清单 4. demo_json.php 示例
<?php header("Content-Type: application/json"); header("Cache-Control: no-cache"); header("Pragma: no-cache"); require 'libs/Smarty.class.php'; $smarty = new Smarty; $smarty->compile_check = true; $smarty->debugging = false; $smarty->force_compile = 1; function json_modifier($value) { return json_encode($value); } function json_function($params, &$smarty) { return json_encode($params); } $smarty->register_modifier('json', 'json_modifier'); $smarty->register_function('json', 'json_function'); $smarty->assign('str', "a/"b/"c"); $smarty->assign('num', 123); $smarty->assign('bool', false); $smarty->assign('arr', array(1,2,3)); $smarty->display('demo_json.tpl'); ?> |
除了注册插件(如修饰符或函数)到 Smarty,您也可以使用 Smarty 文档中描述的一些特别的命名规范 (见 参考资料)。然后您可以将您的代码放到 plug-ins 目录,这样您的 Smarty 修饰符和函数就可以在应用的任何 Web 页面中使用。
因为 JSON 和 Smarty 的语法中都使用了 {
和 }
,所以您必须在 Smarty 模板中使用 {ldelim}
和 {rdelim}
生成 JSON 响应中使用的 {
和 }
字符。您也可以将 {
和 }
放到 {literal}
和 {/literal}
之间。您将在另一个例子中看到,我们可以修改 Smarty 分隔符以避免这样的麻烦。
demo_json.tpl
模板(见清单 5)使用 json
修饰符对 demo_json.php 文件中的四个变量值进行编码。例如,对引号和其它特殊字符进行转义是很有用的,如字符串中的制表符和换行符。Smarty 将会在 demo_json.php 文件中模板每次使用 |json
时调用 json_modifier()
。json
修饰符前面必须加上 @
字符,这样数组变量就可以传递到json_modifier()
。如果没有使用 @
字符,修饰符函数会对数组的每一个元素进行调用。
从 demo_json.tpl 模板构建的 {json ... }
会被转换成 demo_json.php 文件的 json_function()
的一个调用。这个函数会从模板中获取一个名为 params
的数组属性,同时被传递到 json_encode()
,这个函数会返回 JSON 表示的 PHP 数组。
清单 5. demo_json.tpl 模板
{ldelim} s: {$str|json}, n: {$num|json}, b: {$bool|json}, a: {$arr|@json}, o: {json os=$str on=$num ob=$bool oa=$arr}, z: {literal}{ x: 1, y: 2 }{/literal} {rdelim} |
清单 6 包含了由 demo_json.php 文件和 demo_json.tpl 模板生成的 JSON 输出。
清单 6. JSON 输出
{ s: "a/"b/"c", n: 123, b: false, a: [1,2,3], o: {"os":"a/"b/"c","on":123,"ob":false,"oa":[1,2,3]}, z: { x: 1, y: 2 } } |
要避免在 Smarty 模板中使用 {ldelim}
和 {rdelim}
,您可以修改如清单 7 所示的 Smarty 分隔符。
清单 7. 修改 Smarty 分隔符
$smarty->left_delimiter = '<%'; $smarty->right_delimiter = '%>'; |
清单 8 所示的模板使用 Smarty 构件中的 <%
和 %>
分隔符生成 JSON 响应。
清单 8. demo_json2.tpl 模板
{ s: <% $str|json %>, n: <% $num|json %>, b: <% $bool|json %>, a: <% $arr|@json %>, o: <% json os=$str on=$num ob=$bool oa=$arr %>, z: { x: 1, y: 2 } } |
本节的例子说明了如何使用 Smarty 生成使用 Ajax 获得的 HTML 内容。此外,这个 Web 页面也包含了一个 HTML 表单,这个表单的数据是使用 jQuery 框架以 Ajax 方式提交到服务器上的。如果 Web 浏览器禁用了 JavaScript,这个表单仍然会正确工作,Smarty 也仍然可用在服务器端生成内容。
demo_form.tpl 模板(见清单 9)包含了一个 HTML 表单,这个表单的域可能是可编辑的,也可能是不可编辑的,这取决于名为 edit_mode
的变量值。这个变量是在调用模板的 PHP 代码中设置的,您将会在本节的后面看到。edit_mode
的值也会被存储在表单的一个隐藏域中。
清单 9. demo_form.tpl 模板的 HTML 表单
<form method="POST" name="demo_form"> <input type="hidden" name="edit_mode" value="{if $edit_mode}true{else}false{/if}"> <table border="0" cellpadding="5" cellspacing="0"> ... </table> </form> |
清单 10 显示的是表单的第一个域,如果 edit_mode
是 true
,它就是一个输入框;如果 edit_mode
是 false
,它就是一个隐藏域。在后一种情况中,这个域的不可编辑值会被 {$smarty.post.demo_text|escape}
包含在输出中。当用户提交这个可编辑表单时,参数 demo_text
包含了用户的输入。当表单是不可编辑的,由于有一个隐藏域,这个参数仍然会出现。因此,不管表单是否可以编辑,我们都可以使用 $smarty.post.demo_text
获得这个 post 参数的值。
清单 10. demo_form.tpl 模板的文本框
<tr> <td>Demo Text:</td> <td> {if $edit_mode} <input type="text" name="demo_text" size="20" value="{$smarty.post.demo_text|escape}"> {else} <input type="hidden" name="demo_text" value="{$smarty.post.demo_text|escape}"> {$smarty.post.demo_text|escape} {/if} </td> </tr> |
这个表单的下一个输入域是一个复选框(见清单 11)。在可编辑的表单中,元素 input
只有在出现参数demo_checkbox
时才会有一个属性 checked
。类似地,不可编辑的表单只有在所提交表单数据中包含了名为demo_checkbox
的 post 参数时才会包含这个隐藏表单元素。
清单 11. demo_form.tpl 模板的复选框
<tr> <td>Demo Checkbox:</td> <td> {if $edit_mode} <input type="checkbox" name="demo_checkbox" {if $smarty.post.demo_checkbox}checked{/if}> {else} {if $smarty.post.demo_checkbox} <input type="hidden" name="demo_checkbox" value="On"> {/if} {if $smarty.post.demo_checkbox}On{else}Off{/if} {/if} </td> </tr> |
表单的表格的下面一行包含了三个单选按钮(见清单 12)。模板代码通过比较参数 demo_radio
与每个按钮的值决定应该选择哪个单选按钮。不可编辑的表单使用一个隐藏输入域存储参数值并使用 $smarty.post.demo_radio
向用户显示这个值。
清单 12. demo_form.tpl 模板的单选按钮
<tr> <td>Demo Radio:</td> <td> {if $edit_mode} <input type="radio" name="demo_radio" value="1" {if $smarty.post.demo_radio == '1'}checked{/if}>1 <input type="radio" name="demo_radio" value="2" {if $smarty.post.demo_radio == '2'}checked{/if}>2 <input type="radio" name="demo_radio" value="3" {if $smarty.post.demo_radio == '3'}checked{/if}>3 {else} <input type="hidden" name="demo_radio" value="{$smarty.post.demo_radio|escape}"> {$smarty.post.demo_radio|escape} {/if} </td> </tr> |
一个表单列表的选项是在一个循环的 {section}
中生成的,如清单 13 所示。当前循环的次数会被赋值给一个名为 demo_counter
的模板变量,它会与选项元素的值进行比较以便确定这个选项是否被选中。
清单 13. demo_form.tpl 模板的列表
<tr> <td>Demo Select:</td> <td> {if $edit_mode} <select name="demo_select" size="1"> {section name="demo_section" start=10 loop=100 step="10"} {assign var="demo_counter" value=$smarty.section.demo_section.index} <option {if $smarty.post.demo_select == $demo_counter} selected{/if} value="{$demo_counter}"> {$demo_counter} </option> {/section} </select> {else} <input type="hidden" name="demo_select" value="{$smarty.post.demo_select|escape}"> {$smarty.post.demo_select|escape} {/if} </td> </tr> |
提交按钮会根据 edit_mode
标记(见清单 14)的不同值显示不同的标签(Save 或 Edit)。onclick
属性包含了一个名为 submitDemoForm()
的 JavaScript 函数调用。您将会在本文后面的内容中看到这一点,这个函数使用 Ajax 将表单数据提交到服务器,然后返回 false
,这样 Web 浏览器不会多次响应按钮的单击事件而提交相同的数据。然而,如果 JavaScript 被禁用了,submitDemoForm()
将不会被调用,而 Web 浏览器就会将表单提交到服务器上。因此,这个表单不管 JavaScript 是否启用都会生效。
清单 14. demo_form.tpl 模板的提交按钮
<tr> <td> </td> <td> <button type="submit" onclick="return submitDemoForm()"> {if $edit_mode}Save{else}Edit{/if} </button> </td> </tr> |
demo_page.tpl 文件(见清单 15)包含了两个 <script>
元素,其中一个是引用 jQuery,另一个是引用示例应用中的 JavaScript 文件。这个页面模板使用 Smarty 的 {include}
包含了元素 <div>
中的一个表单模板。
清单 15. demo_page.tpl 模板
<html> <head> <title>Demo</title> <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"> </script> <script type="text/javascript" src="demo_js.js"> </script> </head> <body> <div id="demo_div"> {include file="demo_form.tpl"} </div> </body> </html> |
demo_html.php 文件(如清单 16 所示)是 Ajax 和 Smarty 之间的桥梁,负责处理 Ajax 请求并在 demo_form.tpl 模板中使用 Smarty 生成 Ajax 响应,而这个模板只有当出现一个 Ajax 请求头时才会被调用。这个头是在 JavaScript 代码中设置的,您将在下面的小节中看到。如果没有 Ajax 头,这些代码就会使用 demo_page.tpl 模板,这意味着这是 Web 浏览器 的初始页面请求或 JavaScript 被禁用。在每一个请求中,edit_mode
标记的值都会转换成另一个可编辑表单,并且它是不可编辑的。
清单 16. demo_html.php 示例
<?php header("Cache-Control: no-cache"); header("Pragma: no-cache"); require 'libs/Smarty.class.php'; $smarty = new Smarty; $smarty->compile_check = true; $smarty->debugging = false; $smarty->force_compile = 1; $edit_mode = @($_REQUEST['edit_mode'] == "true"); $smarty->assign("edit_mode", !$edit_mode); $ajax_request = @($_SERVER["HTTP_AJAX_REQUEST"] == "true"); $smarty->display($ajax_request ? 'demo_form.tpl' : 'demo_page.tpl'); ?> |
单击表单按钮时会调用 submitDemoForm()
函数(见清单 17)。这个函数会通过 jQuery 使用 POST
和 Web 表单的相同 URL 将表单数据发送到服务器。然后表单数据会被 jQuery 的 serialize()
API 编码成一个字符串。在本例中,jQuery 在发送 Ajax 请求之前会调用 beforeSend()
函数设置 Ajax-Request
头,这个信息是服务器端用以确定 Ajax 请求的。在 Ajax 请求结束后,success()
函数会被调用。这个回调函数会将响应内容插入到 Web 页面的 <div>
元素中。
清单 17. demo_js.js 示例
function submitDemoForm() { var form = $("form[name=demo_form]"); $.ajax({ type: "POST", url: form.action ? form.action : document.URL, data: $(form).serialize(), dataType: "text", beforeSend: function(xhr) { xhr.setRequestHeader("Ajax-Request", "true"); }, success: function(response) { $("#demo_div").html(response); } }); return false; } |
在解压缩示例应用后,您应该会看一个名为 ajaxsmarty 的目录,它包含了多个 PHP 文件、一个 JavaScript 文件和四个子目录:cache、configs、templates 和 templates_c。模板目录包含了示例应用的 Smarty 模板。其它三个子目录是空的。
下载最新稳定版本的 Smarty (见 参考资料),然后解压缩它。(示例项目是在 Smarty 2.6.25 下测试的。)接下来,将 Smarty 的子目录 libs 复制到 ajaxsmarty 目录,这个目录是示例应用的主目录。
将 ajaxsmarty 目录(同时包含示例应用和 Smarty 的 libs
)上传或复制到 Apache 的 HTML 目录。如果您使用的是一个 Web 主机公司的服务器,SELinux 可能被禁用了,因为启用 SELinux 可能会有太多的支持呼叫。如果你在自己的 Linux 服务器上测试应用,您的服务器有可能启用了 SELinux,那么当浏览器请求一个 PHP 文件时您可能会得到下面的错误:“SELinux is preventing the httpd from using potentially mislabeled files.” 解决方法是以 root 身份运行清单 18 所示的命令。
清单 18. 设置 Web 文件的安全性上下文(标签)
restorecon -R -v /var/www/html/ajaxsmarty |
至此,您应该能够在浏览器上打开地址:http://localhost/ajaxsmarty/,页面会显示三个链接。如果您单击其中一个链接,您会在 Web 浏览器上得到以下的 Smarty 错误:“Fatal error: Smarty error: unable to write to $compile_dir ‘/var/www/html/ajaxsmarty/templates_c’. Be sure $compile_dir is writable by the Web server user. in /var/www/html/ajaxsmarty/libs/Smarty.class.php on line 1113”。
出现上面的错误是因为 Smarty 安装还没有完成。您必须给 Web 服务器写 templates_c 和 cache 目录的用户权限。实现这一步的正确做法是修改它们的拥有者,如清单 19 所示。注意 apache
用户名和服务器的 HTML 目录在您的计算机上可能会有所不同。
清单 19. 修改两个目录的拥有者使 Smarty 能够创建文件
chown apache:apache /var/www/html/ajaxsmarty/templates_c chown apache:apache /var/www/html/ajaxsmarty/cache |
如果您不是使用 root 用户登录,您是不能使用 chown
修改 templates_c 和 cache 的写权限的。您可以通过使用 FTP 客户端修改,或者使用 chmod
命令将权限设置为 777。允许所有用户都能写这些文件夹不是一个非常好的做法,但是如果您不能使用 chown
命令,这是您唯一快速有效的方法。如果您的 Web 服务器是公共的,您应该联系服务器管理员帮您完成这个修改。
如果您的计算机启用了 SELinux,您可能还会在浏览器上遇到这样的一个错误:“SELinux prevented httpd reading and writing access to http files.” 或 “SELinux is preventing httpd (httpd_t) write to ./templates_c (public_content_rw_t).” 解决方法是以 root 身份运行清单 20 中的命令。
清单 20. 在启用 SELinux 时允许 Smarty 在它的目录中创建文件
chcon -t public_content_rw_t /var/www/html/ajaxsmarty/templates_c chcon -t public_content_rw_t /var/www/html/ajaxsmarty/cache setsebool -P allow_httpd_anon_write=1 |
带有 allow_httpd_anon_write
参数的 setsebool
命令必须只执行一次。它允许 httpd 守护进程将文件写到public_content_rw_t
目录中。
在本文中,您了解了如何创建为 Ajax 请求产生 JSON、XML 和 HTML 响应的 Smarty 模板,如何使用 Smarty 创建一个即使在 Web 浏览器禁用 JavaScript 时仍然有效的 Ajax 表单,以及如何在启用 SELinux 的 Linux 主机上配置 Smarty。