重学rails: rails8 开发一个blog

学习官方教程
更新于: 2024-10-18 09:57:37

2条哲学

The Rails philosophy includes two major guiding principles:

  • Don't Repeat Yourself(永远不要重复自己): DRY is a principle of software development which states that "Every piece of knowledge must have a single, unambiguous, authoritative representation within a system". By not writing the same information over and over again, our code is more maintainable, more extensible, and less buggy.
  • Convention Over Configuration(约定优于配置): Rails has opinions about the best way to do many things in a web application, and defaults to this set of conventions, rather than require that you specify minutiae through endless configuration files.

devcontainer

Rails 官方提供的 docker 方式运行 rails 开发

环境配置

ruby 3.2.2
rails 8.0.0.beta1

# 启动服务
bin/rails server

Hello Rails

# 生成 Article 的 controller,跳过路由
bin/rails generate controller Articles index --skip-routes

手动路由

文件在这里 config/routes.rb 

Rails.application.routes.draw do
  root "articles#index"
  get "/articles", to: "articles#index"
end

Autoloading

# 如果不希望,则用这种方式配置
gem "bootsnap", require: false

生成 Model

  • 第1条: 生成
  • 第2条: 迁移
bin/rails generate model Article title:string body:text
bin/rails db:migrate

约定

  • singular
Model names are singular, because an instantiated model represents a single data record. To help remember this convention, think of how you would call the model's constructor: we want to write Article.new(...), not Articles.new(...).

模型名称是单数,因为实例化的模型表示单个数据记录。为了帮助记住这个约定,想想你会如何调用模型的构造函数:我们想写Article.new(…),而不是Article.now(…)。

rails console

直接利用这个特性,与 db 交互,有提示: Tab

bin/rails console

新建-保存

article = Article.new(title: "Hello Rails", body: "I am on Rails!")
article.save

查询

  • 单个/多个
Article.find(1)
Article.all

Controller 中为 article 提供数据

class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end
end

View在HTML中显示

<h1>Articles</h1>

<ul>
  <% @articles.each do |article| %>
    <li>
      <%= article.title %>
    </li>
  <% end %>
</ul>

CURD

  • 详情路由:   get "/articles/:id", to: “articles#show”
  • 利用 view helper 简化 html 的书写
  • 一次生成7个路由:   resources :articles
原来的等效写法
<a href="/articles/<%#= article.id %>">
<a href="<%#= article_path(article) %>">
<a href="/articles/<%= article.id %>">
  <%= article.title %>
</a>
<%= link_to article.title, article %>

Strong Parameters

提交的表单数据与捕获的路由参数一起放入params哈希中。因此,创建操作可以通过params[:article][:title]访问提交的标题,并通过params[:article][:body]访问已提交的正文。我们可以将这些值单独传递给Article.new,但这会很冗长,而且可能容易出错。随着我们添加更多字段,情况会变得更糟。
相反,我们将传递一个包含这些值的哈希。但是,我们仍然必须指定该哈希中允许的值。否则,恶意用户可能会提交额外的表单字段并覆盖私人数据。事实上,如果我们将未过滤的参数[:article]哈希直接传递给article.new,Rails将引发一个ForbiddenAttributesError来提醒我们这个问题。因此,我们将使用Rails的一个名为强参数的功能来过滤参数。把它看作是对参数的强输入。
让我们在app/controllers/articles_controller.rb的底部添加一个名为article_params的私有方法,用于过滤
  private
    def article_params
      params.expect(article: [:title, :body])
    end

Model 校验

class Article < ApplicationRecord
  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }
end

对应 View 显示错误信息

<h1>New Article</h1>

<%= form_with model: @article do |form| %>
  <div>
    <%= form.label :title %><br>
    <%= form.text_field :title %>
    <% @article.errors.full_messages_for(:title).each do |message| %>
      <div><%= message %></div>
    <% end %>
  </div>

  // ...
  <div>
    <%= form.submit %>
  </div>
<% end %>

Destroy

这里,官方教程的写法,需要借助: gem ‘turbo-rails’,详情看这里

不过,这里,应该不用引入这个框架,所以,直接用 button_to(submit) 表单的思路完成。

# In Gemfile
gem 'turbo-rails'

# In application.js
import "@hotwired/turbo-rails"

# Run Commands
$ bin/bundle install
$ bin/rails turbo:install
$ bin/rails server
# 这个可以
<li><%= button_to "Destroy", @article, method: :delete %></li>
# 官方教程里的下面这个有问题,不行
<li><%= link_to "Destroy", article_path(@article), data: {
                    turbo_method: :delete,
                    turbo_confirm: "Are you sure?"
                  } %></li>

常用的 Ruby status

HTTP_STATUS_CODES = {
  100 => 'Continue',
  101 => 'Switching Protocols',
  102 => 'Processing',
  103 => 'Early Hints',
  200 => 'OK',
  201 => 'Created',
  202 => 'Accepted',
  203 => 'Non-Authoritative Information',
  204 => 'No Content',
  205 => 'Reset Content',
  206 => 'Partial Content',
  207 => 'Multi-Status',
  208 => 'Already Reported',
  226 => 'IM Used',
  300 => 'Multiple Choices',
  301 => 'Moved Permanently',
  302 => 'Found',
  303 => 'See Other',
  304 => 'Not Modified',
  305 => 'Use Proxy',
  307 => 'Temporary Redirect',
  308 => 'Permanent Redirect',
  400 => 'Bad Request',
  401 => 'Unauthorized',
  402 => 'Payment Required',
  403 => 'Forbidden',
  404 => 'Not Found',
  405 => 'Method Not Allowed',
  406 => 'Not Acceptable',
  407 => 'Proxy Authentication Required',
  408 => 'Request Timeout',
  409 => 'Conflict',
  410 => 'Gone',
  411 => 'Length Required',
  412 => 'Precondition Failed',
  413 => 'Content Too Large',
  414 => 'URI Too Long',
  415 => 'Unsupported Media Type',
  416 => 'Range Not Satisfiable',
  417 => 'Expectation Failed',
  421 => 'Misdirected Request',
  422 => 'Unprocessable Content',
  423 => 'Locked',
  424 => 'Failed Dependency',
  425 => 'Too Early',
  426 => 'Upgrade Required',
  428 => 'Precondition Required',
  429 => 'Too Many Requests',
  431 => 'Request Header Fields Too Large',
  451 => 'Unavailable For Legal Reasons',
  500 => 'Internal Server Error',
  501 => 'Not Implemented',
  502 => 'Bad Gateway',
  503 => 'Service Unavailable',
  504 => 'Gateway Timeout',
  505 => 'HTTP Version Not Supported',
  506 => 'Variant Also Negotiates',
  507 => 'Insufficient Storage',
  508 => 'Loop Detected',
  511 => 'Network Authentication Required'
}

第2个Model

  • 1 个 article 可以有多个 comments: (1-N: article → commnets) 
  • g model: article:references
  • 1 方是被 reference 的对象
bin/rails generate model Comment commenter:string body:text article:references

基本概念

  • 主键(Primary Key): 每个表都有一个主键,用于唯一标识表中的每一条记录。通常是 id 字段。
  • 外键(Foreign Key): 一个表中的字段,它引用另一个表的主键,用于建立两个表之间的关联关系。

关键部分解释:

  • t.references :article, null: false, foreign_key: true
    • t.references :article:创建一个 article_id 整数字段。
    • null: false:确保 article_id 不能为空,每条评论必须关联到一篇文章。
    • foreign_key: true:在数据库层面添加外键约束,确保 article_id 必须对应 articles 表中存在的 id

生成 controller

  • controller 是复数
bin/rails generate controller Comments

redirect_to Vs Render

redirect_to将导致浏览器发出新的请求,而render将呈现当前请求的指定视图。
在更改数据库或应用程序状态后使用 redirect_to 非常重要。
否则,如果用户刷新页面,浏览器将发出相同的请求,并且将重复突变。

render 常见用途

在同一请求中显示不同的视图:如在表单验证失败时,重新渲染表单以显示错误信息。
渲染部分视图:使用局部(partial)渲染页面的一部分内容。
API 返回:在 API 控制器中,直接渲染 JSON 或 XML 数据。
示例:

# ruby
复制代码
class ArticlesController < ApplicationController
  def create
    @article = Article.new(article_params)
    if @article.save
      redirect_to @article, notice: '文章创建成功!'
    else
      render :new  # 表单验证失败,重新渲染 new 视图
    end
  end
end

redirect_to 常见用途

创建-重定向-显示模式(Post/Redirect/Get):在处理表单提交后,使用重定向避免表单重复提交问题。
用户认证:在用户登录后重定向到仪表盘或首页。
资源操作后:如成功创建、更新或删除资源后,重定向到相应的页面(如 show 页面)。
示例:

# ruby
复制代码
class ArticlesController < ApplicationController
  def create
    @article = Article.new(article_params)
    if @article.save
      redirect_to @article, notice: '文章创建成功!'
    else
      render :new
    end
  end
end

Comments → partial

原来的使用 partial
# ./views/articles/show.html.erb

<h2>Comments</h2>
<% @article.comments.each do |comment| %>
  <p>
    <strong>Commenter:</strong>
    <%= comment.commenter %>
  </p>

  <p>
    <strong>Comment:</strong>
    <%= comment.body %>
  </p>
<% end %>
# ./views/comments/_comment.html.erb
<p>
  <strong>Commenter:</strong>
  <%= comment.commenter %>
</p>

<p>
  <strong>Comment:</strong>
  <%= comment.body %>
</p>

# ./views/articles/show.html.erb
<h2>Comments</h2>
<%= render @article.comments %>

Using Concerns

  • 让 controller/model 更方便管理
  • 主要涉及以下目录
app/controllers/concerns
app/models/concerns

给 post/comment 添加 status

  • 会用到上面的 concern 功能
博客文章可能有各种状态 - 例如,它可能对所有人可见(即public),或仅对作者可见(即private)。
它也可能对所有人隐藏但仍可检索(即)。评论也可能隐藏或可见。这可以使用每个模型中的一列archived来表示。

添加状态到db

  • add status filed
  • set default value: public
# 添加 status 字段
bin/rails generate migration AddStatusToArticles status:string
bin/rails generate migration AddStatusToComments status:string
bin/rails db:migrate
# 给 status 添加默认值
bin/rails generate migration AddDefaultToStatusInArticles

class AddDefaultToStatusInArticles < ActiveRecord::Migration[6.0]
  def change
    change_column_default :articles, :status, 'public'
  end
end

rails 教程是 link_to 生效

app/views/layout.html.rb 中添加以下行

<%= javascript_include_tag "turbo", type: "module" %>

删除关联对象

如果删除文章,则其相关评论也需要删除,否则它们只会占用数据库空间。Rails 允许您使用dependent关联选项来实现这一点。修改 Article 模型,app/models/article.rb如下所示:

class Article < ApplicationRecord
  include Visible

  has_many :comments, dependent: :destroy

  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }
end