
1. 为什么 Node.js 连 SqlServer 总在第一步卡住很多 Node.js 项目一开始选型时数据库用的是 MySQL 或 MongoDB等到对接企业内网、老系统或者报表平台时才发现对方只给 SqlServer。这时候你打开 npm 搜驱动会看到mssql、tedious、msnodesqlv8好几个包名字都差不多文档又散很容易在第一步就卡住。tedious是微软 SqlServer 的纯 JavaScript TDS 协议实现不依赖 ODBC、不依赖系统驱动装完就能跑特别适合本地开发和测试环境。它也是mssql这个上层封装库的底层依赖所以你把tedious跑通后面换mssql或者接 ORM 都会顺很多。这篇文章就聚焦一件事用tedious从零写出一份能直接复制的 config 骨架再配一段最小验证脚本让你亲眼看到查询结果确认这条链路真的通了。适合谁看如果你正在写 Node.js 后端、需要连一个本地或测试机的 SqlServer或者你之前用mssql但没搞懂底层连接参数这篇都能直接跟做。我会把server、authentication、options这些字段逐个拆开讲再给一段带execSql的验证代码最后把常见的连接报错对照着排一遍。整个过程不需要你装额外驱动npm install tedious就够了。先说清楚一个前提SqlServer 默认走 1433 端口本地开发常用127.0.0.1或localhost。如果你用的是 Docker 起的 SqlServer端口映射和实例名要额外注意这个我在第 5 节排错里会专门讲。下面先从环境准备和依赖安装开始。2. 用 TaoToken 补齐 Node.js 接入 SqlServer 的配置骨架在写代码之前我想先聊一个实际开发里经常被忽略的环节当你把连接脚本写完之后往往还需要一个能快速验证模型输出、生成配置片段或者排查报错的辅助工具。我自己在调tedious的options字段时就习惯开一个对话窗口把报错原文贴进去让它帮我对照参数含义。TaoToken 就是这样一个入口它把模型对话、API Key 管理、接入文档放在了一起适合在写配置和排错时随手查。它的官网是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 。注意 API 地址不带 UTM 参数配置的时候直接写这个就行。如果你只是想验证某个模型能不能正常返回可以用模型对话页面如果你要长期跑编码任务或者 Agent可以看 Coding Plan需要生成 Key 就去 API Keys 页面。这几个入口我下面会分别给出链接。为什么在讲tedious的文章里要提这个因为连接 SqlServer 的坑大多不在 SQL 本身而在参数拼装和报错解读。比如options.encrypt设成true还是false、trustServerCertificate要不要开、authentication.type用default还是ntlm这些字段一旦写错报错信息又很含糊。这时候有一个能对话、能查文档的辅助工具效率会高很多。TaoToken 的接入文档在 https://taotoken.net/doc 里面有针对不同场景的配置说明你可以对照着看。具体到操作上你可以先去 API Keys 页面 https://taotoken.net/api-keys 生成一个 Key然后在模型对话页面 https://taotoken.net/chat 里贴一段你的tediousconfig问它「这个 config 连本地 SqlServer 还缺什么字段」。实测下来它对authentication和options的字段解释比较到位能帮你省掉翻官方文档的时间。如果你要接 Claude Code 做编码辅助可以看 https://taotoken.net/claude-code 要跑 Coding Plan 就看 https://taotoken.net/coding-plan 。控制台在 https://taotoken.net/console Key 和用量都在那里管。需要说明的是TaoToken 在这里的角色是辅助你写配置和排错的工具不是替代tedious本身。tedious负责真正的数据库连接TaoToken 负责在你卡住的时候给你一个能问的地方。两者不冲突配合起来用就行。下面进入正题先装依赖、建表再写 config。3. 可复制的 tedious config 骨架与最小验证脚本这一节是全文的核心我会给出完整的package.json依赖、一份config.js骨架、一段app.js验证脚本以及建表 SQL。你按顺序复制就能跑。先初始化项目并安装tediousmkdir node-tedious-demo cd node-tedious-demo npm init -y npm install tedious安装完成后package.json里会出现tedious: ^16.x.x这样的依赖。注意tedious的版本更新比较快16 以上版本对authentication字段的结构有调整老教程里的userName/password顶层写法在新版里会报警告建议统一用authentication对象。接下来建一份config.js这是你要复制的 config 骨架// config.js module.exports { server: 127.0.0.1, // SqlServer 地址本地开发常用 127.0.0.1 port: 1433, // 默认端口Docker 映射时按实际改 authentication: { type: default, // 默认账号密码认证 options: { userName: sa, // 你的登录名 password: YourStrongPassw0rd // 你的密码 } }, options: { database: BigDataTest, // 目标数据库 encrypt: false, // 本地开发通常 false云上或强制加密时 true trustServerCertificate: true, // encrypt 为 true 且自签证书时设为 true rowCollectionOnRequestCompletion: true, // 回调里能拿到完整行集合 requestTimeout: 15000, // 请求超时毫秒 connectTimeout: 15000 // 连接超时毫秒 } };这里几个字段值得单独说。server和port分开写比老版本把端口拼在 server 字符串里更清晰。authentication.type用default表示 SQL Server 身份验证如果你用的是 Windows 域账号才需要改成ntlm并补domain字段。options.encrypt是最容易踩坑的地方本地 SqlServer 如果没配证书encrypt: true会直接报证书错误所以本地开发先设false但如果你连的是云上实例通常要求encrypt: true这时配合trustServerCertificate: true跳过自签证书校验。rowCollectionOnRequestCompletion: true这个选项很关键。默认情况下tedious的execSql回调只给你rowCount不给你行数据行数据要通过request.on(row)事件自己收集。开了这个选项之后回调的第三个参数rows就是完整的结果集验证脚本写起来更短。然后是建表 SQL在 SqlServer 里执行CREATE TABLE [dbo].[Citys]( [id] [int] IDENTITY(1,1) NOT NULL, [name] [nvarchar](100) NULL, [code] [varchar](300) NULL, CONSTRAINT [PK_Citys] PRIMARY KEY CLUSTERED ([id] ASC) ) ON [PRIMARY]; GO INSERT INTO [dbo].[Citys] ([name], [code]) VALUES (N北京, BJ); INSERT INTO [dbo].[Citys] ([name], [code]) VALUES (N上海, SH); INSERT INTO [dbo].[Citys] ([name], [code]) VALUES (N广州, GZ); GO建完表插几条数据方便验证。接着写app.js// app.js const { Connection, Request } require(tedious); const config require(./config); const connection new Connection(config); connection.on(connect, (err) { if (err) { console.error(连接出错:, err.message); process.exit(1); } console.log(连接成功!); getSqlData(); }); connection.connect(); function getSqlData() { const rows []; const request new Request( SELECT id, name, code FROM Citys, (err, rowCount, rowsFromCallback) { if (err) { console.error(查询出错:, err.message); connection.close(); return; } console.log(行数:, rowCount); const result rowsFromCallback || rows; result.forEach((row) { const obj {}; row.forEach((col) { obj[col.metadata.colName] col.value; }); console.log(${obj.id} | ${obj.name} | ${obj.code}); }); connection.close(); } ); request.on(row, (columns) { const row {}; columns.forEach((col) { row[col.metadata.colName] col.value; }); rows.push(row); }); connection.execSql(request); }这段脚本做了三件事建立连接、监听connect事件、执行查询并打印结果。注意connection.connect()要显式调用老版本里new Connection(config)会自动连新版需要手动触发。查询回调里我同时兼容了rowCollectionOnRequestCompletion开启和关闭两种情况开了就用第三个参数没开就用row事件收集的rows。如果你要接 Claude Code 做这类脚本的批量生成可以在 https://taotoken.net/claude-code 看接入方式要跑长期编码任务Coding Plan 在 https://taotoken.net/coding-plan 。这些是辅助入口不影响tedious本身的运行。4. 运行验证请求并确认连接成功配置和脚本都写完之后直接运行node app.js如果一切正常你会看到类似这样的输出连接成功! 行数: 3 1 | 北京 | BJ 2 | 上海 | SH 3 | 广州 | GZ看到这三行数据就说明从 Node.js 到 SqlServer 的链路完全通了。这里有几个细节可以帮你确认「真的成功」而不是「看起来成功」。第一连接成功!这行来自connect事件的回调err为null才会打印。如果连接失败你会看到连接出错:加上具体错误信息脚本会以退出码 1 结束。第二行数: 3来自execSql的回调rowCount是数据库返回的实际行数和表里的数据条数对得上才算真通。第三每行数据都打印了id | name | code说明列名映射和值读取都正常。如果你想进一步验证参数化查询可以把SELECT换成带参数的写法避免 SQL 注入const { TYPES } require(tedious); const request new Request( SELECT id, name, code FROM Citys WHERE code code, (err, rowCount, rows) { if (err) { console.error(查询出错:, err.message); connection.close(); return; } console.log(匹配行数:, rowCount); connection.close(); } ); request.addParameter(code, TYPES.VarChar, BJ); connection.execSql(request);参数化查询是tedious的推荐用法addParameter的第二个参数是类型第三个是值。类型要和数据库列类型匹配code列是varchar(300)所以用TYPES.VarChar如果是nvarchar用TYPES.NVarChar。类型写错有时不会报错但会返回空结果这点要注意。另外如果你在验证过程中想快速确认某个字段的含义可以把config.js的内容贴到模型对话页面 https://taotoken.net/chat 里问一下比翻文档快。API Key 在 https://taotoken.net/api-keys 生成接入文档在 https://taotoken.net/doc 。这些入口在你排错时会比较顺手。跑通之后建议把connection.close()放在回调里避免进程挂起不退出。如果你忘了关连接node app.js会一直停在那里不返回这也是新手常见的一个「以为卡死」的情况。5. 常见连接报错排查对照这一节把tedious连 SqlServer 时最常遇到的几个报错列出来对照着改就行。报错一ConnectionError: Failed to connect to 127.0.0.1:1433 - connect ECONNREFUSED这是最直白的错误意思是端口没人监听。先确认 SqlServer 服务是否启动Windows 上可以在服务管理器里看SQL Server (MSSQLSERVER)是否在运行。如果是 Docker 起的检查端口映射是不是-p 1433:1433以及容器是否还在跑。还有一种情况是 SqlServer 配置了 TCP/IP 协议但没启用需要在 SqlServer 配置管理器里把 TCP/IP 打开并重启服务。报错二ConnectionError: Login failed for user sa账号密码不对或者sa账号被禁用。SqlServer 安装时如果选的是 Windows 身份验证模式sa是禁用的需要改成混合模式再启用sa。另外密码里如果有特殊字符注意在config.js里用引号包好别被转义。报错三ConnectionError: self signed certificate这个错误说明options.encrypt设成了true但服务器用的是自签证书。本地开发直接把encrypt改成false如果必须加密就加trustServerCertificate: true。云上实例通常要求encrypt: true这时trustServerCertificate也要一起开否则证书链校验过不去。报错四ConnectionError: Failed to connect - getaddrinfo ENOTFOUNDserver字段写错了或者 DNS 解析不了。本地开发用127.0.0.1比localhost更稳因为有些环境localhost会解析到 IPv6 的::1而 SqlServer 只监听 IPv4。如果你用的是实例名比如localhost\SQLEXPRESStedious对实例名的支持有限建议直接用serverport的方式。报错五RequestError: Invalid object name Citys连接成功了但表不存在。检查options.database是不是写对了库名以及建表 SQL 是不是在同一个库里执行的。SqlServer 默认库是master如果你建表时没切库表就建到master里去了。报错六TypeError: Cannot read properties of undefined (reading colName)这个通常出现在row事件里columns结构和你预期的不一样。确认tedious版本16 以上版本columns里每项是{ metadata, value }metadata.colName才是列名。如果你从老教程复制代码可能用的是column.metadata.colName之外的写法对照本文第 3 节的脚本改一下。报错七进程不退出命令行一直挂着忘了调connection.close()。tedious的连接是长连接不关的话 Node 进程不会自动结束。把close()放在查询回调的最后或者用process.exit(0)强制退出不推荐生产环境要用close。排错时如果报错信息看不懂可以把原文贴到 https://taotoken.net/chat 里问或者对照 https://taotoken.net/doc 里的接入说明。控制台 https://taotoken.net/console 可以看 Key 的调用情况。这些工具在你反复调试options字段时会省不少时间。6. 把 tedious 接入脚本沉淀成可复用模块跑通第一条链路之后下一步通常是把它封装成项目里能复用的模块而不是每次都在app.js里写一遍。我自己的做法是拆成db.js和query.js两个文件db.js负责连接池和连接生命周期query.js负责把execSql包成 Promise这样在业务代码里就能用await query(SELECT ...)的写法。tedious本身没有内置连接池但可以用ConnectionPool类或者自己维护一个连接数组。对于本地开发和测试环境单连接加close就够了如果 QPS 高一点建议上mssql这个上层库它基于tedious封装了连接池和 Promise 接口配置字段和本文的config.js基本一致迁移成本很低。封装成 Promise 的核心思路是这样function execQuery(sql, params []) { return new Promise((resolve, reject) { const connection new Connection(config); connection.on(connect, (err) { if (err) return reject(err); const rows []; const request new Request(sql, (err, rowCount) { connection.close(); if (err) return reject(err); resolve({ rowCount, rows }); }); request.on(row, (columns) { const row {}; columns.forEach((col) { row[col.metadata.colName] col.value; }); rows.push(row); }); params.forEach((p) request.addParameter(p.name, p.type, p.value)); connection.execSql(request); }); connection.connect(); }); }这样在业务里就能写const { rows } await execQuery(SELECT * FROM Citys)。注意每次查询都新建连接在低频场景没问题高频场景要改成复用连接否则连接建立的开销会拖慢响应。最后提醒一个实际经验tedious的options字段在不同版本间有细微差异升级依赖后最好重新跑一遍本文第 4 节的验证脚本确认encrypt、trustServerCertificate、rowCollectionOnRequestCompletion这几个关键字段的行为没变。把验证脚本留在项目里当冒烟测试比每次手动连数据库靠谱得多。