记一次用 Node.js 重写工具的过程

邹业盛 2016-07-31 21:31 更新
  1. 要做什么
  2. 先要解决什么问题
  3. 标准输入输出
  4. Node.js 中的字节与字符
  5. 命令行参数
  6. 文件IO
  7. 当前主模块判断
  8. 使用 cheerio 解析 html
  9. 模板引擎
  10. 多行字符串与占位符功能
  11. 总结

之前用 Python 做了一个方便项目开发的脚手架工具,这个工具中用到了 Python 的 C 绑定形式的模块, 于是在安装时就需要机器中有相应的编译环境,比如 OS X 上先装 ports 然后用它装一些库。如果事先没有这套编译环境,第一次安装时还是比较折腾的,虽然我已经尽可能详尽地写文档了。

稳定用了一段时间之后,我现在考虑用 Node.js 来把这个工具重新实现一下,这样在环境方面应该就会友好很多了,毕竟 Node.js 的环境肯定是事先就准备好了的。(不过,我好像记得 Node.js 的某些模块也是需要编译环境的)

1. 要做什么

就是一个 z.py 的文件,它目前的功能有 5 个:

init 会创建一个代码库的初始目录结构,当然这个功能几乎不会用到(我所有项目都放一个代码库)。

project 是在代码库中创建一个“项目”的目录结构,大概长这样:

test
├── common
│   ├── app.sass
│   └── app.sass.css
├── config.js
├── iconfont
│   ├── app.sass
│   └── app.sass.css
├── index
│   ├── app.js
│   ├── demo.html
│   └── _index.html
├── page-index
│   ├── app.html
│   ├── app.html.js
│   ├── app.js
│   ├── app.sass
│   ├── app.sass.css
│   └── demo.html
└── service
    └── app.js

当然,这个项目名字是 test ,所以里面的一些文件中的名字是跟 test 有关的(一些约定)。

appproject 相似,不过它是创建一个“组件”的目录结构,这是平时使用最多的功能(为此会在 IDE 中专门为它配置“快捷方式”),一个组件大概长这样:

test/first
├── app.html
├── app.html.js
├── app.js
├── app.sass
├── app.sass.css
└── demo.html

同理,这个组件是 test 项目中的名为 first 的组件,那么上面文件中的一些内容,是跟 testfirst 这些词有关的。比如 test/first/app.js 中会有:

var MODULE_NAME = 'test/first';
var DIRECTIVE_NAME = 'testFirst';

这种内容。

sass 的功能是把 app.sass 变成 app.sass.js ,就是普通的 sass 转换行为。

html 的功能是把 app.html 变成 app.html.js ,这个功能简单来说是把 app.html 中的 html 片段(注意只是片段)以一个字符串形式放到 app.html.js 中( app.html.js 中的内容是 AMD 形式的)。

5 个功能,对我这个没正经写过 Node.js 的人来说,我预估还是要花些时间的。

2. 先要解决什么问题

我之所以想把这次重写的过程记录下来,是因为我觉得目前面对的这个问题,算是“一个有经验的人如何去学习使用一种新语言”的典型场景。换句话说,即使选择的不是 Node.js 来重写这个工具,而是其它的 A 语言,X 语言,我接下来要做的事也没有什么不同。

要完成这个工具,我认为我首先要学会使用 Node.js 处理以下场景:

当然,在过程中还有一些零碎的小问题,这些都后面一个一个来搞定。

3. 标准输入输出

console.log 似乎就是标准输出,但是,显示这只是一个“别名”才对,使用 console 这个名字作为标准输出太不专业了。

我刚开始为标准输入输出而去翻官方文档时,整个人是懵逼的,找不到 systemio 这类东西啊,后来通过 Google 才知道标准输入输出是放在 process 这个名字空间下的。

标准输出除了用 console.log 外,它比较“正式”的名字应该是 process.stdout()

var stdout = process.stdout;
stdout.write('ok');
stdout.write('ok');

这里注意一下,跟其它语言类似, console.log 是加 \n 的。

标准输出有了,随便猜一下,标准错误应该是 process.stderrconsole.error 吧:

var stderr = process.stderr;
stderr.write('error\n');
console.error('console');
console.log('stdout');

直接看看不出区别的,在 bash 中稍处理一下(假设上面几行代码在文件 test.js 中):

$ node test 1>/dev/null
error
console

$ node test 2>/dev/null
stdout

看起来没问题。

然后是标准输入, prcess.stdin ,这里就涉及几个读的操作了:

var stdin = process.stdin;
var buff = [];

stdin.on('readable', function(){
  var chunk = stdin.read();
  if(chunk !== null){
    buff.push(chunk);
  }
});

stdin.on('end', function(){
  console.log(buff.join('\n'));
});

在终端中:

$ echo "123"|node test.js
123

stdin 的 api 就是 Node.js 的异步IO 那套,这个地方事件倒是比 while 1 的死循环好看些。

4. Node.js 中的字节与字符

我对 Node.js 中的字符中的细节是没有任何概念的,先从这里着手吧(我源文件是 UTF-8 编码):

console.log(('中文').length);
// 2

能看到输出的结果是 2 ,当然,这个 2 可能真的是表示 '中文' 这个字符串是 2 个字符,但是也不排除 length 这个方法实现上的处理。

暂且认为字符串在 Node.js 中是“字符”的概念,那么接下来,把文件的源码改成 GBK 看看会发生什么:

$ iconv demo.js -f utf8 -t gbk > demo.gbk.js
$ node demo.gbk.js
4

输出的是 4 , WTF …… ,这不被坑成猪头才怪。

通过 google 之后,目前我对 Node.js 的概念是,Node.js 中的字符串是“字符”的概念,但是,它的源文件只能是 utf8 ,换句话说,“编译”时就假定“字符串”中的“字节”是 UTF-8 的编码。

要处理“字节”的话,Node.js 有提供 Buffer 这个对象,它初始化时接收的数组可以看作是“字节序列”,通过 toString(encoding) 方法可以转成“字符”,从最简单的 ASCII 开始:

var buff = new Buffer([65]);
console.log(buff.toString());
// A

给个“中”字的 UTF-8 的三字节:

var buff = new Buffer([0xe4, 0xb8, 0xad]);
console.log(buff.toString());
// 中

看来 toString() 的默认行为就是按 UTF-8 进行“解码”啊。

Buffer 还有 hexbase64 这两个比较常用的编码方式:

var buff = new Buffer([0xe4, 0xb8, 0xad]);
console.log(buff.toString('hex'));
console.log(buff.toString('base64'));
console.log(buff.length); // 3

Buffer.length 方法得到的就是字节长度了。

从 api 层面考虑的话, buff.toString('gbk') 应该是按 GBK 编码进行“解码”了,不过,坏消息是目前实现的只有 UTF-8 一种编码。

所以,在 Node.js 中如果涉及到除 UTF-8 之外的其它“字节串”的话,只能借助额外的模块能力来处理,比如 https://github.com/bnoordhuis/node-iconv

5. 命令行参数

使用:

process.argv

可以获取当前命令行的所有参数,注意,这里的“所有参数”包括了执行程序 node 与目标文件路径,比如:

[ '/opt/node/bin/node', '/home/zys/temp/demo.js' ]

这个样式,是一个列表。

把 js 文件处理成 Linux 风格的可执行文件对命令行参数没有影响。比如对于文件:

#!/opt/node/bin/node
console.log(process.argv);

通过 chmod +x 之后,直接 ./demo.js 结果还是:

[ '/opt/node/bin/node', '/home/zys/temp/demo.js' ]

6. 文件IO

这个标题本来只是我个人随便起来,没想到在 nodejs 的 API 中,“文件”跟“IO”还真弄在一起了,一般来说“文件”是操作系统级别的一组 API ,而“IO”则是上层的逻辑性操作,混在一起略有些不优雅啊。

从官方的文档上看,文件IO这一组的功能,是放在 fs 这个模块中的,它包括的内容大概有:

这里,我记得在 Linux 下文件类型的 fd 是不支持异步的,所有针对“文件”的异步操作没意义吧。

6.1. 文件同步读写

fs.readFileSyncfs.writeFileSync

fs.writeFileSync 的使用方法是:

var fs = require('fs')
fs.writeFileSync('/home/zys/temp/demo.txt', 'data here', { encoding: 'utf8', flag: 'w', mode: 0o666 });

参数当中:

fs.readFileSync 的使用方法是:

var fs = require('fs')
var content = fs.readFileSync('/home/zys/temp/demo.txt', { encoding: null, flag: 'r'});
console.log(content.toString());

encodingflag 这两个参数在这里等于都是写死的,因为我不知道还能是其它哪些值。

注意一点就是, fs.readFileSync 返回结果的类型不是字符串,而是 Buffer

6.2. 完整的模式

就记一下,反正多半用不到。

6.3. fd 读写

首先, fs.open()fs.openSync() 可以打开指定的 path ,并返回一个 fd 用于接下来的操作。但是这里再拿文件 fd 说事就没什么感觉了。用一个命名管道试试吧。

我不知道在 nodejs 中怎么创建命名管道,所以拿 Python 先创建上(“命名管道”在非 Linux 环境下不一定能用):

import os
os.mkfifo('/tmp/pipe')

让 nodejs 的程序准备读:

var fs = require('fs')
var fd = fs.openSync('/tmp/pipe', 'r');
var buff = new Buffer(3); // 3个字节
var length = fs.readSync(fd, buff, 0, 3, null); // 从buff的0个字节开始写, 读3个字节, 从当前文件位置开始读
console.log(length); // 读了多少个字节
console.log(buff.toString());

运行之后,发看到程序一直未响应,因为管道中还没有内容可读。

我们在终端中直接写入一点东西:

$ echo '123' > /tmp/pipe

这样, nodejs 的标准输出就会有:

3
123

的显示了。

除文件外,其它对 fd 的操作,异步可能还更自然一些:

var fs = require('fs')

fs.open('/tmp/pipe', 'r', function(err, fd){
  var buff = new Buffer(3);
  var length = fs.read(fd, buff, 0, 3, null, function(err, length, b){
    console.log(length);
    console.log(b.toString());
  });
});

7. 当前主模块判断

这是在写代码中想到的一个问题。因为是动态语言,所以我习惯边写边运行,而最常用的一种代码组织方式,就是在尾部添加一些代码,它的功能是如果当前文件是“主模块”,则运行。而如果当前文件是作为非主模块被其它引用的,则不运行。

就是 Python 中的:

if __name__ == '__main__':
    run()

nodejs 中的实现方式是:

function run() {
  console.log('here');
}

if(module === require.main){
  run()
  console.log(__filename);
}

8. 使用 cheerio 解析 html

找了一圈,没找到即支持 xpath 又能宽容 html 的实现, cheerio 光用来处理 html 很方便,它的 api 是类 jQuery 风格的,直接就可以上手了。

var cheerio = require('cheerio');
var $ = cheerio.load('<a><b>123</b>kk</a>');
console.log($('a > b').text());
console.log($.html('b'));

9. 模板引擎

模板引擎我选择的是 nunjuckshttp://mozilla.github.io/nunjucks/ ,因为它是照着 Python 中的 jinja 做的 ,那个双大括号 {{ }} 我非常熟悉。

var nunjucks = require('nunjucks');
var template = '{{ a }}';
var s = nunjucks.renderString(template, {a: '123'});
console.log(s);

10. 多行字符串与占位符功能

多行字符串指的是 Python 中的三引号:

s = '''
a
b
c
'''

占位符功指的是 Python 中的:

print 'create {}/src/{}/{}/app.js'.format(INFO['root'], project, name)

或:

print 'create %s/src/%s/%s/app.js' % (INFO['root'], project, name)

这两个机制, nodejs 中 es6 语法“模板字符串”提供了类似实现。

模板字符串 使用反引号,可以跨行:

var s = `
a
b
c
${a}
`

里面可以使用 ${a} 的形式直接嵌入求值表达式。

但是注意,“模板字符串”不支持“传参”,嵌入的求值表达式里的变量需要事先定义好。

11. 总结

上面各点了解之后,剩下的就可以对着之前的 Python 代码直接翻写一遍了。

评论
©2010-2016 zouyesheng.com All rights reserved. Powered by GitHub , txt2tags , MathJax