如何寫好前端業務代碼?

前言

如何寫出可維護和可讀性高的代碼,這一直是一個困擾不少人的問題。關於變量如何起名、如何優化if else之類的小技巧,這裏就不作介紹了,推薦去看《代碼大全2》,千書萬書,都不如一本《代碼大全2》。javascript

工做以來,我一直在寫一些重複且交互複雜的頁面,也沒有整理過本身的思路,這篇文章是我工做一年半來在項目中總結出來的一些經驗。前端

分層

對於業務代碼來講,大部分的前端應用都仍是以展現數據爲主,無非是從接口拿到數據,進行一系列數據格式化後,顯示在頁面當中。java

首先,應當儘量的進行分層,傳統的mvc分層很適用於前端開發,但對於複雜頁面來講,隨着業務邏輯增長,每每會形成controller臃腫的問題。所以,在此之上,能夠將controller其分紅formatter、service等等。react

下面這是一些分層後簡單的目錄結構。

+ pages
        + hotelList
            + components
                + Header.jsx
            + formatter
                + index.js
            + share
                + constants.js
                + utils.js
            + view.js
            + controller.js
            + model.js
複製代碼

Service

統一管理全部請求路徑,而且將頁面中涉及到的網絡請求封裝爲class。api

// api.js
export default {
    HOTELLIST: '/hotelList',
    HOTELDETAIL: '/hotelDetail'
}

// Service.js
class Service {
    fetchHotelList = (params) => {
        return fetch(HOTELLIST, params);
    }
    fetchHotelDetail = (params) => {
        return fetch(HOTELLIST, params);
    }
}
export default new Service
複製代碼

這樣帶來的好處就是,很清楚的知道頁面中涉及了哪些請求,若是使用了TypeScript,後續某個請求方法名修改了後,在全部調用的地方也會提示錯誤,很是方便。數組

formatter

formatter層儲存一些格式化數據的方法,這些方法接收數據,返回新的數據,不該該再涉及到其餘的邏輯,這樣有利於單元測試。單個format函數也不該該格式化過多數據,函數應該根據功能進行適當拆分,合理複用。緩存

mvc

顧名思義,controller就是mvc中的c,controller應該是處理各類反作用操做(網絡請求、緩存、事件響應等等)的地方。網絡

當處理一個請求的時候,controller會調用service裏面對應的方法,拿到數據後再調用formatter的方法,將格式化後的數據存入store中,展現到頁面上。數據結構

class Controller {
    fetchHotelList = () => async (dispatch) => {
        const params = {}
        this.showLoading();
        try {
            const res = await Service.fetchHotelList(params)
            const hotelList = formatHotelList(res.Data && res.Data.HotelList)
            dispatch({
                type: 'UPDATE_HOTELLIST',
                hotelList
            })
        } catch (err) {
            this.showError(err);
        } finally {
            this.hideLoading();
        }
    }
}
複製代碼

view則是指react組件,建議儘可能用純函數組件,有了hooks以後,react也會變得更加純粹(實際上有狀態組件也能夠看作一個mvc的結構,state是model,render是view,各類handler方法是controller)。mvc

對於react來講,最外層的通常稱做容器組件,咱們會在容器組件裏面進行網絡請求等反作用的操做。

在這裏,容器組件裏面的一些邏輯也能夠剝離出來放到controller中(react-imvc就是這種作法),這樣就能夠給controller賦予生命週期,容器組件只用於純展現。

咱們將容器組件的生命週期放到wrapper這個高階組件中,並在裏面調用controller裏面封裝的生命週期,這樣咱們能夠就編寫更加純粹的view,例如:

wrapper.js

// wrapper.js(僞代碼)
const Wrapper = (view) => {
    return class extends Component {
        constructor(props) {
            super(props)
        }
        componentWillMount() {
            this.props.pageWillMount && this.props.pageWillMount()
        }
        componentDidMount() {
                this.props.pageDidMount && this.props.pageDidMount()
            }
        }
        componentWillUnmount() {
            this.props.pageWillLeave && this.props.pageWillLeave()
        }
        render() {
            const {
                store: state,
                actions
            } = this.props
            return view({state, actions})
        }
    }
}
複製代碼

view.js

// view.js
function view({ state, actions }) {
    
    return (
        <>
            <Header 
                title={state.title} 
                handleBack={actions.goBackPage}
            />
            <Body />
            <Footer />
        </>
    )
}
export default Wrapper(view)
複製代碼

controller.js

// controller.js
class Controller {
    pageDidMount() {
        this.bindScrollEvent('on')
        console.log('page did  mount')
    }
    pageWillLeave() {
        this.bindScrollEvent('off')
        console.log('page will leave')
    }
    bindScrollEvent(status) {
        if (status === 'on) {
            this.bindScrollEvent('off');
            window.addEventListener('scroll', this.handleScroll);
        } else if (status === 'off') {
            window.removeEventListener('scroll', this.handleScroll);
        }
    }
    // 滾動事件
    handleScroll() {
        
    }
}
複製代碼

其餘

對於埋點來講,本來也應該放到controller中,但也是能夠獨立出來一個tracelog層,至於tracelog層如何實現和調用,仍是看我的愛好,我比較喜歡用發佈訂閱的形式。

若是還涉及到緩存,那咱們也能夠再分出來一個storage層,這裏存放對緩存進行增刪查改的各類操做。

對於一些經常使用的固定不變的值,也能夠放到constants.js,經過引入constants來獲取值,這樣便於後續維護。

// constants.js
export const cityMapping = {
    '1': '北京',
    '2': '上海'
}
export const traceKey = {
    'loading': 'PAGE_LOADING'
}
// tracelog.js
class TraceLog {
    traceLoading = (params) => {
        tracelog(traceKey.loading, params);
    }
}
export default new TraceLog

// storage.js
export default class Storage {
    static get instance() {
        // 
    }
    setName(name) {
        //
    }
    getName() {
        //
    }
}
複製代碼

數據與交互

不過也不表明着這樣寫就夠了,分層只可以保證代碼結構上的清晰,真正想寫出好的業務代碼,最重要的仍是你對業務邏輯足夠清晰,頁面上的數據流動是怎樣的?數據結構怎麼設計更加合理?頁面上有哪些交互?這些交互會帶來哪些影響?

以以下酒店列表頁爲例,這個頁面看似簡單,實際上包含了不少複雜的交互。

上方的是四個篩選項菜單,點開后里麪包含了不少子類篩選項,好比篩選裏面包括了雙牀、大牀、三牀,價格/星級裏面包含了高檔/豪華、¥150-¥300等等。

下方是快捷篩選項,對應了部分篩選項菜單裏面的子類篩選項。

image_1d6dgvgio1hfg82u1kmo1u141tv69.png-230.3kB

當咱們選中篩選裏面的雙牀後,下方的雙牀也會被默認選中,反之當咱們選中下方的雙牀後,篩選類別裏面的雙牀也會被選中,名稱還會回顯到原來的篩選上。

image_1d6dgvscipuea6nsnhl6a1ijam.png-231.3kB

image_1d6dhiup11mnj12351f0fl36msh1g.png-57.7kB

除此以外,咱們點擊搜索框後,輸入'雙牀',聯想詞會出現雙牀,並表示這是個篩選項,若是用戶選中了這個雙牀,咱們依然須要篩選項和快捷篩選項默認選中。

這三個地方都涉及到了篩選項,而且修改一個,其餘兩個地方就要跟着改變,更況且三者的數據來自於三個不一樣的接口數據,這是多麼蛋疼的一件事情!

image_1d6dhc833375eo118vq1od61j6u13.png-40.4kB

我藉助這個例子來講明,在開始寫頁面以前,必定要對頁面中的隱藏交互和數據流動很熟悉,也須要去設計更加合理的數據結構。

對於深層次的列表結構,鍵值對會比數組查詢速度更快,經過key也會更容易和其餘數據進行聯動,可是卻不能保證順序,有時候可能就須要犧牲空間來換時間。

// 假設篩選項牀型type爲1,大牀id爲1,雙牀id爲2.
const bed = {
    '1-1': {
        name: '大牀',
        id: 1,
        type: 1
    },
    '1-2': {
        name: '雙牀',
        id: 2,
        type: 1
    }
}
const bedSort = ['1-1', '1-2'] // 保證展現順序
複製代碼

當咱們選中大牀的時候,只須要保存'1-1'這個key,再和store中快捷篩選項列表裏面的key進行mapping(快捷篩選項裏面的項也應該格式化爲{'type-id': filterItem}的鍵值對格式),這樣從時間複雜度上說,比直接遍歷兩個數組更高效。

總結

在開始寫業務以前,理應先想清楚需求和業務邏輯,設計出合理的數據結構,對代碼進行好的分層,這樣在必定程度上能夠寫出可維護性更高的代碼。

PS:歡迎你們關注個人公衆號【前端小館】,你們一塊兒來討論技術。

相關文章
相關標籤/搜索