使用AngularJS构建RIA前端架构实践

SegmentFault 2016 年终盛宴——SFDC 杭州开发者大会,已于 12 月 10 日在杭州海外海皇冠假日酒店举行,大会分别从前端、移动端、服务端三个方向,关注开发、项目实践和技术创新,探索问题本质和核心,分享各领域众多优秀的实践和思维。
Worktile 基础版负责人贺天卓,受邀作为本次大会前端会场分享嘉宾,为大家带来《使用AngularJS构建RIA前端架构实践》演讲。

以下为演讲内容回顾:

大家好,接下来由我跟大家分享一下如何用AngularJS构建RIA前端架构。

我是Worktile的前端架构师,目前是Worktile基础版的负责人,做前端开发也有十年了,做过各种打杂的活写过后端也做过设计。

这次session大概由5个阶段组成,首先明确什么是富应用,对Angular进行简单的了解,然后是我们Worktile在这三年里用AngularJS的一些经验。包括更好、更快的使用AngularJS,最后是一些相关资源推荐。

首先我们明确一下什么是富应用,富应用还有一种说法是单页应用,总体来说比普通资讯型网页有更高的交互性,更强大的用户体验。比如下面有一些例子,office360,Worktile,都算是Web应用的东西。相比普通网页难在哪儿呢,前端角度来讲,它的界面会非常的复杂。它的复杂就是会有很多区域,很多模块,而且元素之间关联、会有弹窗,还有元素的堆叠。交互会有拖拽、缩放、快捷键……等等。数据展现形式也是多种多样的,对于列表有各种排序规则,对于日期有各种形态,还有复杂form表单、元素合规性检查……等。

在我三年前没有接触AngularJS之前也做过一些比较复杂的系统,当时主流是ExtJS,或者是Flush之类的东西。(当然你采用Flush已经是另外一个编程模型领域了)无论你怎么做,自己去搭,给我的感觉,最后代码虽然不像面条式的,但很脆弱,就像搭一个纸牌房子的感觉。

接下来给没有实际使用过Angular的同学大概讲一下Angular的基础概念,Hello Angular。首先引入一个AngularJS的脚本,在代码最上面有一个ng-app,声明,声明这一块开始就由Angular托管了。底下这里写双括弧的表达式用来绑定数据层,而且可以在Input表单上指定一个ng-model做双向绑定,这就是特别简单的Hello Angular。当时选型的时候觉得这个东西酷,很简单,就开始用了。结果用着用着发现产生了这个问题,搞前端的同学学jQuery应该知道越了解jQuery它的API,就越觉得学起来很爽。搞后端的Node.JS也是这样,但是在学用Angular的时候,就感觉学习曲线有些诡异,有时候会有一些曲里拐弯的感觉。

再讲一下Angular的知识点,AngularJS的构成,数据绑定、Scope、Modules、依赖注入……每个都是一个小概念,看上去挺多的,其实也不用担心,后面会提供一张图给大家分门别类起来,这里就不多赘述了。现场懂Angular的同学也不少挺多的,太基础的知识点我就快点过。

Angular是MV* 的框架,有Model、View层和ViewModel。最主要的特点是双向绑定的,双向数据绑定的图是官网的,我做了一下自己理解的注释。大家写自己的HTML模板,Angular进行Complie编译,编译后 当数据发生变化会动态更新到View层,而当View层的控件修改了数据,也会同步更新到Model层,这是Angular在研发之初最具特色一个特点。

这是刚才那个Angular构成图的分类后的样子,Angular的知识点看上去很多,但是每个知识点所要处理的业务还是分得很清晰的。Module是所有东西的容器,由Module管理这些概念。比如在Module下可以声明一个Config进行模块的预定配置比如路由配置,也可以定义一个数据过滤器Filter和自定义组件Directive。

讲完Angular概念之后,讲一下用Angular怎么用好它。首先要排除jQuery的编程方式。因为Angular是双向数据绑定的,所以要明白DOM结构不是数据结构,数据才是数据结构。最开始的思路转换是最难的,要从数据结构来考虑控件的编写,如果这是一个业务页面的话,它的业务状态有哪些,要基于业务数据模型来编程。如果是交互控件的话,就要基于控件状态有哪些,有多少个交互状态来编程,而不是回到DOM的思路去写。

这张PPT是模拟的一个场景,如果要写一个图书馆管理系统,怎么样分解代码。图书馆管理系统肯定有书,书就是一个大的模块,它会有自己的Template模板,会有自己的数据展现的转换Filter。也会有书的读取DataService,也会有自己的路由。图书馆系统中有用户来借书,那么User也是一个模块,同样也是这样去分解,每一个大的模块都应该是把的代码组织在自己模块内,用模块去分解,这样的话你的代码会分得比较清晰。

这是Book模块具体的写,Angular.module(Book,[])。它不依赖于任何模块,所以这里就是空的。它可能会有一些静态配置,可能会有一些自己的路由,可能会有自己增删改查的数据请求。会有关于书籍不同的枚举状态,这个书的借还状态,可以写一些Filter。具体的Controller,就是页面控制器。

这里提醒大家一个最开始特别容易忽略的一个地方,Angular.module如果有中括号的话,它会是声明一个模块。如果这里没有的话,表面是在已有模块上附加上新的代码,这是一个小细节。

还有一个需要注意的是依赖注入的写法,依赖注入有3种写法,第一种推断式虽然代码写得比较快的时候,但后来如果采用了代码压缩就不能正常工作了。后期把所有代码改写成注解方式,当然也可以写到内联里。不推荐推断式,不能进行代码压缩,压缩之后会报错。

说完代码分解到模块里之后,那具体文件应该怎么存放呢。我个人的实践经验就是按照模块集中放,但是要把它打水平,水平放置,目录不要太深。

还是拿Book模块来做例子的话,会把它的文件分散到service、controllers,具体的detail,详情页面也会分到html模板上,不用展开太多目录层级。这样还有一个好处,使用SublimeText时摁快捷键Ctrl+P敲入名字就可以快速打开文件。

有了文件存放方式,有了模块,用户输入UI之后,怎么样让它们串联起来呢。这里就要用到路由组件ui-router,早期Worktile是用官方的ngRouter。后期出现一些业务需求,要分区域。比如一个页面有main主区域,sidenav子区域。我在访问book栏目页时,主区域main是显示书籍列表,子区域显示书记分类,采用 ui-router就能很好的处理这种需求,也能将页面 URL和html模板、controller控制器串联起来。

这里讲一将编写Controller的注意事项,在Controller控制器应该只放置场景代码,一个控制器只负责一小块视图,不要写出万能控制器,不要在Controller中考虑重用,当有重用可能,应当考虑是否放到service。也不操应该作DOM,DOM应该由Directive驱动。不写数据模型格式化逻辑,应该放到service和Filter里。最后Controller不要基于内存数据通信,而应该用Angular官方的广播机制$emit或$broadcast去通信。

由于DOM是有层级的,但是在controller中应该避免$scope嵌套混乱,现在Worktile做法是所有的数据,包括方法,controller里都应该放入$scope.vm视图模型对象里。而且开发的每一个组件都应该是独立的isolate scope,确保和外部是有一个隔离的关系。所有数据变化都应该由cache层触发,当然不是所有的福应用都有这个cache层。因为Worktile采用了WebSocket长连接,所以有Cache层,界面显示数据会优先从coche里引用读取,如果coche需要更新的话,WebSocket会通知浏览器更新的。而对于WebSocket通知系统里产生的各种数据变化消息,我们会有一个订阅list,各个组件根据需要来订阅数据变化。

Directive有一个容易让人迷糊的地方,它的绑定方法,有link,compile,controller 三种;其实 link和compile是冲突的,link是compile函数返回函数的缩写方式。controller也可以绑定,但Angular建议只有在多个Directive公用一个Controoler时才这么写,大部分都是写link 绑定。

Directive绑定这里有一些与性能相关的地方,在Directive中不同的时机分别使用compile和link。Compile一般在DOM渲染前对DOM进行操作,并且此时不需要用到scope参数。如果想在所有相同Directive里共享某些方法和数据,这时应该定义在compile里,性能会比较好,link会执行多次。对特定element元素的注册绑定事件会写到link函数里,在需要用到scope参数时也会用到link函数。

Directive里的restrict,支持4种形式的封装AECM,分别代表 属性Attribute、元素Element、样式Class、注释Comment。在实际编写组件Directive是,我们会用 restrict=E来封装元素性的控件,假如封装一个时间选择器的话而如果是个交互性的组件会用restrict=A来封装。一般不建议使用另外两种样式和注释性的组件封装。

大概总结一下Angular框架,虽然Angular有很多概念,上手比较困难。但是如果我们用好用准它的概念分而治之,用Angular的秩序来包容业务系统的变化,最后达到的效果是非常好的。它会对你的代码进行问题领域。路由层可以用官方的ngRouteProvider或者Angular-ui-router配置;视图层采用表达式绑定数据,filter格式化数据显示。通讯层有http,angular-resource封装Ajax/REST请求。各种组件就编写Directive来达到交互组件和元素控件重用。

再接下来讲一下性能方面的知识点,Worktile是一个看版,它的元素会非常多。如果有300个任务时,它的元素会达到3千个之多。当时我做性能优化的时候,发现动画是一个很大的CPU开销。而Angular动画默认是开启全部元素的全部动画,所以如果DOM元素特别多的时候,所有元素的添加和删除都会触发到动画。虽然这个动画很快,在元素少的时候这都不是问题,但元素多了这就是一个灾难,所以我发现如果开启了动画元素过滤器之后,让只有样式名叫wt-animate才会启用动画效果,性能瞬间优化20%,这是在元素特别多的时候。还有CSS3的动画会比位css position的移动画效率高很多,因为它调动浏览器的GPU。

其次DOM操作是昂贵的,ng-repeat是展现列表时非常常用的东西。因为Angular的脏检查也是考虑DOM重用的,但是它无法知道DOM是不是可以重用,所以所有ng-repeat原则上都要指定track  by主键。还有ng-if优于ng-show,因为ng-if能让子元素子组件懒编译。还有一个是以前jQuery中的经验如果你有一个很大的表格,给每个表格td上绑一个事件,这个事件绑定的数量是非常可观的,这时可以将事件绑定到表格行tr上来优化。最后就是上面提到过得跟数据无关的一些DOM操作应该放在compile阶段,因为它只执行一次。

Angular双向绑定也会带来性能的问题,首先我们可以尽量减少绑定范围,这两个写法达到的效果是一样的。但是这个写法会把绑定的元素绑定到“P”标签里,这里内部还有很多表达式的话也是一个性能灾难。还有对一些需要数据更新的东西,尽量用一次性的绑定的语法来绑定,就是两个冒号{{::bind_once}}。特别是对象的主键,主键肯定不会变。主键变了的话,说明这个对象就应该已经删除了。

然后就是浏览器的颤动问题,这上面两个和Angular没有太多的直接关系,在jQuery编程中也是可以优化的,但也是使用中的一些心得。在绑定resize和keydown时都应该加debounce防颤器,在绑定scroll事件时要用throttle节流阀。还有就是当你在用ng-repeat重绘重排时,应该防止重绘重排,先将元素hide,最后一个repeat元素complie后再展示出来,能尽量减少浏览器的重绘重排,速度会快很多。还有慎用ng-mouseenter和ng-mouseleave这两个事件会非常频繁的被触发。

及时$destroy时记得回收资源。

还有是关于双向数据$digest的一些性能点,双向绑定基于原理没有这么复杂,它会有一个脏检查,每次脏检查都是在$digest过程里做的。所以我们要求大家尽量少的触发$digest。因为每一次触发的$digest时候,也会触发filter至少两次,会触发$watch一到两次。还有就是不要写特别复杂的filter和watch。要保证watch和filter没有阻塞,单次执行应该足够快。DOM是很昂贵的东西,不要再watch中修改DOM。不要构造一个很深层次的对象然后$watch。

通过设定好directive的priority优先级,将DOM生成的directive前置。先让DOM生成的控件执行完,把优先级调得很高,最后再把绑定事件的行为组件再去执行,这样的话也会让整个应用性能得到提升。还有如果不触发数据变更的$timeout,函数后来加一个true就会跳过 $digest。

在对于前端团队如何基于Angular框架来协作开发,我们团队现在的做法首先把所有Angular的组件、编码规范、视觉规范全部放在一个DemoCode网站中,方便新的同事来学习和上手。我们还会用看板式API文档,让前端的同事对接口有一个全局的了解。最后就是一些Angular推崇的一些工作,比如说代码检查、自动化测试,还有代码打包,一系列东西都用自动化配置起来。

最后这里给大家推荐一些Angular的相关的学习资源,官方网站、AngularUI的、还有ngNice、还有Angular编码规范,非常推荐其实里面不光是编码规范,还有一些最佳实践在里面,非常好的文档推荐大家去看。