乐趣区

关于后端:前端学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>

如下所示:

然而款式还是默认款式,咱们用 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:全栈论坛(地宫)我的项目一
退出移动版