我正在尝试为"可链接"编写模块。 Express.js验证:
const validatePost = (req, res, next) => {
validator.validate(req.body)
.expect('name.first')
.present('The parameter is required')
.string('The parameter must be a string')
.go(next);
};
router.post('/', validatePost, (req, res, next) => {
return res.send('Validated!');
});
validator.validate
的代码(为简洁而简化):
const validate = (data) => {
let validation;
const expect = (key) => {
validation.key = key;
// Here I get the actual value, but for testing purposes of .present() and
// .string() chainable methods it returns a random value from a string,
// not string and an undefined
validation.value = [ 'foo', 123, void 0 ][Math.floor(Math.random() * 3)];
return validation;
};
const present = (message) => {
if (typeof validation.value === 'undefined') {
validation.valid = false;
validation.errors.push({ key: validation.key, message: message });
}
return validation;
};
const string = (message) => {
if (typeof validation.value !== 'string') {
validation.valid = false;
validation.errors.push({ key: validation.key, message: message });
}
return validation;
};
const go = (next) => {
if (!validation.valid) {
let error = new Error('Validation error');
error.name = 'ValidationError';
error.errors = validation.errors;
// I even wrap async callbacks in process.nextTick()
process.nextTick(() => next(error));
}
process.nextTick(next);
};
validation = {
valid: true,
data: data,
errors: [],
expect: expect,
present: present,
string: string,
go: go
};
return validation;
};
该代码适用于短链,返回正确的错误对象。但是,如果我链接了很多方法,请说:
const validatePost = (req, res, next) => {
validator.validate(req.body)
.expect('name.first')
.present('The parameter is required')
.string('The parameter must be a string')
.expect('name.first') // Same for testing
.present('The parameter is required')
.string('The parameter must be a string')
// [...] 2000 times
.go(next);
};
Node.js抛出RangeError: Maximum call stack size exceeded
。请注意,我将异步回调.go(next)
包装在process.nextTick()
。
答案 0 :(得分:4)
我没有太多时间来看这个,但我确实注意到一个相当大的问题。当next
为!validator.valid
时,您有一个单分支if语句导致true
被称为两次。通常,单分支if
语句是代码气味。
这可能不是您遇到堆栈溢出的原因,但它可能是罪魁祸首。
(代码更改以粗体显示)
const go = (next) => {
if (!validation.valid) {
let error = new Error('Validation error');
error.name = 'ValidationError';
error.errors = validation.errors;
process.nextTick(() => next(error));
}
else {
process.nextTick(next);
}
};
有些人也使用return
欺骗if
。这也有效,但很糟糕
const go = (next) => {
if (!validation.valid) {
let error = new Error('Validation error');
error.name = 'ValidationError';
error.errors = validation.errors;
process.nextTick(() => next(error));
return; // so that the next line doesn't get called too
}
process.nextTick(next);
};
我认为整个go
函数表达得更好......
const go = (next) => {
// `!` is hard to reason about
// place the easiest-to-understand, most-likely-to-happen case first
if (validation.valid) {
process.nextTick(next)
}
// very clear if/else branching
// there are two possible outcomes and one block of code for each
else {
let error = new Error('Validation error');
error.name = 'ValidationError';
error.errors = validation.errors;
// no need to create a closure here
process.nextTick(() => next(error));
process.nextTick(next, error);
}
};
其他评论
您的代码中还有其他单分支if
语句
const present = (message) => {
if (typeof validation.value === 'undefined') {
// this branch only performs mutations and doesn't return anything
validation.valid = false;
validation.errors.push({ key: validation.key, message: message });
}
// there is no `else` branch ...
return validation;
};
这一点不那么冒犯,但我仍然认为一旦你对if
总是有else
的陈述表示赞赏,就更难以推理。考虑强制两个分支的三元运算符(?:
)。还要考虑像Scheme这样的语言,使用if
时总是需要 True 和 False 分支。
以下是我编写present
函数
const present = (message) => {
if (validation.value === undefined) {
// True branch returns
return Object.assign(validation, {
valid: false,
errors: [...validation.errors, { key: validation.key, message }]
})
}
else {
// False branch returns
return validation
}
};
这是一个自以为是的评论,但我认为值得考虑。当您必须返回此代码并稍后阅读时,您会感谢我。当然,一旦你的代码采用这种格式,你就可以解决它的大量语法模板
const present = message =>
validation.value === undefined
? Object.assign(validation, {
valid: false,
errors: [...validation.errors, { key: validation.key, message }]
})
: validation
优点
return
会强制您在函数中使用单个表达式 - 这意味着您不能(轻松地)使函数过于复杂if
没有返回值,因此使用三元可以很好地处理隐式返回true
和 false
分支,以便始终处理谓词的两个结果是的,没有什么可以阻止你使用()
将多个表达式组合成一个表达式,但重点不是将每个函数都减少为单个表达式 - 它更适合于使用它时理想而且很好用解决了。如果您觉得可读性受到影响,您可以使用if (...) { return ... } else { return ... }
来获得熟悉且友好的语法/风格。
答案 1 :(得分:3)
方法链接溢出
validate({ name: { last: 'foo' }})
// Duplicate this line ~2000 times for error
.expect('name.first').present().string()
.go(console.log);
您根本无法在单个表达式中链接那么多方法。
在isolated test中,我们显示这与递归无关或process.nextTick
class X {
foo () {
return this
}
}
let x = new X()
x.foo().foo().foo().foo().foo().foo().foo().foo().foo().foo()
.foo().foo().foo().foo().foo().foo().foo().foo().foo().foo()
.foo().foo().foo().foo().foo().foo().foo().foo().foo().foo()
...
.foo().foo().foo().foo().foo().foo().foo().foo().foo().foo()
// RangeError: Maximum call stack size exceeded
在OSX上使用64位Chrome,在堆栈溢出发生之前,方法链限制为 6253 。这可能因实施而异。
横向思维
方法链DSL似乎是为数据指定验证属性的好方法。在给定的验证表达式中,您不太可能需要链接超过几十行,因此您不应该过于担心限制。
除此之外,完全不同的解决方案可能更好。立即浮现在脑海中的一个例子是JSON schema。不是用代码编写验证,而是用数据声明地写它。
这是一个快速的JSON架构示例
{
"title": "Example Schema",
"type": "object",
"properties": {
"firstName": {
"type": "string"
},
"lastName": {
"type": "string"
},
"age": {
"description": "Age in years",
"type": "integer",
"minimum": 0
}
},
"required": ["firstName", "lastName"]
}
对于架构的大小,实际上没有限制,因此这应该适合解决您的问题。
其他优势