本文转自本人的知乎文章,做了些许简化修改 原文链接
2016年弹指一挥间,转眼间已到了自己学习web的第三个年头。在过去的这一年,前端领域发生了翻天覆地的变化——
- Angular1.x慢慢退出舞台,Angular2、React、Vue等新兴的MVVM框架已接近成熟。经过一年的迭代,API与底层设计思路上有趋同之势,但也慢慢显现出在各自领域中的优势。
- 以Redux和Vuex为代表的类Flux设计架构,经过一年的探索和实践,其在开发效率和维护追踪上的优势已被验证,取代MVC成为了中大型web应用中最流行的框架模式。
- Webpack、Rollup等包管理工具已是工程必备,ES6和Babel普及度已经很高了,前端开发完成从页面到组件化模块化、从解释型到构建型的工程性转变。
写这篇文章的初衷,一是由于这一年中也大大小小做过好几个项目,对于前端工程有一些浅薄的理解和经验,想要成文记录下来;二是结合已有的知识,畅想一下未来web发展的趋势。
关于Flux的经验思考
何为Flux不再过多赘述,不懂的读者推荐这篇文章图解Flux。
说白了,Flux就是一种新的单向数据流的“视图绑定式”解决方案。这里我个人加了“视图绑定式”这个定义,是因为只有在View-Model绑定的基础上,Flux才能充分发挥它的作用。
上图是Flux的架构图。简单地说,Flux将视图变成了状态机,所有对视图的操作都转变为对Store的状态操作。状态的更新会通过各种框架的映射机制反应到视图上。而在具体的实现中(Redux,Vuex),Store往往被设置成全局的单一状态树。这样一来,各组件、各模块之间将不存在任何数据交互关系,完全解耦。所有的模块或组件只做三件事:接收用户在视图层的指令,更改全局Store树,从Store中获取各组件的状态。
由于没有基于Redux做过工程方面的实践,所以这里重点说一下Vuex,不知道Vuex的同学可以戳这里Vuex。(这里要特别安利Vue.js这款非常棒的渐进式框架,本身很轻,适合小型应用的开发,同时又有构建大型应用的解决方案)
个人认为,利用Vuex,与传统的MVVM架构相比,对于构建中大型项目有如下优势——
- 很好地解决了父子组件数据级联与组件间的通讯问题。在传统的MVVM模式下,我们通常采用事件机制进行跨组件的数据传输,这种做法的一大坏处就在于组件之间产生耦合。在中大型规模的项目中,常常有上百甚至上千的组件(像这学期的学雷锋项目教学辅助应用,算是中小型规模,就已经有将近50个组件),组件间的交互将变得极其复杂和难以追踪,导致开发和维护成本非常高。而在Flux,确认地说是Vuex中,组件的所有动作都是发送事件,调用全局的action(action本质上是订阅的回调函数)。同时,组件的状态也是由全局的state树派生出来,并与其绑定。如此,所有组件对视图的更改,都转变为对全局状态的更改,从而避免了组件间的通讯。
- 方便追踪应用数据流。前面我们提到,级联的跨组件通讯带来了很强的耦合性,不仅如此,还使得数据流难以追踪。在使用MVC架构的中大型规模web项目中,前端往往一次性要存储和展示几十甚至上百种数据,牵扯到好几种Model,并且这些Model之间一定是“纠缠不清”的。这时候一旦出现Bug,很难去进行Bug定位。而Vuex中数据流是单向的,事情就简单很多——首先,定位出现Bug的相关视图和操作;然后,根据视图分析出相关State->Mutations->Actions(过程和数据流方向是相反的);最后,通过Actions和视图操作找到出问题的组件。(如果你严格按照模块化或组件化的模式来组织代码,那Debug效率又会提升很多。)
- 方便预测和回溯应用状态。前面也提到,Flux架构下的应用实际是一个状态机,而整个数据流是单向的,因此我们很容易根据一个Action便预测出应用下一阶段的状态。同时,在做“撤回”、“重做”等类似回溯的功能时,也可以很轻易地通过保存和提取应用在某一阶段的状态来实现。另外,Vuex中的Mutations专门用于更改State,官方建议是同步操作放在Mutations(参考这篇答案),这样的好处是每当调用Mutations便可同步获得一个新的应用状态,方便做snapshot。
但任何框架或架构的引入,都必须基于对于项目类型和项目规模的考量之上。Flux尤擅于数据类型繁多,交互密集的应用,但对于数据类型较少、交互不多的应用(例如H5,展示型网站等),就不适用了。
另外,尽管是在中大型项目中,严格遵循Flux有时也会把一些问题复杂化,例如下面的.vue模板代码:
<template>
<div>
<v-loading v-if="isLoading"></v-loading> //一个loading动画
</div>
</template>
<script>
import {mapState} from "vuex";
export default {
data(){
return{
isLoading:false //局部state的形式
}
},
computed:mapState(state){ //引入全局state的形式
isLoading:state=>state.isLoading
},
methods:{
someFunction(){
isLoading=true; //第一种方式
//this.$store.dispatch('isloading',true) //第二种方式
...
}
}
}
</script>
上段代码中使用了两种引入state的模式,一种是组件自身的局部state,由data进行初始化;另一种是严格遵守Flux,从全局Store中提取的state。如果我们使用第二种方式更改一个动画loading,那我们需要专门像下面的代码一样编写actions,mutations:
export const loading=({commit},signal)=>{
commit('loading',signal);
}
const state={
isLoading:false; //loading的全局state状态
}
const mutations={
loading(state,signal){
state.isLoading=signal; //更改state
}
}
当loading比较多的时候,会显得很麻烦。而如果采用局部state的方式,我们只需要在methods中更改状态即可。但问题在于,actions中常常有异步事件(如AJAX),需要在异步完成后调整loading的状态,根据单向数据流的原则,看上去只能通过修改全局的state来达到目的。
ES6提供了一个非常好的工具——Promise。具体请参照Vuex文档。如此我们便可以做到类似如下的调用:
...
},
methods:{
someFunction(){
this.$store.dispatch('loading',true).then(
()=>{ //成功回调
isLoading=true;
},
()=>{} //失败回调
)
}
}}
如此一来便能在Vuex的架构下充分利用局部State管理视图,不仅可以提高开发效率,也有利于保持全局State的简洁。
框架与工具的思考
尤大在文章"Vue2.0 渐进式前端解决方案"里对工具框架引入的考量作了非常恰当的比喻。
工具复杂度是为了处理内在复杂度所做的投资。为什么叫投资?那是因为如果投的太少,就起不到规模的效应,不会有合理的回报。这就像创业公司拿风投,投多少是很重要的问题。如果要解决的问题本身是非常复杂的,那么你用一个过于简陋的工具应付它,就会遇到工具太弱而使得生产力受影响的问题。
这里的复杂度,我认为是针对特定团队而言的。比如你的团队对React全家桶有丰富的实践经验,那么在为相同的项目选型时,React的复杂度于你的团队和于其他团队是不同的。但这么看其实是很虚的,原因是很少有人能准确地评估应用内在复杂度和工具复杂度。
经过观察,通常情况是高估内在复杂度,低估工具复杂度。所以现阶段我总结了一条不是很正确,但起码不会错的方法论:以最佳实践为基础,再增量考察和评估额外的工具引入成本。
举两个栗子:
- 对于非常轻的页面,如H5、活动页面,最佳实践一般是Jquery或zepto+UI库。但现在很多轻页面开始有一些数据交互的需求,需要考虑引入一些MVVM框架,这时候再增量评估引入React、Vue这些View层框架的复杂度及应用内在复杂度,而不是把Jquery或Zepto和它们放到同一地位来进行比较考量。
- 开发单页面应用,一般来说是以数据交互为主的需求。Vue全家桶的最佳实践是Webpack+Vue(单文件组件解决方案)+Vue-router。React全家桶的最佳实践是Webpack+React+React-router。以此为基准,综合项目预期成本,再考虑加入UI库、Babel、Redux(Vuex)带来的额外复杂度和消弭应用内在复杂度之间的平衡关系。
这样的好处是基本跳过最佳实践的复杂度评估,直接对风险较高、把握较低的复杂度部分进行评估,节省了决策成本,原因有二:
- 一般来说,最佳实践是在各种工具的组合下,复杂度最低的方案。
- 最佳实践本身包含了前人的决策经验和教训,没有必要花过多时间来做重复地评估决策。
另外,以上所说的“最佳实践”,有两点需要注意的地方:
- 重点不是在best,而是在experience。不要陷入了找寻最优解的泥潭。
- “最佳”不是绝对概念,而是相对概念。一定要针对个人(团队)的情况来考虑何为“最佳”。
前端工程化的思考总结
下图展示了整个前端工程可能涉及到的各个环节,前端技术体系大局观一文覆盖式地简要介绍了各层系统的职能。
这里我结合自身的知识和实践经验,列出一些实现成本并不大并且性价比很高的工程化方案:
** 网络性能优化**:现在的web应用,大小动则上MB,加上一些静态资源,在目前我大天朝的网络环境下,在不做优化的基础下达到类似“首屏3s”或“动态加载xxs”的非功能性需求,需要付出很昂贵的硬件成本。目前网络优化主要有两个思路:CDN加速和缓存策略。对于前者,大公司往往采取自建CDN节点的方式,而小公司可以使用类似七牛云这样的云储存来进行静态资源的加速。而缓存策略则有许多的解决方案,有时还会牵扯到部署、迭代的问题。之后我会专门写一篇文章,介绍前端如何利用Webpack的Code Spliting和服务器的缓存机制来解决应用缓存与部署问题。(推荐这篇回答大公司里怎样开发和部署前端代码?)流量统计:这一块市面上已有非常成熟的方案,如CNZZ、百度站长统计等。当然也可以自己做一套,难度并不大,基本原理是在页面中拼装出标签,再远程加载javascript。
自动化部署系统:这个系统可以包含仓库代码管理、一键部署、一键Pull/Push,具体可以针对业务定制。它可以简化web应用从构建到上线的流程,提高效率,在快速迭代的web项目中非常有用。一个实现思路是利用后端服务调用预先编好的Shell脚本来完成命令行集操作,实现自动删除旧代码,clone新代码,安装环境等行为。
代码质量监控:可选的工具有JSLint和JSHint。前者对代码要求极为严格,后者对代码要求可以很宽松。Webstorm和PHPStorm已内置两种工具,个人推荐使用JSHint。
除此之外,还有诸如日志系统、监控预警、自动化测试、API测试等细节领域,我还在探索阶段,这里就不过多赘述了~
Web前端的未来
我认为一种技术的未来前景可以从这三个维度来分析:全新体验、成本下降趋势、市场潜力。
- 全新体验:即带给用户一个前所未有的体验。从需求角度来说就是帮助用户塑造全新的预期,并做持续性的满足。
- 成本下降趋势:这里的成本,不仅包括开发人员的人时成本、维护成本、工具链成本等,还包括可能遗留的技术债务,未来“还清债务”的成本。
- 市场潜力:评估某项技术的市场潜力,可以看以这项技术为核心的行业在未来的市场容量。
从这三个维度出发,我们不难发现web未来发展的三个可能趋势:
- WebVR:WebVR是一块全新的领域,能够在低成本设备上塑造VR的体验,同时也是高端平台快速浏览、分发内容的一种形式。早在2016年上半年,Mozilla就携手一众厂商完成了WebVR标准的制定,并在年底发布了Mobile、Oculus Rfit、HTC vive三种平台的解决方案Mozilla VR。同时,优秀了很久的3D引擎库three.js,也将成为WebVR的重要技术基石。
- 大前端:@向昶宇向总在我浙INA的一次内部分享中也提到了对“大前端”的看法,我非常赞同。 Web,准确地说是Javascript,将会成为web应用、移动端Native应用、桌面应用、智能设备应用、智能硬件(没错,JS已经能用来写硬件了——Ruff)的一种更廉价的替代技术。在未来,也许你的公司不再需要同时组件Web、IOS、Android三支开发队伍,取而代之的是一支大前端团队。
- WebRTC:目前各大网站的视音频通讯和游戏都依赖于Flash,而Flash在未来两年应该会被全面淘汰。WebRTC被浏览器原生支持,实现了浏览器之间P2P的实时数据传输,同时是目前对于浏览器来说唯一一个可以不用Flash与外界使用UDP进行数据交换的技术,在流量节省和拥塞控制上具有很大的优势。随着各大浏览器实现对WebRTC的支持(目前只有Chrome、Firefox和Opera),在未来它一定会成为网络音视频和在线游戏实时数据传输的主要解决方案。
延伸阅读
2016-我的前端之路:工具化与工程化
每个架构师都应该研究下康威定律
[译]WebVR 中的“共同在场”
未经允许,禁止转载