Shop Demo

目录

01 课程介绍和项目准备
02 UI以及用户模块依赖Gem安装和设置
03 全局模版设置以及User模型开发
04 用户注册和登录功能开发
05 分类和商品模型设计
06 分类和商品模型开发: 商品UUID和分类树形结构
07 边开发边测试: RSpec单元测试配置和使用

内容

01 课程介绍和项目准备

#数据库默认为sqlite,如果使用mysql,需要另外进行配置

#不用puma(改用unicorn),不用turbolinks
rails new shop --skip-bundle --skip-puma --skip-turbolinks

#更改gemfile,更改淘宝源以及rubyracer
source 'https://ruby.taobao.org'
gem 'therubyracer', platforms: :ruby #在生成环境部署的时候需要用到的js环境

#进行配置config/application.rb,目的是不自动生成项目项目默认代码
config.generators do |generator|
  generator.assets false
  generator.test_framework false
end

02 UI以及用户模块依赖Gem安装和设置
添加三个gem

gem 'bootstrap-sass'
gem 'font-awesome-rails'
gem 'sorcery'

bootstrap和fontawesome的设置

#更改application.css为application.scss

#在application.scss中按照scss格式引入相关文件
@import "bootstrap-sprockets"
@import "bootstrap"
@import "font-awesome"

#在Application.js文件中添加bootstrap相关内容
//= require bootstrap-sprockets

sorcery相关设置

#sorcery的相关设置已经在sorcery这个gem中已经做了介绍,这里只是给出终端提示。

rails g -h  #这句话可以提示如何使用sorcery
#上面的提示结果是 rails g sorcery:install

rails g sorcery:install -h #使用这句话提示安装sorcey的具体模块

#这个章节提供的模块如下
rails g sorcery:install remember_me reset_password user_activation --only-submodules

03 全局模版设置以及User模型开发
设置首页welcome/index

rails g welcome index

#routes.rb
root "welcome#index"

模板文件application.html.erb建立如下:

<!DOCTYPE html>
<html>
  <head>
    <title>Shop</title>
    <!--这个文件是为了兼容在手机端观看-->
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <%= csrf_meta_tags %>

    <%= stylesheet_link_tag 'application', media: 'all' %>
    <!--命名stylesheet-->
    <%= yield :stylesheets %>
  </head>

  <body>
    <%= render 'layouts/menu' %>
    <div class="container">
      <%= yield %>
    </div>
    <!--js放置在后面,提高加载速度-->
    <%= javascript_include_tag 'application' %>
    #命名js
    <%= yield :javascripts %>
  </body>
</html>

头部文件中需要建立views/layouts/_menu.html.erb

<nav class="navbar navbar-default">
  <div class="container">
    <!-- Brand and toggle get grouped for better mobile display -->
    <div class="navbar-header">
      <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
        <span class="sr-only">Toggle navigation</span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      <a class="navbar-brand" href="<%= root_path %>">蛋人商城</a>
    </div>

    <!-- Collect the nav links, forms, and other content for toggling -->
    <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
      <ul class="nav navbar-nav">
        <li class="active"><a href="<%= root_path %>">Home <span class="sr-only">(current)</span></a></li>
        <li><a href="#">分类</a></li>
      </ul>
      <form class="navbar-form navbar-left">
        <div class="form-group">
          <input type="text" class="form-control" placeholder="商品名称..">
        </div>
        <button type="submit" class="btn btn-default">搜索</button>
      </form>
      <ul class="nav navbar-nav navbar-right">
        <li><a href="#">购物车</a></li>
        <li><a href="#">注册</a></li>
        <li><a href="#">登录</a></li>
        <li class="dropdown">
          <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">个人中心 <span class="caret"></span></a>
          <ul class="dropdown-menu">
            <li><a href="#">我的订单</a></li>
            <li><a href="#">我的收获地址</a></li>
            <li><a href="#">我的收藏</a></li>
            <li role="separator" class="divider"></li>
            <li><a href="#">个人信息</a></li>
          </ul>
        </li>
      </ul>
    </div><!-- /.navbar-collapse -->
  </div><!-- /.container-fluid -->
</nav>

设置user.rb文件

class User < ApplicationRecord  
  authenticates_with_sorcery!
  attr_accessor :password, :password_confirmation

  validates :name, presence: { message: "不能为空" }
  validates :email, uniqueness: { message: "已经存在该邮箱"}
  validates :password, presence: { message: "密码不能为空" } 
    if: :need_validate_password
  validates :password_confirmation, presence: { message: "密码确认不能为空" } 
    if: :need_validate_password
  validates :password, confirmation: { message: "密码不一致" } 
    if: :need_validate_password
  validates :password, length: { minimum: 6, too_short: "密码不少于6个字符" } 
    if: :need_validate_password

  private
  def need_validate_password
    self.new_record? || 
      (!self.password.nil? or !self.password_confirmation.nil?)
  end
end

04 用户注册和登录功能开发
上一个章节主要model层面的开发,这个章节是controller和view的开发。
routes.rb设置路由

resources :users
resources :sessions

delete '/logout' => 'sessions#destroy', as: :logout

文件users_controller.rb

class UsersController < ApplicationController

  def new
    @user = User.new
  end

  def create
    @user = User.new(params.require(:user)
      .permit(:email, :password, :password_confirmation))

    if @user.save
      flash[:notice] = "注册成功,请登录"
      redirect_to new_session_path
    else
      render action: :new
    end
  end

end

sessions_controller.rb

class SessionsController < ApplicationController

  def new

  end

  def create
    if user = login(params[:email], params[:password])
      flash[:notice] = "登陆成功"
      redirect_to root_path
    else
      flash[:notice] = "邮箱或者密码不正确"
      redirect_to new_session_path
    end
  end

  def destroy
    logout
    flash[:notice] = "退出成功"
    redirect_to root_path
  end

end

注册页面views/users/new.html.erb

<h1>注册</h1>
<% unless @user.errors.empty? %>
  <div class="alert alert-danger">
    <ul>
      <% @user.errors.messages.values.flatten.each do |error| %>
        <li><%= error %></li>
      <% end -%>
    </ul>
  </div>
<% end -%>

<%= form_for :user, url: users_path, method: :post, html: { class: 'form-horizontal', id: "user_form"} do |f| %>
  <div class="form-group">
    <div class="col-lg-12">邮箱 *</div>
    <div class="col-lg-12">
      <%= f.text_field :email, class: "form-control" %>
    </div>
  </div>
  <div class="form-group">
    <div class="col-lg-12">密码 *</div>
    <div class="col-lg-12">
      <%= f.password_field :password, class: "form-control" %>
    </div>
  </div>
  <div class="form-group">
    <div class="col-lg-12">密码确认 *</div>  
    <div class="col-lg-12">
      <%= f.password_field :password_confirmation, class: "form-control" %>
    </div>
  </div>
  <div class="form-group">
    <div class="col-lg-12">
      <input type="submit" name="create_user_submit" value="创建账户" class="col-xs-12 btn btn-primary"> 
    </div>
  </div>
<% end -%>

登录页面views/sessions/new.html.erb

<h1>登陆</h1>
<%= form_tag sessions_path, method: :post, class: "form-horizontal" do %>
  <div class="form-group">
    <div class="col-lg-12">邮箱</div>
    <div class="col-lg-12">
      <%= text_field_tag :email, nil, class: "form-control" %>
    </div>
  </div>
  <div class="form-group">
    <div class="col-lg-12">密码</div>
    <div class="col-lg-12">
      <%= password_field_tag :password, nil, class: "form-control" %>
    </div>
  </div>
  <div class="form-group">
    <div class="col-lg-12">
      <div class="pull-right">
        <a href="<%= new_user_path %>">注册</a>
      </div>
    </div>
  </div>        
  <div class="form-group">
    <div class="col-lg-12">
      <input type="submit" name="sign_submit" value="登陆" class="col-xs-12 btn btn-primary"> 
    </div>
  </div>
<% end -%>

更新user.rb文件,添加以下内容

class User < ApplicationRecord
  validates :email, presence: { message: "邮箱不能为空" }
  validates :email, format: { 
    with: /\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/,
    message: "邮箱格式不合法" },
    if: proc { |user| !user.email.blank? }

  def username
    self.email.split('@').first
  end
end

更新view层的头部文件views/layouts/_menu.html.erb文件

<nav class="navbar navbar-default">
  <div class="container">
    <!-- Brand and toggle get grouped for better mobile display -->
    <div class="navbar-header">
      <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
        <span class="sr-only">Toggle navigation</span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      <a class="navbar-brand" href="<%= root_path %>">蛋人商城</a>
    </div>

    <!-- Collect the nav links, forms, and other content for toggling -->
    <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
      <ul class="nav navbar-nav">
        <li class="active"><a href="<%= root_path %>">Home <span class="sr-only">(current)</span></a></li>
        <li><a href="#">分类</a></li>
      </ul>
      <form class="navbar-form navbar-left">
        <div class="form-group">
          <input type="text" class="form-control" placeholder="商品名称..">
        </div>
        <button type="submit" class="btn btn-default">搜索</button>
      </form>
      <ul class="nav navbar-nav navbar-right">
        <li><a href="#">购物车</a></li>
        <% unless logged_in? %>
          <li><a href="<%= new_user_path %>">注册</a></li>
          <li><a href="<%= new_session_path %>">登录</a></li>
        <% else %>
          <li><a href="#">欢迎你, <%= current_user.username %></a></li>
          <li><%= link_to "退出", logout_path, method: :delete %></li>
        <% end -%>
        <li class="dropdown">
          <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">个人中心 <span class="caret"></span></a>
          <ul class="dropdown-menu">
            <li><a href="#">我的订单</a></li>
            <li><a href="#">我的收获地址</a></li>
            <li><a href="#">我的收藏</a></li>
            <li role="separator" class="divider"></li>
            <li><a href="#">个人信息</a></li>
          </ul>
        </li>
      </ul>
    </div><!-- /.navbar-collapse -->
  </div><!-- /.container-fluid -->
</nav>

05 分类和商品模型设计
创建分类和商品两个模型:

rails g model category
rails g model product

两个migrate文件

#category
class CreateCategories < ActiveRecord::Migration[5.0]
  def change
    create_table :categories do |t|
      t.string :title
      t.integer :weight, default: 0
      t.integer :products_counter, default: 0
      t.timestamps
    end

    add_index :categories, [:title]
  end
end

#product
class CreateProducts < ActiveRecord::Migration[5.0]
  def change
    create_table :products do |t|
      t.integer :category_id
      t.string :title
      t.string :status, default: 'off'
      t.integer :amount, default: 0
      t.string :uuid
      t.decimal :msrp, precision: 10, scale: 2
      t.decimal :price, precision: 10, scale: 2
      t.text :description
      t.timestamps
    end

    add_index :products, [:status, :category_id]
    add_index :products, [:category_id]
    add_index :products, [:uuid], unique: true
    add_index :products, [:title]
  end
end

06 分类和商品模型开发: 商品UUID和分类树形结构
引入外部库文件生成uuid
在lib文件夹中书写random_code.rb文件,文件内容如下:

module RandomCode
  class << self
    def generate_password len = 8
      seed = (0..9).to_a + ('a'..'z').to_a + ('A'..'Z').to_a + ['!', '@', '#', '$', '%', '.', '*'] * 4
      token = ""
      len.times { |t| token << seed.sample.to_s }
      token
    end

    def generate_cellphone_token len = 6
      a = lambda { (0..9).to_a.sample }
      token = ""
      len.times { |t| token << a.call.to_s }
      token
    end

    def generate_utoken len = 8
      a = lambda { rand(36).to_s(36) }
      token = ""
      len.times { |t| token << a.call.to_s }
      token
    end

    def generate_product_uuid
      Date.today.to_s.split('-')[1..-1].join() << generate_utoken(6).upcase
    end

    def generate_order_uuid
      Date.today.to_s.split('-').join()[2..-1] << generate_utoken(5).upcase
    end
  end
end

将上面提到的文件加载到系统的加载路径中,config/application.rb文件中

config.autoload_paths += %W[#{Rails.root}/lib]

设置product.rb

class Product < ApplicationRecord
  belongs_to :category
  before_create :set_default_attrs

  private
  def set_default_attrs
    self.uuid = RandomCode.generate_product_uuid
  end
end

进行分类的第三方插件安装和category.rb的设置

gem 'ancestry'

将ancestry和category进行关联

rails g migration add_ancestry_to_category

class AddAncestryToCategory < ActiveRecord::Migration[5.0]
  def change
    add_column :categories, :ancestry, :string
    add_index :categories, :ancestry
  end
end

设置category.rb

class Category < ApplicationRecord

  has_ancestry #ancestry的代码
  has_many :products, dependent: :destroy
end

07 边开发边测试: RSpec单元测试配置和使用
rspec只针对的是单元测试(即是一个很小的独立功能),集成测试需要用到其他的测试gem.
gem添加

group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platform: :mri

  gem 'database_cleaner', '~> 1.5.3' #保证测试前后数据库内容一致
  gem 'rspec-rails', '~> 3.5'
  gem 'rails-controller-testing' #rails5之后从rspec中剥离,对controller进行测试
end

rspec安装

rails g rspec:install #终端进行安装

对.rspec文件进行修改

—color
—require spec_helper
- f d #从终端打印出测试成功的用例

对spec_helper.rb文件进行修改,主要是针对gem中的database_cleaner进行设置

  config.before(:suite) do
    DatabaseCleaner.clean_with(:truncation)
  end 

  config.before(:each) do
    DatabaseCleaner.start
  end 

  config.after(:each) do
    DatabaseCleaner.clean
  end

测试用例
假设对userscontroller中create方法进行单元测试,需要在需要建立spec/controllers/users_controller_spec.rb文件

#users_controller_spec.rb
require 'rails_helper'

describe UsersController do

  context "signup" do
    it "should failed" do
      post :create, params: { user: { email: 'eggman' } }
      expect(response).to render_template("new")
    end

    it "should success" do
      post :create, params: { user: 
        { 
      email: '[email protected]', 
      password: '111111', 
      password_confirmation: '111111' 
     } 
       }
     expect(response).to redirect_to(new_session_path)
    end
  end
end

假设对product.rb文件进行测试,需要建立spec/models/product_spec.rb文件

#spec/model/product_spec.rb
require 'rails_helper'

  describe Product do

  it "should have 0 product" do
    expect(Product.count).to eq(0)  
  end
end

终端运行

rspec #运行全部测试文件
rspec spec/models/product_spec.rb #运行单个文件

results matching ""

    No results matching ""