一、regular
Regularjs是基于动态模板实现的用于构建数据驱动型组件的类库。
关键词:动态模板引擎,数据驱动,组件
- 动态模板引擎:实现view是会随着数据变化的而变化
- 数据驱动:强制将你的业务逻辑抽象为数据(状态)的变化-->双向数据绑定实现方式移步 双向数据绑定方法。
- 组件: 组件化开发,将一些公用的东西组件化。比如:城市选择,图片上传,公用头部等等。
二、 模板语法
ES5 表达式:rgl模板几乎完整的按ES5的规范实现了表达式, 你可以几乎按以往js的经验来使用你的表达式,这点你在其它数据驱动的框架如vuejs或avalon中是享受不到的, 当然并不是说必须要在模板里去声明复杂的表达式,只是提供了可能性 。
举个例子,下列表达式在regularjs中都是合法的:
{100 + ‘b’}
{user? ‘login’: ‘logout’}
{parseInt(22.1)}
注意几个要点
- 表达式中的this指向组件本身, 所以你可以通过this.xx来调用组件的某个方法。
- 数据根路径从component.data开始, 即user 其实获取的是
component.data.user。 - rgl不支持自增、自减(++,--)以及位操作符& |等。
- rgl不支持正则表达式的字面量。
- rgl开放了部分JS内建供使用:
Array Date JSON Math NaN RegExp Object String
decodeURI decodeURIComponent encodeURI encodeURIComponent
parseFloat parseInt
**1. 插值 {Expression} **
- 文本插值 {content}
- 属性插值
.modal-{klass}
2. 规则Rule
严格来说,在插值之外的语法功能, 都由RULE , RULE的语法是{#NAME }
- list
list指令用来遍历某个sequence来循环的处理某些重复性的结构(vue中的v-for,angular中的ng-repeat)
在每次循环都会创建一个临时变量代表当前下标
//regular
{#list list as item}
{item_index}{item.name}
{/list}
//vue
{{index}} {{item.name}}
- if/else/elseif
与其它模板引擎(如freemarker,handlebars)一样, regular也提供了if,elseif,else等语法元素提供对逻辑控制的支持
{#if condition} //这个表达式结果会被强制转换为Boolean值
...
{#elseif condition2}
...
{#else}
...
{/if}
** 优点**:使用if控制属性,根据判断依据,指令、属性或事件会被添加或移除
// control the attribute
Home
// control the event
Next
// control the directive
- include
include 用来标准引入一些内容,这些内容可能需要在初始化后指定,或可能发生变动。
{#include template}
template
: 一个Expression
,求值结果是字符串或模板AST
3.一次性绑定
由于脏检查机制的性能极大的依赖于监听器的数量,为了精确控制监听器的数量,regularjs引入了一个新的表达式语法元素@()提供了bind-once的表达式的支持. 这个被监听的表达式在检查到一次值变化就会被解除监听。 @(Expression)
{ @(title) } // the interpolation only trigger once
4.过滤器(支持双向过滤器)
Regular.filter( "last" , function(obj) {
return obj[obj.length - 1];
};
Regular.filter( "lowercase" , function(text) {
return (text).toLowerCase();
};
// Template
{list|last|lowercase}
// {list: ['Add','Update','Delete']},
// output
delete
三、 ES5部分新特性IE8及以下的支持
推荐学习网站:Mozilla 开发者网络
IE8以及以下:indexOf,forEach,filter; 还有一些新特性,如果需要可以参照Mozilla 开发者网络自己加进去。
module.exports = function(){
// String proto ;
extend(String.prototype, {
trim: function(){
return this.replace(/^\s+|\s+$/g, '');
}
});
// Array proto;
extend(Array.prototype, {
indexOf: function(obj, from){
from = from || 0;
for (var i = from, len = this.length; i < len; i++) {
if (this[i] === obj) return i;
}
return -1;
},
forEach: function(callback, ctx){
var k = 0;
// 1. Let O be the result of calling ToObject passing the |this| value as the argument.
var O = Object(this);
//Let len be ToUint32(lenValue).
var len = O.length >>> 0;
if ( typeof callback !== "function" ) {
throw new TypeError( callback + " is not a function" );
}
// 7. Repeat, while k < len
while( k < len ) {
var kValue;
if ( k in O ) {
kValue = O[ k ];
callback.call( ctx, kValue, k, O );
}
k++;
}
},
filter: function(fun, context){
var t = Object(this);
var len = t.length >>> 0;
if (typeof fun !== "function")
throw new TypeError();
var res = [];
for (var i = 0; i < len; i++)
{
if (i in t)
{
var val = t[i];
if (fun.call(context, val, i, t))
res.push(val);
}
}
return res;
}
});
// Function proto;
extend(Function.prototype, {
bind: function(context){
var fn = this;
var preArgs = slice.call(arguments, 1);
return function(){
var args = preArgs.concat(slice.call(arguments));
return fn.apply(context, args);
}
}
})
// Array
extend(Array, {
isArray: function(arr){
return tstr.call(arr) === "[object Array]";
}
})
}
四、快速起步
1.初始化结构
- Regular.extend
Regular.extend用来创建一个继承自Regular的组件类,所有传入extend的属性都会成为此组件类的原型属性。 - ** template**
一般来讲一个组件会需要一个模板来描述组件的结构,这里我们传入包含模板的容器节点的选择器(你也可以直接传入模板字符串) - data
- 组件component可能需要一些初始化状态,这些数据我们可以在实例化组件时作为data传入。
- 需要注意的是在实例化组件传入的参数会被作为实例属性,所以可以在这里覆盖extend的定义(原型属性)。
- $inject(node[, direction])
这是个组件的实例方法,会将组件插入到目标节点制定位置,实际上就是js里面的append - bottom[默认参数]:作为node的lastChild插入
- top:作为node的firstChild插入
- after:作为node的nextSibling插入
- before:作为previousSibling插入
2.插值
Hello, {username}
3.if/else逻辑控制
{#if index==1}
Hello, 我是管理员
{#elseif index==2}
Hello, 我是录单员
{#else}
Hello, 我是报价员
{/if}
4.事件
4.1DOM事件
通过if
动态绑定click
事件
- 支持基本的dom事件,eg:
on-focus
,on-blur
,on-click
,on-change
。 - 支持事件代理
- 所有的on-*都会在节点上绑定对应事件,在某种情况下(比如大列表),这种方式不是很高效。
-
$event
对象:那你可以使用$event来获取事件对象,这个变量会再每次事件触发时临时的定义在data.$event中,即你可以在模板里直接使用它。$event
对象是被修正过的,在兼容IE6的前提下,你可以使用以下规范内的属性:
-
你可以使用
delegate-
来代理on-
来避免可能的性能问题。regularjs只会绑定唯一的事件到组件的第一父元素(无论你是如何$inject的)来处理组件内的所有代理事件。{#if list as item}editUser: function(event){ var target = event.target; if (target.title == "editUser") { //指定触发节点 var id = target.dataset.id; utils.ajax( 'user/initUpdate', { id: id }, 'POST') .then(function(data) { //执行对应的操作 }); } }{item.name}{/if}
总结:
$event
对象被修正过,结合委托,我们可以把需要的值通过这个对象取出来,注意这里传递的id
,命名方式必须是data-*
的形式。 该对象封装之后类似jquery里面的$(this)对象。
-
可以通过
Regular.event
来扩展自定义ui事件,比如on-enter
,on-tap
等等,如下示例on-enter
。var dom = Regular.dom; Regular.event('enter', function(elem, fire){ dom.on(elem, "keypress", update); //绑定监听事件 function update(ev){ if(ev.which == 13){ // ENTER key ev.preventDefault(); fire(ev); // if key is enter , we fire the event; } } return function destroy(){ // return a destroy function dom.off(elem, "keypress", update); } }); // use in template `
4.2组件事件
Regularjs集成了一个轻量级的Emitter,使得所有组件都可以使用以下接口来实现事件驱动的开发
-
component.$on
: 用于添加事件监听 -
component.$off
: 用于解绑事件监听 -
component.$emit
:用于触发某个事件
this.$on("save",function(arg){
console.log(arg);
});
this.$emit("save",2)
this.$off("save");
4.3组件事件和DOM事件的异同之处。
- 事件的共性
回调方式取决于你传入的属性值
取决于你传入的值是表达式插值还是普通属性,regularjs
会做不同的响应处理,例如:
表达式(`e.g. on-click={this.remove()}`)
Delte
remove: function(index){
this.data.list.splice(index ,1);
// other logic
}
非表达式(`e.g. on-click="remove"`)
Delte
var Component = Regular.extend({
template:'example',
init: function(){
this.$on("remove", function($event){
// your logic here
})
}
})
事件的不同
组件事件是由
$emit
方法抛出,而DOM
由用户触发,由浏览器抛出(除了自定义事件)dom
事件由于DOM
本身的特点是可以冒泡的,但是组件事件没有冒泡这一机制。$event
在组件事件中是$emit
传入的第一个参数,而DOM
事件中是封装过的事件对象
5.指令
内建指令有:r-class
,r-model
,r-hide
,r-style
,ref
注意:可以通过Regular. directive
来扩展指令。
自定义指令方式
Regular.directive('r-html', function(elem, value){
this.$watch(value, function(new Value){
elem.innerHTML = new Value
})
})
6.过滤器
//regularjs 几个内建的过滤器
{ [1,2,3] |total}
//6
{ [1,2,3] |last}
//3
{ [1,2,3] |average}
//2
注意:可以通过Regular. filter来扩展过滤器。
//1. 自定义一个双向过滤器,后面传入一个 "{}"
Regular.filter("join",{
get: function(origin, split) {
return origin.join(split || "-");
},
set: function(dest, split) {
return dest.split(split || "-");
}
});
{[1,2,3]|join:"-"}
//1-2-3 //get
enter: function(option) {
this.data.list = "1-2-3";
this.$update("this.data.list | join:'-'",this.data.list); //set
console.log(this.data.list) //["1","2","3"];
}
//2. 单向过滤器 传入一个 "function"
Regular.filter("first",function (arr) {
return arr && arr[0];
});
{[1,2,3] | first}
//1
//3. format 过滤器
Regular.filter("format", function(value, format) {
function fix(str) {
str = "" + (str || "");
return str.length <= 1 ? "0" + str : str;
}
var maps = {
'yyyy': function(date) {
return date.getFullYear() },
'MM': function(date) {
return fix(date.getMonth() + 1); },
'dd': function(date) {
return fix(date.getDate()) },
'HH': function(date) {
return fix(date.getHours()) },
'mm': function(date) {
return fix(date.getMinutes()) }
};
//Object.keys 不支持IE8
var trunk = new RegExp(Object.keys(maps).join('|'), 'g'); // yyyy|MM|dd|HH|mm/g
//or 写死 当然也可以使用for...in去循环 这个只是不支持IE6以下的浏览器
var trunk = new RegExp("yyyy|MM|dd|HH|mm","g");
format = format || "yyyy-MM-dd HH:mm";
value = new Date(value);
return format.replace(trunk, function(capture) { //返回一个个匹配项目
return maps[capture] ? maps[capture](value) : ""; //返回匹配项目对应的值
});
});
//template
{time| format: 'yyyy-MM-dd HH:mm'}
7. 计算属性computed
尽管regularjs的表达式支持非常完备,但是在某些情况下,创建计算属性(computed property)可以让你避免书写冗余的表达式
8. ref
在模板中,你可以使用ref属性来标记一个节点或组件,并且ref可以动态使用。在实例化后,你可以通过
component.$refs
来获取你标记的节点。相当于vue.js里面的v-el,v-ref指令的结合。
//获取component
this.$refs.address;
//获取节点
this.$refs.input;
9. 组件内嵌内容的使用。
1.使用this.$body的好处
this.$body内嵌内容还是属于引用组件的父级,可以操作父级的data,控制显示隐藏。对于公用组件模态框的使用很便利。
2.到底什么是$body?
this.$body实际上是一个函数。运行它相当于对『内嵌的模板』进行了一次编译动作并返回一个『块』,「块」类似于一个阉割版的组件,也可以被插入到某个位置。
3.手动传入$body
在上面的例子中,我们可以很方便的通过在组件节点之间插入内容来自定义组件需要的某些模板片断,但这会引入一个问题,『当我们使用JS初始化组件时,如何定义内嵌内容?』。你可以采用手动传入的方式
var Model = new Model({
$body: ""
});
4.额外话
// app.js 父调用组件alert on-close:给组件绑定close事件
// alert.template 组件模板
我是实例
//js里面调用this.$emit("close");
通过这种给组件绑定事件的方式也是可以实现的。
10. Regular.dom
由于内部实现需要,Regular实现了部分常用的跨浏览器的dom方法,如果只是简单的dom处理,你可以直接使用Regular.dom。
内建的dom方法
1. 绑定事件
Regular.dom.on(element, event, handle);
Regular.dom.on(element,"click", function(event){
});
2. 移除事件监听器
Regular.dom.off(node, event, handle);
3. 添加节点className
Regular.dom.addClass(element, className)
4. 移除节点的某段className
Regular.dom.delClass(element, className)
5. 判断节点是否拥有某个className
Regular.dom.hasClass(element, className)
6. 根据浏览器和节点, 设置节点的textContent或innerText
Regular.dom.text(element[, value])
7. 设置或获取节点的innerHTML值
Regular.dom.html(element[, value])
8.设置或获取节点的指定属性
Regular.dom.attr(element, name [ , value])
五、组件生命周期
了解组件的生命周期来帮我们理解Regular内部运行机制
当实例化组件的时候,会发生以下事情:
1.options将合并原型中的events,data。
options = options || {};
options.data = options.data || {};
options.events = options.events || {};
if(this.data) _.extend(options.data, this.data);
if(this.events) _.extend(options.events, this.events);
2.将options合并到this中
_.extend(this, options, true);
⚠ 实例化中传入的属性会覆盖原型属性
3.解析模板
如果已经被解析了,这步就跳过
4. 根据传入的options.events 注册事件
5.触发$config事件,调用config函数
6.编译模板,触发一次组件脏检查
这里的脏检查是为了确保组件视图正确,到这里我们已经拥有初始化的dom元素,你可以通过$refs来获取你标记的。
7.触发$init事件,并调用this.init函数。
注意:请铭记,与config的区别是,此阶段在compile之后,意味着你可以通过$refs获取到你标记的dom结构。
当component.destory()
当销毁组件时,剧情就要简单的多了。
- 触发$destroy事件
- 销毁所有模板的dom节点,并且解除所有数据绑定、指令等
六、构建单页面应用路由stateman
stateman相关的API:
http://leeluolee.github.io/stateman/?API-zh=undefined&doc=API&lang=zh#stateman-文档-api-new-stateman
七、require+stateman+require构建单页面应用实例
官网作者写了一个单页面应用的demo,我司目前的项目也是参照这个demo来构建的。
https://github.com/regularjs/regular-state
八、本人在项目中遇到的一些"坑"
1.关于r-model
场景1:有时候我们需要select默认选择一些值。但是option列表又是动态生成的。 就会导致r-model执行的时候,发现对应需要处于选择状态的选项是不存的,所以导致匹配不成功。 即使我们在option渲染数组请求成功之后,主动进入脏检查也是没有用的,因为r-model里面传入的值没有发现变化,所以,不会进入对应的watch事件里面进行重新赋值。
场景1解决方式:
场景2
这个问题比较奇怪,到现在我也不知道是什么原因,所以尽量避免吧。这个问题的出现是基于场景1。
config: function(data) {
data.company = [{ agencyCompanyId: '', agencyCompanyName: '请选择' }];
data.obj = {
agencyCompanyId: '', //公司 默认选择 “请选择”
};
}
init:function(){
utils.ajax(config.path + 'list')
.then(function(data) {
data.entity = [{agencyCompanyId:0,name:"平台"},
{agencyCompanyId:1,name:"宝城"}];
_this.data.company = _this.data.company.concat(data.entity);
_this.$update();
});
}
结果:
场景2解决办法:
data.company = [{ agencyCompanyId: '-1', agencyCompanyName: '请选择' }];
//不要设置为空字符串,设置 -1 这种后台不会使用的id。
2. 关于子路由模块插件问题
官方关于子路由渲染API:http://regularjs.github.io/regular-state/docs/core/view.html。 这个文档说是针对0.6版本。但是官网并没有具体的0.6以上的版本代码。所以,我们很容易搞错。
实际上官网的版本0.5.2里面并没有r-view
这个指令。而是通过ref=view
来标记子路由模块渲染的地方。
以下代码里面就是通过r-view来代表子路由模块插入的位置。
http://regularjs.github.io/regular-state/assets/restate.pack.js
3. 关于init函数
在init里面不能直接操作dom,因为init时此组件并没有插入到文档中,所以直接操作dom是不会起作用的。如果一定需要操作dom可以通过以下方式
- Regular的 $ref获取你标记的dom结果。
- 通过Regular.dom.element(component,needAll)获取。
- 如果needAll为true,返回子节点数组。
- 如果needAll为false,返回子节点中的第一个节点。
未完待续