態機簡介:
狀態機是有限狀態自動機的簡稱,是現實事物運行規則抽象而成的一個數學模型。【規則的抽象】
有限狀態機一般都有以下特點:
(1)可以用狀態來描述事物,并且任一時刻,事物總是處于一種狀態;
(2)事物擁有的狀態總數是有限的;
(3)通過觸發事物的某些行為,可以導致事物從一種狀態過渡到另一種狀態;
(4)事物狀態變化是有規則的,A狀態可以變換到B,B可以變換到C,A卻不一定能變換到C;
(5)同一種行為,可以將事物從多種狀態變成同種狀態,但是不能從同種狀態變成多種狀態。
狀態機這種描述客觀世界的方式就是將事物抽象成若干狀態,然后所有的事件和規則導致事物在這些狀態中游走。最終使得事物“自圓其說”。
很多通信協議的開發都必須用到狀態機;一個健壯的狀態機可以讓你的程序,不論發生何種突發事件都不會突然進入一個不可預知的程序分支。
四大概念:
狀態(state)
一個狀態機至少要包含兩個狀態。
分為:現態(源狀態)、次態(目標狀態)
狀態可以理解為一種結果,一種穩態形式,沒有擾動會保持不變的。
狀態命名形式:
1.副詞+動詞;例如:待審批、待支付、待收貨
這種命名方式體現了:狀態機就是事件觸發狀態不斷遷徙的本質。表達一種待觸發的感覺。
2.動詞+結果;例如:審批完成、支付完成
3.已+動詞形式;例如:已發貨、已付款
以上兩種命名方式體現了:狀態是一種結果或者穩態的本質。表達了一種已完成的感覺。
角色很多的時候,為了表示清晰,可以加上角色名:例如:待財務審批、主管批準
命名考慮從用戶的理解的角度出發。
事件(event)
or
觸發條件
又稱為“條件”,就是某個操作動作的觸發條件或者口令。當一個條件滿足時,就會觸發一個動作,或者執行一次狀態遷徙
這個事件可以是外部調用、監聽到消息、或者各種定時到期等觸發的事件。
對于燈泡,“打開開關”就是一個事件。
條件命名形式:動詞+結果;例如:支付成功、下單時間>5分鐘
動作(action)
事件發生以后要執行動作。例如:事件=“打開開關指令”,動作=“開燈”。一般就對應一個函數。
條件滿足后執行動作。動作執行完畢后,可以遷移到新的狀態,也可以仍舊保持原狀態。
動作不是必需的,當條件滿足后,也可以不執行任何動作,直接遷移到新狀態。
那么如何區分“動作”和“狀態”?
“動作”是不穩定的,即使沒有條件的觸發,“動作”一旦執行完畢就結束了;
而“狀態”是相對穩定的,如果沒有外部條件的觸發,一個狀態會一直持續下去。
變換(transition)
即從一個狀態變化到另外一個狀態
例如:“開燈過程”就是一個變化
狀態機其他表達方式:
狀態機的設計:
信息系統中有很多狀態機,例如:業務訂單的狀態。
狀態機的設計存在的問題:什么是狀態?到底有多少個狀態?要細分到什么程度?
信息系統是現實世界的一種抽象和描述。而業務領域中那些已經發生的事件就是事實,信息系統就是將這些事實以信息的形式存儲到數據庫中,即:信息就是一組事實
信息系統就是存儲這些事實,對這些事實進行管理與追蹤,進而起到提高工作效率的作用。
信息系統就是記錄已經發生的事實,信息系統中的狀態基本和事實匹配。即:標識某個事實的完成度。
業務系統,根據實際業務,具體會有哪些發生的事實需要記錄,基本上這些事實就至少對應一個狀態。需要記錄的事實就是一種穩態,一種結果。
例如:【待支付】->【已支付】->【已收貨】->【已評價】
這些都是系統需要記錄的已發生的客觀事實。而這些事實就對應了狀態,而發生這些事實的事件就對應了觸發狀態機的轉換的事件。
根據自己的業務實際進行分析,并畫出狀態圖即可。
狀態機實現方式:狀態模式
下面是經典的自動販賣機例子來說明狀態模式的用法,狀態圖如下:
分析一個這個狀態圖:
a、包含4個狀態(我們使用4個int型常量來表示)
b、包含3個暴露在外的方法(投幣、退幣、轉動手柄、(發貨動作是內部方法,售賣機未對外提供方法,售賣機自動調用))
c、我們需要處理每個狀態下,用戶都可以觸發這三個動作。
我們可以做沒有意義的事情,在【未投幣】狀態,試著退幣,或者同時投幣兩枚,此時機器會提示我們不能這么做。
實現邏輯:
任何一個可能的動作,我們都要檢查,看看我們所處的狀態和動作是否合適。
狀態機使用if-else或switch實現
測試自動售賣機
使用if-else/switch的方式實現狀態有如下問題:
例如:現在增加一個狀態。每個方法都需要添加if-else語句。
升級策略:
【封裝變化】,局部化每個狀態的行為,將每個狀態的行為放到各自類中,每個狀態只要實現自己的動作就可以了。
販賣機只要將動作委托給代表當前狀態的狀態對象即可。
public interface State
{
/**
* 放錢
*/
public void insertMoney();
/**
* 退錢
*/
public void backMoney();
/**
* 轉動曲柄
*/
public void turnCrank();
/**
* 出商品
*/
public void dispense();
}
public class NoMoneyState implements State
{
private VendingMachine machine;
public NoMoneyState(VendingMachine machine)
{
this.machine=machine;
}
@Override
public void insertMoney()
{
System.out.println("投幣成功");
machine.setState(machine.getHasMoneyState());
}
@Override
public void backMoney()
{
System.out.println("您未投幣,想退錢?...");
}
@Override
public void turnCrank()
{
System.out.println("您未投幣,想拿東西么?...");
}
@Override
public void dispense()
{
throw new IllegalStateException("非法狀態!");
}
}
public class HasMoneyState implements State
{
private VendingMachine machine;
public HasMoneyState(VendingMachine machine)
{
this.machine=machine;
}
@Override
public void insertMoney()
{
System.out.println("您已經投過幣了,無需再投....");
}
@Override
public void backMoney()
{
System.out.println("退幣成功");
machine.setState(machine.getNoMoneyState());
}
@Override
public void turnCrank()
{
System.out.println("你轉動了手柄");
machine.setState(machine.getSoldState());
}
@Override
public void dispense()
{
throw new IllegalStateException("非法狀態!");
}
}
public class SoldOutState implements State
{
private VendingMachine machine;
public SoldOutState(VendingMachine machine)
{
this.machine=machine;
}
@Override
public void insertMoney()
{
System.out.println("投幣失敗,商品已售罄");
}
@Override
public void backMoney()
{
System.out.println("您未投幣,想退錢么?...");
}
@Override
public void turnCrank()
{
System.out.println("商品售罄,轉動手柄也木有用");
}
@Override
public void dispense()
{
throw new IllegalStateException("非法狀態!");
}
}
public class SoldState implements State
{
private VendingMachine machine;
public SoldState(VendingMachine machine)
{
this.machine=machine;
}
@Override
public void insertMoney()
{
System.out.println("正在出貨,請勿投幣");
}
@Override
public void backMoney()
{
System.out.println("正在出貨,沒有可退的錢");
}
@Override
public void turnCrank()
{
System.out.println("正在出貨,請勿重復轉動手柄");
}
@Override
public void dispense()
{
machine.releaseBall();
if (machine.getCount() > 0)
{
machine.setState(machine.getNoMoneyState());
} else
{
System.out.println("商品已經售罄");
machine.setState(machine.getSoldOutState());
}
}
}
public class VendingMachine
{
private State noMoneyState;
private State hasMoneyState;
private State soldState;
private State soldOutState;
private State winnerState ;
private int count=0;
private State currentState=noMoneyState;
public VendingMachine(int count)
{
noMoneyState=new NoMoneyState(this);
hasMoneyState=new HasMoneyState(this);
soldState=new SoldState(this);
soldOutState=new SoldOutState(this);
winnerState=new WinnerState(this);
if (count > 0)
{
this.count=count;
currentState=noMoneyState;
}
}
//將這些動作委托給當前狀態.
public void insertMoney()
{
currentState.insertMoney();
}
public void backMoney()
{
currentState.backMoney();
}
// 機器不用提供dispense動作,因為這是一個內部動作.用戶不可以直
//接要求機器發放糖果.我們在狀態對象的turnCrank()方法中調用
//dispense方法;
//dispense無論如何,即使在nomoney狀態也會被執行.
//讓不合法的情形下,dispense拋出異常處理。
public void turnCrank()
{
currentState.turnCrank();
currentState.dispense();
}
public void releaseBall()
{
System.out.println("發出一件商品...");
if (count !=0)
{
count -=1;
}
}
public void setState(State state)
{
this.currentState=state;
}
//getter setter omitted ...
}
我們之前說過,if-else/switch實現方式沒有彈性,那現在按照這種實現模式,需求變更修改起來會輕松點嗎?
紅色部分標記了我們的需求變更:當用戶每次轉動手柄的時候,有10%的幾率贈送一瓶。
實現方式:
我們遵守了【開閉】原則,只要新建一個WinnerState的類即可。然后有限的修改has_money的轉向即可。
為什么WinnerState要獨立成一個狀態,其實它和sold狀態一模一樣。我把代碼寫在SoldState中不行嗎?
如果sold需求變化不一定影響到winner代碼實現,winner需求變化時,也不一定要修改sold,比如促銷方案結束了,中獎概率變了等。
如果他們的變化不是一定互相影響到彼此的,那我們就該將他們分離,即是【隔離變化】也是遵守【單一職責】的原則。
public class WinnerState implements State
{
private VendingMachine machine;
public WinnerState(VendingMachine machine)
{
this.machine=machine;
}
@Override
public void insertMoney()
{
throw new IllegalStateException("非法狀態");
}
@Override
public void backMoney()
{
throw new IllegalStateException("非法狀態");
}
@Override
public void turnCrank()
{
throw new IllegalStateException("非法狀態");
}
@Override
public void dispense()
{
System.out.println("你中獎了,恭喜你,將得到2件商品");
machine.releaseBall();
if (machine.getCount()==0)
{
System.out.println("商品已經售罄");
machine.setState(machine.getSoldOutState());
} else
{
machine.releaseBall();
if (machine.getCount() > 0)
{
machine.setState(machine.getNoMoneyState());
} else
{
System.out.println("商品已經售罄");
machine.setState(machine.getSoldOutState());
}
}
}
}
public class HasMoneyState implements State
{
private VendingMachine machine;
private Random random=new Random();
public HasMoneyState(VendingMachine machine)
{
this.machine=machine;
}
@Override
public void insertMoney()
{
System.out.println("您已經投過幣了,無需再投....");
}
@Override
public void backMoney()
{
System.out.println("退幣成功");
machine.setState(machine.getNoMoneyState());
}
@Override
public void turnCrank()
{
System.out.println("你轉動了手柄");
int winner=random.nextInt(10);
if (winner==0 && machine.getCount() > 1)
{
machine.setState(machine.getWinnerState());
} else
{
machine.setState(machine.getSoldState());
}
}
@Override
public void dispense()
{
throw new IllegalStateException("非法狀態!");
}
}
總結狀態模式:
狀態模式:允許對象在內部狀態改變時改變它的行為,對象看起來好像修改了他的類。
解釋:
狀態模式將狀態封裝成為獨立的類,并將動作委托到代表當前狀態的對象。
所以行為會隨著內部狀態改變而改變。
我們通過組合簡單引用不同狀態對象來造成類改變的假象.
狀態模式策略模式
1.行為封裝的n個狀態中,不同狀態不用行為。
2.context的行為委托到不同狀態中。
3.[當前狀態]在n個狀態中游走,context的行為也隨之[當前狀態]的改變而改變。
4.用戶對context的狀態改變渾然不知。
5.客戶不會直接和state交互,只能通過context暴露的方法交互,state轉換是context內部事情。
6.state可以是接口也可以是抽象類,取決于有沒公共功能可以放進抽象類中。抽象類方便,因為可以后續加方法。
可以將重復代碼放入抽象類中。例如:"你已投入25元,不能重復投" 這種通用代碼放入抽象類中。
7.context可以決定狀態流轉,如果這個狀態流轉是固定的,就適合放在context中進行。但是如果狀態流轉是動態的就適合放在狀態中進行。
例如通過商品的剩余數目來決定流向[已售完]或[等待投幣],這個時候放在狀態類中,因為dispense要根據狀態判斷流轉。
這個寫法決定了,有新需求時候,你是改context還是改state類。
8.可以共享所有的state對象,但是需要修改context的時候時候,需要handler中傳入context引用
1.context主動指定需要組合的策略對象是哪一個。
2.可以在啟動的時候通過工廠動態指定具體是哪個策略對象,但是沒有在策略對象之間游走,即:只組合了一個策略對象。
3.策略作為繼承之外一種彈性替代方案。因為繼承導致子類繼承不適用的方法,且每個類都要維護,策略模式通過不同對象組合來改變行為。
4.策略模式聚焦的是互換的算法來創建業務。
狀態機典型應用:訂單狀態控制
建表語句
如上圖所示:
一種典型的訂單設計。業務訂單和支付退款訂單組合,他們分別有自己的狀態機。
狀態機模式實現訂單狀態機:
日常開發過程中,狀態機模式應用場景之一的就是訂單模型中的狀態控制。但是區別于狀態模式的點有以下幾個:
以支付訂單為例:
/*
Title: PaymentInfo Description:
支付訂單狀態機
該類不可被spring管理,需要new出來,一個類就對應一條數據庫中支付訂單記錄
本文來自博客園,作者:wanglifeng,轉載請注明原文鏈接:https://www.cnblogs.com/wanglifeng717/p/16214122.html
@author wanglifeng
*/
public class PaymentStateMachine {
// 數據庫中當前支付訂單實體
private SapoPayment payment;
// 當前狀態
private PaymentState currentState;
// 需要更新入庫的支付訂單實體。與payment屬性配合,payment為當前數據庫中訂單實體,用于樂觀鎖的前置內容校驗。
private SapoPayment paymentForUpdate;
/* 將最新內容(含狀態)更新入庫,并當前狀態機狀態 */
public void updateStateMachine() {
// 從Spring容器中獲取操作數據的dao
SapoDao dao=SpringUtil.getBean(SapoDao.class);
// 更新數據庫,樂觀鎖機制:帶前置內容數據校驗,其中payment為前置內容,paymentForUpdate為要更新的內容,如果更新結果=0,說明該訂單被其他線程修改過。拋異常,放棄此次修改。
dao.updateSapoPaymentByNull(paymentForUpdate, payment);
// 記錄訂單操作流水
dao.insertSapoOrderStatusLog(SapoOrderStatusLog.getInstance().setOrderId(paymentForUpdate.getId())
.setOrderType(SapoOrderStatusLog.ORDER_TYPE_PAYMENT).setStatus(paymentForUpdate.getStatus()));
// 更新當前PaymentStateMachine狀態機
this.setPayment(paymentForUpdate);
this.setCurrentState(paymentForUpdate.getStatus());
}
// 通過條件獲取一個支付訂單PaymentStateMachine實體
public static PaymentStateMachine getInstance(SapoPayment sapoPaymentForQuery) {
// 1.從spring容器中獲取dao;
SapoDao dao=SpringUtil.getBean(SapoDao.class);
// 2.查出該支付訂單
SapoPayment paymentResult=dao.getSapoPayment(sapoPaymentForQuery);
// 3.初始化訂單狀態機
PaymentStateMachine paymentStateMachine=new PaymentStateMachine();
paymentStateMachine.setPayment(paymentResult);
paymentStateMachine.setCurrentState(paymentResult.getStatus());
paymentStateMachine.setPaymentForUpdate(SapoPayment.getInstance(paymentResult));
return paymentStateMachine;
}
// 設置當前狀態機的狀態。輸入數據庫中status字段,映射成對應的狀態類實體。
public void setCurrentState(Integer status) {
PaymentState currentState=null;
// status數字,映射成對應的狀態類實體
if (SapoPayment.STATUS_APPLY.equals(status)) {
currentState=SpringUtil.getBean(PaymentStateApply.class);
} else if (SapoPayment.STATUS_WAIT_PAY.equals(status)) {
currentState=SpringUtil.getBean(PaymentStateWaitPay.class);
} else if (SapoPayment.STATUS_PAY_FINISH.equals(status)) {
currentState=SpringUtil.getBean(PaymentStatePayFinish.class);
} else if (SapoPayment.STATUS_FAIL.equals(status)) {
currentState=SpringUtil.getBean(PaymentStateFail.class);
} else if (SapoPayment.STATUS_CANCEL.equals(status)) {
currentState=SpringUtil.getBean(PaymentStateCancel.class);
} else {
throw new BusinessException(ResultInfo.SYS_INNER_ERROR.getCode(),
"status not in state machine ,status: " + status);
}
this.currentState=currentState;
}
// TODO 待實現,申請支付訂單
public void apply() {
// 委托給當前狀態執行,將當前訂單狀態機對象傳進去,使用狀態對象處理訂單
currentState.apply(this);
}
// TODO 待實現,通知支付結果
public void resultNotify() {
// 委托給當前狀態執行
currentState.resultNotify(this);
}
// TODO 同步給當前狀態執行
public void sync() {
// 委托給當前狀態執行
currentState.sync(this);
}
// 取消訂單
public void cancel() {
// 委托給當前狀態執行
currentState.cancel(this);
}
}
public interface PaymentState {
public void apply(PaymentStateMachine paymentStateMachine);
public void resultNotify(PaymentStateMachine paymentStateMachine);
public void sync(PaymentStateMachine paymentStateMachine);
public void cancel(PaymentStateMachine paymentStateMachine);
}
@Service
public class PaymentStateApply extends BaseLogger implements PaymentState {
@Autowired
FmPayClientService fmPayClientService;
@Autowired
SapoDao dao;
@Autowired
private JacksonComponent jacksonComponent;
public void apply(PaymentStateMachine paymentStateMachine) {
}
public void sync(PaymentStateMachine paymentStateMachine) {
}
public void resultNotify(PaymentStateMachine paymentStateMachine) {
// TODO Auto-generated method stub
}
public void cancel(PaymentStateMachine paymentStateMachine) {
SapoPayment sapoPaymentForUpdate=paymentStateMachine.getPaymentForUpdate();
sapoPaymentForUpdate.setStatus(SapoPayment.STATUS_CANCEL);
sapoPaymentForUpdate.setExpireTime(null);
paymentStateMachine.updateStateMachine();
}
}
@Service
public class PaymentStateCancel extends BaseLogger implements PaymentState {
public void apply(PaymentStateMachine paymentStateMachine) {
// TODO Auto-generated method stub
}
public void resultNotify(PaymentStateMachine paymentStateMachine) {
// TODO Auto-generated method stub
}
public void sync(PaymentStateMachine paymentStateMachine) {
// TODO Auto-generated method stub
}
public void cancel(PaymentStateMachine paymentStateMachine) {
// TODO Auto-generated method stub
}
}
@Service
public class PaymentStateFail extends BaseLogger implements PaymentState {
public void apply(PaymentStateMachine paymentStateMachine) {
// TODO Auto-generated method stub
}
public void resultNotify(PaymentStateMachine paymentStateMachine) {
// TODO Auto-generated method stub
}
public void sync(PaymentStateMachine paymentStateMachine) {
// TODO Auto-generated method stub
}
public void cancel(PaymentStateMachine paymentStateMachine) {
throw new BusinessException(ResultInfo.SYS_INNER_ERROR.getCode(), "fail status can not cancel");
}
}
@Service
public class PaymentStatePayFinish extends BaseLogger implements PaymentState {
public void apply(PaymentStateMachine paymentStateMachine) {
// TODO Auto-generated method stub
}
public void resultNotify(PaymentStateMachine paymentStateMachine) {
// TODO Auto-generated method stub
}
public void sync(PaymentStateMachine paymentStateMachine) {
// TODO Auto-generated method stub
}
public void cancel(PaymentStateMachine paymentStateMachine) {
throw new BusinessException(ResultInfo.SYS_INNER_ERROR.getCode(), "payfinish status can not cancel");
}
}
@Service
public class PaymentStateWaitPay extends BaseLogger implements PaymentState {
@Autowired
FmPayClientService fmPayClientService;
@Autowired
SapoDao dao;
@Autowired
private JacksonComponent jacksonComponent;
public void payResultNotify() {
// TODO implement here
}
public void apply(PaymentStateMachine paymentStateMachine) {
throw new BusinessException(ResultInfo.SYS_INNER_ERROR.getCode(),
"applyPayPlatform not match payment state machine,currentStatus:"
+ paymentStateMachine.getPayment().getStatus());
}
public void sync(PaymentStateMachine paymentStateMachine) {
// TODO 過期去統一支付查詢
String payStatus=queryPayResultResponse.getPayStatus();
// 1:初始化輸入 2:支付中 3:支付成功 4:支付失敗 5:撤銷
if (QueryPayResultResponse.PAY_STATUS_INIT.equals(payStatus)) {
throw new BusinessException(ResultInfo.SYS_INNER_ERROR.getCode(),
"FMpay queryPay return init status ,we are waitpay");
}
if (QueryPayResultResponse.PAY_STATUS_ING.equals(payStatus)) {
return;
}
SapoPayment sapoPaymentForUpdate=paymentStateMachine.getPaymentForUpdate();
if (QueryPayResultResponse.PAY_STATUS_CANCEL.equals(payStatus)) {
sapoPaymentForUpdate.setStatus(SapoPayment.STATUS_CANCEL);
} else if (QueryPayResultResponse.PAY_STATUS_FAIL.equals(payStatus)) {
sapoPaymentForUpdate.setStatus(SapoPayment.STATUS_FAIL);
} else if (QueryPayResultResponse.PAY_STATUS_SUCCESS.equals(payStatus)) {
sapoPaymentForUpdate.setStatus(SapoPayment.STATUS_PAY_FINISH);
}
sapoPaymentForUpdate.setExpireTime(null);
paymentStateMachine.updateStateMachine();
}
public void resultNotify(PaymentStateMachine paymentStateMachine) {
// TODO Auto-generated method stub
}
public void cancel(PaymentStateMachine paymentStateMachine) {
throw new BusinessException(ResultInfo.SYS_INNER_ERROR.getCode(), "wait pay status can not cancel");
}
}
文章來自https://www.cnblogs.com/wanglifeng717/p/16214122.html
著過年放假在家復習了之前學的JS知識,用原生擼了一個購物車模塊,下面我來整理一下我的思路分享給大家。
1.1 廢話不多說,首先上個效果圖,如下:
購物車功能效果圖
1.2 功能介紹:
功能介紹完畢,下面開始介紹我寫這個購物車的步驟。
2.1 HTML代碼
<table>
<caption>
購物車
</caption>
<thead>
<tr>
<!-- 全選復選框 -->
<th>
<input type="checkbox" name="checkAll" id="check-all" checked /><label for="check-all">全選</label>
</th>
<th>圖片</th>
<th>品名</th>
<th>單位</th>
<th>單價/元</th>
<th>數量</th>
<th>金額/元</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<input type="checkbox" name="item" value="SN-1020" checked />
</td>
<td>
<a href=""><img src="images/p1.jpg" alt="" /></a>
</td>
<td>iPhone 11</td>
<td>臺</td>
<td class="price">4799</td>
<td><input type="number" min="1" value="1" /></td>
<td class="amount">xxxx</td>
</tr>
<tr>
<td>
<input type="checkbox" name="item" value="SN-1020" checked />
</td>
<td>
<a href=""><img src="images/p2.jpg" alt="" /></a>
</td>
<td>小米pro 11</td>
<td>部</td>
<td class="price">3999</td>
<td><input type="number" min="1" value="2" /></td>
<td class="amount">xxxx</td>
</tr>
<tr>
<td>
<input type="checkbox" name="item" value="SN-1030" checked />
</td>
<td>
<a href=""><img src="images/p3.jpg" alt="" /></a>
</td>
<td>MacBook Pro</td>
<td>臺</td>
<td class="price">18999</td>
<td><input type="number" min="1" value="1" /></td>
<td class="amount">xxxx</td>
</tr>
<tr>
<td>
<input type="checkbox" name="item" value="SN-1040" checked />
</td>
<td>
<a href=""><img src="images/p4.jpg" alt="" /></a>
</td>
<td>小米75電視</td>
<td>臺</td>
<td class="price">5999</td>
<td><input type="number" min="1" value="2" /></td>
<td class="amount">xxxx</td>
</tr>
<tr>
<td>
<input type="checkbox" name="item" value="SN-1050" checked />
</td>
<td>
<a href=""><img src="images/p5.jpg" alt="" /></a>
</td>
<td>Canon 90D單反</td>
<td>臺</td>
<td class="price">9699</td>
<td><input type="number" min="1" value="1" /></td>
<td class="amount">xxxx</td>
</tr>
</tbody>
<tfoot>
<tr style="font-weight: bolder; font-size: 1.2em">
<td colspan="5">總計:</td>
<td id="sum">xxxx</td>
<td id="total-amount">xxxx</td>
</tr>
</tfoot>
</table>
2.2 CSS代碼
table {
border-collapse: collapse;
width: 90%;
text-align: center;
margin: auto;
}
table caption {
margin-bottom: 15px;
font-size: 1.5rem;
}
table th, table td {
border-bottom: 1px solid #ccc;
padding: 5px;
font-weight: normal;
}
table thead tr:first-of-type {
background-color: #e6e6e6;
height: 3em;
}
table input[type="checkbox"] {
width: 1.5em;
height: 1.5em;
}
table tbody tr {
border-bottom: 1px solid #ccc;
}
table tbody tr:hover {
background-color: #f6f6f6;
cursor: pointer;
}
tbody img {
width: 3em;
}
tbody input[type="number"] {
width: 3em;
}
button {
width: 150px;
height: 30px;
outline: none;
border: none;
background-color: teal;
color: white;
letter-spacing: 5px;
}
button:hover {
opacity: 0.7;
cursor: pointer;
}
2.3 效果圖
購物車效果圖
以上就是一個簡單的購物車頁面的HTML和CSS樣式代碼。
三、完成相關JS代碼
首先,我們先完成商品的全選與取消全選的功能,所以肯定是需要拿到全選復選框元素和商品前面的復選框元素,代碼如下:
// 獲取全選復選框,所有的商品都有一個獨立的復選框
const checkAll=document.querySelector('#check-all');
const checkItems=document.getElementsByName('item');
拿到全選和每個商品的復選框元素之后,給全選框添加一個change事件,監聽它的checked值的變化。此時全選框的checked值可以通過事件監聽回調函數中的ev參數下的ev.target.checked拿到。
checkALl.onchange=ev=> {
// 如果全選框處于選中狀態,ev.target.checked的值就為true,反之,為false。
console.log(ev.target.checked);
};
如果想讓全選框的的狀態和每個商品前的復選框狀態保持一致,那么就使他們的checked值一致即可。因此,我們可以在全選復選框的change事件中遍歷每個商品的復選框元素。
checkALl.onchange=ev=> {
// 如果全選框處于選中狀態,ev.target.checked的值就為true,反之,為false。
console.log(ev.target.checked);
checkItems.forEach(item=> item.checked=ev.target.checked);
};
這樣點擊全選框的時候,就可以實現全部選中,和取消全選的功能了。效果如圖:
全選與取消全選
全選和取消全選的功能完成之后,下面開始完善逐個勾選商品,直至勾選全部商品,讓全選按鈕自動變成被選中的狀態。
要完成這個功能,我們可以通過對每個商品的復選框添加一個change事件來監聽checked的變化。因此需要通過forEach()方法對遍歷每一個商品。
checkItems.forEach(item=> item.onchange=ev=> {
// 在這里處理每一項的checked值
});
此時,我們可以這樣考慮:當每個商品的復選框都被勾選,即:所有商品復選框的checked的值全部為true時,全選復選框才會顯示被勾選的狀態,也就是全選復選框的checked的值也要為true。
由于checkAll的狀態依賴于每一項商品的checked值,那么可以利用一個數組函數:Array.every()遍歷每一項商品,當所有商品的checked值都為true時,every()方法的返回值就是一個true,然后再賦值給checkAll即可。注意:由于我們拿到的checkItems是一個NodeList數組,需要先將其轉換成數組后再進行操作。
checkItems.forEach(item=> item.onchange=ev=> {
checkAll.checked=Array.from(checkItems).every(checkItem=> checkItem.checked);
});
點擊選中每個商品
至此,全選和單選功能全部完成了。下面開始寫自動計算金額的和總數的功能。
購物車的數量和金額不僅包含每一項商品的數量和每一項商品的總金額,還包含了計算選中的商品總數,以及所有選中的商品的總金額。
下面首先完成單個商品的總金額計算,總金額=單價 * 數量,根據這個公式,我們首先拿到商品的單價和數量元素。
// 獲取單價組成的數組
const priceLists=document.querySelectorAll('.price');
// 獲取數量組成的數組
const numberLists=document.querySelectorAll('body input[type=number]');
以上單價(priceLists)和數量(numberLists)都是NodeList類型的,需要先將它們轉換成數組,由于表單中獲取的內容都是string類型,而參與計算的需要的是整型,所以這里需要進行一下轉換,使用parseInt()方法即可。
// 獲取商品單價組成的數組
const priceLists=document.querySelectorAll('.price');
const priceArr=Array.from(priceLists).map(item=> parseInt(item.textContent)); // [ 4799, 3999, 18999, 5999, 9699 ]
// 獲取商品數量組成的數組
const numberLists=document.querySelectorAll('body input[type=number]');
const numbersArr=Array.from(numberLists).map(item=> parseInt(item.value)); // 默認值:[ 1, 1, 1, 1, 1 ]
注意:商品價格和商品數量在取值時有些不同。商品的單價是普通元素直接使用textContent即可拿到它內部的值,而數量這個用的是表單控件,所以需要使用value才可以拿到值。 我剛開始寫這個功能的時候懵逼了半天,此處一定要注意。
拿到商品的單價和數量之后就可以按照上面的公式進行計算了,由于商品的價格和商品的數量都是一個數組,并且價格和數量在數組中都是一一對應的關系,因此可以使用JS數組的reduce()方法進行遍歷。
let amountArr=[priceArr, numbersArr].reduce((prev, curr)=> {
return prev.map((item, index)=> {
return item * curr[index];
});
});
總感覺上述寫法有點怪怪的,是不是可以進行簡化呢?根據箭頭函數的特征,當只有一條返回語句的時候可以省略掉return關鍵字和大括號,因此上述方法可以簡寫成下面這樣:
let amountArr=[priceArr, numbersArr].reduce((prev, curr)=> prev.map((item, index)=> item * curr[index]));
console.log(amountArr); // [ 4799, 3999, 18999, 5999, 9699 ]
(PS:上面的方法我一開始也沒有發現可以簡寫,我是把代碼發給我朋友看了之后,朋友給我點醒了。還是才疏學淺呀。)
這時已經計算出來了每個商品的總金額,那么我們將其渲染到頁面中。
// 獲取單個商品總金額的元素數組
const amountDOM=document.querySelectorAll('.amount');
amountDOM.forEach((item, index)=> item.textContent=amountArr[index]);
計算每個商品的金額并渲染到頁面中
單個商品的總金額渲染到頁面之后,下面就開始計算商品的總數,和總金額了。根據某東、某寶的購物車功能,我們可以發現,總計那里統計的商品總數是一般是我們勾選上的商品總數,總金額也是一樣的,那么我們就需要根據商品的狀態來進行計算了。
首先聲明一個數組,用于存儲被選中的商品的狀態,如果被選中,值為1,未被選中,則為0。
let isChecked=[];
checkItems.forEach(item=> isChecked.push(item.checked===true ? 1 : 0));
// 打印出商品狀態值
console.log(isChecked);
打印商品狀態值
商品的狀態已經記錄好了,那么現在就需要統計選中的商品對應的數量了。
// 聲明一個用于存儲商品數量的數組,該數組的作用是用于與對應的商品的狀態值的數組進行相乘,得到實際的被選中的商品的數組。
let checkedNumbers=[];
numbersArr.forEach((item, index)=> checkedNumbers.push(item * isChecked[index]));
// 打印被選中的商品的數量
console.log(checkedNumbers);
打印出選中的商品的數量數組
計算出被選中的商品數量的總數并渲染到頁面中:
let checkedSum=checkedNumbers.reduce((prev, curr)=> prev + curr);
// 將獲取的數量結果渲染到頁面中
document.querySelector('#sum').textContent=checkedSum;
效果如上圖已經出來了。
下面開始計算被選中的商品的總金額,該總金額等于上面所有被選中的商品的總金額之和。計算出結果之后渲染到頁面中。
// 聲明一個數組用于存儲每一個被選中的商品的總金額
let checkedPrice=[];
checkedNumbers.forEach((item, index)=> checkedPrice.push(item * priceArr[index]));
// 打印被選中的每個被選中的商品總金額
console.log(checkedPrice);
// 計算被選中的商品總金額
let totalAmount=checkedPrice.reduce((prev, curr)=> prev + curr);
// 將選中的商品總金額渲染到頁面中
document.querySelector('#total-amount').textContent=totalAmount;
將總金額渲染到頁面
至此,關于計算單個商品的總金額以及被選中商品的數量和總金額的功能已經全部完成了,但是我們還需要實現在頁面加載以及更改某個商品數量時自動計算的功能。那么就需要將上述的計算功能封裝成一個函數,以便后面每一次執行計算時使用。
function autoCalculate() {
// 獲取單價組成的數組
const priceLists=document.querySelectorAll('.price');
const priceArr=Array.from(priceLists).map(item=> parseInt(item.textContent));
// 獲取數量組成的數組
const numberLists=document.querySelectorAll('body input[type=number]');
const numbersArr=Array.from(numberLists).map(item=> parseInt(item.value));
console.log(priceArr, numbersArr);
// 由于拿到的表單里的數據都是string類型的,所以需要先將其轉換成int類型,因此需要使用`map()`方法操作一下
let amountArr=[priceArr, numbersArr].reduce((prev, curr)=> prev.map((item, index)=> item * curr[index]));
console.log(amountArr);
const amountDOM=document.querySelectorAll('.amount');
amountDOM.forEach((item, index)=> item.textContent=amountArr[index]);
// 首先聲明一個數組,用于存儲被選中的商品的狀態,如果被選中,值為1,未被選中,則為0
let isChecked=[];
checkItems.forEach(item=> isChecked.push(item.checked===true ? 1 : 0));
console.log(isChecked);
// 聲明一個用于存儲是商品數量的數組,該數組的作用是:如果商品處于被選中的狀態,那么就存儲它真實的數量值,
// 如果沒有被選中,那么數量就是0
let checkedNumbers=[];
numbersArr.forEach((item, index)=> checkedNumbers.push(item * isChecked[index]));
console.log(checkedNumbers);
// 此時,被選中的商品的總數為:
let checkedSum=checkedNumbers.reduce((prev, curr)=> prev + curr);
console.log(checkedSum);
// 將獲取的數量結果渲染到頁面中
document.querySelector('#sum').textContent=checkedSum;
// 下面開始計算被選中的商品的總金額,該總金額等于上面所有被選中的商品的總金額之和。
// 聲明一個數組用于存儲每一個被選中的商品的總金額
let checkedPrice=[];
checkedNumbers.forEach((item, index)=> checkedPrice.push(item * priceArr[index]));
console.log(checkedPrice);
// 計算被選中的商品總金額
let totalAmount=checkedPrice.reduce((prev, curr)=> prev + curr);
// 將選中的商品總金額渲染到頁面中
document.querySelector('#total-amount').textContent=totalAmount;
}
將代碼封裝后我們會發現,單個商品的總金額,商品總數以及總金額的值都沒了,如下圖:
封裝代碼后的效果
這是因為,代碼在第一次加載的時候并沒有執行封裝后的函數,因此需要加一行代碼:
// 頁面第一次加載的時候自動執行一次。
window.onload=autoCalculate;
這樣頁面中的數據在第一次加載的時候就全部都正常了。
下面完成最后一個功能:調整商品的數量,會自動計算總數和金額。該功能還是通過change事件監聽某個表單數據的變化來完成。效果圖下圖:
// 監聽某個控件的事件,首先需要拿到控件元素。
const numInput=document.querySelectorAll('body input[type=number]');
// 上面都用了onchange來監聽,這里換個方法使用addEventListener。
numInput.forEach(item=> item.addEventListener('change', autoCalculate));
但是我們會發現這里有個小bug,就是如果勾選沒有選中的商品,并不會自動計算商品數量和總價,原因很簡單,我們在監聽單個商品選中和全選的時候根本就沒有執行自動計算函數,只需要在二者的事件監聽中加上自動計算的函數即可。
checkAll.onchange=ev=> {
checkItems.forEach(item=> item.checked=ev.target.checked);
// 解決勾選全選框不會自動計算的bug
autoCalculate();
};
checkItems.forEach(item=> item.onchange=ev=> {
checkAll.checked=Array.from(checkItems).every(checkItem=> checkItem.checked);
// 解決勾選全選框不會自動計算的bug
autoCalculate();
});
寫到這里,我們購物車的所有功能都已經完成了。購物車這個模塊看似不難,其實這里面的坑也是不少的,例如:
以上就是我個人在寫這個購物車功能的全部新的,由于本人也是新手,可能還有其他更簡介方便的寫法,如果有問題,請各位大佬批評指正,不勝感激。
如果有剛開始學習JS的同學,想要源碼的各位親,可以關注并私信回復“購物車”即可。
單狀態 JavaScript 資產
您可以通過使用 Shopify 后臺中的自定義腳本框,將 JavaScript 添加到結帳的訂單狀態頁面(以前稱為“感謝”頁面)。
Shopify.Checkout.OrderStatus JavaScript 資產可用于向訂單狀態頁面添加多種類型的內容,包括:
-特定產品的備注
-單個發貨方式的說明
-數字產品的下載鏈接。
也可以通過 ScriptTag 訪問此 JavaScript 資源。
如何實現
Shopify.Checkout.OrderStatus 包含用于生成新內容的函數:
addContentBox(params)
此函數添加一個內容框,從其中傳遞給該函數的每個參數都呈現為單獨的行。
函數內部支持 HTML5,并且您可以在函數外部使用 liquid。
示例
您可以在使用了特定的發貨方式時添加內容:
{% if checkout.shipping_method.title=='Pick-up at the store' %}
Shopify.Checkout.OrderStatus.addContentBox(
'
'
We are open everyday from 9am to 5pm.
'
)
{% endif %}
(來源:Shopify)
以上內容屬作者個人觀點,不代表雨果網立場!如有侵權,請聯系我們。
相關鏈接:Shopify后臺怎么復制現有訂單?Shopify復制現有訂單操作一覽
*請認真填寫需求信息,我們會在24小時內與您取得聯系。