项目设计
组件和状态设计
框架(Vue React)的使用(和高级特性)是必要条件
能独立负责项目?还是需要别人带着?—考察设计能力
面试必考(二面/三面),场景题
考察重点
数据驱动视图
状态:数据结构设计(React - state,Vue - data)
视图:组件结构和拆分
面试题
React设计todolist(组件结构,redux state 数据结构)
Vue设计购物车(组件结构,vuex state数据结构)
React实现Todo List
state数据结构设计
用数据描述所有的内容
数据要结构化,易于程序操作(遍历、查找)
数据要可扩展,以便增加新的功能
组件设计(拆分、组合)和组件通讯
从功能上拆分层次
尽量让组件原子化(一个组件只负责一个功能)
容器组件(只管理数据)& UI组件(只显示视图)
代码演示
//index.js
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
//List.js
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
//ListItem.js
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
//CheckBox.js
import 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
//InputItem.js
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
//Input.js
import 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
总结
state数据结构设计
组件设计组件通讯
结合redux
Vue实现购物车
data数据结构设计
用数据描述所有的内容
数据要结构化,易于程序操作(遍历、查找)
数据要可扩展,以便增加新的功能
组件设计和组件通讯
从功能上拆分层次
尽量让组件原子化
容器组件(只管理数据)& UI组件(只显示视图)
代码演示
//index.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>
// ProductionList
<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>
//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>
//CartList
<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>
//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>
//TotalPrice.vue
<template>
<p>total price</p>
</template>
<script>
export default {
data() {
return {
}
}
}
</script>
结合Vuex实现购物车
//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>
//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>
//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
//index.js
import 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()] : []
})
//mock 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)
}
}
//cart.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
}
//products.js
import 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
}
总结
data数据结构设计
组件设计组件通讯
结合redux