沪江网校前端架构漫谈
作者: 未来
本文转自互联网技术联盟(ITA1024)技术分享实录
正文如下
没有统一架构的时候是怎样的一种情况?
起初前端是没有架构的,大家只是在完成一个一个的页面。我们来看看会发生什么。
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等资源。
保证组件的封闭性。因为JS方面是模块化的,在css方面我们也引入了cssmodule来做到这点。
组件的功能界限问题。也就是什么是应该在组件内部实现,什么是应该由组件的调用者来实现的。看下图,下面这个界面会封装成一个业务组件,因为很多页面上都会有这个组件,所以在我们的系统里面,它是被当作一个公用组件的。顺便提下,这个组件本身是由多个子组件组成的。
现在有两个问题需要考虑下:
为了显示热门词汇有哪些热词,需要调接口从后台获取。那在哪里去调用接口呢,是组件本身去调用,还是由使用者传进来。
现在点击搜索按钮,需要跳转到搜索结果页(还有可能要打点)。那完成这些操作的代码写在哪里呢?是直接写在组件里面还是由调用者传入,由组件在相应的时机调用传入的函数。
这就是组件的功能界限问题。我们的做法是组件只负责跟UI显示相关的部分,所有业务逻辑都不属于组件本身的功能。
根据这个原则,我们来回答上面的问题。
组件只负责显示热词,至于具体有哪些热词,由它的调用者传入。
组件只知道搜索按钮被点击了,至于按钮被点击具体要做些什么,它是不知道的,它能做的就是调用传给它的回调函数。
备注:关于组件的功能界限问题我们也思考了很长时间,并且做过不同的尝试。比如把 点击搜索按钮要做的事情放在组件里面自己做怎么样呢,后来发现不同的页面上点击搜索按钮需要打点的关键字是不一样的,这时候你如果把组件写死了就没法重用 这个组件了。其中关于接口调用是不是要写在组件内部的问题更是一度相持不下,似乎两边都有点道理。后来碰到一个真实的事情就是因为接口调用都写在组件里 面,导致同一个接口在某个页面上被调用了两次。当这件事情发生之后,天平似乎往另外一边倾斜了点。
一般组件化开发之后,我们会碰到两个方面的问题。
第一个问题是我们去看别人的代码的时候,没办法方便的知道这个页面的组件树是怎么组成的,以及每个组件需要哪些数据。
第二个问题是当组件树的层次很深的时候,父子组件间参数的传递会非常繁琐。而且一旦需要增加或删减掉某个参数的时候,整个父组件到根组件路径上所有组件参数的传递都要修改。
关于第二个问题,也就是父子组件之间参数传递的问题,我举个例子来详细说明。假设现在组件树的结构是这样的。
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之间的所有组件都需要改动。想象一下当组件树的横向和纵向的层次都变的非常深的时候,这个时候每个组件的参数传递都会变的非常庞大而且混乱。
如果要把一个组件的位置换一下的话,要改变的地方之多,也是让人非常头疼的。我们想了一个方案来同时解决这两个问题,看下图,
我们每个页面都有一个param.js文件,可以看到从param.js里面可以清晰的看到当前这颗组件树的结构,以及每个组件自己需要的参数。而在组件内部,写法也相当简单,以A组件内部为例,组件内部的写法是这样的,
<B1{…B1_param} />,<B2 {…B2_param} />
可以看到,这样的话,无论是增减参数还是移动某个组件,都会变的非常简单。
由于我们对组件功能界限的定义是只负责UI相关的功能,所有的业务逻辑都是从调用者传递过的。也即是写在param.js。所以param.js文件是非常重要的一个文件,里面基本包涵了这个页面所有业务处理逻辑。
很显然,随着页面业务逻辑变的复杂,param.js将会变得越来越大。没关系,把不同的组件参数分拆到不同的js文件里面去实现,然后建个params文件夹把它们组织起来。
第四点-规范化:
JS语法检查选用了eslint。
项目目录结构非常清晰。当进行开发的时候,哪些代码应该放到哪里都进行了明确的规定,并且每个文件的功能都尽量清晰并且单一。
顶层目录结构如下图:
src文件夹存放的是所有的的源代码和其他静态资源(比如图片,iconfont)。
dist文件夹存放的是所有编译后的代码。
build文件夹存放的是所有工程化所需要的代码。
document文件夹当然存放的文档。
下面重点看下src目录结构,如下图:
app文件夹里的每一个子文件夹代表了一个页面,每个页面所用到的所有静态资源都存放在这个子文件下面(除了引用的公共资源以外),构建的时候,每个子文件夹会生成自己的静态资源供页面引用。
common文件夹里面的所有代码在构建的时候会单独生成js文件和css文件供页面引用。所以一个页面会引用两个js和两个css.里面存放的是每个页面都会用到的一些共用资源。比如触屏端使用了react,那么跟react相关的那些包就会放在common里面。
components文件夹里面存放的是共用组件,每一个子文件夹代表了一个组件。有可能是通用的功能组件,比如分页组件,Loading组件,ModalDialog组件。也有可能是一个通用的业务组件,比如站点通用头部,通用footer,通用分享组件。注意,在其他地方引用这些组件时,是不需要写相对路径的,直接写组件名字就可以了,比如import pager from ‘pager’。这样对使用者更方便。
lib文件夹存放的是通用的js类库。比如检测浏览器用的browserDetect.js,处理日期用的dateUtil.js。同样的,在其他地方需要引入这些JS时,也不需要写相对路径,直接写JS的名字就可以了。比如import{isIE} from ‘browserDetect’。
style文件夹里面存放的一些公用的sass资源。比如function,mixing, variable。其他的sass文件需要引入这些资源的时候,使用方式跟使用通用js一样,直接@import “base.scss"即可。
写在最后
沪江网校的架构才刚刚有个雏形,后面还有更多的功能会加进来,比如脚手架(等到架构更成熟的时候在出一套完整的),NodeJS中间层(在大方向上已经达成统一),前端监控系统等。
架构是个不断完善的过程,而把架构尤其是跟规范相关的部分落实到具体业务系统里面更是个团队不断磨合的过程。它最终考验的,同时也是最终磨合出来的是团队的成熟度。
谢谢大家。
iKcamp原创新书《移动Web前端高效开发实战》已在亚马逊、京东、当当开售。