会话存储(Session storage)技术是由W3C的Web Storage提供的一项新特性。这个新特性可以被Explorer 8+、Firefox、Chrome、Safari 和 Opera的桌面浏览器支持,完整的浏览器支持列表可以查看Can I Use。在这篇文章中,我们将使用session storage会话存储技术的sessionStorage
对象和jQuery来实现一个完整的客户端电子商务购物车。
Session Storage
我们可以使用sessions来存储数据,并在多个页面中使用这些数据。通常,在某个用户选择了某样产品之后,我们需要保存他选择的这样产品的名称、数量和价格。然后,用户需要填写一些个人信息,这些也需要保存在session中,典型的页面时支付页面和对支付网关后续重定向的页面。
一个购物车是如何被创建的呢?例如使用PHP,可以使用一个关联数组来实现一个购物车的基本结构。PHP开发人员可以使用关联数组来保存会话数据的结构和组织。
JavaScript的会话方式则截然不同。一般情况下,一个session的生命周期是用户关闭浏览器时它就会过期(注意:关闭浏览器的概念在移动mobile浏览器上不适用)。当一个session过期后,浏览器上保存在会话存储(session storage)中的session数据会被全部清除。我们不需要明确的初始化一个session,因为在JavaScript中,可以从全局的sessionStorage
对象中获取session。
session数据有键值对组成,每一个key的名称的一个唯一的字符串。要写入数据,我们可以使用sessionStorage.setItem( name, value )
方法:
sessionStorage.setItem( "total", 120 );
在上面的例子中,key的名称是“total”,对应的值是120。我们现在已经通过setItem
方法在session中放入了一个值,这个值在session被销毁前是一直有效的。我们可以使用sessionStorage.removeItem( "total" )
方法来指定销毁某一个项,或使用sessionStorage.clear()
来销毁所有的session项。
null
。当我们在session storage中移除了一个key后,再去获取它的值,将返回null
。现在,我们刚才设置的key值将一直有效,无论我们导航到网站的那个页面,直到浏览器关闭,session被销毁为止。要获取这个key的值,可以简单的使用下面的方法:
var total = sessionStorage.getItem( "total" ); console.log( total ); // '120', a string
我们还可以使用sessionStorage.setItem()
方法来更新key对于的值:
var total = parseInt( sessionStorage.getItem( "total" ) ); var quantity = 2; var updatedTotal = total * quantity; sessionStorage.setItem( "total", updatedTotal ); // '240', a string
上面的语句将名称为"total"的key的值修改为240,。为什么要调用parseInt()
函数呢?不用多说,它可以将字符串解析为一个整数。记住,在session storage中所有的值都是字符串,我们要计算必须进行转换。
对于一个对象,它可以转换为一个JSON字符串(通过JSON.stringify()
方法)再存储于session storage中。然后可以通过javascript对象来读取它(通过JSON.parse()
)。
var cart = { item: "Product 1", price: 35.50, qty: 2 }; var jsonStr = JSON.stringify( cart ); sessionStorage.setItem( "cart", jsonStr ); // now the cart is {"item":"Product 1","price":35.50,"qty":2} var cartValue = sessionStorage.getItem( "cart" ); var cartObj = JSON.parse( cartValue ); // original object
要更新一个对象,我们可以简单的继承它,再按照上面的步骤操作一次即可。
安全注意事项
对于购物系统,安全是非常重要的。我们可以阅读W3C的规范security notes,来了解客户端存储方面的安全知识。
美国计算机应急响应小组(The US Computer Emergency Readiness Team)在 technical paper on website security(PDF)中明确规定:
即使是在浏览器正常关闭的情况下终止session,恶意的攻击行为仍可能会发生,特别是浏览器存在一些严重漏洞的时候。此外,一些木马程序通常可以通过浏览器来传播和入侵。
因此,在使用浏览器存储session数据技术之前,请确保您的网站是安全的。
项目演示
下面我们要制作一个简单的项目演示-一个在线出售酒类产品的购物车项目。这是一个简单的电子商务页面,它可以完成购买商品的更换和价格结算。
用户可以从购物车中添加、移除商品,也可以更新商品的数量和清空所有的商品。
HTML结构
整个项目有以下几个页面:
- index.html-用户购物界面。
- cart.html-购物车界面。
- checkout.html-结算界面。
index.html页面的HTML结构如下:
<div class="product-description" data-name="Wine #1" data-price="5"> <h3 class="product-name">Wine #1</h3> <p class="product-price">€ 5</p> <form class="add-to-cart" action="cart.html" method="post"> <div> <label for="qty-1">Quantity</label> <input type="text" name="qty-1" id="qty-1" class="qty" value="1" /> </div> <p><input type="submit" value="Add to cart" class="btn" /></p> </form> </div>
这里使用的data属性用于存储商品的名称和价格,可以通过jQuery的.data()和$.data()方法来接收。
cart.html页面的HTML结构如下:
购物车界面由三部分组成:一个产品信息表格,一个显示总价的元素和一组执行操作的按钮。
<form id="shopping-cart" action="cart.html" method="post"> <table class="shopping-cart"> <thead> <tr> <th scope="col">Item</th> <th scope="col">Qty</th> <th scope="col">Price</th> </tr> </thead> <tbody></tbody> </table> <p id="sub-total"> <strong>Sub Total</strong>: <span id="stotal"></span> </p> <ul id="shopping-cart-actions"> <li> <input type="submit" name="update" id="update-cart" class="btn" value="Update Cart" /> </li> <li> <input type="submit" name="delete" id="empty-cart" class="btn" value="Empty Cart" /> </li> <li> <a href="index.html" class="btn">Continue Shopping</a> </li> <li> <a href="checkout.html" class="btn">Go To Checkout</a> </li> </ul> </form>
表格的数据开始时空的,我们要通过javascript来相应的填写数据。更新和清空购物车按钮将通过javascrip来执行相应操作,其它两个按钮只是一个链接。
CHECKOUT.html页面的HTML结构如下:
<table id="checkout-cart" class="shopping-cart"> <thead> <tr> <th scope="col">Item</th> <th scope="col">Qty</th> <th scope="col">Price</th> </tr> </thead> <tbody> </tbody> </table> <div id="pricing"> <p id="shipping"> <strong>Shipping</strong>: <span id="sshipping"></span> </p> <p id="sub-total"> <strong>Total</strong>: <span id="stotal"></span> </p> </div> <form action="order.html" method="post" id="checkout-order-form"> <h2>Your Details</h2> <fieldset id="fieldset-billing"> <legend>Billing</legend> <!-- Name, Email, City, Address, ZIP Code, Country (select box) --> <div> <label for="name">Name</label> <input type="text" name="name" id="name" data-type="string" data-message="This field may not be empty" /> </div> <div> <label for="email">Email</label> <input type="text" name="email" id="email" data-type="expression" data-message="Not a valid email address" /> </div> <div> <label for="city">City</label> <input type="text" name="city" id="city" data-type="string" data-message="This field may not be empty" /> </div> <div> <label for="address">Address</label> <input type="text" name="address" id="address" data-type="string" data-message="This field may not be empty" /> </div> <div> <label for="zip">ZIP Code</label> <input type="text" name="zip" id="zip" data-type="string" data-message="This field may not be empty" /> </div> <div> <label for="country">Country</label> <select name="country" id="country" data-type="string" data-message="This field may not be empty"> <option value="">Select</option> <option value="US">USA</option> <option value="IT">Italy</option> </select> </div> </fieldset> <div id="shipping-same">Same as Billing <input type="checkbox" id="same-as-billing" value=""/></div> <fieldset id="fieldset-shipping"> <legend>Shipping</legend> <!-- Same fields as billing --> </fieldset> <p><input type="submit" id="submit-order" value="Submit" class="btn" /></p> </form>
data属性用于验证。data-type
属性指定我们校验的数据的类型,data-message
是用于显示的错误信息。
JAVASCRIPT
我们将使用面向对象的方法来构建js代码。
OBJECT STRUCTURE
我们使用的对象有一个非常简单的结构。构造函数中将初始化包含整个DOM元素的顶级元素,并执行初始化方法。
(function( $ ) { $.Shop = function( element ) { this.$element = $( element ); // top-level element this.init(); }; $.Shop.prototype = { init: function() { // initializes properties and methods } }; $(function() { var shop = new $.Shop( "#site" ); // object's instance }); })( jQuery );
对象实例在页面初始化时被创建。可以使用下面的代码来测试对象是否正常工厂:
$(function() { var shop = new $.Shop( "#site" ); console.log( shop.$element ); });
上面的代码会输出:
x.fn.x.init[1] 0: div#site context: document length: 1 selector: "#site"
现在,对象被正常创建,我们可以开始定义它的属性。
OBJECT PROPERTIES
我们的对象的属性分为两类:第一是处理计算、表单和校验的属性,第二类是HTML元素的引用。
$.Shop.prototype = { init: function() { // Properties this.cartPrefix = "winery-"; // prefix string to be prepended to the cart's name in session storage this.cartName = this.cartPrefix + "cart"; // cart's name in session storage this.shippingRates = this.cartPrefix + "shipping-rates"; // shipping rates key in session storage this.total = this.cartPrefix + "total"; // total key in the session storage this.storage = sessionStorage; // shortcut to sessionStorage object this.$formAddToCart = this.$element.find( "form.add-to-cart" ); // forms for adding items to the cart this.$formCart = this.$element.find( "#shopping-cart" ); // Shopping cart form this.$checkoutCart = this.$element.find( "#checkout-cart" ); // checkout form cart this.$checkoutOrderForm = this.$element.find( "#checkout-order-form" ); // checkout user details form this.$shipping = this.$element.find( "#sshipping" ); // element that displays the shipping rates this.$subTotal = this.$element.find( "#stotal" ); // element that displays the subtotal charges this.$shoppingCartActions = this.$element.find( "#shopping-cart-actions" ); // cart actions links this.$updateCartBtn = this.$shoppingCartActions.find( "#update-cart" ); // update cart button this.$emptyCartBtn = this.$shoppingCartActions.find( "#empty-cart" ); // empty cart button this.$userDetails = this.$element.find( "#user-details-content" ); // element that displays the user's information this.$paypalForm = this.$element.find( "#paypal-form" ); // PayPal form this.currency = "€"; // HTML entity of the currency to be displayed in layout this.currencyString = "€"; // currency symbol as text string this.paypalCurrency = "EUR"; // PayPal's currency code this.paypalBusinessEmail = "yourbusiness@email.com"; // your PayPal Business account email address this.paypalURL = "https://www.sandbox.paypal.com/cgi-bin/webscr"; // URL of the PayPal form // object containing patterns for form validation this.requiredFields = { expression: { value: /^([w-.]+)@((?:[w]+.)+)([a-z]){2,4}$/ }, str: { value: "" } }; // public methods invocation } };
接下来看一看上面的每一个属性的意义。
Storage and other properties:
- cartPrefix:session storage中key名称的前缀。
- cartName:session storage中key的名称(和cartPrefix结合组成car的名称字符串)。
- shippingRates:运费。
- total:总价。
- storage:
sessionStorage
对象。 - currency:显示当前货币的HTML布局。
- currencyString:当前货币的符号。
- paypalCurrency:PayPal的货币code。
- paypalBusinessEmail:你的PayPal的email账号。
- paypalURL:PayPal表单的URL地址。
- requiredFields:表单中的必填字段。
References to elements:
- $formAddToCart:添加商品的表单。
- $formCart:购物车表单。
- $checkoutCart:结账表单。
- $checkoutOrderForm:结账表单中的客户填写信息。
- $shipping:显示运费的元素。
- $subTotal:显示总价的元素。
- $shoppingCartActions:购物车操作相关的元素。
- $updateCartBtn:更新购物车的按钮。
- $emptyCartBtn:清空购物车的按钮。
- $userDetails:显示用户信息的元素。
- $paypalForm:PayPal的表单。
所有这些属性都添加了$
前缀,表示它们是一个jQuery对象,这些属性不是在所有页面上都有效的。要查看一个jQuery元素是否存在,可以简单的使用length
属性来测试它:
if( $element.length ) { // the element exists }
对象方法
在项目时候使用的方法分为私有方法和公共方法。使用方法用于后台操作,辅助公共方法完成任务。私有方法都使用下划线作为前缀,并且不能直接调用。
公共方法用于直接操作页面上的元素和数据,它们没有下划线前缀。
私有方法
第一个私有方法是_emptyCart()
,用于情况当前session storage的数据。
$.Shop.prototype = { // empties session storage _emptyCart: function() { this.storage.clear(); } };
_formatNumber()
方法用于格式化数字。
/* Format a number by decimal places * @param num Number the number to be formatted * @param places Number the decimal places * @returns n Number the formatted number */ _formatNumber: function( num, places ) { var n = num.toFixed( places ); return n; }
这个方法使用Number
对象的javascript toFixed()方法,项目中使用它来格式化商品价格。
因为不是所有的价格数据都保存在data属性中,我们需要知道一个方法来提取商品的价格。这个方法是_extractPrice()
:
/* Extract the numeric portion from a string * @param element Object the jQuery element that contains the relevant string * @returns price String the numeric string */ _extractPrice: function( element ) { var self = this; var text = element.text(); var price = text.replace( self.currencyString, "" ).replace( " ", "" ); return price; }
上面的self
是$.Shop
对象的引用。我们每次需要接受一个属性或方法时都需要用到它。
为了程序更加健壮,可以删除文本前后的空白:
var text = $.trim( element.text() );
需要注意的是,jQuery的$.trim()方法会删除字符串前面和后面的所有的新行、空格和Tabs。
接下来,我们需要两个方法来进行字符串和数字之间的相互转换。
/* Converts a numeric string into a number * @param numStr String the numeric string to be converted * @returns num Number the number, or false if the string cannot be converted */ _convertString: function( numStr ) { var num; if( /^[-+]?[0-9]+.[0-9]+$/.test( numStr ) ) { num = parseFloat( numStr ); } else if( /^d+$/.test( numStr ) ) { num = parseInt( numStr ); } else { num = Number( numStr ); } if( !isNaN( num ) ) { return num; } else { console.warn( numStr + " cannot be converted into a number" ); return false; } }, /* Converts a number to a string * @param n Number the number to be converted * @returns str String the string returned */ _convertNumber: function( n ) { var str = n.toString(); return str; }
_convertString()
方法运行以下的测试:
- 字符串是小数格式?如果是,它使用parseFloat()方法。
- 字符串是整数格式?如果是,它使用parseInt()方法。
- 如果字符串不能转换为数字,它使用Number()构造函数。
- 如果结果是一个数字(使用isNaN()来测试),它返回数字。否则,它会在控制台输出警告信息并返回
false
。
_convertNumber()
方法只是简单的调用toString()方法来将数字转换为字符串。
再接下来,要定义两个方法将一个对象转换为JSON字符串和将JSON字符串转换为对象。
/* Converts a JSON string to a JavaScript object * @param str String the JSON string * @returns obj Object the JavaScript object */ _toJSONObject: function( str ) { var obj = JSON.parse( str ); return obj; }, /* Converts a JavaScript object to a JSON string * @param obj Object the JavaScript object * @returns str String the JSON string */ _toJSONString: function( obj ) { var str = JSON.stringify( obj ); return str; }
第一个方法使用JSON.parse()
方法,第二个方法调用JSON.stringify()
方法。可以参考Mozilla Developer Network的文章Using Native JSON。
为什么我们需要这些方法呢?因为我们的购物车使用下面的数据结构来存储相关的商品信息:
Key | Value |
winery-cart | { "items": [ { "product": "Wine #1", "qty": 5, "price": 5 } ] } |
key winery-cart
包含一个代表数组对象的JSON字符串,该数组包含了商品的名称、数量和价格等信息。
现在,我们需要一个方法来在session storage值为指定的key添加items。
/* Add an object to the cart as a JSON string * @param values Object the object to be added to the cart * @returns void */ _addToCart: function( values ) { var cart = this.storage.getItem( this.cartName ); var cartObject = this._toJSONObject( cart ); var cartCopy = cartObject; var items = cartCopy.items; items.push( values ); this.storage.setItem( this.cartName, this._toJSONString( cartCopy ) ); }
这个方法从会话存储(session storage)中获取key,然后将它转换为一个javascript对象,并且使用JSON数据添加一个新的对象到购物车数组中。新添加的对象的数据结构如下:
this._addToCart({ product: "Test", qty: 1, price: 2 });
添加后的购物车键值对如下:
Key | Value |
winery-cart | { "items": [ { "product": "Wine #1", "qty": 5, "price": 5 }, { "product": "Test", "qty": 1, "price": 2 } ] } |
运费是按产品的数量添加到购物车,而不是每个产品的数量。
/* Custom shipping rates calculated based on total quantity of items in cart * @param qty Number the total quantity of items * @returns shipping Number the shipping rates */ _calculateShipping: function( qty ) { var shipping = 0; if( qty >= 6 ) { shipping = 10; } if( qty >= 12 && qty <= 30 ) { shipping = 20; } if( qty >= 30 && qty <= 60 ) { shipping = 30; } if( qty > 60 ) { shipping = 0; } return shipping; }
这个方法你可以自己按需要更换。
我们还需要一个方法来校验结账表单用户填写的个人信息。
/* Validates the checkout form * @param form Object the jQuery element of the checkout form * @returns valid Boolean true for success, false for failure */ _validateForm: function( form ) { var self = this; var fields = self.requiredFields; var $visibleSet = form.find( "fieldset:visible" ); var valid = true; form.find( ".message" ).remove(); $visibleSet.each(function() { $( this ).find( ":input" ).each(function() { var $input = $( this ); var type = $input.data( "type" ); var msg = $input.data( "message" ); if( type == "string" ) { if( $input.val() == fields.str.value ) { $( "" ).text( msg ). insertBefore( $input ); valid = false; } } else { if( !fields.expression.value.test( $input.val() ) ) { $( "" ).text( msg ). insertBefore( $input ); valid = false; } } }); }); return valid; }
校验会发生在通过检查是否当前字段需要一个简单的字符串比较(或者data-type="string"
)或者一个正则表达式(data-type="expression"
)。所有的测试都基于requiredFields
属性,如果有错误,将通过data-message
来显示错误信息。
注意,上面的字段校验只是用于简单的颜色目的,并不是十分完美。实际项目中建议使用一些jQuery插件,如jQuery Validation。
最后一个私有方法是注册用户在结算表单中填写的信息:
/* Save the data entered by the user in the checkout form * @param form Object the jQuery element of the checkout form * @returns void */ _saveFormData: function( form ) { var self = this; var $visibleSet = form.find( "fieldset:visible" ); $visibleSet.each(function() { var $set = $( this ); if( $set.is( "#fieldset-billing" ) ) { var name = $( "#name", $set ).val(); var email = $( "#email", $set ).val(); var city = $( "#city", $set ).val(); var address = $( "#address", $set ).val(); var zip = $( "#zip", $set ).val(); var country = $( "#country", $set ).val(); self.storage.setItem( "billing-name", name ); self.storage.setItem( "billing-email", email ); self.storage.setItem( "billing-city", city ); self.storage.setItem( "billing-address", address ); self.storage.setItem( "billing-zip", zip ); self.storage.setItem( "billing-country", country ); } else { var sName = $( "#sname", $set ).val(); var sEmail = $( "#semail", $set ).val(); var sCity = $( "#scity", $set ).val(); var sAddress = $( "#saddress", $set ).val(); var sZip = $( "#szip", $set ).val(); var sCountry = $( "#scountry", $set ).val(); self.storage.setItem( "shipping-name", sName ); self.storage.setItem( "shipping-email", sEmail ); self.storage.setItem( "shipping-city", sCity ); self.storage.setItem( "shipping-address", sAddress ); self.storage.setItem( "shipping-zip", sZip ); self.storage.setItem( "shipping-country", sCountry ); } }); }
当用户提交结算页面表单后,session storage会被添加类似下面的数据:
Key | Value |
billing-name | John Doe |
billing-email | jdoe@localhost |
billing-city | New York |
billing-address | Street 1 |
billing-zip | 1234 |
billing-country | USA |
公共方法
公共方法在初始化方法(init()
)中被调用。第一件事是要在会话存储(session storage)中初始化键值对数据。
// Creates the cart keys in session storage createCart: function() { if( this.storage.getItem( this.cartName ) == null ) { var cart = {}; cart.items = []; this.storage.setItem( this.cartName, this._toJSONString( cart ) ); this.storage.setItem( this.shippingRates, "0" ); this.storage.setItem( this.total, "0" ); } }
第一个if
测试是检测数据是否已经被添加到 session storage 中,因为这个方法可以在页面加载完成后再次被调用,如果已经存在数据,保证它们不会被覆盖。
现在session storage类似下面的样子:
Key | Value |
winery-cart | {“items”:[]} |
winery-shipping-rates | 0 |
winery-total | 0 |
现在,我们需要处理用户添加商品到购物车的事件。
// Adds items to shopping cart handleAddToCartForm: function() { var self = this; self.$formAddToCart.each(function() { var $form = $( this ); var $product = $form.parent(); var price = self._convertString( $product.data( "price" ) ); var name = $product.data( "name" ); $form.on( "submit", function() { var qty = self._convertString( $form.find( ".qty" ).val() ); var subTotal = qty * price; var total = self._convertString( self.storage.getItem( self.total ) ); var sTotal = total + subTotal; self.storage.setItem( self.total, sTotal ); self._addToCart({ product: name, price: price, qty: qty }); var shipping = self._convertString( self.storage.getItem( self.shippingRates ) ); var shippingRates = self._calculateShipping( qty ); var totalShipping = shipping + shippingRates; self.storage.setItem( self.shippingRates, totalShipping ); }); }); }
每次用户提交这个表单,都需要获取用户添加的商品数量,并用这个数量来计算商品的价格。然后,读取session storage中的商品总价并将它更新。完成上面的步骤后,调用_addToCart()
方法将商品的明细存储在session storage中。
如果某个用户选择了一个商品Wine #1
,价格为5.00
,购买数量为5。那么他提交表单后,session storage的数据类似下面的样子。
Key | Value |
winery-cart | {“items”:[{“product”:”Wine #1″,”price”:5,”qty”:5}]} |
winery-shipping-rates | 0 |
winery-total | 25 |
加入用户这时又返回去购买了Wine #2
,商品价格为8.00
,数量为2:
Key | Value |
winery-cart | {“items”:[{“product”:”Wine #1″,”price”:5,”qty”:5},{“product”:”Wine #2″,”price”:8,”qty”:2}]} |
winery-shipping-rates | 0 |
winery-total | 25 |
当用户到购物车页面或结账页面的时候,我们需要准确的显示购物车的明细:
// Displays the shopping cart displayCart: function() { if( this.$formCart.length ) { var cart = this._toJSONObject( this.storage.getItem( this.cartName ) ); var items = cart.items; var $tableCart = this.$formCart.find( ".shopping-cart" ); var $tableCartBody = $tableCart.find( "tbody" ); for( var i = 0; i < items.length; ++i ) { var item = items[i]; var product = item.product; var price = this.currency + " " + item.price; var qty = item.qty; var html = "<tr><td class='pname'>" + product + "</td>" + "<td class='pqty'><input type='text' value='" + qty + "' class='qty'/></td>" + "<td class='pprice'>" + price + "</td></tr>"; $tableCartBody.html( $tableCartBody.html() + html ); } var total = this.storage.getItem( this.total ); this.$subTotal[0].innerHTML = this.currency + " " + total; } else if( this.$checkoutCart.length ) { var checkoutCart = this._toJSONObject( this.storage.getItem( this.cartName ) ); var cartItems = checkoutCart.items; var $cartBody = this.$checkoutCart.find( "tbody" ); for( var j = 0; j < cartItems.length; ++j ) { var cartItem = cartItems[j]; var cartProduct = cartItem.product; var cartPrice = this.currency + " " + cartItem.price; var cartQty = cartItem.qty; var cartHTML = "<tr><td class='pname'>" + cartProduct + "</td>" + "<td class='pqty'>" + cartQty + "</td>" + "<td class='pprice'>" + cartPrice + "</td></tr>"; $cartBody.html( $cartBody.html() + cartHTML ); } var cartTotal = this.storage.getItem( this.total ); var cartShipping = this.storage.getItem( this.shippingRates ); var subTot = this._convertString( cartTotal ) + this._convertString( cartShipping ); this.$subTotal[0].innerHTML = this.currency + " " + this._convertNumber( subTot ); this.$shipping[0].innerHTML = this.currency + " " + cartShipping; } }
如果是在购物车界面,该方法会遍历包含winery-cart
键的数组对象,并找到相关的数据来填充购物车表格。在例子中没有添加删除商品的功能。
然后我们需要一个方法来更新购物车中商品的数量:
// Updates the cart updateCart: function() { var self = this; if( self.$updateCartBtn.length ) { self.$updateCartBtn.on( "click", function() { var $rows = self.$formCart.find( "tbody tr" ); var cart = self.storage.getItem( self.cartName ); var shippingRates = self.storage.getItem( self.shippingRates ); var total = self.storage.getItem( self.total ); var updatedTotal = 0; var totalQty = 0; var updatedCart = {}; updatedCart.items = []; $rows.each(function() { var $row = $( this ); var pname = $.trim( $row.find( ".pname" ).text() ); var pqty = self._convertString( $row.find( ".pqty > .qty" ).val() ); var pprice = self._convertString( self._extractPrice( $row.find( ".pprice" ) ) ); var cartObj = { product: pname, price: pprice, qty: pqty }; updatedCart.items.push( cartObj ); var subTotal = pqty * pprice; updatedTotal += subTotal; totalQty += pqty; }); self.storage.setItem( self.total, self._convertNumber( updatedTotal ) ); self.storage.setItem( self.shippingRates, self._convertNumber( self._calculateShipping( totalQty ) ) ); self.storage.setItem( self.cartName, self._toJSONString( updatedCart ) ); }); } }
这个方法遍历相关的购物车表格,并在winery-cart
键中插入一个新的对象,同时它会重新计算商品的价格和运费。
加入会员将Wine #2
的数量从2变为6:
Key | Value |
winery-cart | {“items”:[{“product”:”Wine #1″,”price”:5,”qty”:5},{“product”:”Wine #2″,”price”:8,”qty”:6},{“product”:”Wine #3″,”price”:11,”qty”:6}]} |
winery-shipping-rates | 20 |
winery-total | 139 |
如果用户想清空购物车,使用的是下面的方法:
// Empties the cart by calling the _emptyCart() method // @see $.Shop._emptyCart() emptyCart: function() { var self = this; if( self.$emptyCartBtn.length ) { self.$emptyCartBtn.on( "click", function() { self._emptyCart(); }); } }
现在,用户清空购物车并可以重新开始购物。当用户购物完毕,我们需要在他们填写个人信息表单后处理结账表单:
// Handles the checkout form by adding a validation routine and saving user’s info in session storage handleCheckoutOrderForm: function() { var self = this; if( self.$checkoutOrderForm.length ) { var $sameAsBilling = $( "#same-as-billing" ); $sameAsBilling.on( "change", function() { var $check = $( this ); if( $check.prop( "checked" ) ) { $( "#fieldset-shipping" ).slideUp( "normal" ); } else { $( "#fieldset-shipping" ).slideDown( "normal" ); } }); self.$checkoutOrderForm.on( "submit", function() { var $form = $( this ); var valid = self._validateForm( $form ); if( !valid ) { return valid; } else { self._saveFormData( $form ); } }); } }
首先,如果用户指定计费信息和运费信息相同的话,要隐藏运费字段。可以使用change
事件,结合jQuery的.prop()方法来实现。
然后,我们需要验证表单的各个字段,如果有错误的话返回false
,以阻止表单的提交。如果字段全部验证成功,将用户的信息保存到session storage中,例如:
Key | Value |
winery-cart | {“items”:[{“product”:”Wine #1″,”price”:5,”qty”:5},{“product”:”Wine #2″,”price”:8,”qty”:6},{“product”:”Wine #3″,”price”:11,”qty”:6}]} |
winery-shipping-rates | 20 |
winery-total | 139 |
billing-name | John Doe |
billing-email | jdoe@localhost |
billing-city | New York |
billing-address | Street 1 |
billing-zip | 1234 |
billing-country | USA |
到这里,一个客户端的购物车系统就基本完成了。下面还有一些相关的阅读资料,建议大家阅读一下:
- Mozilla Developer Network-DOM Storage Guide
- Nicholas C. Zakas-Introduction to Session Storage
- Mozilla Developer Network-Using data-* Attributes