简介:面向JavaScript与AJAX开发者的MySQL直连方案文档,围绕JSDBC组件说明如何在前端脚本中直接连接MySQL数据库,省去后台服务转发与繁琐JDBC调用,适合调试环境、轻量应用及需要快速验证数据访问逻辑的场景。压缩包共1个doc文件,大小31KB,以文档形式集中整理JSDBC的安装方式、OCX对象引入、connectMySQL连接参数、insert/execDML/select等SQL执行函数、查询结果数组拆分处理、getLastError错误捕获以及closeMySQL关闭连接等要点,并附有可参考的脚本示例。已有6927人学习或下载,便于读者按文档中的函数说明与示例快速理解JSDBC的调用流程,掌握前端直连数据库的基本用法、结果集解析与异常处理思路,也可作为旧项目维护或技术方案调研的参考资料。
1. 用一行 OBJECT 标签直接连 MySQL:JSDBC 究竟是什么
如果有人告诉你,页面里写几行 JavaScript 就能直接读写 MySQL,不用后端接口、不用部署运行环境,你大概率会先怀疑一句“这不就是把数据库密码摊在明面上吗”。这个怀疑没错,但先把这个组件看清楚:JavaScript DataBase Connector(JSDBC)是一套跑在 ActiveX 上的数据库连接控件,通过一个 OBJECT 标签把本地 OCX 暴露成 JS 里的全局对象mysql,目前覆盖 MySQL、SQLite、ACCESS。它省掉的是接口层和 JDBC 调用,代价是把数据库凭据和网络连通性搬到了浏览器进程里。
它适合的场景很具体:内网单机工具、老旧系统的调试面板、只在本机跑的数据核对页、给非技术同事用的查询小页面。不适合公网、多用户、需要审计的系统,这一点后面会反复回到。如果你的诉求是“临时把某个库的数据捞出来看看”,它够用;如果是要上线给人用,把它当调试设施比当生产组件更稳妥。
2. 把 OCX 跑起来:注册、嵌入与最小连通验证
这一段不走通,后面所有函数都是纸面文章。JSDBC 的加载链路比普通 npm 包长得多:文件落到磁盘 → 注册表写入 CLSID → 浏览器进程按 classid 创建控件实例 → 页面脚本拿到全局对象。任何一环断掉,表现都是同一句“对象不支持此属性或方法”,光看现象没法定位,所以要一步一步验。
2.1 先核对三件事,否则后面全是白忙
动手前把环境对齐,能省掉大半排查时间。这三条里任意一条不满足,控件都不会出现在 JS 里。
| 核对项 | 正确状态 | 不满足时的表现 |
|---|---|---|
| 浏览器内核 | 支持 ActiveX 的旧版内核 | mysql为 undefined,页面无报错 |
| 浏览器进程位数 | 与 OCX 位数一致(通常是 32 位) | 注册成功但创建实例失败,报 0x80004005 |
| 页面所在安全区域 | 本地 Intranet 或受信任站点,允许初始化未标记安全的控件 | 顶部出现拦截条,控件静默不加载 |
第二条最容易被忽略。64 位系统上跑 64 位浏览器进程,去加载一个 32 位的 OCX,注册表里明明有 CLSID,就是创建不出来。判断办法很土但有效:任务管理器里看浏览器进程有没有带“(32 位)”后缀。我没有在 64 位进程里成功加载过 32 位 OCX,这条弯路走过一次就够。
2.2 注册与反注册:regsvr32 只有两条半命令
注册这一步没什么玄学,关键是选对 regsvr32。系统里有两个,System32下面那个是 64 位的,SysWOW64下面那个才是 32 位的,名字反直觉但确实是这么设计的。
:: 必须以管理员身份打开命令行;/s 表示静默,成功不弹框,失败才弹 C:\Windows\SysWOW64\regsvr32.exe /s "D:\jsdbc\JSDBC_MySQL.ocx" :: 确认 CLSID 真的写进注册表了,这一条比任何成功弹框都可靠 reg query "HKCR\CLSID\{9C579403-6745-4695-B14C-96212D319F18}" /s :: 换版本前先反注册,否则旧 CLSID 还指向旧文件,会出现"注册成功但行为没变" C:\Windows\SysWOW64\regsvr32.exe /u /s "D:\jsdbc\JSDBC_MySQL.ocx"逻辑说明:第一条命令把 OCX 里登记的 COM 信息写进注册表,/s只是关掉提示框,不代表一定成功;第二条才是真正的验证手段,能查到InprocServer32子键和它指向的文件路径,说明注册表这一环通了;第三条用于升级替换,反注册不干净就重新注册,很容易留下“看着是新文件、跑的是旧逻辑”的鬼故事。
参数上值得注意的只有路径:带空格一定要加引号,反斜杠用双写或者直接用正斜杠都行,但别混着来。如果第一条命令弹出 0x80070005,那就是权限没给够,用管理员命令行重来一次。
2.3 页面嵌入:classid 是死的,id 决定变量名
原始脚本用document.writeln拼 OBJECT 标签,这个写法在老页面里很常见,因为它保证标签在后续脚本执行前就已经写进文档流。要改的只有两处:id决定后面 JS 里用什么名字访问控件,codebase指向打包好的 cab。
// 控件必须在任何调用脚本之前写入文档流,否则后面拿到的是 undefined document.writeln("<OBJECT id='mysql' classid='clsid:9C579403-6745-4695-B14C-96212D319F18'"); document.writeln(" codebase='JSDBC_MySQL.ocx#Version=1,0,0,000'"); document.writeln(" width='0' height='0'></OBJECT>"); // 拿到对象之后再谈连接,判空这一步不能省 if (typeof mysql === "undefined" || mysql === null) { throw new Error("OCX 未加载:依次检查内核、位数、可信站点、注册状态"); }逻辑说明:classid是控件在注册表里的身份,写死不能改;id='mysql'是 ActiveX 与普通 DOM 元素最大的差别——它会把控件实例直接挂成同名全局变量,所以源码里到处出现的mysql.xxx()并不是某个库,就是这个控件本身。
codebase里的#Version=1,0,0,000是版本声明,配合 cab 包做自动下载安装。这里有个现实问题:cab 需要按 INF 打包,并且带数字签名,否则浏览器会拦下来。内网批量部署时,我一般不用 codebase,而是让运维统一用 regsvr32 预装,codebase留着当兜底,省得每次弹安装提示。
2.4 连接、查询、关闭:一条最小闭环
跑通这一段,说明控件、驱动、网络、账号四件事都对了。剩下的都是在这条链路上加功能。
// 1) 连接:六个参数顺序固定——IP、端口、库名、用户、口令、连接字符集 var flag = mysql.connecte("127.0.0.1", "3306", "testdb", "root", "123210", "GBK"); if (flag !== 1) { // 失败必须立刻取错误,控件内部的错误缓冲会被下一次调用覆盖 throw new Error("连接失败:" + mysql.getLastError()); } // 2) 查询:第二个参数是"字段个数",不是返回行数,写错会直接错位 var raw = mysql.selectData("select id,name from t_user where id < 100", 2); if (!raw) { throw new Error("查询失败:" + mysql.getLastError()); } // 3) 解析:分隔符是 0x06 和 0x05 两个控制字符,不是减号和竖线 var ROW_SEP = String.fromCharCode(6); var FIELD_SEP = String.fromCharCode(5); var body = raw.charAt(raw.length - 1) === ROW_SEP ? raw.slice(0, -1) : raw; var rows = body.split(ROW_SEP).map(function (line) { return line.split(FIELD_SEP); }); // 4) 关闭:放在 finally 或 window.onunload 里,别指望浏览器替你回收 mysql.close();参数逐个说清:第 5 个参数是口令,源码示例里用的是明文,实际部署别这么干;第 6 个参数是连接字符集,不是库的默认字符集,两处不一致就是中文乱码的源头,后面单独讲。selectData的第二个参数cnum表示 SQL 里出现的字段数量,控件靠它做定界,写小了会截断字段,写大了会多出空列。
至于raw的判空,注意它失败时可能返回空串也可能是 null,两种都当失败处理最省事。
3. 九个封装函数逐个拆:返回值、分隔符与三个反直觉点
源码里那层函数封装写得很直白,几乎是把控件方法一比一搬过来,好处是看得懂,坏处是把控件的几个怪脾气也一起搬过来了。这一章按调用顺序拆,重点放在返回值约定和结果集解析上——这两处是后面出问题最密集的地方。
3.1 connecte 与 close:没有连接池,一个实例一条连接
先说清一个前提:这个控件不是连接池,一个页面实例只维持一条连接。所有 SQL 都在这条连接上串行执行,所以你在页面上开两个“并发”请求,实际也是排队。局域网里 connect 一次的开销是几十毫秒量级,看起来不痛,但如果每个操作前都 connect、操作完就 close,一个列表页刷新十次就是十次握手,再加上服务端认证,卡顿感很明显。
我一般的做法是把连接的生命周期绑到页面生命周期:打开时连一次,页面切换或关闭时close。真正需要注意的是不及时关闭的后果——浏览器进程异常退出时,服务端那条连接不会立刻消失,得等wait_timeout到点才回收。调试阶段反复刷新页面,很容易把连接数顶到上限,然后所有新连接都报错。
提示:
close()之后立刻重新connecte()是安全的,但不要在close和下一次connecte之间插入任何查询,控件不会给你友好的报错。
3.2 增删改的返回值:有一个函数是坏的
把源码里的封装横向对比一遍,会发现返回值约定不一致。这不是风格问题,是能不能判断成败的问题。
| 函数 | 底层方法 | 成功 | 失败 | 备注 |
|---|---|---|---|---|
| connectMySQL | connecte | 1 | 0 | 失败取getLastError |
| closeMySQL | close | 1 | 0 | 同上 |
| insertMySQL | insertData | 1 | 0 | 单条插入 |
| deleteMySQL | deleteData | 1 | 0 | 注意 WHERE 条件 |
| updateMySQL | updateData | 1 | 0 | 同上 |
| execDMLMySQL | execDML | 0 | 0 | 两个分支都返回 0 |
| execTranscationMySQL | execTranscation | 1 | 0 | 事务入口 |
| selectMySQL | selectData | 二维数组 | null | 需要二次解析 |
| getLastErrorMySQL | getLastError | 字符串 | 字符串 | 读的是 JS 侧缓存 |
execDMLMySQL那段是这个组件里最典型的一个坑:成功return 0,失败也return 0,调用方拿到的永远是 0,等于没有返回值。这种代码在调试时特别费时间,因为你以为是 SQL 写错了,其实只是判断条件失效。
// 原封装成功和失败都 return 0,判断逻辑完全失效,必须改成统一约定 // 约定:成功返回 1 且清空错误缓存,失败返回 0 并把控件错误存下来 function execDMLMySQL(sql) { if (mysql.execDML(sql) === 1) { lasterr = ""; // 清缓存,避免上一次的错误被当成这一次的 return 1; } lasterr = mysql.getLastError(); return 0; }这里还顺手处理了另一个问题:lasterr是全局变量,不主动清空的话,上一次操作的错误文本会一直挂着,页面报错指向一条早就成功的 SQL。
3.3 selectData 的分隔符:0x05 和 0x06 才是本体
这是整个组件最反直觉的设计。控件不返回记录集对象,而是把整个结果集拼成一个字符串,行之间用0x06(ACK 控制字符)分隔,字段之间用0x05(ENQ 控制字符)分隔,前端再自己split回来。
问题在于,源码注释里明确写了“这并不是普通的 '-'”“这并不是普通的 '|'”,但这两个字符在编辑器里根本看不见,复制粘贴、经过某些输入法、被 git 的换行转换处理过,都可能被替换成真正的减号和竖线。替换之后代码照样能跑,只是split出来永远是一整行,字段全是错的,而且不报错。
// 别从源码里复制那两个不可见字符,用码点现算,永远不会被编辑器改掉 var ROW_SEP = String.fromCharCode(0x06); var FIELD_SEP = String.fromCharCode(0x05); function parseResultSet(raw) { // 失败时可能是 null 也可能是空串,先判类型再 split if (typeof raw !== "string" || raw.length === 0) return []; // 末尾通常多挂一个行分隔符,不剥掉会解析出一行空数组 var body = raw.charAt(raw.length - 1) === ROW_SEP ? raw.slice(0, -1) : raw; return body.split(ROW_SEP).map(function (line) { return line.split(FIELD_SEP); }); }还有一个边界要提前想到:如果字段内容本身含有0x05或0x06(比如二进制字段、某些富文本),这套分隔协议会直接错位,而且无法在前端还原。我的处理办法是查询时把这类字段排除掉,或者用hex()转成十六进制字符串再取。这不是严谨方案,但在这种协议下没有更好的选择。
3.4 存储过程与事务:先做一次单点验证
callProduceMySQL(proname, inparas, outparas, cnum)四个参数,前三个都是字符串,cnum是输出字段数。源码没写清楚多个入参怎么拼,常见做法是用逗号分隔的字符串传进去,但这依赖控件内部的解析规则,不同版本可能不一样。
我的建议很直接:只用单入参、单出参的存储过程。需要多参数时,在服务端再包一层单参数的壳。理由不是懒,是这套协议没有类型信息,字符串里的逗号一旦出现在数据里就会错位,而错位之后控件不会报错,只会返回一堆对不上的值。
事务这边同理。execTranscation(sql)只接受一个 SQL 字符串,想一次提交多条语句就得自己拼分号。但 MySQL 驱动默认不允许一次执行多条语句,所以要先把服务端的多语句开关打开,再验证控件是不是真的把整串发过去了。验证方法很土:拼两条 insert,其中第二条故意写错表名,看第一条有没有留下数据。留下了,说明事务没生效,只有第一条被提交;两条都没留下,说明事务是真事务。
4. 避坑与排查:五条踩过的坑,每条都能让人卡半天
这一章按“现象 → 原因 → 解决”写,都是实际遇到过的。顺序按出现频率排,第一条出现的次数比后面四条加起来还多。
坑一:mysql是 undefined,页面一点报错都没有
现象:脚本里任何mysql.xxx()都抛“对象不支持此属性或方法”,但页面本身正常渲染,控制台安静得可疑。
原因:三种可能——浏览器不是 ActiveX 内核、浏览器进程位数不匹配、页面不在受信任区域被静默拦截。三者表现完全一样,只能逐个排除。
解决:先看进程位数(任务管理器里的 32 位后缀),再看站点是否在受信任站点列表里且允许初始化未标记安全的控件,最后用reg query确认 CLSID 在注册表里。三步都过了还不行,就把 OCX 拷到系统目录再注册一次,路径权限问题也遇到过。
坑二:注册时弹 0x80070005,换管理员也一样
现象:regsvr32报访问被拒绝,用管理员命令行重新执行还是同样的错。
原因:不是权限,是 OCX 依赖的运行库缺失。控件加载时要先加载它依赖的 CRT 或 MFC 运行库,缺了就直接失败,而错误码伪装成权限问题。
解决:用依赖查看工具查一遍 OCX 的导入表,缺哪个运行库就装哪个,装完重新注册。这一步看起来原始,但比反复重装控件有效得多。
坑三:查询有结果,但每行只有一个字段
现象:selectData返回的字符串非空,split之后每行的length都是 1,字段全糊在一起。
原因:源码里那两个分隔符被编辑器替换成真正的减号和竖线了。代码能跑,逻辑全错,而且不报错。
解决:全部改成String.fromCharCode(6)和String.fromCharCode(5),并且在解析函数里加一条断言——行内字段数不等于cnum就抛出,让错误立刻暴露,别让它悄悄往下传。
坑四:中文全是问号,英文和数字正常
现象:查出来的中文变成?或者乱码,数字和英文完全正常。
原因:connecte的第 6 个参数和库表实际字符集不一致。老控件对字符集的支持通常只到 GBK 和 utf8 两档,库里建的是 utf8mb4 时,传 GBK 就会出问题。
解决:库、表、连接三处保持一致。库是 utf8 的,连接就传 utf8;只在 GBK 环境里跑的老系统,就统一 GBK。改完之后一定要做一次写入再读出的往返测试,只读不写测不出问题。
坑五:页面刷新十几次之后,数据库连接数顶满
现象:调试一段时间后,所有连接请求都超时,服务端连接数飙到上限,重启浏览器才恢复。
原因:每次操作都新建连接,close又没调。浏览器进程异常退出时,服务端的连接要等wait_timeout才释放。
解决:连接跟着页面生命周期走,window.onunload里兜底close;同时在服务端把wait_timeout调小一点,当作最后一道保险。这个坑不致命但很烦,尤其是演示前一刻。
5. 把裸调用封成一层:字符集、转义与结果映射
源码那层封装能跑,但不能直接拿去写业务。原因有三个:没有参数绑定、没有统一错误出口、结果集是二维数组。这三件事补完,代码量不大,可维护性差别很明显。
5.1 没有预编译参数,转义只能自己做
控件的方法只接受 SQL 字符串,没有?占位符,也就没有参数绑定。这意味着拼接责任全在前端,任何一处漏转义都是注入入口。
// 控件没有预编译参数,值只能拼进 SQL,转义必须自己兜住 function q(v) { if (v === null || v === undefined) return "NULL"; if (typeof v === "number") return String(v); // 数值先经过 Number 校验再进来 return "'" + String(v) .replace(/\\/g, "\\\\") // 反斜杠要先转,否则后面的转义会被它吃掉 .replace(/'/g, "\\'") // 单引号 .replace(/\0/g, "\\0") // NUL .replace(/\n/g, "\\n") .replace(/\r/g, "\\r") + "'"; }顺序不能乱:反斜杠必须第一个替换,否则后面加进去的\'会被再转一次。这只是权宜之计,能挡住常见的字符串注入,但挡不住业务逻辑层面的问题——比如order by后面拼字段名、limit后面拼数字,这些位置不能用引号包裹,只能走白名单校验。
5.2 结果集映射成对象数组
二维数组在页面里用起来很别扭,取值全靠下标,字段顺序一变全崩。加一层映射,把列名显式声明出来,出错时能第一时间发现。
// 列名显式声明,映射时顺便校验列数,比下标取值安全得多 function toObjects(dataSet, columns) { var out = []; for (var i = 0; i < dataSet.length; i++) { var row = dataSet[i]; // 列数对不上只有两种可能:cnum 传错,或者数据里混进了分隔符 if (row.length !== columns.length) { lasterr = "列数不匹配,第 " + i + " 行:" + row.length + " != " + columns.length; continue; } var obj = {}; for (var j = 0; j < columns.length; j++) obj[columns[j]] = row[j]; out.push(obj); } return out; }参数说明:dataSet是parseResultSet的输出,columns是按 SQL 里字段顺序写好的列名数组。列数校验那一段看起来啰嗦,但它是这套协议下唯一能提前发现错位的检查点。跳过坏行而不是整体抛错,是因为这类错位往往只影响个别行,整体失败反而丢掉可用数据。
5.3 字符集:三处一致比什么都重要
字符集这事没有通用答案,只有一致性要求。下面这张表是我在不同场景下的选法,前提都是改完做一次写入再读出的往返验证。
| 场景 | 库/表 | 连接参数 | 说明 |
|---|---|---|---|
| 老系统改造,原始数据是 GBK | GBK | GBK | 不要动库,动了历史数据全乱 |
| 新建库,纯内网工具 | utf8 | utf8 | 控件对 utf8mb4 支持不稳,别用四字节字符 |
| 混合环境,只读查询 | 按库实际情况 | 与库一致 | 只读场景下改连接参数成本最低 |
| 有 emoji 或生僻字需求 | utf8mb4 | 无解 | 这套协议撑不住,换方案 |
最后一行不是危言耸听。控件返回的是分隔字符串,四字节字符在拼接过程中一旦被按字节截断,前端拿到的就是半个字符,还原不回来。
5.4 什么时候必须放弃它
这一点值得说清楚,因为很多人会在一开始就选错方向。判断标准其实只有三条:是否面向公网、是否多用户并发、是否需要操作审计。命中任意一条,这套方案就不合适——凭据写在页面脚本里,按下 F12 就能看到数据库账号密码,改都没法改。
| 维度 | JSDBC 直连 | 服务端中间层 |
|---|---|---|
| 部署成本 | 注册一个 OCX | 需要运行时环境和进程 |
| 凭据暴露 | 页面可见 | 服务端可见 |
| 并发能力 | 单连接串行 | 连接池 |
| 跨平台 | 仅特定平台 | 基本都行 |
| 适用场景 | 本机、内网、单人调试 | 对外服务 |
把这张表当成决策依据,比事后改架构便宜得多。
6. 三个开关,把这套东西关进笼子里
真正决定这套组件安不安全的,不是控件本身,是你在它外面加了几道限制。我在能用的场景里固定加三个开关,缺一个就不用。
第一个是数据库账号。绝不使用高权限账号,专门建一个只授予目标库、目标表所需权限的账号;纯查询页面就用只读账号,连接串里的口令也换成这个低权账号的。这一步的收益是决定性的——即使页面被人翻出凭据,能造成的破坏也被限制在表级。
第二个是加载开关。OBJECT 标签不写死在页面里,用一个构建期变量或 URL 白名单控制注入,线上构建直接不输出这段。这样同一份代码既能做调试页,也能进正式环境而不带控件。
第三个是启动自检。页面加载完先跑一条探针 SQL,把结果打到页面角落的固定容器里,出问题时不用打开控制台猜。
// 页面加载完先自检,把结果打到 #dbg,比翻控制台快得多 function selfCheck() { var box = document.getElementById("dbg"); if (typeof mysql === "undefined" || mysql === null) { box.innerHTML = "OCX 未加载"; return; } if (mysql.connecte("127.0.0.1", "3306", "testdb", "reader", "******", "GBK") !== 1) { box.innerHTML = "连接失败:" + mysql.getLastError(); return; } var rs = mysql.selectData("select 1", 1); box.innerHTML = rs ? "连通,探针返回:" + rs.charCodeAt(0) : "查询失败:" + mysql.getLastError(); mysql.close(); // 自检完立刻关,别让探针连接挂着 }探针用select 1而不是查业务表,是为了把“网络通不通”和“表存不存在”两件事分开。charCodeAt(0)取的是返回值第一个字符的码点,正常应该是数字 1 的码点 49,不是的话说明返回值格式和预期不一致,得先看解析逻辑。
注意:自检函数里那次
connecte会把页面已有的连接顶掉,如果页面自己维持长连接,自检要放在连接之前执行。
说个教训。早期我在一个内部核对页上直接用了示例里的连接参数,口令明文、账号高权、close写在正常流程里但没写进异常分支。测试阶段没人发现问题,等到数据量上来,一次网络抖动让连接卡住,页面刷新后旧连接一直挂着,服务端的连接数被这个页面吃掉了大半。排查了两个小时才定位到是没在onunload里兜底关闭。从那以后我每次接这类控件,都强制走一遍“断网 → 刷新 → 恢复网络 → 再看连接数”的验证,先确认异常路径能自愈,再谈功能。希望帮到你。
本文还有配套的精品资源,点击获取