分享实录前端框架Regularjs的设计与选型之路,网易杭研技术专家在ITA1024前端精英群的分享
Posted 互联网技术联盟
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了分享实录前端框架Regularjs的设计与选型之路,网易杭研技术专家在ITA1024前端精英群的分享相关的知识,希望对你有一定的参考价值。
网易杭州
前端技术专家
互联网技术联盟
ITA1024嘉宾讲师
本期分享嘉宾:
郑海波(网易杭州前端技术专家)
本期特邀主持:
林洋(去哪儿资深前端工程师)
本期特邀内容评审团:
季建(苏宁易购前端开发部门负责人)
汤桂川(广发证券Web前端资深工程师)
本篇文章整理自前端技术专家郑海波4月6日在『ITA1024前端精英群』里的分享主题:Regularjs的设计与选型。
正文如下
首先来个自我介绍。 我叫郑海波,来自网易杭州,自诩为前端DSL的重度爱好者,热衷于挖掘流行技术的本质。我的 Github: @leeluolee 以及微博: @拴萝卜的棍子
鉴于听众都是有工作经验的同学。今天主要会以分享设计思路为主,安利为辅, 它会是市面上出现的MDV(Model Driven View)技术的一次全面解析,希望可以解答一些朋友关于『为什么有那么多框架技术冒出来的困惑,在技术选型中做到『知其然且知其所以然』。最后,当然欢迎大家提一些尖锐的问题。
我负责或参与过公司的易信、秀品、相册等产品的前端开发,与此同时我也是网易前端的培训讲师之一,并在公司维护了一些应用较广泛的框架工具,目前我在负责网易有数这款BI类新产品的前端开发 ,正在内测阶段,有兴趣可以看下介绍视频了解下。
今天分享主要是以下三个部分:
1、Regularjs简介
2、Regularjs设计与选型之路
• 模板描述部分
• 数据监控部分
3、Regulars的未来展望
◆ ◆ ◆
一、Regularjs简介
『Regularjs是基于动态模板实现的用于构建数据驱动型组件的新一代框架』, 目前我负责的有数的前端业务代码接近了4W行, 能在短时间的开发中 Hold这种量级的开发,至少可以证明它并不是一个玩具,也可以从侧面反映今天内容的含金量。
Github仓库:
指南:
有句话说的好: 『你永远无法叫醒一个装睡的人』, 在程序圈『你永远无法阻止一个想造轮子的人』。13年负责易信前端时,一个同步复杂编辑状态的业务所迫,基于NEJ框架()封装了一个mini版的MVVM实现,解放了生产力。而契机在于Leader也意识到需要一个动态模板,所以有了宝贵的时间去调整这一套方案,一步步从模板开始催生了Regularjs这个名字超山寨的组件框架。
附一张图自黑图 :(
我直接抛一段代码 (ES6)
几个简要说明:
Regularjs组件提供命令式和声明式两种使用方式:当然声明式只是一种高级的接口模式,本质和命令式调用是一致的。
事件统一以 on-* 作为标示, 无论是组件事件还是DOM事件, 组件需要提供一个事件系统来进行内外的解耦。
其中`this.$body` 代表 代表被标签包裹的内容. 当然在命令式调用时,你也可以传入$body这个实例属性。这种语句的用法也可以称之为『组合』。
Regularjs与React类似 是高度推崇组合的框架, 所以在我们的业务代码中,一个业务组件可能是多个组件的组合
在MDV技术中, 组合是一种较高级的解耦方式, 它使得模板层也能得以复用,可以避免不必要的组件继承。
好了,时间关系,我们把重点放在第二部分, 先自卖自夸下Regularjs的特点来结束第一部分吧。
自定义语法拥有独立的解析流程,相较于DOM-based模板有更强的描述能力
无依赖,只有20kb gzip,甚至比同量级的avalon与vue更小些
绝对的安全性,不依赖innerhtml. 相较于dom-based 和string-based的最大优点
以组件为核,每个组件有独立的生命周期,更强的可控性
完善的组件声明式组合能力
精巧的声明式队列任务(一般用于动画)支持
与ng 类似,基于脏检查,熟悉的概念可以用于Regularjs
指令、事件、双向过滤器、计算字段等实用基础配套
兼容低版本IE(当然可以无视这条)
有兴趣的同学可以关注下这个项目: . Regularjs的功能层级接近于市面上的Vue和React,都是着眼于组件层的框架.
(接下来的内容可能和Regularjs没啥直接关系, 但是我觉得应该会是大家真正感兴趣)
◆ ◆ ◆
二、 Regularjs设计与选型之路
Regularjs正是一种典型的MDV技术(model-driven-view:即 模型驱动视图),一般MDV都会包含两个重要部分:
1. 模板技术
2. 数据监控技术
模板技术
在MDV实现中,模板技术是非常重要的一环. 它除了描述view层结构,也负责从例如`<h2> {blog.title}</h2>` 这种标记提取出绑定关系,结合数据监控部分 ,实现视图动态更新。
我14年末在前端乱炖发过一篇相关文章《 一个对前端模板技术的全面总结》(链接: ),收到了不错的反馈,这里我对内容做一下简单回顾。
前端模板技术发展到现在,主要会有三个分类
String-based
Dom-based
Living Tempting
#### String-based模板技术
如dustjs、dot、handlebars 等都是典型的字符串模板,但它们在编译阶段有所区别,重型的实现(如handlebars)会提供中间AST,以提供对输出的更强大控制力,不过它们最终暴露给开发者调用的都会是一个接受数据的预编译函数。
早在几年前,这一领域的竞争已经很焦灼了,才会诞生出dot.js 这样的性能卓绝的实现。但是由于一些本质性的缺陷,使得字符串模板并不能覆盖前端领域的所有开发需求。业务中我们选择一个语法够用、实现并不那么糟糕的轮子即可,而无需在性能上做太多的纠结,经过这么多时间的竞争,只要不是闭门造车,大部分性能问题都已经被规避了。
#### Dom-based Templating
DOM-based模板,会通过浏览器解析获得『原始DOM结构』充当『AST』的职责,框架内部一般会有一个『bootstrap』的流程,它会用标准DOM API 递归遍历这些节点,让DOM节点与ViewModel关联生成所谓的Living DOM. 简要代码可能如下所示。
在这过程中必然会有DOM的读写操作,会导致内容闪动(即FOUC:Flash of unstyled content)。
这种技术选型的关键问题在于:『解析』实际上是浏览器完成的,框架本身无法参与其中,这将导致几个问题:
依赖innerHTML,这个部分请参考我的博文
语法强依赖xml ,不够灵活
现代浏览器的解析非常宽容,其实会带有一些强大的『修复功能』。可能出来的结构和预期是不一致的。比如table、select等对于内部节点都是有需求的。
无法天然的实现Server Side Rendering.
更关键的是『使用DOM作为语法结构的容器是明显冗余的』. 我了解到Vue似乎在首次compile之后,会生成一个类似AST的结构来承载信息, 那何不从开始就抛弃 浏览器解析 直接生成AST呢?
#### Living Template
Living Template 其实就是 Dom-based(掐头) +String-based(去尾) 的杂交,
- 在语法上它是一种自定义DSL,有自己的解析流程(不会使用innerHTML),使用更加轻量且『DOM无关』的AST来代替Dom来作为中间结构,这部分是可以被持久化复用的。
- 而在编译阶段,它不输出字符串,而是直接输出与DOM-based 一致的『Living DOM』来实现动态更新的功能
而更有意思的是,在compile的过程中,构建真实DOM树的流程其实是接近React的,都是使用标准DOM API来手动构建DOM树,这保证了它本质上的安全性。我们可以用一个简单的图来描述与字符串模板的不同.
目前市面上这种选型的技术方案较少, 但是我仍然觉得有其存在意义,而暂时的接受度不高,我觉得主要还是目前React、ng以及 Vue风头正劲,而Living Template在普遍使用场景上和这些技术方案是重合的,导致了对这种技术方案的忽视。
#### 模板技术总结
如上所述,其实是没有银弹的,技术选型才有其必要性。
#### Regularjs与同类对比
对于不同类型的对比已经很明显了。我们拿Regularjs与同属于活动模板技术的Ractivejs进行对比, htmlbar属于Emberjs的框架级配套,我们不予对比.
Regularjs体量更小,压缩后57kb(未gzip) . 只有后者的1/3大小 (相同功能层级)
Regularjs从富逻辑的模板语法( jst )修改而来,天然满足动态模板对于富表达式的依赖
Regularjs基于脏检查,直接操作『裸』数据即可。
Regularjs有更轻巧的、但控制力更强的 序列(动画)支持
支持低版本IE算么?虽然我不好意思讲
Regularjs的组件组合功能支持得更轻量且彻底
据尤雨溪说 Ractivejs本身已经是基于virtual dom 实现,但这个是属于后续的数据监控层面. 从模板描述层面看,它仍然属于Living Template.
#### 关于React
在这里,我没有提到React, 实际上,在render方法中描述virtual-dom的这一过程我们也可以称之为是一种模板技术,但是这种技术与上面三种的巨大不同就是: 无需分析标记 ,因为它无需构建出数据与实际dom之间的映射关系(实际上也无法分析),而只需要做好virtualdom 到 实际dom的映射。
好了, 我们到了第二部分,数据监控。
数据监控层是用来做什么的呢? 就是用来帮助实现『当数据发生变化时, 我要做什么』的需求。
推广到`<h2>{blog.title}</h2>`这个插值,代码就相当于是
```js
var h2 = document.createElement('h2');
this.$watch(‘blog.title', function(title){
h2.textContext = title;
})
```
我们通过$watch来监听我们感兴趣的数据, 并在数据变化时执行指定的操作,退到我们刚才提到的模板分析部分,它会帮我们提取出binding信息,并最终通过数据监听层实现Living Dom的输出。 它解决的的关键就是『如何监控数据发生改变了?』
接下来,我会用尽可能通俗的方式来给出现在市面上常见的5种解决方案,它们分别是
Object.defineProperty
Object.observe
Accessor Function Wraping
Dirty Check - ModelLayer
Dirty Check - ViewLayer
##Object.defineProperty
框架范例:Vuejs 、 Avalonjs (VB黑科兼容IE9-)
简单原理:
Object.defineProperty(obj, 'a', {
set: function(value){
this._a = value;
console.log('字段a发生改变了,值为' + value)
},
get: function(){
return this._a
}
})
obj.a = 1// console输出” 字段a发生改变了,值为1"
```
即通过覆写对象obj 的setter 与getter, 我们成功获得了这个消息通道. 通过模板解析,我们从类似 `<h2>{{a + b}}</h2>` 标记 ,提取出表达式依赖为a 与 b , 每当 a 或 b 发生改变, 我们就去计算a+b的值,决定是否要更新这个插值.
###优点
浏览器支持度较好( IE9+)
大部分场景,直接操作『Plain』 Object 即可
性能上更高效的模型,直接可知在何时在哪个路径发生的数据改变
###缺点
对于删除和增加的字段我们是无从知晓的
并不是所有表达式都与 a + b 一样 是可以提取依赖的
对于Array类型, Object.defineProperty 无法解决
对于深层对象的赋值,我们需要小心以避免破坏引用关系
当然对第三点 , 我们可以通过Monkey Patch的方式来 部分解决 ,比如对于一个数组,我们可以改写其push 方法
```
var a = []
var oldPush =Array.prototype.push
a.push =function(item){
console.log(‘推入新的字段: ‘ + item)
oldPush.apply(this, arguments)
}
// ….其它方法类似
```
注: 当然也有会框架会使用 __proto__ 这个非标准的属性来避免多次覆写.
这种方式也并不完美
Array的API的在不断增加,这种类似 开『白名单』的方式 其实是不够鲁棒的.
无法支持下标赋值,如blogs[0] = 1这个简单但频繁的需求,所以一般都会提供一个$set函数
虽然列了很多缺点,但是瑕不掩瑜,基于Object.defineProperty的框架能让开发者直接感受到便利性,所以也被广泛采纳。
##Object.observe[DEAD]
框架范例: 早先版本的Polymer
简单原理:
```
var obj = { blog: { } };
Object.observe(obj, console.log.bind(console))
// 这三个改动只触发一次回调
obj.user = '@拴萝卜的棍子';
obj.user = '@leeluolee';
delete obj.user;
// 深层赋值这条不会生效
obj.blog.title = '标题Vk2'
setTimeout(function(){
// 触发另外一次回调
delete obj.blog;
},0);
```
输出:
Object.observe解决了一些Object.defineProperty的痛点, 比如对增删字段的响应. 同时我们也无需对所有的字段逐个defineProperty
但它也有一些不可忽视的缺点
目前已从草案退回
并不能解决深层对象的赋值问题如`obj.blog.title`
对象实际上已经是响应式对象,有defineProperty 的类似问题, 即你得小心赋值,避免改写对象引用
是『异步』的, 但异步也是它内部将同步操作得以batch优化的基础 如上例将三个操作合并为一次事件
虽然O.o已死,但是ES新规范中的『Proxy』可以在对象监控中提供有更强的控制力,如
```js
var handler = {
set: function (target, key, val) {
// 判断此字段是否已经在对象中
if (!(key in target)) {
console.log('新建字段 key:' + key , val )
}else{
console.log('修改字段 key:' + key , val )
}
// 使用反射API Reflect 调用默认行为
return Reflect.set(target, key, val);
},
deleteProperty: function(target, key){
console.log('删除字段 key:' + key)
return Reflect.deleteProperty(target, key);
}
};
var obj = { a: 1};
var proxy = new Proxy(obj, handler);
proxy.a = 2; // 输出 "修改字段 key:a 2"
proxy.b = 3; // 输出 "新建字段 key:b 3"
delete proxy.a; // 输出 "删除字段 key:a"
```
了解过Ruby的同学可能会知道其中神奇的method_missing,它可以代理不存在的方法,但是ES6中Proxy更强,任何对代理对象的操作都可以被代理。但它其实并不太适合出现在MDV框架中,因为它会返回全新的对象,这将导致我们想操作『plain Object』成为不可能.
## Accessor Function
自定义模型层,并提供set、get函数来代理对被监控数据的操作.
框架范例:Backbone 、 Ractivejs、Emberjs
简单范例:
function Model(){
this._state = {}
}
Model.prototype.set =function(path,value) {
this._state[path] = value
console.log(path + '发生改变')
}
Model.prototype.remove=function(path,value ) {
delete this._state[path];
console.log(path + ‘被删除')
}
Model.prototype.get=function(path) {
return this._state[path]
}
基于Accessor Function的一般都会有自己真实的Model层, 与基于Plain Object的框架相比,它的这一层抽象会导致数据操作上的不便利性,我必须用类似 `a.set(‘key’ , value)` 来代替 `a.key =value`的操作。但反过来说这一层也可以持有更多的职责, 比如一些框架中,通常会将其与服务端的数据同步封装于此,是把典型的『双刃剑』。
## Dirty Check (ModelLayer)
代表框架:Regularjs , Angularjs
基本原理:
基于脏检查的框架,在进行每次watch时,Observer都会在栈内推入一个观察者对象,隐藏了很多细节的实现如下所示:
如上图所示,每次调用digest检查阶段,就会遍历这些对象,对比前后求值对比,来判断值是否发生改变。所以绑定越多,效率会越低下.
由于基于数据层脏检查的框架一般都是直接操作裸数据,导致无法直接获得数据发生改变的信息。所以这些框架一般会『内建生命周期』,在框架层主动的触发digest . 比如在dom事件触发后, 内建http服务返回时等等. 这个对于在框架生命周期外的操作,用户就需要手动的进行digest 触发, 这也是脏检查除了性能之外被广泛诟病的部分.
所以ng2现在引入了zone.js(github:) ,让开发者可以几乎不关心digest这个操作. 实现原理么, 其实就是Monkey Patch了所有可能异步的操作,比如setTimeout,setInterval等等.
## Dirty Check (ViewLayer)
代表框架:React
有人说,React不也是 Accessor Function 吗?React实际上只是披着Accessor Function 外衣的 Dirty Check. 以深层赋值`blogs[0].title`这类深层对象为例 , 你需要这么做
```js
var blogs =this.state.blogs;
blogs[0].title= ‘新的标题'
this.setState({blogs:blogs})
```
『所以setState并不关心赋值路径,仅仅只是通知内部需要更新的标志, 它的作用本质上和ng或Regularjs的$digest 没有太大区别』
基于vd的框架一般会有三个阶段
create : 即我们在render中所返回的)
diff : 脏检查,检查我们在前后两次render返回的virtual dom树
patch : 在diff结果映射到实际DOM中)
在第二阶段,其实React内部会进行一次树的diff,由于只进行同级节点的对比并且每个组件节点都可以通过Hook的方式来决定是否进行更新,所以是可以达到不错的性能控制的。
这里非常推荐大家去使用/virtual -dom]()做个DEMO, 可以帮助熟悉整个基于virtual-don的 的 流程, 因为在使用jsx的前提下, 这三个重要流程其实都被React隐藏了起来,对于我们深入理解React是个巨大的瓶颈.
React优劣都在于他没有binding的存在, 可以梳理数据流程,使得可以以接近全页刷新的开发体验来完成富逻辑的应用。它还有个非常大的优势就是,其实它就是JS。 你可以使用编程语言里的一些模式来封装它. 而模板不一样,它和js层有天然的隔离, 所以我们需要一个vm层来链接它们,形成一系列binding.
## 数据监控汇总
各种刚才的简单汇总, 我们发现数据监控其实是提供了一种消息机制。 我们可以很清晰总结出,上面五花八门的解决方案,其实只有截然不同的两种模式: 推( PUSH ) 和 拉(PULL) . 这两种消息机制其实在软件工程领域应用非常普遍,特点也很明显。
### 推 ( PUSH )
响应发生在数据变化的同时,在设值的同时,监控系统已经知道什么数据发生改变. 比如 Object.defineProperty, Accessor functionWraping, Object.observe 等.
它们的共性就是,数据层其实是『有状态的』,当你改变了原来对象的引用,响应对象也就失效了, 所以这些框架很难直接与第三方数据层框架(如Redux)直接集成。同时在模板中解析表达式时,它们需要进行依赖收集,来实现更新。
### 拉 (PULL)
这种监控系统有一般会有一个『脏检查机制』 ,去主动『拉取』数据是否发生改变的信息。这种框架的优势是,模型层是100%无状态的,不需要去分析表达式的依赖关系, 所以可以支持任意复杂的表达式,并且可以无缝集成于第三方数据层框架(如Redux). 比如我们在网易有数的开发中,应用了Redux来管理我们的数据,实现了基于mvvm框架的单向数据流。
### 结合两者
我们发现, PUSH和PULL两者的优缺点非常明显, 特性上简直不共戴天, 大家其实很容易可以根据自己可以接受的缺陷来进行技术选型。有没有结合两者的实现呢?
结合了 PULL 和 PUSH两者,本身通过Object.defineProperty, 但通过脏检查去检查那些无法被提取依赖的字段, 比如计算属性。有兴趣的可以去关注下,和ng2有莫大的渊源。但我个人认为其实是一个因小失大,引入额外复杂度的做法, 不如与Vue一样,将可能失效的情形在文档阶段就知会开发者。
## 完整汇总
同样的不同的类型的差异是本质性的,大家自己判断。在同类实现中,我会跟Angular做个对比。
小: 当然这也意味的提供的功能更少,Regularjs 只是一个view层框架。
组件化设计,且一个组件只有一个vm,无论内部创建了几层列表循环
可提供server side rendering支持
以及所有上面提到的 『DOM-based』 VS 『LivingTemplate』 的对比
更重要就是Regularjs在设计初期就是在思考如何在网易杭研已稳定使用多年的框架下, 进行无缝的集成,这也是它在公司内能快速落地的基础所在。所以市面上诸如React等声称自己是类库的,其实它在侵入性方面会更强一些。 Regularjs属于典型的三无产品, 对模块系统无要求, 对build工具无要求、对模型层无定义,保证了它的无侵入性。
## 总结
与上面的模板技术一样, 没有银弹, 只是取舍而已, 这也是Regularjs初期 号称自己是 React(Ractive) + Angular, 这仅仅只是代表在模板技术和数据监控技术的选型上分别与它们有一定的相似度,但是组合之后其实是另一种实现了。
◆ ◆ ◆
三、未来的Regularjs
我们对于Regularjs的未来发展目标: 在保持高开发效率的同时能做到对用户体验的兼顾。
这可能包含几个方向:
同构的开发,与React类似, 只是Regularjs这种基于模板描述的无需在你的节点上加入一大堆react-id 也不会改变你的文本为span标签, 因为从AST里可以拿到实际的数据与模板的对应。
思考如何更好的引入动画,通常我们使用mvvm框架经常会忽视用户体验。因为数据操作往往是离散的,而动画是时间相关的连续赋值(而我们关心的往往是动画结束后的结果),两者不太容易结合。
性能的优化: 理论上讲,脏检查在性能上回差一些。但大家可以对比 一些常见的框架在特定case下的表现: 。其实规避了一些坑之后,性能其实普遍不会差异太大, 加上Regularjs本身可以控制组件的隔离性,是可以达到对性能的绝对控制的。
# 总结
Regularjs是 基于 『动态模板技术』 和 『脏检查』的 MDV技术的实现, 它着眼于简化数据驱动型组件的开发。它特别适合这种同学:
熟悉Angular,对于脏检查有一定理解
轻量级的组件化需求
熟悉字符串模板的同学
今天讲的东西原理性很强,可能有些枯燥, 但是我认为可能是大家在其它途径获取不到的。最后感谢大家,抽出宝贵的时间来参与我的这次在线分享。
1、问下模版引擎如何监听渲染完成事件?
郑海波:框架一般会分为两种: 1) 同步渲染 2) 异步渲染 。 基本所有字符串模板都是同步渲染,所以不存在监听渲染完成的需求 。 但是部分框架 ,比如基于Object.defineProperty的,出于性能的考虑,会将数据变化batch起来,一并渲染到view中,这个需要框架内部提供事件。
2. 能问波神个八卦问题吗?regularjs为啥是MVC模式,而不做成MVVM模式?
郑海波:是属于MVVM模式的,每个组件都相当于一个mvvm系统,有独立生命周期。
3. 比如我需要获取模版中的某个节点但是模版渲染可能会耗费部分时间,是否有回调支持或者事件坚挺?
郑海波:DOM API是同步的, 如果你碰到异步的应该是框架内部操作的。 你可以试下使用setTimeout(fn , 0) 或许可以。
4. 我有一个页面节点比较多,不需要判断数据变化,只是在窗口变化时需要对每个节点的margin作变化,有什么建议?直接修改style、或者修改className还是别的方法?
郑海波:监听resize事件, 直接修改你的数据并映射到节点的margin上。 注意响应做好throttle处理, 因为resize触发很频繁。
5. pull 和 push 结合的方式有哪些优缺点?regularjs未来是否会考虑这种方式?
郑海波:不会考虑这种方式, 但是会考虑结合zone.js。 目前已经有DEMO在使用zone.js。
6.regularjs、妹子ui都类react,是不是比较看好react的前景?
郑海波:Regularjs和妹子UI 可能一点关系都没有。React优劣都在于他没有binding的存在, 可以梳理数据流程,使得可以以接近全页刷新的开发体验来完成富逻辑的应用。它还有个非常大的优势就是, 其实它就是JS。 你可以使用编程语言里的一些模式来封装它。而模板不一样,它和js层有天然的隔离, 所以我们需要一个vm层来链接它们,形成一系列binding。但React不是银弹,它和其它基于mvvm的技术 不是颠覆关系。 但它确实颠覆了Backbone这类老式框架。
7. 想问一下,Regular和Redux是怎么结合的?
郑海波:引入Redux之后, 原本独立解耦的组件体系 会 分离成 全局组件和独立组件两类。全局组件负责从全局store获取数据,并在store 变化时, 进行主动 digest. 这个涉及的问题很多, 可能没时间在短时间说清楚。
8、如果配合后端渲染大量使用regularjs组件,而且需要关注seo,这类场景应该怎么使用regularjs,命令式的方式是不是就不合适了?
郑海波:下个小版本会放出 render API , 可以在服务端生成字符串 , 到达浏览器后,使用相同数据可以将这个组件从静态结构还原成动态组件。但是前提是服务端必须是nodejs。
◆ ◆ ◆
关于ITA1024
以上是关于分享实录前端框架Regularjs的设计与选型之路,网易杭研技术专家在ITA1024前端精英群的分享的主要内容,如果未能解决你的问题,请参考以下文章
实录分享AngularJS框架进阶:如何用Angular写界面
阿里支付宝前端团队负责人分享移动开发框架实践支付宝前端发展之路
10月11日IC咖啡上海站叮咚小区主办:阿里支付宝前端团队负责人分享移动开发框架实践支付宝前端发展之路