如何寫好前端業務程式碼?

尹光耀發表於2019-03-21

前言

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

工作以來,我一直在寫一些重複且互動複雜的頁面,也沒有整理過自己的思路,這篇文章是我工作一年半來在專案中總結出來的一些經驗。

分層

對於業務程式碼來說,大部分的前端應用都還是以展示資料為主,無非是從介面拿到資料,進行一系列資料格式化後,顯示在頁面當中。

首先,應當儘可能的進行分層,傳統的mvc分層很適用於前端開發,但對於複雜頁面來說,隨著業務邏輯增加,往往會造成controller臃腫的問題。因此,在此之上,可以將controller其分成formatter、service等等。

如何寫好前端業務程式碼?
下面這是一些分層後簡單的目錄結構。

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

Service

統一管理所有請求路徑,並且將頁面中涉及到的網路請求封裝為class。

// 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)。

對於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:歡迎大家關注我的公眾號【前端小館】,大家一起來討論技術。

如何寫好前端業務程式碼?

相關文章