Node.js 基于JavaScript编写应用,JavaScript是我的主要开发语言。CoffeeScript是编译为JavaScript的编程语言。为什么我们要用CoffeeScript来编写一段可重用的代码——模块呢?CoffeeScript是一个非常高阶的语言,将JavaScript、Ruby和Python中我最爱的部分结合在了一起。在本教程中,我将展示如何使用CoffeeScript为Node.js创建一个可复用的开源模块。最近我在创建一个播放列表分析模块时get了这个新技能。重点在于如何将一个快速的开发变成一个结构良好的Node.js模块。
步骤如下:
- 将创意放入git仓库。
- 添加目录结构。
- 从测试中分离库函数。
- 添加构建脚本。
- 创建node模块。
- 添加LICENSE 和 README。
- 发布。
首先,我们需要一个创意。它不用是多么革命性的创意。它只需做一件事,并且将它做好。这是UNIX饱受争议的哲学的第一条准则,在Node.js社区激起了共鸣。当我开发的时候,我从单一文件开始,进行一些探索。然后我渐进地改良代码直到我做出了可复用的东西。这样,我们可以复用它,别人也可以复用它,别人也可以从代码中得到启发,世界会因此更美好。
本教程中,我将展示如何为nanomsg创建一个绑定。nanomsg是ZeroMQ的创造者 Martin Sústrik最新开发的一个可伸锁性协议库。我以前曾经玩过ZeroMQ,感觉它非常棒。当我看到ZeroMQ的作者做出了一个基于C的新库的时候,我非常激动。因为我很喜欢他的博客《为什么我应该用C而不是C++编写 ZeroMQ》。
为了快速地上手,我们首先确保node的版本够新。我喜欢使用nvm,以及最新的稳定minor版node(版本格式为major.minor.patch
,稳定版的minor数字是偶数,所以v0.11.0
是非稳定版)。
node -v
-> v0.10.17
接下来我需要下载我打算动态链接的库:
curl -O http://download.nanomsg.org/nanomsg-0.1-alpha.zip && \
unzip nanomsg-0.1-alpha.zip && \
cd nanomsg-0.1-alpha && \
mkdir build && \
cd build && \
../configure && \
make && \
make install
我们将使用node的FFI模块,以便和动态链接库交互。对于编写绑定而言,这比使用原生扩展要容易,而且V8的API最近的修改给原生扩展造成了一些麻烦。
npm install ffi
我们将使用CoffeeScript编写代码:
npm install -g coffee-script
在C++绑定样例的基础上我们创建一个main.coffee
:
ffi = require 'ffi'
assert = require 'assert'
AF_SP = 1
NN_PAIR = 16
nanomsg = ffi.Library 'libnanomsg',
nn_socket: [ 'int', [ 'int', 'int' ]]
nn_bind: [ 'int', [ 'int', 'string' ]]
nn_connect: [ 'int', ['int', 'string' ]]
nn_send: [ 'int', ['int', 'pointer', 'int', 'int']]
nn_recv: [ 'int', ['int', 'pointer', 'int', 'int']]
nn_errno: [ 'int', []]
# test
s1 = nanomsg.nn_socket AF_SP, NN_PAIR
assert s1 >= 0, 's1: ' + nanomsg.nn_errno()
ret = nanomsg.nn_bind s1, 'inproc://a'
assert ret > 0, 'bind'
s2 = nanomsg.nn_socket AF_SP, NN_PAIR
assert s2 >= 0, 's2: ' + nanomsg.nn_errno()
ret = nanomsg.nn_connect s2, 'inproc://a'
assert ret > 0, 'connect'
msg = new Buffer 'hello'
ret = nanomsg.nn_send s2, msg, msg.length, 0
assert ret > 0, 'send'
recv = new Buffer msg.length
ret = nanomsg.nn_recv s1, recv, recv.length, 0
assert ret > 0, 'recv'
console.log recv.toString()
assert msg.toString() is recv.toString(), 'received message did not match sent'
coffee main.coffee
-> hello
这个快速编写的例子显示我们已经能做到一些事情了。目前我们的目录结构是这样的:
tree -L 2
.
├── main.coffee
└── node_modules
└── ffi
2 directories, 1 file
将创意变为git仓库
接着我们使用git创建一个仓库,开始保存我们的工作。更早提交,更多提交。
让我们加入一个.gitignore
文件,这样不需要提交的文件就不会被加入到git仓库。node_modules
文件夹是不必要提交的,因为当安装node模块的时候,它的依赖会被递归地安装,所以没有必要将它们提交到源代码管理系统。因为我使用vim,所以还需要排除vim的交换文件:
node_modules/
*.swp
好了,让我们创建git仓库吧。
git init && \
git add . && \
git commit -am "initial commit"
在github上创建一个未初始化的仓库,然后推送:
git remote add origin git@github.com:nickdesaulniers/node-nanomsg.git && \
git push -u origin master
现在我们的目录结构如下:
tree -L 2 -a
.
├── .gitignore
├── main.coffee
└── node_modules
└── ffi
2 directories, 2 files
添加目录结构
既然我的代码已经处于git之下,让我们开始添加一些目录结构。我们需要创建src/
、lib/
和test/
目录。src/
放置我们的CoffeeScript,lib/
放置编译的JavaScript文件,我们的测试代码会在test/
。
mkdir src lib test
从测试中分离库函数
现在我们把main.coffee
移动到src/
,并把它的一个副本移动到test/
。我们将从测试逻辑中分离出库函数。
cp main.coffee test/test.coffee && \
git add test/test.coffee && \
git mv main.coffee src/nanomsg.coffee
git status
告诉我们:
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# renamed: main.coffee -> src/nanomsg.coffee
# new file: test/test.coffee
#
让我们修改下 src/main.coffee
:
ffi = require 'ffi'
exports = module.exports = ffi.Library 'libnanomsg',
nn_socket: [ 'int', [ 'int', 'int' ]]
nn_bind: [ 'int', [ 'int', 'string' ]]
nn_connect: [ 'int', ['int', 'string' ]]
nn_send: [ 'int', ['int', 'pointer', 'int', 'int']]
nn_recv: [ 'int', ['int', 'pointer', 'int', 'int']]
nn_errno: [ 'int', []]
exports.AF_SP = 1
exports.NN_PAIR = 16
并且修改测试:
assert = require 'assert'
nanomsg = require '../lib/nanomsg.js'
{ AF_SP, NN_PAIR } = nanomsg
s1 = nanomsg.nn_socket AF_SP, NN_PAIR
assert s1 >= 0, 's1: ' + nanomsg.nn_errno()
ret = nanomsg.nn_bind s1, 'inproc://a'
assert ret > 0, 'bind'
s2 = nanomsg.nn_socket AF_SP, NN_PAIR
assert s2 >= 0, 's2: ' + nanomsg.nn_errno()
ret = nanomsg.nn_connect s2, 'inproc://a'
assert ret > 0, 'connect'
msg = new Buffer 'hello'
ret = nanomsg.nn_send s2, msg, msg.length, 0
assert ret > 0, 'send'
recv = new Buffer msg.length
ret = nanomsg.nn_recv s1, recv, recv.length, 0
assert ret > 0, 'recv'
assert msg.toString() is recv.toString(), 'received message did not match sent'
注意到了我们在test
中包含了尚不存在的lib/
下的JavaScript文件?如果我们尝试运行coffee test/test.coffee
,它会崩溃。让我们先编译一下。coffee -o lib -c src/nanomsg.coffee
编译完成之后,使用coffee test/test.coffee
运行我们的测试。
现在让我们提交一下吧。注意不要把lib/
加入版本控制,我下面会解释为什么。
tree -L 2 -C -a -I '.git'
.
├── .gitignore
├── lib
│ └── nanomsg.js
├── node_modules
│ └── ffi
├── src
│ └── nanomsg.coffee
└── test
└── test.coffee
5 directories, 4 files
就目前而言,如果我们添加了特性并打算运行测试,我们需要执行:
coffee -o lib -c src/nanomsg.coffee && coffee test/test.coffee
虽然这个命令很简单,也不难理解,但是任何贡献代码给你的项目的人需要知道这个命令才能运行测试。让我们使用Grunt,JavaScript任务自动化工具来自动化我们的构建和测试过程。
添加一个构建脚本
npm install -g grunt-cli && \
npm install grunt-contrib-coffee
用CoffeeScript创建一个简单的Gruntfile:
module.exports = (grunt) ->
grunt.initConfig
coffee:
compile:
files:
'lib/nanomsg.js': ['src/*.coffee']
grunt.loadNpmTasks 'grunt-contrib-coffee'
grunt.registerTask 'default', ['coffee']
运行grunt
将创建我们的库,让我们提交一下。
但是grunt
不会运行我们的测试。我们的测试输出也不好看。让我们改变这一点:
npm install -g mocha && \
npm install chai grunt-mocha-test
编辑test/test.coffee
:
assert = require 'assert'
should = require('chai').should()
nanomsg = require '../lib/nanomsg.js'
describe 'nanomsg', ->
it 'should at least work', ->
{ AF_SP, NN_PAIR } = nanomsg
s1 = nanomsg.nn_socket AF_SP, NN_PAIR
s1.should.be.at.least 0
ret = nanomsg.nn_bind s1, 'inproc://a'
ret.should.be.above 0
s2 = nanomsg.nn_socket AF_SP, NN_PAIR
s2.should.be.at.least 0
ret = nanomsg.nn_connect s2, 'inproc://a'
ret.should.be.above 0
msg = new Buffer 'hello'
ret = nanomsg.nn_send s2, msg, msg.length, 0
ret.should.be.above 0
recv = new Buffer msg.length
ret = nanomsg.nn_recv s1, recv, recv.length, 0
ret.should.be.above 0
msg.toString().should.equal recv.toString()
然后编辑我们的gruntfile
,加入测试步骤:
module.exports = (grunt) ->
grunt.initConfig
coffee:
compile:
files:
'lib/nanomsg.js': ['src/*.coffee']
mochaTest:
options:
reporter: 'nyan'
src: ['test/test.coffee']
grunt.loadNpmTasks 'grunt-contrib-coffee'
grunt.loadNpmTasks 'grunt-mocha-test'
grunt.registerTask 'default', ['coffee', 'mochaTest']
现在,当我们运行grunt
的时候,将构建我们的程序,然后运行测试,然后我们可以看到开心死了的彩虹猫。彩虹猫mocha测试报告差不多是人类心智所能达到的最高成就。
grunt
Running "coffee:compile" (coffee) task
File lib/nanomsg.js created.
Running "mochaTest:src" (mochaTest) task
1 -__,------,
0 -__| /\_/\
0 -_~|_( ^ .^)
-_ "" ""
1 passing (5 ms)
Done, without errors.
tree -L 2 -C -a -I '.git'
.
├── .gitignore
├── Gruntfile.coffee
├── lib
│ └── nanomsg.js
├── node_modules
│ ├── ffi
│ ├── grunt
│ └── grunt-contrib-coffee
├── src
│ └── nanomsg.coffee
└── test
└── test.coffee
7 directories, 5 files
创建node模块
现在我们的设计已经比较模块化了,内置了构建和测试的逻辑,让我们使这个模块容易再分发吧。首先,我们讨论忽略的文件。创建一个.npmignore
文件,它将指定哪些文件不会被包含在下载中。Node包管理程序,npm,默认会忽略一组文件文件。
Gruntfile.coffee
src/
test/
默认忽略了src/
目录,在我们的.gitignore
中则忽略了lib/
。
node_modules/
lib/
*.swp
为何如此?老实说,在严格意义上而言,忽略这两个目录不是必需的。但是我认为这很有用。当有人获取代码的时候,他不需要编译的结果,毕竟他们可以进行修改,而这需要重新编译。添加lib/nanomsg.js
将增加下载的文件(当然它的大小相对而言无关紧要)。同理,当有人下载模块的时候,他多半只想要编译好的文件,而不是源代码、构建脚本或测试。如果我希望让浏览器可以访问编译好的JavaScript,我可能不会在.gitignore
中包含lib/
,这样就可以通过github的raw URL引用它了。当然,这些只是一般情况下的经验,并不总是正确的。为了弥补不把整个代码放进模块的缺憾,我们将在manifest中添加一个指向仓库的链接。不过在此之前,让我们先提交一下!
现在该是创建manifest文件的时候,其中包含了我们的应用的基本信息。预先使用npm search <packagename>
看看打算使用的包名是否可用是个很好的注意。由于我们已经安装了所有依赖,让我们运行npm init
吧。
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sane defaults.
See `npm help json` for definitive documentation on these fields
and exactly what they do.
Use `npm install <pkg> --save` afterwards to install a package and
save it as a dependency in the package.json file.
Press ^C at any time to quit.
name: (nanomsg)
version: (0.0.0)
description: nanomsg bindings
entry point: (index.js) lib/nanomsg.js
test command: grunt
git repository: (git://github.com/nickdesaulniers/node-nanomsg.git)
keywords: nanomsg
author: Nick Desaulniers
license: (BSD-2-Clause) Beerware
About to write to /Users/Nicholas/code/c/nanomsg/package.json:
{
"name": "nanomsg",
"version": "0.0.0",
"description": "nanomsg bindings",
"main": "lib/nanomsg.js",
"directories": {
"test": "test"
},
"dependencies": {
"chai": "~1.7.2",
"ffi": "~1.2.5",
"grunt": "~0.4.1",
"grunt-mocha-test": "~0.6.3",
"grunt-contrib-coffee": "~0.7.0"
},
"devDependencies": {},
"scripts": {
"test": "grunt"
},
"repository": {
"type": "git",
"url": "git://github.com/nickdesaulniers/node-nanomsg.git"
},
"keywords": [
"nanomsg"
],
"author": "Nick Desaulniers",
"license": "Beerware",
"bugs": {
"url": "https://github.com/nickdesaulniers/node-nanomsg/issues"
}
}
Is this ok? (yes)
这将为npm创建一个package.json
manifest。
现在,除了使用grunt
之外,我们也可以通过npm test
来运行我们的测试了。在发布模块之前,先提交一下。
tree -L 2 -C -a -I '.git'
.
├── .gitignore
├── .npmignore
├── Gruntfile.coffee
├── lib
│ └── nanomsg.js
├── node_modules
│ ├── .bin
│ ├── chai
│ ├── ffi
│ ├── grunt
│ ├── grunt-contrib-coffee
│ └── grunt-mocha-test
├── package.json
├── src
│ └── nanomsg.coffee
└── test
└── test.coffee
10 directories, 7 files
添加 LICENSE 和 README
现在我们差不多已经完成了。但是开发者如何知道该如何复用这些代码呢?不管我有多么喜欢直接查看源代码,npm会抱怨我们的模块没有一个readme文件。而且有readme的话,github仓库会比较好看。
# Node-NanoMSG
Node.js binding for [nanomsg](http://nanomsg.org/index.html).
## Usage
`npm install nanomsg`
```javascript
var nanomsg = require('nanomsg');
var assert = require('assert');
var AF_SP = nanomsg.AF_SP;
var NN_PAIR = nanomsg.NN_PAIR;
var msg = new Buffer('hello');
var recv = new Buffer(msg.length);
var s1, s2, ret;
s1 = nanomsg.nn_socket(AF_SP, NN_PAIR);
assert(s1 >= 0, 's1: ' + nanomsg.errno());
ret = nanomsg.nn_bind(s1, 'inproc://a');
assert(ret > 0, 'bind');
s2 = nanomsg.nn_socket(AF_SP, NN_PAIR);
assert(s2 >= 0, 's2: ' + nanomsg.errno());
ret = nanomsg.nn_connect(s2, 'inproc://a');
assert(ret > 0, 'connect');
ret = nanomsg.nn_send(s2, msg, msg.length, 0);
assert(ret > 0, 'send');
ret = nanomsg.recv(s1, recv, recv.length, 0);
assert(ret > 0, 'recv');
assert(msg.toString() === recv.toString(), "didn't receive sent message");
console.log(recv.toString());
发布之前,我们需要创建一个许可文件,因为我们将公开我们的代码,没有明确许可的公开代码仍然处于版权保护之下,不可复用。
/*
* ----------------------------------------------------------------------------
* "THE BEER-WARE LICENSE" (Revision 42):
* <nick@mozilla.com> wrote this file. As long as you retain this notice you
* can do whatever you want with this stuff. If we meet some day, and you think
* this stuff is worth it, you can buy me a beer in return. Nick Desaulniers
* ----------------------------------------------------------------------------
*/
```
如果你希望正经一点,你可以使用MIT或BSD风格的许可,如果你不在乎你的仓库会被如何使用。如果你在乎,可以使用GPL风格的许可。[TLDRLegal](http://www.tldrlegal.com/)对常见的许可协议都有简要的说明。
```sh
tree -L 2 -C -a -I '.git'
.
├── .gitignore
├── .npmignore
├── Gruntfile.coffee
├── LICENSE
├── README.md
├── lib
│ └── nanomsg.js
├── node_modules
│ ├── .bin
│ ├── chai
│ ├── ffi
│ ├── grunt
│ ├── grunt-contrib-coffee
│ └── grunt-mocha-test
├── package.json
├── src
│ └── nanomsg.coffee
└── test
└── test.coffee
10 directories, 9 files
发布
npm publish
npm http PUT https://registry.npmjs.org/nanomsg
npm http 201 https://registry.npmjs.org/nanomsg
npm http GET https://registry.npmjs.org/nanomsg
npm http 200 https://registry.npmjs.org/nanomsg
npm http PUT https://registry.npmjs.org/nanomsg/-/nanomsg-0.0.0.tgz/-rev/1-20f1ec5ca2eed51e840feff22479bb5d
npm http 201 https://registry.npmjs.org/nanomsg/-/nanomsg-0.0.0.tgz/-rev/1-20f1ec5ca2eed51e840feff22479bb5d
npm http PUT https://registry.npmjs.org/nanomsg/0.0.0/-tag/latest
npm http 201 https://registry.npmjs.org/nanomsg/0.0.0/-tag/latest
+ nanomsg@0.0.0
最后,我喜欢在别的地方创建一个新目录,然后根据readme中的步骤跑一遍,以确保包确实可以复用。这很有用,因为在readme中我不小心遗漏了 errno 和 recv 前的nn_
前缀!
更新readme中的例子之后,让我们修改版本号并重新发布。使用npm version
查看当前版本,然后使用npm version patch
来修改。在此之前你需要提交readme的改动。最后,别忘了再次运行npm publish
。
我们最终的目录结构看起来是这样的:
tree -L 2 -C -a -I '.git'
.
├── .gitignore
├── .npmignore
├── Gruntfile.coffee
├── LICENSE
├── README.md
├── lib
│ └── nanomsg.js
├── node_modules
│ ├── .bin
│ ├── chai
│ ├── ffi
│ ├── grunt
│ ├── grunt-contrib-coffee
│ └── grunt-mocha-test
├── package.json
├── src
│ └── nanomsg.coffee
└── test
└── test.coffee
10 directories, 9 files
最后,我会联系下Martin Sústrik,让他知道nanomsg有一个新绑定了。
这个绑定远远不够完整,测试覆盖率可以更高,API非常像C,可以使用一些OO语法糖的,但是我们已经有了一个良好的起点,可以进一步改进了。如果你有意帮忙,请派生 https://github.com/nickdesaulniers/node-nanomsg.git
你对node模块的构建步骤、测试、目录结构有什么想法?这个教程显然不会是一个权威指南。我期待你们的评论。
原文 Making Great Node.js Modules With CoffeeScript
翻译 SegmentFault