前端框架工程之路人类发展的动力来源于一个“懒”字,就像现在的大前端就是“懒”和聪明的史前群体“剪图”当我进入软件工程的施工现场时,我怀着少代码、少沟通、少错误、少维护的梦想向前冲。从框架的革命到三大框架,从构建工具的争夺到Webpack的称霸,从Javascript跟随ES5的7年统治到自我进化到ES6。前端的发展和成功,离不开一个“技术”,工程。模块的演变史前无框架我们面临的问题:1.全局变量污染:每个文件中的变量都挂载在window对象上,污染了全局变量。2、变量重名:如果不同文件中的变量重名,后面的会覆盖前面的,造成程序错误。3、文件依赖顺序:多个文件之间存在依赖关系,需要保证一定的加载顺序。问题很严重。于是老王想出了用自执行函数来解决问题的方法varfoo=(function(cNum){varaStr='aa';varaNum=cNum+1;return{aStr:aStr,aNum:aNum};})(cNum);最开始的前端模块诞生了,请问这个模块有什么问题吗?有!虽然模块内部的变量对全局是不可见的,但是暴露出来的foo是一个全局变量,像这样的模块多了就会有很多全局变量。老李在老王的方法的基础上加了一个namespace来解决问题:app.util.modA=xxx;app.common.modA=xxx;app.tools.modA.format=xxx;除了写法难看之外,这样的模块绑定力极低,很容易被不遵守的开发者破坏。它要求开发人员具有一定的划分不同模块的能力。更大的问题是需要手动解决模块加载、初始化等管理问题。2009年诞生于框架王者时代的前端框架Angularjs的模块机制angular.module('ConfigModule').service("TextConfig",function(){this.headerText={};});angular.module('HeaderModule',['ConfigModule']).controller('HeaderCtr',['$scope','TextConfig',function($scope,textConfig){$scope.headerText=textConfig.headerText;}]);Angularjs模块机制相比老王和老李的解决方案,增强了模块的约束,除了帮助开发者划分模块外,最重要的是解决了模块运行时管理的问题(模块初始化顺序的问题和依赖模块的自动初始化问题(在依赖多个模块的情况下,模块只加载一次的问题,统一输入输出api的问题)。看起来是一个完美的解决方案,但仍然存在问题。构建工具辅助Angularjs的模块机制只解决了运行时管理的问题,并没有解决模块加载管理的问题。这使得用户不得不在页面中链接引用模块文件。所以那个时候出现了一些构建工具和相应的插件来帮助我们,比如Gulp,Grunt,还有插件Browserify。其实Angularjs的模块机制只是在一定程度上解决了运行时管理的问题。了解过的同学应该知道,在Angularjs中做模块的异步懒加载是非常困难的。在Angular2及更高版本中添加了对动态加载模块的支持。其他的框架,比如Vue组件(我们暂时把Vue组件当成模块,后面会区分)也增加了相应的支持,得益于框架组件或模块的factor机制的支持和Webpack代码拆分功能支持。constfoo=resolve=>{require.ensure(['./Foo.vue'],()=>{resolve(require('./Foo.vue'))})}constrouter=newVueRouter({路由:[{path:'/foo',component:Foo}]})一直在逼近,但从未实现组件化。在之前讲模块化开发的时候,我们讲了Vue组件。组件是一种模块,但它们超越了模块。模块是对逻辑单元的封装,使得开发和维护成本更低。那么组件就是更高一层的抽象,是对一个业务单元和一个可以独立运行的软件单元的封装。组件需要解决的问题:隔离、去污(模块解决的隔离仅限于js变量的隔离,组件还需要解决css的隔离)、状态管理问题、与其他组件的通信问题、生命周期问题,一个好的Component设计也需要遵循软件设计的一些原则。不得不改源码的Jquery组件我们先来看看Jquery组件长什么样子?之前的代码一般都是将自执行函数作为一个类来使用。这里为了方便理解,我就用TS来表示。导出类组件{选择器:元素;选项:任何静态默认选项={'color':'red','fontSize':'16px','textDecoration':'none'}constructor(selector,opt){this.selector=selector;this.options=$.extend({},this.defaultOption,opt)}highlight(){returnthis.selector.css({'color':this.options.color,'fontSize':this.options.fontSize,'textDecoration':this.options.textDecoration});}}组件用户获取组件并初始化它。根据组件上层的一些交互,调用组件方法改变组件内部。我们可以看到组件的上层依赖于这个组件,而依赖于highlight()的具体实现。根据OCP原则,对扩展开放,对修改关闭。当我们的需求发生变化时,比如我们的高亮需要改变背景色,我们只能修改组件的内部逻辑。显然这样的组件不是一个好的设计。数据驱动让组件高可用,进入前端框架时代。Angular使用数据驱动来改变视图的状态。这是一个很大的进步。数据驱动解耦了组件外层对组件的依赖,将真正的依赖抛给外层传递给组件的数据(有点类似依赖倒置),组件负责根据数据的变化。(ReactVirtualDOM-drivenview其实也是一种data-driven,只不过一种是找到数据最小粒度的变化,直接改变对应的view,另一种是生成VirtualDOM,找到最小粒度的VirtualDOM变化而变化对应的view.本质上是一个DataDrivenView)。数据驱动视图解耦组件和组件依赖性。但同时也引入了一个问题,状态混乱的问题。写过Angularjs的同学应该知道状态混乱的痛苦。当我们在Angularjs中有多个组件依赖同一个数据时,当一个组件树中的某个组件改变了数据时,整个组件树中使用该数据的组件就会产生共振。但实际情况是树中的某些组件不需要随着某个数据的变化而变化。状态管理允许个人拒绝骚扰React,Angular使用Immutablejs加强单向数据流。这样确实降低了复杂度,但是这样一来,对于子组件想要通过状态变化来驱动父组件和兄弟组件的变化的情况,只能通过事件通知的形式进行注册。首先,这种形式会违反隔离,耦合度高。组件内部必须知道外部想知道我会有什么变化,预留订阅钩子。其次,对于一棵跨越N层的组件树,通知订阅每一层的子组件事件,使得极值点是从叶子节点到根节点,是非常不合理的。因此,一些派生的FluxRedux库的状态集中托管,允许一个组件的数据驱动视图的变化来自于任何一个组件树节点,而不是使变化成为一个复杂的网络拓扑,而是变成一个星形拓扑。这个开发过程其实就是理解耦合,让组件更加独立。就好像一个人的每一个日常活动都被推出来让全人类都知道。由于日常生活太辛苦,社会负面情绪暴涨,影响太大。(不存在单向数据流的情况)。于是改成了推送前,大家先订阅建立关系,建立关系后再推送。大家都觉得这套太麻烦了。我需要个人知道其他人关心我的哪些日常活动。如果我的家人想知道我吃什么,我会创建一个发布方法,领导想知道工作情况,我设置了写周报的方式,于是我不断改变自己以适应社会(这是一种耦合的,单向的数据流方式)。当个体失去了人性的时候,他终于想到了一个更好的办法,去做自己该做的事情,留下这种拉扯社会的内裤。我会收藏自己在办公室认真工作的照片,系统会让领导看到。让其他同事把我当个人看,不在乎。我在吃大餐的时候,你收集照片,系统会给哪些联系人,这是由我此刻所处环境的社会关系决定的。系统是状态管理者,社会是组件生存的环境。组件进化组件的进化从未停止过。比如css隔离问题,从依赖项目wiki中的css命名约定,改为cssModules自动解决css隔离问题。从Angularjs混乱的网格状态管理到React,Angular使用Immutablejs加强了单向数据流,以及一些派生的FluxRedux库的状态集中托管。从Vue的简单生命周期到2.0,加入keep-alive、activated、deactivated,增强生命周期。组件化的发展正在蓬勃发展,但为什么我仍然认为组件化还没有实现呢?框架的出现让组件具备了它们应有的特性,让开发者不需要重新发明轮子来解决这些问题。但是它也引入了一个新的问题,组件的独立性。如前所述,组件应该是独立运行的软件单元。实际上,组件只是在一定框架下独立运行的软件单元。而工程化也是一种趋势,去除底层服务。我们可以看看近几年的Docker技术和云服务的Serverless概念。他们都强调不需要关注底层的执行环境。想象一下,如果有一天我们开发一个页面,不管任何技术架构和Framework,只需要一个一个引入自定义元素,使用DOMAttribute作为API(或者取各个团队发布的一个在线运行的组件地址),组装这一页。这是近年来微前端技术的一个研究课题。目前的微前端技术也取得了不错的进展。使用CustomElement的实现,解决了一些基本问题,比如前面提到的:隔离、状态管理、通信问题、生命周期问题、不依赖前端架构体系的问题。但是,作为一个可以在服务器端独立部署和使用的组件,还有很多问题需要解决,但这就是组件化的发展方向。这是一个很好的微前端实现方案https://micro-frontends.org/工程乌托邦-标准化的规范是项目的建设蓝图,保证产出的产品健壮且易于维护。如果我们在没有说明的情况下更改一行代码,则可能会发生这种情况。有一个规范文档不是很好吗?在冲线的高压、高疲劳状态下,可能会出现这样的同事。之前我们提到过一些古老的“规范”,比如Jquery时代的模块定义规则,CSS规范的规范,还有上面没有提到的项目结构划分和代码编写规范。你有没有注意到,它们都已经消失在历史舞台上了,或者说你不必为规范的实现花费精力。为什么?当被控制的人和一个“代码”的执行者都是自己时,那么代码就成了一句空话。所以我们需要一个公正的执法者——一台机器(自动化)。最终会让项目wiki消失的自动代码规范,我们有jslint帮我们验证,还有一个编辑器插件帮我们自动按照规范格式化。对于项目结构,我们有对应的CLI来帮我们生成。对于模块的定义,我们有帮助分区解决运行时问题的框架,帮助解决加载问题的Webpack等等。自动化并没有真正让规范消失,而是让规范更具强制性,更容易执行,达到“无契约”自控的效果。我认为没有必要在wiki中规定开发人员应如何编写代码的文章。俗话说:“能行则无BB,能自动化则无文档”。至于项目wiki中允许开发者与其他人协作编写代码的文档,我认为没有必要。例如:我们如何与后端连接?我们可以使用YAPI等工具,让接口定义和数据结构一目了然,保持实时稳定性。对于前端与前端之间如何调用模块或组件,我们可以使用Typescript来让模块组件接口更加清晰,以及强类型带来的稳定性。让我举一个强类型带来的其他好处的例子:这是一个非常低级的错误}},方法:{toggle(){this.isFullScreen=true;}}}好像是大小写的问题,其实完全可以避免的。如果组件是强类型,IDE(支持ts)会推断出this的类型,对该字段是否声明过给出验证提示。这就是写作的好处。强类型也给我们带来了很多好处和便利,比如能够快速理解一个模块提供的API。当多个模块引用同一个数据时,对数据结构进行一定时间的调整后,可以马上知道哪些过时的代码需要随着这次改动一起调整等等。另外,TS还可以在自动化方面起到辅助作用文档。来看看Angular的文档自动生成工具有多牛~https://compodoc.github.io/co...demo:https://compodoc.github.io/co...未完待续...当我们站在巨人的肩膀上,从来没有觉得向前迈出一步是那么容易……希望未来的前端更容易一些。
