@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 补充折扣功能