ساخت اپلیکیشن اینستاگرام با 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 برای صفحهبندی را مشاهده کردیم.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- آموزش روبی (Ruby) | راهنمای کامل و رایگان برای شروع به کار — به زبان ساده
- مجموعه آموزش های پروژه محور برنامه نویسی
- ساخت اپلیکیشن اینستاگرام با Ruby on Rails (بخش اول) — از صفر تا صد
- مجموعه آموزشهای مهندسی نرم افزار
- ساختمان های داده در روبی (Ruby) — درخت های دودویی
- افزایش فالوورهای اینستاگرام با ربات پایتون — راهنمای کاربردی
==