高并發(fā)秒殺API(五)

前言

本篇將完成前端頁面的設計與開發(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é)部分

list.jsp

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

這是detail.jsp的一個大的框架,先是由兩個div組成,一個用于顯示日期或者文本的一個顯示面板,在顯示面板中做一個埋點,因為這個面板在之后的交互邏輯編碼中,在不同時間顯示的是不同的內容

<h1>${seckill.name }</h1>

這里可以直接這樣寫的原因是:

model.addAttribute("seckill", seckill);//SeckillController中的detail方法

另一個div就是登錄彈出層,在進入詳情頁的時候,會通過Cookie判斷用戶時候登錄,沒有登錄的用戶的頁面會顯示這個登錄彈出層,提示用戶登錄

detail.jsp中的登錄彈出層

首先在最外圍的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


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();

至此,前端頁面完成了

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容