如何写好前端业务代码

分层

对于业务代码来说,大部分的前端应用都还是以展示数据为主,无非是从接口拿到数据,进行一系列数据格式化后,显示在页面当中。 首先,应当尽可能的进行分层,传统的mvc分层很适用于前端开发,但对于复杂页面来说,随着业务逻辑增加,往往会造成controller臃肿的问题。因此,在此之上,可以将controller其分成formatter、service等等。 下面这是一些分层后简单的目录结构。

1
2
3
4
5
6
7
8
9
10
11
12
    + pages
        + hotelList
            + components
                + Header.jsx
            + formatter
                + index.js
            + share
                + constants.js
                + utils.js
            + view.js
            + controller.js
            + model.js

Service

统一管理所有请求路径,并且将页面中涉及到的网络请求封装为class。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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中,展示到页面上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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组件,建议尽量用纯函数组件,有了hooks之后,react也会变得更加纯粹(实际上有状态组件也可以看做一个mvc的结构,state是model,render是view,各种handler方法是controller)。 对于react来说,最外层的一般称作容器组件,我们会在容器组件里面进行网络请求等副作用的操作。 在这里,容器组件里面的一些逻辑也可以剥离出来放到controller中(react-imvc就是这种做法),这样可以给controller赋予生命周期,容器组件只用于纯展示。 我们将容器组件的生命周期放到wrapper这个高阶组件中,并在里面调用controller里面封装的生命周期,这样我们可以就编写更加纯粹的view,例如: wrapper.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// view.js
function view({
    state,
    actions
}) {

    return (
        <>
            <Header 
                title={state.title} 
                handleBack={actions.goBackPage}
            />
            <Body />
            <Footer />
        </>
    )
}
export default Wrapper(view)

controller.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 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来获取值,这样便于后续维护。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 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() {
        //
    }
}

数据与交互 不过也不代表着这样写就够了,分层只能够保证代码结构上的清晰,真正想写出好的业务代码,最重要的还是你对业务逻辑足够清晰,页面上的数据流动是怎样的?数据结构怎么设计更加合理?页面上有哪些交互?这些交互会带来哪些影响?

总结 在开始写业务之前,理应先想清楚需求和业务逻辑,设计出合理的数据结构,对代码进行好的分层,这样在一定程度上可以写出可维护性更高的代码。

  • 版权声明: 本博客所有文章,未经许可,任何单位及个人不得做营利性使用!转载请标明出处!如有侵权请联系作者。
  • Copyrights © 2015-2023 翟天野

请我喝杯咖啡吧~