前端框架面試之 項目設計 和 項目流程

一、項目設計

  1. 對於組件和狀態設計,從數據驅動視圖、狀態的數據結構設計,React-stateVue-data,視圖中組件結構和拆分。
  2. 對於 React 實現 TodoListstate 數據結構設計、組件設計組件通訊和結合 redux,如下所示:
  • state 數據結構設計,如下所示:
    • 用數據描述所有的內容
    • 數據要結構化,易於程序操作,遍歷和查找
    • 數據要可擴展,以便增加新的功能
  • 組件設計的拆分和組合,以及組件通訊,如下所示:
    • 從功能上拆分層次
    • 儘量讓組件原子化
    • 容器組件只管理數據,UI 組件只顯示視圖
  1. 對於 React 實現 TodoList,代碼如下所示:
  • UI 文件夾下的 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

  • UI 文件夾下的 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

  • 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

  • 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

  • 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

  1. 對於 Vue 實現購物車,從 data 數據結構設計、組件設計和組件通訊。對於 data 的數據結構設計,也需要用數據描述所有的內容;數據要結構化,易於程序操作,遍歷和查找;數據要可擴展,以便增加新的功能。

  2. 對於 Vue 實現購物車的簡易版,代碼如下所示:

  • CartList 文件夾下的 CartItem.vue
<template>
    <div>
        <span>{{item.title}}</span>
        &nbsp;
        <span>(數量 {{item.quantity}})</span>
        &nbsp;
        <a href="#" @click="addClickHandler(item.id, $event)">增加</a>
        &nbsp;
        <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>
        &nbsp;
        <span>{{item.price}}</span>
        &nbsp;
        <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>
  • event.js
import Vue from 'vue'

export default new Vue()

  • 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>
  1. 對於 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.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
}

  • 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()] : []
})

  • app.js
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)
})

  • currency.js
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
}

  • index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>vuex shopping cart example</title>
    <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>

二、項目流程

  1. 對於項目流程,項目分多人、多角色參與,項目也分多階段,項目需要計劃和運行。
  2. 對於項目角色有哪些,答案如下所示:
  • PM 產品經理
  • UE 視覺設計師
  • FE 前端開發
  • RD 後端開發
  • CRD 移動端開發
  • QA 測試人員
  1. 一個完整的項目要分爲哪些階段,前端如何參與,答案如下所示:
  • 需求分析階段,各個角色都會進行參與
  • 技術方案設計階段,FE 前端開發、RD 後端開發 和 CRD 移動端開發 會進行參與
  • 開發階段,FE 前端開發 會進行參與
  • 聯調階段,FE 前端開發、RD 後端開發 和 CRD 移動端開發 會進行參與
  • 測試階段,FE 前端開發 和 QA 測試人員 會進行參與
  • 上線階段,FE 前端開發 會進行參與
  1. 對於評審項目需求時,需要注意哪些事項,答案如下所示:
  • 瞭解背景
  • 質疑需求是否合理
  • 需求是否閉環
  • 開發難度如何
  • 是否需要其它支持
  • 不用急於給排期
  1. 對於項目,技術方案如何設計,答案如下所示:
  • 求簡,不過度設計
  • 產出文檔
  • 找準設計重點
  • 組內評審
  • RD、CRD 溝通
  • 發出會議結論
  1. 對於開發,如何保證代碼質量,答案如下所示:
  • 如何反饋排期
  • 符合開發規範
  • 寫出開發文檔
  • 及時單元測試
  • Mock API
  • Code Review
  1. 對於聯調階段,如何處理,答案如下所示:
  • RD 後端開發、CRD 移動端開發 技術聯調
  • UE 視覺設計師 確定視覺效果
  • PM 產品經理 確定產品功能
  1. 如果在項目開發過程中,PM 加需求,怎麼辦,答案如下所示:
  • 不能拒絕,走需求變更流程即可
  • 如果公司有規定,則按規定走
  • 否則,發起項目組和 leader 的評審,重新評估排期
  1. 對於測試,出現的問題,如何解決,答案如下所示:
  • 提測發郵件,抄送項目組
  • 測試問題要詳細記錄
  • 有問題及時溝通,QA 測試人員 和 FE 前端開發 天生信息不對稱
  1. 不要對 QA 測試人員說 我的電腦沒有問題,這是因爲,答案如下所示:
  • 不要說這句話
  • 當面討論,讓 QA 測試人員 幫你復現
  • 如果需要特定設備才能復現,讓 QA 測試人員 提供設備
  1. 對於項目上線的問題,答案如下所示:
  • 上線之後及時通知 QA 測試人員 迴歸測試
  • 上線之後及時同步給 PM 產品經理 和項目組
  • 如有問題,及時回滾,先止損,再排查問題
  1. 對於項目溝通的重要性,答案如下所示:
  • 多人協作,溝通是最重要的事情
  • 每日一溝通,如站會,有事說事,無事報平安
  • 及時識別風險,及時彙報
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章