本文记录和总结,电商站点中(Rails)后台更新购物车数量的实现,来自我自己开发的电商程序。
功能分析
因为购物车的数量是显示在顶部导航栏内,任何一次访问页面,都会涉及到此数据的显示和更新问题。主要实现需要放在application_controller
中。
先说显示问题,显然每次都要显示,意味着在任何的controller中都要有此数据,这就需要一个全局的变量来存储这个数据。
全局变量的选择,可以是类变量,@@开头的变量,或者是$开头的变量,该变量设置在顶级controller中,也即是application_controller
中。
更新问题的话,初始化设置一次,这里还有一个问题,什么时候会初始化application_controller
中非函数的部分?
后续执行并不需要每次都更新,只有在购物车改变时需要更新,也就是carts_controller
中的删除、创建、更新这三个action需要更新。
这就是说,在这三个action执行以后,application_controller
需要知道购物车已经更新,数目需要重新计算。这就需要一个通知机制,
这里也使用一个全局变量。
还有,要在前端view中显示,需要一个实例变量来存储,也就是一个@开头的变量。
实现代码
application_controller实现,这里只有本文相关代码,其他代码略去。这里使用一个字典来缓存相应用户的数据。
key为用户的id,value为一个结构体,此结构体有两个字段,一个是缓存的计数count
,一个记录是否购物车做过改变is_changed
。
class ApplicationController < ActionController::Base
before_action :set_cart_num
# for cache num,
CartCount = Struct.new(:count, :is_changed)
# dict, {user_id => CartCount, ... }
@@cached_cart = {}
def set_cart_num
if current_user
id = current_user.id
# exist
if @@cached_cart[id]
if @@cached_cart[id].is_changed
# changed
@cart_num = @@cached_cart[id].count = count_carts
@@cached_cart[id].is_changed = false
else
# "no change"
@cart_num = @@cached_cart[id].count
end
else
# "not exist"
@@cached_cart[id] = CartCount.new(count_carts, false)
@cart_num = @@cached_cart[id].count
end
end
end
def count_carts
current_user.carts.collect{|item| item.amount}.sum
end
# 在涉及到购物车修改的地方,都需要调用此方法,以刷新缓存。
# 例如在cart_controller和orders_controller中
def notify_cart_change
@@cached_cart[current_user.id].is_changed = true
end
end
carts_controller实现,carts_controller主要维护购物车中的条目,每一个条目是一种商品及其数量。
class CartsController < ApplicationController
after_action :notify_cart_change, only: [:create, :update, :destroy]
end
orders_controller中也要调用刷新缓存的方法
class CartsController < ApplicationController
after_action :notify_cart_change, only: [:create, :update, :destroy]
end
view
<a href="/carts"><img src="/assets/shopping-cart1.png" class="" alt="carts">
<span class="badge"><%= @cart_num %></span>
</a>
schema of carts
create_table "carts", id: :serial, force: :cascade do |t|
t.integer "user_id"
t.integer "product_id"
t.integer "amount"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["product_id"], name: "index_carts_on_product_id"
t.index ["user_id"], name: "index_carts_on_user_id"
end
疑问与思考
思考1: 该缓存@@cached_cart = {}
对象,是一个全局对象,会随着用户的增多而不断增大,后面用户量变大,需要考虑设置过期时间,或者将该数据结构保存在外置的内存数据库中,如redis等。可以在用户登出以后,销毁该用户的缓存数据
# routes.rb
devise_for :users, controllers: {
:passwords => 'users/passwords',
:registrations => 'users/registrations',
:sessions => 'users/sessions'
}
# sessions_ontroller.rb
class Users::SessionsController < Devise::SessionsController
before_action :clear_cart_count, only: [:destroy]
private
# after user sign_out, delete the cached cart count.
def clear_cart_count
@@cached_cart.delete current_user.id
end
end
思考2: 使用结构体是否会占用内存过多?替换成数组是否会更节省内存? 毕竟只有两个字段,使用结构体是为了清晰。 我们来对比测试一下,同样是两个字段,用数组和结构体分别使用的内存。
require 'objspace'
CartCount = Struct.new(:count, :is_changed)
c = CartCount.new(12, false)
p ObjectSpace.memsize_of(c)
# => 40
d = [13, false]
p ObjectSpace.memsize_of(d)
# => 40
从上面可以看出,无论是用结构体还是数组,使用内存均为40。 关于整个application_controller的初始化问题,什么时候会初始化application_controller中所有内容?在程序启动初始化时。