nya-bootstrap-select也许应该完全重写了

就在不久前,我还对这个插件很乐观,不过在解决了最近的两个bug之后,我觉得同angularjs自带的directive作斗争是一件不愉快的事情。

由于nya-bootstrap-select依赖bootstra-select这个jQuery插件,而该插件需要使用select标签来定义下拉选框。但Angular内部已经把select标签作为了一个directive,并且定义了一些列特殊行为。这些行为导致了目前各种冲突,其中一个问题就是,当我们使用ng-options来生成选项的时候。select directive会先生成一个?值的option作为默认选项,一开始为了保持美观和一致性,我删除了这个选项,但我发现angular内部似乎会错误的引用位置,导致当select的值由无变成确定值时,删除后的第一个实际option被删除。 这带来了严重的问题,最后不得已,我只能保留那个?的option,但这样很难看。看起来避免使用select标签才是根本解决之道。

Comments

在Node.js中使用promise摆脱回调金字塔

在开始谈论正题之前,我们先来看看下面一段代码:

1
2
3
4
5
6
7
8
9
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// Do something with value4
});
});
});
});

是不是感觉很恐怖,随着嵌套的回调函数增加,结尾会有大量的花括号和圆括号 }); 出现。

Promise

在javascript中实现异步最简单的方式是Callback。遗憾的是,这种编程方式牺牲了控制流,同时你也不能throw new Error()并在外部捕获异常。 Promise的出现解决了这两个需求,又保持了javascript异步的优势,不同于Fiber这种多线程的实现方式,Promise只是一种编程方式的变化。而无须在底层改变。

CommonJS的规范提到了多种Promise,我们只介绍其中一种的实现q (https://github.com/kriskowal/q)

我们在这里不讲解抽象的Promise规范,这多半是实现者应该关心的,我们直接从示例入手,如果你有兴趣,可以参见Promise/A+

q的核心是一个promise对象的then方法,他接受两个回调方法,一个promise被定义之后有3种状态,pending(过渡状态),fullfilled(完成状态),rejected(错误状态)。一个promise只能是这三种状态种的一种,而无法是他们的混合状态。

  • pending状态可以理解为promise还没有获得确定值,就相当于一个任务还没有完成。
  • fullfilled状态可以理解为完成并返回结果。这时then(onFullfilled, onRejected)的onFullfilled方法会被调用。
  • rejected状态可以理解为错误,并结束。返回错误。这时then(onFullfilled, onRejected)的onRejected方法会被调用。

了解了核心思想后,我们来看一个例子,在这个例子中我们先读取一个json文本文件,然后将其解析成javascript对象,最后这个对象进行修改再保存回去。 按照传统的callback写法,有如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fs.readFile('example.json', function(err, data){
if(err) {
console.log(err):
} else {
try {
var obj = JSON.parse(data);
obj.prop = 'something new';
fs.writeFile('example.json', JSON.stringify(obj), function(error){
if(err) {
console.log(error);
} else {
console.log('success');
}
});
} catch(e) {
console.log(e);
}

}
});

在这个例子中,控制流被切割成多个部分(每次异步都要处理一次错误),并且 JSON.parse 的错误必须在内部捕获,但却不能跑到外部。因为在异步回调中无法抛出异常。 现在当我们使用promise的时候,假设我们有个能够返回一个 promise 对象的 readFilewriteFile 方法。那么上面的代码就可以变成如下形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var promise = readFile();
promise
.then(function(data){
// we don't need to catch error. in other words. we can throw error in this callback.
var obj = JSON.parse(data);
obj.prop = 'something new';
// return a promise. so we can chain the then() method.
return writeFile(JSON.stringify(obj));
})
.then(function(){
console.log('success');
}, function(err){
// all error will fall down here.
console.log(err);
});

上面的例子中,我们首先从 readFile() 方法里获得了一个返回的 promise 对象,然后使用这个对象的 then() 方法。在这里,我们只传入了一个 onFullfilled 回调方法,根据Promise/A+的文档。then() 一定会返回一个 promise 对象,所以我们又连接了一个 then() ,由于这个 then() 是最后一个,所以我们需要在这里提供一个 onRejected() 回调方法来处理所有的错误。在第一个 onFullfilled() 回调方法中,我们返回了一个 promise ,这个 promise 的处理结果将会在下一个 then()onFullfilled() 方法中取得。

这段代码执行的时候,当任意位置抛出异常的时候,最后一个then的 onRejected 回调会被执行。否则一切按照从上至下的顺序执行,整个控制流都十分简洁明了。

因为 then() 方法必须返回一个promise,实际上我们也可以结合同步方法返回一个已经fullfilled的promise 比如下面这个例子

1
2
3
4
5
6
7
8
9
10
11
var promise = readFile();
promise
.then(function(data){
return JSON.parse(data);
})
.then(function(obj){
obj.prop = 'something new';
console.log(obj);
},function(err){
console.log(err);
});

上面例子中第二个then会在第一个then返回之后被执行,因为第一个then返回的时候,由于JSON.parse是同步方法,所以返回了一个值,这个值会被包装成一个fullfilled的promise.

制作promise的API

上面例子中我们知道了如何使用promise提供的核心方法 then() 。但是对于平时使用的fs等异步的库我们要怎么才能利用promise呢。 在q的文档中介绍了q-io库,里面将常用的io方法都用promise的模式包装了一遍,在实际使用中,你可以使用那个库的方法。不过我们在这里简单的对fs进行包装,让其支持promise,这样以后遇到任何异步方法,你都可以将其转化。

首先定义改一个readFile方法,返回promise,这里利用了 Qdefer() 方法,创建一个 deferred 对象。这个对象有连个关键的方法 resolvereject() 。当resolve(value)执行之后,promise变成fullfilled状态,fullfilled的值就是value 当 reject(reason) 执行之后,promise变成了rejected状态,reason会被传递到onRejected()方法。

1
2
3
4
5
6
7
8
9
10
11
12
var Q = require('q');
function readFile(callback){
var deferred = Q.defer();
fs.readFile('example.json', function(err, data){
if(err){
deferred.reject(err);
} else {
deferred.resolve(data);
}
});
return deferred.promise.nodeify(callback);
}

注意到这里面我们依然提供了一个callback,用于提供一些需要callback的场合的兼容性,我们利用 promise 对象的nodeify方法来调用这个callback,这个callback可以为undefined。

另外一点需要注意的是,一个promise状态改变之后,不能再次改变,所以,你只能调用一次reject或resolve。

有了这个API,我们便可以像前面例子里那样,使用promise来执行读取文件的操作了。其他异步回调转化成返回promise的异步方法基本上都可以参照这个模式来做。

一次处理多个promise的

如果你有几个异步方法,他们都返回promise,并且当这些方法都处理完之后,你才能进行下一步,Q提供了一个all()方法来帮助你消化多个promise。

1
2
3
4
5
6
7
8
9
10
11
Q.all([
readFile('file1.json'),
readFile('file2.json')
])
.then(function(dataArray){
for(var i = 0; i < dataArray.length; i++){
console.log(dataArray[i]);
}
}, function(err){
console.log(err);
});

在这里例子里,我们将一个promise数组传给 all() all返回一个promise,当数组里面的所有promise都为fullfilled状态时,我们的then()方法才会被调用。这时fullfilled值是一个数组,每个元素对应前面promise的fullfilled值。 当任意一个promise变成rejected状态的时候,all的promise会立即reject而不等其他的完成。

利用promise改写你的项目

最佳的理解方法便是事件,你可以把一些nodejs的基本异步方法包装成promise,这样你就可以在整个程序的多个地方使用这些方法。并且让你的程序的异步代码看起来更整洁,更容易理解。 阅读 Q的文档 了解更多的API和方法。并在程序中使用这些方法,使你的代码更优美,逻辑更健壮。 阅读 Promise/A+ 。了解promise原理。

Comments

开始使用nya-bootstrap-select

好吧,这个post是个广告,如果你是一名web前端开发者,使用AngularJS,bootstrap和jquery。那么我在这里向你推荐我最近发布的一个开源项目:nya-bootstrap-select

nya-bootstrap-select 是一个对bootstrap-select的包装,让这个功能强大的jquery插件可以用angularjs特有的风格在angularjs程序中使用。

依赖: AngularJS 1.0+(这个是当然的) bootstrap-select 1.3+ bootstrap css (支持2.x和3.x)

安装: 推荐使用bower来安装,可以帮助你自动处理依赖关系。

1
bower install nya-bootstrap-select

将src/nya-bootstrap-select.js加入到你的页面里

1
<script src="bower_components/nya-bootstrap-select/src/nya-bootstrap-select.js"></script>

将 nya.bootstrap.select作为angular app的依赖

1
angular.module('yourApp', ['nya.bootstrap.select']);

现在可以开始使用了。本文就只列举一个例子,更多例子请参考文档 http://nya.io/nya-bootstrap-select/

在select标签里,加入class nya-selectpicker 也可以作为属性加入。 然后将你的scope变量绑定到ng-model上,这样你可以获取到选择的值。 假设你的$scope.myOptions就是选项的条件

1
2
3
<select class="nya-selectpicker" data-container="body" ng-model="myModel" ng-options="c.value for c in myOptions">

</select>

Comments

Angular 与 jQuery 加载顺序的问题

最近打算把一个自己写的angular directive开源,于是花了点时间写demo,但是在demo里曾经没有什么问题的插件不能正常的工作,令人匪夷所思。 发现问题之后,困扰了一段时间,把所有能怀疑的对象都检查了一遍,依然没发现问题,最后在无意间对比过去使用这个directive的项目代码和demo代码发现二者的angular.js与jquery.js加载顺序不同,于是调换了一下顺序,果然问题就解决了。瞬间有种要掀桌的感觉(╯°Д°)╯︵ ┻━┻。 不过这个问题是为什么呢,百思不得其解,在Google了一番之后,在Google Group上找到了类似的问题。在原po的提示下,通过研究angular源码发现,原来angular会在加载到内存之后,查找 window.jQuery 如果存在,就对其进行扩展,并将jQLite绑定到jQuery上,以后创建出来的jQLite对象实际上也是jQuery对象。如果没找到,就使用angular自带的jQLite。以下是部分代码片段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function bindJQuery() {
// bind to jQuery if present;
jQuery = window.jQuery;
// reset to jQuery or default to us.
if (jQuery) {
jqLite = jQuery;
extend(jQuery.fn, {
scope: JQLitePrototype.scope,
isolateScope: JQLitePrototype.isolateScope,
controller: JQLitePrototype.controller,
injector: JQLitePrototype.injector,
inheritedData: JQLitePrototype.inheritedData
});
// Method signature:
// jqLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments)
jqLitePatchJQueryRemove('remove', true, true, false);
jqLitePatchJQueryRemove('empty', false, false, false);
jqLitePatchJQueryRemove('html', false, false, true);
} else {
jqLite = JQLite;
}
angular.element = jqLite;
}

Angular在加载到内存之后便会执行这段代码。以后所有的angular.element()创建或包装的对象都是jqLite对象。如果弄错了加载顺序,那么使用$()创建出来的对象与angular的jqLite对象便存在兼容性问题,导致一些奇怪的现象。

正确的做法

如果你要在项目中同时使用jQuery与AngularJS那么,一定要让jQuery在angularjs之前加载。比如下面这样写才能保证程序不出现奇怪的问题:

1
2
<script src="bower_components/jquery/jquery.js"></script>
<script src="bower_components/angular/angular.js"></script>
Comments

用Grunt与livereload构建实时预览的开发环境

自从用了yeoman来开发angular.js之后,就喜欢上这个工具了,顺带也了解了整个workflow所用的所有工具,yeoman给我最深的印象就是它可以在开发的时候启动一个localhost的server来预览你的前端项目,并且能够实时反应你对文件的修改。想想把,你不在需要依赖昂贵又不准确的IDE进行WYSIWYG开发,也不需要自己动手去按F5不停的刷新页面,还要担心缓存问题。现在只要在命令行运行 grunt serve 就可以做到了 这么好的功能当然不是yeoman特有的功能,只要你会用Grunt就可以做到。如果你还不熟悉Gruntjs那么,它的官方页面可以帮你快速熟悉常用配置(是的,你不需要知道怎么写grunt plugin)

我们需要哪些Grunt插件?

过去这个任务需要connect-livereload, grunt-contrib-connect, grunt-contrib-watch来配合完成. 感谢connect和grunt插件的开发者,我们现在只需要2个插件就可以做到这一切了:

  • grunt-contrib-connect, 用来充当一个静态文件服务器,本身集成了livereload功能,因此不再需要connect-livereload中间件
  • grunt-contrib-watch, 用来监视文件的改变,然后执行一些任务,同时保持 grunt-contrib-connect 的服务器一直开启

为了不用不厌其烦的写grunt.loadNpmTask(),我们使用 load-grunt-tasks 来帮助我们自动加载这些插件,为了能看到grunt任务执行时间,我们加入了 time-grunt 插件,这几个插件对于本文的目的来说都不是必须的

下面是配置好的package.json

1
2
3
4
5
6
7
8
9
10
11
{
"name": "grunt-livereload-demo",
"version": "0.0.1",
"devDependencies": {
"grunt": "~0.4.2",
"grunt-contrib-connect": "~0.6.0",
"grunt-contrib-watch": "~0.5.3",
"load-grunt-tasks": "~0.3.0",
"time-grunt": "~0.2.9"
}

}

接下来运行 npm install 来安装就可以了。

配置Gruntfile

现在假设你已经知道Gruntfile的结构。那么让我们开始先搭建一个可以serve静态文件的服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
module.exports = function(grunt) {

// Load all grunt tasks automatically
require('load-grunt-tasks')(grunt);

// Time how long grunt task take. Can help when optimizing build times
require('time-grunt')(grunt);

//Configure grunt
grunt.initConfig({

// The actual grunt server settings
connect: {
options: {
port: 9000,
hostname: 'localhost', // Change this to '0.0.0.0' to access the server from outside.
keepalive: true // keep the server alive. so the grunt task won't stop
},
all: {
options: {
open: true,
base: [
'examples' // This is the base file folder. we suppose our index.html is located in this folder
// replace with the directory you want the files served from
]
}
}
}
});

// Creates the 'serve' task
grunt.registerTask('serve', [
'connect:all'
]);
};

我们用grunt-contrib-connect创建了一个静态服务器,并且能自动打开浏览器。我们把配置好的任务加入到自己创建的serve任务里,别在意这里只有一个子任务,我们接下来会加入其他任务到这个serve任务里。现在你可以在命令行里运行 grunt serve (从Gruntfile所在的目录)。grunt会自动打开浏览器并访问 http://localhost:9000 并保持服务器一直运行下去(任务不会结束)。

Watch和livereload

虽然我们已经配置好了一个静态文件服务器。但是 grunt-contrib-connect 并不会帮助我们监视文件变化并自动加载,我们需要 grunt-contrib-watch 来完成这项工作,并触发 grunt-contrib-connect 里面的livereload功能。所以我们要修改一下Gruntfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
module.exports = function(grunt) {

// Load all grunt tasks automatically
require('load-grunt-tasks')(grunt);

// Time how long grunt task take. Can help when optimizing build times
require('time-grunt')(grunt);

//Configure grunt
grunt.initConfig({

// The actual grunt server settings
connect: {
options: {
port: 9000,
// Change this to '0.0.0.0' to access the server from outside.
hostname: 'localhost',
livereload: 35729 // This does not perform live reloading. this port is used by watch task to trigger a live reloading action.
},
all: {
options: {
open: true,
base: [
'examples'
]
}
}
},

//Watch files for changes, and run tasks base on the changed files.
watch: {

livereload: {
options: {
livereload: '<%= connect.options.livereload %>' // this port must be same with the connect livereload port
},
// Watch whatever files you needed.
files: [
'examples/*.html',
'examples/styles/{,*/}*.css',
'examples/scripts/(,*/}*.js',
'examples/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}'
]
}
}
});

// Creates the 'serve' task
grunt.registerTask('serve', [
'connect:all',
'watch'
]);
};

我们新修改的配置把connect任务的keepalive选项去掉了,换成livereload。因为watch会帮我们让grunt task挂起而不停止,所以不再需要keepalive。这里我们将livereload的端口号设置为35729。你也可以设置成其他的端口,不过必须要在watch的livereload选项里设置成相同的端口。 新加入了一个watch任务,这里我们配置了livereload的端口。配置了需要监视的文件。当这些文件发生改变时,watch任务就会触发livereload。让浏览器刷新页面。最后,我们把这两个任务按照先后顺序组合成serve任务,注意watch必须在后面,因为watch之后的任务永远不会被执行,同时我们也需要watch帮我们保持服务器一直运行。

接下来运行 grunt serve 就可以看到一个跟刚才一样的页面在浏览器窗口中打开了。不同的是,这次你修改任何监视范围内的文件,都会实时的反映在浏览器上,可以说做到的了实时预览。当你需要结束服务器的时候,使用Ctrl+C。 watch并不止能做这些,它还可以让less自动编译,自动运行jshint检查js文件的语法。connect还可以和grunt-connect-proxy结合来制作本地代理访问其他域名的api而不用处理跨域问题。当你觉得某种开发方式不够酷的时候,想想grunt吧。如果你没有找到合适的插件,就自己编写一个。Have a good day!

Comments
NEWER POSTS OLDER POSTS