問題描述
我是否以正確的方式考慮回調? (Am I thinking about Callbacks the right way?)
我對 node 和 js 還很陌生,直到昨天我才知道回調和異步編程,所以跟我說話就像我是個白痴一樣,因為我是......
有mix.io 之死我想我會編寫自己的小型靜態站點構建器。我看著 gulp 和 grunt 但對使用 npm 作為構建工具。
構建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();
但它像我一樣尋找全世界 m 即將陷入菊花鏈地獄,每個函數都調用下一個函數,但如果不這樣做,我就無法訪問 pages 變量。
這一切似乎有點不對勁。
我在想這個嗎?以編程方式構建此結構的更好方法是什麼?
一如既往地感謝Overflowers。
參考解法
方法 1:
You broke out all of the callbacks into function declarations instead of inline expressions so that is already +1, because you have function objects that can be exported and tested.
For this response I'm assuming the priority is isolated progrmatic unittests, without mocking require
. (Which I usually find myself refactoring legacy node.js towards, when I enter a new project).
When I go down this route of nested nested callbacks I think the least easy way to work with is a nested chain of anonymous expressions as callbacks: (in psuedocode)
function doFiles() {
glob('something', function(files) {
for (f in files) {
fs.readFile(file, function(err, data) {
processFile(data);
}
}
}
}
Testing the above programmatically is pretty convoluted. The only way to do it is to mock requires. To test that readFile callback is working, you have to control all calls before it!!! Which violates isolation in tests.
The 2nd best approach, imo, is to break out callbacks as you have done.
Which allows better isolation for unittests, but still requires mocking requires for fs
and glob
.
The 3rd best approach, imo, is injecting all a functions dependencies, to allow easy configuration of mock objects. This often looks very weird, but for me the goal is 100% coverage, in isolated unittests without using a mock require library. It makes it so each function is an isolated object that is easy to test, and configure mock objects for, but often makes calling that function more convoluted!
To achieve this:
function searchFiles () {
glob("*.md", { cwd: SEARCHPATH }, readFiles);
}
Would become
function searchFiles (getFiles, getFilesCallback) {
getFiles("*.md", { cwd: SEARCHPATH }, getFilesCallback);
}
Then it could be called with
searchFiles(glob, readFiles)
This looks a little funky because it is a one line function, but illustrates how to inject the dependencies into your functions, so that your tests can configure mock objects and pass them directly to the function. Refactoring readFiles
to do this:
function readFiles (err, files, readFile, processFileCb) {
if(err) throw err;
for (var file of files) {
readFile(SEARCHPATH + file, 'utf8', processFileCb);
}
}
readFiles takes in a readFile
method (fs.readFile
and callback to execute once the file is read. Which allows easy configuration of mock objects in programmatic testing.
Then tests could be, in psuedocode:
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
});
Once you have these functions that require the client to provide all of the functions dependencies, then they require a dance of creative bind
ing of callbacks, or small functional expressions to wrap calls.
The above assumes an endgoal of complete test coverage without mocking requires, which might not be your goal :)
A "cleaner" approach imo is just to use promises from the beginnning, which is a wonderful abstraction over asynchronous calls.