JavaScript修饰器-让代码更干净

一般在JavaScript中为了让部分代码延迟执行,一想起的自然是 setTimeout,比如:

setTimeout(() => {
  // doing
}, 0);

这种代码或许你不知道写过多少遍,但,我们在 setTimeout 中多数情况下会去调用另一个方法:

setTimeout(() => {
  this.fn();
}, 0);

你会发现,我们一直都在重复写着 setTimeout,再套用一个匿名函数,最后才真正去编写我们需要执行的方法。我越来越讨厌这种写法,老是写着一些无关系要多余的代码。

使用Angular的同学对 @Component 不陌生,里面大量的使用这种ES7才会有的“修饰器”。

修饰器是一个函数,用于修改类行为。

那, 应该怎么编写一个更干净的 setTimeout,比如,我希望这样来编写我的timeout:

@timeout(1000)
fn() {
  // doing
}

this.fn();

对应的 timeout 修饰器代码:

// timeout.ts
export function timeout(milliseconds: number = 0) {
  return function(target, key, descriptor) {
    // value 值相当于上面示例中 `change` 方法。
    var orgMethod = descriptor.value;
    descriptor.value = function(...args) {
      setTimeout(() => {
        orgMethod.apply(this, args);
      }, milliseconds);
    };
    return descriptor;
  }
}

target:实例对象,即 IndexComponent 实例化对象。
key:方法名称,即 fn
descriptor:对象描述,同Object.getOwnPropertyDescriptor() 。

怎么样,这样子写的代码是不是更酷?

修饰器目前只能在ES7才会有,但一些在Typescript、Babel等转码器已经被支持,特别是Angular2应用中更是给予非常重要的地位。而且应用范围可以非常广,比如类、类方法和属性。

结论

以上只是一个很简单的修饰器示例,你可以根据需要生产一些有意思的修饰器,让编写的代码更优雅、更干净。完整示例

函数响应式编程(FRP)

一、什么是FRP

FRP全称叫函数响应式编程(Functional Reactive Programming),是一种编程模型,简单点说可以把若干不同数据在不做任何修改的情况下转换成新的数据,一直交替直到符合系统需求的数据。

比如:百度搜索框的自动补全,input输入下拉列表实时更新。其中input输入我们可以理解为最开始的数据,然后请求服务端,返回JSON数据,最后再转换后HTML写入DOM中。这是典型的一种外部输入数据(信号),数据(信号)的变化通过事件通知,最后经过一系列响应最后反馈给DOM。

这种FRP编程范式,已经有很多第三方类库,比如:ReactiveX(JS/.NET/Java/Swift/Scala)、Flapjax。而Angular2也把Rx(即ReactiveX JS版本)作为依赖关系。

二、一个Suggest示例

这是一个查询维基百科词语的Suggest(实在找不到百度有提供API),虽然很普通很常见,但我们还是画个流程图,以便于我们更清晰需要做什么。

FRP-Suggest

用户通过input输入词语,向维基API服务端请求,服务端处理完成后返回结果,再根据结果转换成相应的HTML代码,最后插入到DOM中;整个过程周而复始。

其实我们会发现整个过程的触发实际都是在一个时间线上,通过这一时间线对数据进行过滤、转换等等,而最后将数据(input输入的单词)转换成一个DOM列表。这就是FRP思想。而往往在语言实现上我们会以事件流的行式来表现。

那么我们来看一下代码是怎么写(完整代码,采用ES6编写,所以最好在FF下查看)。

var ob = Rx.Observable.fromEvent(qEl, 'keyup') // 创建观察手
  .map(e => e.target.value) // 
  .filter(text => text.length > 2) // 过滤
  .throttle(200) // 节流阀,防止按键过快倒置请求过频
  .distinctUntilChanged() // 忽略和上一次相同的操作,以免浪费资源
  .flatMapLatest(searchWikipedia) // 将当前的数据通过 searchWikipedia 映射成为一个新的数据
  .subscribe(data => { // 创建订阅方法,即拉取最新数据。
    var res = data[1];
    resultElr.empty();
    $.each(res, (_, value) => $('<li>' + value + '</li>').appendTo(resultElr));
  }, error => {
    resultElr.empty();
    $('<li>Error: ' + error + '</li>').appendTo(resultElr);
  });

// 这里必须返回一个带promise的函数
function searchWikipedia(term) {
    return $.ajax({
        url: 'http://en.wikipedia.org/w/api.php',
        dataType: 'jsonp',
        data: {
            action: 'opensearch',
            format: 'json',
            search: term
        }
    }).promise();
}

三、事件流处理

由上面的代码,可以了解FRP的几种常见事件流处理方法,而这些方法无疑就是方便对事件流的组合和转换。

1、映射

代码中 map(function)flatMapLatest(function),将事件流通过function映射成一个新的事件流。mapflatMapLatest 的区别在于,后者我们可以提供一个带有Promise对象,而最后被映射的是具体的值,而非带有值具体的值的Promise对象,恩,有点绕。以前面的示例,假设我们使用 map 来操作,那么我们需要这样:

ob.map(function(word) {
    return Rx.Observable.fromPromise(searchWikipedia(word));
});

2、过滤

代码中 filter(function)distinctUntilChanged(),事件流通过function判断结果为 true 的作为一个新的事件流。distinctUntilChanged 我的理解是比较,那么他也是相当于过滤中的一类吧。

使用中常见而代码又未提及的还有很多。

3、合并

merge(es1, es2, ...),把事件流和es1…3事件流合并成一个新的事件流。

4、条件

Rx.js提供了非常多的条件表达式,比如:

skip(n),跳过n个事件流,skip在rx当中还有很多个辅助版本。

四、行为能力

1、时间

FRP是以时间线来处理数据的,所以我时间也变成了最基本的一个。如上面的 throttle 表示200毫秒才会更新一次,换句话在这200毫秒发生的多个事件流最后都以最后一次的事件流为准。

正因为这种以时间线来处理数据,完全可以遏止像这种增量式搜索的资源浪费,就变得非常的简单。

2、计算

像一些加减乘除以及一些常用的统计 sum()min()max() 等等。

Rx.js还提供非常非常多的一些方便对事件流操作的方法,有兴趣可以看:,见:Observable Instance Methods

五、异步HTTP

我们再看一个异步请求HTTP的示例,当然如果只是一次请求看不出RX有什么特效。但假设我们需要同时两次请求,先查询列表,再根据结果再请求详细数据,这是多么觉见的场景。以下是通过常用的jQuery来实现。

$.get('/data1')
 .done(function(res) {
    $.get(res.data, function(res) {
      // process
    });
  });

以上已经尽可能短的描述两次请求的方式,看起来很干净,好像并无多余??等等,怎么看都怎么觉得奇怪,这好像又进入callback的深渊。那么看看FRP里面是怎么实现的:


var result2El = $('#result2'), source = Rx.Observable.fromEvent($('#demo2'), 'click'); source .map(() => 'node' ) .map(function(data) { result2El.html('开始查询带有[' + data + ']的词条...'); return data; }) .flatMapLatest(searchWikipedia) .map(function(res) { var data = res[1]; result2El.html('开始查询[' + data[0] + ']词条的内容...'); return data[0]; }) .flatMapLatest(searchWikipediaContent) .subscribe(data => { result2El.html('结果:' + JSON.stringify(data)); }, error => { result2El.html('错误'); });

怎么样,多么符合线性逻辑思考思维。对代码感兴趣点这

六、为什么需要FRP

对于FRP这种编程模型来讲,他是通过事件流的方式把数据进行重组,同样一个业务我们只需要依赖事件的组装,而不再过多的关注业务细节问题;转向把细节交给各种子函数来处理。其实我个人理解相当于把模块化开发思维更细致的转化到代码层面。

对于这种带有时间线性的开发方式,也非常符合程序员的线性表达业务逻辑思维方式。

在新版的Angular2,其中事件处理也采用这种方式。而对于FRP实现的框架而言,最重要的是有序的事件传播。如前面我们创建一个观察手,而这个观察手监听着 keyup 事件,一但事件发生变化,立即执行变通过一系列的事件流处理最终以DOM的形式展示。

最后

Angular2把RxJs做为依赖库并存着,虽然FRP有实现有很多,我也不知道Google和M$什么时候这么亲密,因为Rx也是M$提供的,而Angualr2的首选开发语言也是TypeScript。而市面上还存在其他很多FRP库(例如:Flapjax、Fran、Yampa),也许提供的API接口各有不同,但他们的FRP思想是一样的。

Rx不光是有JavaScript中使用,前面已经有所提及,它可以在.Net、Java等等后端开发上使用。

Angular 动态创建 directive

一、原由

动态加载一个 directive,其实你会发现这种需求特别的变态,因为对于 angularjs 而言绝大部分情况下应该不会需要这样,这点很大的程度是由于angular是html标记驱动,已经极大的提供很松散、有效的、优美的加载各式各样 directive

然则,故事是发生在一个编辑器上,我们所认知的编辑器是为创建一个富HTML,而为了让用户更好的体验(总不能让客户写HTML语言吧)所产生的按表单填写数据最终生成有效的HTML。那么自然我们不可能只放一种像百度编辑器就完事,你让用创建一个轮播广告几乎是不可能;所以需要有很多组件,比如:商品列表、富文本、图片广告、轮播广告等等,而每一种组件又不可能是统一界面。

废话说了这么多,其核心就是我需要创建很多组件,用户随时可以创建某一组件在列表中。

二、白痴写法

那好,首先想到的是按比较白痴写法,比如:

<div ng-repeat="item in list">
    <ae-config ng-if="item.type=='config'"></ae-config>
    <ae-rich ng-if="item.type=='rich'"></ae-rich>
    <ae-image ng-if="item.type=='image'"></ae-image>
    <ae-nav ng-if="item.type=='nav'"></ae-nav>
    ....
</div>
<button ng-click="list.push({ type: 'rich' })">Add</button>

这的确是一个快速办法,当然这里的 ... 还有很多,但是会有一个很严重的问题,那么:

  1. 页面会有大量的 <!-- ngIf: i.type==='nav' --> 存在,是的,是的,是的,我有洁癖,这么重要的事必须说三遍。
  2. 虽然组件越来越多,维护也跟着升高,比如说:不管我组件下架或上架都需要去修改这种该死的代码,更何况不同业务线所需要组件可能也不相同,倒置哪怕我不需要的组件也要走一遍。
  3. 组件替换太麻烦,是这样,每一种业务都需要一个叫 config 配置信息,即是配置信息肯定不能当组件随意创建或删除,所以就行成另一种麻烦了。

总之,可以列的坏处还有很多。因此……

我们需要更简单、更优美的编写方式。

三、概念

1、当前作用域

理解为 controller 的 scope。

2、子作用域

理解为 directive 创建的作用域。

3、directive 隔离 scope 数据

在了解之前必须先了解 directive 隔离 scope 的一些概念。scope他可以有各种值,他们都代表者不同含义,私以为理解各种方式的使用在 Angular 极为重要,毕竟没有 directive 那 angular 就什么也不是了。

A、false

默认值,创建自己作用域,直接使用当前作用域。

B、true

创建自己作用域,并继承当前作用域。这样不但可以直接使用原作用域的数据模型,还可以拥有自己的数据模型,同时修改自己数据模型并不会污染原作用域。

C、{} 创建自己作用域,继承当前作用域。正常我们理解为 隔离 scope,但我们可以通过三种不同的传值方式,让directive可以和controller进行通信。

scope: {
    config1: '=',
    config2: '@',
    config3: '&'
}
  • =,双向绑定模式,即不管是controller还是directive自身修改 config 数据模板都会在两边同步更新。
  • @,首先值永远是一个字符串,其次只允许当前作用域修改directive的子作用域。
  • &,首先值必须是表达式(即function),由directive的子作用域访问当前作用域的一个function。
    另外注意带有参数形式的表达式:directive调用无须关心参数列表,因为对于隔离模式而言,表达式的参数是无法被改变的,即在html中指定什么参数那便是什么参数。

注意:在controller设置隔离 scope 参数时,对于像:<gen config1="{{type}}"></gen><gen config1="type"></gen>,一个是表达式结果值,一个对象本身。这一点要注意了。

四、$compile 编译HTML

我们经常使用的像 ng-ifng-repeatng-bind 等等,其实内部都是通过 $compile 先编译再将编译后的HTML插入到DOM树上,目的是为了让插入的HTML能够实现双向绑定和事件监听等工作。

$compile 是一个 service,因此我们可以在controller和directive中任意使用。所以这里首先需要创建一个用来动态创建各种组件的 directive。

app.directive('genCom', function($compile) {
    return {
        restrict: 'E',
        scope: true,
        require: '?ngModel',
        link: function(scope, element, attrs, ngModel) {
            if (!ngModel) return;
            ngModel.$render = function() {
                if (!ngModel.$viewValue) return;

                var data = ngModel.$viewValue,
                    comComile = $compile('<div com-' + data.type + '="i"></div>');
                element.html(comComile(scope));
            }
        }
    }
};
});

scope: true

这里为什么是 true,由于 genCom 只是一个中件间,而我们希望通过此动态创建的组件上下文依然能访问到当前 scope 的数据。

require: ‘?ngModel’

当然数据源肯定不能少。

$compile

这里要拆分来看。

首先 comComile 返回Function对象,简单来说是对 HTML 里面所涉及的其他 directive 进行数据收集工作。即将所有 directive 找出来,组成一个数组。

其次 comComile(scope) 才能够算是产生一个可被插入到DOM的HTML元素,这里将 scope 做为所需要的父作用域存在着。这也就是我们前面为什么需要将scope设置为true的原因,这样我们可以保证通过 $compile 编译后其作用域不会跟controller断背,毕竟这个directive只为创建另一个directive本身就没什么业务意义。

因此最后在 controller 中可以这么使用:

<div gen-com="rich" ng-model="item"></div>

当然我们也可以这么玩:

<div class="item" ng-repeat="item in list">
    <div gen-com="{{item.type}}" ng-model="item"></div>
</div>

当然还有组件部分。

// 配置信息组件
app.directive('comConfig', function() {
return {
    restrict: 'AE',
        template: '<p>config</p>',
        transclude: true,
        replace: true,
        scope: {},
        link: function(scope, element) {}
    };
});
// 富文本框组件
app.directive('comRich', function() {
return {
    restrict: 'AE',
        template: '<p>rich</p>',
        transclude: true,
        replace: true,
        scope: {},
        link: function(scope, element) {}
    };
});
// 导航组件
app.directive('comNav', function() {
return {
    restrict: 'AE',
        template: '<p>nav</p>',
        transclude: true,
        replace: true,
        scope: {},
        link: function(scope, element) {}
    };
});

怎么样,很酷吧!完整示例

Javascript 异步

异步对语言来说绝对是一个重要的地位,不管是前端为了用户体验,或者是后端为了吞吐量。正因为如此,如何快速或优美实现异步变成了更重要的事。

一、概念

1、JavaScript 异步

JavaScript中所谓的异步是指:将某个任务拆分两段,分别执行。

2、Event Loop

由于负责JavaScript执行的只是一个单线程,当我们访问一个远程Http时这是需要一个时间等待,而不能因为这种等待让浏览器处于假死。所以就有了Event Loop。

简单说程序实现一个线程来负责JavaScript执行,而Event Loop是负责JavaScript线程体和程序的其他进程(如:I/O操作、Http等等)的通信。

二、实现方式

为了方便,每种实现方式都假定引用jQuery,且有以下配置信息。

var cog = {
    url: 'file.txt'
}

1、回调函数

即前面说到异步的第二段代码,就叫“回调函数”,洋文名叫callback,比如:

$.get(cog.url, function(data){
    console.log(data);
});

get 的第二个参数 function 就叫回调函数,$.get 在请求http返回结果后才会执行该函数。

2、Promise 对象

回调函数的确看起来也非常的简便,使用起来并没有什么问题;可必须依赖第一段代码,且当我们要请求两个远程信息时,就会变成这样子:

$.get(cog.url, function(data){
    console.log(data);
    $.get(cog.url, function(data){
        console.log(data);
    });
});

反正我是受不了。而使用 Promise 变成这样:

$.get(cog.url)
    .then(function(data) {
        console.log('f1:' + data);
    })
    .then(function() {
        return $.get(cog.url);
    })
    .then(function(data) {
        console.log('f2:' + data);
    });

变成了好多 then 的写法,但至少解决两个问题:依赖和变态的嵌套。

Promise 只是一种实现的思路,所以各家在实现上也相对于比较统一接口,比如上面的then。好在ES6版本中,对 Promise 的实现统一了接口。前面是猜jQuery的Promise对象,隐藏了细节,我们来试试ES6的原生写法。

function ajax(url) {
    return new Promise(function(resolve, reject) {
        let xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function() {
            if (this.readyState === 4) {
                if (this.status === 200)
                    resolve(this.response);
                else
                    reject(new Error(this.status));
            }
        };
        xhr.open("GET", url, true);
        xhr.send();
    })
}

ajax(cog.url)
    .then(function(data) {
        console.log('f1:' + data);
    })
    .then(function() {
        return ajax(cog.url);
    })
    .then(function(data) {
        console.log('f2:' + data);
    });

ajax 函数里面只是实例一个并返回 new Promise(),且接受两个参数:resolve(解决)和reject(拒绝)函数。而内部分别是对AJAX请求成功和失败的处理。

可见在 ES6 当中已经简化了 Promise 的实现成本。

然而和回调函数相比,到处的 then 也挺烦人。

3、Generator 函数

在说之前,我们先来认识一下 yield 关键词。说到这个,懂C#的人会很清楚,因为在C#很早就有这个,用于迭代器的延迟加载,可避免一次性加载所有可能并不能完全有用的数据。

Generator只不过一种名词叫法,而在传统编程当中为了解决异步问题,提出一种叫“协程”的东西,做法简单来说就是:函数A的线程执行到一半,然后暂停遇到IO要等待,就把这个执行权交到别的函数B上,然后过段时间回来看一下A,如果处于等待那么就把执行权交还给函数A的线程。

而说白了,ES6的Generator就是协程的一个实现。但感觉好像好高端上的样子,真正的只有一个关键词 yield

先来看一下示例:

function ajax(url) {
    let xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
        if (this.readyState === 4 && this.status === 200) {
            it.next(this.response);
        }
    };
    xhr.open("GET", url, true);
    xhr.send();
}

function* main() {
    let f1 = yield ajax(cog.url);
    console.log(f1);
    let f2 = yield ajax(cog.url);
    console.log(f2);
}

const it = main();
it.next();

抛开一些细节,光看 main() 函数的代码,是不是很爽呢?

首先:function 紧接着一个星号。

其次:我们来分析一下执行的流程。

  • 实例main()后,随后调用 next()
  • 执行到第一个 yield,接着进入ajax(),直到等到下一个next()出现,才会返回到main()函数体。
  • ajax请求结束直到遇到 it.next(this.response); 才会再返回main()函数体。
  • …………交替一直执行至结束…………

三、比较

1、语法

1、回调函数最简单、易理解,但很明显当多个时callback就到处飞,函数彼此之间都存在的依赖。
2、Promise相比回调函数来说函数间解耦了,但是到处then也有很多代码的冗余。但至少漂亮许多。
3、Generator作为ES6的标准存在,进一步解决了Promise冗余问题;但假如要想写好,我们还需要借助一些运行器之类来辅助,这样的代码才会更优美。但此我需要另一篇文章来解释。

2、学习曲线

1、假如明确知道函数只可能单一被使用,或者说如果函数并非是基础库,而是业务运用的次数很单一,回调函数不失是一种非常好的办法。
2、Promise在ES6之前,就已经有好多库:jQuery、when.js、Q等等提供的Promise抽象,大大的简化学习;而ES6提供的接口也极为方便。
3、Generator是一个全新的ES6接口,其实把Generator当作异步来解释我个人觉得真的很牵强,因为Generator目的更为的是迭代器优化。

3、浏览器支持现状

1、回调函数这毋庸置疑任何浏览器都没有问题。
2、Promise的第三方类库也解决了兼容性问题,而如果是ES6的写法估计在老版本的浏览器上特别是IE,是无法支持。
3、Generator同Promise的现状一样。

参考

HTML5之IndexedDB

IndexedDB 是一个为了在客户端中存储结构化数据,也就是我们通常所说的NOSQL。同时也满足一些数据库的特性,比如:大容量、索引检索、事务、安全性、存储类型(包括:二进制对象)、异步API。

基本概念

  • key-values键值对存储:values可以是一个非常复杂的结构体。
  • 事务模式数据库:任何操作都需要一个事务,事务是有生命周期,且只能自动提交。
  • 异步API:虽然也有同步,但是同步目前没有任何一个浏览器版本实现。
  • 同源策略:这里的“源”指的是文档URL所在的协议、域名和端口,在同一个源内的所有数据库都是必须是唯一的。
  • 数据库版本:版本为了区分作数据迁移。
  • object store:同NoSQL中的集合一个意思。

打开数据库

const dbName = 'DemoDb';

let db;
let request = indexedDB.open(dbName, 1);
request.onerror = function  (e) {
    // 错误处理
    alert(e.target.error.message);
}
request.onsuccess = function  (e) {
    // 数据库打开成功处理
    db = request.result;
}
request.onupgradeneeded = function  (e) {
    // 当首次打开数据库或打开的版本号高于当前版本号时执行,同时也是唯一一个最好来创建存储空间、索引、初始数据的地方
}
request.onblocked = function  () {
    // 其他选项卡也打开数据库时
}

可见当我们打开一个数据库时,极为简单。 1是我们的版本号,这里的版本号只允许向上升,而无法对其降级(只能删除数据库)。

onerror IndexedDB被设计来尽可能的减少对错误处理的请求,但是打开数据库会产生错误最有可能会包括:

  • 浏览器会在首次尝试打开一个IndexedDB存储时提醒用户是否允许或拒绝。
  • 隐私模式下访问。
  • 存储设备容量上限。
  • 用户请求清除数据。

而对于错误的处理都是通过事件冒泡方式,从事务再到数据库对象。所以一般如果我们希望处理到所有错误,可以在这里做统一处理。

创建数据库结构

既然前面已经说只能在 onupgradeneeded 事件中创建存储空间,那我们来尝试做一个 customers 存储空间存放客户信息,而每个客户信息包括:唯一编号、姓名、年龄、Email;同时为了加快访问,创建以 name 姓名非唯一索引,以 email 唯一索引(不能重复,否则会收到一个错误消息)。

为了便于查看,我依然放完整代码:

const customerData = [
    { id: 1, name: '苏先生', age: 25, email: 'xx@gmail.com' },
    { id: 2, name: 'asdf', age: 125, email: 'yy@gmail.com' }
];
const dbName = 'DemoDb';

let db;
let request = indexedDB.open(dbName, 1);
request.onerror = function  (e) {
    // 错误处理
    alert(e.target.error.message);
}
request.onsuccess = function  (e) {
    // 数据库打开成功处理
    db = request.result;
}
request.onupgradeneeded = function  (e) {
    // 当首次打开数据库或打开的版本号高于当前版本号时执行
    // 同时也是唯一一个最好来创建存储空间、索引、初始数据的地方

    db = e.target.result;
    // 创建一个存储空间存放customerData
    let objectStore = db.createObjectStore('customers', { keyPath: 'id' });

    // 创建索引
    // 可能会有重复的,因此我们不能使用 unique 索引
    objectStore.createIndex('name', 'name', { unique: false });
    // 希望确保不会有两个相同的email,因此我们使用 unique 索引
    objectStore.createIndex('email', 'email', { unique: true });

    // 初始化存储空间数据
    customerData.forEach(function(item) {
        objectStore.add(item);
    });
}
request.onblocked = function  () {
    // 其他选项卡也打开数据库时
}

增、删、改数据

对象 keypathkey generator

IndexedDB 既然是key-values键值对存储,必然就有键和值才能相关联。然而键的不同会倒置存储值的不同,这取决于对象存储空间是采用 key path 还是 key generator了。

key path

存储的值只能是一个javascript对象,而且对象必须包括和 key path 相同名的属性。比如前面的 let objectStore = db.createObjectStore('customers', { keyPath: 'id' }); 其中是 id 作为 key path,所以在增加对象时必须包含一个叫 id 的属性。

key generator

存储的值就非常自由了,可以是javascript对象或者基础类型(像字符串或数字等等),但是在 put 时就需要指定 optionalKey

事务

如同我们上面说的,每一个操作都需要有一个事务 transaction

db.transaction(['customers'], 'readwrite');

其中第二参数有三个值,代表着不同的事务隔离类型:

readonly:允许读取数据,不改变。
readwrite:允许读取和写入现有数据。
versionchange:允许执行任何操作,包括删除和创建对象存储空间。

使用游标获取所有数据

let listEl = document.getElementById('list');
function loadCustomer() {
    listEl.innerHTML = '';
    let objectStore = db.transaction('customers').objectStore('customers');
    objectStore.openCursor().onsuccess = function(e) {
        let cursor = event.target.result;
        if (cursor) {
            let trEl = document.createElement('tr');
            trEl.innerHTML = `<tr>
                                <td>${cursor.value.id}</td>
                                <td>${cursor.value.name}</td>
                                <td>${cursor.value.age}</td>
                                <td>${cursor.value.email}</td>
                                <td>
                                    <button data-id="${cursor.value.id}" class="view">查看</button>
                                    <button data-id="${cursor.value.id}" class="del">删除</button>
                                </td>
                            </tr>`;
            listEl.appendChild(trEl);
            cursor.continue();
        }
    }
}

openCursor() 包含两个参数:

optionalKeyRange:键的范围,例如:IDBKeyRange.bound("A", "F") 表示 key 的上限为A,下限为F。
optionalDirection:检索的方向,例如:next 表示升序,prev 表示降序。

增改数据

<form action="#" method="post" id="customerForm">
    <p>编号:<input type="number" value="3" id="id" required></p>
    <p>姓名:<input type="text" value="add name" id="name" required></p>
    <p>年龄:<input type="number" value="56" id="age" required></p>
    <p>Email:<input type="email" value="a@qq.com" id="email" required></p>
    <button type="submit" id="save">保存</button>
</form>
let idEl = document.getElementById('id'),
    nameEl = document.getElementById('name'),
    ageEl = document.getElementById('age'),
    emailEl = document.getElementById('email');

document.getElementById('customerForm').onsubmit = function() {
    let idInt = parseInt(idEl.value);
    // 检查id是否存在
    let objectStore = db.transaction(['customers'], 'readwrite').objectStore('customers');

    objectStore.get(idInt).onsuccess = function() {
        var data = this.result || {
            id: idInt,
            name: nameEl.value,
            age: ageEl.value,
            email: emailEl.value
        };
        var updateRequest = objectStore.put(data);
        updateRequest.onsuccess = function() {
            loadCustomer();
        }
        updateRequest.onerror = function(e) {
            alert(this.error.message);
        }
    }
    return false;
}

首先由于我们在创建存储空间时已经指明 id 作为 keypath,所以需要先检索所增加的 id 是否已经存在,存在更新否则新增。

objectStore.put(data) 表示提交一个对象。

注意由于所有API都是异步,因此 updateRequest 需要监听 onsuccess/onerror 事件。而对于 onerror 最有可能出现异步是索引规则,比如我们之前在创建一个email索引是不允许重复,假设插入的数据跟之前的有重复会收到一个错误消息。

删除数据

let req = db.transaction(['customers'], 'readwrite').objectStore('customers').delete(1);
req.onsuccess = function(e) {
    // 删除成功
}

检索数据

根据 key 检索

let id = 1;
let req = db.transaction(['customers'], 'readwrite').objectStore('customers').get(id);

req.onerror = function(e) {
    alert(`未找到 ${id}`);
}
req.onsuccess = function(e) {
    alert(`${id} 的姓名:${req.result.name} `);
}

根据 name 索引检索 name="asdf" 数据

let objectStore = db.transaction('customers').objectStore('customers');
let index = objectStore.index('name');
index.get('asdf').onsuccess = function(e) {
    alert(JSON.stringify(this.result));
}

根据 email 索引检索所有数据

let objectStore = db.transaction('customers').objectStore('customers');
let index = objectStore.index('email');
index.openCursor().onsuccess = function(e) {
    let cursor = e.target.result;
    if (cursor) {
        alert(JSON.stringify(cursor.value));
        cursor.continue();
    }
}

索引的顺序结果和存储对象空间是不一样的。

删除数据库

indexedDB.deleteDatabase("DemoDb");

而相比较 LocalStorage 可存储空间小、同步API。

参考资料

bootstrap插件概述

好像没有找到过关于bootstrap插件部分的文章,前两天在做一个input放大镜,是采用bootstrap插件编码规范,从而发现原来bootstrap的编码非常让人愉悦。故而想系统性的了解一下。

一、自定义编译js文件

默认bootstrap.min.js提供12个完整插件脚本,我没有找到可以通过网上直接构建的,应该有只是源代码结构非常漂亮,每一个写前端的都心需要自己动手构建一个比较短小的bootstrap插件脚本库出来。

bootstrap是通过 grunt 来构建的,关于grunt使用见参考资料。

bootstrap: {
    src: [
        'js/transition.js',
        'js/alert.js',
        'js/button.js',
        'js/carousel.js',
        'js/collapse.js',
        'js/dropdown.js',
        'js/modal.js',
        'js/tooltip.js',
        'js/popover.js',
        'js/scrollspy.js',
        'js/tab.js',
        'js/affix.js'
    ],
    dest: 'dist/js/<%= pkg.name %>.js'
}

以上是 Gruntfile.js 的一部分【他JSON格式】,用于拼接所有插件脚本,所以很简单只需要把不需要的加个 // 即可。

当然这里只有JS部分,还得去掉CSS部分, bootstrap采用的是LESS编写CSS,存放于 /less/bootstrap.less 也只需要找到我们并不需要的插件样式的 @import "xxx.less"

完成所有操作所以,只需要在源代码根目录下,输入CMD命令: grunt dist,会自动在 dist 目录下重新生成一份新的文件。

二、闭包

+function ($) {
  'use strict';

  // BUTTON PUBLIC CLASS DEFINITION
  // ==============================

  var Button = function (element, options) {
    this.$element  = $(element)
    this.options   = $.extend({}, Button.DEFAULTS, options)
    this.isLoading = false
  }}(jQuery);

很典型的一个闭包应用,这里将 jQuery 对象做为 $ 参数,这样可以避免闭包类使用的是 jQuery 对象,而不是其他类库或自己定义的 $

细心的人还会发现,为什么 function 前面会有一个 + 号,这是一个保障作用,特别是将bootstrap脚本库作为项目当中的一个库;并将所有脚本压缩成一个文件时,如果紧邻前面脚本没有加上 ; 来区分结束时,就会合在一起。有时我们也会使用 ;function(){} 逗号。

三、避免插件同名冲突

  var old = $.fn.button

  $.fn.button             = Plugin
  $.fn.button.Constructor = Button


  // BUTTON NO CONFLICT
  // ==================

  $.fn.button.noConflict = function () {
    $.fn.button = old
    return this
  }

$.fn.button.noConflict 每一个插件都会相类似的代码,用来处理插件同名冲突问题,比如:button,在 jquery.ui 下也有一个同名,假如你同时引用这两个类库时,就会产生两个问题:

  1. jquery.ui 在 bootstrap 前面,这个时候你使用 $('').button() 调用的是 bootstrap 的 button
  2. bootstrap 在 jquery.ui 前面,这个时候你使用 $('').button() 调用的是 jquery.ui 的 button

为什么?因为加载顺序的关系插件被覆盖了,而对于 jquery.ui 而言并未有同时冲突的处理,所以这里强烈要求将 jquery.ui库放在bootstrap前面,有这个前提,再来看我下面的示例,就会明白 noConflict 有什么作用。

//$.fn.button.noConflict();
$('button').button();

jquery.ui库放在bootstrap前面 的前提下,我注释掉第1行代码时,调用的是 bootstrapbutton 插件。反之调用的是 jquery.ui 下的 button 插件。怎么样?有没有感叹 bootstrap 的编码方式很完美。

四、data-* 属性

所有bootstrap的插件都可以不需要在页面上写上脚本调用代码,包括插件参数,都是以 data-参数名="值" 这种形式出现,有没有觉得很高大上。

<button type="button" class="btn btn-primary" data-toggle="button" aria-pressed="false" autocomplete="off">
  Single toggle
</button>

以上当我点击按钮时会自动加载 .action 样式,再次点击取消样式。而实现这一切要归于以下代码:

  $(document)
    .on('click.bs.button.data-api', '[data-toggle^="button"]', function (e) {
      var $btn = $(e.target)
      if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn')
      Plugin.call($btn, 'toggle')
      e.preventDefault()
    })
    .on('focus.bs.button.data-api blur.bs.button.data-api', '[data-toggle^="button"]', function (e) {
      $(e.target).closest('.btn').toggleClass('focus', /^focus(in)?$/.test(e.type))
    })

这里的 click.bs.button.data-api 带有多级的事件命名空间,虽然不太会有重复的事件名称存在,但是如果你有洁癖不想bootstrap监控这些事件,我们可以直接注销掉,比如:

$(document).off('.bs'); // 所有bootstrap监控事件
$(document).off('click.bs.button'); // 只对button插件有效

另一方面将参数 data-* 化,核心其实就是调用 jQuery.data() 他会自动处理这一方面,比如:

  Button.prototype.setState = function (state) {
    var d    = 'disabled'
    var $el  = this.$element
    var val  = $el.is('input') ? 'val' : 'html'
    var data = $el.data()

    state = state + 'Text'

    if (!data.resetText) $el.data('resetText', $el[val]())

    $el[val](data[state] || this.options[state])

    // push to event loop to allow forms to submit
    setTimeout(function () {
      state == 'loadingText' ?
        $el.addClass(d).attr(d, d) :
        $el.removeClass(d).removeAttr(d);
    }, 0)
  }

其中 var data = $el.data() 就是自动将当前元素的所有 data-* 转化为对象。关于这个详见 jQuery.Data()

五、API接口

bootstrap插件都是单一入口、可链接式,比如我们可以这么调用一个 button 插件。

$('button').button('loading').addClass('active');

关于单一入口,比如:

$('button').button('loading');
$('button').button('reset');

是不是觉得记忆成本非常低,有没有?

六、关于事件

bootstrap 每个插件都有属于自己的一序列自定义事件,比如 modelhidden.bs.modal,而所有这一切都是通过 jQuery.trigger 来触发,关于 jQuery.trigger 事件的一些细节,比如:版本不同事件是否冒泡也不一样等等。

当我们需要监听这些事件时,我们可以这么做:

$('#model').on('hidden.bs.modal', function(e) {});

其中 $('#model') 是当前模态框的元素,这样有效的限定同一页面多个模态分别监听。

当然也可以用 $(document) 监听,这归于 jQuery.trigger 事件冒泡,但如果有多个模态框时那么任何一个关闭都将引时该监听。

一个题外话,在查bootstrap源代码时,发现大量的使用 $.proxy(this.hide, this),作用是保持在事件回调函数内上下文依然是插件本身。

七、CSS3兼容

每个插件都有做如下处理:

      var callbackRemove = function () {
        that.removeBackdrop()
        callback && callback()
      }
      $.support.transition && this.$element.hasClass('fade') ?
        this.$backdrop
          .one('bsTransitionEnd', callbackRemove)
          .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) :
        callbackRemove()

    } else if (callback) {
      callback()
    }

这里会监测是否支持CSS3中的 transition,当不支持时直接回调用,否则当动画结束后再回调。

参考资料

  1. Grunt 快速入门
  2. bootstrap github

© 2017 卡片机色彩 沪ICP备13032872号-3

Theme by cipchk

to top