1. 程式人生 > >Django 頁面靜態化 商品詳情頁靜態化

Django 頁面靜態化 商品詳情頁靜態化

商品詳情頁

商品詳情頁依然採用頁面靜態化技術。

商品詳情頁的靜態化由運營人員在編輯商品資訊時觸發生成靜態化頁面。

先來實現靜態化非同步任務,在celery_tasks中新建html/tasks.py任務

from celery_tasks.main import celery_app
from django.template import loader
from django.conf import settings
import os

from goods.utils import get_categories
from goods.models import SKU


@celery_app.task(name='generate_static_sku_detail_html')
def generate_static_sku_detail_html(sku_id):
    """
    生成靜態商品詳情頁面
    :param sku_id: 商品sku id
    """
    # 商品分類選單
    categories = get_categories()

    # 獲取當前sku的資訊
    sku = SKU.objects.get(id=sku_id)
    sku.images = sku.skuimage_set.all()

    # 麵包屑導航資訊中的頻道
    goods = sku.goods
    goods.channel = goods.category1.goodschannel_set.all()[0]

    # 構建當前商品的規格鍵
    # sku_key = [規格1引數id, 規格2引數id, 規格3引數id, ...]
    sku_specs = sku.skuspecification_set.order_by('spec_id')
    sku_key = []
    for spec in sku_specs:
        sku_key.append(spec.option.id)

    # 獲取當前商品的所有SKU
    skus = goods.sku_set.all()

    # 構建不同規格引數(選項)的sku字典
    # spec_sku_map = {
    #     (規格1引數id, 規格2引數id, 規格3引數id, ...): sku_id,
    #     (規格1引數id, 規格2引數id, 規格3引數id, ...): sku_id,
    #     ...
    # }
    spec_sku_map = {}
    for s in skus:
        # 獲取sku的規格引數
        s_specs = s.skuspecification_set.order_by('spec_id')
        # 用於形成規格引數-sku字典的鍵
        key = []
        for spec in s_specs:
            key.append(spec.option.id)
        # 向規格引數-sku字典新增記錄
        spec_sku_map[tuple(key)] = s.id

    # 獲取當前商品的規格資訊
    #specs = [
    #    {
    #        'name': '螢幕尺寸',
    #        'options': [
    #            {'value': '13.3寸', 'sku_id': xxx},
    #            {'value': '15.4寸', 'sku_id': xxx},
    #        ]
    #    },
    #    {
    #        'name': '顏色',
    #        'options': [
    #            {'value': '銀色', 'sku_id': xxx},
    #            {'value': '黑色', 'sku_id': xxx}
    #        ]
    #    },
    #    ...
    #]
    specs = goods.goodsspecification_set.order_by('id')
    # 若當前sku的規格資訊不完整,則不再繼續
    if len(sku_key) < len(specs):
        return
    for index, spec in enumerate(specs):
        # 複製當前sku的規格鍵
        key = sku_key[:]
        # 該規格的選項
        options = spec.specificationoption_set.all()
        for option in options:
            # 在規格引數sku字典中查詢符合當前規格的sku
            key[index] = option.id
            option.sku_id = spec_sku_map.get(tuple(key))

        spec.options = options

    # 渲染模板,生成靜態html檔案
    context = {
        'categories': categories,
        'goods': goods,
        'specs': specs,
        'sku': sku
    }

    template = loader.get_template('detail.html')
    html_text = template.render(context)
    file_path = os.path.join(settings.GENERATED_STATIC_HTML_FILES_DIR, 'goods/'+str(sku_id)+'.html')
    with open(file_path, 'w') as f:
        f.write(html_text)

將形成商品類別部分的資料封裝成一個公共函式,放在goods/utils.py中

from collections import OrderedDict

from goods.models import GoodsChannel


def get_categories():
    """
    獲取商城商品分類選單
    :return 選單字典
    """
    # 商品頻道及分類選單
    # 使用有序字典儲存類別的順序
    # categories = {
    #     1: { # 組1
    #         'channels': [{'id':, 'name':, 'url':},{}, {}...],
    #         'sub_cats': [{'id':, 'name':, 'sub_cats':[{},{}]}, {}, {}, ..]
    #     },
    #     2: { # 組2
    #
    #     }
    # }
    categories = OrderedDict()
    channels = GoodsChannel.objects.order_by('group_id', 'sequence')
    for channel in channels:
        group_id = channel.group_id  # 當前組

        if group_id not in categories:
            categories[group_id] = {'channels': [], 'sub_cats': []}

        cat1 = channel.category  # 當前頻道的類別

        # 追加當前頻道
        categories[group_id]['channels'].append({
            'id': cat1.id,
            'name': cat1.name,
            'url': channel.url
        })
        # 構建當前類別的子類別
        for cat2 in cat1.goodscategory_set.all():
            cat2.sub_cats = []
            for cat3 in cat2.goodscategory_set.all():
                cat2.sub_cats.append(cat3)
            categories[group_id]['sub_cats'].append(cat2)
    return categories

頁面模板

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
    <title>商品詳情</title>
    <link rel="stylesheet" type="text/css" href="/css/reset.css">
    <link rel="stylesheet" type="text/css" href="/css/main.css">
    <script type="text/javascript" src="/js/host.js"></script>
    <script type="text/javascript" src="/js/vue-2.5.16.js"></script>
    <script type="text/javascript" src="/js/axios-0.18.0.min.js"></script>
</head>
<body>
    <div id="app" v-cloak>
    <div class="header_con">
        <div class="header">
            <div class="welcome fl">歡迎來到商城!</div>
            <div class="fr">
                <div v-if="username" class="login_btn fl">
                    歡迎您:<em>[[ username ]]</em>
                    <span>|</span>
                    <a @click="logout">退出</a>
                </div>
                <div v-else class="login_btn fl">
                    <a href="/login.html">登入</a>
                    <span>|</span>
                    <a href="/register.html">註冊</a>
                </div>
                <div class="user_link fl">
                    <span>|</span>
                    <a href="/user_center_info.html">使用者中心</a>
                    <span>|</span>
                    <a href="/cart.html">我的購物車</a>
                    <span>|</span>
                    <a href="/user_center_order.html">我的訂單</a>
                </div>
            </div>
        </div>
    </div>

    <div class="search_bar clearfix">
            <a href="/index.html" class="logo fl"><img src="/images/logo.png"></a>
            <div class="search_wrap fl">
                <form method="get" action="/search.html" class="search_con">
                    <input type="text" class="input_text fl" name="q" placeholder="搜尋商品">
                    <input type="submit" class="input_btn fr" name="" value="搜尋">
                </form>
                <ul class="search_suggest fl">
                    <li><a href="#">索尼微單</a></li>
                    <li><a href="#">優惠15元</a></li>
                    <li><a href="#">美妝個護</a></li>
                    <li><a href="#">買2免1</a></li>
                </ul>
            </div>

            <div class="guest_cart fr">
            <a href="#" class="cart_name fl">我的購物車</a>
            <div class="goods_count fl" id="show_count">15</div>

            <ul class="cart_goods_show">
                <li>
                    <img src="#" alt="商品圖片">
                    <h4>商品名稱手機</h4>
                    <div>4</div>
                </li>
                <li>
                    <img src="#" alt="商品圖片">
                    <h4>商品名稱手機</h4>
                    <div>5</div>
                </li>
                <li>
                    <img src="#" alt="商品圖片">
                    <h4>商品名稱手機</h4>
                    <div>6</div>
                </li>
                <li>
                    <img src="#" alt="商品圖片">
                    <h4>商品名稱手機</h4>
                    <div>6</div>
                </li>
            </ul>            
        </div>

        </div>

        <div class="navbar_con">
                <div class="navbar">
                    <div class="sub_menu_con fl">
                        <h1 class="fl">商品分類</h1>
                        <ul class="sub_menu">
                            {% for group in categories.values %}
                            <li>
                                <div class="level1">
                                    {% for channel in group.channels %}
                                    <a href="{{ channel.url }}">{{ channel.name }}</a>
                                    {% endfor %}
                                </div>
                                <div class="level2">
                                    {% for cat2 in group.sub_cats %}
                                    <div class="list_group">
                                        <div class="group_name fl">{{cat2.name}} &gt;</div>
                                        <div class="group_detail fl">
                                            {% for cat3 in cat2.sub_cats %}
                                            <a href="/list.html?cat={{cat3.id}}">{{cat3.name}}</a>
                                            {% endfor %}
                                        </div>
                                    </div>
                                    {% endfor %}
                                </div>
                            </li>
                            {% endfor %}
                        </ul>
                    </div>

                    <ul class="navlist fl">
                        <li><a href="">首頁</a></li>
                        <li class="interval">|</li>
                        <li><a href="">真划算</a></li>
                        <li class="interval">|</li>
                        <li><a href="">抽獎</a></li>
                    </ul>
                </div>
            </div>

    <div class="breadcrumb">
        <a href="{{ goods.channel.url }}">{{ goods.category1.name }}</a>
        <span>></span>
        <span>{{ goods.category2.name }}</span>
        <span>></span>
        <a href="/list.html?cat={{ goods.category3.id }}">{{goods.category3.name }}</a>
    </div>

    <div class="goods_detail_con clearfix">
        <div class="goods_detail_pic fl"><img src="{{ sku.default_image_url }}"></div>
        <div class="goods_detail_list fr">
            <h3>{{ sku.name }}</h3>
            <p>{{ sku.caption }}</p>
            <div class="prize_bar">
                <span class="show_pirze">¥<em>{{ sku.price }}</em></span><span> 市場價¥{{sku.market_price}}</span>
                <a href="javascript:;" class="goods_judge">{{ sku.comments }}人評價</a>
            </div>
            <div class="goods_num clearfix">
                <div class="num_name fl">數 量:</div>
                <div class="num_add fl">
                    <input v-model="sku_count" type="text" class="num_show fl">
                    <a @click="sku_count++" class="add fr">+</a>
                    <a @click="on_minus()" class="minus fr">-</a>
                </div>
            </div>
            {% for spec in specs %}
            <div class="type_select">
                <label>{{ spec.name }}:</label>
                {% for option in spec.options %}
                {% if option.sku_id == sku.id %}
                <a href="javascript:;" class="select">{{ option.value }}</a>
                {% elif option.sku_id %}
                <a href="/goods/{{option.sku_id}}.html">{{ option.value }}</a>
                {% else %}
                <a href="javascript:;">{{ option.value }}</a>
                {% endif %}
                {% endfor %}
            </div>
            {% endfor %}
            <div class="total">總價:<em>[[sku_amount]]元</em></div>
            <div class="operate_btn">
                <a @click="add_cart" class="add_cart" id="add_cart">加入購物車</a>
            </div>
        </div>
    </div>

    <div class="main_wrap clearfix">
        <div class="l_wrap fl clearfix">
            <div class="new_goods">
                <h3>熱銷排行</h3>
                <ul>
                    <li v-for="sku in hots">
                        <a :href="sku.url"><img :src="sku.default_image_url"></a>
                        <h4><a :href="sku.url">[[sku.name]]</a></h4>
                        <div class="prize">¥[[sku.price]]</div>
                    </li>
                </ul>
            </div>
        </div>

        <div class="r_wrap fr clearfix">
            <ul class="detail_tab clearfix">
                <li @click="on_tab_content('detail')" :class="tab_content.detail?'active':''">商品詳情</li>
                <li @click="on_tab_content('pack')" :class="tab_content.pack?'active':''">規格與包裝</li>
                <li @click="on_tab_content('comment')" :class="tab_content.comment?'active':''">商品評價([[comments.length]])</li>
                <li @click="on_tab_content('service')" :class="tab_content.service?'active':''">售後服務</li>
            </ul>
            <div @click="on_tab_content('detail')" class="tab_content" :class="tab_content.detail?'current':''">
                <dl>
                    <dt>商品詳情:</dt>
                    <dd>{{ goods.desc_detail|safe }}</dd>
                </dl>
            </div>
            <div @click="on_tab_content('pack')" class="tab_content" :class="tab_content.pack?'current':''">
                <dl>
                    <dt>規格與包裝:</dt>
                    <dd>{{ goods.desc_pack|safe }}</dd>
                </dl>
            </div>
            <div @click="on_tab_content('comment')" class="tab_content" :class="tab_content.comment?'current':''">
                <ul class="judge_list_con">
                    <li class="judge_list fl" v-for="comment in comments">
                        <div class="user_info fl">
                            <b>[[comment.username]]</b>
                        </div>
                        <div class="judge_info fl">
                            <div :class="comment.score_class"></div>
                            <div class="judge_detail">[[comment.comment]]</div>
                        </div>
                    </li>
                </ul>
            </div>
            <div @click="on_tab_content('service')" class="tab_content" :class="tab_content.service?'current':''">
                <dl>
                    <dt>售後服務:</dt>
                    <dd>{{ goods.desc_service|safe }}</dd>
                </dl>
            </div>
        </div>
    </div>

    <div class="footer">
        <div class="foot_link">
            <a href="#">關於我們</a>
            <span>|</span>
            <a href="#">聯絡我們</a>
            <span>|</span>
            <a href="#">招聘人才</a>
            <span>|</span>
            <a href="#">友情連結</a>
        </div>
        <p>CopyRight © 2016 北京商業股份有限公司 All Rights Reserved</p>
        <p>電話:010-****888    京ICP備*******8號</p>
    </div>
    </div>
    <script type="text/javascript">
        var price = {{sku.price}};
        var cat = {{ goods.category3.id }};
    </script>
    <script type="text/javascript" src="/js/detail.js"></script>

</body>
</html>

頁面的detail.js

var vm = new Vue({
    el: '#app',
    delimiters: ['[[', ']]'],
    data: {
        host,
        username: sessionStorage.username || localStorage.username,
        user_id: sessionStorage.user_id || localStorage.user_id,
        token: sessionStorage.token || localStorage.token,
        tab_content: {
            detail: true,
            pack: false,
            comment: false,
            service: false
        },
        sku_id: '',
        sku_count: 1,
        sku_price: price,
        cart_total_count: 0, // 購物車總數量
        cart: [], // 購物車資料
        hots: [], // 熱銷商品
        cat: cat, // 商品類別
        comments: [], // 評論資訊
        score_classes: {
            1: 'stars_one',
            2: 'stars_two',
            3: 'stars_three',
            4: 'stars_four',
            5: 'stars_five',
        }
    },
    computed: {
        sku_amount: function(){
            return (this.sku_price * this.sku_count).toFixed(2);
        }
    },
    mounted: function(){
        // 新增使用者瀏覽歷史記錄
        this.get_sku_id();

        this.get_cart();
        this.get_hot_goods();
        this.get_comments();
    },
    methods: {
        // 退出
        logout: function(){
            sessionStorage.clear();
            localStorage.clear();
            location.href = '/login.html';
        },
        // 控制頁面標籤頁展示
        on_tab_content: function(name){
            this.tab_content = {
                detail: false,
                pack: false,
                comment: false,
                service: false
            };
            this.tab_content[name] = true;
        },
        // 從路徑中提取sku_id
        get_sku_id: function(){
            var re = /^\/goods\/(\d+).html$/;
            this.sku_id = document.location.pathname.match(re)[1];
        },
        // 減小數值
        on_minus: function(){
            if (this.sku_count > 1) {
                this.sku_count--;
            }
        },
        // 新增購物車
        add_cart: function(){

        },
        // 獲取購物車資料
        get_cart: function(){

        },
        // 獲取熱銷商品資料
        get_hot_goods: function(){

        },
        // 獲取商品評價資訊
        get_comments: function(){

        }
    }
});

非同步任務的觸發

運營人員在Admin站點儲存商品資訊時,應該觸發生成商品靜態頁的非同步任務。

我們需要調整Admin站點儲存和刪除商品資訊時行為。

在Admin站點儲存或刪除資料時,Django是呼叫的Admin站點管理器類的save_model()方法和delete_model()方法,我們只需重新實現這兩個方法,在這兩個方法中呼叫非同步任務即可。

編輯goods/admin.py

from django.contrib import admin

# Register your models here.

from goods import models


class SKUAdmin(admin.ModelAdmin):
    def save_model(self, request, obj, form, change):
        obj.save()
        from celery_tasks.html.tasks import generate_static_sku_detail_html
        generate_static_sku_detail_html.delay(obj.id)


class SKUSpecificationAdmin(admin.ModelAdmin):
    def save_model(self, request, obj, form, change):
        obj.save()
        from celery_tasks.html.tasks import generate_static_sku_detail_html
        generate_static_sku_detail_html.delay(obj.sku.id)

    def delete_model(self, request, obj):
        sku_id = obj.sku.id
        obj.delete()
        from celery_tasks.html.tasks import generate_static_sku_detail_html
        generate_static_sku_detail_html.delay(sku_id)


class SKUImageAdmin(admin.ModelAdmin):
    def save_model(self, request, obj, form, change):
        obj.save()
        from celery_tasks.html.tasks import generate_static_sku_detail_html
        generate_static_sku_detail_html.delay(obj.sku.id)

        # 設定SKU預設圖片
        sku = obj.sku
        if not sku.default_image_url:
            sku.default_image_url = obj.image.url
            sku.save()

    def delete_model(self, request, obj):
        sku_id = obj.sku.id
        obj.delete()
        from celery_tasks.html.tasks import generate_static_sku_detail_html
        generate_static_sku_detail_html.delay(sku_id)


admin.site.register(models.GoodsCategory)
admin.site.register(models.GoodsChannel)
admin.site.register(models.Goods)
admin.site.register(models.Brand)
admin.site.register(models.GoodsSpecification)
admin.site.register(models.SpecificationOption)
admin.site.register(models.SKU, SKUAdmin)
admin.site.register(models.SKUSpecification, SKUSpecificationAdmin)
admin.site.register(models.SKUImage, SKUImageAdmin)

指令碼工具

為了開發方便,我們還可以編寫手動生成所有商品靜態頁面的指令碼regenerate_detail_html.py

#!/usr/bin/env python

"""
功能:手動生成所有SKU的靜態detail html檔案
使用方法:
    ./regenerate_detail_html.py
"""
import sys
sys.path.insert(0, '../')

# 設定Django執行所依賴的環境變數
import os
if not os.getenv('DJANGO_SETTINGS_MODULE'):
    os.environ['DJANGO_SETTINGS_MODULE'] = 'meiduo_mall.settings.dev'

# 讓Django進行一次初始化
import django
django.setup()

from django.template import loader
from django.conf import settings

from goods.utils import get_categories
from goods.models import SKU


def generate_static_sku_detail_html(sku_id):
    """
    生成靜態商品詳情頁面
    :param sku_id: 商品sku id
    """
    # 商品分類選單
    categories = get_categories()

    # 獲取當前sku的資訊
    sku = SKU.objects.get(id=sku_id)
    sku.images = sku.skuimage_set.all()

    # 麵包屑導航資訊中的頻道
    goods = sku.goods
    goods.channel = goods.category1.goodschannel_set.all()[0]

    # 構建當前商品的規格鍵
    sku_specs = sku.skuspecification_set.order_by('spec_id')
    sku_key = []
    for spec in sku_specs:
        sku_key.append(spec.option.id)

    # 獲取當前商品的所有SKU
    skus = goods.sku_set.all()

    # 構建不同規格引數(選項)的sku字典
    # spec_sku_map = {
    #     (規格1引數id, 規格2引數id, 規格3引數id, ...): sku_id,
    #     (規格1引數id, 規格2引數id, 規格3引數id, ...): sku_id,
    #     ...
    # }
    spec_sku_map = {}
    for s in skus:
        # 獲取sku的規格引數
        s_specs = s.skuspecification_set.order_by('spec_id')
        # 用於形成規格引數-sku字典的鍵
        key = []
        for spec in s_specs:
            key.append(spec.option.id)
        # 向規格引數-sku字典新增記錄
        spec_sku_map[tuple(key)] = s.id

    # 獲取當前商品的規格資訊
    specs = goods.goodsspecification_set.order_by('id')
    # 若當前sku的規格資訊不完整,則不再繼續
    if len(sku_key) < len(specs):
        return
    for index, spec in enumerate(specs):
        # 複製當前sku的規格鍵
        key = sku_key[:]
        # 該規格的選項
        options = spec.specificationoption_set.all()
        for option in options:
            # 在規格引數sku字典中查詢符合當前規格的sku
            key[index] = option.id
            option.sku_id = spec_sku_map.get(tuple(key))

        spec.options = options

    # 渲染模板,生成靜態html檔案
    context = {
        'categories': categories,
        'goods': goods,
        'specs': specs,
        'sku': sku
    }

    template = loader.get_template('detail.html')
    html_text = template.render(context)
    file_path = os.path.join(settings.GENERATED_STATIC_HTML_FILES_DIR, 'goods/'+str(sku_id)+'.html')
    with open(file_path, 'w') as f:
        f.write(html_text)


if __name__ == '__main__':
    skus = SKU.objects.all()
    for sku in skus:
        print(sku.id)
        generate_static_sku_detail_html(sku.id)