简介:JS直接访问MySQL数据库的完整方案,基于JSDBC(JavaScript DataBase Connector)组件实现浏览器端对MySQL的直连访问。该方案让前端JavaScript绕过后台服务器,直接通过OCX控件与MySQL通信,免去部署Java运行环境和编写大量JDBC调用的繁琐工作,适合前端开发者、AJAX调试场景以及需要快速搭建数据交互原型的工程师使用,在日常开发与调试中非常实用。压缩包内为1个doc格式的文档,体积仅31KB,内容覆盖JSDBC组件安装方法、connectMySQL连接MySQL服务器所需的IP地址、端口号、数据库名称、用户名、密码、字符集等参数配置;同时给出了insertMySQL、execDMLMySQL、updateMySQL、deleteMySQL等增删改查操作函数,以及selectMySQL查询结果解析为数据集数组的处理方式。文档还包含getLastError错误捕获、closeMySQL连接释放的代码示例,并提供了通过OBJECT标签引入JSDBC_MySQL.ocx的写法,读者可参考完整封装的JavaScript函数直接集成到自己的项目中,便于快速理解调用逻辑。目前已有6926人次学习下载,是一份值得收藏的JS操作MySQL数据库速查手册。
1. 别被"JS直接访问数据Mysql"带偏:直连只能在 Node.js 里做
「JS 直接访问数据 Mysql」这个说法,十个人里有八个问的是浏览器里的 JavaScript 能不能绕过后端直接操作数据库。答案很明确:不能。浏览器没有资格拿 3306 端口去连 MySQL,凭据写在页面里等于公开密码。真正可行的路径是让 Node.js 作为 JS 运行环境,在服务端用 mysql2 驱动直连 MySQL,再把查询结果包成 JSON 接口给页面用。这篇文章按这条路径走:从环境搭建、最小连接、增删改查,到连接池和排错,最后落成一个可直接跑起来的 Express 数据接口。适合刚转 Node.js 后端的同学,也适合需要把老 MySQL 数据快速开放给前端的场景。
2. 环境三件套:数据库、Node 驱动、第一个连接怎么通
开始写代码之前,先把 MySQL 本体和 Node 驱动准备好。很多同学一上来就查 mysql 安装教程,其实难点不在安装向导本身,而在安装完之后连接参数怎么配才能对上。
2.1 本地验证用 Windows 安装 MySQL 8,线上再按 Linux 那套来
我本机做验证时,习惯用 Windows 安装 MySQL 8 的官方 MSI 包,网上的 mysql 安装配置教程里讲的安装向导步骤基本适用:一路 Next,选 Server only,设置好 root 密码,服务名默认 MySQL80。装完以后先在命令行敲一遍mysql -uroot -p,能进去再谈后续。这里容易踩的第一个坑是忘记了安装时设的 root 密码,导致后面在 Node 里怎么配都进不去。真忘了也别慌,用 MySQL 提供的--initialize-insecure初始化一个空密码实例,或者直接重装,别去手动改数据目录下的 root 权限表,那是把自己绕进权限黑洞的常见操作。
线上 Linux 环境是另一套玩法。CentOS 系推荐用 rpm 安装 mysql,依赖通常需要 libaio、numactl 这些包,装完用mysqld --initialize初始化数据目录,再交给 systemd 启动。如果手上是一台已有数据库的服务器,别乱动 root 密码,否则应用层所有连接串都会跟着失效。我一般会在初始化阶段单独建一个业务账号,只授对应库的权限,比如CREATE USER 'app'@'%' IDENTIFIED BY '密码',再GRANT SELECT, INSERT, UPDATE, DELETE ON test.* TO 'app'@'%'。这样就算连接串泄露,损失也能控制在一个库的几张表内。
2.2 驱动选 mysql2,理由在认证和 Promise 这两点上
Node 里连 MySQL 的驱动主要有 mysql 和 mysql2。老牌的 mysql 包实现得早,对 MySQL 5.7 很友好,但放进 MySQL 8 环境经常报认证协议错误,而且它不支持 Promise,要手动包一层回调。mysql2 在生态里沉淀了很久,支持 MySQL 8 默认的 caching_sha2_password 认证,直接返回 Promise,和 async/await 配合起来可读性高很多。所以我的默认选择是 mysql2,这也是把标题里「JS 直接访问数据 Mysql」落到工程里最省心的驱动。
安装两步,先初始化 package.json,再装依赖:
npm init -y npm install mysql2安装完成后会在 node_modules/mysql2 下看到 lib、promise.js 等文件。注意mysql2/promise那个入口才是带 Promise API 的,直接require('mysql2')拿到的是回调版,写起来会啰嗦很多。后面所有代码都从mysql2/promise引入。
2.3 写一个最小连接脚本,验证网络层和认证层
驱动装好,先别急着写业务函数,跑通一个最小连接脚本,确认数据库、用户名、密码、端口四个要素都没错:
const mysql = require('mysql2/promise'); async function main() { const conn = await mysql.createConnection({ host: '127.0.0.1', port: 3306, user: 'root', password: '你的密码', database: 'test' }); const [rows] = await conn.query('SELECT NOW() AS now'); console.log(rows); await conn.end(); } main().catch((err) => { console.error('连接失败:', err); process.exit(1); });这段脚本干的事是:建立一个 TCP 连接,完成 MySQL 握手和认证,执行一条SELECT NOW(),然后把结果打到控制台。query在 mysql2/promise 里返回一个数组,第一个元素是行数据,第二个是字段元数据;这里用rows接住行数据就够了。参数配置上,host建议优先写127.0.0.1而不是localhost。因为 Linux 下localhost会触发 unix socket 连接,如果 MySQL 的 socket 路径不在默认位置,就会抛出Error 2002。port保持 3306;user和password必须对上 MySQL 里真实账号;database指定的库如果不存在,连接阶段不会立刻报错,执行第一条查询时才报unknown database。
脚本跑失败时,先用 MySQL 自带客户端试一次mysql -h127.0.0.1 -P3306 -uroot -p,能连说明数据库侧没问题,问题在驱动或连接参数;连不上就先去检查 MySQL 服务状态和端口监听。把这条排查顺序固定下来,后面所有连接类报错都能分成两类:数据库没起来,和参数没配对。跑通之后,再把这个脚本扩成业务查询就顺理成章了。
2.4 MySQL 侧快速检查:端口、权限、字符集三个变量
连接脚本报错时,我习惯先在 MySQL 里执行一条组合检查,把三个最常出问题的变量一次看清:
mysql -uroot -p -e "SHOW VARIABLES LIKE 'port'; SHOW VARIABLES LIKE 'character_set_server'; SHOW VARIABLES LIKE 'max_connections';"port确认 MySQL 监听在哪个端口,character_set_server确认服务端默认字符集,max_connections决定连接池上限能设多少。这几项检查完,再回头看 Node 连接配置,基本能定位八成问题。比如服务端字符集是 latin1,连接参数里再怎么写 utf8mb4,某些表的默认值还是会乱,需要连表结构一起改。
3. 把增删改查封装成能被 JS 调用的函数:参数化查询、模糊搜索和排序
连接通了以后,下一步就是把「直接访问数据」变成一套可以被前端业务调用的 JS 函数。这里的关键不是 SQL 写得多花哨,而是参数怎么传、返回值怎么用、排序和模糊搜索怎么不翻车。
3.1 为什么我坚持用 execute 而不是拼字符串
很多从 phpMyAdmin 或 Navicat 过来的同学,习惯把 SQL 拼成一大段字符串再发给 MySQL,也就是query。mysql2 的query也支持?占位符并做转义,但execute走的是服务端预处理语句,SQL 结构提前编译好,每次只换参数值。对同样一条 SQL 多次执行时,execute的规划成本更低,也更不容易被参数带偏出歧义。所以我给项目定的惯例是:凡是带用户输入的 SQL,一律用execute。
下面这段代码是条件查询的基础写法:
async function findByCategory(conn, categoryId) { const [rows] = await conn.execute( 'SELECT id, name, price FROM products WHERE category_id = ? AND status = 1', [categoryId] ); return rows; }?是参数占位符,[categoryId]里的值会被当作纯数据传给 MySQL,而不是拼进 SQL 文本。这能挡住最常见的注入方式,比如入参是1 OR 1=1时,它只会被当成一个普通值去匹配category_id,不会改变查询结构。参数化之后,查询结果是一个对象数组,每个对象对应一行,字段名就是列名,可以直接res.json(rows)给前端用。
这里要解释一下(conn, categoryId)里这个conn从哪来。你可以从mysql.createConnection得到它,也可以来自下一章的连接池。把它当作参数传入,函数本身就不关心连接来源,将来做单元测试时也能传一个 mock 连接。
3.2 模糊搜索里的 JS 字符串判断和忽略大小写
业务里最常见的搜索是「按关键字查商品名」。写这段逻辑时,可以先在 JS 侧做一层参数清洗:判断字符串是否包含%或_,这两个字符在 LIKE 里是通配符,用户输入它们会把搜索范围整个放大。用String.prototype.includes就能干这件事:
function normalizeKeyword(keyword) { const kw = String(keyword || '').trim(); if (kw.includes('%') || kw.includes('_')) { throw new Error('关键字不能包含 % 或 _'); } return kw; }接下来是排序参数。MySQL 的ORDER BY方向不能直接绑定在预处理语句的参数里,因为它是 SQL 语法的一部分,不是数据。新手最容易在这里踩坑,把ASC/DESC直接放到?里,执行时 MySQL 会报语法错误。我的办法是在 JS 侧做白名单校验:
function validOrder(dir) { const allowed = new Set(['asc', 'desc']); const normalized = String(dir || 'asc').toLowerCase(); return allowed.has(normalized) ? normalized : 'asc'; }toLowerCase()在这里就是在做 JS 忽略大小写,把前端传来的ASC、Asc、asc统一成小写,再和白名单比对。数据库端如果希望查询时也不需要关心大小写,可以在建表时用utf8mb4_general_ci排序规则,MySQL 会在字符串比较时自动忽略大小写;如果表已经建好,也可以对字段做LOWER(name) = LOWER(?),但那样会让字段索引失效,数据量大了以后性能下降明显。
组合起来,一个带模糊搜索的查询函数长这样:
async function searchProducts(conn, keyword, order) { const kw = normalizeKeyword(keyword); const dir = validOrder(order); const [rows] = await conn.execute( `SELECT id, name, price FROM products WHERE name LIKE ? ORDER BY price ${dir}`, [`%${kw}%`] ); return rows; }这里把dir经白名单校验后拼进 SQL,是安全的;kw作为参数传入 LIKE,也是安全的。两个动作分开处理,逻辑就清晰了。注意%${kw}%是放在参数里而不是 SQL 文本里,占位符只对应kw本身,MySQL 拿到的是完整的%xxx%字符串,查询行为不会跑偏。
3.3 INSERT、UPDATE、DELETE 与事务的写法
查询之外,写入和删除同样走参数化。下面是三个最常用的写入型操作,返回值的含义值得记清楚:
async function addProduct(conn, { name, price, categoryId }) { const [result] = await conn.execute( 'INSERT INTO products (name, price, category_id) VALUES (?, ?, ?)', [name, price, categoryId] ); return result.insertId; } async function updatePrice(conn, id, price) { const [result] = await conn.execute( 'UPDATE products SET price = ? WHERE id = ?', [price, id] ); return result.affectedRows; } async function deleteProduct(conn, id) { const [result] = await conn.execute( 'DELETE FROM products WHERE id = ?', [id] ); return result.affectedRows; }INSERT的result.insertId是新记录的自增主键,前端可能需要用它做跳转或关联;UPDATE和DELETE的result.affectedRows表示影响的行数,为 0 时说明条件没命中。这里有个容易误解的地方:UPDATE更新前后值一样时,某些场景下 affectedRows 也可能是 0,所以别把它当作「更新成功」的唯一依据,必要时用SELECT验证一下。
如果一个操作里要更新多张表,就得用事务。mysql2/promise 的连接对象上有beginTransaction、commit、rollback三个方法:
async function transfer(conn, fromId, toId, amount) { await conn.beginTransaction(); try { await conn.execute( 'UPDATE accounts SET balance = balance - ? WHERE id = ?', [amount, fromId] ); await conn.execute( 'UPDATE accounts SET balance = balance + ? WHERE id = ?', [amount, toId] ); await conn.commit(); } catch (err) { await conn.rollback(); throw err; } }beginTransaction之后,两条 UPDATE 要么都成功,要么都回滚,不会出现转出成功但转入失败的情况。rollback放在 catch 里,是为了避免异常发生后把连接留给一个半完成的事务状态。这里我把连接对象作为参数传进函数,是为了配合下一章的连接池来复用,而不是每次调用都新建连接。等到接入连接池之后,事务函数的连接来自pool.getConnection(),用完release(),结构还是一样的。
4. 连 MySQL 的必选项:连接池参数怎么设、事务怎么不翻车
只要你的 JS 服务要被 HTTP 请求访问,就一定绕不开连接池。直接在每次接口调用里createConnection再end,数据量小的时候没感觉,并发一上来就会看到大量连接报错。
4.1 为什么每个请求都新建连接会把自己打趴
MySQL 连接不是一次普通函数调用。建立一条连接要经历 TCP 三次握手、MySQL 握手认证、可能还有 SSL 握手,完整走完往往需要几十毫秒。如果每个请求都走一遍完整流程,数据库有限的连接数很快被打满,后面的请求要么排队要么直接拒绝。连接池的做法是把连接建立好之后放在池子里,请求来了借一条,用完了还回去,下次复用。
我刚从回调写法切到 Promise 时犯过一个错误:在每个查询函数里createConnection,函数结束再end()。接口压测到 50 并发就出现ECONNREFUSED,后来换成连接池,同样压测脚本跑满 200 并发也没再报错。这个对比很直观,也是连接池存在的意义。连接池另一个好处是能把连接建立开销平摊到整个应用生命周期,而不是让每个请求都承担一次冷启动。
4.2 连接池参数清单与推荐值
mysql2 的createPool参数比createConnection多出一批和连接生命周期相关的配置。我把常用的列成一张表,照这个清单调基本够用:
| 参数 | 默认值 | 作用 |
|---|---|---|
| waitForConnections | true | true 时连接不够用就让请求排队,false 时直接抛错 |
| connectionLimit | 10 | 连接池最多维持的连接数 |
| maxIdle | 10 | 空闲状态最多保留多少条连接 |
| idleTimeout | 60000 | 空闲连接超过该毫秒数会被释放 |
| queueLimit | 0 | 排队请求数的上限,0 表示不限制 |
| enableKeepAlive | true | 定期发心跳包,避免 MySQL 把空闲连接断掉 |
connectionLimit不是越大越好。它要明显小于 MySQL 的max_connections,否则连接池把数据库连接耗尽,别的客户端也会被牵连。我的习惯是单机 Node 服务设 10~20,微服务模式下每个实例设 5~10,再配合排队等待,宁可让请求稍等也不让数据库崩溃。
const mysql = require('mysql2/promise'); const pool = mysql.createPool({ host: process.env.MYSQL_HOST || '127.0.0.1', port: 3306, user: process.env.MYSQL_USER || 'root', password: process.env.MYSQL_PASSWORD || '', database: process.env.MYSQL_DATABASE || 'test', waitForConnections: true, connectionLimit: 10, maxIdle: 10, idleTimeout: 60000, queueLimit: 0, enableKeepAlive: true, charset: 'utf8mb4' }); module.exports = pool;这里用process.env做默认值,是因为连接串里通常带着数据库密码,不能直接写死在源码里并提交到仓库。charset: 'utf8mb4'则保证中文和 emoji 在连接层就以完整编码传输,避免后面出现乱码。如果想要应用启动时就把池子预热好,可以在初始化后执行一次await pool.query('SELECT 1'),让最消耗时间的握手发生在启动阶段而不是第一个用户请求里。
4.3 池化之后的查询与事务封装
有了 pool,查询函数可以封装得更短。池对象本身就有execute和query,会自动从池里借连接、执行、归还,不需要我们在业务代码里手动管理连接:
async function query(sql, params) { const [rows] = await pool.execute(sql, params); return rows; } const rows = await query('SELECT * FROM products WHERE status = ?', [1]);手动管理连接的场景主要在事务里。事务要求全程使用同一条连接,所以要显式向池申请连接,用完之后release()而不是end():
async function runTransaction(fn) { const conn = await pool.getConnection(); try { await conn.beginTransaction(); const result = await fn(conn); await conn.commit(); return result; } catch (err) { await conn.rollback(); throw err; } finally { conn.release(); } } await runTransaction(async (conn) => { await conn.execute('UPDATE accounts SET balance = balance - ? WHERE id = ?', [100, 1]); await conn.execute('UPDATE accounts SET balance = balance + ? WHERE id = ?', [100, 2]); });release()是把连接还回池子,不是关闭它;如果误用conn.end(),这条连接会被真正销毁,多调用几次池里的连接数就会慢慢少于预期。runTransaction这个封装把 begin、commit、rollback、release 全部收拢到一个函数里,业务代码只需要专注写 SQL,这是我认为最不容易出错的事务写法。注意别在pool.execute里启动事务,因为两次execute可能被分配到不同的连接上,事务就失效了;事务必须用getConnection取同一条连接。
连接池还有一个常被忽略的收益:它可以保持到数据库的链路长期活跃。经过一段空闲期后的第一个请求总是慢一些,那就是池里的空闲连接被数据库回收、需要新建连接的过程。把idleTimeout调大一点,比如 60 秒到 120 秒,能减少这种冷启动式的抖动。enableKeepAlive打开后,驱动会定期发探活包,让中间设备和 MySQL 都认为这条连接还活着。
5. MySQL 连接排错对照:socket 报错、SSL 报错、认证报错和乱码
就算配置写得再顺,实际跑起来也会遇到一堆从黑匣子里蹦出来的错误。这一章把高频的几条按现象、原因、解决的顺序列出来,每一条都是可以直接对着日志定位的。
5.1 Error 2002:连不上 localhost 的 socket 文件
现象:Node 服务在 Linux 上启动,连接数据库时报Error 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock'。
原因:host用的是localhost,Node 客户端尝试走 unix socket 而不是 TCP。MySQL 服务没启动,或者 socket 文件不在/tmp/mysql.sock,都会触发这个错。
解决:先确认 MySQL 在运行,CentOS 下用systemctl status mysqld查看;如果没启动就跑systemctl start mysqld。服务没问题还报错,就把连接串里的host改成127.0.0.1,强制走 TCP 端口,绕开 socket 路径问题。
5.2 ER_NOT_SUPPORTED_AUTH_MODE:MySQL 8 认证协议不认账
现象:驱动返回ER_NOT_SUPPORTED_AUTH_MODE,后面跟着Client does not support authentication protocol requested by server。
原因:MySQL 8 默认的caching_sha2_password是老 mysql 驱动不认识的,老驱动只实现过mysql_native_password。
解决:第一选择是换 mysql2 驱动,它完整支持 caching_sha2_password。如果数据库已经跑起来、想快速救急,可以登录 MySQL 执行ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '新密码';然后FLUSH PRIVILEGES;。这只是开发环境的止损做法,生产环境尽量保留默认认证方式,并把驱动升级到支持 caching_sha2_password 的版本。
5.3 MySQL SSL 连接错误:证书校验还是强制加密
现象:连接时报ER_SSL_CONNECTION_ERROR,或者提示Unable to connect via SSL。有时候普通连接能通,换成特定服务器地址后就开始报 SSL 错误。
原因:MySQL 8 默认开启 SSL 配置,服务端可能要求加密传输;客户端又没带证书或者证书链不完整。开发环境里最常见的是自签名证书不被 Node 信任。
解决:先看服务端SHOW VARIABLES LIKE 'require_secure_transport';。如果确实强制加密,临时开发可以给连接配置加ssl: { rejectUnauthorized: false },表示跳过证书校验。这条只适合本地联调,线上必须把 CA 证书文件路径配进去,并保持rejectUnauthorized: true。顺便检查 MySQL 的ssl_ca、ssl_cert路径,确保服务端证书文件和权限没问题。
5.4 中文乱码:连接层 charset 没对齐
现象:往表里插中文,库里看到的是???,或者 JS 端写 emoji 时 MySQL 报Incorrect string value。
原因:MySQL 连接层使用的字符集不是 utf8mb4。utf8mb4 才是完整支持中文和 emoji 的字符集,旧的 utf8 是它的子集,存 emoji 会直接失败。
解决:连接串或 createPool 参数里显式加charset: 'utf8mb4';再检查表结构,SHOW CREATE TABLE products;,如果建表时没指定 charset,执行ALTER TABLE products CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;。连接层、表、字段三层字符集都对齐,乱码问题基本消失。
5.5 Too many connections:连接泄漏和连接数配比
现象:服务跑一段时间后接口报Too many connections,或者ER_CON_COUNT_ERROR,数据库客户端也连不进去。
原因:最常见的是某段代码pool.getConnection()之后异常分支没release(),连接只借不还,池被掏空;另一种是连接池的connectionLimit设得比 MySQLmax_connections还高,把数据库连接数打满。
解决:把事务代码里的release()放进finally;检查所有手动 getConnection 的分支,确保每条路径都归还。连接池上限不要超过数据库max_connections的七成,比如数据库最大 500,池子最多设 350。简单查询直接用pool.execute,不要手动 getConnection,让驱动自己管理借用和归还。
6. 进阶:用 Express 把 MySQL 查到的数据暴露成 JSON 接口,再顺手加一个健康检查
最后一步,是把你封装好的查询函数挂到 HTTP 接口上,让浏览器或小程序通过 fetch 拿到数据。这样既保住了「JS 直接访问数据 Mysql」的便利,又避开了前端直连的安全禁区。下面这段代码是一个最小的商品列表接口:
const express = require('express'); const pool = require('./pool'); const app = express(); app.use(express.json()); app.get('/api/products', async (req, res, next) => { try { const keyword = String(req.query.q || '').trim(); const orderRaw = String(req.query.order || 'asc').toLowerCase(); const order = ['asc', 'desc'].includes(orderRaw) ? orderRaw : 'asc'; const limit = Number.parseInt(req.query.limit, 10) || 20; const offset = Number.parseInt(req.query.offset, 10) || 0; const [rows] = await pool.execute( `SELECT id, name, price FROM products WHERE name LIKE ? ORDER BY price ${order} LIMIT ? OFFSET ?`, [`%${keyword}%`, limit, offset] ); res.json({ code: 0, data: rows }); } catch (err) { next(err); } }); app.get('/api/health', async (req, res) => { const [rows] = await pool.query('SELECT 1 AS ok'); res.json({ ok: rows[0].ok === 1, ts: Date.now() }); }); app.listen(3000, () => console.log('api listening on 3000'));这段代码把上一章的连接池、白名单排序、参数化查询全串起来了。limit和offset在接口里要被转成 Number,因为req.query里的值是字符串,直接用会让 MySQL 报语法错误;用String()包一层,也能避免?order=asc&order=desc这类数组参数让toLowerCase崩掉。/api/health里的SELECT 1是最轻量的数据库探活语句,验证连接没断、账号还能执行查询,比单纯检查端口开放可靠得多。我一般会把健康检查挂到服务的探活脚本里,每次发布前先看它,再决定要不要切流量。复杂 SQL 也可以提前落成存储过程,Node 侧用CALL procedure_name(?, ?)调用,返回的行数据形式和普通查询一致。这套链路跑顺之后,前端拿到的始终是 JSON,后端到 MySQL 的直连细节全部被收在中间层里,数据访问路径简单、责任边界也清楚。希望帮到你。
本文还有配套的精品资源,点击获取