首页 > 网站 > WEB开发 > 正文

2015前端组件化框架之路(转)

2024-04-27 14:27:25
字体:
来源:转载
供稿:网友
2015前端组件化框架之路(转)

https://github.com/xufei/blog/issues/19

1. 为什么组件化这么难做

Web应用的组件化是一个很复杂的话题。

在大型软件中,组件化是一种共识,它一方面提高了开发效率,另一方面降低了维护成本。但是在Web前端这个领域,并没有很通用的组件模式,因为缺少一个大家都能认同的实现方式,所以很多框架/库都实现了自己的组件化方式。

前端圈最热衷于造轮子了,没有哪个别的领域能出现这么混乱而欣欣向荣的景象。这一方面说明前端领域的创造力很旺盛,另一方面却说明了基础设施是不完善的。

我曾经有过这么一个类比,说明某种编程技术及其生态发展的几个阶段:

  • 最初的时候人们忙着补全各种API,代表着他们拥有的东西还很匮乏,需要在语言跟基础设施上继续完善
  • 然后就开始各种模式,标志他们做的东西逐渐变大变复杂,需要更好的组织了
  • 然后就是各类分层MVC,MVP,MVVM之类,可视化开发,自动化测试,团队协同系统等等,说明重视生产效率了,也就是所谓工程化

那么,对比这三个阶段,看看关注这三种东西的人数,觉得Web发展到哪一步了?

细节来说,大概是模块化和组件化标准即将大规模落地(好坏先不论),各类API也大致齐备了,终于看到起飞的希望了,各种框架几年内会有非常强力的洗牌,如果不考虑老旧浏览器的拖累,这个洗牌过程将大大加速,然后才能释放Web前端的产能。

但是我们必须注意到,现在这些即将普及的标准,很多都会给之前的工作带来改变。用工业体系的发展史来对比,前端领域目前正处于蒸汽机发明之前,早期机械(比如《木兰辞》里面的机杼,主要是动力与材料比较原始)已经普及的这么一个阶段。

所以,从这个角度看,很多框架/库是会消亡的(专门做模块化的AMD和CMD相关库,专注于标准化DOM选择器铺垫的某些库),一些则必须进行革新,还有一些受的影响会比较小(数据可视化等相关方向),可以有机会沿着自己的方向继续演进。

2. 标准的变革

对于这类东西来说,能获得广泛群众基础的关键在于:对将来的标准有怎样的迎合程度。对前端编程方式可能造成重大影响的标准有这些:

  • module
  • Web Components
  • class
  • observe
  • PRomise

module的问题很好理解,javaScript第一次有了语言上的模块机制,而Web Components则是约定了基于泛HTML体系构建组件库的方式,class增强了编程体验,observe提供了数据和展现分离的一种优秀方式,promise则是目前前端最流行的异步编程方式。

这里面只有两个东西是绕不过去的,一是module,一是Web Components。前者是模块化基础,后者是组件化的基础。

module的标准化,主要影响的是一些AMD/CMD的加载和相关管理系统,从这个角度来看,正如seajs团队的@afc163 所说,不管是AMD还是CMD,都过时了。

模块化相对来说,迁移还比较容易,基本只是纯逻辑的包装,跟AMD或者CMD相比,包装形式有所变化,但组件化就是个比较棘手的问题了。

Web Components提供了一种组件化的推荐方式,具体来说,就是:

  • 通过shadow DOM封装组件的内部结构
  • 通过Custom Element对外提供组件的标签
  • 通过Template Element定义组件的HTML模板
  • 通过HTML imports控制组件的依赖加载

这几种东西,会对现有的各种前端框架/库产生很巨大的影响:

  • 由于shadow DOM的出现,组件的内部实现隐藏性更好了,每个组件更加独立,但是这使得CSS变得很破碎,LESS和SASS这样的样式框架面临重大挑战。
  • 因为组件的隔离,每个组件内部的DOM复杂度降低了,所以选择器大多数情况下可以限制在组件内部了,常规选择器的复杂度降低,这会导致人们对jQuery的依赖下降。
  • 又因为组件的隔离性加强,致力于建立前端组件化开发方式的各种框架/库(除Polymer外),在自己的组件实现方式与标准Web Components的结合,组件之间数据模型的同步等问题上,都遇到了不同寻常的挑战。
  • HTML imports和新的组件封装方式的使用,会导致之前常用的以Javascript为主体的各类组件定义方式处境尴尬,它们的依赖、加载,都面临了新的挑战,而由于全局作用域的弱化,请求的合并变得困难得多。

3. 当下最时髦的前端组件化框架/库

在2015年初这个时间点看,前端领域有三个框架/库引领时尚,那就是Angular,Polymer,React(排名按照首字母),在知乎的这篇2014 年末有哪些比较火的 Web 开发技术?里,我大致回答过一些点,其他几位朋友的答案也很值得看。关于这三者的细节分析,侯振宇的这篇讲得很好:2015前端框架何去何从

我们可以看到,Polymer这个东西在这方面是有先天优势的,因为它的核心理念就是基于Web Components的,也就是说,它基本没有考虑如何解决当前的问题,直接以未来为发展方向了。

React的编程模式其实不必特别考虑Web标准,它的迁移成本并不算高,甚至由于其实现机制,屏蔽了UI层实现方式,所以大家能看到在native上的使用,canvas上的使用,这都是与基于DOM的编程方式大为不同的,所以对它来说,处理Web Components的兼容问题要在封装标签的时候解决,反正之前也是要封装。

Angular 1.x的版本,可以说是跟同时代的多数框架/库一样,对未来标准的兼容基本没有考虑,但是重新规划之后的2.0版本对此有了很多权衡,变成了激进变更,突然就变成一个未来的东西了。

这三个东西各有千秋,在可以预见的几年内将会鼎足三分,也许还会有新的框架出现,能不能比这几个流行就难说了。

此外,原Angular 2.0的成员Rob Eisenberg创建了自己的新一代框架aurelia,该框架将成为Angular 2.0强有力的竞争者。

4. 前端组件的复用性

看过了已有的一些东西之后,我们可以大致来讨论一下前端组件化的一些理念。假设我们有了某种底层的组件机制,先不管它是浏览器原生的,或者是某种框架/库实现的约定,现在打算用它来做一个大型的Web应用,应该怎么做呢?

所谓组件化,核心意义莫过于提取真正有复用价值的东西。那怎样的东西有复用价值呢?

  • 控件
  • 基础逻辑功能
  • 公共样式
  • 稳定的业务逻辑

对于控件的可复用性,基本上是没有争议的,因为这是实实在在的通用功能,并且比较独立。

基础逻辑功能主要指的是一些与界面无关的东西,比如underscore这样的辅助库,或者一些校验等等纯逻辑功能。

公共样式的复用性也是比较容易认可的,因此也会有bootstrap,foundation,semantic这些东西的流行,不过它们也不是纯粹的样式库了,也带有一些小的逻辑封装。

最后一块,也就是业务逻辑。这一块的复用是存在很多争议的,一方面是,很多人不认同业务逻辑也需要组件化,另一方面,这块东西究竟怎样去组件化,也很需要思考。

除了上面列出的这些之外,还有大量的业务界面,这块东西很显然复用价值很低,基本不存在复用性,但仍然有很多方案中把它们“组件化”了,使得它们成为了“不具有复用性的组件”。为什么会出现这种情况呢?

组件化的本质目的并不一定是要为了可复用,而是提升可维护性。这一点正如面向对象语言,Java要比C++纯粹,因为它不允许例外情况的出现,连main函数都必须写到某个类里,所以Java是纯面向对象语言,而C++不是。

在我们这种情况下,也可以把组件化分为:全组件化,局部组件化。怎么理解这两个东西的区别呢,有人问过js框架和库的区别是什么,一般来说,有某种较强约定的东西,称为框架,而约定比较松散的,称为库。框架很多都是有全组件化理念的,比如说,很多年前就出现的ExtJS,它是全组件化框架,而jQuery和它的插件体系,则是局部组件化。所以用ExtJS写东西,不管写什么都是差不多一样的写法,而用jQuery的时候,大部分地方是原始HTML,哪里需要有些不一样的东西,就只在那个地方调用插件做一下特殊化。

对于一个有一定规模的Web应用来说,把所有东西都“组件化”,在管理上会有较大的便利性。我举个例子,同样是编写代码,短代码明显比长代码的可读性更高,所以很多语言里会建议“一个方法一般不要超过多少行,一个类最好不要超过多少行”之类。在Web前端这个体系里,JavaScript这块是做得相对较好的,现在入门水平的人,也已经很少会有把一堆js都写在一起的了。CSS这块,最近在SASS,LESS等框架的引领下,也逐步往模块化方面发展,否则直接编写bootstrap那种css,会非常痛苦。

这个时候我们再看HTML的部分,如果不考虑模板等技术的使用,某些界面光布局代码写起来就非常多了,像一些表单,都需要一层套一层,很多简单的表单元素都需要套个三层左右,更不必说一些有复杂布局的东西了。尤其是整个系统单页化之后,界面的header,footer,各种nav或者aside,很可能都有一定复杂性。如果这些东西的代码不作切分,那么主界面的HTML一定比较难看。

我们先不管用什么方式切分了,比如用某种模板,用类似Angular中的include,或者Polymer,React中的标签,或者直接使用原生Web Components,总之是把一块一块都拆开了,然后包含进来。从这个角度看,这些拆出去的东西都像组件,但如果从复用性的角度看,很可能多数东西,每一块都只有一个地方用,压根没有复用度。这个拆出去,纯粹是为了使得整个工程易于管理,易于维护。

这时候我们再来关注不同框架/库对UI层组件化的处理方式,发现有两个类型,模板和函数。

模板是一种很常见的东西,它用HTML字符串的方式表达界面的原始结构,然后通过代入数据的方式生成真正的界面,有的是生成目标HTML,有的还生成各种事件的自动绑定。前者是静态模板,后者是动态模板。

另外有一些框架/库偏爱用函数逻辑来生成界面,早期的ExtJS,现在的React(它内部还是可能使用模板,而且对外提供的是组件创建接口的进一步封装——jsx)等,这种实现技术的优势是不同平台上编程体验一致,甚至可以给每种平台封装相同的组件,调用方轻松写一份代码,在Web和不同Native平台上可用。但这种方式也有比较麻烦的地方,那就是界面调整比较繁琐。

本文前面部分引用侯振宇的那篇文章里,他提出这些问题:

如何能把组件变得更易重用? 具体一点:

  • 我在用某个组件时需要重新调整一下组件里面元素的顺序怎么办?
  • 我想要去掉组件里面某一个元素怎么办? 如何把组件变得更易扩展? 具体一点:
  • 业务方不断要求给组件加功能怎么办?

为此,还提出了“模板复写”方案,在这一点上我有不同意见。

我们来看看如何把一个业务界面切割成组件。

有这么一个简单场景:一个雇员列表界面包括两个部分,雇员表格和用于填写雇员信息的表单。在这个场景下,存在哪些组件?

对于这个问题,主要存在两种倾向,一种是仅仅把“控件”和比较有通用性的东西封装成组件,另外一种是整个应用都组件化。

对前一种方式来说,这里面只存在数据表格这么一个组件。对后一种方式来说,这里面有可能存在:数据表格,雇员表单,甚至还包括雇员列表界面这么一个更大的组件。

这两种方式,就是我们之前所说的“局部组件化”,“全组件化”。

我们前面提到,全组件化在管理上是存在优势的,它可以把不同层面的东西都搞成类似结构,比如刚才的这个业务场景,很可能最后写起来是这个样子:

<Employee-Panel>    <Employee-List></Employee-List>    <Employee-Form></Employee-Form></Employee-Panel>

对于UI层,最好的组件化方式是标签化,比如上面代码中就是三个标签表达了整个界面。但我个人坚决反对滥用标签,并不是把各种东西都尽量封装就一定好。

全标签化的问题主要有这些:

第一,语义化代价太大。只要用了标签,就一定需要给它合适的语义,也就是命名。但实际用的时候,很可能只是为了把一堆html简化一下而已,到底简化出来的那东西应当叫什么名字,光是起名也费不知多少脑细胞。比如你说雇员管理的表单,这个表单有heading吗,有footer吗,能折叠吗,等等,很难起一个让别人一看就知道的名字,要么就是特别长。这还算简单的,因为我们是全组件化,所以很可能会有组合了多种东西的一个较复杂的界面,你想来想去也没法给它起个名字,于是写了个:

<Panel-With-Department-Panel-On-The-Left-And-Employee-Panel-On-The-Right></Panel-With-Department-Panel-On-The-Left-And-Employee-Panel-On-The-Right>

这尼玛……可能我夸张了点,但很多时候项目规模够大,你不起这么复杂的名字,最后很可能没法跟功能类似的一个组件区分开,因为这些该死的组件都存在于同一个命名空间中。如果仅仅是当作一个界面片段来include,就不存在这种心理负担了。

比如Angular里面的这种:

<div ng-include="'aaa/bbb/ccc.html'"></div>

就不给它什么名字,直接include进来,用文件路径来区分。这个片段的作用可以用其目录结构描述,也就是通过物理名而非逻辑名来标识,目录层次充当了一个很好的命名空间。

现在的一些主流MVVM框架,比如knockout,angular,avalon,vue等等,都有一种“界面模板”,但这种模板并不仅仅是模板,而是可以视为一种配置文件。某一块界面模板描述了自身与数据模型的关系,当它被解析之后,按照其中的各种设置,与数据建立关联,并且反过来再更新自身所对应的视图。

不含业务逻辑的UI(或者是业务逻辑已分离的UI)基本不适合作为组件来看待,因为即使在逻辑不变的情况下,界面改版的可能性也太多了。比如即使是换了新的CSS实现方式,从float布局改成flex布局,都有可能把DOM结构少套几层div,因此,在使用模板的方案中,只能把界面层视为配置文件,不能看成组件,如果这么做,就会轻松很多。

部队行军的时候讲究“逢山开路,遇水搭桥”,这句话的重点在于只有到某些地形才开路搭桥,使用MVVM这类模式解决的业务场景,多数时候是一马平川,横着走都可以,不必硬要造路。所以从整个方案看的话,UI层实现应该是模板与控件并存,大部分地方是模板,少数地方是需要单独花时间搞的路和桥。

第二,配置过于复杂。有很多东西其实不太适合封装,不但封装的代价大,使用的代价也会很大。有时候会发现,调用代码的绝大部分都是在写各种配置。

就像刚才的雇员表单,既然你不从标签的命名上去区分,那一定会在组件上加配置。比如你原来想这样:

<EmployeeForm heading="雇员表单"></EmployeeForm>

然后在组件内部,判断有没有设置heading,如果没有就不显示,如果有,就显示。过了两天,产品问能不能把heading里面的某几个字加粗或者换色,然后码农开始允许这个heading属性传入html。没多久之后,你会惊奇地发现有人用你的组件,没跟你说,就在heading里面传入了折叠按钮的html,并且用选择器给折叠按钮加了事件,点一下之后还能折叠这个表单了……

然后你一想,这个不行,我得给他再加个配置,让他能很简单地控制折叠按钮的显示,但是现在这么写太不直观,于是采用对象结构的配置:

<EmployeeForm>    <Option collapsible="true">        <Heading>            <h4><strong>雇员</strong>表单</h4>        </Heading>    </Option></EmployeeForm>

然后又有一天,发现有很多面板都可以折叠,然后特意创建了一个可折叠面板组件,又创建了一种继承机制,其他普通业务面板从它继承,从此一发不可收拾。

我举这例子的意思是为了说明什么呢,我想说,在规模较大的项目中,企图用全标签化加配置的方式来描述所有的普通业务界面,是一定事倍功半的,并且这个规模越大就越坑,这也正是ExtJS这类对UI层封装过度的体系存在的最大问题。

这个问题讨论完了,我们来看看另外一个问题:如果UI组件有业务逻辑,应该如何处理。

比如说,性别选择的下拉框,它是一个非常通用化的功能,照理说是很适合被当做组件来提供的。但是究竟如何封装它,我们就有些犯难了。这个组件里除了界面,还有数据,这些数据应当内置在组件里吗?理论上从组件的封装性来说,是都应当在里面的,于是就这么造了一个组件:

<GenderSelect></GenderSelect>

这个组件非常美好,只需直接放在任意的界面中,就能显示带有性别数据的下拉框了。性别的数据很自然地是放在组件的实现内部,一个写死的数组中。这个太简单了,我们改一下,改成商品销售的国家下拉框。

表面上看,这个没什么区别,但我们有个要求,本公司商品销售的国家的信息是统一配置的,也就是说,这个数据来源于服务端。这时候,你是不是想把一个http请求封装到这组件里?

这样做也不是不可以,但存在至少两个问题:

  • 如果这类组件在同一个界面中出现多次,就可能存在请求的浪费,因为有一个组件实例就会产生一个请求。
  • 如果国家信息的配置界面与这个组件同时存在,当我们在配置界面中新增一个国家了,下拉框组件中的数据并不会实时刷新。

第一个问题只是资源的浪费,第二个就是数据的不一致了。曾经在很多系统中,大家都是手动刷新当前页面来解决这问题的,但到了这个时代,人们都是追求体验的,在一个全组件化的解决方案中,不应再出现此类问题。

如何解决这样的问题呢?那就是引入一层Store的概念,每个组件不直接去到服务端请求数据,而是到对应的前端数据缓存中去获取数据,让这个缓存自己去跟服务端保持同步。<

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表