前言
随着前端项目数量和规模越来越大,参与的人员也越来越多,如何在前端项目开发过程中保证优质的开发者体验和项目的可维护性,同时确保极致的用户体验将会是一个非常大的挑战。
为了应对这个挑战,美团点评境外度假前端研发团队自2016年6月起启动了面向C端用户的”赫尔墨斯”项目,主要围绕以下几个方面进行展开:
- 前后端分离:前端拥有完整独立的开发、测试、部署的流程,与后端完全分离,减少沟通成本。
- 模块化与组件化:封装可重用UI组件、业务逻辑,提升代码库的可复用性、可测试性。
- 流程自动化:提升效率、避免重复手工工作、保证质量、自动资源优化等等。
- 页面加载性能优化:建立前端监控体系、优化资源加载、使用离线化策略。
前后端分离
在之前的项目中,页面是由Java后端项目中通过FTL模板引擎拼装,前端团队会维护另外一个前端的项目,存放相应的CSS和JS文件,最后通过公司内部的Cortex系统打包发布。
这个流程的问题在于前端对于整个页面入口没有控制力,需要依赖后端的FTL拼装,页面的内容需要更改时,前后端同学就要反复沟通协调,整体效率比较差,容易出错,也不方便实现前端相关的优化。更坑的是有时候还要求前端同学安装一整套后端的开发环境,费时费力不说,光维护这套不断变换的环境就要费不少精力。
因此,我们认为前后端分离的关键点在于前端拥有完整独立的开发、测试、部署的流程,与后端完全分离。
在赫尔墨斯项目中,我们把页面的组装完全放置到了前端项目,后端只提供AJAX的接口用于获取和提交数据。前端页面完全静态化,构建完毕之后连同相应的静态资源通过CI直接发布到CDN。
模块化
模块化开发在其它开发领域(比如客户端后端开发)已经实施了很多年了,而在前端开发领域,一直没有一个统一的模块化的规范。随着ES6 Module规范的落地,这个问题终于(部分)解决了。模块化开发的优势主要有以下几个方面。
- 更好的代码组织结构和开发协作:通过细致的文件夹、文件拆分,更易于管理复杂的代码库,更易于多人协作开发,降低文件合并时候冲突的发生概率,方便编写单元测试。
- 依赖管理:不再需要手动管理脚本的加载顺序。
- 优化:
- 代码打包(Bundle):合并小模块,抽取公共模块,在资源请求数和浏览器缓存利用方面进行合适的取舍。
- 代码分割(Split):允许按需加载JS代码(分路由、异步组件),解决单页面应用(SPA)首屏加载速度问题。
- Tree Shaking:利用ES6模块的静态化特性,可以在构建过程中分析出代码库中未使用到的代码,从最终的bundle中去除,从而减少JS Bundle的尺寸。
- Scope Hoisting:ES6模块内容导入和导出绑定是活动的,可以将多个小模块合并到一个函数当中,对于重复变量名进行合适的重命名,从而减少Bundle的尺寸和提升加载速度。
组件化
如果说模块化是解决如何封装和复用一段逻辑代码的话,组件化要解决的是如何封装和复用一个用户界面元素,例如,一个按钮、一个弹出框,亦或是一个轮播图。由于浏览器原生并没有提供这么一套组件化开发的API,这个领域目前也是处在相对不稳定的状态中,各种框架层出不穷,比较有代表性的有React、Vue和Angular。我们最终选择的是Vue.js作为我们组件化开发的基础API(W3C实际上有一套Web Component的规范,目前已定稿,但是浏览器支持非常有限。同时功能上缺乏了现在框架普遍拥有的数据绑定、同构渲染等等)。
主要是基于以下几个方面的考虑。
- 体积:19kB(min+gzip)
- API和学习成本:
- 声明式组件模板和分离样式表,更接近于传统开发模式,抵触心理小。
- 响应式的组件状态跟踪:更新状态代码更简洁,组件树重新渲染效率更高。
- 清晰简洁的生命周期钩子函数和单向数据流:页面逻辑和状态更新更可控。
- 运行时报错和告警详细:方便新手入门和规避常见错误。
- 工具链完整性:webpack Loader(加载Vue单文件组件)、开发者工具(Dev Tools)、脚手架(vue-cli)、单元测试友好(vue-test-utils)。
- 运行时性能:
- Virtual DOM来管理组件树渲染到真实DOM的状态同步,使用高效的算法来最小化DOM操作的次数。
- 由于响应式设计,不需要优化组件树再次渲染的范围。
- 组件树静态部分被单独处理,重新渲染不需要重新构建。
- 同构渲染:
- 高性能、开箱即用的方案,包括前后端可用的路由和状态管理组件,降低了使用的门槛。
- 深度webpack集成,简化了代码分割和构建调试流程。
Vue.js提供了一种单文件组件的格式允许把一个组件相关联的模板、逻辑和样式写在一个文件当中,通过上文提到的一个定制化的webpack loader可以把它转换为一个包含Vue.js的组件配置对象的模块被其它模块引用。
基于Vue.js,我们开发了一套适合移动端开发的组件库dora-ui,提供了一套符合我们团队业务需求的基础组件库,它主要由以下几个部分构成
- 20个Vue.js 2.0兼容组件,涵盖布局、导航、数据输入、数据展示、信息反馈等等方面。
- 组件文档:每一个组件需要有一个相应的Readme(markdown格式)文件,描述组件的用途、属性、事件、插槽等等。
- 组件示例:每一个组件可以有一个或者多个示例,来展示组件的用法。
- 组件复用度查询:可以快速查找一个组件被多少个页面所引用以及一个页面引用了多少个组件。
- webpack plugin:在项目构建时候收集项目页面和组件引用关系,输出一个JSON文件。
- 查询页面:通过读取上述JSON文件,提供一个界面供开发人员查询。
流程自动化
在工程标准化自动化方面,我们想要达到的目标是统一技术栈,保持技术栈的先进性,规范化代码样式以及自动化一切可以自动化的任务。
所有可以自动化的任务都应该被自动完成。
工程模板
我们建立了统一的项目模板,基于约定大于配置的理念,简化了新项目创建的流程以及页面和组件的开发和调试。
本地组件测试开发
为了方便开发和测试单个组件,我们在每个组件的目录下面会创建一个demo目录。在构建过程中,借助webpack的require.context API来获取components目录下所有组件的demo文件,随后为每个组件Demo创建一个路由。
var demoRequire = require.context('@component', true, /demo\/.*\.vue$/);
//遍历取出所有demo组件
const demos = demoRequire.keys().map(demoKey => {
var [componentName, demoName] = demoKey.split('/demo/');
componentName = componentName.substring(2);
demoName = demoName.substring(0, demoName.lastIndexOf('.'));
return {
componentName: componentName,
demoName: demoName,
component: demoRequire(demoKey)
}
});
//组成key + value 形式的demo组件对象集合
const demosByComponent = _.groupBy(demos, demo => {
return demo.componentName;
});
//整个组件页面的路由
const routesByComponent = Object.keys(demosByComponent).map(componentName => {
return {
path: '/' + componentName,
component: require('./component.vue'),
meta: {
componentName: componentName,
demoComponents: demosByComponent[componentName]
}
}
});
//组件页面内调试每个单独demo的路由
const routesByDemo = demos.map(demo => {
return {
path: '/' + demo.componentName + '/' + demo.demoName,
component: demo.component,
meta: {
componentName: demo.componentName,
}
}
});
本地Mock服务
前后端分离之后,为了加速前后端并行开发的效率,我们基于webpack-dev-server,实现了一套本地Mock服务,能够在本地开发环境模拟任意API请求的响应。
同时为了提升效率,根据模板工程目录的约定,这些Mock文件能够被自动发现同时一旦发生变更可以实时刷新。
页面加载性能优化
关于页面加载性能优化,我们首先要建立监控体系,收集用户侧真实数据,然后基于数据进行页面加载的优化。
同时,为了进一步提升用户体验,我们还进行了前端离线化的支持。
监控体系
建立一个完整的监控体系是性能优化的前提条件。我们认为,前端监控体系大体由3部分构成(下图)。
技术监控服务于开发人员,收集开发人员所需要的性能及异常相关的数据。
用户行为监控服务于产品和运营,主要收集用户在页面上操作的行为,比如点击、曝光等等。
展示查询提供可视化查询工具,通过报表、图表、仪表盘的形式,满足对于数据可视化的需求。
网络链路优化
对于静态资源,从海外回源的成本非常高,通过对接海外的CDN供应商,能够在世界各地部署多个静态资源的缓存代理,根据用户的地理位置选择最近的位置进行静态资源的分发。
同时通过增加香港中间源,及中间源到源站的专线减少从海外直接回源源站造成的性能开销。
对于AJAX请求,在香港部署了SLB来做中转,SLB与后台服务是通过专线连接的。
主文档回源优化
由于主文档无法进行长缓存,针对主文档回源过于频繁的问题,我们通过在CDN边缘节点覆盖源站缓存设置,将主文档缓存30天,使得主文档回源减少(注意:用户侧看到的仍然是源站设置的缓存时间,用户侧设置为10分钟)。
同时,通过在发布流程当中加入主动清除海外CDN缓存的功能,来解决缓存更新的问题。
域名收敛 & 减少请求数
存在问题:
- 页面引用的第三方脚本,比如监控、打点,缺乏海外CDN及长缓存支持,这些脚本的存在影响了加载时间。
- 多个域名也增加了域名解析的成本和建立连接的成本。
我们的做法是把第三方脚本打包到我们的代码里面,并抽取公共代码已增加缓存的效率,同时把所有静态资源和主文档公用一个域名。
离线化
由于境外行中场景网络不稳当,无法保持实时在线,我们有些工具类的页面比方说汇率助手等等实际上在离线情况下也能够使用。此外,离线化也能提升加载速度,因为主文档也不再需要网络请求了。
考虑到浏览器多平台兼容性问题,我们最终是基于HTML Application Cache API来打造了我们的离线化方案(下图)。
在构建流程中,通过分析页面资源依赖关系,自动生成资源manifest文件,这样就能够确保页面及资源发生变更时,manifest文件内容同步更新。
需要特别注意的是,当用户再次访问访问页面的时候,如果页面的manifest发生变更,浏览器会自动重新下载manifest里面的文件,完成之后会在applicationCache对象上发出updateready事件,但是并不会自动刷新页面,也就是说这个时候用户会看到之前的版本,而不是最新的版本。当用户再次进入这个页面的时候,将会访问到最新的版本。在大部分情况下,这都不是问题,因为移动端网页的停留时间是非常有限的。假设某一次页面更新非常重要,期待用户立即就进行页面刷新,我们可以在监听到updateready事件之后,给用户一个友好的提示,让他主动刷新页面(如上图左下所示)。
后续规划
现在使用的静态页+前端渲染的策略,针对初次访问的用户在首屏时间上仍然有可改善的空间。后续我们会采用基于Vue的同构渲染+代码分割对于这一问题进行进一步优化。
对于离线化方案,AppCache未来会逐步被Service Worker所取代,无论从灵活性还是可扩展性而言,SW都更胜一筹。后续我们会逐步过渡到基于SW的方案,实现一个更加透明的网络层代理,能同时处理静态资源和动态请求。
总结
Web平台正在以飞快地速度向前发展,比如WebGL、WebVR、HTTP/2、Service Worker、Web Assembly、WebRTC这些激动人心的功能逐渐在各大浏览器中落地,前端开发人员能够写出更快更酷炫的用户界面,用户能够得到更优质的Web体验。
在赫尔墨斯项目中,我们实施了前后端分离、模块化和组件化改造、流程自动化、接入了监控和报表系统,极大的提高了我们的开发效率和项目代码的可维护、可复用性,同时通过自动化的资源优化,确保了有效的优化策略被以极低的成本在多个项目中复用。
作者介绍
毓杰,美团点评前端技术专家,全栈开发工程师。2016年加入美团点评,负责境外度假前端研发组的工作。崇尚自由、开放、互通的技术平台,追求极致的用户体验和开发效率。
如发现文章有错误、对内容有疑问,都可以关注美团技术团队微信公众号(meituantech),在后台给我们留言。
分享一线技术实践,沉淀成长学习经验