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 #运行单个文件