接上文前端學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> 關注 <%= @article.user.username %>
<% end %>
<% end %>
在上述示例中,我們通過 button_to 方法創建了一個鏈接,當用户點擊該鏈接時,會向 follow_user_path 路徑發送 POST 請求,並將 remote 參數設置為 true,以便在不刷新整個頁面的情況下完成請求(ajax請求)
在 profiles 控制器中定義 follow、unfollow 動作,用於處理關注和取消關注事件,同時返回 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? 方法和控制器層的 follow、unfollow 方法我們都去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>
如下所示:
但是樣式還是默認樣式,我們用 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:全棧論壇(地宮)項目一