動態

詳情 返回 返回

前端學Ruby:全棧論壇(地宮)項目二 - 動態 詳情

接上文前端學Ruby:全棧論壇(地宮)項目一,休息一晚後,我們繼續

各個模型建立了我們想要的

筆者是前端出身,對數據庫的理解僅限於用 node + mysql (mongodb)做過微型博客。除此之外,數據庫的知識點就無了,以下寫的不好的,多多擔待

文章模型與用户模型結合

文章模型與用户模型的結合,一個人必須要先登錄後才能寫文章,其次,一個人可以有很多文章,但當他註銷後,文章就沒了

先在 article model 中創建一個 user_id,將它指向 user model

rails g migration add_user_id_to_articles user_id:integer:index

app/models/article.rb 中加上:

class Article < ApplicationRecord
    belong_to :user
end

app/models/user.rb 中加上:

class User < ApplicationRecord
  # 意為一個人有很多文章,當人不在時,文章也就沒了
  + has_many :articles, dependent: :destroy

end

這時,在文章詳情頁,可以通過 @article.user 來獲取這篇文章對應的用户信息:

<h2><%= @article.title %></h2>
<p><%= @article.content %></p>
<p>Written by <%= @article.user.name %></p>

當然,如果你想獲取一個用户所寫的所有文章,則是在個人頁,找到用户後,就能展示:

<% @user.articles.each do |article| %>
    <h2><%= article.title %></h2>
    <p><%= article.body %></p>
<% end %>

轉換日期

將 create_at 轉換為 ”March 28, 2023“ 這種格式

用 Ruby 的 strftime 方法

<%= @article.created_at.strftime("%B %d, %Y") %>
  • %B 表示月份的全名
  • %d 表示日期(兩位數)
  • %Y 表示四位數的年份

建立評論model

建立 comment model

rails g model Comment body:text article:references user:references

遷移數據庫

rails db:migrate

在建立 model 時,models/comment 就 belongs_to 文章和用户,即

class Comment < ApplicationRecord
  belongs_to :article
  belongs_to :user
end

所以我們需要在文章模型和用户模型中都加一下擁有多個評論

class User < ApplicationRecord
    ...
      has_many :articles, dependent: :destroy
  + has_many :comments, dependent: :destroy

end
class Article < ApplicationRecord
    belongs_to :user

    + has_many :comments, dependent: :destroy
end

Comment 模型和 Article 和 User 模型已經關聯好了

現在我們創建 comment 控制器

rails g controller comment

rails 會幫忙生成controller、view、helper 等文件,這裏我們只用到app/controllers/comments_controller,在應用中,我們的文章頁面下會有評論,所以不單獨做頁面

我們前往config/routes.rb ,在 articles 下新增 resources :comments

  resources :articles do
    + resources :comments
  end

這是符合 restful 風格的,如果嚴格一點,再加上 only: [:create, :destroy],只允許創建和刪除,其他的接口不開放。回到最重要的 comments_controller 處,我們需要新增 create 和 destroy 方法,這裏筆者嘗試了一段時間不得解,還好藉助 chatgpt 幫忙渡過,真乃神器

class CommentsController < ApplicationController
    before_action :authenticate_user!
    before_action :set_article!, only: %i[create destroy]

    def create
        @comment = @article.comments.create(comment_params)
        redirect_to article_path(@article)
    end

    def destroy
        @comment = @article.comments.find(params[:id])
        @comment.destroy
        redirect_to article_path(@article)
    end

    private

    def set_article!
        @article = Article.find(params[:article_id])
    end

    def comment_params
        params.require(:comment).permit(:body).merge(user: current_user)
    end

end

其中 @comment = @article.comments.create(comment_params) 這行代碼很有趣,讀起來像英文,在文章的 comment 中創建一個評論,其中 comment_params 中有 merge(user: current_user) 意為當前用户

Relationship 模型

一個用户可以關注別人,可以取關別人,別人也可以關注他,也可以去管他。用户之間的關注是多對多,筆者解釋不了為什麼再建一個表來關聯兩個用户,也許是性能,也許是結構,總之,筆者失敗過,稚嫩的臉龐上多過一道淚痕

我們沒必要創建 Relationship model 文件,直接創建遷移文件即可:

rails g migration CreateRelationship

修改遷移文件

class CreateRelationship < ActiveRecord::Migration[7.0]
  def change
    create_table :relationships do |t|
      t.integer :follower_id
      t.integer :following_id

      t.timestamps
    end

    change_column_null :relationships, :follower_id, true
    change_column_null :relationships, :following_id, true
    add_index :relationships, :follower_id
    add_index :relationships, :following_id
  end
end

遷移數據

rails db:migrate

因為關注是和用户有關,所以我們前往models/user 模型,加入 relationships 與 user 的關聯

  
  has_many :articles, dependent: :destroy
  has_many :comments, dependent: :destroy
 
 + has_and_belongs_to_many :following,
 + class_name: 'User',
 + join_table: 'relationships',
 + foreign_key: 'follower_id',
 + association_foreign_key: 'following_id'

 + has_and_belongs_to_many :followers,
 + class_name: 'User',
 + join_table: 'relationships',
 + foreign_key: 'following_id',
 + association_foreign_key: 'follower_id'

模型建好了,接着弄 config/routes,文檔 上寫的很清楚,他是在 profiles 路由下的動作,所以我們修改:

- get '/:name', to: 'profile#show', as: :profile

+  scope :profiles do
+    get ':username', to: 'profiles#show'
+    post ':username/follow', to: 'profiles#follow'
+    delete ':username/follow', to: 'profiles#unfollow'
+  end

前往視圖層:

<% if current_user.following?(@article.user) %>
    <%= button_to unfollow_user_path(@article.user.username), method: :delete, remote: true,
     form_class: "d-inline-block", class: "btn btn-sm btn-outline-secondary", id: "unfollow-button" do %>
        取消關注 <%= @article.user.username %>
    <% end %>
<% else %>
    <%= button_to follow_user_path(@article.user.username), method: :post, remote: true,
    form_class: "d-inline-block", class: "btn btn-sm btn-outline-secondary", id: "follow-button" do %>
        <i class="fa-solid fa-plus"></i>&nbsp;關注 <%= @article.user.username %>
    <% end %>
<% end %>

在上述示例中,我們通過 button_to 方法創建了一個鏈接,當用户點擊該鏈接時,會向 follow_user_path 路徑發送 POST 請求,並將 remote 參數設置為 true,以便在不刷新整個頁面的情況下完成請求(ajax請求)

在 profiles 控制器中定義 followunfollow 動作,用於處理關注和取消關注事件,同時返回 JS 視圖

class ProfilesController < ApplicationController
    before_action :authenticate_user!, except: [:show]
    before_action :set_profile

    def show
    end

    def follow
        current_user.follow @user

        respond_to do |format|
            format.js
        end
    end

    def unfollow
        current_user.unfollow @user

        respond_to do |format|
            format.js
        end
    end

    private
    
    def set_profile
        @user = User.find_by_username(params[:username])
    end

end

其中,視圖層中的following? 方法和控制器層的 followunfollow 方法我們都去user 模型中定義

...
    def following?(other_user)
        following.include?(other_user)
    end

    def follow(user)
        following << user unless following.include? user   
    end

    def unfollow(user)
        following.delete(user)
    end
...
這裏,筆者沒有弄出 format.js ,因為加上後也沒有效果,如果機會,會補上這塊,也就是當點擊關注後,接口請求成功後頁面彈出 已關注,取消關注後,頁面彈出已取消

like 模型

按照上述的經驗,我們知道了,如果是多對多,就需要建立一箇中間表來存儲兩者之間的關係。如果要做某個用户給某篇文章點贊呢?也屬於多對多關係,

基於 articles 和 user 模型建立新模型 Like:

# 創建 migration 文件
rails g model Like article:references user:references
# 運行 migration
rails db:migrate

前往config/routes:

resources :articles do
    resources :comments, only: [:create, :destroy]
    
    member do
      post 'like'
      delete 'unlike'
    end
end

再去 app/models/article.rb 模型中,新增方法

class Article < ApplicationRecord
    belongs_to :user

    has_many :comments, dependent: :destroy
    + has_many :likes, dependent: :destroy
    
    + def liked_by?(user)
    +    likes.where(user_id: user.id).exists?
    + end
end

再去控制器新增 like 和 unlike 方法

 before_action :set_article, only: %i[ show edit update destroy like unlike ]

 def like
    unless @article.liked_by?(current_user)
      @like = @article.likes.create(user_id: current_user.id)
    end
    respond_to do |format|
        format.js
    end
  end

  def unlike
    if @article.liked_by?(current_user)
      @like = @article.likes.find_by(user_id: current_user.id)
      @like.destroy
    end
    respond_to do |format|
        format.js
    end
  end

其實,這個和 follow 很像,都是多對多的

標籤模型

創建標籤模型,它屬於文章模型

建立一個多對多關係,一篇文章有多個標籤,一個標籤下有多篇文章

# 創建 Tag model
rails g model Tag name:string
# 修改 Article 模型文件。在 app/models/article.rb 文件中,添加以下代碼
class Article < ApplicationRecord
  has_and_belongs_to_many :tags
end
# 修改 Tag 模型文件。在 app/models/tag.rb 文件中,添加以下代碼
class Tag < ApplicationRecord
  has_and_belongs_to_many :articles
end
# 創建 articles_tags 關係表
rails g migration CreateJoinTableArticlesTags articles tags
# 運行 migration
rails db:migrate

如此,我們就建立起了多對多的關係

代碼方面筆者踩了一下坑,首先要在 models/article 層注入:

# 用於 view 層
def tag_list
    tags.map(&:name).join(",")
end
# 用於 controller 層
def sync_tags(tag_list)
    tagArr = JSON.parse(tag_list)
    tagArr.each do |tag_name|
        tag = Tag.find_or_create_by(name: tag_name)

        tags << tag
    end
end

前往 controllers/articles_controller.rb 注入:

def create
  @article = current_user.articles.new(article_params.except(:tag_list))

    respond_to do |format|
      if @article.save
        @article.sync_tags(article_params[:tag_list])
        ...
      else
        ...
      end
    end
end

def article_params
    # 新增 tag_list 變量
    params.require(:article).permit(:title, :description, :body, :tag_list)
end

再回到views/articles 層,在 body 下加入相關 tag 代碼

...
<div class="form-group mt-3">
    <%= f.hidden_field :tag_list, id: 'tag-input' %>
    <input 
      id="tag-field"
      class="form-control" 
      type="text"
      placeholder="輸入標籤" 
      onkeydown="addToList(event)"
      >

    <div class="tag-list mt-1" id="tag-list">
    </div>
</div>

當然,還有 js 代碼,就不貼了,邏輯是,輸入標籤後回車,生成一個標籤

受歡迎的標籤,我們要通過查詢來找到前十的

# 獲取最受歡迎的十大標籤
tag_counts = Tag.joins(:articles_tags).group(:tag_id).order('count_all desc').limit(10).count
popular_tag_ids = tag_counts.keys
@popular_tags = Tag.where(id: popular_tag_ids).sort_by { |t| popular_tag_ids.index(t.id) }

查詢功能

既然喜歡刺激,那就進行到底

既然做到這個份上了,那就把剩下的功能給補齊,這也是筆者最菜的地方——ORM

先補上slug,在文章詳情中,我們是通過 id 來查詢文章,這樣不安全。我們可以用隨機字符串,這裏我們使用標題來作為我們查詢點,專業術語叫“slug”,指「字符串轉換成合法的URL路徑的過程」

先在 artilce model 中增加字段,然後再遷移數據

# 創建 migration 文件
rails g migration addSlugToArticle slug:string
# 修改 migration 文件,添加搜索索引
class AddSlugToArticle < ActiveRecord::Migration[7.0]
  def change
    add_column :articles, :slug, :string
  end

  + add_index :articles, :slug
end
# 運行 migration
rails db:migrate

前往conf/routes,在resources :articles 後加上 param: :slug

+ resources :articles, param: :slug do
    resources :comments, only: [:create, :destroy]

    member do
      post 'like'
      delete 'unlike'
    end
    
  end

將類似<%= link_to article ...%> 的地方改成 <%= link_to article_path(article.slug) ,至於 sync_tags,我們因為有修改標籤的操作,所以有標籤時,更新原來的標籤列表,但是筆者説過,操作數據庫或者説 rails 相關的 api 接觸的太少,所以筆者先把標籤清空,再將新的標籤放進去,也許會影響性能,但又有什麼辦法

def sync_tags(tag_list)
    tagArr = JSON.parse(tag_list)
    # 如果已經有標籤,刪除原有標籤
    if tags.any?
        tags.destroy_all
    end
    tagArr.each do |tag_name|
        tag = Tag.find_or_create_by(name: tag_name)

        tags << tag
    end
end

訂閲功能

到現在,我們已經完成了一個小論壇的基本雛形,現在,補上論壇中最重要的一點,訂閲

def feed
    user =  User.find(current_user.following_ids)
    @articles = Article.order(created_at: :desc).where(user:user).includes(:user)
end

分頁功能

分頁應該有很多 gem 庫,從Rails 談談 Rails 中的分頁 - 簡易版 知道兩個庫,kaminari 和 pagy 。兩者相比, kaminari 更簡單,pagy 複雜一點但性能更好,這裏我以 kaminari 為例繼續我的論壇項目

先加上 gem

gem 'kaminari'

再安裝它

bundle

生成默認配置

rails g kaminari:config

此時會生成 config/initializers/kaminari_config.rb ,我們修改配置

# frozen_string_literal: true

Kaminari.configure do |config|
  config.default_per_page = 5 # 修改它,默認為25,將其修改為5做測試用
  # config.max_per_page = nil
  # config.window = 4
  # config.outer_window = 0
  # config.left = 0
  # config.right = 0
  # config.page_method_name = :page
  # config.param_name = :page
  # config.max_pages = nil
  # config.params_on_first_page = false
end

在 controller 層修改

def index
- @articles = Article.order(created_at: :desc).includes(:user)
+ @articles = Article.page(params[:page]).order(created_at: :desc).includes(:user)
end

在 view 層加入

<% @articles.each do |article| %>
  <%= render article %>
<% end %>
+<div class="text-center">
+  <%= paginate @articles %>
+</div>

如下所示:

使用kaminari

但是樣式還是默認樣式,我們用 bootstrap5,所以儘量也用相關的UI,於是在 RubyToolbox 上找到了 bootstrap5-kaminari-views ,按照 demo 使用

<div class="text-center">
  +<%= paginate @articles, theme: 'bootstrap-5',
  +pagination_class: "flex-wrap justify-content-center" %>
</div>

樣式是好了,但還是是英文的,所以還需要按照 i18n,所以還要安裝 kaminari-i18n,安裝好 kaminari-i18n,UI 就成了我們想要的樣子了

圖片

再次部署

我們還是在 fly.io 中部署,分兩步,一是將項目重新部署下,二是遷入數據

# 實例化應用
fly launch
# 部署應用
fly deploy
# 打開應用
fly open

如此,我們能看到頁面,但是因為創建的數據庫沒導入,所以會報錯,我們需要遷入數據

# 進入控制枱
flyctl ssh console
# 遷入數據
bin/rails db:migrate

這時, https://underground-palace.fly.dev 就能正常訪問

Logo設計

在項目初期階段,完全不用擔心 logo 的事情,沒人會在意你,你要做的就是做個可以看的logo貼上去,如果在 logo 上花費太多時間,得不償失

筆者習慣在 favicon.io 中找 emoji 來做logo,這次也是,看到合適的,下載,然後把文件拉到 public 中即可

後記

我當然知道,如果要做一個完整的項目,以上這些是不夠的,還要有更考究的UI、交互,還要加上搜索,靜態資源的中文化、錯誤提示的中文化等等。但,那又怎麼樣呢

系列文章

  • 前端學Ruby:前言
  • 前端學 Ruby:安裝Ruby、Rails
  • 前端學 Ruby:熟悉 Ruby 語法
  • 前端學 Ruby:熟悉Rails
  • 前端學 Ruby:唐詩API項目
  • 前端學 Ruby:唐詩項目部署優化
  • 前端學Ruby:全棧論壇(地宮)項目一
user avatar front_yue 頭像 sunplay 頭像 u_17443142 頭像 zzd41 頭像 shumile_5f6954c414184 頭像 Z-HarOld 頭像 libubai 頭像 huangmingji 頭像 weishiledanhe 頭像 best-doraemon 頭像 dalideshoushudao 頭像 happy2332333 頭像
點贊 57 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.