xufei/blog

Web应用的组件化(二)——管控平台

xufei opened this issue · 5 comments

Web应用的组件化(二)

管控平台

在上一篇中我们提到了组件化的大致思路,这一篇主要讲述在这么做之后,我们需要哪些外围手段去管控整个开发过程。从各种角度看,面对较大规模前端开发团队,都有必要建立这么一个开发阶段的协作平台。

在这个平台上,我们要做哪些事情呢?
#1. HTML片段

我们为什么要管理HTML片段?因为有界面要用它们,当这些片段多了之后,需要有个地方来管理起来,可以检索、预览它们,还能看到大致描述。

这应该是整个环节中一个相对很简单的东西,照理说,有目录结构,然后剩下的就是单个的HTML片段文件了,这就可以解决存储和检索的问题了,但我们还要考虑更多。

已有的HTML片段,如何被使用呢?这肯定是一种类似include的方式,通过某种特殊标签(不管是前端还是后端的方式)把这些片段引用进来,这时候就有了第一个问题:

假设有界面A和界面B同时引用了片段C,在某个开发人员修改片段C内容的时候,他如何得知将会影响到界面A和B呢?一个比较勉强的方式是全项目查找,但这在很多情况下是不够的。

如果我们的HTML片段是作为独立的公共库存在的,它已经不能通过项目内查找去解决这一问题了,因为不管A还是B,只要他不处于片段C的项目空间,就无从追寻。

这时候很多人会问两个问题:

  1. 跨项目的界面片段重用,意义在哪里?

    如果我们的产品是针对一个小领域,它的复杂度根本不需要划分多个项目部分来协作完成。设想场景是面对很大的行业,各项目都是子产品,将来可能是其中若干个联合部署,这时候,保持其中的一致性是非常重要的。比如我们有个基本配置界面,在多个子产品中都要用,如果各自开发一个,其操作风格很可能就是不一致的,给人的印象就是不专业。所以会需要把常见的界面片段都归集起来,供业务方挑选使用。

  2. 修改C,只提供说明,但是不通知A和B,不实时更新他们的版本,然后自行决定怎样升级,如何?

    这会有一个问题,每次有小功能升级的时候,代码是最容易同步合并的,所以才会有“持续集成”这个概念,如果是一直伴随升级,总要比隔一个大阶段才升级好,升级成本应尽量分摊到平时,就像农妇养小猪,小猪每天长一点,每天都抱来抱去,不觉得吃力,即使长大了也还能抱得动。

现在问题就很明确了,一定要有一种方式来把这个依赖关系管理起来,很显然,已有的版本库是肯定管不了这些的,所以只能在外围做一些处理。

我们建立一个管理平台,除了管理实体文件的版本,还管它们之间的关系。具体这个关系如何收集整理,有两种方式:手动配置,代码分析。

手动配置是比较土的方式,开发人员每提交一个文件,就去这系统上手动配置它的依赖关系。代码分析的话,要在每次提交文件的时候解析文件的包含规则,找出确切的文件。这两者各有利弊,前者比较笨,但容易做,后者对代码格式的要求比较高,要考虑的情况较多。

我们的界面往往不是那么简单,HTML片段也可能有层次的,举例来说:

界面A里面包含了片段B,但是片段B自身又包含了片段C,所以这个依赖关系也是有层级的,需要在设计的时候一并考虑。
#2. JavaScript模块

JavaScript代码的管理,比HTML片段的状况好一些,因为业界很多这方面的解决方案。但它们还是没有解决当依赖项产生变更的时候反向通知的问题。

所以我们还是得像HTML片段一样,把它们的依赖关系都管理到平台里。于是,每个JavaScript模块都显式配置了自己所依赖的其他模块,通过这种单向关系,形成了一套完整的视图。

在JavaScript模块的代码实现中,我们是不提倡直接写依赖关系的。很多通用规范,比如AMD,往往建议我们这样写模块:

    define(['dep1', 'dep2'], function (dep1, dep2) {
        var moduleA = function () {};
        return moduleA;
    });

但我们的系统是面向行业的,比这种通用解决方案要苛刻一些。比如说,如果有一天重构代码,JavaScript模块们调整了目录或者名字,这么写的就痛苦了,他必须把所有影响到的都去调整一遍,这是要搜索替换的。况且,就像上面HTML模板的部分提到的,影响了处于其他项目中依赖它的代码,缺少合适的方式去通知他们修改。

所以我们期望的是,在每个编写的JavaScript模块中只存放具体实现,而把依赖关系放在我们的平台上管理,这样,即使当前模块作了改名之类的重构处理,处于外部项目中依赖它的那些代码也不必修改,下一次版本发布的生成过程会自动把这些事情干掉。

对应到上面的这段代码,我们需要开发人员做的只是其中的实现,也就是moduleA的那个部分,外面这些依赖的壳子,是会在发布阶段根据已配置的依赖关系自动生成的。

如果需要,JavaScript模块还可以细分,比如类似Angular里面那样,把factory,controller和directive分离出来,这会对后续有些处理提供方便。

现在我们有必要讨论一下模块的粒度了,我们这里提到的都是基本的粒度,每个JavaScript模块中存放的应该只有一个很具体东西的实现。那么,有个问题,在我们发布的时候,是不是就按照这个粒度发布出去呢?

很显然不行,如果这么做,很可能会出现复杂界面一次要用10多个HTTP请求才能加载完它所需要的所有JavaScript代码的情况,所以需要做一些合并。

那么,合并的策略是什么?在我们这个平台上,开发人员又是要怎样定义这个合并关系的呢?我们需要在模块之上定义一个更大粒度的组织方式,这个方式与模块的关系,就好比Java里面,jar文件与class的关系。如果开发人员不显式配置,也可以通过全局策略,比如按最下层目录来合并。

这个时候,在实际使用这些代码的时候,需要带两个配置信息过去,一个是要动态载入的JavaScript文件(合并之后的),二是每个JavaScript文件中包含的原始模块。
#3. 单元测试

如果JavaScript模块都已经被良好有序管理起来,就可以为它们考虑单元测试的事情了。单元测试对于提高基础单元的可靠度,是有非常重要意义的。

在我们这个平台里,可以把单元测试跟JavaScript模块关联起来,每个JavaScript模块可以挂一组单元测试代码,这些代码可以在线编写,在线运行。

单元测试的本质就是编写模拟代码来调用已有模块,考虑到我们的模块是JavaScript,所以很多思路都倾向于在浏览器端执行它们,对于单个模块的单元测试,这不是个问题。

如果要批量执行整个系统的单元测试,那就不一样了。把JavaScript代码先加载到浏览器中,然后再执行,很多时候并不需要这么复杂。我们完全可以在服务端把它们做了。

借助Node.js的能力,我们可以在服务端执行JavaScript代码,也就意味着能够把绝大多数JavaScript模块的单元测试在服务端就执行掉。当然,我们为此可能要多做不少事情,比如说,有些库需要移植一份node版的,常见的有AJAX调用等等。

注意了,能够在服务端做JavaScript单元测试是有先决条件的,代码的分层必须很良好,除了视图层,其他任何层面都不能操作DOM。所以我们这里主要测试的也正是除了视图层之外的所有JavaScript业务逻辑。至于视图层怎么办?这个真的很难解决,这世界上不是所有东西都能自动做的,只能先把可做的做了,以后再来考虑这些。
#4. 文档和示例管理

4.1. 文档

现在我们有HTML片段和JavaScript模块了,需要给它们多一些描述信息。简单描述显然是不够的,我们还要详细文档。

这种详细文档可以通过某种方式生成,也可以由开发人员手动编写。与传统的离线文档不同,在线的文档更实时,并且,每当一个开发人员变更了他的文档之后,不需要经过全量构建,访问者可以实时访问到他的最新版本。

熟悉GitHub的朋友们可能早已习惯这种方式,在项目库里面存在一些以md格式结尾的文本文件,使用markdown语法来编写一些说明文档。

毫无疑问,这类格式很适合在线协作,所以我们也会在平台上集成这么一种编写文档的方式,无论是针对HTML模板还是JavaScript模块,或者是其他什么类型,甚至还可以用来当博客,就像月影同学的gitpress平台,能直接从GitHub上拉取文本或者HTML文件形成博客。

文档除了以集成的形式浏览之外,应当也可以以单独链接的方式发出去,这时候用户就可以像看一个新闻网页一样去浏览。如果再进一步做下去,还可以做电子书的生成,提供打包的离线文档。

4.2. 示例

在编写代码文档的过程中,可能免不了要插入示例,示例有两种形态,一种是纯文本,类似gist这样,一种是可在线运行,类似jsfiddle和jsbin这样。

这两种都有各自的优点,所以可以都做,示例的存放可以与文档类似,也应当能通过一个链接独立运行。

4.3. 幻灯片

有时候我们看到一些在线的幻灯片,觉得效果很帅,比如reveal.js,我们的开发人员有时候作代码分析或者走查的时候也不免要写一些演示,如果能把这些东西也随项目管理起来,能在线查看,会是很不错的一件事。所以我们也可以考虑给它们加个存储界面,甚至做个简易的在线编写器。
#5. 项目与目录管理

说到现在,我们似乎还遗漏了一点什么。那就是以上提到的这些东西,以什么为组织单位来存储?

考虑到我们的这个平台是要管理一整个大产品的全部前端内容的,它里面应该分了很多项目,对应到子产品上,这么一来,很自然地,项目就成了第一级组织单位。项目之下,没有悬念地,只有目录了。

对于一个项目而言,它有哪些要做的事情呢?首先要能配置其实体存储位置。前面提到的这么多代码、文档之类,最终都是要实体存储的,怎么存?我们当然可以自己搞一套,在文件系统上做起来,但是还要考虑它们的版本管理,非常麻烦,所以不如直接对接某个版本库,调用它的接口去存取文件,这里配置的就是版本库的路径。

其次,要考虑从已有项目复制,类似GitHub里面的fork功能,不过内部处理机制可以略有不同,fork的项目默认未必要有实体文件,只有当产生了修改或者新增操作的时候才创建,剩下的还引用原来的就可以了。我们这里的项目复制功能是为项目化版本而考虑的,经常出现一个产品版本支持多个客户项目的情况,所以可能会用得着这个特性。

然后,也要考虑项目的依赖关系。依赖一个项目,意思是需要用到它里面的组件,所以实质是组件的依赖。提供项目依赖这个视图,只是为了未来变更的一些考虑。
#6. 评论管理

之前提到,我们整个平台的目的是为了提高大型前端团队的协作能力,协作是离不开交流的。上述的任何功能,都应当带有交流沟通的能力。

比如说,如果开发人员A使用了其他人写的一个代码组件a,对其中一些细节有疑问,他应当可以对它进行评论。在他评论的时候,任何参与维护过这个组件的人员都能收到一个提醒,这时候他可以选择过来看看,回复这个疑问。同理,在文档、示例下也可以如此操作。

在互联网上有这类产品,用于在任意URL下挂接评论交流系统,比较有名的就是Disqus,我们可以看到很多网站下面挂着它,用于做交流评论,这样用户可以用一个账号在多个网站之间交流。国内也有同类的,比如多说,能够用微博、QQ等账号登录进行交流。

从我们这个平台本身看,如果是部署在企业内部作流程提升,引入外部评论系统的可能性就比较小了。因为在企业内部用,一定是希望这个员工的账号信息跟工号挂钩,也能够跟版本服务器账号等模块作集成,权限也便于控制。

从另外一个角度讲,某个人员登录这个系统的时候,他可能收到很多消息,来自不同的代码或文档位置,挨个点过去回复也有些麻烦,我们应当给他提供一个全局视图,让他能在一个统一的界面把这些问题都答复掉,如果他需要的话,也是可以点进去到实际的位置。
#7. 用户和权限控制

从以上部分我们已经看到,这个系统是一个比较复杂的开发过程管控平台。这样的话,每个使用的人就应当可以登录,然后分配不同的权限等级。

未登录用户应当有一些东西的查看权限,但是不能发表评论。已登录的用户根据权限级别,可以控制能否创建、修改项目,创建、修改目录,代码,单元测试,文档等。
#8. 国际化字符串管理

一个跨语言区域的Web应用不可避免要跟国际化打交道,这个事情通常是在服务端做,比如通过在界面代码中嵌入类似<% =getRes(key, lan) %>这样的代码,去获取相应的字符串,替换到界面里来。

这个事情是要占用应用服务器资源的,而且国际化本身其实是一个在运行之前就已经确定的事,完全可以把这个过程放在发布阶段就做掉。比如说,我们给每种语言预先就把代码生成多份,只是部署在一起,根据需要的情况来动态加载特定的那一份。

有不少客户端的国际化方案,是把资源文件拆细,以页面为单位存储,但这其实是不太合理的。第一个原因就是在Web2.0时代,“页面”这个概念本身就已经弱化了,到了单页应用里,整个应用都只是一个页面,这个时候,资源文件以什么粒度来组织呢?

我们提到过,采用MV*框架去做Web应用的架构,有一个目标是做组件化。组件化的意图就是某个组件可以尽可能随心所欲地放在需要的地方用。如果把资源文件的粒度弄小到对应HTML片段和JavaScript模块这一级,灵活性倒是有了,带来的问题就是管理成本增大。

做一个行业应用,最重要的就是业务一致性,这包括逻辑的一致性,也包括了术语的一致性。某一个词,可能在多个资源文件中都出现,这就增加了不一致的可能性。

所以,应当有一个统一的术语管理平台,一切界面上出现的文字或者提示,都必须来自这个平台。
#9. 静态资源的管理

在发布系统的时候,除了需要发布代码,还需要发布图片等静态资源,这些东西也应当被管理起来。

静态资源在两种情况下可用:随产品发布,在本平台被引用。比如说有一个图片,在这个平台上作了管理,它可以被配置到某个项目上,在发布的时候导出。这个图片还可以被用链接的方式查看或者下载,如果本平台内部的一个文档或者示例要引用它,也是可以的。
#10. 样式与主题管理

在Web系统里,样式和主题是很重要的一环。样式的管理和发布一直是一个比较复杂的话题,早几年一般都是分块写,然后组合合并,最近这些年有LESS,SASS和Stylus这类技术,解决了编写和发布的分离问题。

我们看看发布的最大问题是什么?是不同部分的合并。为了追求灵活性,不得不把东西拆得很细,之前HTML片段和JavaScript模块的处理方式都是这样。这么做,我们就需要另外一件事:这些细小的东西,尽可能要覆盖全面。

对应到CSS里面,我们要做的是把每种在系统中可能出现的元素、类别都作为单独的规则维护起来,生成一个全局的规则列表。不同项目间,实现可以不同,但规则的名字是固定的,定制只允许修改实现,不允许修改规则。如果要新增之前没有的规则,也必须在全局规则列表里先添加,再作实现。

样式规则被管理之后,可以在界面组件上对它作关联,也可以不做。做的好处是发布的时候能只把用到的那些样式规则生成发布出去,如果能接受每次发布全量CSS,那也无所谓。

除了规则,也需要考虑一些变量的管理,在CSS中合理使用变量,会大为减轻定制化所导致的工作量。
#11. 一键发布

我们引入了这么一堆东西,其实是增加了发布的复杂度。为什么呢?

之前不管HTML、JavaScript还是CSS,都是手写出来,最多经过一个minify的工作,就发布了,整个过程很简单,两句脚本搞定。

现在可复杂了,先要分析依赖关系,然后提取文件,然后国际化字符串替换,然后合并,然后代码压缩,整个过程很折腾,不给配置管理员一个解释的话,他一定过来砍人。

我们有个原则:解决问题的过程中,如果引入了新的问题,要求负责解决原问题的人也一起解决掉。现在为了一些意图,增加了版本发布的复杂度,那也要有个办法再把这事摆平,至少不能比原来复杂。

所以我们就要把这些过程都集成到管控平台里,做一个一键发布的过程,把所有的这些操作都集成起来,配置管理员发布版本的时候只要点一下就可以把所有这些事情做掉。甚至说,这些流程还可以配置,能够加减环节。

这时候我们做到了跟之前发版本一样方便,能不能多做点什么呢?

可以把JavaScript单元测试集成到版本发布阶段。因为我们已经把JavaScript按照职责做了分层,并且把UI部分做了隔离,就可以在浏览器之外把这个单元测试做掉,平时提交代码的时候也可以做,最终在版本发布阶段再全量做一下,也是很有意义的。

代码依赖关系管理的另一个目的是什么呢?是最小化发布,既然我们都管理了文件之间的关系,那么,从根出发,显然是能够得出哪些代码文件在本项目中使用的,就可以每次从我们的全量代码库中取得确切需要的一部分来发布。这也是我们整个管控平台带来的优势。
#12. 小结

我们这一篇比较复杂,提出了一整套解决大规模前端协作的管控机制。这套理论的本质是在开发和版本发布之间加了一个环节,把Web体系中除了服务之外的一切静态资源都纳入其中,强化了现有主流的一些基于命令行的前端工程化组织模式。

相比于传统行业,比如汽车制造,我们这个环节相当于生产流水线的设计,其中一些组件的存储就类似仓储机制,发布就类似出厂过程。

这个平台本身还有不少其他的可做的东西,比如甚至可以在上面做界面的可视化定制等,这些是长远的终极目标,在后面的文章里会谈谈一些考虑。

后续文章中,我们会展望有了这个平台之后,整个前端的协作流程是怎样的。

好复杂,先大概了解思路。
如果用外部的平台怎么样? 例如类似coding的,能实现托管和团队协作。 那某一个小module的变化的监控这一步怎么做呢?

@alexsunxl 外部的平台管不了这些,要实现模块的变化监控,必须先把依赖关系和版本信息管理起来,而且这个已经没法内置到代码中了,所以已有的外部平台都是干不了的,jspm这种比较接近了

@xufei 最近准备用ng做公司内部一系列的管理系统,要求多人协作开发。请问大大在这种项目的工程目录、开发方式、模块划分中有什么心得呢?

感觉你说了好多书面的理解。但是觉得可能少些心得体会,就是实际中是否遇到过,自己是怎么解决的。这样是不是更好啊】

组件化之后修改某个基础组件,所有依赖它的功能都需要重新测试,这个过程依靠自动化测试靠谱吗?如果不靠谱应该怎样解决/合理控制呢?