微信扫描登录

沪江网校前端架构漫谈

本篇文章整理自沪江网web前端架构师易未来5月24日在『ITA1024前端技术精英群』里的分享实录:沪江网校前端架构漫谈。

enter image description here

没有统一架构的时候是怎样的一种情况?

起初前端是没有架构的,大家只是在完成一个一个的页面。我们来看看会发生什么。

• A同事是一个非常有意思的人,他喜欢把跟这个页面相关的所有的JS都写在同一个文件里面。嗯,传说中2000行代码的JS文件就是这么出来的。

• B同事是一个对技术比较有追求的人。他觉得模块化不错,所以他在自己做的页面里选用了requireJS。看上去不错哦,巧的是C同事也是一个对技术有追求的人,但是他不喜欢AMD的规范,所以他支持国货seaJS,哦,他还在里面使用了他喜欢的模版引擎Jade。

嗯,同一个网站,每个页面的技术选择完全不一样的。别忘了,网站是需要维护的,修Bug阿,改需求啊。有一天B同事跑去负责C同事做的那个页面的需求改动,当他看到那些他不熟悉的技术时,内心是极度崩溃的。

• D同事玩的就比较高级了,他喜欢写es6,也用sass。这就要求无论在开发的时候还是发布的时候代码都要先编译,而且显然你是不可能每次都是手动去做这个事情的。这就是他对工程化有需求了。

这个时候他很可能选择放弃,或者妥协(只使用少部分构建工具很好支持的功能),毕竟要实现一套完整的工程化是很花时间的事情(即使只是支持他自己的页面),而程序员的时间往往会被业务需求所淹没。毕竟以单个人和单个页面的单位来看,工程化这个东西是得不偿失的,如果以团队和整个系统去看那就不一样了。

其实到这里就可以看出来,个人所使用的技术的天花板往往会被整个团队的现状所制约。

架构是不是必须的?

上面描述的是没有架构的时候发生的情况,这些情况看上去当然都不太好,但是如果跳出前端这个角色,我们怎么去描述说这些问题会造成的影响,反过来的意思就是,如果有一个统一架构能够带来的好处。

回答出这个问题,可以解决两方面的困扰。

第一是说服大团队里的其它角色前端架构这件事情的重要性(在现在的大环境下,这个其实很重要);

第二是自己要时刻记住架构的目的,能够不忘初心,不沉迷于形式和概念。

现在回过头来看看,

架构的目的是什么?

答案是提升质量和效率。

没有架构的情况下,新技术无法得到引入,技术无法统一,使得团队的整体技术能力无法得到提升,也无法提供技术上的通用解决方案,从团队的角度来考量的话,效率是非常低下的。

同时,因为技术过于陈旧,再加上代码没有统一规范,导致碰到页面业务逻辑比较复杂,或者对老页面进行维护的时候,产生Bug的概率非常高,产品质量堪忧。

架构应该怎么玩?

上面讲到,架构的目的是提升质量和效率。那我们看看架构应该做到哪些方面才能实现这个目的。

• 架构是一个抽象的过程,它是架构师根据自己的经验对大量具体的业务项目进行分析,发现其中的规律,抽象出具体的规范,最终又应用于具体的业务项目中去。比如常说的MVVM就是一种规范。

• 要把跟业务无关的问题都在架构层面处理掉。比如代码压缩,打包这种工程化的问题都要在架构层面统一解决的。要做到业务的归业务,架构的归架构。

• 架构要考虑到可以方便团队成员提供和使用通用技术解决方案。比如分页组件这种。

• 架构设计的时候要综合考虑当前的主流技术跟自己业务系统的实际情况。因为前端正处在高速发展,各种新技术,工具,插件,框架层出不穷,这个时候要特别谨慎,有时候一个坑跳下去,就呵呵了。

沪江网校现在的架构是怎么样的?

基于以上原则,在搭建架构的时候,经过讨论和尝试,我们最终确定出4个方向,模块化,组件化,工程化,规范化。(你也看出来了,大方向是跟主流走的,太阳底下没有新鲜事啊。)

说了这么多虚的^_^,下面来点干货。

第一点-工程化:

构建工具用的是webpack,发布系统用的是jekins。

构建这里是分开发环境和生产环境。开发环境需要提供jsmap, css map,livereload等开发时候需要的功能,而生产环境需要压缩,打包,静态资源文件名添加hash等功能的。这里插一句,如果要启动开发环境,只需要 npm start。

第二点-模块化:

现在都是commonJS当道了,所以选择es6+ babel。这里顺便提下我们使用的框架,PC端knockout(为了支持IE7), 触屏端和hybrid端redux+react。

第三点-组件化:

这一块我们是做的挺彻底的,也思考了很多。

• 我们的页面是由一颗组件树组成的。看下图,invitationActivity代表了一个页面,components下面的每一个文件夹都代表一个组件。每个组件包含自己需要的js,css,image等资源。

enter image description here

• 保证组件的封闭性。因为JS方面是模块化的,在css方面我们也引入了cssmodule来做到这点。

• 组件的功能界限问题。也就是什么是应该在组件内部实现,什么是应该由组件的调用者来实现的。看下图,下面这个界面会封装成一个业务组件,因为很多页面上都会有这个组件,所以在我们的系统里面,它是被当作一个公用组件的。顺便提下,这个组件本身是由多个子组件组成的。

enter image description here

现在有两个问题需要考虑下:

1、为了显示热门词汇有哪些热词,需要调接口从后台获取。那在哪里去调用接口呢,是组件本身去调用,还是由使用者传进来。

2、现在点击搜索按钮,需要跳转到搜索结果页(还有可能要打点)。那完成这些操作的代码写在哪里呢?是直接写在组件里面还是由调用者传入,由组件在相应的时机调用传入的函数。

这就是组件的功能界限问题。我们的做法是组件只负责跟UI显示相关的部分,所有业务逻辑都不属于组件本身的功能。

根据这个原则,我们来回答上面的问题。

1、组件只负责显示热词,至于具体有哪些热词,由它的调用者传入。

2、组件只知道搜索按钮被点击了,至于按钮被点击具体要做些什么,它是不知道的,它能做的就是调用传给它的回调函数。

备注:关于组件的功能界限问题我们也思考了很长时间,并且做过不同的尝试。比如把
点击搜索按钮要做的事情放在组件里面自己做怎么样呢,后来发现不同的页面上点击搜索按钮需要打点的关键字是不一样的,这时候你如果把组件写死了就没法重用
这个组件了。其中关于接口调用是不是要写在组件内部的问题更是一度相持不下,似乎两边都有点道理。后来碰到一个真实的事情就是因为接口调用都写在组件里
面,导致同一个接口在某个页面上被调用了两次。当这件事情发生之后,天平似乎往另外一边倾斜了点。

• 一般组件化开发之后,我们会碰到两个方面的问题。

第一个问题是我们去看别人的代码的时候,没办法方便的知道这个页面的组件树是怎么组成的,以及每个组件需要哪些数据。

第二个问题是当组件树的层次很深的时候,父子组件间参数的传递会非常繁琐。而且一旦需要增加或删减掉某个参数的时候,整个父组件到根组件路径上所有组件参数的传递都要修改。

关于第二个问题,也就是父子组件之间参数传递的问题,我举个例子来详细说明。假设现在组件树的结构是这样的。

A组件所有子组件加起来需要的参数是9个,那么调用A组件的写法是

<Aa1= “1” a2=“2” … a9=“9 />

然后在A组件内部,A组件本身只需要参数a1,其它的八个参数是被它的子组件B1,B2消费的。那么A组件内部的写法大概是这样的。

<B1a2=“2” a3=“3” a4=“4” a5=“5” />,<b2 a5=“5” a6=“6” a7=“7” a8=“8” a9=“9” />

B1组件本身不需要参数,四个参数都是被子组件C1,C2消费。那么B1组件内部的写法大概是这样的。

<C1a2=“2” a3=“3” />,<C2a4=“4” a5=“5” />

这个时候已经可以看出来,如果C1需要增加或者删减一个参数,从组件本身C1到根组件A之间的所有组件都需要改动。想象一下当组件树的横向和纵向的层次都变的非常深的时候,这个时候每个组件的参数传递都会变的非常庞大而且混乱。

如果要把一个组件的位置换一下的话,要改变的地方之多,也是让人非常头疼的。我们想了一个方案来同时解决这两个问题,看下图:

enter image description here

我们每个页面都有一个param.js文件,可以看到从param.js里面可以清晰的看到当前这颗组件树的结构,以及每个组件自己需要的参数。而在组件内部,写法也相当简单,以A组件内部为例,组件内部的写法是这样的,

<B1{…B1_param} />,<B2 {…B2_param} />

可以看到,这样的话,无论是增减参数还是移动某个组件,都会变的非常简单。

由于我们对组件功能界限的定义是只负责UI相关的功能,所有的业务逻辑都是从调用者传递过的。也即是写在param.js。所以param.js文件是非常重要的一个文件,里面基本包涵了这个页面所有业务处理逻辑。

很显然,随着页面业务逻辑变的复杂,param.js将会变得越来越大。没关系,把不同的组件参数分拆到不同的js文件里面去实现,然后建个params文件夹把它们组织起来。

第四点-规范化:

• JS语法检查选用了eslint。 • 项目目录结构非常清晰。当进行开发的时候,哪些代码应该放到哪里都进行了明确的规定,并且每个文件的功能都尽量清晰并且单一。

顶层目录结构如下图:

enter image description here

1、src文件夹存放的是所有的的源代码和其他静态资源(比如图片,iconfont)。 2、dist文件夹存放的是所有编译后的代码。 3、build文件夹存放的是所有工程化所需要的代码。 4、document文件夹当然存放的文档。

下面重点看下src目录结构,如下图:

enter image description here

1、app文件夹里的每一个子文件夹代表了一个页面,每个页面所用到的所有静态资源都存放在这个子文件下面(除了引用的公共资源以外),构建的时候,每个子文件夹会生成自己的静态资源供页面引用。

2、common文件夹里面的所有代码在构建的时候会单独生成js文件和css文件供页面引用。所以一个页面会引用两个js和两个css.里面存放的是每个页面都会用到的一些共用资源。比如触屏端使用了react,那么跟react相关的那些包就会放在common里面。

3、components文件夹里面存放的是共用组件,每一个子文件夹代表了一个组件。有可能是通用的功能组件,比如分页组件,Loading组件,ModalDialog组件。也有可能是一个通用的业务组件,比如站点通用头部,通用footer,通用分享组件。注意,在其他地方引用这些组件时,是不需要写相对路径的,直接写组件名字就可以了,比如import pager from ‘pager’。这样对使用者更方便。

4、lib文件夹存放的是通用的js类库。比如检测浏览器用的browserDetect.js,处理日期用的dateUtil.js。同样的,在其他地方需要引入这些JS时,也不需要写相对路径,直接写JS的名字就可以了。比如import{isIE} from ‘browserDetect’。

5、style文件夹里面存放的一些公用的sass资源。比如function,mixing, variable。其他的sass文件需要引入这些资源的时候,使用方式跟使用通用js一样,直接@import “base.scss"即可。

写在最后

沪江网校的架构才刚刚有个雏形,后面还有更多的功能会加进来,比如脚手架(等到架构更成熟的时候在出一套完整的),NodeJS中间层(在大方向上已经达成统一),前端监控系统等。

架构是个不断完善的过程,而把架构尤其是跟规范相关的部分落实到具体业务系统里面更是个团队不断磨合的过程。它最终考验的,同时也是最终磨合出来的是团队的成熟度。

谢谢大家。

Q&A

1、具体页面目录结构怎么划分的?

易未来:如果你问的是每个页面里面又是怎么划分结构的,可以参考讲组件化的时候发的第一张图。

2、现在是还没有做nodejs直出吗?未来是否有这个方向。

易未来:现在线上还没有,一开始NodeJS在整个公司层面上是有一些阻力的,现在已经达成共识,我们正在加入NodeJS中间层做直出或者同构。

3、架构的形成需要很多的内容,想请教一下:我们是如何分配团队资源,在繁多的业务实现工作之余,还能推进架构工作快速有序的进行呢?

易未来:业务上的任务肯定是要排第一位的,架构真的要看团队里有没有人愿意牵头了。尤其是对前端架构必要性的认可现在大环境不是太好。所以还是人很重要啊,要主动做这方面的事情,优秀的前端大家都缺嘛。

4、问下组件化用的是不是react,如果与后端对接如何进行,可能后端对此不懂?

易未来:触屏是react,PC是knockout。其实组件化只是一个思想,跟框架的选择关系不是特别大,当然有些框架做起组件化来特别方便。至于后端的问题,我们现在是前端渲染的,跟后端没关系,如果将来要做同构,我们也是上的NodeJS。

5、问下老师在迭代前端开发架构的时候怎么过渡比较好?是一整套重构还是逐步进行?

易未来:逐步进行,一步到位对架构师和团队成员的要求都太高了,个人感觉基本不太现实啊。而且架构要经过不断在业务线落地磨合,才知道是不是真的合适。

6、考虑nodejs直出是基于什么考虑?如何从公司层面上说服他们使用nodejs呢?

易未来:主要是seo,当然性能上也会有优势。至于说服公司层面,慢慢磨吧,看你口才了^_^。毕竟nodejs的大环境是越来越好了。

7、 这样模块化之后,对模块质量值怎么把控的,比如单元测试完备到一个什么程度?

易未来:很遗憾,我们现在是没有单元测试的,人少活多望谅解。话说能做完备单元测试的团队真的是很成熟了。

8、刚才讲的属性是在param.js中统一处理,组件有对外公开的方法吧,如果B组件作为A组件的子组件,B组件中的方法也是在A组件中公开的么?还是说外部调用者直接调用B组件方法?

易未来:不公开的。所有处理逻辑都是从最外面一层传进来的,不会出现父组件要去调用子组件的方法的情况。

9、问下老师刚讲架构利用了一些js框架,对html5、css3这块是怎么考虑的呢?

易未来:对技术的选择,比如是否使用flex也算是架构的一部分吧。这个要具体技术具体讨论了。比如flex在触屏端我们是使用的。

10、Hybrid开发,Native升级,导致组件A必须跟着升级,不升级将导致项目挂掉,如何通知所有引用组件A的地方跟着升级呢?业务线很多,各个项目组情况也不一样,遇到这个情况特别棘手。

易未来:如果组件被多条业务线被使用,升级确实是个大问题。这个问题值得深入讨论。我们因为现在是只在网校线使用了,其实没有太多这方面的经验,不敢乱说。这个可以线下大家多交流。

11、组件是用第三方的组件,还是自己开发组件?还是可以混着用?谢谢

易未来:我的建议是自己开发,不要引入第三方的,我暂时没看到说自己开发成本会特别高的。一是怕踩坑,二是可以提高团队的水平。