老葛的Drupal培训班 Think in Drupal
在一个文本编辑器中打开空的plusone.module,并添加标准的Drupal头部文件:
<?php
// $Id$
/**
* @file
* A simple +1 voting widget.
*/
接下来我们将逐个的添加我们用到的Drupal钩子。一个比较简单的就是hook_perm(),它让你向Drupal的基于角色的访问控制页面添加一个“rate content”权限。使用这一权限,你就可以阻止那些没有账号或者没有登录的匿名用户进行投票了。
/**
* Implementation of hook_perm().
*/
function plusone_perm() {
return array('rate content');
}
现在,我们开始实现一些AJAX功能。jQuery的一个重要特性,就是它能够提交自己的HTTP GET 或 POST请求,使用这一特性你就可以将投票提交给Drupal,而不用刷新整个页面了。jQuery将拦截投票链接上的点击事件,并向Drupal发送一个请求,让其保存投票并返回更新后的总分数。jQuery将使用这个新值来更新页面上的分数。图17-9展示了整个流程的概览。
一旦jQuery拦截了对投票链接的点击,它需要通过一个URL来调用一个Drupal函数。我们使用hook_menu(),将由jQuery提交的投票URL映射到一个Drupal PHP函数上。这个PHP函数将投票保存到数据库中,并以JSON (JavaScript Object Notation)的形式为jQuery返回一个新分数(噢,这样我们就没有使用XML,因此这也就不是严格意义上的AJAX了)。
/**
* Implementation of hook_menu().
*/
function plusone_menu() {
$items['plusone/vote'] = array(
'page callback' => 'plusone_vote',
'access arguments' => array('rate content'),
'type' => MENU_CALLBACK,
);
return $items;
}
在前面的函数中,当路径为plusone/vote的请求进来以后,如果请求该路径的用户拥有“rate content”权限,那么函数plusone_vote()就会处理这个请求。
图 17-9.投票更新流程概览
注意 如果发送请求的用户没有“
rate content”权限,Drupal将返回一个拒绝访问页面。然而,我们将确保动态的构建我们的投票小部件,这样那些无权投票的用户就看不到投票链接了。对于那些恶意的用户,他们可能会绕过我们的小部件,直接访问http://example.com/?q=plusone/vote,此时Drupal的权限系统仍然会保护我们免受他们的攻击。
路径plusone/vote/3翻译成了PHP函数调用plusone_vote(3)(关于Drupal的菜单/回调系统的更多详细,参看第4章)。
/**
* Called by jQuery, or by browser if JavaScript is disabled.
* Submits the vote request. If called by jQuery, returns JSON.
* If called by the browser, returns page with updated vote total.
*/
function plusone_vote($nid) {
global $user;
$nid = (int)$nid;
// Authors may not vote on their own posts. We check the node table
// to see if this user is the author of the post.
$is_author = db_result(db_query('SELECT uid FROM {node} WHERE nid = %d AND
uid = %d', $nid, $user->uid));
if ($nid > 0 && !$is_author) {
// Get current vote count for this user.
$vote_count = plusone_get_vote($nid, $user->uid);
if (!$vote_count) {
// Delete existing vote count for this user.
db_query('DELETE FROM {plusone_votes} WHERE uid = %d AND nid = %d',
$user->uid, $nid);
db_query('INSERT INTO {plusone_votes} (uid, nid, vote_count) VALUES
(%d, %d, %d)', $user->uid, $nid, $vote_count + 1);
watchdog('plusone', 'Vote by @user on node @nid.', array(
'@user' => $user->name, '@nid' => $nid));
}
}
// Get new total to display in the widget.
$total_votes = plusone_get_total($nid);
// Check to see if jQuery made the call. The AJAX call used
// the POST method and passed in the key/value pair js = 1.
if (!empty($_POST['js'])) {
// jQuery made the call.
// This will return results to jQuery's request.
drupal_json(array(
'total_votes' => $total_votes,
'voted' => t('You voted')
)
);
exit();
}
// It was a non-JavaScript call. Redisplay the entire page
// with the updated vote total by redirecting to node/$nid
// (or any URL alias that has been set for node/$nid).
$path = drupal_get_path_alias('node/'. $nid);
drupal_goto($path);
}
前面的plusone_vote()函数,保存了当前的投票,并向jQuery返回信息;信息的形式是一个关联数组,里面包含了新总分和字符串You voted(你已投票),这个字符串用于替换投票小部件下面的Vote(投票)文本。这个数组将被传给drupal_json(),这样就可以将PHP变量转化为它们的JavaScript等价物,在这里,就是将一个PHP关联数组转化为了一个JavaScript对象,并将HTTP头部设置为Content-type: text/javascript。关于JSON的更多详细,参看http://en.wikipedia.org/wiki/JSON。
注意,在前面的函数中,我们是如何处理浏览器禁用了JavaScript这种情况的。当我们编写jQuery代码时,我们将确保,来自jQuery的AJAX调用会传递一个名为js的参数并使用POST方法。如果没有js参数的话,我们就知道此时是用户点击了投票链接,浏览器本身请求了该路径----例如,plusone/vote/3。在这种情况下,因为浏览器期望的是一个普通的HTML页面,所以我们不会返回JSON。相反,我们更新了投票总分来反映用户投票的事实,接着,我们将浏览器重定向到最初的页面,Drupal将负责重新构建该页面并显示新的投票总分。
在前面的代码中,我们调用了plusone_get_vote()和plusone_get_total(),现在让我们创建这两个函数:
/**
* Return the number of votes for a given node ID/user ID pair.
*/
function plusone_get_vote($nid, $uid) {
return (int)db_result(db_query('SELECT vote_count FROM {plusone_votes} WHERE
nid = %d AND uid = %d', $nid, $uid));
}
/**
* Return the total vote count for a node.
*/
function plusone_get_total($nid) {
return (int)db_result(db_query('SELECT SUM(vote_count) FROM {plusone_votes}
WHERE nid = %d', $nid));
}
现在,让我们集中精力将投票小部件显示在文章旁边。这包括两部分。首先,我们在plusone_widget()函数内部定义一些变量。接着我们将这些变量传递给一个主题函数。下面是第一部分:
/**
* Create voting widget to display on the web page.
*/
function plusone_widget($nid) {
global $user;
$total = plusone_get_total($nid);
$is_author = db_result(db_query('SELECT uid FROM {node} WHERE nid = %d
AND uid = %d', $nid, $user->uid));
$voted = plusone_get_vote($nid, $user->uid);
return theme('plusone_widget', $nid, $total, $is_author, $voted);
}
还记不记得,当我们需要一个可主题化的项目时,我们需要使用hook_theme()来向Drupal声明它,这样就将其包含在了主题注册表中。下面就是这个钩子函数:
/**
* Implementation of hook_theme().
* Let Drupal know about our theme function.
*/
function plusone_theme() {
return array(
'plusone_widget' => array(
'arguments' => array('nid', 'total', 'is_author', 'voted'),
),
);
}
接着,我们就需要实际的主题函数了。注意,在这里我们加载了我们的JavaScript和CSS文件。
/**
* Theme for the voting widget.
*/
function theme_plusone_widget($nid, $total, $is_author, $voted) {
// Load the JavaScript and CSS files.
drupal_add_js(drupal_get_path('module', 'plusone') .'/plusone.js');
drupal_add_css(drupal_get_path('module', 'plusone') .'/plusone.css');
$output = '<div class="plusone-widget">';
$output .= '<div class="score">'. $total .'</div>';
$output .= '<div class="vote">';
if ($is_author) {
// User is author; not allowed to vote.
$output .= t('Votes');
}
elseif ($voted) {
// User already voted; not allowed to vote again.
$output .= t('You voted');
}
else {
// User is eligible to vote.
$output .= l(t('Vote'), "plusone/vote/$nid", array(
'attributes' => array('class' => 'plusone-link')
));
}
$output .= '</div>'; // Close div with class "vote".
$output .= '</div>'; // Close div with class "plusone-widget".
return $output;
}
在前面代码的plusone_widget()函数中,我们先设置了一些变量,接着将小部件的主题化委托给了我们创建的自定义主题函数theme_plusone_widget()。记住theme('plusone_widget')实际上调用的就是theme_plusone_widget()(更多详细,可参看第8章)。创建一个单独的主题函数,而不是在plusone_widget()函数内部构建HTML,这样设计者在想要修改外观时就能覆写这个函数了。
在我们的主题函数theme_plusone_widget()中,一定要为关键的HTML元素添加CSS类属性,这样在jQuery中就可以非常方便的定位这些元素了。还有,看一下链接的URL。它指向了plusone/vote/$nid,其中$nid是文章的当前节点ID。当用户点击这个链接时,由于我们使用jQuery监听该链接上的onClick事件,所以jQuery将代替Drupal来拦截并处理这个事件。看到没有,我们在构建链接时,是如何定义CSS选择器plusone-link的?在我们后面的Javascript中,找到该选择器出现的地方,这就是a.plus1-link。它就是说,一个带有css类plusone-link的HTML元素<a>。
<div class="plusone-widget">
<div class="score">0</div>
<div class="vote">
<a class="plusone-link" href="/plusone/vote/4">Vote</a>
</div>
</div>
theme_plusone_widget()函数用来生成发送给浏览器的小部件。我们想让这个小部件显示在节点视图中,这样当用户查看节点时,就可以进行投票了。你能猜一下,我们将使用哪个Drupal钩子呢?这就是我们的老朋友hook_nodeapi(),它允许我们修改正被构建的任意节点。
/**
* Implementation of hook_nodeapi().
*/
function plusone_nodeapi(&$node, $op, $teaser, $page) {
switch ($op) {
case 'view':
// Show the widget, but only if the full node is being displayed.
if (!$teaser) {
$node->content['plusone_widget'] = array(
'#value' => plusone_widget($node->nid),
'#weight' => 100,
);
}
break;
case 'delete':
// Node is being deleted; delete associated vote data.
db_query('DELETE FROM {plusone_vote} WHERE nid = %d', $node->nid);
break;
}
}
我们将weight元素的值设置为一个大的(或者“重的”)数字,这样就确保了小部件显示在文章的底部,而不是顶部。我们还偷偷的加了一个delete情况,这样当节点被删除时,该节点的投票记录也将被一同删除。
这就是plusone.module的全部内容了。我们的模块马上就要完工了,现在就剩下填写plusone.js了,在里面填写我们的jQuery代码,用于执行AJAX调用,更新投票总分,并将字符串Vote修改为You voted。
// $Id$
// Only run if we are in a supported browser.
if (Drupal.jsEnabled) {
// Run the following code when the DOM has been fully loaded.
$(document).ready(function () {
// Attach some code to the click event for the
// link with class "plusone-link".
$('a.plusone-link').click(function () {
// When clicked, first define an anonymous function
// to the variable voteSaved.
var voteSaved = function (data) {
// Update the number of votes.
$('div.score').html(data.total_votes);
// Update the "Vote" string to "You voted".
$('div.vote').html(data.voted);
}
// Make the AJAX call; if successful the
// anonymous function in voteSaved is run.
$.ajax({
type: 'POST', // Use the POST method.
url: this.href,
dataType: 'json',
success: voteSaved,
data: 'js=1' // Pass a key/value pair.
});
// Prevent the browser from handling the click.
return false;
});
});
}
你应该把你所有的jQuery代码都包装在一个Drupal.jsEnabled测试中。这个测试将确保当前的浏览器支持特定的DOM方法(如果不支持的话,那么将不会执行我们的JavaScript)。
这个JavaScript向a.plusone-link添加了一个事件侦听器(还记不记得我们将plusone-link定义为了CSS类选择器?),这样当用户点击链接时,它将触发一个HTTP POST请求,来请求它指向的URL。前面的代码还演示了,jQuery是如何处理从Drupal中传递回来的数据的。当AJAX请求完成以后,返回值(从Drupal中返回)将作为data参数传递到匿名函数中,我们把这个函数赋值给了变量voteSaved。关联数组中的键所引用的数组,最初是在Drupal内部的plusone_vote()函数中构建的。最后,Javascript更新了分数,并将文本“Vote”修改为了“You Voted”。
为了阻止加载整个页面(因为JavaScript负责处理点击事件),在JavaScript jQuery函数中,我们把返回值设置为了false。