如何写出可维护和可读性高的代码,这一直是一个困扰不少人的问题。关于变量如何起名、如何优化if else之类的小技巧,这里就不作介绍了,推荐去看《代码大全2》,千书万书,都不如一本《代码大全2》。javascript
工做以来,我一直在写一些重复且交互复杂的页面,也没有整理过本身的思路,这篇文章是我工做一年半来在项目中总结出来的一些经验。前端
对于业务代码来讲,大部分的前端应用都仍是以展现数据为主,无非是从接口拿到数据,进行一系列数据格式化后,显示在页面当中。java
首先,应当尽量的进行分层,传统的mvc分层很适用于前端开发,但对于复杂页面来讲,随着业务逻辑增长,每每会形成controller臃肿的问题。所以,在此之上,能够将controller其分红formatter、service等等。react
下面这是一些分层后简单的目录结构。api
+ pages + hotelList + components + Header.jsx + formatter + index.js + share + constants.js + utils.js + view.js + controller.js + model.js
统一管理全部请求路径,而且将页面中涉及到的网络请求封装为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层储存一些格式化数据的方法,这些方法接收数据,返回新的数据,不该该再涉及到其余的逻辑,这样有利于单元测试。单个format函数也不该该格式化过多数据,函数应该根据功能进行适当拆分,合理复用。网络
顾名思义,controller就是mvc中的c,controller应该是处理各类反作用操做(网络请求、缓存、事件响应等等)的地方。数据结构
当处理一个请求的时候,controller会调用service里面对应的方法,拿到数据后再调用formatter的方法,将格式化后的数据存入store中,展现到页面上。mvc
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 = (components) => { 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等等。
下方是快捷筛选项,对应了部分筛选项菜单里面的子类筛选项。
当咱们选中筛选里面的双床后,下方的双床也会被默认选中,反之当咱们选中下方的双床后,筛选类别里面的双床也会被选中,名称还会回显到原来的筛选上。
除此以外,咱们点击搜索框后,输入'双床',联想词会出现双床,并表示这是个筛选项,若是用户选中了这个双床,咱们依然须要筛选项和快捷筛选项默认选中。
这三个地方都涉及到了筛选项,而且修改一个,其余两个地方就要跟着改变,更况且三者的数据来自于三个不一样的接口数据,这是多么蛋疼的一件事情!
我借助这个例子来讲明,在开始写页面以前,必定要对页面中的隐藏交互和数据流动很熟悉,也须要去设计更加合理的数据结构。
对于深层次的列表结构,键值对会比数组查询速度更快,经过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}的键值对格式),这样从时间复杂度上说,比直接遍历两个数组更高效。
在开始写业务以前,理应先想清楚需求和业务逻辑,设计出合理的数据结构,对代码进行好的分层,这样在必定程度上能够写出可维护性更高的代码。