我眼中的uee黑科技

备注: uee是我厂内部的一个前端框架.

这可能是最后一篇关于uee的文章了,因为已经不关注这个方面的技术很久了。术业有专攻,前端嘛,那可怕的前端摩尔定律,还是让专业人士去操心吧。

上个星期有一天,有同事反馈uee的watch不好使,变量已经修改,但watch回调方法不走进去。今天主要围绕这些类似的问题展开,不过呢,今天不对具体问题进行解析,今天主要讲讲uee的黑魔法,大道至简嘛,抽象(原理)和具体(使用)得两手抓,两手都要硬。

下面的内容,主要针对非专业前端(从其他领域过来的开发人员),或者对uee有兴趣的同事。专业人士请绕路。

先进行术语简介。ng1代表angular1.x版本,ng2代表angular2.x以上版本。

uee的本质是ng1

uee是一个前端mvvm框架,常用于SPA(single page application)的开发,虽然开发指南上没有明目张胆的宣传,但底层是ng1是事实,所以ng1.x的文档总是可以借鉴的,网上找到的关于ng1的吐槽也同样适用。关于什么是MVVM,参考阮大的博客http://www.ruanyifeng.com/blog/2015/02/mvcmvp_mvvm.html

ng1和ng2的区别,和雷锋、雷峰塔的区别差不多,外边新项目考虑ng的话,只会考虑ng2之后的版本。uee后来有个ts(typescript)版本,写法上高仿了ng2的写法,不过这里不关注。

ng1的核心概念比较多,DI,scope,双向绑定,指令,digest都是比较重要的,还是那句话,即使使用uee,熟悉ng1的核心概念没有任何坏处。

关于开发模式

这部分主要针对从传统MVC过来的童鞋,因为开发习惯的原因,总是会考虑操作页面,例如某个操作要让按钮高亮,有同学就可能贪方便直接操作style,但这不是正确的做法。mvvm关注的是数据的管理,界面的变化(dom)是自动的。习惯不纠正,写出来的代码可能很诡异。参考文章http://www.infoq.com/cn/news/2013/11/how-to-think-angularjs

变化来自watch

框架的第一印象就是一个页面上一个占位符,只要对应的变量发生变化,页面就会自动变化。这个特性是非常自然的,直到有一天你需要使用watch来监控一些变量,并在发生变化的时候做些什么。

其实,页面变化的魔法也是使用watch机制。所以某个页面上可能会生成非常多watch表达式。这个就是单向绑定的内容了。

例如下面的例子

<div>\{\{content\}\}</div>,就是会生成类似watch("content", function(){//修改div内容})的逻辑。

变化如何被感知 digest过程

现在页面有这么多watch,怎么鉴别有发生变化了?

客户端js是单线程执行的,所以不能写个死循环。可以想象到的做法有2种,一种就是死循环加个timeout或者interval,一种是被动触发。ng1是采用被动触发的,只是就是一些特殊的页面事件会导致检查的发生,这个检查的过程叫digest,俗称dirty check 脏检查。

主要的事件有,页面初始化,页面发生操作(例如点击,输入)等,至于一些异步行为,例如timeout,ajax等,正常是不会触发的,不过使用框架包装过的调用方式,也是会触发的。如果和其他框架例如jq插件等集成的话,或者需要确保调用到digest的话,就需要使用框架提供的apply这个功能,它会确保操作会被页面感知到,相信很多同学也见识过了。

不过,这里还有个细节: 联动更新,例如watch表达式a的时候改了表达式b,同时watch表达式b的时候又会修改a,那么页面中有一个表达式的值可能是旧的。ng1规避的做法比较简单,就是一次digest之后,发现有变更的话,再次执行一次digest,直到所有的表达式的值都稳定了,或者是超过了最大检查次数才停止。

示例伪代码如下:

watches =[] //保存所有的watch表达式和表达式的当前值
for each watch in watches //循环所有的watch表达式
    if watch中的变量发生变化(包括第一次初始化)
        记录表达式的新值
        执行回调,这个过程可能修改了其他变量

if 没有任何表达式发生变化
  exit
else
  重新执行一遍,直到检查达到最大次数

变化没起作用的常见原因

  • 跨页面修改数据

框架的作用域顶多就是一个html页面。但如果项目有复杂的集成方式,那么就可能有多个页面并存的情况,通过a页面去更新b页面的数据变得没那么自然。首先,无论是直接修改变量还是通过所谓的emit事件通知,都有可能没生效(要等b页面下一次digest才能感知到),直接修改dom的话,则存在其他一些问题。所以规避的方式,通常的做法只能调用b页面的某个方法,而这个方法通过嵌套apply来修改变量。

  • 修改数据但没有触发digest

这种常见于采用第三方js插件,或者直接使用普通的ajax,timeout异步操作等情况,同理只能使用apply。

  • ABA问题

ABA概念可以参考知乎文https://www.zhihu.com/question/23281499 就是修改了但没看出变化,例如改了之后又改回去。这种比较特殊,ng会认为你没有变化。开头提到的那个例子,就是某种情况下,一次digest过程中出现了变量又改回原值的情况。所以确实有这种需要,就得多点信息来标记你做了修改,例如增加个时间戳的字段。

  • 变量引用发生变化

意思就是说你修改的和你显示的,已经不是同一个对象了。这个涉及js对象和scope的一些特性,可以参考https://github.com/angular/angular.js/wiki/Understanding-Scopes 这个问题曾经也出现过

谈双向绑定

前面提到了单向绑定,当然双向绑定更是一个非常神奇的特性,特别是你写了一个页面控件,想和某个变量进行绑定,可能就需要考虑了。它的实现过程是这样的: 当变量发生变化的时候,需要修改页面,很明显这个可以通过建立watch表达式实现。另一方面,页面变化的时候,需要修改变量,这个可以通过dom事件,触发包裹在apply中的变量变更操作。掌握这个基本原理的话,在需要定位自带组件问题的情况下,就会减少很多阻碍。

谈自定义gadget

gadget是uee的概念,类似于ng2的component概念,是一种页面组件,包括页面,脚本,样式的大集合。需要了解一点,凡是自定义标签,在ng1中,都采用directive特性实现,只是directive以复杂难懂著称,在使用便利性上,gadget还是做得不错的。如果有需要调试gadget或者ui component的内部实现的情况,就需要掌握指令的各种奇怪接口。

业务开发中的同步与异步

在其他项目习惯了ajax同步操作带来的开发便利性,开始使用uee的时候,可能会发现强大如fire的请求,是没办法做同步的!例如请求a以后,再请求b,这种变得比较麻烦,实现通常是这样的,一是把请求合并,只搞一次就可以了,不过说的容易,实际处理会有很多限制。一种就是在回调方法中发起下一个请求,不过这么写少量还可以,多了就存在一个callback hell的问题。所以应该考虑引入异步控制库让代码更符合人的思维,例如这个async,https://caolan.github.io/async/docs.html

我的看法

uee作为一个前端框架,使用上还是可以的,gadget表现出来的特性也是不错的。不过,也有一些毛病(可能他们觉得是特性)。

一个比较奇怪的就是webroot占位符,宣称可以表示根路径,用来引用静态资源的时候很方便,这套路好难理解,硬生生和后端绑定一起了。而且处理静态资源分离的时候,规则也没那么自然。

另外一个类似fire这种大而全的怪物,源码长达数千行,我总感觉使用fire之后MVVM的边界反而更模糊了。

还有就是,其实uee不是完全定位在纯前端的框架,还包括一个gateway来对接(虽然不是强绑定),包括了一种类似mvc的绑定风格和一种服务总线的绑定实现。说白了,绑定实现是很忌讳的事情,如果它提供一个spring mvc的实现,我会觉得比较安心。

目前最流行的前端开源框架,有ng2,react,vue, ng2的套路比较深,是一个一站式框架,而其他两个更倾向于做好view这一块,其他由外部插件去实现。uee虽然也以时俱进,借鉴了ng2采用原生ts的方式,推出了ts版本,写法和ng2也比较像,不过累感不爱呀,这一次ng2的组件特性已经非常强大了,被人诟病的脏检查也改进了,窃以为直接native也是可以接受的。