微信扫描登录

Regularjs设计与选型之路

鉴于听众都是一定工作经验的同学。今天主要会以分享设计思路为主,安利为辅。希望对大家的技术选型有所帮助。也希望可以解答一些朋友关于『为什么有那么多框架技术冒出来的困惑,在技术选型中做到『知其然且知其所以然』。 最后,当然欢迎大家提一些尖锐的问题。

我叫郑海波,来自网易杭州的前端技术专家,自诩为前端DSL的重度爱好者,热衷于挖掘流行技术的本质。

我负责或参与过公司的易信、秀品、相册等产品的前端开发,与此同时我也是网易前端的培训讲师之一,并在公司维护了一些应用较广泛的框架工具,目前我在负责网易有数这款BI类新产品的前端开发。

• Github: @leeluolee • weibo: @拴萝卜的棍子

关于我目前参与的产品,我来个简单的硬广: 『网易有数—让数据推动决策』,有兴趣的可以用桌面浏览器访问下这个https://youdata.163.com/ 。 目前仍在内测阶段,大家可以看下介绍视频了解下。

今天分享主要是以下三个部分

  1. Regularjs简介
  2. Regularjs设计与选型之路
    • 模板描述部分
    • 数据监控部分
  3. Regulars的未来展望

    Regularjs是什么

『Regularjs是基于动态模板实现的用于构建数据驱动型组件的新一代框架』, 大家如果看了刚才的有数的产品介绍视频,会发现它有别于那些传统的内容为主的互联网产品,它具有一定的业务复杂性,事实上目前前端业务代码接近了4W行, 能在短时间的开发中 Hold这种量级的开发,至少可以证明它并不是一个玩具,也可以从侧面反映今天内容的含金量

目前Regularjs也广泛应用于网易包括考拉、易信、秀品、教育产品部、云计算、URS、质保部、前端/前台、人力资源、网站部等部门的产品线中。

Github仓库: https://github.com/regularjs/regular 指南: http://regularjs.github.io/guide/zh/index.html

历史

有句话说的好: 『你永远无法叫醒一个装睡的人』, 在程序圈『你永远无法阻止一个想造轮子的人』。 所以产生的根本原因就是,我觉得 该写个框架了 :) 。13年负责易信前端时,一个需要实时同步复杂编辑状态的业务所迫,基于NEJ框架(http://nej.netease.com/)封装了一个mini版的MVVM实现,解放了生产力。而契机在于Leader也意识到需要一个动态模板,所以有了宝贵的时间去调整这一套方案。

从最初的是做一个纯粹的模板来解决局部更新的问题,一步步催生了Regularjs这个名字超山寨的组件框架。

附一张图自黑图 :(

enter image description here

简单范例

我直接抛一段代码 (ES6)

enter image description here

2.1 命令式的调用(JS)

enter image description here

2.2 声明式 ( 模板 )

enter image description here

几个简要说明:

  • Regularjs组件提供命令式和声明式两种使用方式

当然声明式只是一种高级的接口模式,本质和命令式调用是一致的。由于一个组件只暴露数据、事件,实际上在以命令式调用你的组件时, 其它开发者可以完全对Regularjs的模板不知情,只需要使用就行了,所以支持命令式调用是无侵入性的基本要求。

  • 事件统一以 on-* 作为标示, 无论是组件事件还是DOM事件

  • 其中this.$body 代表 代表被标签包裹的内容. 当然在命令式调用时,你也可以传入$body这个实例属性。这种语句的用法也可以称之为『组合』。

Regularjs与React类似 是高度推崇组合的框架, 所以在我们的业务代码中,一个业务组件可能是多个组件这个样子

enter image description here

在mdv技术中, 组合是一种较高级的解耦方式, 它使得模板层也能得以复用,并避免不必要的组件继承。

好了,时间关系,我们把重点放在第二部分, 先自卖自夸下Regularjs的特点来结束第一部分吧。

Regularjs的特点:

  1. 自定义语法拥有独立的解析流程,相较于DOM-based模板有更强的描述能力
  2. 无依赖,只有20kb gzip,甚至比同量级的avalon与vue更小些
  3. 绝对的安全性,不依赖innerHTML. 相较于dom-based 和string-based的最大优点
  4. 以组件为核,每个组件有独立的生命周期,更强的可控性
  5. 完善的组件声明式组合能力
  6. 精巧的声明式队列任务(一般用于动画)支持
  7. 与ng 类似,基于脏检查,熟悉的概念可以用于Regularjs
  8. 指令、事件、双向过滤器、计算字段等实用基础配套
  9. 兼容低版本IE(当然可以无视这条)

有兴趣的同学可以关注下这个项目: https://github.com/regularjs/regular . Regularjs的功能层级接近于市面上的Vue和React,都是着眼于组件层的框架.

(接下来的内容可能和Regularjs没啥关系, 但是我觉得应该会是大家真正感兴趣)

MDV技术纵览: Regularjs设计与选型之路

MDV(model-driven-view) 即 模型驱动视图技术。第二部分是关于我在实现过程中在,其中会包括两个部分

  1. 模板技术
  2. 数据监控技术

模板技术

在MDV实现中,模板技术是非常重要的一环. 它除了描述view层结构,也负责从例如<h2> {blog.title}</h2> 这种标记提取出绑定关系,结合后续的数据监控部分 ,实现后续的视图动态更新流程。

我14年末在前端乱炖发过一篇相关文章《 一个对前端模板技术的全面总结》(链接: http://www.html-js.com/article/Regularjs-Chinese-guidelines-for-a-comprehensive-summary-of-the-front-template-technology ),受到了不错的反馈。这里我简单的回顾一下。

前端模板技术发展到现在,主要会有三个分类

  • String-based: 如mustache 等
  • Dom-based: 如Angular
  • Living Tempting: 如Regularjs

我们先来了解下这三个分类:

String-based模板技术

enter image description here

字符串模板输入 比如dustjs、dot、handlebars 等都是典型的字符串模板,它们在编译阶段有所区别,重型的实现会提供中间AST输出以达到更加灵活的控制,但一般最终输出的都会是一个接受数据的函数。 早在几年前,这一领域的竞争已经很焦灼了,才会诞生出dot.js 这样的性能卓绝的实现。但是由于一些本质性的缺陷,使得字符串模板并不能覆盖前端领域的所有开发需求。事实上我们在日常上选择一个语法够用实现并不那么糟糕的轮子即可,而无需在性能上做太多的纠结,经过这么多时间的竞争,只要不是闭门造车,大部分的性能问题都已经被规避了。

Dom-based 介绍

enter image description here

(这里会有简单介绍)

DOM-based模板,会通过浏览器解析获得『原始DOM结构』充当『AST』的职责,然后呢,框架内部会通过一个bootstrap的流程来进行”compile”的过程. 这个流程已经用以下代码片段来解释。

enter image description here

这种技术选型的问题在于,『解析』实际上是浏览器完成的,框架本身无法参与其中,这将导致几个问题:

  1. 浏览器的解析非常宽容,其实会带有一些强大的『修复功能』。如果不满足的可能出来的结构和预期是不一致的。比如table、select等对于内部节点都是有需求的。
  2. 无法天然的实现Server Side Rendering的.
  3. 内容闪动, 当然通过编码是可以优化这一现象

Living Template

enter image description here

Living Template 其实就是 Dom-based(掐头) + String-based(去尾) 的杂交,

  • 在语法上它是一种自定义DSL,有自己的解析流程(不会使用innerHTML),使用更加轻量且『DOM无关』的AST来代替Stateless Dom来作为中间结构
  • 而在编译阶段,它不输出字符串,而是直接输出与DOM-based 一致的『Living DOM』来实现动态更新的功能

而更有意思的是,在compile的过程中,构建真实DOM树的流程其实是接近React的,都是使用标准DOM API来手动构建DOM树,这保证了它本质上的安全性。我们可以用一个简单的图来描述与字符串模板的不同.

enter image description here

目前市面上这种选型的技术方案较少, 但是我仍然觉得有其存在意义,而暂时的接受度不高, 我觉得主要还是目前React、ng以及 Vue风头正劲,而Living Template在普遍使用场景上和这些技术方案是重合的,导致了对这种技术方案的忽视。

分类总结

enter image description here

如上所述,其实是没有银弹的,技术选型才有其必要性。 但是从使用场景来看,dom-based和living template技术的解答域是一样的,大家从两者挑选一个趁手的即可。

同类对比

对于不同类型的对比已经很明显了。我们拿Regularjs与同属于Ractivejs进行对比, htmlbar属于Emberjs的框架级配套,我们不予对比.

  • Regularjs体量更小,压缩后57kb . 只有后者的1/3大小 (相同功能层级)
  • Regularjs从富逻辑的模板语法( jst )修改而来,天然满足动态模板对于富表达式的依赖
  • Regularjs有更轻巧的、但控制力更强的 序列(动画)支持
  • 支持低版本IE算么?虽然我不好意思讲
  • Regularjs的组件组合功能支持得更轻量且彻底

当然版本其实一直在更迭, 我说的可能并不满足

关于模板技术的一些补充说明.

弱逻辑还是富逻辑

富逻辑还是弱逻辑,其实模板技术和语法无关,而是首先体现在表达式的支持上,mustache这个模板有大量的拥簇者,但是它是个弱逻辑的模板。 对于弱逻辑模板,我们在渲染之前,我们需要进行一些数据的预处理器,就比如

enter image description here

而动态模板为何因为动态模板不是一次性的产生,如果将复杂表达式需要转化为简单对象 而这一且需要传入到模板框架内部,才能被框架所感知 问题发生在 ,在动态模板中, 由于数据与view是长期响应(reactive)的关系, 这要求有表达式的存在来简化数据模型的操作.

关于React

在这里,我没有提到React, 实际上,在render方法中构建virtual-dom的这一过程我们也可以称之为是一种模板技术,但是这种技术与上面三种的巨大不同就是: 无需分析标记(实际上也无法分析 ,因为它无需构建出数据与实际dom之间的映射关系)。

数据监控层

数据监控层是用来做什么的呢? 就是用来帮助实现 『当数据发生变化时, 我要做什么』的需求。

推广到<h2>{blog.title}</h2> 这个插值,代码就相当于是

var h2 = document.createElement('h2');

this.$watch(‘blog.title', function(title){
  h2.textContext =  title;
})

我们通过$watch来监听我们感兴趣的数据, 并在数据变化时执行制定的操作,退到我们刚才提到的模板分析部分,它会帮我们提取出很多的数据监听,并最终通过数据监听层帮我们实现Living Dom的输出。 它解决的的关键就是『如何监控数据发生改变了?』

接下来,我会用尽可能通俗的方式来给出现在市面上常见的5种解决方案

  • Object.defineProperty
  • Object.observe
  • Accessor Function Wraping
  • Dirty Check - Model Layer
  • Dirty Check - View Layer

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, 我们成功获得了这个消息通道. 通过上层模板的 类似 {{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);

输出

enter image description here

Object.observe解决了一些Object.defineProperty的痛点, 比如增删字段的响应. 同时我们也无需对所有的字段逐个defineProperty

但它也有一些不可忽视的缺点

enter image description here

  • 并不能解决深层对象的赋值问题如obj.blog.title
  • 对象实际上已经是响应式对象,有defineProperty 的类似问题, 即你得小心赋值,避免改写对象引用
  • 是『异步』的, 但异步也是它内部将同步操作得以batch优化的基础 如上例将三个操作合并为一次事件

虽然O.o已死, 但是ES新规范中提供的Proxy可以在对象监控中有更强的控制力(但并不适合使用在MDV框架中),我们就不做衍生了。

enter image description here

Accessor Function

框架范例: Backbone 、 Ractivejs、Emberjs 简单范例:

function Model(){ this._state = {} }

Model.prototype.set =function(path,value ) { this._state[path] = value console.log('数据发生该表了') } Model.prototype.get =function(path) { return this._state[path] }

基于Accessor Function的一般都会有自己真实的Model层, 与基于Plain Object的框架相比,它的这一层抽象会导致数据操作上的不便利性,但同时这一层可以持有更多的职责, 比如一些框架中,通常会将其与服务端的数据同步封装于此,是把标准的『双刃剑』。

有人说,React 不也是 Accessor Function 吗?React实际上只是披着Accessor Function 外衣的 Dirty Check. 以深层赋值blogs[0].title这类深层对象为例 , 你需要这么做

var blogs = this.state.blogs;
blogs[0].title = ‘新的标题'
this.setState({blogs: blogs})

『所以setState并不关心赋值路径, 仅仅只是通知内部需要更新的标志, 它的作用本质上和ng或Regularjs的$digest 没有太大 区别』

Dirty Check (View Layer)

代表框架: React 原理解析:基于脏检查的框架,在进行每次watch时,Observer都会在栈内推入一个观察者对象,隐藏了很多细节的实现如下所示:

enter image description here

React优劣都在于他没有binding的存在, 『Full-refresh』的流程, 可以简化我们的思路,使得可以以接近全页刷新的开发体验来完成富逻辑的应用。

中期汇总

各种刚才的简单汇总, 我们发现数据监控其实是提供了一种消息机制。 我们可以很清晰总结出,上面五花八门的解决方案,其实只有截然不同的两种模式: 推( PUSH ) 和 拉( PULL) . 这两种消息机制其实在软件工程领域应用非常普遍,特点也很明显。

推 ( PUSH )

响应发生在数据变化的同时,在设置的同时,监控系统已经知道什么数据发生改变. 比如 Object.defineProperty, Accessor Wraping, Object.observe 等.

它们的共性就是,数据层其实是『有状态的』,比如你通过defineProperty 激活了这个对象,。。。

拉 (PULL)

监控系统在每个时间点(框架内的生命周期), 在, 拉的方式的限制在于在监控系统尝试拉取(mvvm中,去,而React则是拿到render的结果),它并不知道具体哪个发生了变化, 所以一般需要有一个内部的脏检查机制。

完整汇总

enter image description here

同样的不同的类型的差异是本质性的,大家自己判断。在同类实现中,我会跟Angular做个对比。

小: 当然这也意味的提供的功能更少,Regularjs 只是一个view层框架。 组件化设计,且一个组件只有一个vm,无论内部创建了几层列表循环 可提供server side rendering支持 以及所有上面提到的 『DOM-based』 VS 『Living Template』 的对比

更重要就是Regularjs在设计初期就是在思考如何在 网易杭研已稳定使用多年的框架下, 进行无缝的集成,这也是它在公司内能快速落地的基础所在。所以市面上诸如React等声称自己是类库的,其实它在侵入性方面会更强一些 。 Regularjs属于典型的三无产品, 对模块系统无要求, 对build工具无要求、对模型层无定义,保证了它的无侵入性。

总结

与上面的模板技术一样, 没有银弹, 只是取舍而已, 这也是Regularjs 初期 号称自己是 React(Ractive) + Angular, 这仅仅只是代表在模板技术和数据监控技术的选型上分别与它们有一定的相似度, 但是组合之后其实是另一种实现了。

未来的Regularjs

我们对于Regularjs的未来发展目标: 在保持高开发效率的同时能做到对用户体验的兼顾。

这可能包含几个方向:

同构的开发,与React类似, 只是Regularjs这种基于模板描述的无需在你的节点上加入一大堆react-id 也不会改变你的文本为span标签, 因为从AST里可以拿到实际的数据与模板的对应。 思考如何更好的引入动画, 通常我们使用mvvm框架经常会忽视用户体验。因为数据操作往往是离散的,而动画是时间相关的连续赋值(而我们关心的往往是动画结束后的结果),两者不太容易结合。 性能的优化: 理论上讲,脏检查在性能上回差一些。但大家可以对比 一些常见的框架在特定case下的表现: http://mathieuancelin.github.io/js-repaint-perfs/ 。其实规避了一些坑之后,性能其实普遍不会差异太大, 加上Regularjs本身可以控制组件的隔离性,是可以达到对性能的绝对控制的。

总结

Regularjs是 基于 『动态模板技术』 和 『脏检查』的 MDV技术的实现, 它着眼于简化数据驱动型组件的开发。它特别适合这种同学:

熟悉Angular,对于脏检查有一定理解 轻量级的组件化需求 熟悉字符串模板的同学

今天讲的东西原理性很强,可能有些枯燥, 但是我认为可能是大家在其它途径获取不到的。最后感谢大家,抽出宝贵的时间来参与我的这次在线分享。

Q&A

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。