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

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


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

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


“分享、點贊、在看” 支持一波 
