|
分层 对于业务代码来说,大部分的前端应用都还是以展示数据为主,无非是从接口拿到数据,进行一系列数据格式化后,显示在页面当中。 首先,应当尽可能的进行分层,传统的mvc分层很适用于前端开发,但对于复杂页面来说,随着业务逻辑增加,往往会造成controller臃肿的问题。因此,在此之上,可以将controller其分成formatter、service等等。 下面这是一些分层后简单的目录结构。 + pages
+ hotelList
+ components
+ Header.jsx
+ formatter
+ index.js
+ share
+ constants.js
+ utils.js
+ view.js
+ controller.js
+ model.jsService 统一管理所有请求路径,并且将页面中涉及到的网络请求封装为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 { 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 // 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() { } } 其他 对于埋点来说,原本也应该放到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() { // } } |
|