tag:blog.chrisyip.im,2014:/feed
Chris Yip
2015-01-30T00:02:28-08:00
Chris Yip
http://blog.chrisyip.im
i@chrisyip.im
Svbtle.com
tag:blog.chrisyip.im,2014:Post/use-iojs-and-nodejs-with-homebrew-at-os-x
2015-01-30T00:02:28-08:00
2015-01-30T00:02:28-08:00
Use io.js and Node.js with Homebrew at OS X
<p>Install <a href="https://iojs.org/">io.js</a> and <a href="http://nodejs.org/">Node.js</a> with <a href="http://brew.sh/">homebrew</a> is super easy:</p>
<pre><code class="prettyprint lang-shell">brew install iojs node
</code></pre>
<p>But you make notice this message for io.js formula:</p>
<blockquote class="short">
<p>This formula is keg-only.<br>
iojs conflicts with node (which is currently more established)</p>
</blockquote>
<p>Because of io.js uses <code class="prettyprint">node</code> for its binary’s name, so Homebrew won’t link io.js’s binary to <code class="prettyprint">/usr/local/bin</code>, unless force to link with command <code class="prettyprint">brew link iojs --force</code>. That means you can’t use io.js directly (e.g. <code class="prettyprint">node app.js</code>). You have to use absolute path (e.g. <code class="prettyprint">/usr/local/Cellar/iojs/1.0.4/bin/node app.js</code>), or use <a href="https://github.com/creationix/nvm">nvm</a>:</p>
<pre><code class="prettyprint lang-shell">nvm use 0.11
node -v # v0.11.15
nvm use iojs
node -v # v1.0.4
</code></pre>
<p>Although it can work, but you have to change asolute path after io.js updated, or <code class="prettyprint">nvm</code> may be unavailable in some situations (e.g. needs <code class="prettyprint">source $NVM_PATH/nvm.sh</code>). It’s inconvenience. So I created a shell script to help things easier:</p>
<pre><code class="prettyprint lang-shell">#!/usr/bin/env sh
if [ -d "$(brew --prefix)/Cellar" ]; then
node=`find /usr/local/Cellar -name node | grep iojs/.*/bin/node`
if [ ! -z "$node" ]; then
$node $@
else
echo "io.js not installed"
fi
else
echo "Homebrew folder not found"
fi
</code></pre>
<p>This script searches io.js’s binary in Homebrew’s <code class="prettyprint">Cellar</code> folder, and run it with passed arguments. You can consider it is an alias of <code class="prettyprint">/usr/local/Cellar/iojs/1.0.4/bin/node</code>, except it will maintain version for you.</p>
<p>All you need to do is put this script into your <code class="prettyprint">$PATH</code> and make it executable. If you don’t know how to, here is a guide:</p>
<pre><code class="prettyprint lang-shell">cd $HOME
touch .bash_profile # or .bashsrc, .zshrc, depends on your environment
echo 'export PATH="$HOME/.scripts:$PATH"' >> .bash_profile
mkdir .script && cd $_
touch iojs && chmod +x iojs
open -a "EDITOR" iojs
</code></pre>
<p>Replace <code class="prettyprint">EDITOR</code> to your favorite editor, then copy and paste the shell script into the file, and save.</p>
<p>That’s all. You can now run io.js and Node.js like this:</p>
<pre><code class="prettyprint lang-shell">node -v # v0.11.15
iojs -v # v1.0.4
node app_for_node.js
iojs app_for_iojs.js
</code></pre>
<p>Note that this script can’t handle multiple versions for now, so after io.js updated, remember to run <code class="prettyprint">brew cleanup</code>.</p>
<p><em>The shell script is published on <a href="https://gist.github.com/chrisyip/8fb02770b92bf9d6d8a7">Gist</a>.</em></p>
tag:blog.chrisyip.im,2014:Post/function-direct-call-vs-func-call-vs-func-bind-in-javascript
2014-10-15T02:44:56-07:00
2014-10-15T02:44:56-07:00
Function direct call vs func.call() vs func.bind() in JavaScript
<p>JavaScript 有三种函数调用方式 (其他奇葩的不讨论) :</p>
<ol>
<li><code class="prettyprint">func()</code></li>
<li>
<code class="prettyprint">func.call()</code> 和 <code class="prettyprint">func.apply()</code>
</li>
<li><code class="prettyprint">var binded = func.bind()</code></li>
</ol>
<p>第一种很好理解,就是直接调用。</p>
<p>第二种很常用,经常用于调整函数的 <code class="prettyprint">this</code> 指向的对象。如:</p>
<pre><code class="prettyprint lang-js">function say_hello () {
return 'Hello, ' + this.name
}
say_hello.call({ name: 'John' }) // 'Hello, John'
</code></pre>
<p>第三种是 <a href="http://www.ecma-international.org/ecma-262/5.1">EcmaScript 5</a> 新加入的 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind">Function.prototype.bind()</a>。它和 <code class="prettyprint">call()</code> 类似,但它不是直接调用,而是返回一个新的函数:</p>
<pre><code class="prettyprint lang-js">function withdrawCash (amount) {
if (this.bank.cash < amount) {
throw new Error('You do not have enough money')
}
this.bank.cash = this.bank.cash - amount
console.log('Cash ' + amount + ' out')
}
var John = withdrawCash.bind({
bank: {
cash: 1000
}
})
var Marry = withdrawCash.bind({
bank: {
cash: 10000
}
})
John(1000) // 'Cash 1000 out'
Marry(10000) // 'Cash 10000 out'
Marry(1) // throw 'You do not have enough money'
withdrawCash(100) // throw 'TypeError: Cannot read property 'cash' of undefined'
</code></pre>
<p>从例子可以看到,<code class="prettyprint">John</code> 和 <code class="prettyprint">Marry</code> 的数据是分开,相当于为他们单独定义了 <code class="prettyprint">withdrawCash()</code>。如果用 <code class="prettyprint">call()</code> 实现的话就是这样:</p>
<pre><code class="prettyprint lang-js">var John = {
bank: {
cash: 1000
}
}
var Marry = {
bank: {
cash: 10000
}
}
withdrawCash.call(John, 1000)
withdrawCash.call(Marry, 10000)
withdrawCash.call(Marry, 1)
</code></pre>
<p>虽然可以达到一样的效果,但是 <code class="prettyprint">withdrawCash.call()</code> 明显不如 <code class="prettyprint">var func_name = withdrawCash.bind(BankInfo)</code> 易读和易维护,而且后续调用时更简单,不需要再次指定 <code class="prettyprint">call()</code> 的第一个参数,减少 typo 造成的隐患。</p>
<p>看到这里,有人会想:「既然 <code class="prettyprint">bind()</code> 比 <code class="prettyprint">call()</code> 好,那以后就用它好了」。</p>
<p>虽然不少浏览器已经支持,也有 shim 可用,而且至少 Node 环境是可以无障碍地使用,但我还是要说,慎用 <code class="prettyprint">bind()</code>,因为它有性能问题。</p>
<p>这是在 <a href="http://jsperf.com/">jspert.com</a> 上的<a href="http://jsperf.com/function-direct-call-vs-call-vs-bind">测试结果</a>:</p>
<p><a href="https://svbtleusercontent.com/ngikpmruypp1kq.png"><img src="https://svbtleusercontent.com/ngikpmruypp1kq_small.png" alt="function direct call vs call() vs bind()"></a></p>
<p>这是在 Node 上的测试结果:</p>
<pre><code class="prettyprint">{ http_parser: '2.3',
node: '0.11.14',
v8: '3.26.33',
uv: '1.0.0',
zlib: '1.2.3',
modules: '14',
openssl: '1.0.1i' }
✓ direct call x 68,038,524 ops/sec ±0.88% (94 runs sampled)
✓ call() x 17,655,227 ops/sec ±0.87% (95 runs sampled)
✓ bind() x 4,607,937 ops/sec ±0.91% (93 runs sampled)
Fastest is direct call
</code></pre>
<p>虽然 <code class="prettyprint">bind()</code> 的性能是超过了每秒百万次,但如果一处代码多次使用,性能就会急剧下降:</p>
<pre><code class="prettyprint lang-js">func.call(John)
func.call(Marry)
func.bind(John)();
func.bind(Marry)();
</code></pre>
<pre><code class="prettyprint">✓ call() x 9,328,537 ops/sec ±0.88% (93 runs sampled)
✓ bind() x 221,863 ops/sec ±1.92% (85 runs sampled)
</code></pre>
<p>因此建议:</p>
<ul>
<li>尽量不要依赖函数内的 <code class="prettyprint">this</code>,尽量采用直接传入参数来传递对象的方式</li>
<li>在 <code class="prettyprint">this</code> 对象固定,且会在多处使用的时候,优先考虑用类的形式来实现</li>
<li>多个函数的 <code class="prettyprint">this</code> 不固定的场合,如 <a href="http://koajs.com/#application">Koa Middleware</a>,通过 <code class="prettyprint">call()</code> 来传递 <code class="prettyprint">this</code> 对象</li>
</ul>
tag:blog.chrisyip.im,2014:Post/apm-upgrade-failed-to-change-folder-ownership-under-npm-cache
2014-10-02T04:28:35-07:00
2014-10-02T04:28:35-07:00
apm upgrade: Failed to change folder ownership under npm cache
<p>If you meet errors similar to the following one while upgrading <a href="https://github.com/atom/apm">apm</a> packages via command line:</p>
<pre><code class="prettyprint lang-shell">npm ERR! Failed to change folder ownership under npm cache for %s /Users/Chris/.atom/.node-gyp/.atom/.apm/_git-remotes/https-github-com-Glavin001-Coffee-Formatter-git-41cd2663
npm ERR! Error: ENOENT, readdir '/Users/Chris/.atom/.node-gyp/.atom/.apm/_git-remotes/https-github-com-Glavin001-Coffee-Formatter-git-41cd2663/description'
npm ERR! If you need help, you may report this *entire* log,
npm ERR! including the npm and node versions, at:
npm ERR! <http://github.com/npm/npm/issues>
</code></pre>
<p>Its mostly just because of there are not just one versions of <code class="prettyprint">node-gyp</code> under <code class="prettyprint">~/.atom/.node-gyp</code>.</p>
<p>In my case, there were 3 versions:</p>
<pre><code class="prettyprint lang-shell">ls ~/.atom/.node-gyp/.node-gyp
0.11.10 0.11.13 0.17.0
</code></pre>
<p>The way to fix this issue is very simple, just run the following command to force <a href="https://atom.io/">Atom</a> to recreate <code class="prettyprint">.node-gyp</code> folder:</p>
<pre><code class="prettyprint lang-shell">rm -dr ~/.atom/.node-gyp
</code></pre>
<p>Now, <code class="prettyprint">apm upgrade</code> works again.</p>
tag:blog.chrisyip.im,2014:Post/markdown-and-commonmark
2014-09-19T09:31:59-07:00
2014-09-19T09:31:59-07:00
Markdown and CommonMark
<p><a href="http://commonmark.org/">CommonMark</a>,最早的名字叫 Standard Markdown,后来迫于 <a href="http://daringfireball.net/projects/markdown/">Markdown</a> 原作者 <a href="http://daringfireball.net/">John Gruber</a> 的压力而改名。</p>
<p>虽然这已经是今年九月初的事情了,而且我在当时就已经表达了我的看法,但还是完整说说我的观点。</p>
<blockquote class="twitter-tweet" lang="en">
<p>我对的 Markdown 的看法是初始版本加上 GitHub Flavored Markdown 的代码块语法即可,其他的,算了吧,何必这么复杂?</p>— Chris (@chrisyipw) <a href="https://twitter.com/chrisyipw/status/507453067789217792">September 4, 2014</a>
</blockquote>
<p>Markdown 根据不同的需求、场景是可以被扩展成不同的 Markdown。比如说 <a href="https://help.github.com/articles/github-flavored-markdown">GitHub Flavored Markdown</a> 的代码块语法和 Task Lists 就是很好的例子。</p>
<p><a href="https://svbtleusercontent.com/q1o2a2i5hjxgpq.gif"><img src="https://svbtleusercontent.com/q1o2a2i5hjxgpq_small.gif" alt="GitHub Flavored Markdown Task Lists"></a></p>
<p>GitHub 针对 Markdown 所扩展的特性对程序员来说非常有用,但是对于一般的使用者呢?没用。</p>
<p>如果有了一个 CommonMark,这些特性要不要加进去标准里呢?</p>
<p>所以我认为 Markdown 应该保持它作为一个简洁高效的写作工具的纯粹性,剩下的就由特定群体去扩展、去维护。</p>
<p>请看 Gruber 通过 <a href="https://twitter.com/markdown">@Markdown</a> 账号所表达的意思:</p>
<blockquote class="twitter-tweet" lang="en">
<p>Markdown is a text-to-HTML syntax and conversion tool for writers, created by John Gruber: <a href="http://t.co/8VEYyW4">http://t.co/8VEYyW4</a></p>— Markdown (@markdown) <a href="https://twitter.com/markdown/status/72711470076526592">May 23, 2011</a>
</blockquote>
<blockquote class="twitter-tweet" lang="en">
<p>“Standard Markdown” is neither.</p>— Markdown (@markdown) <a href="https://twitter.com/markdown/status/507341395137658880">September 4, 2014</a>
</blockquote>
tag:blog.chrisyip.im,2014:Post/nodejs-travis-ci-and-coveralls
2014-09-17T10:07:57-07:00
2014-09-17T10:07:57-07:00
Node.js, Travis CI and Coveralls
<p>在 GitHub 或 npm 社区混多了,你肯定见过这些图示:</p>
<p><a href="https://svbtleusercontent.com/rhx0evqlzpaowa.png"><img src="https://svbtleusercontent.com/rhx0evqlzpaowa_small.png" alt="Badges"></a></p>
<p>这些图示是 <a href="http://shields.io/">shield.io</a> 提供的服务,当然,像 npm、build、coverage 这些是别的服务提供的:</p>
<ul>
<li>npm - <a href="https://david-dm.org/">david-dm.org</a>
</li>
<li>build - <a href="https://travis-ci.org/">travis-ci.org</a>
</li>
<li>coverage - <a href="https://coveralls.io/">coveralls.io</a>
</li>
</ul>
<p>其中 build 的作用是在你有 push 或 pull request 的时候,会执行设置好的 build 脚本,并记录 build 的详细日志:</p>
<p><a href="https://svbtleusercontent.com/cup3ghcnj6jysg.png"><img src="https://svbtleusercontent.com/cup3ghcnj6jysg_small.png" alt="Travis CI Project Page"></a></p>
<p>通过 <a href="https://travis-ci.org/">Travis CI</a> 可以很方便地执行自动化 build 测试,并且跟踪是否有 commit 会导致 build 错误。</p>
<p>而 <a href="https://coveralls.io/">Coveralls</a> 则是跟踪测试用例的覆盖程度。我之前在 <a href="https://github.com/chrisyip/node-extensions">node-extensions</a> 里添加了 <a href="http://visionmedia.github.io/mocha/Travis">Mocha</a> 测试,但是有部分没写测试用例,结果 push 到 npm 之后,在项目里更新之后引发别的包出错,排查后发现是没写测试用例的那部分代码造成的。像这种情况,就可以要求 coverage 达到 100% 才能用在项目里,这样就能大大提高健壮性。</p>
<h2 id="travis-ci_2">启用 Travis CI <a class="head_anchor" href="#travis-ci_2">#</a>
</h2>
<p>首先去到 Travis CI 官网使用 GitHub 账号登录,然后进行如下操作:</p>
<p><a href="https://svbtleusercontent.com/zomoeqgvecddsa.png"><img src="https://svbtleusercontent.com/zomoeqgvecddsa_small.png" alt="Travis CI Add Project"></a></p>
<p><a href="https://svbtleusercontent.com/4bokdikux4rg2w.png"><img src="https://svbtleusercontent.com/4bokdikux4rg2w_small.png" alt="Travis CI Enable Project"></a></p>
<p>完成后在项目根目录添加一个 <code class="prettyprint">.travis.yml</code> 文件,内容可以参考我的:</p>
<pre><code class="prettyprint lang-yml">language: node_js
node_js:
- "0.11"
- "0.10"
matrix:
allow_failures:
- node_js: "0.11"
fast_finish: true
script: "npm run-script test-travis"
after_script: "npm install coveralls && cat ./coverage/lcov.info | coveralls"
</code></pre>
<p><code class="prettyprint">language</code> 和 <code class="prettyprint">node_js</code>:告诉 Travis CI 这个项目是一个 Node.js 项目,并指定需要测试 v0.10 和 v0.11。</p>
<p><code class="prettyprint">matrix</code>:是额外配置,这里是告诉 Travis CI 把 v0.11 的 build 结果作为参考,不作为整体 build 成功或失败的依据。</p>
<p><code class="prettyprint">script</code>:告诉 Travis CI 使用哪个脚本进行 build。</p>
<p><code class="prettyprint">ater_script</code>:<code class="prettyprint">script</code> 执行完毕之后执行的脚本。在这里是把结果发送到 Coveralls。</p>
<p>随后 push 到 GitHub 即可,相当简单!</p>
<h2 id="coveralls_2">启用 Coveralls <a class="head_anchor" href="#coveralls_2">#</a>
</h2>
<p>依旧是先在 Coveralls 官网使用 GitHub 账号登录,然后进行如下操作:</p>
<p><a href="https://svbtleusercontent.com/luv9r17dafwpa.png"><img src="https://svbtleusercontent.com/luv9r17dafwpa_small.png" alt="Coveralls Add Repo 1"></a></p>
<p><a href="https://svbtleusercontent.com/zpsyoszwl7n7w.png"><img src="https://svbtleusercontent.com/zpsyoszwl7n7w_small.png" alt="Coveralls Add Repo 2"></a></p>
<p>完成后在项目里执行:</p>
<pre><code class="prettyprint lang-bash">npm install --save-dev mocha istanbul
</code></pre>
<p>在 <code class="prettyprint">package.json</code> 中的 <code class="prettyprint">scripts</code> 添加一个 <code class="prettyprint">test-travis</code> 字段:</p>
<pre><code class="prettyprint lang-json">"scripts": {
"test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --reporter spec --check-leaks test/"
}
</code></pre>
<p>Coveralls 依赖 LCOV 进行 coverage 的分析,因此需要使用 <a href="https://www.npmjs.org/package/istanbul">istanbul</a> 来生成报告。然后通过在 <code class="prettyprint">.travis.yml</code> 中的 <code class="prettyprint">after_script</code> 将结果传送给 Coveralls:</p>
<pre><code class="prettyprint lang-yml">after_script: "npm install coveralls && cat ./coverage/lcov.info | coveralls"
</code></pre>
<p>注意的是,我将测试代码放在 <code class="prettyprint">test/</code> 下,需要根据实际需要修改。</p>
<p>把改动推送到 GItHub 即可。</p>
<p><em>注:以上例子均使用主分支 (默认分支) ,如果需要指定分支,请查阅文档。</em></p>
<h2 id="conclusion_2">Conclusion <a class="head_anchor" href="#conclusion_2">#</a>
</h2>
<p>Travis CI 和 Coveralls 的设置都非常简单,但却可以让开发变得更简单——只要提交代码即可。</p>
<p>对于前端来说,测试用例可能不太好写,但可以从一些简单的入手,如 JSHint / ESLint 代码检查、RequireJS / Browserify 代码打包等等。在这方面,<a href="https://github.com/jquery/jquery">jQuery</a> 是一个非常好的学习例子。</p>
tag:blog.chrisyip.im,2014:Post/grunt-and-gulp
2014-07-01T07:20:46-07:00
2014-07-01T07:20:46-07:00
Grunt and gulp.js
<iframe src="//slides.com/chrisyipw/grunt-and-gulpjs/embed" width="576" height="420"></iframe>
<p>这是为敝司暑期实习生准备的 Grunt 和 gulp.js 入门的 slide,就目前而言,我倾向于在项目中使用 gulp.js,在小插件、module 上使用 Grunt。</p>
tag:blog.chrisyip.im,2014:Post/nodejs-101-generator
2014-07-01T07:00:46-07:00
2014-07-01T07:00:46-07:00
Node.js 101: generator
<p>之前介绍了 <a href="http://blog.chrisyip.im/nodejs-101-package-promise-and-async">Promise and async</a>,现在来说说 ECMAScript 6 新加入的 <a href="http://wiki.ecmascript.org/doku.php?id=harmony:generators">Generator</a>。</p>
<blockquote>
<p>In computer science, a generator is a special routine that can be used to control the iteration behaviour of a loop. In fact, all generators are iterators. – <a href="http://en.wikipedia.org/wiki/Generator_(computer_programming)">Wikipedia</a></p>
</blockquote>
<p>Generator 是一种控制程序迭代的方法,让顺序执行的迭代程序变成异步执行 (此异步和 Node.js 的异步调用有差异) 。在 Node.js 里,它的存在目的就是可以写出更好的控制流。</p>
<p>假设有如下代码:</p>
<pre><code class="prettyprint lang-js">function loop (arr, cb) {
for (let i = 0, len = arr.length; i < len; i++) {
cb(arr[i])
}
}
</code></pre>
<p>如果需要扩展这个代码,增加更多的回调:</p>
<pre><code class="prettyprint lang-js">function loop (arr, cb) {
for (let i = 0, len = arr.length; i < len; i++) {
for (let j = 0, jlen = cb.length; j < jlen; j++) {
cb[j](arr[i])
}
}
}
loop(arr, [cb1, cb2, cb3, cb4, ...cbN])
</code></pre>
<p>换成 generator 就可以这样:</p>
<pre><code class="prettyprint lang-js">function* loop (arr) {
for (let i = 0, len = arr.length; i < len; i++) {
yield arr[i]
}
}
var ge = loop(['Phil Colson', 9])
var name = ge.next().value
, profile = getProfile(name)
accessLog(name)
var accessLevel = ge.next().value
if (profile.level !== accessLevel) {
throw new Error('You don\'t have privilege!')
}
if (ge.next().done) {
loginDone(profile)
}
</code></pre>
<p>从例子可以看到,generator 和一般的函数并没有太大的区别,仅仅需要在 <code class="prettyprint">function</code> 后加一个星号即可,而最大差别是,函数是直接调用并返回结果,而 generator 则是调用后,创建并返回一个 constructor 为 <code class="prettyprint">GeneratorFunctionPrototype</code> 的对象,然后通过它的 <code class="prettyprint">next</code> 方法来让函数执行并获取 <code class="prettyprint">yield</code> 语句的值:</p>
<pre><code class="prettyprint lang-js">function* ge () { yield 1 }
var g = ge()
g.next()
// { value: 1, done: false }
</code></pre>
<p><code class="prettyprint">yield</code> 关键字是 generator 的核心,当 <code class="prettyprint">next</code> 被调用时,将会执行 generator 内的代码,一旦遇到 <code class="prettyprint">yield</code> 时将停止执行,并等待下一个 <code class="prettyprint">next</code> 的调用,不停重复,直到所有的代码执行完毕。这赋予了函数异步执行的能力。</p>
<pre><code class="prettyprint lang-js">function* ge () {
console.log('called')
yield 1
console.log('yield called')
}
var g = ge()
g.next()
// 'called'
// { value: 1, done: false }
g.next()
// 'yield called'
// { value: undefined, done: true }
</code></pre>
<p><code class="prettyprint">yield</code> 后面可以是值、对象、函数或表达式,<code class="prettyprint">next</code> 调用时返回的 <code class="prettyprint">value</code> 默认是 <code class="prettyprint">yield</code> 右侧语句的值,那像 <code class="prettyprint">var x = yield 1</code>、<code class="prettyprint">fn(yield ARGV)</code> 这样的语句呢?</p>
<pre><code class="prettyprint lang-js">function* ge () {
var x = yield 10;
console.log(x)
return x + 5
}
var g = ge()
g.next()
// { value: 10, done: false }
g.next(1)
// 1
// { value: 6, done: true }
g = ge()
g.next()
// { value: 10, done: false }
g.next()
// undefined
// { value: NaN, done: true )
</code></pre>
<p>第一个 <code class="prettyprint">next().value</code> 会是 <code class="prettyprint">yield</code> 右侧语句的值,而第二个 <code class="prettyprint">next()</code> 的参数将会传递给 <code class="prettyprint">yield</code> 左侧的语句;如果 <code class="prettyprint">next()</code> 没参数,将是 <code class="prettyprint">undefined</code>。利用这个特性,就可以和 Promise 等组合在一起,实现更方便的功能,这点会在后面介绍。</p>
<p><code class="prettyprint">done</code> 标记是否所有代码已执行完毕。</p>
<p>上面的 <code class="prettyprint">loop</code> 经改造成 generator 后,三个 <code class="prettyprint">ge.next()</code> 分别返回:</p>
<ol>
<li><code class="prettyprint">{ value: 'Phil Colson', done: false }</code></li>
<li><code class="prettyprint">{ value: 9, done: false }</code></li>
<li><code class="prettyprint">{ value: undefined, done: true }</code></li>
</ol>
<p>注意,最后一个 <code class="prettyprint">next</code> 被调用时,它的 <code class="prettyprint">value</code> 等于 generator function 的返回值,如无 <code class="prettyprint">return</code>,将会是 <code class="prettyprint">undefined</code>:</p>
<pre><code class="prettyprint lang-js">(function* demo () {
return 'demo'
})().next()
// { value: 'demo', done: true }
</code></pre>
<p>Generator 的基础介绍完毕,开始利用 generator 改造 <a href="https://github.com/chrisyip/Sagase/blob/0296a15b6195124e09586a3b8ee0b7ddb438747b/lib/sagase.js#L18">readFiles</a>。</p>
<p>首先把 generator 封装成返回 Promise 的函数:</p>
<pre><code class="prettyprint lang-js">// https://www.promisejs.org/generators/
function async (makeGenerator) {
return function () {
var generator = makeGenerator.apply(null, arguments);
function handle(result){
if (result.done) {
return Promise.resolve(result.value);
}
return Promise.resolve(result.value).then(function (res){
return handle(generator.next(res));
}, function (err){
return handle(generator.throw(err));
});
}
try {
return handle(generator.next());
} catch (ex) {
return Promise.reject(ex);
}
};
}
</code></pre>
<p><code class="prettyprint">async</code> 通过 <code class="prettyprint">next()</code> 把 <code class="prettyprint">value</code> 给传递到下一个 <code class="prettyprint">Promise.resolve</code> 里,再通过 <code class="prettyprint">next(res)</code> 把结果传递到 <code class="prettyprint">yield</code> 的左边,从而让程序可以像一个同步运行的程序一样。不停重复这几步,直到 <code class="prettyprint">next().done === true</code> 为止。</p>
<p>接着要把 <code class="prettyprint">fs</code> 的接口封装成 Promise 模式,这里使用了 <a href="https://www.npmjs.org/package/fs-extra">fs-extra</a> 和 <a href="https://www.npmjs.org/package/bluebird">bluebird</a> 这两个库:</p>
<pre><code class="prettyprint lang-js">const Promise = require('bluebird'),
fs = Promise.promisifyAll(require('fs-extra'))
</code></pre>
<p>经过 <code class="prettyprint">bluebird.promisifyAll</code> 处理的对象的所有函数将被封装成 Promise 模式,封装后的函数一般会在函数名后面增加 <code class="prettyprint">Async</code>,如 <code class="prettyprint">fs.readdirAsync</code>。</p>
<p>经过这两步处理,<code class="prettyprint">readFiles</code> 就可以改造成如下所示 (为了缩减代码行数,移除了过滤功能) :</p>
<pre><code class="prettyprint lang-js">function* readFiles (opts) {
var res = [], files
files = yield fs.readdirAsync(opts.folder);
for (var i = 0, len = files.length; i < len; i++) {
var file = files[i],
fullPath = path.resolve(opts.folder + '/' + file).replace(process.cwd(), '.'),
stat = yield fs.statAsync(fullPath)
if (stat.isDirectory()) {
res = res.concat(
yield async(readFiles)(_.assign({}, opts, { folder: fullPath }))
)
} else if (opts.pattern.test(file)) {
res.push(fullPath)
}
}
return res
}
function Sagase () {
Object.defineProperties(
this,
{
find: {
enumerable: true,
value: function (opts) {
return async(readFiles)(formatOptions(opts))
}
}
}
)
}
</code></pre>
<p>经过这样的处理后,<code class="prettyprint">readFiles</code> 少了一些多余的嵌套,又保留了该有的异步能力。</p>
<p>Generator 的概念其实就是<a href="http://zh.wikipedia.org/wiki/%E6%83%B0%E6%80%A7%E6%B1%82%E5%80%BC">惰性求值</a>,通过暂停函数执行来让开发者控制程序的<a href="http://blog.chrisyip.im/control-flow-in-javascript">控制流</a>,而不是像<a href="http://zh.wikipedia.org/wiki/%E5%8F%8A%E6%97%A9%E6%B1%82%E5%80%BC">及早求值</a>那样,语句到那里了就需要立刻求出值。</p>
<p>Generator 并不是用来代替别的控制流的灵丹妙药,它只是一种可能性,让你写出更好的代码的可能性,<a href="http://koajs.com/">Koa</a> 的官方例子就很好地解释了这一点:</p>
<p><img src="http://f.cl.ly/items/342I3N463F0z0p1F1s1F/1-J9xwCLo1jeQhb23KbHy9eA.png" alt="How generators work in Koa"></p>
<p><em>(via <a href="https://medium.com/koa-next-generation-web-framework-for-node-js/working-with-koa-and-generators-1776085cb4af">Christoffer Hallas</a></em></p>
<p>是不是很像 DOM 的冒泡事件呢?</p>
tag:blog.chrisyip.im,2014:Post/nodejs-101-package-promise-and-async
2014-06-24T07:05:25-07:00
2014-06-24T07:05:25-07:00
Node.js 101: Promise and async
<p>先回顾一下 <a href="">Sagase</a> 的项目结构:</p>
<pre><code class="prettyprint">lib/
cli.js
sagase.js
Gruntfile.js
package.json
</code></pre>
<p>上一篇讲了 <code class="prettyprint">package.json</code>,这一篇讲 <code class="prettyprint">lib/sagase.js</code>。</p>
<p>因为代码比较长,就分开一节节地讲,完整的点开 <a href="https://github.com/chrisyip/Sagase/blob/0296a15b6195124e09586a3b8ee0b7ddb438747b/lib/sagase.js">GitHub</a> 看吧。</p>
<pre><code class="prettyprint lang-js">'use strict';
</code></pre>
<p>通知编译器进入 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/Strict_mode">strict mode</a>,主要的作用是让编译器帮你检查一些坑,有经验的 JavaScript 开发者可以忽略。</p>
<pre><code class="prettyprint lang-js">var fs = require('fs-extra'),
async = require('async'),
_ = require('lodash'),
Promise = require('bluebird'),
path = require('path')
</code></pre>
<p>引用所有依赖项,如果是使用 <code class="prettyprint">--harmony</code>,建议使用 <code class="prettyprint">const</code> 代替 <code class="prettyprint">var</code> 关键字,可避免变量被修改。</p>
<pre><code class="prettyprint lang-js">var mar = 'March'
mar = 'June'
console.log(mar) // 'June'
const july = 'July'
july = 'May'
console.log(july) // 'July'
</code></pre>
<p><code class="prettyprint">function readFiles (opts) {}</code> 包含很多信息,一个一个说。</p>
<pre><code class="prettyprint lang-js">return new Promise(function (resolve, reject) {}
</code></pre>
<p>返回一个 <a href="https://www.promisejs.org/">Promise</a> 对象。</p>
<p>由于 Node.js 的特点是异步,一般都需要通过异步来处理:</p>
<pre><code class="prettyprint lang-js">// get all files in DIR
fs.readdir(DIR, function (err, files) {
if (err) {
return errorHandler(err)
}
// loop files
files.forEach(function (file) {
// get the stat of each files
fs.stat(file, function (err, stat) {
// if it's file
if (stat.isFile()) {
// get content of file
fs.readFile(file, function (err, buff) {
// do whatever you want
})
}
})
})
})
</code></pre>
<p>如果需要在一个函数里处理很多事情,甚至说需要让这个函数的返回结果可在多个文件里使用,只靠回调会很吃力——不知道哪个文件在什么时候才需要使用它的返回结果。</p>
<p>如果使用 Promise,就会简单很多:</p>
<pre><code class="prettyprint lang-js">// in a.js
var Promise = require('bluebird'),
readFile
module.exports = new Promise(function (resolve, reject) {
fs.readdir(DIR, function (err, files) {
err ? reject(err) : resolve(files)
})
})
</code></pre>
<pre><code class="prettyprint lang-js">// in b.js
var readFile = require('./a.js')
readFile
.then(function (files) {
// do something with files
return NEW_RESULT;
}, function (err) {
// handle error here
})
.then(function (data) {
// do something with NEW_RESULT
}, function (err) {
// handle error here
})
</code></pre>
<pre><code class="prettyprint lang-js">// in c.js
var readFile = require('./a.js')
readFile.then(function (files) {
// the files still exist and accessable
})
</code></pre>
<p>通过 Promise 的封装,就可以让 <code class="prettyprint">resolve</code> 的结果跨不同的文件,不需要在一个回调函数里处理所有事情;另外,通过 <code class="prettyprint">.then()</code> 的第二个函数处理 <code class="prettyprint">reject</code> 的错误结果,避免了多重判断。</p>
<p>注意的是,在这里引入了一个叫 <a href="https://github.com/petkaantonov/bluebird">bluebird</a> 的第三方 Promise 库,如果 Node.js 版本是 <code class="prettyprint">>= 0.11.13</code> 的话,是不需要引入第三方库的。</p>
<pre><code class="prettyprint lang-js">async.each(
files,
function (file, cb) {
},
function (err) {
err ? reject(err) : resolve(res)
}
)
</code></pre>
<p><a href="https://github.com/caolan/async">async</a> 是一个改善异步回调流程、把异步处理能力赋予普通数组处理函数的库,比如 <code class="prettyprint">async.each</code> 就相当于多线程版的 <code class="prettyprint">Array.forEach</code>,不过在实际使用中,不要期待执行顺序是乱序或者正序,关键是第三个参数。</p>
<pre><code class="prettyprint lang-js">async.each([1, 2, 3], function (item, next) {
console.log(item)
next()
}, function (err) {
console.log('all tasks done')
})
// output:
// 1
// 2
// 3
// "all tasks done"
</code></pre>
<p>一般来说,因为 <code class="prettyprint">fs.stat</code> 是异步调用的,所以 <code class="prettyprint">Array.forEach</code> 遍历完数组之后,很难保证里面的任务是否全部已完成,这时候调用 <code class="prettyprint">Promise.resolve()</code> 就无法保证数据的正确性。而通过 <code class="prettyprint">async.each</code> 的第三个参数,就可以得知任务的状态,并保证 Promise 可以得到正确的数据。</p>
<pre><code class="prettyprint lang-js">path.resolve(opts.folder + '/' + file).replace(process.cwd(), '.')
</code></pre>
<p><code class="prettyprint">path</code> 是一个用于解决和目录有关问题的库。<code class="prettyprint">path.resolve</code> 会将 <code class="prettyprint">./dir</code> 转变为 <code class="prettyprint">/Users/USER/PATH/TO/dir</code> (Mac) 格式的完整目录。</p>
<p><code class="prettyprint">process.cwd()</code> 会返回调用这个脚本的进程所在目录;另外,还有一个 <code class="prettyprint">__dirname</code> 是指脚本所在目录:</p>
<p>假设有如下文件:</p>
<pre><code class="prettyprint">lib/index.js
index.js
</code></pre>
<pre><code class="prettyprint lang-js">// in lib/index.js
module.exports = function () {
return __dirname
}
</code></pre>
<pre><code class="prettyprint lang-js">// in index.js
console.log(process.cwd()) // '/Users/USER/PATH/'
console.log(require('./lib')()) // '/Users/USER/PATH/lib/'
</code></pre>
<p>剩下的代码不一一解释,大致做了以下工作:</p>
<ol>
<li>先读取指定目录的所有文件 (<code class="prettyprint">fs.readdir</code>)</li>
<li>使用 <code class="prettyprint">async.each</code> 遍历获取的结果</li>
<li>判断每个文件的 stat 是不是目录 (<code class="prettyprint">fs.stat</code>)
<ol>
<li>若是,检查符不符合条件,符合则进入下一轮递归 (<code class="prettyprint">readFiles</code>)</li>
<li>若否,检查符不符合条件,符合则添加到最终结果的数组中 (<code class="prettyprint">res.push()</code>)</li>
</ol>
</li>
<li>重复 1-3 直至遍历完所有文件</li>
</ol>
<p>需注意的是:</p>
<ul>
<li>
<code class="prettyprint">cb()</code> 在多处出现,用于通知 <code class="prettyprint">async.each</code> 这个任务已执行完毕</li>
<li>递归中用 Promise <code class="prettyprint">readFiles.then()</code> 的第一个参数处理 <code class="prettyprint">res</code> 的返回、用 <code class="prettyprint">readFiles.catch</code> 处理 <code class="prettyprint">error</code>、用 <code class="prettyprint">readFiles.finally</code> 处理 <code class="prettyprint">cb()</code> (因必须通知 async 任务已完成,在此处统一处理)</li>
<li>
<code class="prettyprint">Promise.finally</code> 是 bluebird 特有的 API,原生 Promise 需这样实现:<code class="prettyprint">readFiles.then().catch().then()</code>,第二个 <code class="prettyprint">then</code> 相当于 <code class="prettyprint">finally</code> (不过不够直观)</li>
</ul>
<pre><code class="prettyprint lang-js">function formatOptions (opts) {}
</code></pre>
<p>用于格式化传入的参数,就不多解释了。</p>
<p>需要注意的是,在这里用了一个 <code class="prettyprint">opts</code> 对象来包含所有参数并传递给 <code class="prettyprint">readFiles</code>。由于 JavaScript 的特性,用一个 JSON 对象传递参数会方便很多,比如说:</p>
<pre><code class="prettyprint lang-js">function formatOptions (folder, pattern, ignoreCase, nameOnly, exclude, excludeNameOnly, recusive) {}
</code></pre>
<p>由于 Node.js 用的 V8 仍未包含<a href="http://wiki.ecmascript.org/doku.php?id=harmony:parameter_default_values">函数参数默认值</a>,所以最方便的做法是用 JSON 对象:</p>
<pre><code class="prettyprint lang-js">var options = _.assign({}, {
key_1: default_value_1,
key_2: default_value_2
key_3: default_value_3
}, opts)
</code></pre>
<p>并且 JSON 对象也利于扩展——无论是增加还是删除 key,都不需要更改接口。</p>
<p>最后,定义了一个 <code class="prettyprint">Sagase</code> 类,并在外部调用这个类时,创建一个新的 <code class="prettyprint">Sagase</code> 对象:</p>
<pre><code class="prettyprint lang-js">function Sagase () {
Object.defineProperties(
this,
{
find: {
enumerable: true,
value: function (opts) {
return readFiles(formatOptions(opts))
}
}
}
)
}
module.exports = new Sagase()
</code></pre>
<p><code class="prettyprint">Object.defineProperty</code> 和 <code class="prettyprint">Object.defineProperties</code> 是 ECMAScript 5 中新加入的特性,通过它们就可以创建很多好玩的东西,比如传统的 <code class="prettyprint">jQuery.fn.size()</code>:</p>
<pre><code class="prettyprint lang-js">jQuery.fn.size = function () {
return Number.MAX_SAFE_INTEGER
}
var body = $('body')
body.length // 1
body.size() // 9007199254740991
</code></pre>
<p>换成 ES 5 的写法:</p>
<pre><code class="prettyprint lang-js">Object.defineProperties(
jQuery.fn,
{
size: {
enumerable: true,
get: function () {
return this.length
}
}
}
)
var body = $('body')
body.length // 1
body.size // 1
jQuery.fn.size = function () {
return Number.MAX_SAFE_INTEGER
}
body.size // 1
body.size() // TypeError: number is not a function
</code></pre>
<p>合理利用 <code class="prettyprint">const</code> 和 <code class="prettyprint">Object.defineProperty</code> 可以避开一些非预期的情况,保证程序健壮性。</p>
<p>从 <code class="prettyprint">lib/sagase.js</code> 的代码可以看出,Node.js 的异步特性导致函数是一层套一层 (`fs.readdir -> fs.stat -> isDirectory()) ,写起来其实不好看,也不利于理解,比如:</p>
<pre><code class="prettyprint lang-js">function getJSON () {
var json
fs.readFile('demo.json', function (err, buff) {
json = JSON.parse(buff.toString())
})
return json
}
getJSON() // undefined
</code></pre>
<p>当然,要在 Node.js 里使用同步接口也是可以的,如 <code class="prettyprint">fs.readdirSync</code>,但:</p>
<ul>
<li>Node.js 同步接口不见得比 Python、Ruby 等语言高效</li>
<li>不是所有接口都有同步版本,如 <code class="prettyprint">child_process.exec</code>
</li>
</ul>
<p>为了发挥 Node.js 的优势,就需要正确利用 Promise、async 来编写程序。比如说有这样的一个场景,浏览器端需要获取购物车里所有商品、赠品的数据,常见的步骤大概是:找商品数据,通过商品 ID 找促销规则得到赠品,计算总价,返回结果。这些步骤可以通过多次请求数据库最后用后端语言拼接;如果是 RESTful API 模式,也可以发起多次请求,最后在浏览器端拼接。</p>
<p>如果在浏览器端和服务器端加入异步处理呢?</p>
<pre><code class="prettyprint lang-js">var router = express.Router()
router.get('/cart', function (req, res, next) {
async.parallel(
[
// get all products data
function (next) {
request('/api/products', OPTIONS)
.then(function (data) {
next(null, data)
})
},
// get products gifts
function (next) {
async.map(
PRODUCT_LIST,
function (p, cb) {
request('/api/product/:id/gifts', OPTIONS)
.then(function (data) {
cb(null, data)
})
},
function (err, results) {
next(null, results)
}
)
}
],
function (err, results) {
RESPONSE_BODY = {
products: results[0],
gifts: results[1],
total: calcTotal(results[0])
}
res.send(RESPONSE_BODY)
}
)
})
</code></pre>
<pre><code class="prettyprint lang-js">$.ajax('/cart').then(function () {
// handle products and gifts here
})
</code></pre>
<p>通过异步的处理,浏览器就可以用一次请求完成多次请求的效果,并且不会破坏 RESTful API 的结构。这对于资源紧张、网络环境多变的移动端来说,是非常有利的;而对于电脑端则通过减少请求时间来提高交互响应速度,提高用户体验。</p>
<p>这一篇主要内容是怎样利用 Promise、async 等库绕开 Node.js 的回调函数坑。回调函数算是 Node.js 最多人黑的地方,如果不能掌控它,写出来的 Node.js 代码将会相当丑陋、不易维护。</p>
<p>而为了让代码好看一些,ECMAScript 6 里加入了一个新特性——<a href="http://wiki.ecmascript.org/doku.php?id=harmony:generators">Generator</a>。<a href="http://koajs.com/">Koa</a> 已经开始使用 generator 来构建项目,具体怎么用,下一篇说吧。</p>
tag:blog.chrisyip.im,2014:Post/nodejs-101-create-a-package
2014-06-24T07:01:15-07:00
2014-06-24T07:01:15-07:00
Node.js 101: create a package
<p>这是我为公司同事科普如何开发 <a href="http://nodejs.org/">Node.js</a> app 的系列文章。</p>
<h1 id="nodejs_1">什么是 Node.js? <a class="head_anchor" href="#nodejs_1">#</a>
</h1><blockquote>
<p>Node.js is a platform built on Chrome’s JavaScript runtime for easily building fast, scalable network applications. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient, perfect for data-intensive real-time applications that run across distributed devices.</p>
</blockquote><h2 id="_2">如何安装? <a class="head_anchor" href="#_2">#</a>
</h2>
<p><a href="http://nodejs.org/download/">Download</a> 或 <code class="prettyprint">brew install node</code>。</p>
<h2 id="nodejs_2">如何运行 Node.js <a class="head_anchor" href="#nodejs_2">#</a>
</h2>
<pre><code class="prettyprint lang-bash"># bash script, works on Linux / Mac
touch demo.js
echo "console.log('hello world)" > demo.js
node --harmony demo.js
# output "hello world"
node --hamrony -e "console.log('hi there')"
# output "hi there"
</code></pre>
<h2 id="code-classprettyprintharmonycode_2">什么是 <code class="prettyprint">--harmony</code> <a class="head_anchor" href="#code-classprettyprintharmonycode_2">#</a>
</h2>
<p><a href="http://wiki.ecmascript.org/doku.php?id=harmony:harmony">Harmony</a> 可以理解为一个为了解决 ECMAScript / JavaScript 混乱局面的项目。</p>
<p>作为一个 JavaScript 开发者都应该去了解 ECMAScript 5,作为一个 Node.js 开发者更应该去了解 ECMAScript 6。</p>
<p>因此,Chrome 在内核里加入 ECMAScript 5 / 6 的内容,并提供选项,供开发者尽早试验新的特性;而基于 V8 引擎的 Node.js 自然也会有,这个选项就是 <code class="prettyprint">--harmony</code>。</p>
<p>具体的细节请善用 Google,Node.js 能用的 ES 6 特性请查阅 GitHub 上的 <a href="https://github.com/JustinDrake/node-es6-examples">node-es6-examples</a>。</p>
<p>只要记住跑 Node.js 脚本时,加上 <code class="prettyprint">--harmony</code> 即可:<code class="prettyprint">node --harmony</code>。</p>
<p>Linux / Mac 使用者可以建立别名:<code class="prettyprint">alias node='node --harmony'</code>。</p>
<p>Windows?再见。</p>
<p>注:本系列所有代码均运行在 Harmony 模式下,Node.js 版本为 <code class="prettyprint">0.11.13</code>。</p>
<h1 id="nodejs_1">如何写一个 Node.js 项目 <a class="head_anchor" href="#nodejs_1">#</a>
</h1>
<p>如果一个教程只是围绕 <code class="prettyprint">hello world</code> 就太没趣了,本文将通过 <a href="https://github.com/chrisyip/Sagase">Sagase</a> package 介绍如何写一个实际有用的项目。</p>
<p>Sagase 项目是提供一个使用正则搜索文件的工具,包括命令行和用于 Node.js 的库。项目结构如下:</p>
<pre><code class="prettyprint">lib/
cli.js
sagase.js
Gruntfile.js
package.json
</code></pre>
<p>先记住这么多,稍后一一解释。</p>
<h2 id="nodejs-package_2">Node.js package <a class="head_anchor" href="#nodejs-package_2">#</a>
</h2>
<p>Node.js package 是 Node.js 的代码包 (集合) ,可以是:</p>
<ul>
<li>方法、类等 (像 <code class="prettyprint">stdio.h</code>) </li>
<li>用于命令行的工具</li>
<li>web app 之类的项目</li>
</ul>
<p>如果用过 Ruby 的 <a href="https://rubygems.org/">Gems</a>、Python 的 <a href="https://github.com/pypa/pip">pip</a>,应该很容易就能理解。</p>
<p>几乎所有的 Node.js 项目都是一个 package——虽然也可以用独立的 <code class="prettyprint">.js</code>,但效率将大打折扣。</p>
<h3 id="npm_3">npm <a class="head_anchor" href="#npm_3">#</a>
</h3>
<p>Node.js package 通过 <a href="https://www.npmjs.org/">npm</a> 管理,常用命令:</p>
<pre><code class="prettyprint">npm init
npm install PACKAGE
npm uninstall PACKAGE
npm update
</code></pre>
<h2 id="nodejs_2">创建 Node.js 项目 <a class="head_anchor" href="#nodejs_2">#</a>
</h2>
<p>在命令行执行 <code class="prettyprint">npm init</code>,回答若干问题,或者一路回车,将会自动创建一个典型的 <code class="prettyprint">package.json</code>,这时候就可以开始写 Node.js 项目了。</p>
<h2 id="packagejson_2">package.json <a class="head_anchor" href="#packagejson_2">#</a>
</h2>
<p>每一个 Node.js 项目基本会有一个 <code class="prettyprint">package.json</code>,用于描述这个包功能、作者等信息,同时也包括运行环境、依赖项等等重要信息。一般长这样:</p>
<pre><code class="prettyprint lang-js">{
"name": "sagase",
"version": "0.1.1",
"main": "lib/sagase.js",
"description": "Searching files recursively.",
"bin": {
"sagase": "lib/cli.js"
},
"dependencies": {
"async": "^0.9.0",
"bluebird": "^2.1.2",
"chalk": "^0.4",
"fs-extra": "^0.8",
"lodash": "^2.4.1",
"nomnom": "^1.6"
},
"devDependencies": {
"grunt-contrib-jshint": "~0.7.0",
"grunt-contrib-watch": "~0.5.0",
"jshint-stylish": "~0.1.3",
"load-grunt-tasks": "~0.2.0",
"time-grunt": "~0.2.0"
},
"engines": {
"node": ">= 0.8"
}
}
</code></pre>
<p>不太重要的就略过,请自行查看<a href="https://www.npmjs.org/doc/package.json.html">官方文档</a>。</p>
<pre><code class="prettyprint">"main": "lib/sagase.js"
</code></pre>
<p>指明这个包的入口,就像 C 的 <code class="prettyprint">main</code>。如果包的根目录存在 <code class="prettyprint">index.js</code>,即使没有 <code class="prettyprint">main</code> 也没问题的,否则引用这个包的时候将会报错:<code class="prettyprint">Error: Cannot find module 'xxxx'</code>。建议遵循 <code class="prettyprint">index.js</code> 命名习惯。</p>
<pre><code class="prettyprint">"bin": {
"sagase": "lib/cli.js"
}
</code></pre>
<p>告诉 npm 将 <code class="prettyprint">lib/cli.js</code> 软链接到系统的可执行文件目录,并命名为 <code class="prettyprint">sagase</code>,这样就可以在命令行里直接使用这个包。</p>
<pre><code class="prettyprint">"dependencies": {
"async": "^0.9.0",
"bluebird": "^2.1.2",
"chalk": "^0.4",
"fs-extra": "^0.8",
"lodash": "^2.4.1",
"nomnom": "^1.6"
},
"devDependencies": {
"grunt-contrib-jshint": "~0.7.0",
"grunt-contrib-watch": "~0.5.0",
"jshint-stylish": "~0.1.3",
"load-grunt-tasks": "~0.2.0",
"time-grunt": "~0.2.0"
}
</code></pre>
<p><code class="prettyprint">dependencies</code> 和 <code class="prettyprint">devDependencies</code> 代表着这个项目依赖什么包。npm 在 <code class="prettyprint">npm install</code> 的时候,会根据这个包所在的环境安装相应的依赖包,并存放在项目根目录的 <code class="prettyprint">node_modules</code> 文件夹,当需要使用时就像官方包一样即可:</p>
<pre><code class="prettyprint lang-js">const async = require('async'), fs = require('fs')
</code></pre>
<p><code class="prettyprint">devDependencies</code> 和 <code class="prettyprint">dependencies</code> 的区别在于,如果这个包是项目的主包,<code class="prettyprint">npm install</code> 时将会同时安装所有依赖包;如果这个包是别的项目的依赖包,<code class="prettyprint">devdependencies</code> 将被忽略,不会安装。</p>
<p>因此,一些代码测试、打包等流程需要用到的包就可以放在 <code class="prettyprint">devDependencies</code>。当在别的项目里使用这个包时,就不用浪费时间安装这些包。</p>
<p>而 <code class="prettyprint">^0.9.0</code> 则是指定可安装的版本范围,此例指 <code class="prettyprint">>= 0.9.0 && < 0.10.0</code>。</p>
<pre><code class="prettyprint lang-js">"engines": {
"node": ">= 0.8"
}
</code></pre>
<p>指定这个包运行时所需的 Node.js 版本,避免 API 版本不一致造成运行失败。建议 <code class="prettyprint">>= 0.10</code> 或 <code class="prettyprint">>= 0.11.9</code>。</p>
<p>关键部分就这么多,上述的 package.json 将允许我们可以通过下述方式使用这个包。</p>
<p>首先是命令行模式,因为 Sagase 已发布到 npm 的公共服务器,可以通过 <code class="prettyprint">-g</code> 参数安装到全局环境:</p>
<pre><code class="prettyprint lang-bash">npm install -g sagase
sagase ./assets .js$ .min.js
</code></pre>
<p>在 Node.js 项目中使用则无需安装到全局环境,但一般使用 <code class="prettyprint">--save</code> 在安装的同时把这个包添加到项目的 package.json:</p>
<pre><code class="prettyprint lang-bash">npm install --save sagase
</code></pre>
<pre><code class="prettyprint lang-js">var find = require('sagase').find
find({
folder: './',
pattern: /.js$/,
exclude: /.min.js/
}).then(function (files) {
// do whatever you want
})
</code></pre>
<p>这一篇到此结束,下一篇开始讲开发部分。</p>
tag:blog.chrisyip.im,2014:Post/control-flow-in-javascript
2014-06-24T06:45:31-07:00
2014-06-24T06:45:31-07:00
Control Flow in JavaScript
<p>这是一篇我在<a href="http://feifei.com/">飞飞商城</a>针对 JavaScript Control Flow 的演讲稿文字版。</p>
<hr>
<h3 id="control-flow_3">Control Flow 是什么? <a class="head_anchor" href="#control-flow_3">#</a>
</h3>
<p>Control Flow 就是<a href="http://en.wikipedia.org/wiki/Control_flow">控制流</a>,简单地归纳,就是代码执行的顺序(和控制的手段)。</p>
<p>常见的控制流有:</p>
<ul>
<li>if-else</li>
<li>switch-case</li>
<li>for / while loop</li>
<li>goto</li>
</ul>
<h3 id="javascript_3">问题儿:JavaScript <a class="head_anchor" href="#javascript_3">#</a>
</h3>
<p>由于 JavaScript 的运行环境,决定了她拥有异步执行的能力。</p>
<pre><code class="prettyprint lang-js">if (checkEmail() === false ||
checkUsername() === false) {
// validation failed
}
</code></pre>
<p>如果是使用 ajax 来做验证的话,就不能如此实现。</p>
<pre><code class="prettyprint lang-js">function checkEmail () {
$.get({
url: 'api/check_email.json'
})
// where is the return?!
}
function checkUsername () {
$.get({
url: 'api/check_username.json'
})
// where is the return?!
}
</code></pre>
<p>如果使用 <code class="prettyprint">async: false</code>,那就会造成浏览器的阻塞。</p>
<p>怎么办呢?幸好,解决方案是有的。</p>
<h3 id="callback_3">Callback <a class="head_anchor" href="#callback_3">#</a>
</h3>
<p>亦即<strong>回调函数</strong>。</p>
<pre><code class="prettyprint lang-js">function checkEmail (data, callback) {
$.get({
url: 'api/check_email.json',
data: data,
success: function (data) {
callback.success.call(null, data)
},
error: function (err) {
callback.error.call(null, err)
},
complete: function () {
callback.complete.call(null)
}
})
}
</code></pre>
<pre><code class="prettyprint lang-js">var validating = 2
, errorCount = 0
, done
done = function () {
if (validating) {
return;
}
if (errorCount) {
// validation failed
}
}
checkEmail({
success: function () {
validating--
},
error: function () {
errorCount++
},
complete: done
})
</code></pre>
<p>这不是一个好的实现方式,只是为大家演示一下,callback 的使用方法。</p>
<p>Callback 的优点有:</p>
<ul>
<li>简单易用</li>
<li>无需额外的库支持</li>
</ul>
<p>缺点有:</p>
<ul>
<li>越复杂的逻辑越容易产生很深的嵌套</li>
</ul>
<h3 id="event_3">Event <a class="head_anchor" href="#event_3">#</a>
</h3>
<p>事件大家都知道:</p>
<pre><code class="prettyprint lang-js">$('button').click(function(){})
</code></pre>
<p>上例就可以改造成这样:</p>
<pre><code class="prettyprint lang-html"><!-- 假设 HTML 架构如下 -->
<form>
<input id="email">
<input id="username">
</form>
</code></pre>
<pre><code class="prettyprint lang-js">var $form = $('form')
, $email = $('#email')
function checkEmail () {
$email.attr('data-required', true)
$.get({
success: function(){
$email.attr('data-valid', true)
},
error: function(){
$email.attr('data-valid', false)
},
complete: function(){
$email.removeAttr('data-required')
$form.trigger('validationDone')
}
})
}
$form.on('validationDone', function(){
if ($form.find('[data-required]').length) {
return;
}
if ($form.find('[data-valid=false]').length) {
// validation failed
}
})
</code></pre>
<p>不是一个好例子,是吧?不过这是为了和之后的方案进行对比,实际事件的使用场景可以这样:</p>
<pre><code class="prettyprint lang-html"><div id="user-profile">
<!-- ... -->
<form>
<input type="submit" value="Save">
</form>
</div>
</code></pre>
<pre><code class="prettyprint lang-js">var $userProfile = $('#user-profile')
, $form = $userProfile.find('form')
$userProfile
.on('userProfileDidUpdate',
function (e, profile) {}
)
.on('userProfileUpdateFailed',
function (e) {}
)
.on('userProfileDidFinishUpdate',
function (e) {}
)
$form.on('submit', function(){
$.ajax({
success: function (data) {
$form.trigger('userProfileDidUpdate', data)
},
error: function () {
$form.trigger('userProfileUpdateFailed')
},
complete: function () {
$form.trigger('userProfileDidFinishUpdate')
}
})
})
</code></pre>
<p><code class="prettyprint">form</code> 通过事件,告诉大家「我完成该做的事了,至于进一步该干什么,我不管」。</p>
<p><code class="prettyprint">userProfile</code> 通过接受事件,执行不同的操作,至于是「谁」发起的,并不需要关心。</p>
<p>这样就降低了耦合度,<code class="prettyprint">form</code> 和 <code class="prettyprint">userProfile</code> (功能上)并没有依赖关系,都在完成属于自己的职责。</p>
<p>但是也是有缺陷的:</p>
<ul>
<li>易(人为)出错,难调试。<code class="prettyprint">userProfileDidFinishUpdated</code> 和 <code class="prettyprint">userProfileDidFinishUpdate</code> 就是两个不同事件,如果用到全局变量做事件名,则增加了依赖</li>
<li>DOM 上使用容易被 <code class="prettyprint">stopPropagtion</code> 影响</li>
</ul>
<p>针对第二个问题,通过引入第三方库可解决,如 <a href="https://github.com/hij1nx/EventEmitter2">EventEmitter2</a>:</p>
<pre><code class="prettyprint lang-js">var em = new EventEmitter2({
wildcard: true
})
em.on('form.*', function () {
console.log(arguments)
})
em.on('form.submit-disable', function (disabled) {
$form.find('[type=submit]').prop('disabled', disabled)
})
em.emit('form.submit-disable', true)
$(form).on('submit', function () {
if (valid) {
em.emit('form.valid', true)
} else {
em.emit('form.invalid', true)
}
})
</code></pre>
<p>EventEmitter2 其实是为 Node.js 做准备的,但用在浏览器端也毫无问题,而 Node.js 也有自己的 <a href="http://nodejs.org/api/events.html#events_emitter_addlistener_event_listener">events.EventEmitter</a>,用哪一个,萝卜青菜咯。</p>
<h3 id="messaging_3">Messaging <a class="head_anchor" href="#messaging_3">#</a>
</h3>
<p>消息其实是和事件很相似的东西,只是概念上有差异,以 <a href="https://github.com/postaljs/postal.js">Postal.js</a> 为例:</p>
<pre><code class="prettyprint lang-js">var channel1 = postal.channel()
, channel2 = postal.channel()
channel1.subscribe(
'demo',
function (data) {
console.log('channel 1 says:', data)
}
)
channel2.subscribe(
'demo',
function (data) {
console.log('channel 2 says:', data)
}
)
channel1.publish('demo', 'hello world')
// console logs:
// channel 1 says: hello world
// channel 2 says: hello world
</code></pre>
<p>事件和消息的差异在于:</p>
<ul>
<li>事件只能作用于事件的直接对象(或父节点)</li>
<li>消息可以作用于任何对象——只要该对象订阅了该消息</li>
</ul>
<p>Node.js 为例:</p>
<pre><code class="prettyprint lang-js">var EventEmitter = require('events').EventEmitter
, ee1 = new EventEmitter()
, ee2 = new EventEmitter()
ee1.on('demo', function (data) {
console.log('Hello', data)
})
ee2.on('demo', function (data) {
console.log('Hi', data)
})
ee2.emit('demo', 'Chris')
// console logs:
// hi Chris
</code></pre>
<p>因此 Objective-C 也使用了 <a href="https://developer.apple.com/library/mac/documentation/general/conceptual/devpedia-cocoacore/Notification.html">Notification</a>,以使 app 可以响应系统事件。</p>
<h3 id="promise_3">Promise <a class="head_anchor" href="#promise_3">#</a>
</h3>
<p>Promise 是什么就不必多说了,在 2013 年 12 月也确定了被加入到 JavaScript 内置对象中(<a href="http://www.html5rocks.com/en/tutorials/es6/promises/">link</a>),也就是尘埃落定,在 Node.js 和新浏览器里可以肆无忌惮地使用。</p>
<p>因为用 Promise 来改造文章开头的示例,实际和 callback 也差不多,所以就用别的例子来说说 Promise:</p>
<pre><code class="prettyprint lang-js">// a.js
function getUsername () {
var promise = new Promise(function (resolve, reject) {
if (done) {
resolve(data)
} else {
reject(err)
}
})
return promise
}
window.userNamePromise = getUsername()
// b.js
userNamePromise.then(
function (data) {
// do sth. with data
return something
},
function (err) {
// handle error
throw new Error()
}
)
// c.js
userNamePromise
.then(
function (something) {}
).catch(function (err) {})
</code></pre>
<p>如果看不懂上面的例子,建议好好看一遍上面的 link,学习学习。</p>
<p>简要地说,Promise 解决了事件只能作用于特定对象和同一时间内只能触发一次的限制。</p>
<p>通过对 <code class="prettyprint">then</code> 等接口的调用,可以在任何时候根据 <code class="prettyprint">resolve</code> 和 <code class="prettyprint">reject</code> 的结果执行不同的操作,同时通过暴露 promise(本例中的 <code class="prettyprint">window.userNamePromise</code>)到外部环境中,就可以在不同地方去使用这个 promise,根据其结果做不同的事。而由于 promise 会保证 <code class="prettyprint">then</code> 等接口调用时 <code class="prettyprint">resolve</code> 或 <code class="prettyprint">reject</code> 的结果是被正确取回(甚至缓存),因此不必担心执行时机不对的问题。</p>
<p>BTW,jQuery 也是带有 promise 机制的,比如 <code class="prettyprint">$.ajax</code> 返回的就是一个 promise 对象。</p>
<h3 id="finite-state-machines_3">Finite State Machines <a class="head_anchor" href="#finite-state-machines_3">#</a>
</h3>
<p><a href="http://en.wikipedia.org/wiki/Finite-state_machine">无限状态机</a>是比较好玩的东西,但遗憾的是用在本文最初的例子的话,代码量很多,也会复杂,因此使用别的例子:</p>
<pre><code class="prettyprint lang-js">var user = new machina.Fsm({
initialState: 'notLogin',
login: function (username, password) {
if (username && password) {
this.transition('login')
} else {
this.transition('notLogin')
}
},
isLoggedIn: function () {
return this.state === 'login'
},
logout: function () {
this.transition('notLogin')
},
states: {
'login': {
_onEnter: function () {
this.handle('user.login')
},
_onExit: function () {
this.handle('user.logout')
},
'user.login': function (payload) {},
'user.exit': function (payload) {}
}
}
})
user.isLoggedIn() // false
user.login('feifei', '123456')
user.isLoggedIn() // true
user.logout()
</code></pre>
<p>本例使用了 <a href="https://github.com/ifandelse/machina.js">machina.js</a>。从代码中可以看到,其实状态机是通过它的接口,本例是 <code class="prettyprint">login</code> 和 <code class="prettyprint">logout</code> 来接受外部信息并依此改变状态或直接改变状态,通过状态的改变,将执行特定状态下的行为。</p>
<p>一个无限状态机的特点是:</p>
<ul>
<li>可存在的状态有限</li>
<li>某一时刻,只能处于一个状态中</li>
<li>可以从一个状态转变成另一个状态</li>
<li>可以接受输入或者产生输出</li>
</ul>
<p>对前端来说,像是 <code class="prettyprint">a:hover</code>、<code class="prettyprint">input:focus</code> 其实就是一种状态。</p>
<h3 id="control-flow-libs_3">Control Flow Libs <a class="head_anchor" href="#control-flow-libs_3">#</a>
</h3>
<p>这一节只是介绍一下目前很流行的 <a href="https://github.com/caolan/async">Async.js</a> 和 <a href="https://github.com/creationix/step">Step</a>。</p>
<p>在 Node.js 开始大肆抢占众人眼球的时候,人们也发现了一个问题:异步是爽啊,可「怎么让不同的异步操作完成后最后调用一个接口去处理所有结果呢?」,又或者,「好多嵌套,能不能少一些啊」。</p>
<p>Async.js 和 Step 就是为了解决这类问题而生的。以 Async.js 为例,该怎么去解决本文最初的例子:</p>
<pre><code class="prettyprint lang-js">function checkEmail (next) {
$.get().then(function () {
next(null, data)
}, function () {
next(err)
})
}
async.parallel([
checkEmail,
checkUsername
], function (err, results) {
if (err) {
// validation failed
return false;
}
// post data
})
</code></pre>
<p>通过 <code class="prettyprint">async.parallel</code>,<code class="prettyprint">checkEmail</code> 和 <code class="prettyprint">checkUsername</code> 将会以异步形式执行,而通过对 <code class="prettyprint">next</code> 的调用,将会把结果汇总到 <code class="prettyprint">async.parallel</code> 的第二个参数,在这里就可以进行最后的处理。</p>
<p>通过这种方法,不仅不需要去设置 <code class="prettyprint">setInterval</code>,也不需要去检查 <code class="prettyprint">errorCount</code> 之类的多余变量,一切交给 async 就可以了。</p>
<p>Async 除了 <code class="prettyprint">parallel</code> 之外,还有 <code class="prettyprint">series</code>、<code class="prettyprint">waterfall</code> 等接口,也有 <code class="prettyprint">each</code> 等 <code class="prettyprint">Array.forEach</code> 的特殊封装。前三个接口是我最常用的,而后面如 <code class="prettyprint">each</code> 之类的,其实只是普通的数组操作的话,我建议直接 loop 或者 <a href="http://lodash.com/">Lo-Dash</a>,因为 async 对 <code class="prettyprint">each</code> 的处理是只要 <code class="prettyprint">Array.forEach</code> 存在就用,不存在就用 loop,在浏览器上使用会存在性能不足的隐患(在 Node.js 也可能会,毕竟是 V8)。</p>
<p>而 Step 则是实现了 <code class="prettyprint">async.parallel</code> 等三个接口的超小巧库,搭配 Lo-Dash 使用还是非常不错的。</p>
<h3 id="generator_3">Generator <a class="head_anchor" href="#generator_3">#</a>
</h3>
<p><a href="http://wiki.ecmascript.org/doku.php?id=harmony:generators">Generator</a> 是 ECMAScript 6 新增加的特性,具体内容在 <a href="http://blog.chrisyip.im/nodejs-101-generator">Node.js 101: generator</a> 里讲了,不冗述。</p>