我是否正确地思考回调?

时间:2015-11-24 15:12:08

标签: javascript node.js callback

我是node和js的新手,直到昨天我都不知道回调和异步编程,所以跟我说话就像我是个白痴一样,我是...

随着mixed.io的死亡,我想我会编写自己的小静态网站构建器。我看着gulp和grunt,但是因使用npm as a build tool而感到沮丧。

构建css,缩小,列出等等是非常容易的,但是当构建页面时,生活很快就会陷入回调地狱。

一点阅读,我开始编写页面构建脚本:

var fm          = require('front-matter'),
    fs          = require('fs'),
    glob        = require('glob'),
    md          = require('marked');

const SEARCHPATH = "content/pages/";


pages = [];



function searchFiles () {
    glob("*.md", { cwd: SEARCHPATH }, readFiles);
}


function readFiles (err, files) {
    if(err) throw err;

    for (var file of files) {
        fs.readFile(SEARCHPATH + file, 'utf8', processFiles);
    }
}


function processFiles(err, data) {
    if(err) throw err;

    var attributes = fm(data).attributes;
    var content = md(fm(data).body);

    pages.push(attributes, content);

    applyTemplate(pages);
}

function applyTemplate(pages) {
    console.log(pages);
}

searchFiles();

但它会像我一样寻找所有的世界,即将进入菊花链地狱,每个函数都会调用下一个函数,但我无法访问页面变量而不这样做。

这一切似乎都有点过了。

我在想这个吗?以编程方式构建此更好的方法是什么?

感谢Overflowers。

1 个答案:

答案 0 :(得分:0)

您将所有回调分解为函数声明而不是内联表达式,因此您已经有+1,因为您有可以导出和测试的函数对象。

对于这个回答,我假设优先级是孤立的程序性单元测试,而不是模仿require。 (当我进入一个新项目时,我通常会发现自己正在重构遗留的node.js。)

当我沿着嵌套嵌套回调的这条路走时,我认为最简单的方法是使用嵌套的匿名表达式链作为回调:(在psuedocode中)

function doFiles() {
   glob('something', function(files) {
      for (f in files) {
         fs.readFile(file, function(err, data) {
            processFile(data);
         }

      }
   }
}

以编程方式测试上述内容非常复杂。唯一的方法是模拟需求。要测试readFile回调是否正常工作,您必须先控制所有调用!这违反了测试中的隔离。

第二个最好的方法,imo,就是像你一样打破回调。

这样可以更好地隔离单元测试,但仍需要fsglob的模拟需求。

第三种最佳方法imo是注入所有函数依赖项,以便轻松配置模拟对象。这通常看起来很奇怪,但对我来说,目标是100%覆盖率,在不使用模拟需求库的独立单元测试中。它使得每个函数都是一个易于测试的独立对象,并为其配置模拟对象,但通常会使调用该函数更加复杂!

实现这一目标:

function searchFiles () {
    glob("*.md", { cwd: SEARCHPATH }, readFiles);
}

会变成

function searchFiles (getFiles, getFilesCallback) {
    getFiles("*.md", { cwd: SEARCHPATH }, getFilesCallback);
}

然后可以用

调用它
searchFiles(glob, readFiles)

这看起来有点时髦,因为它是一个单行函数,但说明了如何将依赖项注入函数,以便测试可以配置模拟对象并将它们直接传递给函数。重构readFiles来执行此操作:

function readFiles (err, files, readFile, processFileCb) {
    if(err) throw err;

    for (var file of files) {
        readFile(SEARCHPATH + file, 'utf8', processFileCb);
    }
}

readFiles接受readFile方法(fs.readFile并在读取文件后执行回调。这样可以在程序化测试中轻松配置模拟对象。

然后测试可以是,在伪代码中:

it('throws err when error is found', function() {
    var error = true;
    assert throws readFiles(error)
});

it('calls readFile for every file in files', function() {
   var files = ['file1'];
   var error = false;
   var readFile = createSpyMaybeSinon?();
   var spyCallback = createSpy();
   readFiles(error, files, readFile, spyCallback);

   assert(readFile.calls.count(), files.length)
   assert readFile called with searchpath + file1, 'utf8', spyCallback
});

一旦你拥有了这些需要客户端提供所有函数依赖关系的函数,那么它们需要一个创造性的bind回调或小函数表达式来包装调用。

以上假定完整测试覆盖的结束而没有嘲弄要求,这可能不是您的目标:)

A"清洁工"方法imo只是使用来自beginnning的promises,这是异步调用的一个很好的抽象。