React-state
、Vue-data
,视图中组件结构和拆分。React
实现 TodoList
,state
数据结构设计、组件设计组件通讯和结合 redux
,如下所示:state
数据结构设计,如下所示:
UI
组件只显示视图React
实现 TodoList
,代码如下所示:UI
文件夹下的 CheckBox.jsimport React from 'react'
class CheckBox extends React.Component {
constructor(props) {
super(props)
this.state = {
checked: false
}
}
render() {
return <input type="checkbox" checked={this.state.checked} onChange={this.onCheckboxChange}/>
}
onCheckboxChange = () => {
const newVal = !this.state.checked
this.setState({
checked: newVal
})
// 传给父组件
this.props.onChange(newVal)
}
}
export default CheckBox
UI
文件夹下的 Input.jsimport React from 'react'
class Input extends React.Component {
render() {
return <input value={this.props.value} onChange={this.onChange}/>
}
onChange = (e) => {
// 传给父组件
const newVal = e.target.value
this.props.onChange(newVal)
}
}
export default Input
import React from 'react'
import List from './List'
import InputItem from './InputItem'
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
list: [
{
id: 1,
title: '标题1',
completed: false
},
{
id: 2,
title: '标题2',
completed: false
},
{
id: 3,
title: '标题3',
completed: false
}
]
}
}
render() {
return <div>
<InputItem addItem={this.addItem}/>
<List
list={this.state.list}
deleteItem={this.deleteItem}
toggleCompleted={this.toggleCompleted}
/>
</div>
}
// 新增一项
addItem = (title) => {
const list = this.state.list
this.setState({
// 使用 concat 返回不可变值
list: list.concat({
id: Math.random().toString().slice(-5), // id 累加
title,
completed: false
})
})
}
// 删除一项
deleteItem = (id) => {
this.setState({
// 使用 filter 返回不可变值
list: this.state.list.filter(item => item.id !== id)
})
}
// 切换完成状态
toggleCompleted = (id) => {
this.setState({
// 使用 map 返回不可变值
list: this.state.list.map(item => {
const completed = item.id === id
? !item.completed
: item.completed // 切换完成状态
// 返回新对象
return {
...item,
completed
}
})
})
}
}
export default App
import React from 'react'
import Input from './UI/Input'
class InputItem extends React.Component {
constructor(props) {
super(props)
this.state = {
title: ''
}
}
render() {
return <div>
<Input value={this.state.title} onChange={this.changeHandler}/>
<button onClick={this.clickHandler}>新增</button>
</div>
}
changeHandler = (newTitle) => {
this.setState({
title: newTitle
})
}
clickHandler = () => {
const { addItem } = this.props
addItem(this.state.title)
this.setState({
title: ''
})
}
}
export default InputItem
import React from 'react'
import ListItem from './ListItem'
function List({ list = [], deleteItem, toggleCompleted }) {
return <div>
{list.map(item => <ListItem
item={item}
key={item.id}
deleteItem={deleteItem}
toggleCompleted={toggleCompleted}
/>)}
</div>
}
export default List
import React from 'react'
import CheckBox from './UI/CheckBox'
class ListItem extends React.Component {
render() {
const { item } = this.props
return <div style={{ marginTop: '10px' }}>
<CheckBox onChange={this.completedChangeHandler}/>
<span style={{ textDecoration: item.completed ? 'line-through' : 'none' }}>
{item.title}
</span>
<button onClick={this.deleteHandler}>删除</button>
</div>
}
completedChangeHandler = (checked) => {
console.log('checked', checked)
const { item, toggleCompleted } = this.props
toggleCompleted(item.id)
}
deleteHandler = () => {
const { item, deleteItem } = this.props
deleteItem(item.id)
}
}
export default ListItem
对于 Vue
实现购物车,从 data
数据结构设计、组件设计和组件通讯。对于 data
的数据结构设计,也需要用数据描述所有的内容;数据要结构化,易于程序操作,遍历和查找;数据要可扩展,以便增加新的功能。
对于 Vue
实现购物车的简易版,代码如下所示:
CartList
文件夹下的 CartItem.vue<template>
<div>
<span>{{item.title}}</span>
<span>(数量 {{item.quantity}})</span>
<a href="#" @click="addClickHandler(item.id, $event)">增加</a>
<a href="#" @click="delClickHandler(item.id, $event)">减少</a>
</div>
</template>
<script>
import event from '../event'
export default {
props: {
item: {
type: Object,
default() {
return {
// id: 1,
// title: '商品A',
// price: 10,
// quantity: 1 // 购物数量
}
}
}
},
methods: {
addClickHandler(id, e) {
e.preventDefault()
event.$emit('addToCart', id)
},
delClickHandler(id, e) {
e.preventDefault()
event.$emit('delFromCart', id)
}
}
}
</script>
CartList
文件夹下的 index.vue<template>
<div>
<CartItem
v-for="item in list"
:key="item.id"
:item="item"
/>
<p>总价 {{totalPrice}}</p>
</div>
</template>
<script>
import CartItem from './CartItem'
export default {
components: {
CartItem,
},
props: {
productionList: {
type: Array,
default() {
return [
// {
// id: 1,
// title: '商品A',
// price: 10
// }
]
}
},
cartList: {
type: Array,
default() {
return [
// {
// id: 1,
// quantity: 1
// }
]
}
}
},
computed: {
// 购物车商品列表
list() {
return this.cartList.map(cartListItem => {
// 找到对应的 productionItem
const productionItem = this.productionList.find(
prdItem => prdItem.id === cartListItem.id
)
// 返回商品信息,外加购物数量
return {
...productionItem,
quantity: cartListItem.quantity
}
// 如:
// {
// id: 1,
// title: '商品A',
// price: 10,
// quantity: 1 // 购物数量
// }
})
},
// 总价
totalPrice() {
return this.list.reduce(
(total, curItem) => total + (curItem.quantity * curItem.price),
0
)
}
}
}
</script>
CartList
文件夹下的 TotalPrice.vue<template>
<p>total price</p>
</template>
<script>
export default {
data() {
return {
}
}
}
</script>
ProductionList
文件夹下的 index.vue<template>
<div>
<ProductionItem
v-for="item in list"
:key="item.id"
:item="item"
/>
</div>
</template>
<script>
import ProductionItem from './ProductionItem'
export default {
components: {
ProductionItem,
},
props: {
list: {
type: Array,
default() {
return [
// {
// id: 1,
// title: '商品A',
// price: 10
// }
]
}
}
}
}
</script>
ProductionList
文件夹下的 ProductionItem.vue<template>
<div>
<span>{{item.title}}</span>
<span>{{item.price}}元</span>
<a href="#" @click="clickHandler(item.id, $event)">加入购物车</a>
</div>
</template>
<script>
import event from '../event'
export default {
props: {
item: {
type: Object,
default() {
return {
// id: 1,
// title: '商品A',
// price: 10
}
}
}
},
methods: {
clickHandler(id, e) {
e.preventDefault()
event.$emit('addToCart', id)
}
},
}
</script>
import Vue from 'vue'
export default new Vue()
<template>
<div>
<ProductionList :list="productionList"/>
<hr>
<CartList
:productionList="productionList"
:cartList="cartList"
/>
</div>
</template>
<script>
import ProductionList from './ProductionList/index'
import CartList from './CartList/index'
import event from './event'
export default {
components: {
ProductionList,
CartList
},
data() {
return {
productionList: [
{
id: 1,
title: '商品A',
price: 10
},
{
id: 2,
title: '商品B',
price: 15
},
{
id: 3,
title: '商品C',
price: 20
}
],
cartList: [
{
id: 1,
quantity: 1 // 购物数量
}
]
}
},
methods: {
// 加入购物车
addToCart(id) {
// 先看购物车中是否有该商品
const prd = this.cartList.find(item => item.id === id)
if (prd) {
// 数量加一
prd.quantity++
return
}
// 购物车没有该商品
this.cartList.push({
id,
quantity: 1 // 默认购物数量 1
})
},
// 从购物车删除一个(即购物数量减一)
delFromCart(id) {
// 从购物车中找出该商品
const prd = this.cartList.find(item => item.id === id)
if (prd == null) {
return
}
// 数量减一
prd.quantity--
// 如果数量减少到了 0
if (prd.quantity <= 0) {
this.cartList = this.cartList.filter(
item => item.id !== id
)
}
}
},
mounted() {
event.$on('addToCart', this.addToCart)
event.$on('delFromCart', this.delFromCart)
}
}
</script>
Vue
实现购物车的复杂版,代码如下所示:api
文件夹下的 shop.js/**
* Mocking client-server processing
*/
const _products = [
{"id": 1, "title": "iPad 4 Mini", "price": 500.01, "inventory": 2},
{"id": 2, "title": "H&M T-Shirt White", "price": 10.99, "inventory": 10},
{"id": 3, "title": "Charli XCX - Sucker CD", "price": 19.99, "inventory": 5}
]
export default {
// 获取所有商品,异步模拟 ajax
getProducts (cb) {
setTimeout(() => cb(_products), 100)
},
// 结账,异步模拟 ajax
buyProducts (products, cb, errorCb) {
setTimeout(() => {
// simulate random checkout failure.
// 模拟可能失败的情况
(Math.random() > 0.5 || navigator.userAgent.indexOf('PhantomJS') > -1)
? cb()
: errorCb()
}, 100)
}
}
components
文件夹下的 App.vue<template>
<div id="app">
<h1>Shopping Cart Example</h1>
<hr>
<h2>Products</h2>
<ProductList/>
<hr>
<ShoppingCart/>
</div>
</template>
<script>
import ProductList from './ProductList.vue'
import ShoppingCart from './ShoppingCart.vue'
export default {
components: { ProductList, ShoppingCart }
}
</script>
components
文件夹下的 ProductList.vue<template>
<ul>
<li
v-for="product in products"
:key="product.id">
{{ product.title }} - {{ product.price | currency }}
(inventory: {{product.inventory}})<!-- 这里可以自己加一下显示库存 -->
<br>
<button
:disabled="!product.inventory"
@click="addProductToCart(product)">
Add to cart
</button>
</li>
</ul>
</template>
<script>
import { mapState, mapActions } from 'vuex'
export default {
computed: mapState({
// 获取所有商品
products: state => state.products.all
}),
methods: mapActions('cart', [
// 添加商品到购物车
'addProductToCart'
]),
created () {
// 加载所有商品
this.$store.dispatch('products/getAllProducts')
}
}
</script>
components
文件夹下的 ShoppingCart.vue<template>
<div class="cart">
<h2>Your Cart</h2>
<p v-show="!products.length"><i>Please add some products to cart.</i></p>
<ul>
<li
v-for="product in products"
:key="product.id">
{{ product.title }} - {{ product.price | currency }} x {{ product.quantity }}
</li>
</ul>
<p>Total: {{ total | currency }}</p>
<p><button :disabled="!products.length" @click="checkout(products)">Checkout</button></p>
<p v-show="checkoutStatus">Checkout {{ checkoutStatus }}.</p>
</div>
</template>
<script>
import { mapGetters, mapState } from 'vuex'
export default {
computed: {
...mapState({
// 结账的状态
checkoutStatus: state => state.cart.checkoutStatus
}),
...mapGetters('cart', {
products: 'cartProducts', // 购物车的商品
total: 'cartTotalPrice' // 购物车商品的总价格
})
},
methods: {
// 结账
checkout (products) {
this.$store.dispatch('cart/checkout', products)
}
}
}
</script>
``
- store 文件夹下的 modules 文件夹下的 cart.js
```js
import shop from '../../api/shop'
// initial state
// shape: [{ id, quantity }]
const state = {
// 已加入购物车的商品,格式如 [{ id, quantity }, { id, quantity }]
// 注意,购物车只存储 id 和数量,其他商品信息不存储
items: [],
// 结账的状态 - null successful failed
checkoutStatus: null
}
// getters
const getters = {
// 获取购物车商品
cartProducts: (state, getters, rootState) => {
// rootState - 全局 state
// 购物车 items 只有 id quantity ,没有其他商品信息。要从这里获取。
return state.items.map(({ id, quantity }) => {
// 从商品列表中,根据 id 获取商品信息
const product = rootState.products.all.find(product => product.id === id)
return {
title: product.title,
price: product.price,
quantity
}
})
},
// 所有购物车商品的价格总和
cartTotalPrice: (state, getters) => {
// reduce 的经典使用场景,求和
return getters.cartProducts.reduce((total, product) => {
return total + product.price * product.quantity
}, 0)
}
}
// actions —— 异步操作要放在 actions
const actions = {
// 结算
checkout ({ commit, state }, products) {
// 获取购物车的商品
const savedCartItems = [...state.items]
// 设置结账的状态 null
commit('setCheckoutStatus', null)
// empty cart 清空购物车
commit('setCartItems', { items: [] })
// 请求接口
shop.buyProducts(
products,
() => commit('setCheckoutStatus', 'successful'), // 设置结账的状态 successful
() => {
commit('setCheckoutStatus', 'failed') // 设置结账的状态 failed
// rollback to the cart saved before sending the request
// 失败了,就要重新还原购物车的数据
commit('setCartItems', { items: savedCartItems })
}
)
},
// 添加到购物车
// 【注意】这里没有异步,为何要用 actions ???—— 因为要整合多个 mutation
// mutation 是原子,其中不可再进行 commit !!!
addProductToCart ({ state, commit }, product) {
commit('setCheckoutStatus', null) // 设置结账的状态 null
// 判断库存是否足够
if (product.inventory > 0) {
const cartItem = state.items.find(item => item.id === product.id)
if (!cartItem) {
// 初次添加到购物车
commit('pushProductToCart', { id: product.id })
} else {
// 再次添加购物车,增加数量即可
commit('incrementItemQuantity', cartItem)
}
// remove 1 item from stock 减少库存
commit('products/decrementProductInventory', { id: product.id }, { root: true })
}
}
}
// mutations
const mutations = {
// 商品初次添加到购物车
pushProductToCart (state, { id }) {
state.items.push({
id,
quantity: 1
})
},
// 商品再次被添加到购物车,增加商品数量
incrementItemQuantity (state, { id }) {
const cartItem = state.items.find(item => item.id === id)
cartItem.quantity++
},
// 设置购物车数据
setCartItems (state, { items }) {
state.items = items
},
// 设置结算状态
setCheckoutStatus (state, status) {
state.checkoutStatus = status
}
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}
store
文件夹下的 modules
文件夹下的 products.jsimport shop from '../../api/shop'
// initial state
const state = {
all: []
}
// getters
const getters = {}
// actions —— 异步操作要放在 actions
const actions = {
// 加载所有商品
getAllProducts ({ commit }) {
// 从 shop API 加载所有商品,模拟异步
shop.getProducts(products => {
commit('setProducts', products)
})
}
}
// mutations
const mutations = {
// 设置所有商品
setProducts (state, products) {
state.all = products
},
// 减少某一个商品的库存(够买一个,库存就相应的减少一个,合理)
decrementProductInventory (state, { id }) {
const product = state.all.find(product => product.id === id)
product.inventory--
}
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}
store
文件夹下的 index.jsimport Vue from 'vue'
import Vuex from 'vuex'
import cart from './modules/cart'
import products from './modules/products'
import createLogger from '../../../src/plugins/logger'
Vue.use(Vuex)
const debug = process.env.NODE_ENV !== 'production'
export default new Vuex.Store({
modules: {
cart,
products
},
strict: debug,
plugins: debug ? [createLogger()] : []
})
import 'babel-polyfill'
import Vue from 'vue'
import App from './components/App.vue'
import store from './store'
import { currency } from './currency'
Vue.filter('currency', currency) // 转换为 `$19.99` 格式,无需过多关注
new Vue({
el: '#app',
store,
render: h => h(App)
})
const digitsRE = /(\d{3})(?=\d)/g
export function currency (value, currency, decimals) {
value = parseFloat(value)
if (!isFinite(value) || (!value && value !== 0)) return ''
currency = currency != null ? currency : '$'
decimals = decimals != null ? decimals : 2
var stringified = Math.abs(value).toFixed(decimals)
var _int = decimals
? stringified.slice(0, -1 - decimals)
: stringified
var i = _int.length % 3
var head = i > 0
? (_int.slice(0, i) + (_int.length > 3 ? ',' : ''))
: ''
var _float = decimals
? stringified.slice(-1 - decimals)
: ''
var sign = value < 0 ? '-' : ''
return sign + currency + head +
_int.slice(i).replace(digitsRE, '$1,') +
_float
}
<html lang="en">
<head>
<meta charset="utf-8">
<title>vuex shopping cart exampletitle>
<link rel="stylesheet" href="/global.css">
head>
<body>
<div id="app">div>
<script src="/__build__/shared.js">script>
<script src="/__build__/shopping-cart.js">script>
body>
html>
PM
产品经理UE
视觉设计师FE
前端开发RD
后端开发CRD
移动端开发QA
测试人员FE
前端开发、RD
后端开发 和 CRD
移动端开发 会进行参与FE
前端开发 会进行参与FE
前端开发、RD
后端开发 和 CRD
移动端开发 会进行参与FE
前端开发 和 QA
测试人员 会进行参与FE
前端开发 会进行参与RD、CRD
沟通Mock API
Code Review
RD
后端开发、CRD
移动端开发 技术联调UE
视觉设计师 确定视觉效果PM
产品经理 确定产品功能PM
加需求,怎么办,答案如下所示:leader
的评审,重新评估排期QA
测试人员 和 FE
前端开发 天生信息不对称QA
测试人员说 我的电脑没有问题,这是因为,答案如下所示:QA
测试人员 帮你复现QA
测试人员 提供设备QA
测试人员 回归测试PM
产品经理 和项目组