MV* 框架 與 DOM操作為主 JS庫 的案例對比

子凡發表於2015-02-05

最近分別使用 Zepto 和 Avalon框架寫了個 SPA專案,貼出來討論下 JS DOM操作為主 JS庫 與 MV* 框架的對比

案例(MV* 框架 與 DOM操作 JS庫 例項對比)

購物車頁面 JS業務邏輯(忽略 Ajax請求–Ajax請求時機根據產品具體情況而定)

  • 以列表方式展示購物車商品
  • 每件商品有”+”、”-” 按鈕,點選該按鈕則:
    • 檢測是否達到購買極限(如:最小購買數量不能小於 1件)
    • 達到購買極限則給該按鈕新增相應的 class 以便提示使用者該按鈕不能再點選
    • 反之則去掉該按鈕上的該 class(如:現在商品數量為 1,”-” 為不可用狀態,現在點選 “+” 則 “-“變為可用狀態)
    • 改變該件商品數量
    • 計算 並 更新該商品總價金額
    • 重新計算 並 更新 購物車所有商品金額
  • 移除單種商品
    • 從檢視上找到該 DOM
    • 然後移除該 DOM
    • 重新計算 並 更新 購物車所有商品金額
  • 移除購物車所有商品
    • 顯示購物車無商品 div,引導使用者到商品列表等其它頁面

實現:

實現一:Zepto 版

以 DOM操作 JS庫實現:jQuery、Zepto、MooTools)

通過後端控制器渲染頁面

view:

<{css src="page/cart.css"}>

<header class="titleHead">
    <div class="leftBtns">
        <a class="leftBack goBack" href="javascript:void(0)">←</a>
    </div>
    <b>購物車</b>
</header>




<div id="emptyBox" <{if !empty($cart.cartList)}> style=`display: none;`<{/if}> >
<p>購物車空空如也</p>

<p><a href="<{link app=`wechatecoupon` ctl=`site_cashcoupon` act=`group`}>">去新增</a></p>
</div>



<{if !empty($cart.cartList)}>


<div class="box">
    <div id="topTotal">
        <span>商品總價(不含運費)
          <b class="total"><{$cart.totalAmount|cur}></b></span>
        <a href="<{link app=`wechatecoupon` ctl=`site_order` act=`create`}>" class="topBtn">去結算</a>
    </div>
    <div class="goodslistWrap">
        <{foreach from=$cart.cartList item=item}>
        <div class="goodslist" data-cart-id="<{$item.cart_id}>" data-cashcoupon-id="<{$item.obj_id}>"
             data-price="<{$item.cashcoupon.price}>">
            <div class="imgWrap">
                <img src="<{$item.cashcoupon.image_id|storager}>" height="100%"/>
            </div>
            <div class="txtWrap">
                <p><{$item.cashcoupon.name}></p>

                <div class="handleBox">
                    <div class="numBox">
                        <span class="handle minus <{if 1==$item.quantity}>bg_gray<{/if}>">-</span>
                        <span>
                            <input type="text" name="num" value="<{$item.quantity}>"
                                   data-max-count="<{$item.cashcoupon.sale_count}>" maxlength="4"
                                   readonly="readonly" />
                        </span>
                        <span class="handle plus <{if $item.quantity >= $item.cashcoupon.sale_count}>bg_gray<{/if}>">+</span>
                    </div>
                    <div class="trashBox del">
                        <i class="trash"></i>
                    </div>
                </div>
            </div>
            <div class="priceWrap">
                <span><strong><{$item.cashcoupon.price|cur}></strong></span>
                <span><del><{$item.cashcoupon.mktprice|cur}></del></span>
            </div>
        </div>
        <{/foreach}>

        <div class="clear"></div>
    </div>
    <div class="goodslistTotal">
        <span>商品總價(不含運費)<b class="total"><{$cart.totalAmount|cur}></b></span>
    </div>
    <div class="bottomTotal">
        <div class="delAll">
            <i class="trash_b"></i>
            <span>清空全部</span>
        </div>
        <div class="payBtnBox">
            <a href="<{link app=`wechatecoupon` ctl=`site_order` act=`create`}>" class="bottomBtn">去結算</a>
        </div>
    </div>
</div>


<{script src="page/cart.js"}>
<{/if}>

JS 邏輯

javascript
// 全域性常量 var UA = navigator.userAgent; var ipad = !!(UA.match(/(iPad).*OSs([d_]+)/)), isIphone = !!(!ipad && UA.match(/(iPhonesOS)s([d_]+)/)), isAndroid = !!(UA.match(/(Android)s+([d.]+)/)), isMobile = !!(isIphone || isAndroid); var CLICK = isMobile ? "tap" : `click`; // 移動端觸控、PC單擊 事件 // 購物車 var Cart = { // 更新 info update: function ($id, $number, fun) { var data = { id: $id || ``, number: $number || 0 }; CGI.POST(`wecart-update.html`, data, function (data) { // 回撥方法 fun && fun(data); }); }, // 計算總價 figurePrice: function () { var goodsList = $(".goodslist"); console.log(goodsList); if (0 === goodsList.length) { this.removeAll(); return false; } // 當前商品金額 var price; // 當前商品數量 var number; // 總金額 var allPrice = 0; $.each(goodsList, function (index, item) { item = $(item); price = parseFloat(item.attr("data-price")); number = parseInt(item.find("input").val()); console.log({`數量`: number, `單價`: price}); allPrice += price * number; }); console.log(`總價:` + allPrice); // DOM 操作 $(".total").text("¥" + allPrice.toFixed(2)); }, // 移除所有 removeAll: function () { // DOM 操作 $("#emptyBox").show(); $(".box").hide(); } }; // 遞增、遞減 $(".numBox").on(CLICK, function (e) { // numBox var _t = $(this); var dom = $(e.target); // 商品數量 DOM var numDom = _t.find("input"); //console.log(numDom); // 數量 var _v = parseInt(numDom.val()), now_v; // 最大購買數 var max = parseInt(numDom.attr("data-max-count")); if (dom.hasClass("plus")) { // 遞增 // 最大購買數量限制 if (_v === max) { return false; } now_v = (_v < 1) ? 1 : (_v >= max ? max : _v + 1); } else if (dom.hasClass("minus")) { // 遞減 // 最小購買數量限制 if (_v === 1) { return false; } now_v = (_v < 1) ? 1 : (_v > max ? max : _v - 1); } else { return false; } var cartId = dom.parents(".goodslist").attr("data-cashcoupon-id"); // ajax Cart.update(cartId, now_v, function (data) { // 更改數量 numDom.val(now_v); // 遞減數量按鈕 var minus = _t.find(".minus"); // 遞增數量按鈕 var plus = _t.find(".plus"); now_v > 1 && minus.hasClass("bg_gray") && minus.removeClass("bg_gray"); now_v === 1 && !minus.hasClass("bg_gray") && minus.addClass("bg_gray"); now_v < max && plus.hasClass("bg_gray") && plus.removeClass("bg_gray"); now_v >= max && !plus.hasClass("bg_gray") && plus.addClass("bg_gray"); // 計算總價 Cart.figurePrice(); }); event.preventDefault(); }); // 刪掉商品 $(".del").on(CLICK, function () { var dom = $(this).parents(".goodslist"); var cartId = dom.attr("data-cashcoupon-id"); cartId && Cart.update(cartId, 0, function (data) { // 提示 SD.Toast({`text`: `刪除成功!`}); // 移除當先列 dom.remove(); // 計算總價 Cart.figurePrice(); }); }); // 刪掉所有商品 $(".delAll").on(CLICK, function (event) { SD.Confirm({ content: `你確定要清空購物車嗎?`, //lock: false, ok: { text: `殘忍清空`, fun: function () { Cart.update(``, 0, function (data) { // 提示 SD.Toast({`text`: `刪除成功!`}); // DOM 操作 Cart.removeAll(); }); } }, cancel: { text: `再忍忍` } }); });

實現二:Avalon版

以 MV* 框架實現:Angular、Avalon)
通過後端介面獲取購物車資料,JS動態渲染頁面

view:

html

<div ms-controller="cart"> <div id="emptyBox" ms-visible="goodsList.length==0" style="display: none"> <p>購物車空空如也</p> <p><a href="#!/">去新增</a></p> </div> <div ms-visible="goodsList.length>0"> <div id="topTotal"> <span>商品總價(不含運費)<b>{{price|currency}}</b></span> <a href="pay.html" class="topBtn">去結算</a> </div> <div class="goodslistWrap"> <div class="goodslist" ms-repeat="goodsList"> <div class="imgWrap"> <img ms-src="{{el.cashcoupon.image}}" height="100%"/> </div> <div class="txtWrap"> <p>{{el.cashcoupon.name}}</p> <div class="handleBox"> <div class="numBox"> <span class="handle minus" ms-class-bg_gray="el.quantity<=1" ms-click="minus($index)">-</span> <span><input type="text" ms-attr-value="{{el.quantity}}"/></span> <span class="handle plus" ms-click="plus($index)">+</span> </div> <div class="trashBox" ms-click="remove($index,$remove)"> <i class="trash"></i> </div> </div> </div> <div class="priceWrap"> <span><strong>{{el.cashcoupon.price|currency}}</strong></span> <span><del>{{el.cashcoupon.mktprice|currency}}</del></span> </div> </div> <!--last--> <div class="clear"></div> </div> <div class="goodslistTotal"> <span>商品總價(不含運費)<b>{{price|currency}}</b></span> </div> <div class="bottomTotal"> <div class="delAll" ms-click="removeAll"> <i class="trash_b"></i> <span>清空全部</span> </div> <div class="payBtnBox"> <a href="pay.html" class="bottomBtn">去結算</a> </div> </div> </div> </div>

JS 業務程式碼

javascriptdefine("cart", ["avalon", "request"], function (avalon) {

    var avalonAjax = avalon.ajax;

    var model = avalon.define({
        $id: "cart",
        toggle: true,
        price: 0, // 總金額
        goodsList: [],
        // 遞加數量
        plus: function (index) {
            model.goodsList[index].quantity = parseInt(model.goodsList[index].quantity) + 1;

            // 計算總數量 和 總金額
            count();
        },
        // 遞減數量
        minus: function (index) {
            if (model.goodsList[index].quantity <= 1) {
                return false;
            }
            model.goodsList[index].quantity = parseInt(model.goodsList[index].quantity) - 1;

            // 計算總數量 和 總金額
            count();
        },
        // 移除當前
        remove: function (index, perish) {
            perish();   // 移除
        },
        // 移除所有
        removeAll: function () {
            avalon.vmodels.cart.goodsList.clear();

            // 計算總數量 和 總金額
            count();
        }
    });

    // 獲取資料
    var getData = function () {
        if (avalonAjax) {
            avalon.getJSON(``, {method: `ecoupon.cart.list`}, function (data) {
                model.price = data.totalAmount;
                model.goodsList = data.cartList;
            })
        }
    }();

    // 計算總數量 和 總金額
    function count() {
        var _count = 0;
        var _price = 0;
        model.goodsList.forEach(function (goods, index) {
            _count += parseInt(goods.quantity);
            _price += parseFloat(goods.cashcoupon.price) * parseInt(goods.quantity);
        });
        avalon.vmodels.page.cartNum = _count;
        model.price = _price.toFixed(2);
    };

    // 購物車數量監聽(目前只能監聽商品種類變化, 不能監聽商品數量變化)
    model.goodsList.$watch("length", function () {
        count();
    });

    return model;

});

zepto 版本中,js 業務程式碼大量使用了 選擇器 來 操作DOM,導致 js 與 view 極度耦合。

avalon版本,利用avalon可以大大加快我們專案的開發速度,可以使我們遠離 DOM的世界,我們的程式設計變成了只圍繞 model層而不圍繞 DOM,即操作 model就是操作 DOM,同時能讓我們開發人員離開 DOM都能輕鬆進行前端開發。avalon中定義的 VM處理業務邏輯與提供資料來源,HTML中的繫結負責渲染與響應使用者點選拖拽等行為,這樣就最大保證了檢視邏輯相分離。

優點

  • JS 與 view 解耦。遠離 DOM的世界,圍繞 model層
  • 控制器、路由從後端放到前端,更加適合 Web APP開發。SPA應用可以提供更好的使用者體驗
  • 業務實現的方式轉變(由直接操作 DOM變為 操作 VM,更加適合後端童鞋思維方式)
  • 更容易實現前後端分離
  • 官方講程式碼量比 jQuery減少50%
  • ….

缺點

  • MV* 框架學習成本高,概念多(控制器、路由、指令、過濾器、服務、依賴注入…)[哈哈、後端同學最喜歡這種了,學習起來無壓力]
  • 發展時間沒有 jQuery這種時間長,元件相比 jQuery相差比較大(大多數需要自己實現)
  • 動畫
  • SEO(貌似有解決方案了)

具體選擇就看場景了吧,動畫效果、特效多用 jQuery這種,業務複雜用 MV* 這種

原文發在:http://www.webdevs.cn/article/93.html

相關文章