如何寫(xiě)好前端業(yè)務(wù)代碼?(一些個(gè)人小見(jiàn)解)
點(diǎn)擊上方 程序員成長(zhǎng)指北,關(guān)注公眾號(hào)
回復(fù)1,加入高級(jí)Node交流群
前言
原本只是想簡(jiǎn)單群發(fā)一下,但是預(yù)覽之后看到格式不友好,還是簡(jiǎn)單寫(xiě)一篇文章吧,這是我工作一年半來(lái)自己總結(jié)出來(lái)的一些經(jīng)驗(yàn)。
分層
對(duì)于業(yè)務(wù)代碼來(lái)說(shuō),大部分的前端應(yīng)用都還是以展示數(shù)據(jù)為主,無(wú)非是從接口拿到數(shù)據(jù),進(jìn)行一系列數(shù)據(jù)格式化后,顯示在頁(yè)面當(dāng)中。
首先,應(yīng)當(dāng)盡可能的進(jìn)行分層,傳統(tǒng)的mvc分層很適用于前端開(kāi)發(fā),但對(duì)于復(fù)雜頁(yè)面來(lái)說(shuō),隨著業(yè)務(wù)邏輯增加,往往會(huì)造成controller臃腫的問(wèn)題。因此,在此之上,可以將controller其分成formatter、service等等。
下面這是一些分層后簡(jiǎn)單的目錄結(jié)構(gòu)。
+ pages
+ hotelList
+ components
+ Header.jsx
+ formatter
+ index.js
+ share
+ constants.js
+ utils.js
+ view.js
+ controller.js
+ model.js
Service
統(tǒng)一管理所有請(qǐng)求路徑,并且將頁(yè)面中涉及到的網(wǎng)絡(luò)請(qǐng)求封裝為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
這樣帶來(lái)的好處就是,很清楚的知道頁(yè)面中涉及了哪些請(qǐng)求,如果使用了TypeScript,后續(xù)某個(gè)請(qǐng)求方法名修改了后,在所有調(diào)用的地方也會(huì)提示錯(cuò)誤,非常方便。
formatter
formatter層儲(chǔ)存一些格式化數(shù)據(jù)的方法,這些方法接收數(shù)據(jù),返回新的數(shù)據(jù),不應(yīng)該再涉及到其他的邏輯,這樣有利于單元測(cè)試。單個(gè)format函數(shù)也不應(yīng)該格式化過(guò)多數(shù)據(jù),函數(shù)應(yīng)該根據(jù)功能進(jìn)行適當(dāng)拆分,合理復(fù)用。
mvc
顧名思義,controller就是mvc中的c,controller應(yīng)該是處理各種副作用操作(網(wǎng)絡(luò)請(qǐng)求、緩存等等)的地方。
當(dāng)處理一個(gè)請(qǐng)求的時(shí)候,controller會(huì)調(diào)用service里面對(duì)應(yīng)的方法,拿到數(shù)據(jù)后再調(diào)用formatter的方法,將格式化后的數(shù)據(jù)存入store中,展示到頁(yè)面上。
class Controller {
fetchAndSaveHotelList = () => 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組件,建議盡量用純函數(shù)組件,有了hooks之后,react也會(huì)變得更加純粹(實(shí)際上有狀態(tài)組件也可以看做一個(gè)mvc的結(jié)構(gòu),state是model,render是view,各種handler方法是controller)。
對(duì)于react來(lái)說(shuō),最外層的一般稱(chēng)作容器組件,我們會(huì)在容器組件里面進(jìn)行網(wǎng)絡(luò)請(qǐng)求等副作用的操作。
在這里,容器組件里面的一些邏輯也可以剝離出來(lái)放到controller中(react-imvc就是這種做法),這樣可以給controller賦予生命周期,容器組件只用于純展示。
我們將容器組件的生命周期放到wrapper這個(gè)高階組件中,并在里面調(diào)用controller里面封裝的生命周期,這樣我們可以就編寫(xiě)更加純粹的view,例如:
wrapper.js
// wrapper.js(偽代碼)
const Wrapper = (components) => {
return class extends Component {
constructor(props) {
super(props)
}
componentWillMount() {
this.props.pageWillMount()
}
componentDidMount() {
this.props.pageDidMount()
}
componentWillUnmount() {
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);
}
}
// 滾動(dòng)事件
handleScroll() {
}
}
其他
對(duì)于埋點(diǎn)來(lái)說(shuō),原本也應(yīng)該放到controller中,但也是可以獨(dú)立出來(lái)一個(gè)tracelog層,至于tracelog層如何實(shí)現(xiàn)和調(diào)用,還是看個(gè)人愛(ài)好,我比較喜歡用發(fā)布訂閱的形式。
如果還涉及到緩存,那我們也可以再分出來(lái)一個(gè)storage層,這里存放對(duì)緩存進(jìn)行增刪查改的各種操作。
對(duì)于一些常用的固定不變的值,也可以放到constants.js,通過(guò)引入constants來(lái)獲取值,這樣便于后續(xù)維護(hù)。
// 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() {
//
}
}數(shù)據(jù)與交互
不過(guò)也不代表著這樣寫(xiě)就夠了,分層只能夠保證代碼結(jié)構(gòu)上的清晰,真正想寫(xiě)出好的業(yè)務(wù)代碼,最重要的還是你對(duì)業(yè)務(wù)邏輯足夠清晰,頁(yè)面上的數(shù)據(jù)流動(dòng)是怎樣的?數(shù)據(jù)結(jié)構(gòu)怎么設(shè)計(jì)更加合理?頁(yè)面上有哪些交互?這些交互會(huì)帶來(lái)哪些影響?
以如下酒店列表頁(yè)為例,這個(gè)頁(yè)面看似簡(jiǎn)單,實(shí)際上包含了很多復(fù)雜的交互。
上方的是四個(gè)篩選項(xiàng)菜單,點(diǎn)開(kāi)后里面包含了很多子類(lèi)篩選項(xiàng),比如篩選里面包括了雙床、大床、三床,價(jià)格/星級(jí)里面包含了高檔/豪華、¥150-¥300等等。
下方是快捷篩選項(xiàng),對(duì)應(yīng)了部分篩選項(xiàng)菜單里面的子類(lèi)篩選項(xiàng)。

當(dāng)我們選中篩選里面的雙床后,下方的雙床也會(huì)被默認(rèn)選中,反之當(dāng)我們選中下方的雙床后,篩選類(lèi)別里面的雙床也會(huì)被選中,名稱(chēng)還會(huì)回顯到原來(lái)的篩選上。


除此之外,我們點(diǎn)擊搜索框后,輸入'雙床',聯(lián)想詞會(huì)出現(xiàn)雙床,并表示這是個(gè)篩選項(xiàng),如果用戶(hù)選中了這個(gè)雙床,我們依然需要篩選項(xiàng)和快捷篩選項(xiàng)默認(rèn)選中。
這三個(gè)地方都涉及到了篩選項(xiàng),并且修改一個(gè),其他兩個(gè)地方就要跟著改變,更何況三者的數(shù)據(jù)來(lái)自于三個(gè)不同的接口數(shù)據(jù),這是多么蛋疼的一件事情!

我借助這個(gè)例子來(lái)說(shuō)明,在開(kāi)始寫(xiě)頁(yè)面之前,一定要對(duì)頁(yè)面中的隱藏交互和數(shù)據(jù)流動(dòng)很熟悉,也需要去設(shè)計(jì)更加合理的數(shù)據(jù)結(jié)構(gòu)。
對(duì)于深層次的列表結(jié)構(gòu),鍵值對(duì)會(huì)比數(shù)組查詢(xún)速度更快,但是卻不能保證順序,這個(gè)時(shí)候就需要犧牲空間來(lái)?yè)Q時(shí)間。
總結(jié)
在開(kāi)始寫(xiě)業(yè)務(wù)之前,理應(yīng)先想清楚需求和業(yè)務(wù)邏輯,設(shè)計(jì)出合理的數(shù)據(jù)結(jié)構(gòu),對(duì)代碼進(jìn)行好的分層,這樣在一定程度上可以寫(xiě)出可維護(hù)性更高的代碼。
代碼例子:目前對(duì)前半段寫(xiě)了一個(gè)簡(jiǎn)單的例子,有需要的可以學(xué)習(xí)下:https://github.com/yinguangyao/mvc-demo


“分享、點(diǎn)贊、在看” 支持一波 
