前端进阶篇之如何编写可维护可升级的代码

前言

我还在携程的做业务的时候,每个看似简单的移动页面背后往往会隐藏5个以上的数据请求,其中最过复杂的当属机票与酒店的订单填写业务代码

这里先看看比较“简单”的机票代码:

然后看看稍微复杂的酒店业务逻辑:

机票一个页面的代码量达到了5000行代码,而酒店的代码竟然超过了8000行,这里还不包括模板(html)文件!!!

然后初略看了机票的代码,就该页面可能发生的接口请求有19个之多!!!而酒店的的交互DOM事件基本多到了令人发指的地步:

当然,机票团队的交互DOM事件已经多到了我笔记本不能截图了:

就这种体量的页面,如果需要迭代需求、打BUG补丁的话,我敢肯定的说,一个BUG的修复很容易引起其它BUG,而上面还仅仅是其中一个业务页面,后面还有强大而复杂的前端框架呢!如此复杂的前端代码维护工作可不是开玩笑的!

PS:说道此处,不得不为携程的前端水平点个赞,业内少有的单页应用,一套代码H5&Hybrid同时运行不说,还解决了SEO问题,嗯,很赞。

如何维护这种页面,如何设计这种页面是我们今天讨论的重点,而上述是携程合并后的代码,他们两个团队的设计思路不便在此处展开。

今天,我这里提供一个思路,认真阅读此文可能在以下方面对你有所帮助:

文中是我个人的一些框架&业务开发经验,希望对各位有用,也希望各位多多支持讨论,指出文中不足以及提出您的一些建议

由于该项目涉及到了项目拆分与合并,基本属于一个完整的前端工程化案例了,所以将之放到了github上:https://github.com/yexiaochai/mvc

其中工程化一块的代码,后续会由另一位小伙伴持续更新,如果该文对各位有所帮助的话请各位给项目点个赞、加颗星:)

我相信如果是中级水平的前端,认真阅读此文一定会对你有一点帮助滴。

一个实际的场景

演示地址

http://yexiaochai.github.io/mvc/webapp/bus/list.html

代码仓促,可能会有BUG哦:)

代码地址:https://github.com/yexiaochai/mvc/

页面基本构成

因为订单填写页一般有密度,我这里挑选相对复杂而又没有密度的产品列表页来做说明,其中框架以及业务代码已经做过抽离,不会包含敏感信息,一些优化后续会同步到开源blade框架中去。

我们这里列表页的首屏页面如下:

简单来说组成如下:

① 框架级别UI组件UIHeader,头部组件

② 点击日期会出框架级别UI,日历组件UICalendar

③ 点击出发时段、出发汽车站、到达汽车站,皆会出框架级别UI

④ header下面的日期工具栏需要作为独立的业务模块

⑤ 列表区域可以作为独立的业务模块,但是与主业务靠太近,不太适合

⑥ 出发时段、出发汽车站、到达汽车站皆是独立的业务模块

一个页面被我们拆分成了若干个小模块,我们只需要关注模块内部的交互实现,而包括业务模块的通信,业务模块的样式,业务模块的重用,暂时有以下约定:

这里有些朋友可能认为单个模块的CSS以及image也应该参与独立,我这里不太同意,业务页面样式粒度太细的话会给设计带来不小的麻烦,这里再以通俗的话来说:尼玛,我CSS功底一般,拆分的太细,对我来说难度太高……

不好的做法

不好的这个事情其实是相对的,因为不好的做法一般是比较简单的做法,对于一次性项目或者业务比较简单的页面来说反而是好的做法,比如这里的业务逻辑可以这样写:

根据之前的经验,如果仅仅包含这些业务逻辑,这样写代码问题不是非常大,代码量预计在800行左右,但是为了完成完整的业务逻辑,我们这里马上产生了新的需求。

需求迭代

因为我这里的班次列表,最初是没有URL参数,所以根本无法产出班次列表,页面上所有组件模块都是摆设,于是这里新增一个需求:

于是,我们这里会新增一个简单的弹出层:

这个看似简单的弹出层,背后却隐藏了一个巨大的陷阱,因为点击出发或者到达时会出城市列表,而城市列表本身就是一个比较复杂的业务:

于是页面的组成发生了改变:

① 本身业务逻辑约800行代码

② 新增出发到达筛选弹出层

③ 出发城市页面,预计300行代码

而弹出层的新增对业务本身造成了深远的影响,本来url是不带有业务参数的,但是点击了弹出层的确定按钮,需要改变URL参数,并且刷新本身页面的数据,于是简单的一个弹出层新增直接将页面的复杂程度提升了一倍。

于是该页面代码轻轻松松破千了,后续需求迭代js代码量破2000仅仅是时间问题,到时候维护便复杂了,页面复杂无规律的DOM操作将会令你焦头烂额,这个时候组件化开发的优势便得以体现了,于是下面进入组件化开发的设计。

准备工作

总体架构

这次的代码依赖于blade骨架,包括:

① MVC模块,完成通过url获取正确的page控制器,从而通过view.js完成渲染页面的功能

② 数据请求模块,完成接口请求

全站依赖于javascript的继承功能,详情见:【一次面试】再谈javascript中的继承,如果不太了解面向对象编程,文中代码可能会有点吃力,也请各位多多了解。

总体业务架构如图:

框架架构图:

.

下面分别介绍下各个模块,帮助各位在下文中能更好的了解代码,首先是基本MVC的介绍,这里请参考我这篇文章:简单的MVC介绍

全局控制器

其实控制器可谓是变化万千的一个对象,对于服务器端来说,控制器完成的功能是将本次请求分发到具体的代码模块,由代码模块处理后返回字符串给前端;

对于请求已经来到浏览器的前端来说,根据这次请求URL(或者其它判断条件),判断该次请求应该由哪个前端js控制器执行,这是前端控制器干的事情;

当真的这次处理逻辑进入一个具体的page后,这个page事实上也可以作为一个控制器存在……

我们这里的控制器,主要完成根据当前请求实例化View的功能,并且会提供一些view级别希望单例使用的接口:

这里属于框架控制器层面的代码,与今天的主题不是非常相关,有兴趣的朋友可以详细读读。

页面基类

这里的核心是页面级别的处理,这里会做比较多的介绍,首先我们为所有的业务级View提供了一个继承的View:

一个Page级别的View会有以下几个关键属性&方法:

① template,html字符串,不包含请求的基础模块,会构成页面的html骨架层

② events,所有的DOM事件定义处,以事件代理的方式定义,所以不必担心执行顺序

③ addEvent,用于页面级别各个阶段的监控事件注册点,一般来说用户只需要关注很少几个事件,比如:

一个页面的基本写法:

只要按照这种规则写,便能展示页面,并且具备DOM交互事件。

页面模块类

所谓页面模块类,便是用于拆分一个页面为单个组件模块所用类,这里有这些约定:

这里代码可以再优化,但不是我们这里关注的重点:

数据实体类

这里的数据实体对应着,MVC中的Model,因为之前已经使用model用作了数据请求相关的命名,这里便使用Entity做该工作:

这里的数据实体会以实例的方式注入给模块类实例,他的工作是起一个中枢左右,完成模块之间的通信,反正非常重要就是了

其它

数据请求统一使用abstract.model,数据前端缓存使用abstract.store,这里因为目标是做页面拆分,请求模块不是关键,各位可以把这段代码看层一个简单的ajax即可:

业务入口

最后简单说下业务入口文件:

很简单的代码,指定了下require的path配置,最后我们看看入口页面的调用:

接下来,让我们真实的开始拆分页面吧。

组件式编程

骨架设计

首先,我们进行最简单的骨架设计,这里依次是其js代码与模板代码:

页面展示如图:

日历工具栏的实现

这里要做的第一步是将日历工具栏模块实现,以数据为先的思考,我们先实现了一个与日历业务有关的数据实体:

里面完成日期工具栏所有相关数据操作,并且不包含实际的业务逻辑。

然后这里开始设计日期工具栏的模块View:

这个组件模块干了几个事情:

① 首先,dateEntity实体需要由list.js这个主view注入

② 这里为dateEntity注册了两个数据响应事件:

render方法继承至基类,使用template与数据生成html,其中数据产生必须重写父类一个方法:

因为这里的日历数据,默认取当前时间,但是url参数可能传递日期参数,所以定义了一个数据初始化方法:

该方法在主页面渲染结束后会第一时间调用,这个时候日历工具栏便渲染出来,其中日历组件的使用便不予理睬了,主控制器的代码改变如下:

于是,整个界面变成了这个样子:

这里是对应的日历工具模板文件tpl.calendar.html:

搜索工具栏的实现

我们现在的页面,就算不传任何URL参数,已经能渲染出部分页面了,但是下面出发站汽车等业务数据必须等待班次列表数据请求结束才能替换数据,但是这些数据如果没有出发城市和到达城市是不能发起请求的,所以这里先实现搜索工具栏功能:

在出发城市或者到达城市不存在的话便弹出搜索工具栏,引导用户选择城市,这里新增弹出层需要在主页面控制器(检测主控制器)中使用一个UI组件:

对应搜索弹出层html模板:

这里核心代码是:

于是当URL什么参数都没有的时候,就会弹出这个搜索框

这里也迎来了一个难点,因为城市列表事实上应该是一个独立的可访问的页面,但是这里是想用弹出层的方式调用他,所以我在APP层实现了一个方法可以用弹出层的方式调起一个独立的页面。

这里有一个不同的地方是,因为我们点击查询的时候才会做实体数据更新,这里是单纯的做DOM操作了,这里不设置数据实体一个原因就是:

这个搜索弹出层是一个页面级DOM之外的部分,数据实体变化一般只应该影响Page级别的DOM,除非真的有两个页面级View会公用一个数据实体。

搜索功能完成后,我们这里便可以进入真正的数据请求功能渲染列表了。

其余模块

在实现数据请求之前,我按照日期模块的方式将下面三个模块的功能也一并完成了,这里唯一不同的是,这些模块的DOM已经存在,我们不需要渲染了,完成后的代码大概是这样的: