首页 前端知识 HTML5 WebSocket、PHP 和 jQuery 实时 Web 应用(三)

HTML5 WebSocket、PHP 和 jQuery 实时 Web 应用(三)

2024-08-14 00:08:20 前端知识 前端哥 131 659 我要收藏

原文:Realtime Web Apps

协议:CC BY-NC-SA 4.0

九、构建后端:第二部分

在上一章中,您构建了一个可工作的 MVC 框架,并将家庭控制器和视图放在一起。在本章中,您将通过构建QuestionRoom组件来完成应用的后端。本章使用了Model类,以及您在前一章中构建的表单提交处理。

构建问题

Room控制器之前构建Question控制器似乎有些落后,但是因为Room控制器需要来自Question控制器的几个标记片段来输出它的视图,所以让我们从这里开始,然后处理Room控制器。

构建问题控制器

首先,在system/controllers/中创建一个名为class.question.inc.php的新文件,并添加以下代码:

<?php
/**
* Processes output for the Question view
*
* @author Jason Lengstorf <jason@lengstorf.com>
* @author Phil Leggetter <phil@leggetter.co.uk>
*/
class Question extends Controller
{
public $room_id,
$is_presenter = FALSE;
/**
* Initializes the class
*
* @param $options array Options for the controller
* @return void
*/
public function __construct( $options )
{
parent::__construct($options);
$this->room_id = isset($options[0]) ? (int) $options[0] : 0;
if ($this->room_id===0) {
throw new Exception("Invalid room ID supplied");
}
}
/**
* Generates the title of the page
*
* @return string The title of the page
*/
public function get_title( )
{
// Questions can't be called directly, so this is unused
return NULL;
}
/**
* Loads and outputs the view's markup
*
* @return string The HTML markup to display the view
*/
public function output_view( )
{
$questions = $this->get_questions();
$output = NULL;
foreach ($questions as $question) {
/*
* Questions have their own view type, so this section initializes
* and sets up variables for the question view
*/
$view = new View('question');
$view->question = $question->question;
$view->room_id = $this->room_id;
$view->question_id = $question->question_id;
$view->vote_count = $question->vote_count;
if ($question->is_answered==1) {
$view->answered_class = 'answered';
} else {
$view->answered_class = NULL;
}
// TODO: Check if the user has already voted up the question
$view->voted_class = NULL;
// TODO: Load the vote up form for attendees, but not presenters
$view->vote_link = '';
// TODO: Load the answer form for presenters, but not attendees
$view->answer_link = '';
// Returns the output of render() instead of printing it
$output .= $view->render(FALSE);
}
return $output;
}
}
复制

这个方法还没有完成,但是构建模块已经就位,可以开始查看视图将如何形成。

构造函数触发主Controller构造函数,然后检查有效的房间 id,如果在 URI 中没有传递,则抛出一个错误。

因为问题永远不会单独显示——意思是在房间的上下文之外——get_title()方法只返回NULL。记住它不需要声明,因为它是抽象父类的一部分。

output_view()方法使用get_questions()方法加载房间的所有问题,稍后您将对其进行定义。然后它遍历每个问题,加载问题视图并用单个问题的数据填充它。一些变量需要更新;它们中的每一个都被标上了TODO的注释,所以以后回来写这些内容时,很容易就能发现它们。

添加问题视图

问题应用的视图看起来不太像;这只是你在第七章写的 HTML 的一个片段。然而,它有很多变数。

创建一个名为question.inc.php的新文件,并将其存储在system/views/中。在里面,添加以下内容:

<li id="question-<?php echo $question_id; ?>"
data-count="<?php echo $vote_count; ?>"
class="<?php echo $voted_class, ' ', $answered_class; ?>">
<?php echo $answer_link; ?>
<p>
<?php echo $question; ?>
</p>
<?php echo $vote_link; ?>
</li><!--/#question-<?php echo $question_id; ?>-->
复制

这个标记显示了在output_view()中设置的变量,如前所示。目前,这个视图看起来不太像,因为$voted_class$voted_link$answer_link都是NULL或空的。

完成视图

声明的几个变量是NULL或空的,因为检索所需数据的方法还不存在。根据注释中的TODO s,循环还需要:

  • 检查用户是否已经对问题投了赞成票
  • 为与会者而不是演示者加载“向上投票”表单
  • 为演示者而不是与会者加载答案表单

检查用户是否已经对某个问题投了赞成票

为了确定用户是否对某个问题投了赞成票,我们将使用一个简单的 cookie。当用户为一个问题投票时,会存储一个名为voted_for_n(其中 n 是问题的 ID)的 cookie。这将允许应用防止一个用户提交多个投票。

要检查 cookie,将以下粗体代码添加到output_view():

public function output_view( )
{
$questions = $this->get_questions();
$output = NULL;
foreach ($questions as $question) {
/*
* Questions have their own view type, so this section initializes
* and sets up variables for the question view
*/
$view = new View('question');
$view->question = $question->question;
$view->room_id = $this->room_id;
$view->question_id = $question->question_id;
$view->vote_count = $question->vote_count;
if ($question->is_answered==1) {
$view->answered_class = 'answered';
} else {
$view->answered_class = NULL;
}
// Checks if the user has already voted up this question
$cookie = 'voted_for_' . $question->question_id;
if (isset($_COOKIE[$cookie]) && $_COOKIE[$cookie]==1) {
$view->voted_class = 'voted';
} else {
$view->voted_class = NULL;
}
// TODO: Load the vote up form for attendees, but not presenters
$view->vote_link = '';
// TODO: Load the answer form for presenters, but not attendees
$view->answer_link = '';
// Returns the output of render() instead of printing it
$output .= $view->render(FALSE);
}
return $output;
}
复制

这段代码检查一个 cookie,这个 cookie 表示这个问题已经被投票了,如果是的话,设置一个类名来改变样式。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 警告在生产环境中,应用需要为关闭 cookie 的用户添加故障保护,或者在未启用 cookie 的情况下禁止用户使用应用。完全锁定表单提交超出了本书的范围,但是如果你想了解更多,网上有很多很好的资源。

加载向上投票表单

为了向与会者而不是演示者显示向上投票表单,需要创建一个新方法,该方法将有条件地生成一个视图,该视图可用于问题视图中的输出。

system/controllers/class.question.inc.php中,添加以下粗体代码:

public function output_view( )
{
$questions = $this->get_questions();
$output = NULL;
foreach ($questions as $question) {
/*
* Questions have their own view type, so this section initializes
* and sets up variables for the question view
*/
$view = new View('question');
$view->question = $question->question;
$view->room_id = $this->room_id;
$view->question_id = $question->question_id;
$view->vote_count = $question->vote_count;
if ($question->is_answered==1) {
$view->answered_class = 'answered';
} else {
$view->answered_class = NULL;
}
// Checks if the user has already voted up this question
$cookie = 'voted_for_' . $question->question_id;
if (isset($_COOKIE[$cookie]) && $_COOKIE[$cookie]==1) {
$view->voted_class = 'voted';
} else {
$view->voted_class = NULL;
}
$view->vote_link = $this->output_vote_form(
$this->room_id,
$question->question_id,
$question->is_answered
);
// TODO: Load the answer form for presenters, but not attendees
$view->answer_link = '';
// Returns the output of render() instead of printing it
$output .= $view->render(FALSE);
}
return $output;
}
/**
* Generates the voting form for attendees
*
* @param $question_id int The ID of the question
* @param $answered int 1 if answered, 0 if unanswered
* @return mixed Markup if attendee, NULL if presenter
*/
protected function output_vote_form( $room_id, $question_id, $answered )
{
$view = new View('question-vote');
$view->room_id = $room_id;
$view->question_id = $question_id;
$view->form_action = APP_URI . 'question/vote';
$view->nonce = $this->generate_nonce();
$view->disabled = $answered==1 ? 'disabled' : NULL;
return $view->render(FALSE);
}
复制

output_vote_form()方法接受三个参数:当前房间的 ID、当前问题的 ID 以及该问题是否已被回答。

然后它加载一个新的视图—question-vote—并为输出设置变量。房间 ID、问题 ID、表单动作和 nonce 以及$disabled都被存储,如果问题已经被标记为已回答,则防止表单被提交。

这个方法返回它的输出,存储在$vote_link变量中,在这里它成为问题视图的一部分。

添加向上投票表单视图

投票表单的视图非常简单。在system/views/中创建一个名为question-vote.inc.php的新文件,并添加以下标记:

<form method="post" class="vote"
action="<?php echo $form_action; ?>">
<input value="I also have this question."
type="submit"<?php echo $disabled; ?>/>
<input type="hidden" name="question_id"
value="<?php echo $question_id; ?>" />
<input type="hidden" name="room_id"
value="<?php echo $room_id; ?>" />
<input type="hidden" name="nonce"
value="<?php echo $nonce; ?>" />
</form>
复制

该标记使用在output_vote_form()中设置的变量来动态生成按钮,该按钮允许与会者提交对某个问题的投票。

加载答案表单

与 vote up 表单非常相似,您现在需要添加一个方法来加载答案表单,这允许演示者标记一个已回答的问题。将以下代码添加到Question类:

public function output_view( )
{
$questions = $this->get_questions();
$output = NULL;
foreach ($questions as $question) {
/*
* Questions have their own view type, so this section initializes
* and sets up variables for the question view
*/
$view = new View('question');
$view->question = $question->question;
$view->room_id = $this->room_id;
$view->question_id = $question->question_id;
$view->vote_count = $question->vote_count;
if ($question->is_answered==1) {
$view->answered_class = 'answered';
} else {
$view->answered_class = NULL;
}
// Checks if the user has already voted up this question
$cookie = 'voted_for_' . $question->question_id;
if (isset($_COOKIE[$cookie]) && $_COOKIE[$cookie]==1) {
$view->voted_class = 'voted';
} else {
$view->voted_class = NULL;
}
$view->vote_link = $this->output_vote_form(
$this->room_id,
$question->question_id,
$question->is_answered
);
$view->answer_link = $this->output_answer_form(
$this->room_id,
$question->question_id
);
// Returns the output of render() instead of printing it
$output .= $view->render(FALSE);
}
return $output;
}
protected function output_vote_form( $room_id, $question_id, $answered )
{
$view = new View('question-vote');
$view->room_id = $room_id;
$view->question_id = $question_id;
$view->form_action = APP_URI . 'question/vote';
$view->nonce = $this->generate_nonce();
$view->disabled = $answered==1 ? 'disabled' : NULL;
return $view->render(FALSE);
}
/**
* Generates the answering form for presenter
*
* @param $room_id int The ID of the room
* @param $question_id int The ID of the question
* @return mixed Markup if presenter, NULL if attendee
*/
protected function output_answer_form( $room_id, $question_id )
{
$view = new View('question-answer');
$view->room_id = $room_id;
$view->question_id = $question_id;
$view->form_action = APP_URI . 'question/answer';
$view->nonce = $this->generate_nonce();
return $view->render(FALSE);
}
复制

该方法遵循与 output_vote_form() 相同的模式:它使用 question-answer 创建一个新视图,并设置用于生成标记的变量。

添加答案表单视图

创建一个名为question-answer.inc.php的新文件,并将其保存在system/views/中,其中包含以下标记:

<form method="post" class="answer"
action="<?php echo $form_action; ?>">
<input type="submit" value="Answer this question." />
<input type="hidden" name="question_id"
value="<?php echo $question_id; ?>" />
<input type="hidden" name="room_id"
value="<?php echo $room_id; ?>" />
<input type="hidden" name="nonce"
value="<?php echo $nonce; ?>" />
</form>
复制

该标记使用在output_answer_form()中设置的变量来生成标记,供演示者将问题标记为已回答。

为投票和回答的问题添加样式

因为已经被投票或回答的问题不再是交互式的,按钮应该不再显示为可点击的。打开assets/styles/main.css并在媒体查询上方插入以下 CSS:

/* Voted and answered styles
----------------------------------------------------------------------------*/
#questions .voted .vote input {
background-position: left bottom;
width: 78px;
cursor: initial;
}
#questions .answered .answer input:active,
#questions .answered .answer input:hover,
#questions .answered .answer input:focus {
background-position: right top;
cursor: initial;
}
#questions .answered .vote input:active,
#questions .answered .vote input:hover,
#questions .answered .vote input:focus {
background-position: left bottom;
cursor: initial;
}
/* Transition effects
----------------------------------------------------------------------------*/
#questions li,#questions .vote {
-webkit-transition: opacity 1s ease-in-out;
-moz-transition: opacity 1s ease-in-out;
-ms-transition: opacity 1s ease-in-out;
-o-transition: opacity 1s ease-in-out;
transition: opacity 1s ease-in-out;
}
#questions.closed,#questions li.answered { opacity: .4; }
#questions.closed .vote,#questions .answered .vote { opacity: .2; }
复制

这些样式防止按钮在悬停时高亮显示,并防止鼠标光标变成指针,这是元素可点击的标准指示。

当投票或回答问题时,过渡效果可以创建动画淡入淡出效果。在这种情况下,为了触发 CSS 转换,元素需要添加一个类,所以请记住,在下一章实现 realtime 和 jQuery 效果之前,转换是不可见的。

加载房间的所有问题

output_view()方法的最后一部分是目前不存在的方法get_questions()。实际的数据库查询将在本章稍后添加到问题模型中,但是现在,让我们在控制器中声明方法。

将以下粗体代码添加到Question类中:

public function output_ask_form( $is_active, $email )
{
if ($is_active) {
$view = new View('ask-form');
$view->room_id = $this->room_id;
$view->form_action = APP_URI . 'question/ask';
$view->nonce = $this->generate_nonce();
return $view->render(FALSE);
} else {
$view = new View('room-closed');
$view->email = $email;
return $view->render(FALSE);
}
}
/**
* Loads questions for the room
*
* @return array The question data as an array of objects
*/
protected function get_questions( )
{
return $this->model->get_room_questions($this->room_id);
}
复制

这个方法只是从 Question_Model 类中调用一个方法,这个类还没有被定义。一旦建立了模型,这个方法将返回给定房间 ID 的所有问题。

添加提问表单

除了投票和回答表单之外,还有一个表单需要添加到Question类中:用于提出新问题的表单。

添加提问方法

Question类中,使用下面的粗体代码添加新方法:

protected function output_answer_form( $room_id, $question_id )
{
$view = new View('question-answer');
$view->room_id = $room_id;
$view->question_id = $question_id;
$view->form_action = APP_URI . 'question/answer';
$view->nonce = $this->generate_nonce();
return $view->render(FALSE);
}
/**
* Generates the form to ask a new question
*
* @param $is_active bool Whether or not the room is active
* @param $email string The email address of the presenter
* @return string The markup to display the form
*/
public function output_ask_form( $is_active, $email )
{
if ($is_active) {
$view = new View('ask-form');
$view->room_id = $this->room_id;
$view->form_action = APP_URI . 'question/ask';
$view->nonce = $this->generate_nonce();
return $view->render(FALSE);
} else {
$view = new View('room-closed');
$view->email = $email;
return $view->render(FALSE);
}
}
复制

这个方法与其他两个 form 方法非常相似,但是有一个重要的区别:根据房间是否活动,这个方法可以返回两个视图。

ask-form视图输出允许与会者提出新问题的表单。

room-closed视图使用演示者的电子邮件地址,允许任何进入封闭房间的人跟进任何其他问题。

添加提问视图

system/views/中创建一个名为ask-form.inc.php的新文件,并插入以下标记:

<form id="ask-a-question" method="post"
action="<?php echo $form_action; ?>">
<label>
If you have a question and you don't see it below, ask it here.
<input type="text" name="new-question" tabindex="1" />
</label>
<input type="submit" value="Ask" tabindex="2" />
<input type="hidden" name="room_id"
value="<?php echo $room_id; ?>" />
<input type="hidden" name="nonce"
value="<?php echo $nonce; ?>" />
</form><!--/#ask-a-question-->
复制

这种标记创建了询问新问题的形式。

添加房间封闭视图

system/views/中,用以下标记添加一个名为room-closed.inc.php的新文件:

<h3>This session has ended.</h3>
<p>
If you have a question that wasn't answered, please
<a href="mailto:<?php echo $email; ?>">email the presenter</a>.
</p>
复制

这种标记让与会者知道房间关闭了,但提供了一个电子邮件地址,以便与演示者联系,这样他就不会完全不走运。

构建问题模型

为了存储关于问题及其投票的数据,您现在需要创建一个模型类,它将包含所有与问题相关的数据库操作方法。

首先在system/models/中创建一个名为class.question_model.inc.php的新文件,其类定义如下:

<?php
/**
* Creates database interaction methods for questions
*
* @author Jason Lengstorf <jason@lengstorf.com>
* @author Phil Leggetter <phil@leggetter.co.uk>
*/
class Question_Model extends Model
{
}
复制

加载房间的所有问题

为了加载一个房间的所有问题,房间 ID 被传递给get_room_questions()方法。结果作为对象加载,然后传递回控制器进行处理。

为了按逻辑顺序检索问题(即,最高票数、未回答的问题显示在列表顶部),使用LEFT JOIN来利用来自question_votes的投票计数进行排序。

将以下粗体代码添加到Question_Model:

class Question_Model extends Model
{
/**
* Loads all questions for a given room
*
* @param $room_id int The ID of the room
* @return array The questions attached to the room
*/
public function get_room_questions( $room_id )
{
$sql = "SELECT
id AS question_id,
room_id,
question,
is_answered,
vote_count
FROM questions
LEFT JOIN question_votes
ON( questions.id = question_votes.question_id )
WHERE room_id = :room_id
ORDER BY is_answered, vote_count DESC";
$stmt = self::$db->prepare($sql);
$stmt->bindParam(':room_id', $room_id, PDO::PARAM_INT);
$stmt->execute();
$questions = $stmt->fetchAll(PDO::FETCH_OBJ);
$stmt->closeCursor();
return $questions;
}
}
复制

保存新问题

为了将新问题保存到数据库,房间 ID 和新问题文本(作为字符串)都被传递给create_question()方法。第一个查询将问题插入到questions表中,然后将新保存的问题的 ID 存储在$question_id中。

接下来,使用新创建的问题 ID 将问题的第一票(因为提问的用户算作第一票)添加到question_votes表中。

通过将以下粗体代码添加到Question_Model来实现该方法:

public function get_room_questions( $room_id )
{
$sql = "SELECT
id AS question_id,
room_id,
question,
is_answered,
vote_count
FROM questions
LEFT JOIN question_votes
ON( questions.id = question_votes.question_id )
WHERE room_id = :room_id
ORDER BY is_answered, vote_count DESC";
$stmt = self::$db->prepare($sql);
$stmt->bindParam(':room_id', $room_id, PDO::PARAM_INT);
$stmt->execute();
$questions = $stmt->fetchAll(PDO::FETCH_OBJ);
$stmt->closeCursor();
return $questions;
}
/**
* Stores a new question with all the proper associations
*
* @param $room_id int The ID of the room
* @param $question string The question text
* @return array The IDs of the room and the question
*/
public function create_question( $room_id, $question )
{
// Stores the new question in the database
$sql = "INSERT INTO questions (room_id, question)
VALUES (:room_id, :question)";
$stmt = self::$db->prepare($sql);
$stmt->bindParam(':room_id', $room_id);
$stmt->bindParam(':question', $question);
$stmt->execute();
$stmt->closeCursor();
// Stores the ID of the new question
$question_id = self::$db->lastInsertId();
/*
* Because creating a question counts as its first vote, this adds a
* vote for the question to the database
*/
$sql = "INSERT INTO question_votes
VALUES (:question_id, 1)";
$stmt = self::$db->prepare($sql);
$stmt->bindParam(":question_id", $question_id, PDO::PARAM_INT);
$stmt->execute();
$stmt->closeCursor();
return array(
'room_id' => $room_id,
'question_id' => $question_id,
);
}
复制

向问题添加投票

更新投票计数很简单:对于给定的问题 ID,vote_question()方法将投票计数增加1。将这个方法(粗体)添加到Question_Model类:

return array(
'room_id' => $room_id,
'question_id' => $question_id,
);
}
/**
* Increases the vote count of a given question
*
* @param $room_id int The ID of the room
* @param $question_id int The ID of the question
* @return array The IDs of the room and the question
*/
public function vote_question( $room_id, $question_id )
{
// Increments the vote count for the question
$sql = "UPDATE question_votes
SET vote_count = vote_count+1
WHERE question_id = :question_id";
$stmt = self::$db->prepare($sql);
$stmt->bindParam(':question_id', $question_id, PDO::PARAM_INT);
$stmt->execute();
$stmt->closeCursor();
return array(
'room_id' => $room_id,
'question_id' => $question_id,
);
}
}
复制

数据库查询通过在当前值vote_count = vote_count+1上加 1,将具有给定 ID 的问题的投票计数增加 1,然后返回房间和问题 ID。

将问题标记为已回答

最后,为了将问题标记为已回答,具有给定 ID 的问题的is_answered列被更新为1。将以下粗体代码添加到Question_Model :

$stmt->closeCursor();
return array(
'room_id' => $room_id,
'question_id' => $question_id,
);
}
/**
* Marks a given question as answered
*
* @param $room_id int The ID of the room
* @param $question_id int The ID of the question
* @return array The IDs of the room and question
*/
public function answer_question( $room_id, $question_id )
{
$sql = "UPDATE questions
SET is_answered = 1
WHERE id = :question_id";
$stmt = self::$db->prepare($sql);
$stmt->bindParam(':question_id', $question_id, PDO::PARAM_INT);
$stmt->execute();
$stmt->closeCursor();
return array(
'room_id' => $room_id,
'question_id' => $question_id,
);
}
}
复制

向控制器添加表单处理程序和数据访问方法

应用问题部分的最后一点是将动作数组、模型和动作处理程序类添加到Question控制器中。

首先用模型声明和动作数组更新构造函数。将以下内容添加到system/controllers/class.question.inc.php :

public function __construct( $options )
{
parent::__construct($options);
$this->model = new Question_Model;
// Checks for a form submission
$this->actions = array(
'ask' => 'create_question',
'vote' => 'vote_question',
'answer' => 'answer_question',
);
if (array_key_exists($options[0], $this->actions)) {
$this->handle_form_submission($options[0]);
exit;
} else {
$this->room_id = isset($options[0]) ? (int) $options[0] : 0;
if ($this->room_id===0) {
throw new Exception("Invalid room ID supplied");
}
}
}
复制

这将加载用于数据访问的Question_Model类,然后声明三种可能的表单动作及其所需的动作处理程序方法。

保存新问题

保存新问题的操作处理程序相当复杂,因为它是在下一章构建的——在下一章中,我们将开始添加实时功能——考虑在内。因此,它不仅存储新的问题,还生成新的问题视图以供返回,这样以后就不需要在客户端呈现标记了。它还为用户添加了一个 cookie,表明他们已经为这个问题投了票。

将下面的粗体代码添加到Question类中:

protected function get_questions( )
{
return $this->model->get_room_questions($this->room_id);
}
/**
* Adds a new question to the database
*
* @return array Information about the updated question
*/
protected function create_question( )
{
$room_id = $this->sanitize($_POST['room_id']);
$question = $this->sanitize($_POST['new-question']);
$output = $this->model->create_question($room_id, $question);
// Make sure valid output was returned
if (is_array($output) && isset($output['question_id'])) {
$room_id = $output['room_id'];
$question_id = $output['question_id'];
// Generates markup for the question (for realtime addition)
$view = new View('question');
$view->question = $question;
$view->room_id = $room_id;
$view->question_id = $question_id;
$view->vote_count = 1;
$view->answered_class = NULL;
$view->voted_class = NULL;
$view->vote_link = $this->output_vote_form(
$room_id,
$question_id,
FALSE
);
$view->answer_link = $this->output_answer_form(
$room_id,
$question_id
);
$output['markup'] = $view->render(FALSE);
} else {
throw new Exception('Error creating the room.');
}
// Stores a cookie so the attendee can only vote once
setcookie('voted_for_' . $question_id, 1, time() + 2592000, '/');
return $output;
}
}
复制

该方法首先净化提交的数据,使用模型的create_question()方法将其存储在数据库中,并检查有效的返回值。然后,它创建一个新的question视图,并存储所有变量,为一个新问题生成标记。存储了一个 cookie,表明与会者发布了问题的第一个向上投票;然后返回标记。

向问题添加投票

向一个问题添加新的投票会执行vote_question()方法,将新的投票存储在数据库中,然后为投票者设置一个 cookie,以防止对同一个问题进行多次投票。将以下粗体代码添加到Question控制器中:

// Stores a cookie so the attendee can only vote once
setcookie('voted_for_' . $question_id, 1, time() + 2592000, '/');
return $output;
}
/**
* Increments the vote count for a given question
*
* @return array Information about the updated question
*/
protected function vote_question( )
{
$room_id = $this->sanitize($_POST['room_id']);
$question_id = $this->sanitize($_POST['question_id']);
// Makes sure the attendee hasn't already voted for this question
$cookie_id = 'voted_for_' . $question_id;
if (!isset($_COOKIE[$cookie_id]) || $_COOKIE[$cookie_id]!=1) {
$output = $this->model->vote_question($room_id, $question_id);
// Sets a cookie to make it harder to post multiple votes
setcookie($cookie_id, 1, time() + 2592000, '/');
} else {
$output = array('room_id'=>$room_id);
}
return $output;
}
}
复制

将问题标记为已回答

为了将问题标记为已回答,提交表单的用户必须是演示者。这和投票一样,是基于 cookie 的。该方法在执行answer_question()方法之前检查演示者的 cookie。

将以下粗体显示的代码添加到Question控制器中:

// Stores a cookie so the attendee can only vote once
setcookie('voted_for_' . $question_id, 1, time() + 2592000, '/');
return $output;
}
/**
* Marks a given question as answered
*
* @return array Information about the updated question
*/
protected function answer_question( )
{
$room_id = $this->sanitize($_POST['room_id']);
$question_id = $this->sanitize($_POST['question_id']);
// Makes sure the person answering the question is the presenter
$cookie_id = 'presenter_room_' . $room_id;
if (isset($_COOKIE[$cookie_id]) && $_COOKIE[$cookie_id]==1) {
return $this->model->answer_question($room_id, $question_id);
}
return array('room_id'=>$room_id);
}
}
复制

该方法清理提交的表单值,然后检查演示者的 cookie,以验证当前用户是否有权将问题标记为已回答。如果 cookie 有效,模型的answer_question()方法被触发,其返回的数据被传递;无效或缺失的 cookie 只会将用户返回到房间,而不会通过返回房间 id 进行任何处理。

建造房间

这个应用的最后一部分是为房间添加控制器、模型和视图。这与问题的功能非常相似,除了它实际上加载了Question控制器来加载那些视图并利用它的方法。

添加房间控制器

第一步是创建Room控制器。在system/controllers/中,添加一个名为class.room.inc.php的新文件,并从以下代码开始:

<?php
/**
* Processes output for the Room view
*
* @author Jason Lengstorf <jason@lengstorf.com>
* @author Phil Leggetter <phil@leggetter.co.uk>
*/
class Room extends Controller
{
public $room_id,
$is_presenter,
$is_active;
/**
* Initializes the view
*
* @param $options array Options for the view
* @return void
*/
public function __construct( $options )
{
parent::__construct($options);
$this->model = new Room_Model;
$this->room_id = isset($options[0]) ? (int) $options[0] : 0;
if ($this->room_id===0) {
throw new Exception("Invalid room ID supplied");
}
$this->room = $this->model->get_room_data($this->room_id);
$this->is_presenter = $this->is_presenter();
$this->is_active = (boolean) $this->room->is_active;
}
/**
* Generates the title of the page
*
* @return string The title of the page
*/
public function get_title( )
{
return $this->room->room_name . ' by ' . $this->room->presenter_name;
}
/**
* Loads and outputs the view's markup
*
* @return void
*/
public function output_view( )
{
$view = new View('room');
$view->room_id = $this->room->room_id;
$view->room_name = $this->room->room_name;
$view->presenter = $this->room->presenter_name;
$view->email = $this->room->email;
if (!$this->is_presenter) {
$view->ask_form = $this->output_ask_form();
$view->questions_class = NULL;
} else {
$view->ask_form = NULL;
$view->questions_class = 'presenter';
}
if (!$this->is_active) {
$view->questions_class = 'closed';
}
$view->controls = $this->output_presenter_controls();
$view->questions = $this->output_questions();
$view->render();
}
}
复制

除了标准的构造函数——调用父构造函数,设置模型,并确保提供了有效的选项—Room构造函数还设置了一些特定于房间的属性。

$room属性将保存房间的基本信息,这些信息作为对象从get_room_data()方法返回。该模型将在本章后面的内容中进行设置以检索这些数据。

为了处理演示者和与会者在房间标记上的差异,$is_presenter保存了一个由is_presenter()方法确定的布尔值,稍后将编写这个方法。

最后,$is_active属性作为房间活动状态的快捷方式。因为它在数据库中存储为10,所以它被转换为布尔值,以允许在控制器的方法中进行严格的布尔比较。

get_title()方法使用房间和演示者的名字为房间生成一个有意义的标题。

output_view()中,加载了房间视图,并设置了一些变量,这将在本章的稍后部分进行介绍。

确定用户是否是演示者

因为向演示者显示的标记不同于向与会者显示的标记,所以应用需要一种方法来确定用户是否是当前房间的演示者。这将被存储为一个 cookie。

将以下粗体代码添加到Room控制器中:

$view->controls = $this->output_presenter_controls();
$view->questions = $this->output_questions();
$view->render();
}
/**
* Determines whether or not the current user is the presenter
*
* @return boolean TRUE if it's the presenter, otherwise FALSE
*/
protected function is_presenter( )
{
$cookie = 'presenter_room_' . $this->room->room_id;
return (isset($_COOKIE[$cookie]) && $_COOKIE[$cookie]==1);
}
}
复制

该方法使用当前房间的 ID 计算出 cookie 名称,然后如果 cookie 设置为等于1,则返回TRUE

添加房间视图

使用第七章的中的房间标记,通过将以下代码添加到位于system/views/room.inc.php : 的新文件中来创建房间视图

<section>
<header>
<h2><?php echo $room_name; ?></h2>
<p>
Presented by<?php echo $presenter; ?>
(<a href="mailto:<?php echo $email; ?>">email</a>)
</p>
<?php echo $controls; ?>
</header>
<?php echo $ask_form; ?>
<ul id="questions" class="<?php echo $questions_class; ?>">
<?php echo $questions; ?>
</ul><!--/#questions-->
</section>
复制

该标记生成向与会者和演示者显示房间及其问题所需的一切。

显示 Ask 表单

ask 表单需要一个新视图,该视图将由Room类中的一个新方法加载。如果您还记得,ask 表单的生成已经由Question控制器处理了,所以新方法将简单地调用Question控制器上的output_ask_form()方法。

将以下粗体代码添加到Room控制器中:

$view->controls = $this->output_presenter_controls();
$view->questions = $this->output_questions();
$view->render();
}
/**
* Shows the "ask a question" form or a notice that the room has ended
*
* @param $email string The presenter's email address
* @return string Markup for the form or notice
*/
protected function output_ask_form( )
{
$controller = new Question(array($this->room_id));
return $controller->output_ask_form(
$this->is_active,
$this->room->email
);
}
/**
* Determines whether or not the current user is the presenter
*
* @return boolean TRUE if it's the presenter, otherwise FALSE
*/
protected function is_presenter( )
{
复制

显示演讲者控制

对于房间的展示者,我们需要提供房间的直接链接和关闭房间的选项(如果房间已经关闭,则重新打开房间)。为此,向Room控制器添加一个新方法,检查用户是否是演示者;然后检查该房间是否是活动的。

对于非活动房间,重新打开控件将被加载并返回,以便在主房间视图中使用。

活动房间加载标准控件并返回它们。

将以下粗体代码添加到class.room.inc.php:

return $controller->output_ask_form(
$this->is_active,
$this->room->email
);
}
/**
* Shows the presenter his controls (or nothing, if not the presenter)
*
* @return mixed Markup for the controls (or NULL)
*/
protected function output_presenter_controls( )
{
if ($this->is_presenter) {
if (!$this->is_active) {
$view_class = 'presenter-reopen';
$form_action = APP_URI . 'room/open';
} else {
$view_class = 'presenter-controls';
$form_action = APP_URI . 'room/close';
}
$view = new View($view_class);
$view->room_id = $this->room->room_id;
$view->room_uri = APP_URI . 'room/' . $this->room_id;
$view->form_action = $form_action;
$view->nonce = $this->generate_nonce();
return $view->render(FALSE);
}
return NULL;
}
/**
* Determines whether or not the current user is the presenter
*
* @return boolean TRUE if it's the presenter, otherwise FALSE
*/
protected function is_presenter( )
{
复制

添加不活跃的会议室演示者控制视图

非活动房间的视图是一个简单的表单,输入内容为“打开该房间”。

使用以下标记在system/views/中创建一个名为presenter-reopen.inc.php的文件:

<form id="close-this-room" method="post"
action="<?php echo $form_action; ?>">
<input type="submit" value="Open This Room" />
<input type="hidden" name="room_id"
value="<?php echo $room_id; ?>" />
<input type="hidden" name="nonce"
value="<?php echo $nonce; ?>" />
</form><!--/#close-this-room-->
复制

添加活跃的会议室演示者控制视图

对于活动房间,显示带有房间 URI 的禁用输入,以及显示“关闭此房间”的按钮。

将以下标记添加到system/views/中名为presenter-controls.inc.php的新文件中:

<form id="close-this-room" method="post"
action="<?php echo $form_action; ?>">
<label>
Link to your room.
<input type="text" name="room-uri"
value="<?php echo $room_uri; ?>"
disabled />
</label>
<input type="submit" value="Close This Room" />
<input type="hidden" name="room_id"
value="<?php echo $room_id; ?>" />
<input type="hidden" name="nonce"
value="<?php echo $nonce; ?>" />
</form><!--/#close-this-room-->
复制

展示问题

为了显示房间的问题,output_questions()方法将利用Question控制器遍历现有的问题并返回标记。

在生成标记之前,它设置了$is_presenter属性,以便为用户类型返回正确的标记。

将以下粗体代码添加到Room控制器中:

return $view->render(FALSE);
}
return NULL;
}
/**
* Loads and formats the questions for this room
*
* @return string The marked up questions
*/
protected function output_questions( )
{
$controller = new Question(array($this->room_id));
// Allows for different output for presenters vs. attendees
$controller->is_presenter = $this->is_presenter;
return $controller->output_view();
}
/**
* Determines whether or not the current user is the presenter
*
* @return boolean TRUE if it's the presenter, otherwise FALSE
*/
protected function is_presenter( )
{
复制

建立房间模型

因为应用需要存储和操作房间数据,所以您需要为房间创建一个模型,名为Room_Model。这将存储在名为class.room_model.inc.php的文件的system/models/子目录中。

创建文件,并以这个基本的类定义开始:

<?php
/**
* Creates database interaction methods for rooms
*
* @author Jason Lengstorf <jason@lengstorf.com>
* @author Phil Leggetter <phil@leggetter.co.uk>
*/
class Room_Model extends Model
{
}
复制

创建房间

模型的第一种方法是将新房间保存到数据库中。

这是一个多步骤的过程;该方法需要做以下工作:

  • rooms表格中创建新房间
  • 检索新房间的 ID
  • 将演示者添加到presenters表中(或者在电子邮件重复的情况下更新演示者的显示名称)
  • presenters表中检索演示者的 ID
  • 将房间 ID 映射到room_owners表中演示者的 ID。

将以下粗体代码添加到房间模型中:

class Room_Model extends Model
{
/**
* Saves a new room to the database
*
* @param $presenter string The name of the presenter
* @param $email string The presenter's email address
* @param $name string The name of the room
* @return array An array of data about the room
*/
public function create_room( $presenter, $email, $name )
{
// Creates a new room
$sql = 'INSERT INTO rooms (name) VALUES (:name)';
$stmt = self::$db->prepare($sql);
$stmt->bindParam(':name', $name, PDO::PARAM_STR, 255);
$stmt->execute();
$stmt->closeCursor();
// Gets the generated room ID
$room_id = self::$db->lastInsertId();
// Creates (or updates) the presenter
$sql = "INSERT INTO presenters (name, email)
VALUES (:name, :email)
ON DUPLICATE KEY UPDATE name=:name";
$stmt = self::$db->prepare($sql);
$stmt->bindParam(':name', $presenter, PDO::PARAM_STR, 255);
$stmt->bindParam(':email', $email, PDO::PARAM_STR, 255);
$stmt->execute();
$stmt->closeCursor();
// Gets the generated presenter ID
$sql = "SELECT id
FROM presenters
WHERE email=:email";
$stmt = self::$db->prepare($sql);
$stmt->bindParam(':email', $email, PDO::PARAM_STR, 255);
$stmt->execute();
$pres_id = $stmt->fetch(PDO::FETCH_OBJ)->id;
$stmt->closeCursor();
// Stores the room:presenter relationship
$sql = 'INSERT INTO room_owners (room_id, presenter_id)
VALUES (:room_id, :pres_id)';
$stmt = self::$db->prepare($sql);
$stmt->bindParam(":room_id", $room_id, PDO::PARAM_INT);
$stmt->bindParam(":pres_id", $pres_id, PDO::PARAM_INT);
$stmt->execute();
$stmt->closeCursor();
return array(
'room_id' => $room_id,
);
}
}
复制

检查房间是否存在

作为加入房间过程的一部分,Room控制器需要能够验证房间的存在。这个方法简单地选择与给定 ID 匹配的rooms表中的COUNT()个房间;计数1表示房间存在,0表示不存在。

将以下粗体代码添加到Room_Model:

// Stores the room:presenter relationship
$sql = 'INSERT INTO room_owners (room_id, presenter_id)
VALUES (:room_id, :pres_id)';
$stmt = self::$db->prepare($sql);
$stmt->bindParam(":room_id", $room_id, PDO::PARAM_INT);
$stmt->bindParam(":pres_id", $pres_id, PDO::PARAM_INT);
$stmt->execute();
$stmt->closeCursor();
return array(
'room_id' => $room_id,
);
}
/**
* Checks if a given room exists
*
* @param $room_id int The ID of the room being checked
* @return bool Whether or not the room exists
*/
public function room_exists( $room_id )
{
// Loads the number of rooms matching the provided room ID
$sql = "SELECT COUNT(id) AS the_count FROM rooms WHERE id = :room_id";
$stmt = self::$db->prepare($sql);
$stmt->bindParam(':room_id', $room_id, PDO::PARAM_INT);
$stmt->execute();
$room_exists = (bool) $stmt->fetch(PDO::FETCH_OBJ)->the_count;
$stmt->closeCursor();
return $room_exists;
}
}
复制

使用(bool)将计数显式转换为布尔值意味着该方法将总是返回TRUEFALSE,这对于唯一目的是检查某个东西是否存在的方法很有帮助。

开房

对于一个已经关闭的房间,重新打开它非常简单,只需将具有给定 ID 的房间的rooms表中的is_active列设置为1

将粗体显示的代码添加到class.room_model.inc.php :

$room_exists = (bool) $stmt->fetch(PDO::FETCH_OBJ)->the_count;
$stmt->closeCursor();
return $room_exists;
}
/**
* Sets a given room's status to "open"
*
* @param $room_id int The ID of the room being checked
* @return array An array of data about the room
*/
public function open_room( $room_id )
{
$sql = "UPDATE rooms SET is_active=1 WHERE id = :room_id";
$stmt = self::$db->prepare($sql);
$stmt->bindParam(':room_id', $room_id, PDO::PARAM_INT);
$stmt->execute();
$stmt->closeCursor();
return array(
'room_id' => $room_id,
);
}
}
复制

关闭房间

关闭一个房间与打开一个房间的过程相同,只是在rooms表中is_active列被设置为0。将以下粗体代码插入Room_Model :

public function open_room( $room_id )
{
$sql = "UPDATE rooms SET is_active=1 WHERE id = :room_id";
$stmt = self::$db->prepare($sql);
$stmt->bindParam(':room_id', $room_id, PDO::PARAM_INT);
$stmt->execute();
$stmt->closeCursor();
return array(
'room_id' => $room_id,
);
}
/**
* Sets a given room's status to "closed"
*
* @param $room_id int The ID of the room being checked
* @return array An array of data about the room
*/
public function close_room( $room_id )
{
$sql = "UPDATE rooms SET is_active=0 WHERE id = :room_id";
$stmt = self::$db->prepare($sql);
$stmt->bindParam(':room_id', $room_id, PDO::PARAM_INT);
$stmt->execute();
$stmt->closeCursor();
return array(
'room_id' => $room_id,
);
}
}
复制

装货间信息

加载房间信息可以说是应用中最复杂的查询。它要求如下:

  • room_owners工作台连接到rooms工作台;然后将presenters表连接到该表,形成一个完整的数据集
  • rooms表中加载idnameis_active
  • id更名为room_id,将name更名为room_name
  • 从演示者表格中加载idnameemail
  • id更名为presenter_id,将name更名为presenter_name

使用添加到class.room_model.inc.php中的以下粗体代码所示的查询完成上述步骤:

public function close_room( $room_id )
{
$sql = "UPDATE rooms SET is_active=0 WHERE id = :room_id";
$stmt = self::$db->prepare($sql);
$stmt->bindParam(':room_id', $room_id, PDO::PARAM_INT);
$stmt->execute();
$stmt->closeCursor();
return array(
'room_id' => $room_id,
);
}
/**
* Retrieves details about a given room
*
* @param $room_id int The ID of the room being checked
* @return array An array of data about the room
*/
public function get_room_data( $room_id )
{
$sql = "SELECT
rooms.id AS room_id,
presenters.id AS presenter_id,
rooms.name AS room_name,
presenters.name AS presenter_name,
email, is_active
FROM rooms
LEFT JOIN room_owners
ON( rooms.id = room_owners.room_id )
LEFT JOIN presenters
ON( room_owners.presenter_id = presenters.id )
WHERE rooms.id = :room_id
LIMIT 1";
$stmt = self::$db->prepare($sql);
$stmt->bindParam(':room_id', $room_id, PDO::PARAM_INT);
$stmt->execute();
$room_data = $stmt->fetch(PDO::FETCH_OBJ);
$stmt->closeCursor();
return $room_data;
}
}
复制

向房间控制器添加表单处理程序

应用后端的最后一步是将表单动作和动作处理程序添加到Room控制器中。

将表单动作添加到房间控制器

Room控制器支持四种动作:

  • 加入房间
  • 创建房间
  • 开房
  • 关闭房间

将以下粗体代码添加到Room类的构造函数中,以添加对这四个动作的支持:

public function __construct( $options )
{
parent::__construct($options);
$this->model = new Room_Model;
// Checks for a form submission
$this->actions = array(
'join' => 'join_room',
'create' => 'create_room',
'open' => 'open_room',
'close' => 'close_room',
);
if (array_key_exists($options[0], $this->actions)) {
$this->handle_form_submission($options[0]);
exit;
} else {
$this->room_id = isset($options[0]) ? (int) $options[0] : 0;
if ($this->room_id===0) {
throw new Exception("Invalid room ID supplied");
}
}
$this->room = $this->model->get_room_data($this->room_id);
$this->is_presenter = $this->is_presenter();
$this->is_active = (boolean) $this->room->is_active;
}
复制

该数组为应该调用的方法创建一个操作映射。接下来,if...else语句检查一个有效的动作,如果传递了一个动作,它就调用handle_form_submission()来处理它。

加入房间

当用户试图加入一个房间时,控制器需要首先使用来自Room_Modelroom_exists()方法检查它是否存在。如果是,用户应该被重定向到所请求的房间;否则,他们应该会收到一条错误消息。

将以下粗体代码添加到Room控制器中:

protected function output_questions( )
{
$controller = new Question(array($this->room_id));
// Allows for different output for presenters vs. attendees
$controller->is_presenter = $this->is_presenter;
return $controller->output_view();
}
/**
* Checks if a room exists and redirects the user appropriately
*
* @return void
*/
protected function join_room( )
{
$room_id = $this->sanitize($_POST['room_id']);
// If the room exists, creates the URL; otherwise, sends to a 404
if ($this->model->room_exists($room_id)) {
$header = APP_URI . 'room/' . $room_id;
} else {
$header = APP_URI . 'no-room';
}
header("Location: " . $header);
exit;
}
/**
* Determines whether or not the current user is the presenter
*
* @return boolean TRUE if it's the presenter, otherwise FALSE
*/
protected function is_presenter( )
{
复制

创建新房间

要创建一个新房间,控制器需要整理演示者的姓名、电子邮件和房间名称;使用房间模型的create_room()方法存储它们;然后检查房间是否创建成功。它还应该设置一个 cookie,将房间的创建者标识为演示者。

class.room.inc.php中,添加以下粗体代码:

if ($this->model->room_exists($room_id)) {
$header = APP_URI . 'room/' . $room_id;
} else {
$header = APP_URI . 'no-room';
}
header("Location: " . $header);
exit;
}
/**
* Creates a new room and sets the creator as the presenter
*
* @return array Information about the updated room
*/
protected function create_room( )
{
$presenter = $this->sanitize($_POST['presenter-name']);
$email = $this->sanitize($_POST['presenter-email']);
$name = $this->sanitize($_POST['session-name']);
// Store the new room and its various associations in the database
$output = $this->model->create_room($presenter, $email, $name);
// Make sure valid output was returned
if (is_array($output) && isset($output['room_id'])) {
$room_id = $output['room_id'];
} else {
throw new Exception('Error creating the room.');
}
// Makes the creator of this room its presenter
setcookie('presenter_room_' . $room_id, 1, time() + 2592000, '/');
return $output;
}
/**
* Determines whether or not the current user is the presenter
*
* @return boolean TRUE if it's the presenter, otherwise FALSE
*/
protected function is_presenter( )
{
复制

重新打开一个封闭的房间

为了重新打开一个已经关闭的房间,清理后的房间 ID 被传递给模型的open_room()方法。

// Makes the creator of this room its presenter
setcookie('presenter_room_' . $room_id, 1, time() + 2592000, '/');
return $output;
}
/**
* Marks a given room as active
*
* @return array Information about the updated room
*/
protected function open_room( )
{
$room_id = $this->sanitize($_POST['room_id']);
return $this->model->open_room($room_id);
}
/**
* Determines whether or not the current user is the presenter
*
* @return boolean TRUE if it's the presenter, otherwise FALSE
*/
protected function is_presenter( )
{
复制

关闭房间

关闭一个房间和打开一个房间几乎是一样的,只是调用了close_room()方法。

protected function open_room( )
{
$room_id = $this->sanitize($_POST['room_id']);
return $this->model->open_room($room_id);
}
/**
* Marks a given room as closed
*
* @return array Information about the updated room
*/
protected function close_room( )
{
$room_id = $this->sanitize($_POST['room_id']);
return $this->model->close_room($room_id);
}
/**
* Determines whether or not the current user is the presenter
*
* @return boolean TRUE if it's the presenter, otherwise FALSE
*/
protected function is_presenter( )
{
复制

全部测试完毕

此时,您的应用代码已经完成,可以进行测试了。让我们运行每一个可用的行动,以验证一切都在按计划进行。

创建您的第一个房间

首先,在您选择的浏览器中加载应用,并输入新房间的详细信息(参见图 9-1 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-1。在应用的主页上创建新房间

点击创建你的房间,你将被带到新的空房间(见图 9-2 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-2。新创建的房间

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意在一些服务器配置中,运行PDO::__construct()时可能会出现 MySQL 错误。这通常意味着您需要将您的php.ini文件指向mysql.sock的正确位置。在https://gist.github.com/jlengstorf/5184301有一个简单的演练,如果您在 Google 上搜索错误消息,还有几个可用的演练。

关闭房间

关闭房间,测试演示者控制是否正常工作。点击关闭此房间按钮,房间变为非活动状态(参见图 9-3 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-3。封闭的房间

重新开放房间

确保房间可以通过点击打开此房间按钮重新打开,这将使房间回到其最初的活动状态(参见图 9-4 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-4。重新开放的房间

加入房间

接下来,打开不同的浏览器(这意味着完全不同的应用:Firefox、Safari、Opera 或 Internet Explorer,如果你开始使用 Google Chrome 的话)并导航到http://rwa.local/

根据本节中的数字,输入 1 作为房间的 ID,并点击加入此房间按钮(参见图 9-5 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-5。从主页加入房间(使用不同的浏览器)

房间打开,您现在可以看到“提问”表单(参见图 9-6 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-6。您作为与会者查看房间时,会看到“提问”表单

问你的第一个问题

通过在表格字段中键入新问题,测试与会者是否可以提出新问题(参见图 9-7 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-7。通过“提问”表单提出新问题

点击提问按钮后,新问题被创建并显示为已被您投票通过(见图 9-8 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-8。问题被创建;创造者的投票已经被计算了

投票表决这个问题

为了测试对问题的投票,您需要打开第三个浏览器,这样您就可以作为尚未对新问题投票的与会者加入房间(参见图 9-9 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-9。未投票的与会者看到的问题

点击投票按钮,将投票数增加 1(见图 9-10 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-10。收到第二次投票后的问题

回答问题

回到第一个浏览器——您的用户是演示者的浏览器——重新加载以查看新问题(参见图 9-11 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-11。演示者仪表盘上的问题,完成计票

现在点击回答按钮,将问题标记为已回答(参见图 9-12 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-12。演示者眼中的已回答问题

通过检查其他两个浏览器中的任何一个,验证这也能正确显示给与会者(参见图 9-13 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-13。与会者眼中的已回答问题

摘要

从这一章到上一章,你已经在最后 100 页左右的篇幅里涉及了很多内容。您现在应该有一个功能齐全的 MVC 应用,允许创建、关闭和重新打开房间;以及提问、投票和回答问题。

在下一章中,您将向应用添加实时事件通知,并实现 JavaScript 效果来动画显示这些事件。

十、实现实时事件和 jQuery 效果

至此,你有了一个全功能的 app。然而,为了让这个特殊的应用有用,它需要实现实时功能,这样它的用户就不会被迫不断地重新加载以获取新数据。

在这一章中,你将把应用挂接到 Pusher 上,并在后端添加代码来创建实时事件。您还将使用 Pusher 的 JavaScript 应用编程接口(API)来订阅这些事件,并使用 jQuery 来制作应用的动画,这样就可以在屏幕上以一种和谐的方式操作新数据。

哪些事件需要实时增强?

  • 关闭房间
  • 开房
  • 问问题
  • 投票表决一个问题
  • 回答问题

添加所需的凭证和库

在开始添加 realtime 之前,您需要确保手头上有所有适当的凭据和库来配置应用。

获取您的 Pusher API 证书

Pusher API 要求您的应用使用应用密钥、应用密码和应用 ID 进行身份验证。

要获取它们,请在http://pusher.com登录您的帐户,并从您的仪表板左上角选择“添加新应用”。将你的新应用命名为“实时网络应用”,不要勾选下面的两个框(见图 10-1 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-1。在 Pusher 中创建新应用

点击“创建应用”,然后在下一个屏幕上点击“API 访问”,调出您的 API 凭证(见图 10-2 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-2。在 Pusher 仪表盘上查看应用的 API 凭证

将推送器 API 凭证添加到配置文件

现在您已经有了 API 凭证,它们需要包含在应用中。为此,向system/config/config.inc.php添加三个新的常量——以粗体显示:

//-----------------------------------------------------------------------------
// Database credentials
//-----------------------------------------------------------------------------
$_C['DB_HOST'] = 'localhost';
$_C['DB_NAME'] = 'rwa_app';
$_C['DB_USER'] = 'root';
$_C['DB_PASS'] = '';
//-----------------------------------------------------------------------------
// Pusher credentials
//-----------------------------------------------------------------------------
$_C['PUSHER_KEY'] = '9570a71016cf9861a52b';
$_C['PUSHER_SECRET'] = '65cc09ede8e2c18701cc';
$_C['PUSHER_APPID'] = '37506';
//-----------------------------------------------------------------------------
// Enable debug mode (strict error reporting)
//-----------------------------------------------------------------------------
$_C['DEBUG'] = TRUE;
复制

为 Pusher 下载 PHP API 包装

对于应用的后端部分,我们需要使用 API 包装器来使访问 Pusher 变得轻而易举。

https://github.com/pusher/pusher-php-server下载 API 包装。ZIP 将包含几个文件和目录,但你需要抓取的唯一一个是在lib/Pusher.php,你现在应该在你的应用中复制到system/lib/

包括应用中的 PHP API 包装器

既然 Pusher API 包装器已经在应用的目录结构中,就需要包含它以供使用。在index.php中,将以下粗体代码添加到初始化块中:

// Starts the session
if (!isset($_SESSION)) {
session_start();
}
// Loads the configuration variables
require_once SYS_PATH . '/config/config.inc.php';
// Loads Pusher
require_once SYS_PATH . '/lib/Pusher.php';
// Turns on error reporting if in debug mode
if (DEBUG===TRUE) {
ini_set('display_errors', 1);
error_reporting(E_ALL^E_STRICT);
} else {
ini_set('display_errors', 0);
error_reporting(0);
}
复制

加载 Pusher 的 JavaScript API 包装器

对于实时实现的前端部分,应用需要包含 Pusher 的 JavaScript API 包装器。在system/inc/footer.inc.php中,添加以下粗体代码:

</ul>
</footer>
<script src="[`js.pusher.com/1.12/pusher.min.js"></script`](http://js.pusher.com/1.12/pusher.min.js"></script)>
</body>
</html>
复制

正在加载 jQuery

为了获得效果,您的应用将需要 jQuery 库。将其添加到页脚中的 Pusher JS 之后:

</ul>
</footer>
<script src="http://js.pusher.com/1.12/pusher.min.js"></script>
<script src="[`code.jquery.com/jquery-1.8.2.min.js"></script`](http://code.jquery.com/jquery-1.8.2.min.js"></script)>
</body>
</html>
复制

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意该应用中的代码只经过了 jQuery 1 . 8 . 2 版本的测试。较新的版本可能会带来一些问题,只有经过全面测试后才能使用。

在后端实现实时

为了让球滚动起来,需要在应用的后端创建和触发事件。

创建事件

由于应用处理表单提交的方式——所有表单都通过一个表单处理程序方法传递——发送实时事件通知只需要几行代码,这些代码将被添加到抽象Controller类中的handle_form_submission()方法中。

打开system/core/class.controller.inc.php并插入以下粗体代码:

protected function handle_form_submission( $action )
{
if ($this->check_nonce()) {
// Calls the method specified by the action
$output = $this->{$this->actions[$action]}();
if (is_array($output) && isset($output['room_id'])) {
$room_id = $output['room_id'];
} else {
throw new Exception('Form submission failed.');
}
// Realtime stuff happens here
$pusher = new Pusher(PUSHER_KEY, PUSHER_SECRET, PUSHER_APPID);
$channel = 'room_' . $room_id;
$pusher->trigger($channel, $action, $output);
header('Location: ' . APP_URI . 'room/' . $room_id);
exit;
} else {
throw new Exception('Invalid nonce.');
}
}
复制

创建一个新的Pusher对象并存储在$pusher变量中,然后使用其 ID 创建房间的通道。使用动作名作为事件名,使用发送输出数组供客户端使用的trigger()方法在房间的通道上触发一个新事件。

测试实时事件

为了确保你的实时事件在后端被触发,回到你的 Pusher 仪表盘,打开你的应用的调试控制台。打开此页面,在新标签或浏览器中导航至http://rwa.local,然后创建一个新房间。

创建房间后,查看调试控制台,您会看到类似于图 10-3 的内容。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-3。推进器调试控制台实时显示房间的创建

接下来,关闭房间;然后重新打开它。再次检查控制台,您将看到closeopen事件被触发(参见图 10-4 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-4。Pusher 控制台显示关闭和重新打开房间的事件

在第二个浏览器中,加入房间(本例中为 ID 5)并提出问题。新的连接以及触发的ask事件显示在控制台上(见图 10-5 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-5。控制台显示新的问题标记,由 ask 事件发送

在第三个浏览器中,对问题进行投票,以查看触发的投票事件(参见图 10-6 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-6。控制台显示发生的投票事件

最后,返回到创建房间的浏览器,将问题标记为已回答。这触发了控制台中的answer事件(参见图 10-7 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-7。控制台中的回答事件

在前端实现实时

现在,应用的后端正在触发事件,前端需要监听它们。

订阅频道

第一步是创建一个 JavaScript Pusher对象,并用它来订阅房间的频道。

确定通道名称

在订阅频道之前,您首先需要创建一个新的模板变量来保存正确的频道名称。在index.php中,添加以下粗体代码以生成通道名称:

require_once SYS_PATH . '/inc/header.inc.php';
$controller->output_view();
// Configures the Pusher channel if we're in a room
$channel = !empty($uri_array[0]) ? 'room_' . $uri_array[0] : 'default';
require_once SYS_PATH . '/inc/footer.inc.php';
复制

添加频道订阅 JavaScript

现在频道名称已经确定,创建一个新的Pusher对象,并通过在system/inc/footer.inc.php中添加以下粗体代码来订阅该频道:

</footer>
<script src="http://js.pusher.com/1.12/pusher.min.js"></script>
<script src="http://code.jquery.com/jquery-1.8.2.min.js"></script>
<script>
var pusher = new Pusher('<?php echo PUSHER_KEY; ?>'),
channel = pusher.subscribe('<?php echo $channel; ?>');
</script>
</body>
</html>
复制

绑定到事件

该应用现在订阅了一个频道,但此时它仍需要监听各个事件。

创建一个初始化 JavaScript 文件

为了保持页脚的整洁,在assets/scripts/中创建一个名为init.js的新文件,并用下面的代码初始化它:

/**
* Initialization script for Realtime Web Apps
*/
(function($) {
})(jQuery);
复制

这个文件将包含应用的 JavaScript 的其余部分。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 提示用闭包[脚注]包装你的应用脚本可以防止与其他使用美元符号($)快捷方式的库发生冲突,比如 Prototype 和 MooTools。

通过将以下粗体代码插入页脚,将此文件加载到您的应用中:

<script src="http://js.pusher.com/1.12/pusher.min.js"></script>
<script src="http://code.jquery.com/jquery-1.8.2.min.js"></script>
<script>
var pusher = new Pusher('<?php echo PUSHER_KEY; ?>'),
channel = pusher.subscribe('<?php echo $channel; ?>');
</script>
<script src="<?php echo APP_URI; ?>assets/scripts/init.js"></script>
</body>
</html>
复制

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意Pusher对象初始化和通道订阅被直接放在页脚中,以利用 PHP 支持的模板。

为每个支持的动作添加事件绑定

对于每一个需要实时响应的动作,我们的应用都会触发一个需要“监听”的事件。Pusher 通过bind()方法使这变得非常容易,这对于任何在以前的项目中使用过 JavaScript 的开发人员来说都应该很熟悉。

bind()方法将被监听的事件的名称作为第一个参数,将事件发生时要执行的函数作为第二个参数。

使用以下粗体代码为应用中的每个事件绑定一个函数:

(function($) {
channel.bind('close', function(data){ });
channel.bind('open', function(data){ });
channel.bind('ask', function(data){ });
channel.bind('vote', function(data){ });
channel.bind('answer', function(data){ });
})(jQuery);
复制

这些绑定实际上不会增加应用的开销,因此同时绑定所有五个不会影响性能。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意这些方法现在还不做任何事情;您将在下一节中添加该功能。

添加效果

该应用现在发送和接收实时事件,所以剩下要做的就是添加新数据的效果。

处理房间事件

当演示者关闭一个房间时,需要立即让与会者知道,这样他们就不会试图问任何新问题或投新票。

类似地,如果一个演示者重新打开一个房间,所有与会者都应该立即知道这个房间现在又打开了。

由于标记在封闭的房间和开放的房间之间变化很大,所以提醒与会者房间的开放状态发生变化的最直接、最不容易出错的方法是简单地刷新页面。

init.js,创建一个名为room的新对象,它有两个方法:open()close()。两者都应该在被调用时重新加载页面。

相应的事件也应该触发这些方法。为此,将以下粗体代码添加到init.js:

(function($) {
channel.bind('close', function(data){ room.close(data); });
channel.bind('open', function(data){ room.open(data); });
channel.bind('ask', function(data){ });
channel.bind('vote', function(data){ });
channel.bind('answer', function(data){ });
var room = {
open: function(data){
location.reload();
},
close: function(data){
location.reload();
}
};
})(jQuery);
复制

请注意,我们已经选择在演示者关闭房间时自动重新加载页面。通常情况下,你不希望在没有用户输入的情况下重新加载一个页面,因为这可能会造成混乱,但是我们有充分的理由这样做。在这种特殊情况下,如前所述,当房间关闭时,页面上的许多标记会发生变化。此外,用户不能向关闭的房间提交新问题,因此关闭房间的演示者应该是破坏性的;否则,与会者可能会花额外的时间处理一个无论如何都无法提交的问题,这可能比仅仅意识到房间现在已经关闭,他们应该给演示者发电子邮件更令人沮丧。

添加带动画的新问题

当一个新问题被提出时,它应该立即提供给查看房间的每个人。为了使新数据的引入不那么刺耳,应该添加一个动画。

在第八章中,当动作处理器方法create_question()被添加到Question控制器中以询问新问题时,您已经为该事件生成了视图,因此格式化的 HTML 将在data对象中发送。

但是,因为后端不可能知道视图当前使用的随机数,所以我们需要读取随机数并将其插入到所有新生成的随机数字段的value属性中,然后用slideDown()将问题添加到列表的顶部:

(function($) {
channel.bind('close', function(data){ room.close(data); });
channel.bind('open', function(data){ room.open(data); });
channel.bind('ask', function(data){ question.ask(data); });
channel.bind('vote', function(data){ });
channel.bind('answer', function(data){ });
varnonce = $('input[name=nonce]:eq(0)').val(),
room = {
open: function(data){
location.reload();
},
close: function(data){
location.reload();
}
},
question = {
ask: function(data){
$(data.markup)
.find('input[name=nonce]').val(nonce).end()
.hide().prependTo('#questions').slideDown('slow');
}
};
})(jQuery);
复制

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意为了保持 JavaScript 简洁,我们在这个脚本中对所有三个变量只使用了一次var声明。这主要是一种风格上的选择,但是也有一些观点认为使用这种方法可以获得微小的性能提升。

为问题添加投票

当与会者对某个问题进行投票时,投票按钮旁边的计数应该会更新。然而,我们想吸引更多的注意力,所以让我们添加一个微妙的动画。

将动画添加到样式表

因为 CSS3 引入了动画,并且因为大多数现代浏览器支持 CSS3 动画的硬件加速,所以您的应用将使用关键帧 CSS 动画,而不是 jQuery 动画。

为了实现这一点,您首先必须确定哪个类(在本例中为.new-vote)将触发动画,然后设置它。对于这个名为vote的动画,我们将快速淡出问题,然后通过调整不透明度再次淡入。这将被执行或迭代两次。

不幸的是,您将需要特定于供应商的前缀来确保动画在所有浏览器中都能工作,所以应该是快速添加的内容变成了相当大量的 CSS。

将以下代码添加到assets/styles/main.css的底部:

/*
* ANIMATION
*****************************************************************************/
#questions li.new-vote {
-webkit-animation-name: vote;
-webkit-animation-duration: 0.5s;
-webkit-animation-timing-function: ease-in-out;
-webkit-animation-iteration-count: 2;
-moz-animation-name: vote;
-moz-animation-duration: 0.5s;
-moz-animation-timing-function: ease-in-out;
-moz-animation-iteration-count:2;
-ms-animation-name: vote;
-ms-animation-duration: 0.5s;
-ms-animation-timing-function: ease-in-out;
-ms-animation-iteration-count: 2;
animation-name: vote;
animation-duration: 0.5s;
animation-timing-function: ease-in-out;
animation-iteration-count: 2;
}
@-webkit-keyframes vote {
0% { opacity: 1; }
50% { opacity: .4; }
100% { opacity: 1; }
}
@-moz-keyframes vote {
0% { opacity: 1; }
50% { opacity: .4; }
100% { opacity: 1; }
}
@-ms-keyframes vote {
0% { opacity: 1; }
50% { opacity: .4; }
100% { opacity: 1; }
}
@keyframes vote {
0% { opacity: 1; }
50% { opacity: .4; }
100% { opacity: 1; }
}
复制

用一个类触发动画

现在动画已经就绪,JavaScript 需要做的就是添加一个类来触发它。

除了动画之外,脚本还需要更新投票计数,因为可能会有多个人为一个问题投票,所以在动画完成后删除该类,以便它可以被多次触发。

将以下粗体代码添加到init.js以完成投票效果:

(function($) {
channel.bind('close', function(data){ room.close(data); });
channel.bind('open', function(data){ room.open(data); });
channel.bind('ask', function(data){ question.ask(data); });
channel.bind('vote', function(data){ question.vote(data); });
channel.bind('answer', function(data){ });
var nonce = $('input[name=nonce]:eq(0)').val(),
room = {
open: function(data){
location.reload();
},
close: function(data){
location.reload();
}
},
question = {
ask: function(data){
$(data.markup)
.find('input[name=nonce]').val(nonce).end()
.hide().prependTo('#questions').slideDown('slow');
},
vote: function(data){
var question = $('#question-'+data.question_id),
cur_count = question.data('count'),
new_count = cur_count+1;
// Updates the count
question
.attr('data-count', new_count)
.data('count', new_count)
.addClass('new-vote');
setTimeout(1000, function(){
question.removeClass('new-vote');
});
}
};
})(jQuery);
复制

测试动画

要查看实际效果,请使用两个浏览器加入一个房间(确保没有一个浏览器是演示者),并将它们并排放置,以便您可以同时看到两个浏览器。

在一个浏览器中,问一个新问题;当ask事件被触发时,它将被动态添加到另一个浏览器窗口。

在另一个浏览器中,对新问题进行投票。提交投票时,观看第一个浏览器:它将在动画中运行一遍,然后返回正常状态。很难在静态图像中演示这一点,但是图 10-8 显示了正在进行的动画。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-8。当使用左边的浏览器对首要问题投票时,动画会在右边的浏览器中触发

用动画和重新排序回答问题

当一个问题被标记为已回答时,它应该淡出,然后从列表中移除(通过向上的动画幻灯片)并重新附加到底部(也通过动画幻灯片),以便为未回答的问题腾出空间。

将以下粗体显示的代码添加到init.js中,使其发生:

(function($) {
channel.bind('close', function(data){ room.close(data); });
channel.bind('open', function(data){ room.open(data); });
channel.bind('ask', function(data){ question.ask(data); });
channel.bind('vote', function(data){ question.vote(data); });
channel.bind('answer', function(data){ question.answer(data); });
var nonce = $('input[name=nonce]:eq(0)').val(),
room = {
open: function(data){
location.reload();
},
close: function(data){
location.reload();
}
},
question = {
ask: function(data){
$(data.markup)
.find('input[name=nonce]').val(nonce).end()
.hide().prependTo('#questions').slideDown('slow');
},
vote: function(data){
var question = $('#question-'+data.question_id),
cur_count = question.data('count'),
new_count = cur_count+1;
// Updates the count
question
.attr('data-count', new_count)
.data('count', new_count)
.addClass('new-vote');
setTimeout(1000, function(){
question.removeClass('new-vote');
});
},
answer: function(data){
var question = $("#question-"+data.question_id),
detach_me = function() {
question
.detach()
.appendTo('#questions')
.slideDown(500);
}
question
.addClass('answered')
.delay(1000)
.slideUp(500, detach_me);
}
};
})(jQuery);
复制

测试答题

要查看标记为已回答的问题在与会者看来是什么样子,请打开两个浏览器并将窗口并排放置,以便您可以同时看到两个窗口。在一个浏览器中,创建一个新房间;在另一个房间里,问一个问题。

问题将显示在创建房间的浏览器上,此时您可以将问题标记为已回答。在与会者视图中,问题将淡化、消失,然后以“已回答”状态重新附加(参见图 10-9 )。因为房间里只有一个问题,所以不会演示如何将问题移到列表的底部,但是如果您愿意,您可以进行自己的实验来查看实际效果。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-9。一个在与会者和演示者视图中都被标记为已回答的问题

摘要

在这一章中,你确切地了解了实现实时事件是多么的快捷和容易,以及将它们集成到你的应用中的效果。

至此,应用已经完成,您已经准备好开始构建自己令人惊叹的实时应用了。请在 Twitter 上找到你的作者——@ jlengstorf 和@ leggetter——并分享你的创作。

朋友,欢迎来到网页设计的未来。

转载请注明出处或者链接地址:https://www.qianduange.cn//article/15479.html
标签
VKDoc
评论
还可以输入200
共0条数据,当前/页
发布的文章

安装Nodejs后,npm无法使用

2024-11-30 11:11:38

大家推荐的文章
会员中心 联系我 留言建议 回顶部
复制成功!