20、Web开发中的元素扩展、测试与应用实践

Web开发中的元素扩展、测试与应用实践

1. 元素扩展方法

在Web开发中,有许多实用的元素扩展方法,这些方法大多定义在 effects.js 文件中,除特别说明外,都会添加到Prototype的 Element.Methods 对象中,通过 $() $$() 函数访问的所有DOM元素会自动混入这些方法。以下是一些常用方法:
| 方法名 | 功能 | 示例 |
| ---- | ---- | ---- |
| collectTextNodes(element) | 返回元素所有子文本节点连接成的字符串 | Element.collectTextNodes('target') |
| collectTextNodesIgnoreClass(element, className) | 返回元素所有子文本节点连接成的字符串,排除指定类名的节点 | Element.collectTextNodesIgnoreClass('target', 'a') |
| setContentZoom(element, percent) | 通过改变字体大小样式设置元素的缩放级别 | Element.setContentZoom('target', 200) |
| getOpacity(element) | 返回元素的不透明度,值为0 - 1之间的浮点数 | Element.getOpacity('target') |
| setOpacity(element, value) | 设置元素的不透明度,值应在0 - 1之间 | Element.setOpacity('target', 0.5) |
| childrenWithClassName(element, className[, findFirst]) | 返回元素所有匹配指定类名的子元素,若 findFirst true ,则只返回第一个元素 | Element.childrenWithClassName('container', 'green', true) |
| forceRerendering(element) | 通过添加并移除一个空格字符的文本节点,强制元素重新渲染 | Element.forceRerendering('target') |
| visualEffect(element, effect[, options]) | 为指定元素创建一个新的效果对象,返回元素ID,支持链式调用 | $('target').visualEffect('blind_up').visualEffect('fade') |

此外,还有一些定义在 dragdrop.js 中的方法:
- isParent(child, element) :判断 child 是否包含在 element 中。
- findChildren(element, only, recursive, tagName) :返回元素所有指定标签名的子元素,可选择限制类名,若 recursive true ,则搜索所有后代元素。
- offsetSize(element, type) :若 type vertical height ,返回元素的偏移高度,否则返回偏移宽度。

2. DOM构建器

DOM 构建器定义在 builder.js 中,默认的Rails骨架不包含该文件。 Builder.node() 方法可用于创建DOM元素:

element = Builder.node('p', {className:'green'}, 
            'Here is a green paragraph.');
document.body.appendChild(element);

上述代码会创建一个带有指定类名和文本内容的段落元素。 Builder.node() 方法还支持嵌套调用,可用于创建复杂的DOM结构,如表格:

$('my_div').appendChild(
  Builder.node('table', { width:'100%', border:'1'}, [
    Builder.node('tbody', [
      Builder.node('tr', {className:'header'}, [
        Builder.node('td', [ Builder.node('strong', 'Table Cell')])
      ])
    ])
  ])
);

需要注意的是,动态创建的表格中 TBODY 元素是必需的。

3. JavaScript单元测试

JavaScript 单元测试工具定义在 unittest.js 中,默认的Rails骨架不包含该文件。 Test.Unit.Runner 是编写单元测试用例的实用类,测试用JavaScript编写并在浏览器中运行。以下是创建测试运行器实例的示例:

new Test.Unit.Runner({
  // 可选的设置函数,在每个测试用例前运行
  setup: function(  ) { with(this) {
    // code
  }},
  // 可选的清理函数,在每个测试用例后运行
  teardown: function(  ) { with(this) {
    // code
  }},
  // 测试用例,每个以 "test" 开头的方法被视为一个测试用例
  testATest: function(  ) { with(this) {
    // code
  }},
  testAnotherTest: function(  ) { with(this) {
    // code
  }}
});

构造函数的第二个可选参数是一个选项对象,其键包括:
- testLog :指定接收测试输出的元素ID,默认为 testlog
- test :若指定,则只运行给定的测试用例。

测试用例在以下页面模板中创建:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
  <head>
    <title>JavaScript Unit Test</title>
    <script src="prototype.js" type="text/javascript"></script>
    <script src="unittest.js" type="text/javascript"></script>
    <!-- 测试所需的其他JavaScript文件 -->
    <link href="test.css" type="text/css" />
  </head>
  <body>
    <h1>JavaScript Unit Test</h1>
    <!-- 日志输出 -->
    <div id="testlog"> </div>
    <!-- 沙箱 -->
    <div id="sandbox"> </div>
    <!-- 测试 -->
    <script type="text/javascript">
      new Test.Unit.Runner({
        // 测试用例
      });
    </script>
  </body>
</html>

沙箱元素可包含测试用例所需的任何HTML标记。测试运行结果可通过在测试模板URL中添加 resultsURL 查询参数报告回服务器。

4. 断言方法

Test.Unit.Runner 的测试方法中,基本的断言调用如下:

testExample: function(  ) { with(this) {
  var myElement = $('mydiv');
  assertEqual("DIV", myElement.tagName);
  assertEqual("DIV", myElement.tagName, "Hmm, not a DIV?");
}};

所有断言方法的最后一个参数是可选的消息,用于断言失败时的额外日志备注。常见的断言方法有:
| 方法名 | 功能 |
| ---- | ---- |
| assert(expression [, message ]) | 断言表达式求值为 true |
| assertEqual(expected, actual[, message ]) | 断言 expected actual 相等 |
| assertNotEqual(expected, actual[, message ]) | 断言 expected actual 不相等 |
| assertNull(object[, message ]) | 断言对象为 null |
| assertNotNull(object[, message ]) | 断言对象不为 null |
| assertHidden(element[, message ]) | 断言元素的 display 属性为 none |
| assertVisible(element[, message ]) | 断言元素可见(其自身及其所有祖先元素的 display 属性都不为 none ) |
| assertNotVisible(element[, message ]) | 断言元素(或其某个祖先元素)的 display 属性为 none |
| assertInstanceOf(object, expected[, message ]) | 断言对象是指定类型的实例 |
| assertNotInstanceOf(object, expected[, message ]) | 断言对象不是指定类型的实例 |
| assertEnumEqual(expected, actual[, message ]) | 断言实际集合的所有成员与预期集合的成员匹配 |

5. 实用方法

script.aculo.us 还定义了一些不属于主要类别的实用方法:
- Scriptaculous.Version :返回 script.aculo.us 的当前版本号。
- Scriptaculous.require(libraryName) :加载指定URL的JavaScript文件。
- Scriptaculous.load() :加载 script.aculo.us 发行版中的标准文件,包括 builder.js effects.js dragdrop.js controls.js slider.js
- String.prototype.parseColor :将 rgb(x, x, x) #xxx 格式的字符串转换为 #xxxxxx 格式。

"rgb(255,255,255)".parseColor(  ); // '#ffffff'
"#123".parseColor(  ); // '#112233'
  • Array.prototype.call(arg1[, arg2 ...]) :数组中的每个元素应为函数,依次调用每个函数并传递参数。
var functions = [ function(v){ alert('hello, ' + v);},
                  function(v){ alert('hi, ' + v);} ];
functions.call('scott');
6. 复习测验应用

复习测验应用是一个用于提供共享测验的应用,类似于抽认卡,用户可以创建和使用测验进行自我学习和复习。该应用没有用户账户和登录机制,但采用了基于会话的身份验证。当用户创建新测验时,会存储其会话ID,只有使用相同会话ID才能进行更改。

6.1 数据库和模型

应用的数据库和模型非常简单,只有两个表和两个模型: quizzes questions 。以下是数据库结构的定义:

ActiveRecord::Schema.define(:version => 1) do
  create_table "questions", :force => true do |t|
    t.column "quiz_id",  :integer
    t.column "position", :integer
    t.column "question", :text, :default => "", :null => false
    t.column "answer",   :text, :default => "", :null => false
  end
  add_index "questions", ["quiz_id"], 
    :name => "questions_quiz_id_index"
  add_index "questions", ["position"], 
    :name => "position"
  create_table "quizzes", :force => true do |t|
    t.column "name", :string,
      :default => "New Quiz", :null => false
    t.column "session_id", :string, 
      :limit => 50, :default => "", :null => false
    t.column "created_at", :datetime,
      :null => false
  end
  add_index "quizzes", ["created_at"], :name => "created_at"
end

Question 模型除了基本的 ActiveRecord 功能外,还定义了一个返回下一个问题的方法:

class Question < ActiveRecord::Base
  belongs_to   :quiz
  acts_as_list :scope => :quiz

  # 返回当前问题之后的下一个问题,排除指定的正确答案ID
  def next right_keys
    quiz.questions.find :first,
      :conditions => "position > #{position}" +
        (right_keys.blank? ? "" : " and id not in (#{right_keys})")
  end
end

Quiz 模型添加了一个关联方法,用于轻松查找未错过的问题:

class Quiz < ActiveRecord::Base
  # 关联方法,如 quiz.questions.unmissed 用于检索未错过的问题
  module AssociationExtension
    def unmissed right_keys
      cond = "id not in (#{right_keys})" unless right_keys.blank?
      find :all, :conditions => (cond || nil), :limit => 5
    end
  end
  has_many :questions, 
    :order     =>'position', 
    :dependent => :destroy, 
    :extend    => AssociationExtension

  # 查找最后创建的20个测验
  def self.recent
    find :all, :limit => 20, :order => "created_at desc"
  end
end
6.2 控制器

应用通过 QuizzesController 实现,路由映射包括Rails默认路由、主页路由和一个为测验控制器定义的命名路由集合:

ActionController::Routing::Routes.draw do |map|
  map.resources :quizzes,
    :member => { :create_q  => :post,
                 :destroy_q => :post,
                 :reorder   => :post,
                 :answer    => :post,
                 :reset     => :post }
  map.home '', :controller =>'quizzes'
  map.connect ':controller/:action/:id'
end

QuizzesController 使用 before_filter 确保有当前测验(如果需要),并在需要权限的操作中确保用户有权编辑测验。以下是部分主要方法:

class QuizzesController < ApplicationController

  before_filter :find_quiz, :except => [ :index, :create ]
  before_filter :check_permissions, 
    :only => [ :edit, :reorder, :questions, :destroy_question ]

  # 列出最近的测验
  def index
    @quizzes = Quiz.recent
  end

  # 创建新测验并保存用户的会话ID
  def create
    quiz = Quiz.new params[:new_quiz]
    quiz.session_id = session.session_id
    quiz.save
    redirect_to edit_quiz_url(:id => quiz)
  end

  # 显示编辑测验的视图
  def edit
  end

  # 创建新问题,支持Ajax和传统表单提交
  def create_q
    @question = @quiz.questions.create params[:question]
    respond_to do |format|
      format.html { redirect_to edit_quiz_url }
      format.js
    end
  end

  # 处理拖放重新排序问题
  def reorder
    params[:quiz].each_with_index do |id, position|
      q = @quiz.questions.find id
      q.position = position + 1
      q.save
    end
    render :nothing => true
  end

  # 处理删除问题
  def destroy_q
    question = @quiz.questions.find params[:question_id]
    question.destroy
    render :nothing => true
  end

  # 显示前五个未错过的问题
  def show
    @questions = @quiz.questions.unmissed right_keys
  end

  # 返回问题的答案响应
  def answer
    score @quiz.id, params[:question_id], params[:right]=='true'
    last = @quiz.questions.find params[:last]
    @next = last.next right_keys
  end

  # 重置用户的测验计分板
  def reset
    reset_scoreboard params[:id]
    redirect_to quiz_url
  end

  private

    # 查找测验的前置过滤器
    def find_quiz(  ) @quiz = Quiz.find params[:id] end

    # 检查权限的前置过滤器
    def check_permissions
      redirect_to home_url and return false unless mine?
    end

    # 判断当前用户是否是测验的创建者
    def mine?
      @quiz.session_id == session.session_id
    end
    helper_method :mine?

    # 包装会话以跟踪用户的测验结果
    def scoreboard id=nil
      return (session[:quizzes] ||= {}) unless id
      return (scoreboard[id.to_i] ||= {})
    end

    # 清除指定测验的用户计分板
    def reset_scoreboard id
      scoreboard[id.to_i] = {}
    end

    # 记录问题的答案结果
    def score id, q, right
      scoreboard(id)[q.to_i] = right
    end

    # 获取指定测验的正确答案数组
    def right(id) scoreboard(id).reject{ |q, v| !v } end
    helper_method :right

    # 获取指定测验的错误答案数组
    def wrong(id) scoreboard(id).reject{ |q, v| v } end
    helper_method :wrong

    # 获取当前测验正确答案的ID组成的逗号分隔字符串
    def right_keys
      questions = right(@quiz.id)
      questions.keys.join ','
    end
end
6.3 视图和模板

应用的视图和模板包括布局视图 application.rhtml 、编辑模板 edit.rhtml 、显示模板 show.rhtml 等。
- application.rhtml :测验的顶级布局,包含一个添加新测验的简单Ajax表单和一个用于显示问题的 DIV

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  <head>
    <title>Review Quiz</title>
    <%= stylesheet_link_tag "application" %>
    <%= javascript_include_tag :defaults %>
  </head>
  <body>
    <h1><%= link_to "Review Quiz", home_url %></h1>
    <% form_for :new_quiz, Quiz.new, :url => quizzes_url, 
          :html => { :id => "new_quiz" } do |f| %>
      <label for="new_quiz_name">Create a quiz:</label> 
      <%= f.text_field :name %> <%= submit_tag "Create" %>
    <% end %>
    <div class="clear"></div>
    <%= yield %>
  </body>
</html>
  • edit.rhtml :允许用户添加新问题、删除现有问题并通过拖放重新排序问题:
<h2>Edit: <%= @quiz.name %></h2>
<ul id="links">
  <li id="done" <% unless @quiz.questions.any? %>
    style="display: none"<% end %>>
    <%= link_to "Take the quiz", quiz_url %>
  </li>
</ul>
<ul id="quiz">
  <% @quiz.questions.each do |question| %>
    <%= render :partial => "edit_question", 
          :locals => { :question => question } %>
  <% end %>
</ul>
<%= sortable_element :quiz, :url => reorder_quiz_url %>
<% remote_form_for :question, Question.new, 
     :url => create_q_quiz_url,
     :html => { :id => "new_question",
     :onKeyPress  => "return Quiz.captureKeypress(event);" } do |f| %>
  <div id="starting" <% if @quiz.questions.any? %>
    style="display: none"<% end %>>
    <strong>Add the first question</strong> to your new quiz.
  </div>
  <h3>Add a Question</h3>
  <label for="question_question">Question</label>
  <%= f.text_area :question %>
  <label for="question_answer">Answer</label>
  <%= f.text_area :answer %>
  <%= submit_tag "Save" %>
  <%= javascript_tag "$('question_question').focus(  )" %>
<% end %>
  • show.rhtml :负责渲染给定的测验,包括计分板和供用户回答的问题列表:
<%= render :partial =>'scoreboard' %>
<h2><%= h(@quiz.name) %></h2>
<ul id="links">
  <% if mine? %>
    <li><%= link_to "Edit this quiz", edit_quiz_url %></li>
  <% end %>
  <li style="display: none" id="startover">
    <%= link_to "Start Over", reset_quiz_url, :method => :post %>
  </li>
</ul>
<div id="questions">
  <%= render :partial => "question", :collection => @questions %>
</div>
<div id="finished" style="display: none">
    <strong>You're done!</strong> Now you can 
    <%= link_to "start over", reset_quiz_url, :method => :post %>, 
    or just <%= link_to "review what you missed.", quiz_url %>
</div>
6.4 JavaScript代码

应用特定的JavaScript代码定义在 application.js 中,包含处理表单按键、显示提示信息、显示答案、提交答案等功能:

var Quiz = {
  /* 处理创建问题表单中的回车事件 */
  captureKeypress: function(evt) {
    var keyCode = evt.keyCode ? evt.keyCode :
      evt.charCode ? evt.charCode : evt.which;
    if (keyCode == Event.KEY_RETURN) {
      if(Event.element(evt).id=='question_question')
        $('question_answer').focus(  );
      if(Event.element(evt).id=='question_answer')
        $('new_question').onsubmit(  );
      return false;
    }
    return true;
  },

  /* 编辑测验时隐藏和显示帮助信息 */
  updateHints: function(  ) {
    $('quiz').cleanWhitespace(  );
    if($A($('quiz').childNodes).any(  )) {
      $('done').show(  );
      $('starting').hide(  );
    }
  },

  /* 显示问题的答案节点 */
  reveal: function(questionId) {
    $(questionId+'_a').visualEffect('blind_down', {duration:0.25})
  },

  /* 处理提交答案 */
  answer: function(quizId, questionId, right) {
    var url = '/quizzes/' + quizId + ';answer';
    var params ='question_id=' + questionId +
                 '&right='      + (right ?'true' : false) +
                 '&last='       + this.questions().last(  ).id;
    new Ajax.Request(url, {parameters:params});
    $(questionId.toString(  )).visualEffect('fade_up', {duration:0.5});
    if(this.showingQuestions() && !$('finished').visible(  ))
      $('finished').visualEffect('appear_down');
    $('startover').show(  );
  },

  /* 返回所有问题的DOM节点 */
  questions: function(  ) {
    var questions = $('questions');
    questions.cleanWhitespace(  );
    return $A(questions.childNodes);
  },

  /* 判断是否有显示的问题节点 */
  showingQuestions: function(  ) {
    return this.questions(  ).select(function(e){
      return e.visible(  );
    }).length==1;
  }
}

// 自定义效果:结合BlindUp和Fade
Effect.FadeUp = function(element) {
  element = $(element);
  element.makeClipping(  );
  return new Effect.Parallel(
    [ new Effect.Opacity(element, {from:1,to:0}),
      new Effect.Scale(element, 0, 
        {scaleX:false,scaleContent:false,restoreAfterFinish: true}) ],
    Object.extend({
      to: 1.0,
      from: 0.0,
      transition: Effect.Transitions.linear,
      afterFinishInternal: function(effect) { 
        effect.effects[0].element.hide(  );
        effect.effects[0].element.undoClipping(  );
      }}, arguments[1] || {})
  );
}

// 自定义效果:结合BlindDown和Appear
Effect.AppearDown = function(element) {
  element = $(element);
  var elementDimensions = element.getDimensions(  );
  return new Effect.Parallel(
    [ new Effect.Opacity(element, {from:0,to:1}),
      new Effect.Scale(element, 100, 
        {from:0,to:1,scaleX:false,
         scaleContent:false,restoreAfterFinish:true,
         scaleMode:{originalHeight:elementDimensions.height,
           originalWidth:elementDimensions.width}}) ],
    Object.extend({
      transition: Effect.Transitions.linear,
      afterSetup: function(effect) {
        effect.effects[0].element.makeClipping(  );
        effect.effects[0].element.setStyle({height:'0px'});
        effect.effects[0].element.show(  ); 
      },  
      afterFinishInternal: function(effect) { 
        effect.effects[0].element.undoClipping(  );
      }}, arguments[1] || {})
  );
}

通过以上介绍,我们了解了Web开发中的元素扩展方法、DOM构建器、JavaScript单元测试、实用方法以及一个完整的复习测验应用的实现。这些知识和技术可以帮助我们更好地开发和优化Web应用。

Web开发中的元素扩展、测试与应用实践

7. 样式表设计

应用的样式表对页面的布局和外观起着关键作用,以下是样式表的详细内容:

/* Basics */
/* ------------------------- */
html {
    background-color: #ddd;
    padding: 20px;
    border-top: 8px solid #494;
    height: 100%;
}
body {
    width: 80%;
    margin: 0 auto 0 auto;
    padding: 0 20px 0 20px;
    border-top: 1px solid #bbb;
    border-right: 1px solid #999;
    border-bottom: 1px solid #999;
    border-left: 1px solid #bbb;
    background-color: #fff;
    font-family: helvetica, arial, sans-serif;
    min-height: 100%;
}
h1 {
    float: left;
}
h2 a {
    font-size: 0.5em;
}
.clear {
    clear: both;
}
#links {
    margin-top: -1.7em;
    padding-left: 15px;
    list-style-type: square;
    font-size: 0.7em;
}

/* Links */
/* ------------------------- */
a {
    color: #a44;
    text-decoration: none;
}
a:hover {
    text-decoration: underline;
    color: #464;
}
a.green, a.red, a.yellow {
    text-transform: uppercase;
    font-size: 0.7em;
    padding: 1px 2px;
}
a.green {
    color: #363;
    background-color: #cfc;
    border: 1px solid #696;
}
a.red {
    color: #633;
    background-color: #fcc;
    border: 1px solid #966;
}
a.yellow {
    color: #663;
    background-color: #ffc;
    border: 1px solid #996;
}

/* Create Quiz */
/* ------------------------- */
#new_quiz {
    font-size: .7em;
    text-transform: uppercase;
    float: right;
    margin-top: 20px;
    background-color: #bdb;
    padding: 5px 10px;
    border: 1px solid #9b9;
}
#new_quiz input[type='text'] {
    width: 100px;
    font-weight: bold;
    background-color: #cfc;
}

/* Edit Quiz */
/* ------------------------- */
#new_question {
    clear: right;
    background-color: #bdb;
    padding: 5px 10px;
    border: 1px solid #9b9;
    width: 55%;
    padding-right: 80px;
    padding-top: 10px;
    margin-top: 50px;
}
#new_question h3 {
    margin-top: 0;
    margin-bottom: 8px;
    font-size: 0.7em;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    font-weight: bold;
}
#new_question label {
    font-size: 0.7em;
    text-transform: uppercase;
    font-weight: normal;
    float: left;
    width: 65px;
    margin-top: 5px;
}
#new_question textarea {
    width: 100%;
    display: block;
    height: 40px;
    vertical-align: top;
    margin-bottom: 10px;
}
#new_question input {
    margin-left: 65px;
}
#starting {
    color: #331;
    background-color: #ffc;
    border: 1px solid #cca;
    padding: 5px;
    margin-bottom: 10px;
}
#finished {
    color: #331;
    background-color: #ffc;
    border: 1px solid #cca;
    padding: 10px;
    width: 270px;
}

/* Take Quiz */
/* ------------------------- */
#questions {
    padding-top: 20px;
}
.question .q {
    margin-bottom: 10px;
}
.question .a {
    margin-bottom: 30px;
    margin-left: 30px;
}

/* Scoreboard */
/* ------------------------- */
#scoreboard {
    padding: 6px;
    float: right;
    width: 150px;
    color: #331;
    background-color: #ffc;
    border: 1px solid #cca;
    text-align: center;
    margin-left: 20px;
    margin-bottom: 10px;
}
#scoreboard #total, #scoreboard #remaining {
    text-transform: uppercase;
    font-size: 0.7em;
    color: #888;
    letter-spacing: 0.1em;
}
#scoreboard #score {
    font-weight: bold;
    margin: 2px 0 3px 0;
}
#scoreboard #right {
    color: #090;
}
#scoreboard #wrong {
    color: #900;
} 

样式表涵盖了多个方面的样式设置,以下是一些关键部分的总结:
| 部分 | 描述 |
| ---- | ---- |
| Basics | 设置HTML和body的背景、边框、字体等基本样式 |
| Links | 定义链接的颜色、悬停效果以及不同颜色链接的样式 |
| Create Quiz | 设置创建测验表单的样式 |
| Edit Quiz | 设置编辑测验表单和相关提示信息的样式 |
| Take Quiz | 设置测验问题显示的样式 |
| Scoreboard | 设置计分板的样式 |

8. 整体流程分析

下面通过一个mermaid流程图来展示复习测验应用的整体流程:

graph LR
    A[用户访问首页] --> B{选择操作}
    B -->|创建新测验| C[输入测验名称,提交表单]
    C --> D[保存测验,记录会话ID]
    D --> E[进入编辑测验页面]
    E --> F{编辑操作}
    F -->|添加问题| G[输入问题和答案,提交表单]
    G --> H[保存问题,更新页面]
    F -->|删除问题| I[删除问题,更新页面]
    F -->|重新排序问题| J[拖放问题,更新位置,保存排序]
    B -->|选择已有测验| K[进入测验页面]
    K --> L[显示问题列表和计分板]
    L --> M{回答问题}
    M -->|回答正确| N[记录正确答案,更新计分板]
    M -->|回答错误| O[记录错误答案,更新计分板]
    N --> P{是否还有问题}
    P -->|是| L
    P -->|否| Q[显示完成信息,提供重新开始或复习选项]

从流程图可以看出,用户可以在首页选择创建新测验或选择已有测验。创建新测验后可以进行编辑操作,包括添加、删除和重新排序问题。选择已有测验后进入测验页面,回答问题并根据回答结果更新计分板,直到完成所有问题。

9. 应用特点总结
  • 低门槛使用 :应用没有用户账户和登录机制,采用会话ID进行简单的身份验证,降低了新用户的使用门槛。
  • Ajax交互 :大部分操作如添加问题、删除问题、重新排序问题、提交答案等都通过Ajax实现,提高了用户体验,减少了页面刷新。
  • 代码结构清晰 :控制器负责数据管理,视图负责页面展示,JavaScript代码负责处理用户交互和页面效果,各部分职责明确。
  • 可扩展性 :可以通过添加更多的功能,如用户账户系统、更多的测验类型、统计分析等,进一步扩展应用的功能。
10. 技术要点回顾
  • 元素扩展方法 :提供了一系列操作DOM元素的方法,方便对元素进行文本收集、透明度设置、子元素查找等操作。
  • DOM构建器 :使用 Builder.node() 方法可以动态创建复杂的DOM结构,如表格。
  • JavaScript单元测试 :通过 Test.Unit.Runner 和各种断言方法,可以对JavaScript代码进行单元测试,确保代码的正确性。
  • 实用方法 script.aculo.us 提供的实用方法,如版本获取、文件加载、颜色转换等,提高了开发效率。

通过对这些技术要点的掌握和应用,我们可以更好地开发和优化Web应用,实现丰富的交互效果和功能。

综上所述,Web开发涉及到多个方面的知识和技术,从元素扩展到应用实践,每个环节都有其重要性。通过不断学习和实践,我们可以提升自己的开发能力,打造出更加优秀的Web应用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值