function bindClass($el, expr) {
var $el = $(el)
var clazz = expr.split(':')[0]
var key = expr.split(':')[1]
// 返回一个更新方法
return function () {
var state = data[key]
if (state) $el.addClass(clazz)
else $el.removeClass(clazz)
}
}
初始化绑定(只示例单个元素)
var $el = $('[bindClass]')
var update = bindClass(
$el[0],
$el.attr('bindClass')
)
var vm = this
var tpl = require('list.tpl')
$.ajax(url, function (results) {
vm.$data.list.concat(results)
var el = vm.$compile(tpl)
vm.$el.appendChild(el)
})
前言
腾讯视频相关的前端项目,已逐步切换到组件化开发模式中,在业务也积累了丰富的实践经验,希望通过自身的理解与思考,向大家描绘前端组件化开发是怎么一回事。不是React,也不是Web Components,只是纯粹的组件化开发。
JS的交互逻辑
HTML是一门标签语言,通过它,我们以一种声明式的方法去描述页面,配合上CSS做样式上的修饰,即可得到一个静态的Web页面,我们一般称之为重构稿。
在前端开发流程中,通常,重构稿是由设计稿转换为线上产品这个流程中的一个上游步骤,在它之后,还需要使用JavaScript添加页面交互或者数据交互逻辑,该步骤我们暂且称之为JS施工。如果重构稿说一块平地,那么经过JS施工后,上面可能铺满了各种坑或标识,JS逻辑就是在交互事件触发时根据标识去填坑。
回到真实的场景,那么就是前端开发所熟悉的JS交互逻辑:
初始化
:(添加选择器) => (选中元素) => (绑定事件)交互时
:(选中元素) => (操作DOM)流程逻辑看上去没什么不对,所有的步骤也是完成对应交互所必需的,会有什么问题吗?所以接下来我们以“两性”作为入口分析下。
前端工程的“两性”
HTML提供了页面元素与结构的描述,JS负责修饰操作,所以需要给定选择器用以JS逻辑执行时定位到对应的元素,可以说选择器就是对一段或多段JS逻辑的绑定。
此类选择器代表了元素的名称,往往是没有语意的。在没有对逻辑进行抽象的前提下,是没法赋予选择器相对应的含意,也就不能简单通过选择器的名字来读懂背后的逻辑。
在逻辑绑定方面,如果选择器与逻辑是一对一的绑定,那是较为理想的状态,但是一对多的绑定,那就是场灾难。因为,多个逻辑由选择器作为约束而关联在一起了,好比关系型数据库中的一对多关系,例如下面这些代码片段:
HTML:
我们交互逻辑可能是这样子的(模块A):
还是这样的(模块B):
甚至是这样的(模块C):
和这样的(模块D):
最终:
友谊的小船说翻就翻...
这时候研发需要把所有与 “header” 相关逻辑删掉,像A,B这样纯粹只有“header”相关的逻辑可能会简单点,找齐全部相关模块注释掉(可维护性与干涉的模块数量成反比),但对于C,D这样结合了其它模块逻辑的,就需要仔细看次具体逻辑注释掉,还要保证不影响其它逻辑(可维护性与干涉的逻辑数量成反比),给跪了。
所以,要想提高可维护性,需要从减少干涉模块与减少干涉逻辑两方面入手来改善逻辑结构。
复用性是与开发效率休戚相关的,在开发一个前端项目的过程中,某些交互逻辑是可以被复用的,在被复用之前,我们需要完成抽象封装的工作。
在维护性的描述中提到了选择器的语意化,也就是要让选择器代表一段逻辑。如此一来,在开发的时候,只需要在HTML元素添加一个选择器标识,就可以复用一段逻辑。
以上面的模块A代码片段作为示例,现在我们要抽象一些逻辑:
A逻辑由两部分组成,
定时检查页面当前滚动的高度
根据高度状态修改DOM的样式
可以认为逻辑1是非DOM操作相关的,也就是说不需要通过选择器来声明使用,我们将它称为状态逻辑。逻辑2直接表达了DOM操作的强烈意愿,我们可以将它抽象为一种DOM操作逻辑,然后通过HTML属性声明使用:
背后绑定的实现逻辑流程:
Class操作绑定方法
这是声明式class操作的封装,绑定逻辑与及具体的DOM操作逻辑,这也就是我们希望被复用的逻辑:
在所有相同逻辑的DOM操作场景下,只需要通过在HTML上声明依赖数据,与及数据变更时触发更新,这两个简单的操作就完成了一个交互逻辑的复用。这样的逻辑表达方式我们称之为数据绑定,它就是一种DOM操作逻辑的声明式绑定。
数据绑定让“选择器”变得有语意,我们“举个栗子”:
对于上面的例子,我们可以清晰地读懂该选择器与表达式背后的含义。
还有,绑定声明所依赖的是一个数据,而数据逻辑也不依赖DOM,两者是解耦的,也就是说,我们可以轻松地注释掉HTML元素无需担心逻辑代码出错,反之,能随意迭代数据逻辑而不担心DOM结构。显然,从“减少干涉逻辑”指标上提高了可维护性
所以,我们可以说:
从上面的“两性”说明中,我们得到了好几个关于交互逻辑开发方式优化的指标:
数据绑定涉及到三个指标,但还有一个指标问题需要解决的,我们该怎样做,也就是如何减少交互逻辑所干涉的模块数量?
“减少干涉模块”那就需要分而治之,将属于你的任务交付给你,自己单独办法处理, 分治策略 Get√。
而具体点的行为就是:对相同HTML元素的交互逻辑放在相同的模块中,该模块的生命周期都是负责该HTML范围内的交互逻辑,这样的方式就是组件化的原型。
业界对于组件化的定义是比较含糊的,没有唯一标准,你可以认为Web Components 是一种组件化方式,React也是一种,又如我们团队所用的框架Real ,也是一种组件化方式。总的来说,组件化就是按照某种规范对相关交互逻辑的封装。
对于组件化,我们就这样简单明确地理解:
最终,组件的成分是:HTML模板与交互逻辑代码,在运行时环境就是:DOM元素与组件的JS实例。
组件化的实例
以上大篇幅介绍了组件化/数据绑定的原理与概念,但前端在开发的时候,直接打交道的是具体的JS库或者框架,是各种具体的接口方法,所以下文会以一个框架为例介绍组件化开发在应用层面的表现。
作为例子的框架:Real ,是我们团队在组件化开发模式的主力框架,gzip后体积 8kb ,还有一个不值一提的长处:兼容IE6+。
诞生背景是为了解决服务端渲染场景下的交互逻辑组件化问题,为此,它有部分接口是为了解决在前端运行时的组件反射问题。
所谓的组件反射:后端模板如果是按照组件组合方式吐出,那么前端运行的时候也是保持一致的,那么框架需要将静态的HTML片段转换为具体组件实例,转换的过程就是反射。
对于前端开发来说,需要关注的是组件定义,使用与交互,那么对应着组件化框架来看,就是构造,实例与更新:
构造
数据,模板,生命周期是组件的三大要素,在Real中,对应的就是 data, template, created/ready/destroy 这三类构造选项。
Real有三种构造组件的方法,每种的适用场景不一样:
Real.create( ) 与 Real.component( ) 的区别在于是否能被声明式使用,在接下来的实例的话题中会有描述。
下面示例使用Real构造一个具名的组件:
示例用于前端渲染,在运行时能访问实例对应根元素(this.$el),构造出来的组件是天然多例化的,只负责实例对应的DOM元素。
r-*
的属性标识是Real的数据绑定声明,如示例中的r-class
是Real的绑定class的声明。对应前文中的bindClass
Real用许多通用的内置的绑定声明,与Angular一样,绑定声明称为: directive,也可以通过Real.directive( )方法自定义directive。
实例
任何的组件都可以被其它组件使用,Real提供了两种不同的使用方式:
通过 data 传递数据给子组件(header); 通过 methods 传递回调方法给子组件; 获取组件实例根元素,添加到父容器中。
声明式的使用方式接口是与命令式保持一致的,区别在于:
r-data 会自动建立与父组件的绑定,也就是说父组件数据更新会出发子组件的更新
r-ref 获取该组件实例的引用
r-data/r-methods 是directive的一种键值对格式,左值是key, 右值可以是任何JS表达式,包括使用function,多个值以分号 “;” 分隔。 directive 还有另外一种表达式格式:
声明的属性值包括在花括号里就会当成JS语句来执行。
更新
Real有三个触发更新的方法:
this.$update( )
组件实例通过this.
$data
可以拿到实例数据引用,变更this.$data
后 调用this.$update( )
触发更新this.$set( key, value )
变更this.
$data
并立即触发更新this.$set( { key: value, .... } )
批量变更this.
$data
并立即触发更新Real在触发更新时会执行模版中声明的所有表达式,如果值有变更,就会触发该绑定的更新方法。可以认为表达式是轻量的(避免在模板表达式中写过多逻辑),所以每次更新执行并不影响性能。
One More Thing
这里要介绍的东西是Real关于后端渲染的一些方法。
后端渲染模式吐出的是HTML,有些场景下在前端的执行环境是需要后端的数据的,Real也提供了注入组件数据的接口:
<%= title %>
是后端使用的模板语法,也可能是:ejs, handlebars, art-template...,通过 r-props 注入数据,在组件实例中可以直接通过 this.
$data
获取到,这样的接口对于获取后端的初始数据是至关重要的。关于Real还有一样东西是需要一提的:动态编译。 在后端渲染模式下,会出现组件的子模版只能在前端渲染,例如:一个长列表加载更多的交互逻辑。
使用Real的处理方式:
就如上面示例,使用 $compile 实例方法可以在异步数据回来后,再编译子模板并渲染添加到父组件中。