前言
本篇將完成前端頁面的設計與開發(fā),包括:
- 使用Bootstrap開發(fā)頁面結構
- 交互邏輯編程
一、使用Bootstrap開發(fā)頁面結構
在設計SeckillController中我們已經設置了jsp文件的路徑,在/WEB-INF/新建一個jsp目錄,在該目錄下新建list.jsp和detail.jsp
使用Bootstrap的模板,這個模板基本上是固定的
<%@ page language="java" contentType="text/html; charset=UTF-8" %>
<!DOCTYPE html>
<html>
<head>
<title>Bootstrap 模板</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 引入 Bootstrap -->
<link rel="stylesheet">
<!-- HTML5 Shim 和 Respond.js 用于讓 IE8 支持 HTML5元素和媒體查詢 -->
<!-- 注意: 如果通過 file:// 引入 Respond.js 文件,則該文件無法起效果 -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
<![endif]-->
</head>
<body>
<h1>Hello, world!</h1>
<!-- jQuery (Bootstrap 的 JavaScript 插件需要引入 jQuery) -->
<script src="https://code.jquery.com/jquery.js"></script>
<!-- 包括所有已編譯的插件 -->
<script src="js/bootstrap.min.js"></script>
</body>
</html>
1、list.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" %>
<!-- 引入jstl -->
<%@ include file="common/tag.jsp" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>秒殺列表頁</title>
<%@ include file="common/head.jsp" %>
</head>
<body>
</body>
<!-- jQuery文件。務必在bootstrap.min.js 之前引入 -->
<script src="http://cdn.static.runoob.com/libs/jquery/2.1.1/jquery.min.js"></script>
<!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
<script src="http://cdn.static.runoob.com/libs/bootstrap/3.3.7/js/bootstrap.min.js"></script>
</html>
在最上面的jsp內置對象page中的contentType修改為UTF-8,這個模板已經引入了一些文件包含了 jquery.js、bootstrap.min.js 和 bootstrap.min.css 文件,用于讓一個常規(guī)的 HTML 文件變?yōu)槭褂昧薆ootstrap的模板
最下面有兩個script標簽,通過CDN加載一些Bootstrap資源,** JavaScript有一個先后引入規(guī)則,jQuery作為Bootstrap的底層依賴,要先于Bootstrap聲明 **,這兩個script標簽在上面介紹的網站上都有
這里有些通用的標簽以及要引入的文件都單獨提取出來,不用把這些相同的代碼都寫在每一個頁面中
在jsp目錄下新建一個common目錄,專門存放通用的jsp文件
新建一個tag.jsp,用于引入jstl,如果以后還要引入別的標簽,再添加
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
新建一個head.jsp,head標簽中的內容所有頁面基本都一樣
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 引入 Bootstrap -->
<link rel="stylesheet">
<!-- HTML5 Shim 和 Respond.js 用于讓 IE8 支持 HTML5元素和媒體查詢 -->
<!-- 注意: 如果通過 file:// 引入 Respond.js 文件,則該文件無法起效果 -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
<![endif]-->
然后使用jsp的內置對象include,靜態(tài)引入head.jsp,** 靜態(tài)包含 是會把引入的文件合并過來 ,也就是head.jsp中的內容會放到外層list.jsp中作為一個Servlet輸出,如果是 動態(tài)包含 的話,那么head.jsp會作為一個 獨立的jsp,先轉換為Servlet **,轉換后的結果再和list.jsp合并
接著開始編寫lsit.jsp的細節(jié)部分

panel-default、text-center都是使用Bootstrap提供的樣式
在panel-body中使用表格,通過jstl提供的方法來顯示要展示的秒殺商品
<thead>
<tr>
<th>名稱</th>
<th>庫存</th>
<th>開始時間</th>
<th>結束時間</th>
<th>創(chuàng)建時間</th>
<th>詳情頁</th>
</tr>
</thead>
<tbody>
<c:forEach var="sk" items="${list}">
<tr>
<td>${sk.name}</td>
<td>${sk.number}</td>
<td>
<fmt:formatDate value="${sk.startTime}" pattern="yyyy-MM-dd HH:mm:ss"/>
</td>
<td>
<fmt:formatDate value="${sk.endTime}" pattern="yyyy-MM-dd HH:mm:ss"/>
</td>
<td>
<fmt:formatDate value="${sk.createTime}" pattern="yyyy-MM-dd HH:mm:ss"/>
</td>
<td>
<a class="btn btn-info" href="/seckill/${sk.seckillId}/detail" target="_blank">link</a>
</td>
</tr>
</c:forEach>
</tbody>
首先使用jstl的c:forEach標簽,用來迭代從SeckillController中的list方法傳過來的"list",這個list是存放秒殺的商品,屬性var代表當前項目的變量名,items表示進行循環(huán)的項目
一個tr標簽是一行,每個td標簽是一列,數(shù)據庫有多少個秒殺商品這個表格就有多少行
@RequestMapping(value = "/list", method = RequestMethod.GET)
public String list(Model model){
//獲取列表頁
List<Seckill> list = seckillService.getSeckillList();
model.addAttribute("list", list);
return "list";
}
從SeckillController的list方法返回的是字符串,但是之前說過,Spring MVC會拼接成一個URL地址,返回的數(shù)據是個泛型,類型是Seckill
public class Seckill {
private long seckillId;
private String name;
private int number;
private Date startTime;
private Date endTime;
private Date createTime;
}
這是Seckill定義的屬性,所以在list.jsp頁面中通過sk.name來調用相關的參數(shù)
日期類型的輸出默認是直接調用日期類型的toString,這不符合我們的規(guī)范,所以使用jstl的fmt:formatDate標簽來格式化輸出的時間
最后一列給一個超鏈接,用于鏈接這個秒殺商品的詳情頁,可以把這個超鏈接做成一個按鈕,使用的也是Bootstrap的CSS
2、detail.jsp

這是detail.jsp的一個大的框架,先是由兩個div組成,一個用于顯示日期或者文本的一個顯示面板,在顯示面板中做一個埋點,因為這個面板在之后的交互邏輯編碼中,在不同時間顯示的是不同的內容
<h1>${seckill.name }</h1>
這里可以直接這樣寫的原因是:
model.addAttribute("seckill", seckill);//SeckillController中的detail方法
另一個div就是登錄彈出層,在進入詳情頁的時候,會通過Cookie判斷用戶時候登錄,沒有登錄的用戶的頁面會顯示這個登錄彈出層,提示用戶登錄

首先在最外圍的div中進行埋點
<div id="killPhoneModal" class="modal fade">
因為這個登錄彈出層不是每次用戶到詳情頁都要出現(xiàn),只有驗證Cookie中沒有用戶登錄信息才會出現(xiàn),所以在這里埋點,如果Cookie中有用戶的信息,在交互邏輯中我們會控制這個div不出現(xiàn)
登錄彈出層實際是一個模態(tài)框,在頁面顯示的時候主要由三個部分:
- modal-header:顯示一些文本
- modal-body:用戶輸入登錄信息
- modal-footer:登錄按鈕
<div class="modal-header">
<h3 class="modal-title text-center">
<span class="glyphicon glyphicon-phone"></span>秒殺電話:
</h3>
</div>
在modal-header中有個span面板用于顯示一些文本和圖標
<div class="modal-body">
<div class="row">
<div class="col-xs-8 col-xs-offset-2">
<input type="text" name="killPhone" id="killPhoneKey"
placeholder="填寫手機號^o^" class="form-control">
</div>
</div>
</div>
在modal-body中有一個輸入框,這里需要在輸入框中進行埋點,之后的交互邏輯要通過這個埋點來獲取用戶輸入的信息
<div class="modal-footer">
<!-- 驗證信息 -->
<span id="killPhoneMessage" class="glyphicon"></span>
<button type="button" id="killPhoneBtn" class="btn btn-success">
<span class="glyphicon glyphicon-phone"></span>
</button>
</div>
在modal-footer中由兩部分組成:
- span:顯示錯誤信息
- button:登錄按鈕
在button中也需要埋點,用于綁定點擊事件
body標簽中的內容完成了,下面也要通過CDN引入一些文件
<!-- jQuery文件。務必在bootstrap.min.js 之前引入 -->
<script src="http://cdn.static.runoob.com/libs/jquery/2.1.1/jquery.min.js"></script>
<!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
<script src="http://cdn.static.runoob.com/libs/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<!-- 使用CDN獲取公共js -->
<!-- jQuery cookie操作插件 -->
<script src="http://cdn.bootcss.com/jquery-cookie/1.4.1/jquery.cookie.js"></script>
<!-- jQuery countDown倒計時插件 -->
<script src="http://cdn.bootcss.com/jquery.countdown/2.2.0/jquery.countdown.min.js"></script>
jquery文件和bootstrap.min.js之前在list.jsp也引入了
對Cookie的操作使用jQuery Cookie插件,倒計時使用jQuery的countDown插件
2、交互邏輯
1、交互流程

當用戶點擊某一個秒殺商品的按鈕的時候,會進入到相應的詳情頁,這個詳情頁會判斷用戶是否登錄過,如果登錄過就展示詳情頁頁面,如果沒有登錄過,就彈出登錄彈出層,在用戶正確填寫登錄信息后就可以進入詳情頁

獲取標準系統(tǒng)時間,因為用戶可能處在不同的時區(qū),用戶終端的時間也不可能完全一致,所以要統(tǒng)一地采用一個標準時間,也就是服務器時間
-
通過秒殺商品的開始時間和結束時間來做出不同的判斷:
- 系統(tǒng)時間大于結束時間:秒殺活動已結束,在detail.jsp的顯示面板顯示“秒殺結束”字樣
- 系統(tǒng)時間小于開始時間:秒殺活動未開始,在detail.jsp的顯示面板顯示倒計時,使用的是jQuery的countDown插件,倒計時完成后,會出現(xiàn)秒殺按鈕,用戶可以執(zhí)行秒殺操作
- 系統(tǒng)時間介于開始時間和結束時間之間:秒殺活動正在進行,直接出現(xiàn)秒殺按鈕,用戶可以執(zhí)行秒殺操作
2、頁面展示





3、交互邏輯編程
在src/main/webapp目錄下新建一個resources文件夾,再在其中新建一個script文件夾,用于存放腳本文件
創(chuàng)建一個seckill.js

這是最后完成的總覽,接著一步步來,整個seckil這樣寫的原因是模擬高級語言分包的概念,使JavaScript模塊化,這樣當調用一個方法可以用seckill.detail.init(params)的形式
在詳情頁初始化中,首先要做的就是獲取killPhone節(jié)點,這個killPhone節(jié)點不是程序中具體的標簽,而是Cookie中的用于標識用戶信息的數(shù)據,用戶的信息都放在Cookie中名為killPhone的節(jié)點
//在cookie中查找手機號
var killPhone = $.cookie('killPhone');
//驗證手機號
if(!seckill.validatePhone(killPhone)){
var killPhoneModal = $('#killPhoneModal');
killPhoneModal.modal({
show : true,//顯示登錄彈出層
backdrop : 'static',//禁止位置關閉
keyboard : false//關閉鍵盤事件
});
$('#killPhoneBtn').click(function(){
var inputPhone = $('#killPhoneKey').val();
if(seckill.validatePhone(inputPhone)){
$.cookie('killPhone', inputPhone, {expires:7, path:'/seckill'});//手機號寫入cookie
window.location.reload();//刷新頁面
}else{
$('#killPhoneMessage').hide().html('<label class="label label-danger">手機號錯誤!</label>').show(300);
}
});
}
從Cookie的killPhone中獲取數(shù)據后,就要驗證手機號,驗證手機號的邏輯建議提取到更上層,因為可能多個地方都要用到
創(chuàng)建一個函數(shù),名字為validatePhone,這個函數(shù)的位置在這一節(jié)最開始的圖片上可以看到
//驗證手機號
validatePhone : function(phone){
if(phone && phone.length == 11 && !isNaN(phone)){
return true;
}else{
return false;
}
},
要驗證手機號,所以傳入一個手機號的參數(shù),這里使用if語句簡單的判斷一下
首先要判斷手機號是否為空,在js中直接傳入參數(shù),它會判斷這個參數(shù)是否為空,空的話就是undefine,就認為是false
手機號長度必須為11位
isNaN是判斷這個參數(shù)是否是非數(shù)字,如果是非數(shù)字的話就是true,所以這里要取反
接著就可以在init方法中調用validatePhone函數(shù)來驗證手機號
if(!seckill.validatePhone(killPhone)){
var killPhoneModal = $('#killPhoneModal');
killPhoneModal.modal({
show : true,//顯示登錄彈出層
backdrop : 'static',//禁止位置關閉
keyboard : false//關閉鍵盤事件
});
如果手機號存在,就可以直接跳轉到詳情頁了,所以這里處理手機號不存在的情況,因為這個if語句中東西比較多,所以分開來說,完整的代碼在前面已經展示過了
手機號不存在,就需要用戶進行綁定,之前在detail.jsp中也提前做好了一個登錄彈出層,并進行了埋點

id為killPhoneModal,在seckill.js中使用jQuery的選擇器可以取到這個節(jié)點
var killPhoneModal = $('#killPhoneModal');
這個登錄彈出層已經不是單純的div了,因為使用了Bootstrap的modal,它本身有一個modal的方法,向這個方法傳入json, 用于設置這個模態(tài)框的一些屬性
之前在detail.jsp中這個modal的屬性為fade,是隱藏的,既然要讓用戶綁定手機號,所以要把這個彈出層顯示出來
killPhoneModal.modal({
show : true,//顯示登錄彈出層
backdrop : 'static',//禁止位置關閉
keyboard : false//關閉鍵盤事件
我們希望在用戶沒有正確的填寫手機號之前,是不能關掉這個彈出層,所以把backdrop關掉,因為用戶點擊其他區(qū)域可能把這個彈出層關掉;通過鍵盤的ESC也可能關閉彈出層,所以要禁止鍵盤事件
彈出層顯示出來后,要給按鈕做事件綁定
$('#killPhoneBtn').click(function(){
var inputPhone = $('#killPhoneKey').val();
if(seckill.validatePhone(inputPhone)){
$.cookie('killPhone', inputPhone, {expires:7, path:'/seckill'});//手機號寫入cookie
window.location.reload();//刷新頁面
}else{
$('#killPhoneMessage').hide().html('<label class="label label-danger">手機號錯誤!</label>').show(300);
}
});
}
按鈕事件綁定完成后整個驗證手機號的if語句才完成了
對按鈕做綁定,首先就是要獲取到按鈕在詳情頁的節(jié)點
<div class="modal-footer">
<!-- 驗證信息 -->
<span id="killPhoneMessage" class="glyphicon"></span>
<button type="button" id="killPhoneBtn" class="btn btn-success">
<span class="glyphicon glyphicon-phone"></span>
</button>
</div>
可以看到,按鈕的節(jié)點為killPhoneBtn
當用戶點擊了按鈕,我們認為用戶已經填寫了在登錄彈出層的input
<div class="modal-body">
<div class="row">
<div class="col-xs-8 col-xs-offset-2">
<input type="text" name="killPhone" id="killPhoneKey"
placeholder="填寫手機號^o^" class="form-control">
</div>
</div>
</div>
在input中,之前已經提前進行了埋點,id為killPhoneKey
在seckill.js中獲取到這個節(jié)點,同時使用val()方法獲取到用戶輸入的內容
var inputPhone = $('#killPhoneKey').val();
拿到用戶輸入的內容,還要再進行驗證,再調用用于驗證手機號的函數(shù)validatePhone
if(seckill.validatePhone(inputPhone)){
$.cookie('killPhone', inputPhone, {expires:7, path:'/seckill'});//手機號寫入cookie
window.location.reload();//刷新頁面
}else{
$('#killPhoneMessage').hide().html('<label class="label label-danger">手機號錯誤!</label>').show(300);
}
如果驗證通過了,先將inputPhone的值也就是用戶輸入的手機號寫入Cookie中
- expires:Cookie的有效期,單位是“天”
- path:給出有效路徑,Cookie只在該路徑下有效
為什么path不寫全路徑?
因為當一些URL沒有用到這個Cookie的時候,如果把Cookie中的path設置為全路徑,那么這個Cookie中的數(shù)據也會傳遞到后端,對后端處理會有一些影響,所以這只這個killPhone只在seckill模塊下有效
然后就是刷新頁面,會重新調用detail屬性的init方法
如果驗證沒有通過,在detail.jsp中登錄彈出層的modal-footer提前預留了一個span,用于顯示錯誤信息
<span id="killPhoneMessage" class="glyphicon"></span>
同樣,在seckill.js中獲取到這個span節(jié)點
$('#killPhoneMessage').hide().html('<label class="label label-danger">手機號錯誤!</label>').show(300);
對html標簽進行操作的時候,通常是先隱藏一下,避免用戶看到中間過程,然后插入一些內容,顯示的時候給一個時間,單位毫秒,這樣看起來有動態(tài)的效果
插入的是label標簽,使用Bootstrap的CSS,這里顯示的文本沒有經過處理,直接是寫死了,實際的工作中這里應該是要配合前端的數(shù)據字典,根據不同的情況顯示不同的文本
至此,詳情頁初始化部分完成,也就是開頭的if語句
整個前端的流程基本完成

接著是詳情頁的流程

首先就是要獲取標準系統(tǒng)時間
所以在detail.jsp的最下面添加一些內容,首先是要引入seckill.js
<!-- 開始編寫交互邏輯 -->
<script src="/resources/script/seckill.js" type="text/javascript"></script>
然后使用EL表達式傳入參數(shù)
<script type="text/javascript">
$(function(){
//使用EL表達式傳入參數(shù)
seckill.detail.init({
seckillId : "${seckill.seckillId}",
startTime : "${seckill.startTime.time}",
endTime : "${seckill.endTime.time}"
});
});
</script>
接著在seckill.js中獲取到這些參數(shù)
//已經登錄
//計時交互邏輯
var startTime = parseInt(params['startTime']);
var endTime = parseInt(params['endTime']);
var seckillId = parseInt(params['seckillId']);
$.get(seckill.URL.now(), {}, function(result){
if(result && result['success']){
var nowTime = result['data'];
//時間判斷,計時交互
seckill.countdown(seckillId, nowTime, startTime, endTime);
}else{
console.log('result: ' + result);
}
});
** 這里從列表頁傳遞過來的日期參數(shù)需要轉型,否則之后會出現(xiàn)日期無效的情況 **
然后通過ajax請求來獲取到系統(tǒng)當前時間
@RequestMapping(value = "/time/now", method = RequestMethod.GET)
@ResponseBody
public SeckillResult<Long> time(){
Date now = new Date();
return new SeckillResult<Long>(true, now.getTime());
}
在SeckillController中的time方法就是用來獲取系統(tǒng)時間的,在@RequestMapping注解中顯示系統(tǒng)當前時間的URL是“/time/now”,限制了請求方式為GET,所以在seckill.js中使用$.get()方法
簡單說下$.get()方法
$.get(URL,data,function(data,status,xhr),dataType)
- URL:必需,規(guī)定您需要請求的 URL
- data:可選,規(guī)定連同請求發(fā)送到服務器的數(shù)據
- function(data,status,xhr):可選,規(guī)定當請求成功時運行的函數(shù)
- data:包含來自請求的結果數(shù)據
- status:包含請求的狀態(tài)("success"、"notmodified"、"error"、"timeout"、"parsererror")
- xhr:包含 XMLHttpRequest 對象
- dataType:可選,規(guī)定預期的服務器響應的數(shù)據類型,默認地,jQuery 會智能判斷。
可能的類型:- xml - 一個 XML 文檔
- html - HTML 作為純文本
- text - 純文本字符串
- script - 以 JavaScript 運行響應,并以純文本返回
- json - 以 JSON 運行響應,并以 JavaScript 對象返回
- jsonp - 使用 JSONP 加載一個 JSON 塊,將添加一個 "?callback=?" 到 URL 來規(guī)定回調
$.get(seckill.URL.now, {}, function(result){
if(result && result['success']){
var nowTime = result['data'];
//時間判斷,計時交互
seckill.countdown(seckillId, nowTime, startTime, endTime);
}else{
console.log('result: ' + result);
}
});
第一個參數(shù)是請求的URL,由于URL太多,為了后期維護、代碼的整潔,所以要對URL進行統(tǒng)一的管理,在seckill中新建一個屬性URL,用于封裝秒殺相關ajax的URL
//封裝秒殺相關ajax的URL
URl : {
now : function(){
return '/seckill/time/now';
}
},
在SeckillController中的time方法返回的是SeckillResult<Long>類型的對象
public class SeckillResult<T> {
private boolean success;
private T data;
private String error;
}
這是SeckillResult中定義的屬性,其中success是判斷是否成功請求,所以在$.get()方法的回調函數(shù)中要判斷請求是否為空,如果不為空,則在控制臺輸出信息
if(result && result['success']){
var nowTime = result['data'];
//時間判斷,計時交互
seckill.countdown(seckillId, nowTime, startTime, endTime);
}else{
console.log('result: ' + result);
}
如果請求成功,就可以獲取到系統(tǒng)當前時間,再加上之前獲取到的三個參數(shù),就可以進行時間判斷,判斷系統(tǒng)當前時間在不在秒殺活動期內,如果不在是秒殺未開始還是秒殺已結束
在seckill中創(chuàng)建countdown函數(shù),用于時間判斷
countdown : function(seckillId, nowTime, startTime, endTime){
var seckillBox = $('#seckill-box');
//時間判斷
if(nowTime > endTime){
//秒殺結束
seckillBox.html('秒殺結束!');
}else if(nowTime < startTime){
//秒殺未開始,計時事件綁定
var killTime = new Date(startTime + 1000);//設置基準時間
seckillBox.countdown(killTime, function(event){
//時間格式
var format = event.strftime('秒殺倒計時: %D天 %H時 %M分 %S秒');
//時間完成后回調事件
}).on('finish.countdown', function(){
//調用執(zhí)行秒殺的函數(shù)
seckill.handleSeckill(seckillId, seckillBox);
});
}else{
//調用執(zhí)行秒殺的函數(shù)
seckill.handleSeckill(seckillId, seckillBox);
}
},
因為對于時間判斷的不同結果,要在詳情頁中展示不同的內容,所以在detail.jsp中專門設置了一個span,用于顯示時間判斷的結果
<div class="panel-body">
<h2 class="text-danger">
<!-- 顯示time圖標 -->
<span class="glyphicon glyphicon-time"></span>
<!-- 顯示面板 -->
<span class="glyphicon" id="seckill-box"></span>
</h2>
</div>
提前設置了埋點,id為seckill-box,在seckill.js通過jQuery的加載器獲取到這個span節(jié)點
然后進行時間判斷
if(nowTime > endTime){
//秒殺結束
seckillBox.html('秒殺結束!');
}
系統(tǒng)當前時間大于秒殺的結束時間,說明秒殺結束,這里不用和后端做通信,可以直接通過時間的判斷就再詳情頁顯示“秒殺結束”的字樣,因為時間到了,不管有沒有庫存,都無所謂了
if(nowTime < startTime){
//秒殺未開始,計時事件綁定
var killTime = new Date(startTime + 1000);//設置基準時間
seckillBox.countdown(killTime, function(event){
//時間格式
var format = event.strftime('秒殺倒計時: %D天 %H時 %M分 %S秒');
//時間完成后回調事件
}).on('finish.countdown', function(){
//調用執(zhí)行秒殺的函數(shù)
seckill.handleSeckill(seckillId, seckillBox);
});
}
系統(tǒng)當前時間小于秒殺開啟時間,秒殺未開始,在詳情頁顯示倒計時,既然是倒計時,就要給系統(tǒng)一個基準時間,其實也就是秒殺的開啟時間,但是這里在秒殺開始時間的基礎+1s,防止用戶端的計時偏移
接著使用Bootstrap提供的countdown方法,實際上就是一個事件綁定方法
seckillBox.countdown(killTime, function(event){
//時間格式
var format = event.strftime('秒殺倒計時: %D天 %H時 %M分 %S秒');
seckillBox.html(format);
//倒計時完成后回調事件
})
countdown事件綁定方法中也有一個回調函數(shù),當日期在不斷的變化的時候,這個回調函數(shù)會做相應的輸出,對日期的格式做個調整
countdown插件只是負責倒計時,倒計時完成后就可以執(zhí)行秒殺操作了,所以在countdown時間綁定后再接上一個事件操作
.on('finish.countdown', function(){
//調用執(zhí)行秒殺的函數(shù)
seckill.handleSeckill(seckillId, seckillBox);
});
事件的名字是finish.countdown,再加上一個回調函數(shù),用于倒計時完成后回調事件,在這個函數(shù)中要調用執(zhí)行秒殺的函數(shù)
這里把執(zhí)行秒殺的函數(shù)單獨的提取出來,一是降低耦合,二是避免代碼重復,因為在最初調用時間判斷函數(shù)countdown的時候,可能秒殺正在進行,而上面的代碼是秒殺未開始,倒計時完成后才可以執(zhí)行秒殺,在多個地方需要執(zhí)行秒殺的操作,所以要把執(zhí)行秒殺的操作單獨創(chuàng)建一個函數(shù)
handleSeckill : function(seckillId, node){
//獲取秒殺地址,控制顯示邏輯,執(zhí)行秒殺
node.hide()
.html('<button class="btn btn-primary btn-lg" id="killBtn">開始秒殺</button>');
$.post(seckill.URL.exposer(seckillId), {}, function(result){
//在回調函數(shù)中執(zhí)行交互流程
if(result && result['success']){
var exposer = result['data'];
if(exposer['exposed']){
//開啟秒殺,獲取秒殺地址
var md5 = exposer['md5'];
var killUrl = seckill.URL.execution(seckillId, md5);
console.log('killUrl: ' + killUrl);
//綁定一次點擊事件
$('#killBtn').one('click', function(){
//執(zhí)行秒殺請求
//1.禁用按鈕
$(this).addClass('disabled');
//2.發(fā)送秒殺請求執(zhí)行秒殺
$.post(killUrl, {}, function(result){
if(result && result['success']){
var killResult = result['data'];
var state = killResult['state'];
var stateInfo = killResult['stateInfo'];
//3.顯示秒殺結果
node.html('<span class="label label-success">' + stateInfo + '</span>');
}
});
});
node.show();
}else{
//未開啟秒殺
var now = exposer['now'];
var start = exposer['start'];
var end = exposer['end'];
seckill.countdown(seckillId, now, start, end);
}
}else{
console.log('result: ' + result);
}
});
}.
這個方法的參數(shù)有個node,用來獲取節(jié)點的,因為之前在detail.jsp中有專門顯示時間判斷的結果的span,當可以進行秒殺的時候,這個span顯示的就是一個按鈕,所以這里也要獲取這個span節(jié)點,來對這個span進行操作,加入一個button標簽
node.hide()
.html('<button class="btn btn-primary btn-lg" id="killBtn">開始秒殺</button>');
插入按鈕后先不要顯示出來,因為后面還要對用戶信息也就是手機號進行驗證
執(zhí)行秒殺操作之前,就要先取得秒殺的地址
@RequestMapping(
value = "/{seckillId}/exposer",
method = RequestMethod.POST,
produces = {"application/json;charset=UTF-8"})
@ResponseBody
public SeckillResult<Exposer> exposer(@PathVariable("seckillId") Long seckillId){
SeckillResult<Exposer> result;
try {
Exposer exposer = seckillService.exportSeckillUrl(seckillId);
result = new SeckillResult<Exposer>(true, exposer);
} catch (Exception e) {
logger.error(e.getMessage(), e);
result = new SeckillResult<Exposer>(false, e.getMessage());
}
return result;
}
在SeckillController的exposer方法就是用來暴露秒殺地址的,這個方法只接收POST請求,返回的是SeckillResult對象,類型是Exposer、
在seckill.js中使用$.post()方法,類似前面講過的$.get()方法
$.post(seckill.URL.exposer(seckillId), {}, function(result){
//在回調函數(shù)中執(zhí)行交互流程
if(result && result['success']){
var exposer = result['data'];
}else{
console.log('result: ' + result);
}
});
要傳入請求的URL,也要放在seckill的URL屬性中
exposer : function(seckillId){
return '/seckill/' + seckillId + '/exposer';
}
這個URL需要傳遞秒殺商品的id,因為不同的秒殺商品需要相應的UEL
首先還是要判斷ajax請求是否成功,如果沒有請求成功,在控制臺打印信息
如果請求成功,獲取$.post()方法返回過來的數(shù)據,是Exposer類型的,封裝在SeckillResult的data屬性中
public class Exposer {
//是否開啟秒殺
private boolean exposed;
//加密措施
private String md5;
//id
private long seckillId;
//系統(tǒng)當前時間(毫秒)
private long now;
//秒殺開啟時間
private long start;
//秒殺結束時間
private long end;
}
獲取到Exposer對象后,在Exposer類中有一個exposed屬性,用來判斷是否開啟秒殺,如果開啟秒殺,就要控制之前定義的按鈕,先綁定點擊事件,然后顯示出來
如果不開啟秒殺,就返回系統(tǒng)當前時間、秒殺開啟時間、秒殺結束時間,再調用countdown函數(shù)
if(exposer['exposed']){
}else{
//未開啟秒殺
var now = exposer['now'];
var start = exposer['start'];
var end = exposer['end'];
seckill.countdown(seckillId, now, start, end);
}
既然都到這一步了,什么情況下還是秒殺未開始?
當不同的終端顯示過長的時間的時候,可能出現(xiàn)一些偏差,用戶顯示已經開啟秒殺,但是實際上服務器的時間還沒到,雖然時間差很小,但是還是要重新計算計時邏輯,所以調用countdown函數(shù)
判斷開啟秒殺之后,先要獲取秒殺地址
//開啟秒殺,獲取秒殺地址
var md5 = exposer['md5'];
var killUrl = seckill.URL.execution(seckillId, md5);
console.log('killUrl: ' + killUrl);
用于執(zhí)行秒殺操作的URL需要經過MD5的加密,所以還要從后端獲取到MD5,同樣,ajax請求的URL都要封裝在seckill.js的URL屬性中
execution : function(seckillId, md5){
return '/seckill/' + seckillId + '/' + md5 + '/execution';
}
這些URL之前在Controller層都已經定義好的
@RequestMapping(
value = "/{seckillId}/{md5}/execution",
method = RequestMethod.POST,
produces = {"application/json;charset=UTF-8"})
@ResponseBody
public SeckillResult<SeckillExecution> execute(@PathVariable("seckillId") Long seckillId,
@PathVariable("md5") String md5,
@CookieValue(value = "killPhone", required = false) Long phone)
獲取到了執(zhí)行秒殺的URL,就可以控制按鈕,綁定點擊事件
//綁定一次點擊事件
$('#killBtn').one('click', function(){
//執(zhí)行秒殺請求
//1.禁用按鈕
$(this).addClass('disabled');
//2.發(fā)送秒殺請求執(zhí)行秒殺
$.post(killUrl, {}, function(result){
if(result && result['success']){
var killResult = result['data'];
var state = killResult['state'];
var stateInfo = killResult['stateInfo'];
//3.顯示秒殺結果
node.html('<span class="label label-success">' + stateInfo + '</span>');
}
});
});
但是只綁定一次點擊事件,防止用戶連續(xù)點擊,比如用戶不放心頁面是否響應,所以可能會連續(xù)的點擊按鈕,如果不在這控制的話,這些點擊最后都會發(fā)送到服務器端,會造成服務器端在同一時間接到大量相同的URL請求,對各方面都有影響
所以點擊完之后就要禁用按鈕,通過this指代當前對象,也就是相當于使用$('#killBtn')
之后就是發(fā)送秒殺請求,執(zhí)行秒殺操作,在SeckillController的execute方法只接收POST請求,所以使用$.post()方法
然后通過SeckillResult中的success屬性判斷是否請求成功
if(phone == null){
return new SeckillResult<SeckillExecution>(false, "未注冊");
}
//SeckillResult<SeckillExecution> result;
try {
SeckillExecution execution = seckillService.executeSeckill(seckillId, phone, md5);
return new SeckillResult<SeckillExecution>(true, execution);
} catch (RepeatKillException e) {
SeckillExecution execution = new SeckillExecution(seckillId, SeckillStateEnum.REPEAT_KILL);
return new SeckillResult<SeckillExecution>(true, execution);
} catch (SeckillCloseException e) {
SeckillExecution execution = new SeckillExecution(seckillId, SeckillStateEnum.END);
return new SeckillResult<SeckillExecution>(true, execution);
} catch (Exception e) {
logger.error(e.getMessage(), e);
SeckillExecution execution = new SeckillExecution(seckillId, SeckillStateEnum.INNER_ERROR);
return new SeckillResult<SeckillExecution>(true, execution);
}
這是SeckillController的execute方法,返回的都是SeckillExecution對象,這些對象存放在SeckillResult的data屬性中
public class SeckillExecution {
private long seckillId;
//秒殺結果執(zhí)行后的狀態(tài)
private int state;
//狀態(tài)信息
private String stateInfo;
//秒殺成功對象
private SuccessKilled successKilled;
}
這是SeckillExecution類中定義的方法,在seckill.js中獲取到這些屬性
$.post(killUrl, {}, function(result){
if(result && result['success']){
var killResult = result['data'];
var state = killResult['state'];
var stateInfo = killResult['stateInfo'];
//3.顯示秒殺結果
node.html('<span class="label label-success">' + stateInfo + '</span>');
}
});
獲取到執(zhí)行秒殺的結果后,還要在詳情頁中顯示出來,所以控制節(jié)點,輸出狀態(tài)信息,因為在SeckillController的execute方法中已經定義了重復秒殺、秒殺結束等異常也算請求成功,只是不對數(shù)據庫進行操作,但是結果信息要返回到詳情頁
最后就可以把按鈕顯示出來了
node.show();
至此,前端頁面完成了