NodeJS:需要惯用:读取dir中的文件,连接,转换,写入

时间:2017-06-06 12:53:02

标签: node.js shell io promise

我试图在Node中编写内容manglement程序。多年来我一直是一个旧的Ruby / Perl / Shell手,我似乎无法在这些语言中使用简单的代码,在Node中看起来类似,呃,简单。

任务:查找所有*.md个文件,阅读它们(按ls顺序),转换它们,并用标题注释和页脚注释括起来。这些文件按顺序有一段Markdown,当组装和转换时,它们是一个合理的HTML文档。这是一个shell实现:

echo '<!-- Generate at:' $(date) ' -->' $(ls *.md |xargs cat|markdown)'<!-- Copyright Mumble-demo Inc. -->'

生成所需的HTML:

<!-- Generate at: Tue Jun 6 08:25:59 EDT 2017  --> <h1>This is a Markdown File</h1> <h2>Heading 1</h2> <p>Inside of markdown we can create many interesting items</p> <ul> <li>such</li> <li>as</li> <li>lists</li> </ul><!-- Copyright Mumble-demo Inc. -->

Ruby同样合理......

#!/usr/bin/env ruby
require 'kramdown'

HEADER = "<!-- Generated at #{Time.now} -->\n"
FOOTER = "\n<!-- Copyright Mumble-demo Inc. -->"

OUTPUT = File.open("./output", "w")

results =  Dir.glob("*.md").map { |f| File.open(f).readlines.join() }.reduce(:+)
OUTPUT.print(HEADER, Kramdown::Document.new(results).to_html, FOOTER)

但我无法弄清楚如何以一种感觉正确的方式在Node中做到这一点(™)

感觉错误的方式(™)是使用同步接口:

const fs = require("fs")
const marked = require("marked")

const HEADER = `<!-- Generated at ${new Date()} -->\n`
const FOOTER = `\n<!-- Copyright Mumble-demo Inc. -->`

fs.readdir(".", (err, files) => {
  if (err) throw err;
  let markdownFiles = files.filter((f) => f.endsWith(".md"))

  let content = markdownFiles.reduce((memo, fileName) => {
    return memo + fs.readFileSync(fileName, 'utf8')
  }, "")

  let contentString = [HEADER, marked(content), FOOTER].reduce((m, i) => m + i, "")
  fs.writeFileSync("derp", contentString);
  console.log(contentString);

})

一种感觉良好但我无法工作的方式(™)是:

  1. 构建阅读流
  2. 将它们传输到降价转换流
  3. 打开输出流并将转换后的数据重定向到它
  4. 好消息是 - 这种方法有效,直到将标题注释放在顶部和底部。它们存在于代码中,而不是存在于文件系统中,因此我无法添加&#34;它们作为另一个文件流,无法转换,进入输出流。大多数方法最终产生:页眉,页脚,流数据

    显然,pipe() - 工作是异步工作的,并且在读取+转换工作完成之前页脚打印会触发。我已经尝试了最终无效的可怕(和破碎)Promise链条。

    另一种方法是将页眉和页脚转换为流(看起来很奇怪......)并将它们流入输出流(看起来非常奇怪)。

    我已经让几个经验丰富的开发人员难以理解......当然我们在这里找不到一些常见的习语,或者实际上这很难在Node中完成这个简单的任务?

3 个答案:

答案 0 :(得分:1)

思想:

  • 对于我的大多数shell脚本,我只是将文件内容同步读入字符串。这种方法不能扩展,但通常不需要。使用琴弦,一切都变得如此简单。
  • 如果您执行任何异步操作:使用异步函数和util.promisify()
  • 长期来看,asynchronous iteration and async generators也会对这种情况有所帮助。

答案 1 :(得分:0)

你可以运行它&#34;一种感觉正确的方式&#34;通过同步执行器nsynjs。您的代码可能会与此工作示例类似地转换:

MD-cat.js:

var nsynjs = require('nsynjs');
var nsynFs = require('../wrappers/nodeFs'); // part of nsynjs package, needs to be added manually

var synchronousCode = function(nsynFs) {
    var HEADER = "<!-- Generated at "+new Date()+" -->\n";
    var FOOTER = "\n<!-- Copyright Mumble-demo Inc. -->";

    var files = nsynFs.readdir(nsynjsCtx, ".").data;

    var content="";
    for(var i=0; i<files.length; i++) {
        var file = files[i];
        if(file.endsWith('.md'))
            content+=nsynFs.readFile(nsynjsCtx,file,"utf8").data;
    }
    nsynFs.writeFile(nsynjsCtx,"derp",HEADER+content+FOOTER);
};

nsynjs.run(synchronousCode, {},nsynFs, function () {
    console.log('synchronousCode done')
});

即使它看起来是同步的,它也不会使用任何同步函数,因此它不会阻止节点的事件循环。

答案 2 :(得分:0)

尝试Gulp,这是当今最惯用的方式。

如果不能或不想要,请使用Promise链,它们就像是外壳管道。

#!/usr/bin/env node
'use strict';

const Promise = require('bluebird');
const fs = Promise.promisifyAll(require('fs'));
const path = require('path');
const marked = require('marked');

const HEADER = `<!-- Generated at ${(new Date()).toISOString()} -->`;
const FOOTER = '<!-- Copyright Mumble-demo Inc. -->';

fs.readdirAsync(process.cwd())
  .map((fileName) => Promise.all([fileName, fs.statAsync(fileName)]))
  .filter(([fileName, stat]) => stat.isFile() && path.extname(fileName) === '.md')
  .call('sort', ([a], [b]) => a.localeCompare(b, 'en-US'))
  .map(([mdFileName]) => fs.readFileAsync(mdFileName, 'utf8'))
  .then((mdFiles) => {
    let out = [HEADER, marked(mdFiles.join('\n')), FOOTER].join('').replace(/\n/g, '');
    console.log(out);
    return fs.writeFileAsync('out.html', out);
  })
  .catch((err) => {
    console.error(err);
    process.exit(1);
  });

思想:

  • 永远不要在Node中写同步代码,你会一直后悔。
  • 承诺链最适合此类任务。
  • stat.isFile()和sort()只是bash和Ruby示例中缺少的安全功能。删除它们可以保存两行代码。
  • 在大多数情况下,应将Date.prototype.toString()的用法视为错误,因为输出是不可预测的,它是特定于平台和区域设置的。
  • 在处理大型文件之前,节点流是过度的,这通常不适用于降价任务。
  • Shell管道也没有使用文件系统流并将所有内容加载到内存中。效率大致相同。