(十四)综合应用

111 阅读2分钟

@TOC

综合应用

使用 jQuery 做一个模拟购物车的小 demo ,不用 vue 或 React 是因为它们封装了很多东西,使用封装的框架,就不容易分析设计模式。

包括:显示购物车列表、加入购物车、从购物车删除 在这里插入图片描述 用到的设计模式:

  • 工厂模式:$(...),创建商品实例
  • 单例模式:购物车
  • 装饰器模式:log(点击按钮日志打点)
  • 观察者模式:事件监听、Promise
  • 状态模式:添加到购物车 & 从购物车删除
  • 模板方法模式:渲染的方法统一成一个,里面再分别写渲染不同部分的代码
  • 代理模式:优惠商品打折:name 有“优惠”字样、price 是原价 80%
  • 外观模式:???
  • 适配器模式:???
  • 职责连模式:Promise 多个 then

注意,这不是一个完整的项目,是为了演示设计模式而做的一个真实项目的一部分

学习设计模式前后的对比:

  • 学习之前:主要考虑如何操作 DOM ,绑定事件,发送请求
  • 学习之后:考虑面向对象,设计模式,合理性和可扩展性(其实这些都是 vue React 统一做的,是前端开发的升级)

搭建开发环境

使用现有 ES6 环境即可,不用重新搭建。

演示项目功能

待项目做完之后,演示一下功能,让学员知道即将做了个什么项目

画 UML 类图

根据项目演示,分析功能,并画出 UML 类图。

在这里插入图片描述

开始写代码

在这里插入图片描述

准备工作

在 index.html 中增加一个容器,即<div id="app"></div>

安装 jQuery npm install jquery -save

入口文件

新建src/index.js作为入口文件,内容如下

import App from './demo/App.js'

let app = new App('app')
app.init()

App.js

参考 App.js 的内容,注意要引入 jQuery

import $ from 'jquery'
import ShoppingCart from './ShoppingCart/ShoppingCart.js'
import List from './List/List.js'

export default class App {
    constructor(id) {
        this.$el = $(`#${id}`)//工厂模式
    }

    // 初始化购物车
    initShoppingCart() {
        let shoppingCart = new ShoppingCart(this)
        shoppingCart.init()
    }

    // 初始化商品列表
    initList() {
        let list = new List(this)
        list.init()
    }

    init() {
        this.initShoppingCart()
        this.initList()
    }
}

ShoppingCart.js

参考 ShoppingCart.js 的内容。一开始showCart方法内容可不写,待Cart类创建完之后再补充

import $ from 'jquery'
import getCart from './GetCart.js'

export default class ShoppingCart {
    constructor(app) {
        this.app = app
        this.$el = $('<div>').css({
            'padding-bottom': '10px',
            'border-bottom': '1px solid #ccc'
        })
        this.cart = getCart()
    }

    // 显示购物车内容
    showCart() {
        alert(this.cart.getList())
    }

    // 初始化按钮
    initBtn() {
        let $btn = $('<button>购物车</button>')
        //观察者模式
        $btn.click(() => {
            this.showCart()
        })
        this.$el.append($btn)
    }

    // 渲染
    render() {
        this.app.$el.append(this.$el)
    }

    init() {
        this.initBtn()
        this.render()
    }
}

GetCart.js

参考 GetCart.js 的内容,要说明这是一个单例。创建完之后,完善 ShoppingCart.js 的内容。

class Cart {
    constructor() {
        this.list = []
    }
    add(data) {
        this.list.push(data)
    }
    del(id) {
        this.list = this.list.filter(item => {
            if (item.id === id) {
                return false
            }
            return true
        })
    }
    getList() {
        return this.list.map(item => {
            return item.name
        }).join('\n')
    }
}

// 返回单例,单例模式
let getCart = (function () {
    let cart
    return function () {
        if (!cart) {
            cart = new Cart();
        }
        return cart
    }
})()

export default getCart

List.js

参考 List.js 的内容。

在写loadData之前要先:

  • 要先创建好api/list.json
  • webpack-dev-server 的 proxy
  • 另外要介绍 fetch 的使用
  • 创建config/config.js

在没有createItem之前,可先不写initItemList的内容。

//list.json
[
    {
        "id": 1,
        "name": "《JS 基础面试题》",
        "price": 149,
        "discount": 1
    },
    {
        "id": 2,
        "name": "《JS 高级面试题》",
        "price": 366,
        "discount": 1
    },
    {
        "id": 3,
        "name": "《React 模拟大众点评 webapp》",
        "price": 248,
        "discount": 0
    },
    {
        "id": 4,
        "name": "《zepto 设计与源码解读》",
        "price": 0,
        "discount": 0
    }
]
//config.js 
export const GET_LIST = '/api/list.json'
import $ from 'jquery'
import createItem from '../Item/CreateItem.js'
import { GET_LIST } from '../config/config.js'

export default class List {
    constructor(app) {
        this.app = app
        this.$el = $('<div>')
    }

    // 获取数据
    loadData() {
        // 使用 fetch (低版本浏览器可使用 https://github.com/github/fetch 兼容)
        // 返回 promise
        return fetch(GET_LIST).then(result => {
            return result.json()
        })
    }

    // 生成列表
    initItemList(data) {
        data.map(itemData => {
            let item = createItem(this, itemData)
            item.init()
            return item
        })
    }

    // 渲染
    render() {
        this.app.$el.append(this.$el)
    }

    init() {
    	//观察者模式
        this.loadData().then(data => {
            this.initItemList(data)
        }).then(() => {
            // 最后再一起渲染 DOM ,以避免重复渲染的性能问题
            this.render()
        })
    }
}

Item.js

参考 Item.js 的内容。CreateItem.js 一开始可先不考虑折扣的功能,后面再补充。

完成之后补充 List.js 的遗留内容。

状态管理和增加日志,可先不写,后面单独讲。

import $ from 'jquery'
import StateMachine from 'javascript-state-machine'
import { log } from '../util/log.js'
import getCart from '../ShoppingCart/GetCart.js'

export default class Item {
    constructor(list, data) {
        this.list = list
        this.data = data
        this.$el = $('<div>')
        this.cart = getCart()
    }

    initContent() {
        let $el = this.$el
        let data = this.data
        $el.append($(`<p>名称:${data.name}</p>`))
        $el.append($(`<p>价格:${data.price}</p>`))
    }

    initBtn() {
        let $el = this.$el
        let $btn = $('<button>')

        // 状态管理,状态模式
        let _this = this
        let fsm = new StateMachine({
            init: '加入购物车',
            transitions: [
                {
                    name: 'addToCart', 
                    from: '加入购物车',
                    to: '从购物车删除'
                },
                {
                    name: 'deleteFromCart',
                    from: '从购物车删除',
                    to: '加入购物车'
                }
            ],
            methods: {
                // 加入购物车
                onAddToCart: function () {
                    _this.addToCartHandle()
                    updateText()
                },
                // 删除
                onDeleteFromCart: function () {
                    _this.deleteFromCartHandle()
                    updateText()
                }
            }
        })
        function updateText() {
            $btn.text(fsm.state)
        }

        $btn.click(() => {
            if (fsm.is('加入购物车')) {
                fsm.addToCart()
            } else {
                fsm.deleteFromCart()
            }
        })
        updateText()

        $el.append($btn)
    }

    // 加入购物车,装饰器模式
    @log('add')
    addToCartHandle() {
        this.cart.add(this.data)
    }

    // 从购物车删除,装饰器模式
    @log('del')
    deleteFromCartHandle() {
        this.cart.del(this.data.id)
    }

    render() {
        this.list.$el.append(this.$el)
    }

    init() {
    	//模板方法模式
        this.initContent()
        this.initBtn()
        this.render()
    }
}

CreateItem.js

import Item from './Item.js'

function createDiscount(item) {
    // 用代理做折扣显示,代理模式
    return new Proxy(item, {
        get: function (target, key, receiver) {
            if (key === 'name') {
                return `${target[key]}【折扣】`
            }
            if (key === 'price') {
                return target[key] * 0.8
            }
            return target[key]
        }
    })
}

// 工厂模式
export default function (list, itemData) {
    if (itemData.discount) {
        itemData = createDiscount(itemData)
    }
    return new Item(list, itemData)
}

状态管理

按钮状态管理,以及增加、删除购物车。记得npm i javascript-state-machine --save

模拟日志log.js

增加、删除购物车时,增加日志功能。代码写在util/log.js中。

nodejs v6 及一下的需要npm i babel-plugin-transform-decorators-legacy --save-dev,以及修改.babelrc增加plugin配置。

//装饰器模式
export function log(type) {
    return function (target, name, descriptor) {
        var oldValue = descriptor.value;
    
        descriptor.value = function() {
            //  此处统一上报日志
            console.log(`日志上报 ${type}`);
    
            // 执行原有方法
            return oldValue.apply(this, arguments);
        };
    
        return descriptor;
    }
}

补充折扣功能

完善 CreateItem.js 补充折扣功能