MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Ruby on Rails 表单处理

2023-04-244.6k 阅读

表单基础概念

在 Web 应用开发中,表单是用户与服务器进行交互的重要方式。用户通过填写表单中的各种字段(如文本框、下拉框、复选框等),将数据提交给服务器进行处理。在 Ruby on Rails 框架中,表单处理是构建交互式 Web 应用的核心部分之一。

表单的创建

在 Rails 中,创建表单有多种方式。最常用的是使用 form_with 方法。例如,假设我们有一个 Article 模型,包含 titlecontent 字段,我们可以在对应的视图文件(如 app/views/articles/new.html.erb)中创建一个用于创建新文章的表单:

<%= form_with(model: @article, local: true) do |form| %>
  <div class="field">
    <%= form.label :title %>
    <%= form.text_field :title %>
  </div>
  <div class="field">
    <%= form.label :content %>
    <%= form.text_area :content %>
  </div>
  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

上述代码中,form_with 方法接受一个 model 参数,这里是 @article,它可以是一个新的 Article 实例(用于创建操作),也可以是一个已存在的实例(用于更新操作)。local: true 表示表单提交将以本地方式进行,不会触发页面的完全刷新(如果不设置此选项,表单提交默认会进行全页刷新)。

form.label 用于生成字段的标签,form.text_fieldform.text_area 分别用于生成文本框和文本区域。form.submit 生成提交按钮。

表单数据的传递

当用户点击提交按钮时,表单数据会被发送到服务器。在 Rails 中,这些数据会根据表单的 action 属性(如果未显式设置,form_with 会根据模型的状态自动确定合适的 URL)发送到对应的控制器动作。例如,上述表单提交后,数据会发送到 ArticlesControllercreate 动作。在 create 动作中,我们可以通过 params 对象获取表单数据:

class ArticlesController < ApplicationController
  def create
    @article = Article.new(article_params)
    if @article.save
      redirect_to @article, notice: 'Article was successfully created.'
    else
      render :new
    end
  end

  private
  def article_params
    params.require(:article).permit(:title, :content)
  end
end

article_params 方法中,params.require(:article) 表示从 params 中获取名为 article 的参数哈希,这是 Rails 约定的参数命名方式(与模型名对应)。permit(:title, :content) 则表示只允许 titlecontent 这两个参数用于创建或更新 Article 实例,这是为了防止参数注入攻击,确保只有我们期望的参数能被用于操作模型。

表单字段类型

文本字段

文本字段是最常见的表单字段类型之一,用于用户输入单行文本。除了前面提到的 text_field,还有 password_field 用于输入密码,输入的内容会以掩码形式显示。例如,在用户注册表单中用于输入密码:

<%= form_with(model: @user, local: true) do |form| %>
  <div class="field">
    <%= form.label :username %>
    <%= form.text_field :username %>
  </div>
  <div class="field">
    <%= form.label :password %>
    <%= form.password_field :password %>
  </div>
  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

当服务器接收到密码字段的数据后,通常需要对其进行加密存储,以确保用户信息的安全。在 Rails 中,可以使用 bcrypt 等加密库来实现密码加密。

文本区域

文本区域用于用户输入多行文本,如文章内容、评论等。前面创建文章表单的例子中已经展示了 text_area 的使用。我们还可以通过传递 rowscols 属性来指定文本区域的初始行数和列数:

<%= form.text_area :content, rows: 10, cols: 50 %>

这样会创建一个初始显示 10 行、50 列的文本区域。

下拉框

下拉框(select 标签)允许用户从预定义的选项列表中选择一个值。假设我们有一个 Category 模型,并且希望在创建文章时选择文章所属的类别,可以这样创建下拉框:

<%= form.collection_select :category_id, Category.all, :id, :name %>

这里 collection_select 方法的第一个参数 :category_id 是关联的外键字段名,Category.all 是数据源,即所有的类别记录。:id 表示每个选项的值是类别记录的 id:name 表示每个选项显示的文本是类别记录的 name 字段。

复选框和单选框

复选框允许用户选择多个选项,而单选框只允许选择一个选项。例如,假设文章可以有多个标签,我们可以创建复选框来选择标签:

<% @tags.each do |tag| %>
  <div class="field">
    <%= form.check_box :tag_ids, { multiple: true }, tag.id, nil %>
    <%= form.label tag.name %>
  </div>
<% end %>

这里 form.check_box 的第一个参数 :tag_ids 表示关联的标签 id 数组字段(假设在 Article 模型中有 has_and_belongs_to_many :tags 关联),{ multiple: true } 表示这是一个多选的复选框组,tag.id 是每个复选框的值,nil 用于未选中时的默认值。

对于单选框,假设文章有一个布尔类型的 published 字段表示是否发布,可以这样创建单选框:

<div class="field">
  <%= form.radio_button :published, true %>
  <%= form.label 'Published' %>
  <%= form.radio_button :published, false %>
  <%= form.label 'Unpublished' %>
</div>

radio_button 的第一个参数是字段名,第二个参数是单选框的值。

表单验证

模型层验证

在 Rails 中,模型层的验证是确保表单数据有效性的重要手段。继续以 Article 模型为例,我们可以在模型中添加验证规则:

class Article < ApplicationRecord
  validates :title, presence: true, length: { minimum: 5 }
  validates :content, presence: true
end

上述代码中,validates :title, presence: true 表示 title 字段不能为空,length: { minimum: 5 } 表示 title 的长度至少为 5 个字符。validates :content, presence: true 确保 content 字段也不能为空。

当在控制器中调用 @article.save 时,如果验证不通过,@article 会保存失败,并且 @article.errors 会包含所有验证错误信息。在视图中,可以通过以下方式显示这些错误信息:

<% if @article.errors.any? %>
  <div id="error_explanation">
    <h2><%= pluralize(@article.errors.count, "error") %> prohibited this article from being saved:</h2>
    <ul>
    <% @article.errors.full_messages.each do |message| %>
      <li><%= message %></li>
    <% end %>
    </ul>
  </div>
<% end %>

这样,用户在提交表单后,如果数据不符合验证规则,会看到具体的错误提示。

自定义验证

除了使用内置的验证器,我们还可以创建自定义验证。例如,假设我们希望文章标题不能包含特定的敏感词汇,可以这样实现自定义验证:

class Article < ApplicationRecord
  SENSITIVE_WORDS = ['敏感词1', '敏感词2']

  validate :title_does_not_contain_sensitive_words

  private
  def title_does_not_contain_sensitive_words
    SENSITIVE_WORDS.each do |word|
      if title&.include?(word)
        errors.add(:title, "不能包含敏感词汇 #{word}")
      end
    end
  end
end

在上述代码中,我们定义了一个 title_does_not_contain_sensitive_words 方法,并在 validate 块中调用它。如果标题包含敏感词汇,就会向 title 字段添加相应的错误信息。

客户端验证

虽然模型层验证可以确保服务器端数据的有效性,但为了提供更好的用户体验,也可以在客户端进行验证。Rails 可以借助 JavaScript 库(如 jquery-validate)来实现客户端验证。首先,在 Gemfile 中添加 jquery-railsjquery-validate-rails 宝石:

gem 'jquery-rails'
gem 'jquery-validate-rails'

然后运行 bundle install 安装宝石。接着,在 app/assets/javascripts/application.js 中引入相关脚本:

//= require jquery
//= require jquery_ujs
//= require jquery.validate
//= require jquery.validate.additional-methods

在视图中,可以通过为表单添加 data-validate 属性来启用客户端验证。例如:

<%= form_with(model: @article, local: true, data: { validate: true }) do |form| %>
  <!-- 表单字段 -->
<% end %>

这样,当用户在表单中输入数据时,客户端会根据模型层定义的验证规则实时进行验证,并显示相应的错误提示,无需等待表单提交到服务器。

表单嵌套与关联处理

一对一关联表单

假设 User 模型与 Profile 模型是一对一关联,即一个用户有一个个人资料。我们可以在用户注册表单中同时创建用户和对应的个人资料。首先,在 User 模型中设置关联:

class User < ApplicationRecord
  has_one :profile
  accepts_nested_attributes_for :profile
end

accepts_nested_attributes_for :profile 表示允许在创建或更新 User 实例时,同时处理关联的 Profile 实例的属性。在视图中,可以这样创建嵌套表单:

<%= form_with(model: @user, local: true) do |form| %>
  <div class="field">
    <%= form.label :username %>
    <%= form.text_field :username %>
  </div>
  <%= form.fields_for :profile do |profile_form| %>
    <div class="field">
      <%= profile_form.label :bio %>
      <%= profile_form.text_area :bio %>
    </div>
  <% end %>
  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

这里 fields_for :profile 会创建一个子表单用于处理 Profile 模型的字段。在控制器中,接收并处理嵌套参数:

class UsersController < ApplicationController
  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to @user, notice: 'User was successfully created.'
    else
      render :new
    end
  end

  private
  def user_params
    params.require(:user).permit(:username, profile_attributes: [:bio])
  end
end

user_params 方法中通过 profile_attributes: [:bio] 允许 Profile 模型的 bio 字段作为 User 参数的一部分进行处理。

一对多关联表单

如果 Article 模型与 Comment 模型是一对多关联,即一篇文章可以有多个评论。我们可以在文章详情页面提供一个表单用于创建新评论。在 Article 模型中设置关联:

class Article < ApplicationRecord
  has_many :comments
  accepts_nested_attributes_for :comments
end

在文章详情视图(app/views/articles/show.html.erb)中创建评论表单:

<%= form_with(model: [@article, @article.comments.build], local: true) do |form| %>
  <div class="field">
    <%= form.label :author %>
    <%= form.text_field :author %>
  </div>
  <div class="field">
    <%= form.label :content %>
    <%= form.text_area :content %>
  </div>
  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

这里 form_with(model: [@article, @article.comments.build]) 表示创建一个与 @article 关联的新 Comment 实例的表单。在 CommentsController 中处理评论创建:

class CommentsController < ApplicationController
  def create
    @article = Article.find(params[:article_id])
    @comment = @article.comments.new(comment_params)
    if @comment.save
      redirect_to @article, notice: 'Comment was successfully created.'
    else
      render :new
    end
  end

  private
  def comment_params
    params.require(:comment).permit(:author, :content)
  end
end

这种方式确保了新创建的评论与对应的文章正确关联。

多对多关联表单

ArticleTag 多对多关联为例,假设文章可以有多个标签,标签也可以属于多篇文章。在 Article 模型中设置关联:

class Article < ApplicationRecord
  has_and_belongs_to_many :tags
  accepts_nested_attributes_for :tags
end

Tag 模型中也设置相应关联:

class Tag < ApplicationRecord
  has_and_belongs_to_many :articles
end

在创建或编辑文章的表单中,可以这样处理标签选择:

<%= form.collection_select :tag_ids, Tag.all, :id, :name, { multiple: true } %>

这里通过 collection_select 创建一个多选下拉框,用户可以选择多个标签。在控制器中,处理标签关联参数:

class ArticlesController < ApplicationController
  def create
    @article = Article.new(article_params)
    if @article.save
      redirect_to @article, notice: 'Article was successfully created.'
    else
      render :new
    end
  end

  private
  def article_params
    params.require(:article).permit(:title, :content, tag_ids: [])
  end
end

tag_ids: [] 允许接收并处理用户选择的标签 id 数组,从而正确建立文章与标签的多对多关联。

文件上传表单

在 Rails 应用中,文件上传是常见的需求,比如用户上传头像、文档等。Rails 提供了方便的方式来处理文件上传表单。

使用 CarrierWave 进行文件上传

首先,在 Gemfile 中添加 carrierwave 宝石:

gem 'carrierwave'

然后运行 bundle install 安装宝石。接着,生成一个上传器:

rails generate uploader Avatar

这会在 app/uploaders 目录下生成一个 AvatarUploader 文件。在上传器文件中,可以定义文件的存储方式、处理规则等。例如,设置文件存储到本地文件系统,并对图片进行缩放处理:

class AvatarUploader < CarrierWave::Uploader::Base
  storage :file

  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  process resize_to_fill: [200, 200]
end

在模型中,添加上传器关联。假设 User 模型有头像字段:

class User < ApplicationRecord
  mount_uploader :avatar, AvatarUploader
end

在视图中,创建文件上传表单:

<%= form_with(model: @user, local: true, multipart: true) do |form| %>
  <div class="field">
    <%= form.label :username %>
    <%= form.text_field :username %>
  </div>
  <div class="field">
    <%= form.label :avatar %>
    <%= form.file_field :avatar %>
  </div>
  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

注意,这里 form_with 方法需要设置 multipart: true,因为文件上传表单需要使用 multipart/form - data 格式。在控制器中,处理文件上传参数:

class UsersController < ApplicationController
  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to @user, notice: 'User was successfully created.'
    else
      render :new
    end
  end

  private
  def user_params
    params.require(:user).permit(:username, :avatar)
  end
end

这样,用户上传的头像文件会被 CarrierWave 处理并存储到指定位置,模型中的 avatar 字段会保存文件的路径信息。

使用 ActiveStorage 进行文件上传

ActiveStorage 是 Rails 内置的文件上传解决方案。首先,需要在项目中安装并配置 ActiveStorage:

rails active_storage:install
rails db:migrate

在模型中,添加 ActiveStorage 关联。例如,还是以 User 模型的头像为例:

class User < ApplicationRecord
  has_one_attached :avatar
end

在视图中,创建文件上传表单:

<%= form_with(model: @user, local: true, multipart: true) do |form| %>
  <div class="field">
    <%= form.label :username %>
    <%= form.text_field :username %>
  </div>
  <div class="field">
    <%= form.label :avatar %>
    <%= form.file_field :avatar %>
  </div>
  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

同样,需要设置 multipart: true。在控制器中,处理文件上传:

class UsersController < ApplicationController
  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to @user, notice: 'User was successfully created.'
    else
      render :new
    end
  end

  private
  def user_params
    params.require(:user).permit(:username, :avatar)
  end
end

ActiveStorage 会将文件存储到配置的存储服务(如本地文件系统、Amazon S3 等),并在数据库中记录文件的相关元数据。在视图中,可以通过以下方式显示上传的文件(如头像图片):

<% if @user.avatar.attached? %>
  <%= image_tag @user.avatar.variant(resize_to_fill: [200, 200]).processed %>
<% end %>

这里通过 variant 方法对图片进行缩放处理,并使用 image_tag 显示图片。

表单的 AJAX 处理

基本的 AJAX 表单提交

在 Rails 中,使用 form_with 进行 AJAX 表单提交非常方便。只需设置 local: true,表单提交就会以 AJAX 方式进行,而不会导致页面刷新。例如,前面创建文章的表单:

<%= form_with(model: @article, local: true) do |form| %>
  <!-- 表单字段 -->
  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

当用户提交表单时,数据会以 AJAX 请求发送到服务器。在控制器中,根据请求类型进行相应处理。例如,对于 AJAX 请求,返回 JSON 格式的响应:

class ArticlesController < ApplicationController
  def create
    @article = Article.new(article_params)
    if @article.save
      if request.xhr?
        render json: { status: :success, message: 'Article was successfully created.' }
      else
        redirect_to @article, notice: 'Article was successfully created.'
      end
    else
      if request.xhr?
        render json: { status: :error, errors: @article.errors.full_messages }, status: :unprocessable_entity
      else
        render :new
      end
    end
  end

  private
  def article_params
    params.require(:article).permit(:title, :content)
  end
end

上述代码中,通过 request.xhr? 判断是否是 AJAX 请求。如果是,根据操作结果返回不同的 JSON 响应,状态码为 200 表示成功,422 Unprocessable Entity 表示验证失败。

处理 AJAX 响应

在前端,需要处理 AJAX 响应。可以使用 Rails 提供的 jquery-ujs 库,它会自动处理表单的 AJAX 提交和响应。例如,在视图中添加 JavaScript 代码来处理响应:

<script>
  $(document).on('turbolinks:load', function() {
    $('form[data-remote="true"]').on('ajax:success', function(event, data, status, xhr) {
      if (data.status ==='success') {
        alert(data.message);
        // 可以在这里进行页面更新等操作
      } else {
        alert('Error: '+ data.errors.join('\n'));
      }
    }).on('ajax:error', function(event, xhr, status, error) {
      alert('An error occurred: '+ error);
    });
  });
</script>

上述代码在页面加载(turbolinks:load 事件)时,为所有带有 data-remote="true" 的表单(form_with 设置 local: true 后会自动添加此属性)绑定 ajax:successajax:error 事件。在成功响应时,根据返回的 JSON 数据中的 statusmessage 进行相应提示;在错误响应时,显示错误信息。

实时验证与 AJAX

结合 AJAX 可以实现实时验证。例如,在用户注册表单中,当用户输入用户名后,通过 AJAX 检查用户名是否已存在。在视图中,为用户名输入框添加事件监听器:

<%= form_with(model: @user, local: true) do |form| %>
  <div class="field">
    <%= form.label :username %>
    <%= form.text_field :username, id: 'username_field' %>
    <span id="username_error" style="color: red;"></span>
  </div>
  <!-- 其他表单字段 -->
  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

<script>
  $(document).on('turbolinks:load', function() {
    $('#username_field').on('blur', function() {
      var username = $(this).val();
      $.ajax({
        url: '/users/check_username',
        method: 'GET',
        data: { username: username },
        success: function(data) {
          if (data.exists) {
            $('#username_error').text('用户名已存在');
          } else {
            $('#username_error').text('');
          }
        },
        error: function() {
          $('#username_error').text('检查用户名时出错');
        }
      });
    });
  });
</script>

在控制器中,添加检查用户名的动作:

class UsersController < ApplicationController
  def check_username
    username = params[:username]
    exists = User.where(username: username).exists?
    render json: { exists: exists }
  end
end

这样,当用户离开用户名输入框时,会通过 AJAX 请求检查用户名是否已存在,并实时显示相应的提示信息。

表单国际化

配置国际化

Rails 支持多语言表单,通过配置国际化(I18n)可以实现。首先,在 config/application.rb 中确保 config.i18n.load_path 包含正确的路径,默认情况下它会加载 config/locales 目录下的所有 .yml 文件。例如,创建一个 config/locales/en.yml 文件用于英文语言配置,config/locales/zh - CN.yml 文件用于中文简体配置。

en.yml 中,可以这样配置表单相关的文本:

en:
  activerecord:
    attributes:
      article:
        title: 'Title'
        content: 'Content'
  errors:
    messages:
      blank: 'can\'t be blank'
      too_short: 'is too short (minimum is %{count} characters)'

zh - CN.yml 中配置相应的中文文本:

zh - CN:
  activerecord:
    attributes:
      article:
        title: '标题'
        content: '内容'
  errors:
    messages:
      blank: '不能为空'
      too_short: '太短了(最少为 %{count} 个字符)'

在表单中使用国际化

在表单视图中,使用 t 方法来获取国际化文本。例如,对于文章表单:

<%= form_with(model: @article, local: true) do |form| %>
  <div class="field">
    <%= form.label :title, t('activerecord.attributes.article.title') %>
    <%= form.text_field :title %>
  </div>
  <div class="field">
    <%= form.label :content, t('activerecord.attributes.article.content') %>
    <%= form.text_area :content %>
  </div>
  <!-- 显示错误信息 -->
  <% if @article.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@article.errors.count, t('errors.messages.error')) %> prohibited this article from being saved:</h2>
      <ul>
      <% @article.errors.full_messages.each do |message| %>
        <li><%= t('errors.messages.' + message.split(' ')[0], count: message.split(' ')[2].to_i) %></li>
      <% end %>
      </ul>
    </div>
  <% end %>
  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

上述代码中,通过 t('activerecord.attributes.article.title') 获取 article 模型 title 字段的国际化标签文本。在显示错误信息时,根据错误类型动态获取相应的国际化错误信息文本,并根据需要替换占位符(如 %{count})。

通过这种方式,应用可以根据用户的语言设置,在表单中显示不同语言的文本,提升用户体验,特别是对于多语言用户群体的应用。

表单安全性

防止 CSRF 攻击

CSRF(Cross - Site Request Forgery,跨站请求伪造)是一种常见的 Web 攻击方式,攻击者通过伪造用户的请求,在用户不知情的情况下执行恶意操作。Rails 通过在表单中添加 CSRF 令牌来防止这种攻击。在视图中,当使用 form_with 创建表单时,Rails 会自动在表单中添加一个隐藏的 CSRF 令牌字段:

<%= form_with(model: @article, local: true) do |form| %>
  <!-- 表单字段 -->
  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

在提交表单时,这个 CSRF 令牌会随表单数据一起发送到服务器。在 Rails 控制器中,默认会验证 CSRF 令牌的有效性。如果验证失败,会返回 422 Unprocessable Entity 错误。如果出于某些特殊原因需要禁用 CSRF 保护,可以在控制器中使用 skip_before_action :verify_authenticity_token,但这会增加应用的安全风险,应谨慎使用。

防止 XSS 攻击

XSS(Cross - Site Scripting,跨站脚本攻击)是攻击者在网页中注入恶意脚本,当用户访问该网页时,恶意脚本会在用户浏览器中执行,从而窃取用户信息等。在 Rails 中,输出到视图的数据默认会进行 HTML 转义,以防止 XSS 攻击。例如,假设文章内容可能包含用户输入的 HTML 标签,在视图中显示文章内容时:

<%= @article.content %>

Rails 会自动将特殊字符(如 < 转换为 &lt;> 转换为 &gt;)进行转义,这样即使内容中包含恶意脚本,也不会在浏览器中执行。如果确实需要允许用户输入的内容以 HTML 格式显示,可以使用 raw 方法,但要确保对输入内容进行严格的过滤和验证。例如,使用 sanitize 方法对内容进行清理后再以 HTML 格式显示:

<%= raw sanitize(@article.content, tags: %w(a b i u p)) %>

上述代码中,sanitize 方法只允许 abiup 这几个 HTML 标签,其他标签会被过滤掉,从而降低 XSS 攻击的风险。

通过以上对 Ruby on Rails 表单处理各方面的深入探讨,包括表单的创建、字段类型、验证、关联处理、文件上传、AJAX 处理、国际化以及安全性等,开发者可以全面掌握在 Rails 应用中构建高效、安全且用户友好的表单的方法,为开发高质量的 Web 应用奠定坚实基础。