一个添加了一些安全性的查询字符串解析和字符串化库。
var qs = require('qs');
var assert = require('assert');
var obj = qs.parse('a=c');
assert.deepEqual(obj, { a: 'c' });
var str = qs.stringify(obj);
assert.equal(str, 'a=c');
解析对象
qs.parse(string, [options]);
qs 允许你在查询字符串中通过使用方括号 []
来创建嵌套对象。例如,字符串 'foo[bar]=baz'
会被转换为:
assert.deepEqual(qs.parse('foo[bar]=baz'), {
foo: {
bar: 'baz'
}
});
使用 plainObjects
选项时,解析后的值会以 null
对象的形式返回,它通过 Object.create(null)
创建。因此需要注意,该对象不会继承原型方法,用户可以将这些方法名设置为任意值:
var nullObject = qs.parse('a[hasOwnProperty]=b', { plainObjects: true });
assert.deepEqual(nullObject, { a: { hasOwnProperty: 'b' } });
默认情况下,参数将不会覆盖对象原型上的属性。如果希望保留这些字段的数据,可以使用前面提到的 plainObjects
,或者将 allowPrototypes
设置为 true
,这将允许用户输入覆盖这些属性。警告:启用此选项通常不是一个好主意,因为它可能在使用被覆盖的属性时引发问题。因此在使用此选项时应小心。
var protoObject = qs.parse('a[hasOwnProperty]=b', { allowPrototypes: true });
assert.deepEqual(protoObject, { a: { hasOwnProperty: 'b' } });
URI 编码的字符串同样有效:
assert.deepEqual(qs.parse('a%5Bb%5D=c'), {
a: { b: 'c' }
});
你还可以嵌套对象,例如 'foo[bar][baz]=foobarbaz'
:
assert.deepEqual(qs.parse('foo[bar][baz]=foobarbaz'), {
foo: {
bar: {
baz: 'foobarbaz'
}
}
});
默认情况下,qs 解析嵌套对象的深度限制为 5 层。这意味着如果尝试解析像 'a[b][c][d][e][f][g][h][i]=j'
这样的字符串,结果对象将是:
var expected = {
a: {
b: {
c: {
d: {
e: {
f: {
'[g][h][i]': 'j'
}
}
}
}
}
}
};
var string = 'a[b][c][d][e][f][g][h][i]=j';
assert.deepEqual(qs.parse(string), expected);
可以通过 depth
选项来重写深度限制:
var deep = qs.parse('a[b][c][d][e][f][g][h][i]=j', { depth: 1 });
assert.deepEqual(deep, { a: { b: { '[c][d][e][f][g][h][i]': 'j' } } });
使用 strictDepth
选项(默认为 false
),qs 可以在解析嵌套输入超出深度限制时抛出错误:
try {
qs.parse('a[b][c][d][e][f][g][h][i]=j', { depth: 1, strictDepth: true });
} catch (err) {
assert(err instanceof RangeError);
assert.strictEqual(err.message, 'Input depth exceeded depth option of 1 and strictDepth is true');
}
深度限制有助于防止滥用 qs 解析用户输入,因此建议保持较小的数值。strictDepth
选项通过在超过限制时抛出错误,为此提供了另一层保护,让你可以捕捉和处理这种情况。
出于类似的原因,qs 默认只会解析最多 1000 个参数。可以通过 parameterLimit
选项来重写这个限制:
var limited = qs.parse('a=b&c=d', { parameterLimit: 1 });
assert.deepEqual(limited, { a: 'b' });
为了跳过前导的问号,可以使用 ignoreQueryPrefix
:
var prefixed = qs.parse('?a=b&c=d', { ignoreQueryPrefix: true });
assert.deepEqual(prefixed, { a: 'b', c: 'd' });
也可以传入自定义的分隔符:
var delimited = qs.parse('a=b;c=d', { delimiter: ';' });
assert.deepEqual(delimited, { a: 'b', c: 'd' });
分隔符也可以是正则表达式:
var regexed = qs.parse('a=b;c=d,e=f', { delimiter: /[;,]/ });
assert.deepEqual(regexed, { a: 'b', c: 'd', e: 'f' });
allowDots
选项可以启用点号表示法:
var withDots = qs.parse('a.b=c', { allowDots: true });
assert.deepEqual(withDots, { a: { b: 'c' } });
decodeDotInKeys
选项用于解码键中的点号。注意:它隐含了 allowDots
,因此如果将 decodeDotInKeys
设置为 true
而 allowDots
为 false
,则解析将出错。
var withDots = qs.parse('name%252Eobj.first=John&name%252Eobj.last=Doe', { decodeDotInKeys: true });
assert.deepEqual(withDots, { 'name.obj': { first: 'John', last: 'Doe' }});
allowEmptyArrays
选项允许对象中存在空数组值:
var withEmptyArrays = qs.parse('foo[]&bar=baz', { allowEmptyArrays: true });
assert.deepEqual(withEmptyArrays, { foo: [], bar: 'baz' });
duplicates
选项可以改变处理重复键的行为:
assert.deepEqual(qs.parse('foo=bar&foo=baz'), { foo: ['bar', 'baz'] });
assert.deepEqual(qs.parse('foo=bar&foo=baz', { duplicates: 'combine' }), { foo: ['bar', 'baz'] });
assert.deepEqual(qs.parse('foo=bar&foo=baz', { duplicates: 'first' }), { foo: 'bar' });
assert.deepEqual(qs.parse('foo=bar&foo=baz', { duplicates: 'last' }), { foo: 'baz' });
如果需要处理遗留浏览器或服务,qs 还支持将百分比编码的八位字节解码为 iso-8859-1 编码:
var oldCharset = qs.parse('a=%A7', { charset: 'iso-8859-1' });
assert.deepEqual(oldCharset, { a: '§' });
某些服务会在表单中添加初始的 utf8=✓
值,以便旧版 Internet Explorer 更有可能以 utf-8 提交表单。此外,服务器可以检查该值与错误编码的对勾字符,从而检测查询字符串或 application/x-www-form-urlencoded 消息体未按 utf-8 发送,例如如果表单有 accept-charset
参数或包含页面有不同字符集。
qs 通过 charsetSentinel
选项支持这种机制。如果指定了此选项,解析后的对象将忽略 utf8
参数。它会根据对勾字符的编码方式切换到 iso-8859-1 或 utf-8 模式。
重要提示:当同时指定 charset
选项和 charsetSentinel
选项时,charset
将在请求包含 utf8
参数时被覆盖,从而根据实际字符集推断。也就是说,charset
将表现为默认字符集,而不是权威字符集。
var detectedAsUtf8 = qs.parse('utf8=%E2%9C%93&a=%C3%B8', {
charset: 'iso-8859-1',
charsetSentinel: true
});
assert.deepEqual(detectedAsUtf8, { a: 'ø' });
// 浏览器在以 iso-8859-1 提交时将对勾编码为 ✓:
var detectedAsIso8859_1 = qs.parse('utf8=%26%2310003%3B&a=%F8', {
charset: 'utf-8',
charsetSentinel: true
});
assert.deepEqual(detectedAsIso8859_1, { a: 'ø' });
如果希望将 &#...;
语法解码为实际字符,可以指定 interpretNumericEntities
选项:
var detectedAsIso8859_1 = qs.parse('a=%26%239786%3B', {
charset: 'iso-8859-1',
interpretNumericEntities: true
});
assert.deepEqual(detectedAsIso8859_1, { a: '☺' });
当字符集在 charsetSentinel
模式中被检测到时,这个选项也有效。
数组解析
qs
也可以使用类似 []
的表示法来解析数组:
var withArray = qs.parse('a[]=b&a[]=c');
assert.deepEqual(withArray, { a: ['b', 'c'] });
你也可以指定索引:
var withIndexes = qs.parse('a[1]=c&a[0]=b');
assert.deepEqual(withIndexes, { a: ['b', 'c'] });
注意,数组中的索引与对象中的键之间唯一的区别是括号中的值必须是数字,以创建数组。当使用特定索引创建数组时,qs
会压缩稀疏数组,只保留现有值并保持其顺序:
var noSparse = qs.parse('a[1]=b&a[15]=c');
assert.deepEqual(noSparse, { a: ['b', 'c'] });
你还可以使用 allowSparse
选项来解析稀疏数组:
var sparseArray = qs.parse('a[1]=2&a[3]=5', { allowSparse: true });
assert.deepEqual(sparseArray, { a: [, '2', , '5'] });
注意,空字符串也是一个值,并将被保留:
var withEmptyString = qs.parse('a[]=&a[]=b');
assert.deepEqual(withEmptyString, { a: ['', 'b'] });
var withIndexedEmptyString = qs.parse('a[0]=b&a[1]=&a[2]=c');
assert.deepEqual(withIndexedEmptyString, { a: ['b', '', 'c'] });
qs
还将限制数组中指定的最大索引为 20。任何索引大于 20 的数组成员将被转换为一个对象,以索引作为键。这是为了处理某些情况下,如 a[999999999]
的情况,这将需要大量时间来迭代这个巨大的数组。
var withMaxIndex = qs.parse('a[100]=b');
assert.deepEqual(withMaxIndex, { a: { '100': 'b' } });
这个限制可以通过传递 arrayLimit
选项来覆盖:
var withArrayLimit = qs.parse('a[1]=b', { arrayLimit: 0 });
assert.deepEqual(withArrayLimit, { a: { '1': 'b' } });
要完全禁用数组解析,可以将 parseArrays
设置为 false
:
var noParsingArrays = qs.parse('a[]=b', { parseArrays: false });
assert.deepEqual(noParsingArrays, { a: { '0': 'b' } });
如果你混合使用不同的表示法,qs
将把两个项合并为一个对象:
var mixedNotation = qs.parse('a[0]=b&a[b]=c');
assert.deepEqual(mixedNotation, { a: { '0': 'b', b: 'c' } });
你还可以创建对象的数组:
var arraysOfObjects = qs.parse('a[][b]=c');
assert.deepEqual(arraysOfObjects, { a: [{ b: 'c' }] });
一些人使用逗号连接数组,qs
可以解析它:
var arraysOfObjects = qs.parse('a=b,c', { comma: true })
assert.deepEqual(arraysOfObjects, { a: ['b', 'c'] });
(这不能转换嵌套对象,例如 a={b:1},{c:d}
)
解析原始/标量值(数字、布尔值、null 等)
默认情况下,所有值都被解析为字符串。这种行为不会改变,并在问题 #91 中解释。
var primitiveValues = qs.parse('a=15&b=true&c=null');
assert.deepEqual(primitiveValues, { a: '15', b: 'true', c: 'null' });
如果你希望自动转换看起来像数字、布尔值和其他值的原始对应物,你可以使用 query-types
Express JS 中间件,它会自动转换所有请求查询参数。
字符串化
qs.stringify(object, [options])
在字符串化时,qs
默认会对输出进行 URI 编码。对象会按照预期的方式被字符串化:
assert.equal(qs.stringify({ a: 'b' }), 'a=b');
assert.equal(qs.stringify({ a: { b: 'c' } }), 'a%5Bb%5D=c');
可以通过将 encode 选项设置为 false 来禁用此编码:
复制代码
var unencoded = qs.stringify({ a: { b: 'c' } }, { encode: false });
assert.equal(unencoded, 'a[b]=c');
禁用键的编码
可以通过将 encodeValuesOnly
选项设置为 true
来禁用键的编码:
var encodedValues = qs.stringify(
{ a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] },
{ encodeValuesOnly: true }
);
assert.equal(encodedValues, 'a=b&c[0]=d&c[1]=e%3Df&f[0][0]=g&f[1][0]=h');
此编码也可以通过设置为 encoder
选项的自定义编码方法进行替换:
var encoded = qs.stringify({ a: { b: 'c' } }, { encoder: function (str) {
// 传入的值 `a`、`b`、`c`
return // 返回编码后的字符串
}});
注意: 如果
encode
为false
,则encoder
选项不适用。
与 encoder
类似,parse
也有一个 decoder
选项,用于覆盖属性和值的解码:
var decoded = qs.parse('x=z', { decoder: function (str) {
// 传入的值 `x`、`z`
return // 返回解码后的字符串
}});
您可以使用传递给编码器的 type
参数对键和值进行不同逻辑的编码:
var encoded = qs.stringify({ a: { b: 'c' } }, { encoder: function (str, defaultEncoder, charset, type) {
if (type === 'key') {
return // 编码后的键
} else if (type === 'value') {
return // 编码后的值
}
}});
type
参数也适用于解码器:
var decoded = qs.parse('x=z', { decoder: function (str, defaultDecoder, charset, type) {
if (type === 'key') {
return // 解码后的键
} else if (type === 'value') {
return // 解码后的值
}
}});
从此处开始的示例将显示输出未进行 URI 编码以便于理解。请注意,在实际使用中,这些情况下的返回值将被 URI 编码。
当数组被字符串化时,它们遵循 arrayFormat
选项,默认为索引:
qs.stringify({ a: ['b', 'c', 'd'] });
// 'a[0]=b&a[1]=c&a[2]=d'
您可以通过将 indices
选项设置为 false
来覆盖此设置,或者更明确地将 arrayFormat
选项设置为 repeat
:
qs.stringify({ a: ['b', 'c', 'd'] }, { indices: false });
// 'a=b&a=c&a=d'
你可以使用 arrayFormat
选项来指定输出数组的格式:
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'indices' })
// 'a[0]=b&a[1]=c'
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'brackets' })
// 'a[]=b&a[]=c'
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'repeat' })
// 'a=b&a=c'
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'comma' })
// 'a=b,c'
注意: 当使用 arrayFormat
设置为 'comma'
时,你可以传递 commaRoundTrip
选项并设置为 true
或 false
,以便在只有一个元素的数组时追加 []
,使其能够通过 parse
函数回传。
对象的字符串化
当对象被字符串化时,默认使用括号表示法:
qs.stringify({ a: { b: { c: 'd', e: 'f' } } });
// 'a[b][c]=d&a[b][e]=f'
你可以通过设置 allowDots
选项为 true
来使用点号表示法:
qs.stringify({ a: { b: { c: 'd', e: 'f' } } }, { allowDots: true });
// 'a.b.c=d&a.b.e=f'
你可以使用 encodeDotInKeys
选项将点号编码到对象的键中:
qs.stringify({ "name.obj": { "first": "John", "last": "Doe" } }, { allowDots: true, encodeDotInKeys: true });
// 'name%252Eobj.first=John&name%252Eobj.last=Doe'
允许空数组值
你可以通过设置 allowEmptyArrays
选项为 true
来允许空数组值:
qs.stringify({ foo: [], bar: 'baz' }, { allowEmptyArrays: true });
// 'foo[]&bar=baz'
空字符串和 null
值
空字符串和 null
值会省略值,但等号 =
仍然存在:
assert.equal(qs.stringify({ a: '' }), 'a=');
没有值的键(如空对象或数组)将返回空:
assert.equal(qs.stringify({ a: [] }), '');
assert.equal(qs.stringify({ a: {} }), '');
assert.equal(qs.stringify({ a: [{}] }), '');
assert.equal(qs.stringify({ a: { b: []} }), '');
assert.equal(qs.stringify({ a: { b: {}} }), '');
忽略 undefined
的属性
设置为 undefined
的属性将被完全忽略:
assert.equal(qs.stringify({ a: null, b: undefined }), 'a=');
添加问号前缀
查询字符串可以选择性地在前面加上问号:
assert.equal(qs.stringify({ a: 'b', c: 'd' }, { addQueryPrefix: true }), '?a=b&c=d');
自定义分隔符
你可以使用 stringify
来覆盖默认分隔符:
assert.equal(qs.stringify({ a: 'b', c: 'd' }, { delimiter: ';' }), 'a=b;c=d');
日期序列化
如果你只想覆盖 Date
对象的序列化,可以提供 serializeDate
选项:
var date = new Date(7);
assert.equal(qs.stringify({ a: date }), 'a=1970-01-01T00:00:00.007Z'.replace(/:/g, '%3A'));
assert.equal(
qs.stringify({ a: date }, { serializeDate: function (d) { return d.getTime(); } }),
'a=7'
);
影响键顺序
你可以使用 sort
选项来影响参数键的顺序:
function alphabeticalSort(a, b) {
return a.localeCompare(b);
}
assert.equal(qs.stringify({ a: 'c', z: 'y', b : 'f' }, { sort: alphabeticalSort }), 'a=c&b=f&z=y');
使用 filter
选项
你可以使用 filter
选项来限制哪些键会包含在字符串化输出中。如果你传递一个函数,它会被每个键调用以获取替代值。否则,如果你传递一个数组,它将用于选择要字符串化的属性和数组索引:
function filterFunc(prefix, value) {
if (prefix == 'b') {
// 返回 `undefined` 来忽略属性
return;
}
if (prefix == 'e[f]') {
return value.getTime();
}
if (prefix == 'e[g][0]') {
return value * 2;
}
return value;
}
qs.stringify({ a: 'b', c: 'd', e: { f: new Date(123), g: [2] } }, { filter: filterFunc });
// 'a=b&c=d&e[f]=123&e[g][0]=4'
处理 null
值
默认情况下,null
值会被处理为空字符串:
var withNull = qs.stringify({ a: null, b: '' });
assert.equal(withNull, 'a=&b=');
要完全跳过 null
值的渲染,请使用 skipNulls
选项:
var nullsSkipped = qs.stringify({ a: 'b', c: null}, { skipNulls: true });
assert.equal(nullsSkipped, 'a=b');
自定义字符集
如果你与遗留系统通信,可以使用 charset
选项切换到 iso-8859-1
:
var iso = qs.stringify({ æ: 'æ' }, { charset: 'iso-8859-1' });
assert.equal(iso, '%E6=%E6');
字符集中的字符将被转换为数字实体:
var numeric = qs.stringify({ a: '☺' }, { charset: 'iso-8859-1' });
assert.equal(numeric, 'a=%26%239786%3B');