在Node.js服务端或浏览器端脚本里,拼SQL最怕的就是引号错位和用户输入直接进语句。JavaScript的模板字符串不只是写起来省事,它背后的标签函数(tagged template)能让我们在字符串真正生成前拿到所有片段和变量,从而构建一个轻量级的SQL查询构建器。

一、模板字符串与标签函数原理
普通模板字符串写成`select * from user where id = ${id}`,引擎会直接把${id}求值并转成字符串。但如果前面带一个函数名,比如sql`...`,这就变成了标签函数调用。该函数会收到两个核心参数:一个是被插值点切开的字符串数组,另一个是插值表达式依次求值后的结果数组。
具体来说,对于sql`select * from t where a=${x} and b=${y}`,第一个参数是["select * from t where a=", " and b=", ""],第二个参数是[x, y]。我们可以在函数中遍历它们,把字符串原样保留,把值统一处理成占位符或转义后的文本,最后拼出安全且结构清晰的SQL。
// 标签函数基本结构
function sql(strings, ...values) {
let result = '';
strings.forEach((str, i) => {
result += str;
if (i < values.length) {
// 此处暂不直接拼接值,后续做转义或占位符
result += '?';
}
});
return { text: result, params: values };
}
const id = 10;
const name = 'tom';
const query = sql`select * from user where id=${id} and name=${name}`;
console.log(query.text); // select * from user where id=? and name=?
console.log(query.params); // [10, 'tom']
二、构建一个支持动态条件的查询构建器
实际业务中where条件往往动态变化:有时按id查,有时按状态查,有时两者都要。如果靠if判断拼字符串,代码很快变成一堆加号和空格的迷宫。用标签函数可以把“值”先收起来,再在外部根据是否有值来决定是否保留对应片段。
我们的思路是:在标签函数里不直接生成最终SQL,而是返回一个中间对象,记录每个片段和对应值。然后提供一个build方法,过滤掉那些值为undefined或null的条件片段,自动修正and/or连接词,生成干净的语句。这样调用方写起来就像普通模板字符串,却不用操心条件缺失导致的语法错误。
function sql(strings, ...values) {
const parts = [];
strings.forEach((str, i) => {
parts.push({ text: str, value: i < values.length ? values[i] : undefined });
});
return {
parts,
build() {
let text = '';
const params = [];
let pendingAnd = false;
this.parts.forEach((p, idx) => {
if (p.value === undefined || p.value === null) {
// 如果该片段后紧跟的插值为空,尝试去掉前面的and/or
text = text.replace(/s*(and|or)s*$/i, '');
return;
}
text += p.text;
if (p.value !== undefined) {
text += '?';
params.push(p.value);
}
});
return { text: text.trim(), params };
}
};
}
const status = 'active';
const minAge = undefined;
const q = sql`select * from member where and status=${status} and age>${minAge}`;
const built = q.build();
console.log(built.text); // select * from member where status=?
console.log(built.params); // ['active']
三、防注入与值转义处理
很多人担心模板字符串拼SQL会引发注入,其实风险不在模板字符串本身,而在是否把用户输入直接混进字符串。上面例子用问号占位符把值和语句分离,交给数据库驱动(如mysql2、pg)做预处理,就能天然防注入。如果必须内联值(比如生成报表文件),则在标签函数里对字符串、数字分别转义。
下面的增强版在build时判断类型:字符串用单引号包裹并把内部单引号翻倍,数字直接原样,布尔转成对应关键字。这样即便不用驱动预处理,也能挡住大部分恶意输入。当然生产环境仍推荐占位符方案,内联转义只适合受信任环境或日志输出。
function escapeValue(v) {
if (typeof v === 'number') return v;
if (typeof v === 'boolean') return v ? 'true' : 'false';
if (typeof v === 'string') return "'" + v.replace(/'/g, "''") + "'";
return 'null';
}
function sqlInline(strings, ...values) {
let text = '';
strings.forEach((str, i) => {
text += str;
if (i < values.length) {
text += escapeValue(values[i]);
}
});
return text;
}
const userInput = "tom' or '1'='1";
const dangerous = sqlInline`select * from user where name=${userInput}`;
console.log(dangerous);
// select * from user where name='tom'' or ''1''=''1'
四、对比传统拼接方式
用加号拼接SQL时,开发者要手动在每段后面补空格,稍不留神就写成"select*from"。条件动态时还要写很多if (x) sql += " and x="+x,既难读又易错。模板字符串构建器把结构和数据分开,代码片段即文档,新人也能一眼看出最终语句形状。
从性能看,标签函数调用本身开销极小,远小于网络往返和数据库解析。在高频场景可缓存生成的text结构,仅替换params。下表列出两者差异:
| 维度 | 加号拼接 | 模板字符串构建器 |
|---|---|---|
| 可读性 | 差,易漏空格 | 好,结构直观 |
| 注入风险 | 高,常直接混用户输入 | 低,可用占位符 |
| 动态条件 | 需大量if分支 | 标签函数内统一过滤 |
| 调试 | 字符串散落各处 | 集中生成易打印 |
五、完整可复用模块示例
把前面的思路合并,就能得到一个小型查询构建器。它支持select、where动态片段以及问号占位符输出,可直接接mysql2的execute方法。注意函数名sql只是个标签,不是HTML里的标签,调用时别和DOM搞混。
以下代码可直接复制到项目里使用,根据业务扩展orderBy、limit也很简单:只需在parts里追加对应片段并在build中保留即可。这种写法比引入重型ORM轻便,又比裸拼字符串稳妥。
function sql(strings, ...values) {
const parts = [];
strings.forEach((s, i) => parts.push({ t: s, v: i < values.length ? values[i] : undefined }));
return {
build() {
let text = '';
const params = [];
parts.forEach(p => {
if (p.v === undefined || p.v === null) {
text = text.replace(/s*(and|or)s*$/i, '');
return;
}
text += p.t + '?';
params.push(p.v);
});
return { sql: text.replace(/s+/g, ' ').trim(), params };
}
};
}
const type = 'vip';
const age = 18;
const builder = sql`select id,name from user where type=${type} and age>=${age} and deleted=${undefined}`;
const out = builder.build();
console.log(out.sql); // select id,name from user where type=? and age>=?
console.log(out.params); // ['vip', 18]
JavaScript模板字符串SQL查询构建器修改时间:2026-08-04 23:45:38