ساخت اپلیکیشن اینستاگرام با Ruby On Rails (بخش دوم) — از صفر تا صد

۸۸ بازدید
آخرین به‌روزرسانی: ۲۴ مهر ۱۴۰۱
زمان مطالعه: ۱۰ دقیقه
ساخت اپلیکیشن اینستاگرام با Ruby On Rails (بخش دوم) — از صفر تا صد

در بخش قبلی این نوشته به بررسی مقدمات مورد نیاز برای طراحی اپلیکیشن اینستاگرام پرداختیم. در این نوشته برخی موارد دیگر که شامل آپلود عکس برای پست و همین طور آواتار کاربران است را معرفی می‌کنیم.

مواردی که در این نوشته بررسی خواهند شد شامل فهرست زیر هستند:

  • ایجاد مدل پست
  • ارتباط بین کاربر و مدل‌های پست
  • استفاده از Active Storage برای الصاق یک تصویر به هر پست
  • ایجاد یک پست جدید و افزودن اعتبارسنجی پست
  • نمایش پست‌ها روی صفحه اصلی و صفحه پروفایل کاربران
  • استفاده از Kaminari gem برای صفحه‌بندی پست‌ها
  • آپلود آواتار برای کاربر

ایجاد مدل پست

مدل‌ها در Rails از یک اسم مفرد و جداول متناظر آن‌ها در پایگاه داده از یک اسم جمع استفاده می‌کنند. برای ایجاد مدل Post باید از generator در Rails استفاده کنیم و دستور زیر را در ترمینال وارد نماییم:

rails generate model Post description:string user_id:integer

دستور فوق دسته‌ای از فایل‌ها را ایجاد می‌کند:

Running via Spring preloader in process 5011
      invoke  active_record
      create    db/migrate/20180922091640_create_posts.rb
      create    app/models/post.rb
      invoke    test_unit
      create      test/models/post_test.rb
      create      test/fixtures/posts.yml

اکنون باید 2 فایل به نام‌های app/models/post.rb و db/migrate/20180922091640_create_posts.rb را در نظر بگیریم.

ابتدا فایل db/migrate/20180922091640_create_posts.rb را در ویرایشگر متنی باز کنید:

class CreatePosts < ActiveRecord::Migration[5.1]
  def change
    create_table :posts do |t|
      t.string :description
      t.integer :user_id

      t.timestamps
    end
  end
end

این فایل یک migration است که جدول Posts را با 2 ستون به نام های description (نوع string) و user_id (نوع integer) ایجاد می‌کند.

Migration فوق را اجرا می‌کنیم تا جدول Posts ایجاد شود:

rails db:migrate

و نتیجه به صورت زیر است:

== 20180922091640 CreatePosts: migrating ======================================
-- create_table(:posts)
   -> 0.0404s
== 20180922091640 CreatePosts: migrated (0.0405s) =============================

برای مطالعه موارد بیشتر در مورد migration می‌توانید به این آدرس (+) مراجعه کنید.

مدل Post را در فایل app/models/post.rb باز کنید:

class Post < ApplicationRecord
end

این همان مدل پست است که به جدول Posts در پایگاه داده ما نگاشت شده است.

افزودن ارتباط بین مدل‌های کاربر (User) و پست (Post)

در طراحی پایگاه داده ما، هر کاربر پست‌های زیادی می‌تواند داشته باشد و از این رو باید بین کاربر و پست‌ها ارتباطی ایجاد کنیم. بدین ترتیب عملیات معمول در کد ساده و آسان‌تر می‌شود.

ویژگی پشتیبانی Active Record موجب شده است که اعلان ارتباط‌ها ساده‌تر صورت بگیرد. بدون وجود ارتباط وضعیت به صورت زیر است:

class User < ApplicationRecord
end

class Post < ApplicationRecord
end

اما پس از اعلان یک ارتباط به صورت زیر در می‌آید:

class User < ApplicationRecord
  has_many :posts, dependent: :destroy
end

class Post < ApplicationRecord
  belongs_to :user
end

ارتباط has_many نشان دهنده یک اتصال «یک به چند» با مدل دیگر است. و گزینه dependent::destroy نشان دهنده این است که همه پست‌های مرتبط زمانی که کاربر حذف شود باید از بین بروند.

معرفی Active Storage

Active Storage یکی از ویژگی‌های عالی نسخه 5.2 Rails است. این ویژگی برای آپلود فایل‌ها به یک سرویس ذخیره‌سازی ابری مانند Amazon S3، Google Cloud Storage یا Microsoft Azure Storage معرفی شده است و این فایل‌ها را به شی‌ءهای Active Storage الصاق می‌کند.

Active Storage به وسیله یک سرویس local disk-based برای توسعه و تست کردن ارائه شده است. برای راه‌اندازی Active Storage باید دستور زیر را اجرا کنیم:

rails active_storage:install

یک فایل migration مانند db/migrate/20180924134051_create_active_storage_tables.active_storage.rb ایجاد می‌کنیم و سپس دستور rails db:migrate را برای انجام مهاجرت اجرا می‌کنیم:

$ rails db:migrate

== 20180924134051 CreateActiveStorageTables: migrating ============ 
 — create_table(:active_storage_blobs)
 -> 0.0576s
 — create_table(:active_storage_attachments)
 -> 0.0106s
== 20180924134051 CreateActiveStorageTables: migrated (0.0684s) ====

این migration 2 جدول به نام‌های active_storage_blobs و active_storage_attachments ایجاد می‌کند که Active Storage از آن‌ها برای ذخیره‌سازی فایل‌ها استفاده می‌کند.

پیکربندی سرویس‌هایی برای ذخیره‌سازی فایل‌ها

Active Storage سرویس‌هایی در config/storage.yml اعلان می‌کند. این فایل را باز کنید تا پیکربندی پیش‌فرض را به صورت زیر مشاهده کنید:

test:
  service: Disk
  root: <%= Rails.root.join("tmp/storage") %>

local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

این پیکربندی بدان معنی است که اپلیکیشن ما دو سرویس به نام های test و local دارد. هر دو این سرویس‌ها از سرویس Disk استفاده می‌کنند. شما می‌توانید از سرویس‌های دیگری نیز استفاده کنید. برای مثال می‌توانیم سرویس جدیدی با نام amazon به صورت زیر به فایل config/storage.yml اضافه کنیم:

amazon:
 service: S3
 access_key_id: your_access_key_id
 secret_access_key: your_secret_access_key
 region: us-east-1
 bucket: your_own_bucket

در اغلب موارد هر محیط از یک سرویس متفاوت استفاده می‌کند. در محیط توسعه می‌توانیم از سرویس Disk با افزودن کد پیکربندی به config/environments/development.rb استفاده کنیم:

# Store uploaded files on the local file system
config.active_storage.service =:local

برای استفاده از سرویس Amazon S3 در محیط production می‌توانید کد زیر را به فایل config/environments/production.rb اضافه کنید:

# Store uploaded files on Amazon S3
config.active_storage.service =:amazon

هنگام به‌روزرسانی پیکربندی در هر محیط باید سرور را ری‌استارت کنید.

استفاده از Active Storage برای ذخیره‌سازی تصاویر پست‌ها

در این بخش به بررسی عملی روش استفاده از Active Storage برای ذخیره‌سازی تصاویر مربوط به هر پست می‌پردازیم.

الصاق تصویر به پست

ما از ماکروی has_one_attached برای راه‌اندازی رابطه یک به یک بین Post و Image استفاده می‌کنیم. هر پست می‌تواند یک تصویر الصاق شده داشته باشد.

class Post < ApplicationRecord
  belongs_to :user

  has_one_attached :image
end

ایجاد یک پست جدید

تصاویر پست‌ها در اپلیکیشن ما یک کارکرد اصلی محسوب می‌شود. هر پست شامل یک تصویر، توضیح و کاربری که پست را ایجاد کرده خواهد بود.

گردش کار ایجاد یک Post جدید به صورت زیر است:

  • ایجاد یک کنترلر Post
  • افزودن اکشن Create به کنترلر پست
  • افزودن یک Form برای ایجاد Post

ایجاد کنترلر Post

rails generate controller Posts
   >>
      create  app/controllers/posts_controller.rb
      invoke  erb
      create    app/views/posts
      invoke  test_unit
      create    test/controllers/posts_controller_test.rb
      invoke  helper
      create    app/helpers/posts_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/posts.coffee
      invoke    scss
      create      app/assets/stylesheets/posts.scss

افزودن اکشن create

ما قصد داریم یک اکشن Create به کنترلر Post اضافه کنیم:

def create
  Post.create(post_params)

  redirect_to root_path
end

private

def post_params
  params.require(:post).permit(:description, :images, :user_id)
end

این اکشن یک پست جدید را از روی پارامترها می‌سازد و صفحه را بارگذاری مجدد می‌کند (در واقع به صفحه اصلی بازهدایت می‌کند).

افزودن یک فرم برای ایجاد Post جدید به صفحه اصلی

کد HTML فرم ما به صورت زیر خواهد بود:

<%= form_for Post.new do |f| %>
  <div class="form-group">
    <%= f.text_field :description %>
  </div>
  <div class="form-group">
    <%= f.file_field :images %>
  </div>
  <div class="form-group">
    <%= f.text_field :user_id, value: current_user.id, class:'d-none'%>
  </div>
  <br>
  <div class="text-center">
    <%= f.submit 'Create Post', class: 'btn btn-primary' %>
  </div>
<% end %>

ما از متد کمکی file_field برای آپلود تصویر استفاده می‌کنیم.

<%= f.file_field:images%>

user_id آن کاربری که پست را ایجاد می‌کند یعنی current_user است و این فیلد را در فرم با استفاده از کلاس CSS به نام d-name پنهان می‌کنیم:

<%= f.text_field:user_id, value: current_user.id, class:'d-none' %>

مسیر اکشن‌های new و create را به Post اضافه می‌کنیم:

resources:posts, only: [:new,:create]

کد CSS به صورت زیر است:

.form-upload{
  border: 1px solid #dbdbdb;
  height: auto;
  margin: auto 160px;
  padding: 30px;
  border-radius: 3px;

  input[type='text']{
    width: 100%;
  }
}
فرم ایجاد یک پست جدید
فرم ایجاد یک پست جدید

افزودن اعتبارسنجی به Post

در حال حاضر ما یک باگ کوچک داریم، یعنی زمانی که فرم ایجاد پست ارسال می‌شود، امکان خالی گذاشتن مقدار تصویر وجود دارد. در این حالت یک Post بدون تصویر ایجاد می‌شود و خطایی در صفحه اصلی نمایش می‌یابد:

Can't resolve image into URL: to_model delegated to attachment, but attachment is nil

خطاهای فوق بدین معنی هستند که تصویر الصاقی به پست خالی بوده است. بنابراین باید پست را پیش از ایجاد اعتبارسنجی کنیم تا مطمئن شویم که حتماً تصویری برای آن تعریف شده است.

از آنجا که Active Storage هنوز از اعتبارسنجی الصاق فایل‌ها پشتیبانی نمی‌کند، باید از یک متد سفارشی برای تأیید وجود تصویر در پست‌ها استفاده کنیم.

افزودن اعتبارسنجی به مدل Post

class Post < ApplicationRecord
  
  validate :image_presence

  def image_presence
    errors.add(:image, "can't be blank") unless image.attached?
  end
end

ما از image_presence برای اعتبارسنجی پست استفاده می‌کنیم. این متد هنگام افزودن پستی بدون تصویر یک خطا با پیام can’t be blank نمایش می‌دهد و زمانی که پست نامعتبر باشد، پست نمی‌تواند ایجاد شود.

متدهای اعتبارسنجی چه زمانی اجرا می‌شوند؟

Rails پیش از ذخیره‌سازی شیء Active Record، اعتبارسنجی ما را اجرا می‌کند. اگر اعتبارسنجی هر گونه خطایی تولید کند، Rails شیء را ذخیره نخواهد کرد. این وضعیت را می‌توانید در کنسول مرورگر امتحان کنید:

post = Post.new(description: "abc", user_id: User.last.id)
=> #<Post id: nil, description: "abc", user_id: 1, created_at: nil, updated_at: nil

post.save   
#=> false

post.valid? 
#=> false

post.errors.messages
#=> {:image=>["can't be blank"]}

post.image.attached?
#=> false

اینک پست‌های ما با وجود اعتبارسنجی در شیء Post همیشه شامل تصویر خواهند بود.

نمایش پست‌ها در صفحه اصلی

پس از افزودن موفق پست‌های جدید، در این مرحله قصد داریم آن‌ها را در صفحه اصلی نمایش دهیم. ما همه پست‌ها را بر اساس ترتیب فیلد created_at نمایش می‌دهیم.

Post.all.order(created_at::desc)

در HomeController متغیر posts را اضافه می‌کنیم که شامل همه پست‌های مرتب شده در اکشن index است.

class HomeController < ApplicationController
  def index
    @posts = Post.order(created_at: :desc)
  end
end

در ویوی app/views/home/index.html.erb پست‌ها را به صورت زیر نمایش می‌دهیم:

<div class="posts">
  <% @posts.each do |post| %>
    <section class="post">
      <!-- post view -->
    </section>
  <% end %>
</div>

بخش پست شامل آواتار کاربر، نام کاربری، تصویر و توضیح است. ما به صورت موقت از یک تصویر پیش‌فرض برای آواتار کاربران استفاده می‌کنیم. و تصاویر پست را به وسیله دستور زیر نشان می‌دهیم:

<%= image_tag post.image, class: 'main-image' %>

کد HTML این بخش به صورت زیر است:

<section class="post">
  <div class="user">
    <div class="avatar">
      <img src="user_avatar.jpg">
    </div>
    <div class="username">
      <%= post.user.username %>
    </div>
  </div>
  <%= image_tag post.image, class: 'main-image' %>
  <div class="description">
    <%= post.description %>
  </div>
</section>

استایل CSS نیز به صورت زیر است:

.posts{
  margin: 50px 180px 10px;

  .post{
    border: 1px solid #efefef;
    margin: 60px 0;
    border-radius: 3px;
    .user{
      border-bottom: 1px solid #efefef;
      display: flex;
      .avatar{
        width: 30px;
        margin: 8px;

        img{
          width: 100%;
          border: 1px solid #efefef;
          border-radius: 50%;
        }
      }
      .username{
        padding-top: 13px;
        color: #262626;
        font-size: 14px;
        font-weight: 600;
      }
    }

    .main-image{
      width: 100%;
      border-bottom: 1px solid #f4f4f4;
    }
    .description{
      padding: 20px;
    }
  }
}

اینک پست‌های ما به صورت زیر در آمده‌اند:

یک پست در صفحه اصلی
یک پست در صفحه اصلی

نمایش پست‌ها روی صفحه پروفایل کاربر

در این بخش پست‌های کاربر را در صفحه پروفایلشان نمایش می‌دهیم. تصاویر موقت را با تصاویری که کاربر پست کرده است جایگزین می‌کنیم.

از آنجا که User و Post ارتباط‌هایی به صورت has_many دارند، این بدان معنی است که یک کاربر می‌تواند چندین پست داشته باشد و از این رو می‌توانیم به سهولت پست‌های کاربر را با استفاده از current_user.posts بازیابی کنیم.

در بخش show در UsersController یک متغیر وهله posts در صفحه اصلی اضافه می‌کنیم. این همان posts است که در آن کاربر جاری بر اساس فیلد created_at مرتب‌سازی شده‌اند.

def show
   @posts = current_user.posts.order(created_at::desc)
End

کد HTML را در بخش user-images به‌روزرسانی می‌کنیم:

<div class="user-images">
  <% @posts.each do |post|%>
    <div class="wrapper">
      <%=image_tag post.image %>
    </div>
  <% end %>
</div>

تعداد پست‌های کاربر جاری را نیز به‌روزرسانی می‌کنیم:

<div class="col-md-8 basic-info">
  ...
  <ul class="posts">
    <li><span><%= @posts.count %></span> posts</li>
  </ul>
</div>

افزودن صفحه‌بندی برای پست‌ها

اضافه کردن امکان صفحه‌بندی در Rails با استفاده از kaminari gem کاملاً ساده است. kaminari یک ابزار صفحه‌بندی مبتنی بر Scope و Engine به صورت تمیز، قدرتمند، قابل سفارشی‌سازی و با امکانات مختلف برای فریمورک‌های وب اپلیکیشن مدرن و ORM ها محسوب می‌شود.

استفاده از kaminari gem کاملاً آسان است، کافی است gem را در اپلیکیشن وارد کنید تا مدل‌ها آماده استفاده باشند. در واقع هیچ پیکربندی مورد نیاز نیست. لزومی به تعریف کردن هیچ چیزی در مدل‌ها یا helper-ها نیست.

نصب

برای نصب در اپلیکیشن خط زیر را در Gemfile خود بنویسید:

gem 'kaminari'

سپس bundle را اجرا کنید:

bundle install

روش استفاده از صفحه‌بندی برای پست‌ها

کد موجود در کنترلر Home به صورت زیر است:

@posts = Post.order(created_at::desc).page(params[:page]).per(5)
  • حیطه (scope) برای page: به تعریف چندمین صفحه‌ای از پست‌ها که باید واکشی شوند می‌پردازد.

مثال: (Post.page(3 به این معنی است که باید سومین صفحه از پست‌ها واکشی شود.

دقت کنید که صفحه‌بندی از صفحه 1 آغاز می‌شود و نه از صفحه صفر، چون, (page(0 همان نتیجه مشابه (page(1 را بازگشت می‌دهد.

  • حیطه (scope) برای per: برای نمایش تعداد پست‌هایی که باید در هر صفحه قرار گیرند استفاده می‌شود و مقدار پیش‌فرض آن 25 است.

مثال (Post.page(3).per(5 به این معنی است که تعداد پست‌ها باید 5 باشد.

در View یک helper برای paginate ایجاد کنید:

<%= paginate @users%>

این کد چند لینک صفحه‌بندی?page=N که با تگ <nav> اچ‌تی‌ام‌ال پیچیده شده‌اند رندر می‌کند. بدین ترتیب چندین لینک صفحه‌بندی مانند زیر در خروجی ارائه می‌شود:

«First ‹ Prev ... 2 3 4 5 6 7 8 9 10 ... Next › Last»

متد helper به نام paginate را به ویوی با نام home/index.html.erb اضافه کنید:

<div class="col-md-8 basic-info">
  ...
  <ul class="posts">
    <li><span><%= @posts.count %></span> posts</li>
  </ul>
</div>

اینک به صفحه اصلی بروید و صفحه را بارگذاری مجدد کنید تا ببینید چه اتفاقی می‌افتد. البته برای مشاهده صفحه‌بندی باید بیش از 5 پست داشته باشید، زیرا ما مقدار per_page را به صورت 5 پیکربندی کردیم.

به سمت انتهای صفحه اسکرول کنید تا لینک‌های صفحه‌بندی را ببینید:

لینک‌های صفحه‌بندی
لینک‌های صفحه‌بندی

شما می‌توانید لینک‌های صفحه‌بندی 1 2 3 4 Next ›Last » را ببینید. UI این اپلیکیشن چندان زیبا نیست و به همین دلیل CSS زیر را برای بهتر شدن ظاهر آن اضافه می‌کنیم:

.homepage{
  [...]

.pagination{
    span{
      padding-right: 10px;
    }
  }
}

افزودن یک آواتار برای کاربر

ما می‌توانیم از Active Storage جهت افزودن آواتار برای کاربران استفاده کنیم.

در Model

در مدل از آنجا که هر کاربر یک آواتار دارد، بنابراین مدل User را به صورت زیر تعریف می‌کنیم:

class User < ApplicationRecord
  
  ...

  has_one_attached :avatar

end

در View (فرم ویرایش کاربر)

<div class="form-group row">
  <%= f.label :avatar, class: 'col-sm-3 col-form-label' %>
  <div class="col-sm-9">
    <%= f.file_field :avatar, class: 'form-control' %>
  </div>
</div>

در Controller

افزودن آواتار به strong params

def user_params
  params.require(:user).permit(:username, :name, :website,
                    :bio, :email, :phone, :gender, :avatar)
end

اکنون می‌توانیم به صفحه Edit user برویم و یک آواتار برای کاربر آپلود کنیم.

نمایش آواتار برای کاربر

  • در صفحه پروفایل کاربر (users/show.html.erb):
<div class="wrapper">
  <% if current_user.avatar.attached? %>
    <%= image_tag current_user.avatar %>
  <%end %>
</div>
  • در صفحه فرم ویرایش پروفایل کاربر (users/edit.html.erb):
<%= form_with model: current_user, local: true, html: {class: "form-horizontal form-edit-user"} do |f| %>
  
  <div class="form-group row">
    <div class="col-sm-3 col-form-label">
      <% if current_user.avatar.attached? %>
        <%= image_tag current_user.avatar, class: 'avatar' %>
      <%end %>
    </div>
    <div class="col-sm-9">
      ...
    </div>
  </div>
<% end %>
  • در صفحه اصلی (home/index.html.erb):

آواتار کاربران هر پست به صورت زیر است:

<section class="post">
  <div class="user">
    <div class="avatar">
      <% if post.user.avatar.attached? %>
        <%= image_tag post.user.avatar %>
      <% end %>
    </div>
    ...
  </div>
</section>

برای تعیین این که کاربر آواتار دارد یا نه می‌توانید متد ?user.avatar.attached را فراخوانی کنید.

با کلیک روی آواتار کاربران می‌توانید پروفیل کاربران دیگر را مشاهده کنید. اکشن show را در UsersController به‌روزرسانی کنید. current_user را با کاربری که با به وسیله id کوئری کرده‌اید تعویض کنید:

class UsersController < ApplicationController

  def show
    @user  = User.find(params[:id])
    @posts = @user.posts.order(created_at: :desc)
  end
  • در صفحه اصلی (home/index.html.erb):

لینک‌ها برای آواتار و نام کاربری آن کاربر که به صفحه کاربری‌اش می‌روید اضافه کنید.

<div class="user">
  <div class="avatar">
    <% if post.user.avatar.attached? %>
      <%= link_to user_path(post.user) do %>
        <%= image_tag post.user.avatar %>
      <% end %>
    <% end %>
  </div>
  <%= link_to post.user.username, user_path(post.user), class: 'username' %>
</div>

اکنون در صفحه اصلی می‌توانیم روی آواتار یا نام کاربری آن کاربری که می‌خواهیم جزییات پروفایلش را مشاهده کنیم کلیک کنیم.

سخن پایانی

در این مقاله به معرفی جزییاتی در مورد مدل (Active Record) آموختیم. این موارد شامل ایجاد یک مدل جدید، اعتبارسنجی، و ایجاد ارتباط بوده است. همچنین در مورد ویژگی Active Storage و چگونگی استفاده از آن برای آپلود تصاویر آموختیم. در نهایت شیوه استفاده از Kaminari gem برای صفحه‌بندی را مشاهده کردیم.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۱ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
luanotes
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *