angular中的$q小结

近日,公司小伙伴把angular的$q服务,运用的炉火纯青,登峰造极,遂对之做个小结。

名词解释:

resolveHandler:then方法的第一个参数

rejectHandler:then方法的第二个参数或catch方法的第一个参数(catch方法即为then(null, function)的简写)

先来个基本用法:

1
2
3
4
5
6
7
8
var p = $q(function (resolve, reject) {
setTimeout(function () {
resolve('success');
}, 100)
});
p.then(function (data) {
console.log('then1:', data);
});

结果会输出

1
then1:success

这应该很好理解。

然后再来一个:

1
2
3
4
5
6
7
8
9
10
11
12
13
var p = $q(function (resolve, reject) {
setTimeout(function () {
resolve('success');
}, 100)
});
p.then(function (data) {
console.log('then1:', data);
});
p.then(function (data) {
console.log('then2:', data);
});

结果会输出:

1
2
then1: success
then2: success

再看这个:

1
2
3
4
5
6
7
8
9
10
11
12
13
var p = $q(function (resolve, reject) {
setTimeout(function () {
resolve('success');
}, 100)
});
p.then(function (data) {
console.log('then1:', data);
return 'haha'
})
.then(function (data) {
console.log('then2:', data);
});

结果却是:

1
2
then1: success
then2: haha

两者非常相似,结果却不一致,为什么呢?

前者属于『分发式』调用(我自己YY的),『分发』后的resolveHandler的入参是分发者的返回值,即

后者这种属于链式调用,后一个then的resolveHandler的入参就是上一个then的resolveHandler的返回值, 即

再看:

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
var p = $q(function (resolve, reject) {
setTimeout(function () {
resolve('success');
}, 20)
});
p.then(function (data) {
console.log('then1:', data);
return 'haha'
})
.then(function (data) {
console.log('then2:', data);
return $q.resolve('xixi');
})
.then(function (data) {
console.log('then3:', data);
return $q(function (resolve, reject) {
setTimeout(function () {
resolve('success again')
},20)
})
})
.then(function (data) {
console.log('then4:', data);
})

输出结果为:

1
2
3
4
then1: success
then2: haha
then3: xixi
then4: success again

如果上一个then的resolveHandler返回的是一个promise对象,那么下一个then的resolveHandler的入参为这个promise对象resolve的值.

为了印证上述结论,再看一个更复杂的例子:

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
var getPromise = function (resolveValue) {
return $q(function (resolve, reject) {
setTimeout(function () {
resolve(resolveValue);
},20)
})
};
getPromise('p1')
.then(function (data) {
console.log('then1:', data);
return 'haha'
})
.then(function (data) {
console.log('then2:', data);
return $q.resolve('xixi');
})
.then(function (data) {
console.log('then3:', data);
return getPromise('p2').then(function (data) {
console.log('then4:', data);
return getPromise('p3');
})
})
.then(function (data) {
console.log('then5:', data);
})

输出结果为:

1
2
3
4
5
then1: p1
then2: haha
then3: xixi
then4 p2
then5: p3

OK,上述都是resolve的情况,下面看看reject的情况。

来一个基本的:

1
2
3
4
5
6
7
8
9
10
$q(function (resolve, reject) {
setTimeout(function () {
reject('failed');
}, 20)
})
.then(function (data) {
console.log('success1:', data);
},function (data) {
console.log('fail1:', data);
});

结果输出:

1
fail1: failed

应该很好理解。

来看一个复杂的:

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
$q(function (resolve, reject) {
setTimeout(function () {
reject('failed');
}, 20)
})
.then(function (data) {
console.log('success1:', data);
})
.then(function (data) {
console.log('success2:', data);
},function (data) {
console.log('fail2:', data);
return 'fffff';
})
.then(function (data) {
console.log('success3:', data);
return $q.reject('reject 3');
}, function (data) {
console.log('fail3:', data);
})
.then(function (data) {
console.log('success4:', data);
})
.catch(function (data) {
console.log(data);
});

输出结果为:

1
2
3
fail2: failed
success3: fffff
reject 3

promise被reject后,第一个rejectHandler将接受到reject的值,但是这个rejectHandler返回的值就此『迷途知返』,传给接下来的resolveHandler。如果返回的是将被reject的promise对象,被reject的值才会被传递给下一个rejectHandler。

 

总结

似乎讲的并不是那么清楚,请读者仔细推敲代码的运行结果,可以更加深刻理解$q的用法,用于处理复杂异步逻辑非常合适。本文没有涉及$q的所有API,而且promise规范也不仅仅只有一种,请读者自行延伸查阅。

内网web服务穿透微攻略

虽然这些天经常加班,但有时候加班处于待命状态,所以抽空研究了下内网穿透,即从外网访问内网web服务。

本来想想这种事情除了开VPN,似乎没有别的办法,直到前段时间一同事告诉我他通过ssh隧道实现了这个需求,并发给我一篇英文博客,当时只是随便瞄了几眼文章里的图,没有时间去看。于是后来断断续续的暗暗研究,由于英语不好,都是搜索中文文章,但看了好多篇文章,都是单单讲SSH,没有涉及到HTTP,没有说到我需求的关键点。直到最后,再回头看看前面那篇英文博客,一切都豁然开朗了!

那篇文章在这 https://chamibuddhika.wordpress.com/2012/03/21/ssh-tunnelling-explained/ (请自备梯子)

好了,下面开始进入正题。由于是『微攻略』,只涉及到操作的步骤,不讲具体原理。

看了好多篇文章,ssh隧道需要借助中间主机(外网需可访问)进行端口转发,而且分为本地转发和远程转发,我们这里需要的是远程转发。

核心命令,即在内网主机执行:

ssh -C -f -N -R *:{中间主机监听端口}:localhost:{内网主机监听端口}{中间主机用户名}@{中间主机} [-p {中间主机ssh端口}]

举个栗子,假设:

内网主机:192.168.100.101

中间主机:123.123.123.123(ssh端口为333,一般默认为22)

需要从外网访问内网主机的web服务端口:8080

希望中间主机监听的端口:2222

登陆主机的用户:root

那么在内网主机上需要执行的命令为:

1
ssh -C -f -N -R 2222:localhost:8080 root@123.123.123.123 -p 333

然后会提示输入中间主机的用户密码,输入完毕后,则建立ssh隧道完毕。

此时在中间主机执行:

1
curl localhost:2222

就可以看到打印出内网主机返回的http信息。

至此核心部分已经完成了,还剩下几个问题:

  1. 第三方主机(HTTP客户端)尚无法通过中间主机访问内网web服务
  2. 每次执行此命令还需要输入中间主机的用户密码
  3. 即使是守护进程中运行的,也会在一段时间后断开连接
    我们一个个解决:

1.需要开启中间主机的GatewayPorts:

通过vi /etc/ssh/sshd_config 文件,将 GatewayPorts 设置为 yes,保存退出vi。然后执行

1
/etc/init.d/sshd restart

这是centos的命令,其他linux版本可能有所不同
然后就可以在家里用自己电脑,访问内网的WEB服务了!比如在浏览器里打开http://123.123.123.123:2222/something就能看到内网的web页面了~

2.把内网主机的ssh公钥添加到中间主机的相应用户的~/.ssh/authorized_keys中,甚至可以设置中间主机别名

3.通过autossh(一个命令行工具),当隧道断开连接后,可自动重连,通过包管理工具安装好后,只需将上述命令的ssh替换为autossh即可,比如上面的例子,改为autossh就是:

1
autossh -C -f -N -R 2222:localhost:8080 root@123.123.123.123 -p 333

好了,接下来就可以愉快的玩耍了~

本周停更通知

这周末老家来了三个亲戚,陪了近两天,而且前几天一直忙于公司的一个紧急项目,暂时没有时间也没有干货可分享(抑或没来得及整理),见谅。

react-webpack雏形诞生

请了一个礼拜的陪产假,6天多没碰电脑,最后一个晚上再次拿起react,又弄了一个demo,然后这两天继续改进,终于可以非常勉强的拿出来分享一下,当然还有很多不足,后续会持续改进。最大的一个不足(或者叫缺失更合适囧)是,还没有加入Ajax的支持,下一步先解决这个问题,工程化还有很多优化空间。

地址: https://github.com/stoneChen/myReact

react

DIY使用Node代理HTTP请求

近日,为了更加方便前后端联调,响应组里小伙伴的需求,为grunt工作流中的connect任务增加代理中间件,将非静态资源请求转发到测试环境或后端本地服务。

google了一番,找到了 proxy-middleware 这个模块,正好符合我的需求,使用很简单,只需在connect的中间件数组里添加一项即可:

1
2
3
4
5
6
7
8
9
10
var doProxy = require('proxy-middleware');
//...other code
connect: {
livereload: {
options: {
middleware: function (connect) {
return [
//其他中间件
connect().use('/', doProxy('http://example.com'))
]

后来,为了在服务启动过程中可根据配置文件代理host的不同值,实时代理不同host,改为在中间件里读取yml文件配置的host:

1
2
3
4
5
6
7
8
9
//在靠后的中间件里
//每次请求都读取代理host配置,若配置了该值,则代理到这个host,若未配置,则使用本地mock
var apiRoot = envCfg.proxy;
if (apiRoot) {
grunt.log.writeln('apiRoot found, dispatch to `' + apiRoot + '`');
return doProxy(apiRoot)(req, res, next);
} else {
grunt.log.writeln('apiRoot NOT found, use local mocking');
}

往github上推了一个 demo

完成了这个需求后,读了一下 [proxy-middleware](https://github.com/andrewrk/node-proxy-middleware) 的源码,里面包含了对http头的各种处理,包括cookie,主要通过一个新的http/https模块的request对象,结合源request对象中的http头(包括cookie),向指定host发起一个新的http请求,在响应回调中,将代理host 的响应pipe给源响应(添加了少数头字段),完成了整个代理的过程。

回头想想,之前书上看过的HTTP知识这次真的派上用场了,正好《深入浅出NodeJS》快看完了,作者说Node可以帮助我们更加底层的了解HTTP原理,真的是这样!反观早前从事的JAVA开发,那框架封装的简直了,根本难以理解HTTP的运作原理。赞Node!

《深入浅出NodeJS》这本书对Node底层的介绍偏多,感觉实战还远远不够,还应该再来一本。

昨晚『开发者头条』推送了一条『百度母婴技术团队基于 ReactJS 实现 webapp』的消息,瞬间又激动了,里面提到的整个架构,与我之前所想大都相符,抓紧时间整出一套可行的方案!

发发牢骚,谈谈我心目中的新一代架构

国庆后的这几天,是我至少近一年半来,心里最不踏实,最没有成就感的一段时间。项目进度始终不理想,虽然主要责任不在我们前端,后端的接口不断在变,总是在返工,心力憔悴。我虽然没有直接负责开发某个模块,但是经常在前后端之间游走,以及引导同事熟悉项目的开发。这段时间自己少有时间可以了解新技术,即使有时间,也因为脑子一团浆糊无法静下心来。难道是因为自己的不进步而感到焦虑么。国庆期间花了大把时间看的Webpack/React,某个文档只剩下一个尾巴,至今还未结束掉,状态甚是不好。白天上班时,几乎每天都是『在自己电脑前待不到几分钟就被叫走,各种奔波,开会』这样的状态。这样的工作状态会让人觉得非常空虚,没有安全感。觉得是时候应该调整下策略,使自己的工作变得更有价值了。

再说说我心目中的下一代前端架构吧。

在我看来,前端开发大致经历了这么几个阶段:

  1. 『原始社会』,一切都是用原生javascript开发
  2. PrototypeJS/Mootools/jQuery等类库封装DOM操作,用这些类库解决几乎所有前端问题,主页面后端渲染
  3. 基于简化DOM操作的类库,封装各种UI组件,主页面后端渲染
  4. 框架(如Angular,React)封装绝大部分DOM操作,并封装各类UI组件,主页面前端渲染,并诞生了前端路由,形成了SPA,甚至前后端同构应用
    虽然对Angular有了一定经验,对其高效率的开发赞叹不已,但对于大型应用Angular似乎还是难以驾驭。

当我知晓有React这个框架的时候,我正在学习Angular,当时并没有对它关注太多,随着业界多方对之的宣扬,逐渐增加了对它的了解,去阅读了官方文档,并试着敲下若干demo,别有一番体验。再之后,了解了FLUX思想,在此之前我甚至都没有『数据流向』这个概念,只知道双向数据绑定真的很棒,随着应用变的越来越复杂,双向数据绑定变得难以掌控。

随着近段时间对React + Flux的了解,心中逐渐有了一个模糊的轮廓:

  1. React:View渲染
  2. react-router:react的路由组件
  3. react-redux:react的Flux实现
  4. redux-devtools:redux配套的开发者工具,使调试更加容易
  5. react-hot-module-replacement:react配套的热模块替换,更新组件代码并保留state,提升开发体验与开发效率
  6. ES6:从未发现ES6如此美妙,解决了很多ES5的痛点,尤其是模块系统和对象/函数的语法糖,更是向未来标准看齐;通过babel转换为ES5
  7. CSS模块化:这个优先级靠后
  8. Eslint:用于代码规范检测,保证代码质量,统一代码风格
  9. 前端单元测试,UI测试,尝试TDD
  10. Webpack:
    a) 打包javascript,css,图片,各种loader编译(包括babel,css预处理/后处理,图片压缩/base64转换/多倍图)
    b) 与npm结合,实现一行命令切换dev,prod环境
    c) 合理输出sourceMapping
    d) 可持续构建生成最优html,javascript,css,并解决浏览器缓存问题(自动添加文件hash后缀)
    以上是我近段时间总结的一系列需要实现的需求,网上有一些案例,实现了小部分需求,很多细节还需要继续推敲。

使用XtraFinder增强OS X的finder

XtraFinder

国庆后的周末只有一天,很多事情都赶到了一起,这次的文章就从简了,介绍一款OS X的finder增强工具XtraFinder

介绍页比较简单,安装后的偏好设置也基本上看几眼就明白(而且大部分是中文的)。更推荐使用homebrew cask安装,执行sudo brew cask install xtrafinder即可(需root权限)。运行后会出现在顶部工具栏上(logo跟finder一致),所有功能在其偏好设置内开启/关闭。

介绍几个个人觉得比较实(zhuang)用(bi)的功能:

  1. 首当其冲的便是高大上的彩色侧边栏图标:
    真的是为finder增『色』不少~开关在『Appearance』 -> 『显示彩色侧栏图标』
  2. tab功能,自OS X 10.9后已经支持
  3. 不少人对于OS X不支持像windows那样的CTRL + X / CTRL + V剪切功能而烦恼(其实剪切可以通过 ⌘+C / ⌘ + ⎇ + V 实现),现在你可以通过XtraFinder实现,位于『特性』 -> 『剪切和粘贴』
  4. 获取文件(夹)的路径一直是OS X的痛点,通过XtraFinder有两种方式可以获取:
    a) 勾选 『将项目添加到Finder菜单中』-> 『拷贝路径』,然后在你想要获取路径的文件(夹)上右键,就可以看到多了一项『拷贝路径』:
    直接点击『拷贝路径』默认就是拷贝了路径到剪贴板。
    b) 在a的基础上,勾选 『特性』 -> 『在工具栏中添加XtraFinder的功能』,然后回到finder,在工具栏右键选择『自定义工具栏…』,将『拷贝路径功能拖上去』:

    然后,选择需要获取路径的文件(夹),再点击工具栏上那个按钮即可将该路径复制到粘贴板了
  5. 在OS X中新建文件也是一个痛点,在上面的截图中,我另外把『New File』框出来了,和『拷贝路径』同理,前提要勾选 『将项目添加到Finder菜单中』-> 『新建文件』,可自定义添加别的类型文件:

     

还有一些finder实用功能它自身就有提供,比如『显示』-> 『显示路径栏』,『显示状态栏』等。

之所以会写这篇文章,是因为国庆期间更新了 OS X 10.11,导致了XtraFinder不可用了,Google了一下,跟系统的debug模式有关,参见 这篇文章。根据文章上说,将系统debug关闭,再重启,XtraFinder果然可以启动了,不过似乎有些功能失效了,比如『自动调节宽度』,等待开发者团队修复这些问题吧。