打破次元壁!WPS + JSA + VBA混合编程实现数据库操作终极方案
一、技术背景与需求痛点
- JSA原生限制
WPS JS宏(JSA)目前未内置ADO对象模型支持,无法直接进行数据库连接、记录集操作等底层交互。经研究可以通过VBA类模块来实现: - 跨语言调用可行性验证
通过研究WPS对象模型发现:JSA可通过ActiveXObject直接调用VBA工程中的类模块,实现跨语言对象互操作。
二、技术实现原理
核心机制
- ActiveX桥接技术
JSA通过new ActiveXObject("VBAProject.ClassName")实例化VBA类,建立跨语言对象引用。 - 动态类型转换
VBA与JSA数据类型自动映射: - 基础类型(String/Number/Boolean)无缝转换
- 对象类型(Recordset/Connection)保持COM引用
- 错误冒泡机制
VBA类抛出的错误会穿透到JSA环境,可通过try...catch统一捕获处理。
架构设计
三、实施步骤详解
第一阶段:VBA类开发
- 创建标准化数据库类
- 类名严格遵循E8AdoBridgeADO命名规范
- 封装连接/查询/事务等核心方法
- 添加完善的错误处理(Err.Raise)
- 关键方法设计
四:JSA封装层开发
包装器类设计
class JSA_ADOWrapper {
constructor() {
this.vbaObj = new ActiveXObject("VBAProject.E8AdoBridgeADO");
}
connect(connConfig) {
// 转换配置为连接字符串
this.vbaObj.Connect(fullConnStr);
}
query(sql) {
// 执行查询并返回JS风格结果集
}
}五、方案优势分析
- VBA工程配置
- 引用Microsoft ActiveX Data Objects x.x Library
- 开启"信任对VBA工程对象模型的访问"
第一章:环境准备
步骤1:安装MySQL ODBC驱动
- 访问官网下载页面:
https://dev.mysql.com/downloads/connector/odbc/
步骤2:启用WPS宏功能
- 打开WPS表格 → 顶部菜单栏选择 "开发工具"
步骤3:添加VBA类模块
- 在WPS表格中按 Alt + F11 打开VBA编辑器
- 右键左侧的 "VBAProject" → 选择 "插入" → "类模块"
- 在属性窗口(按F4)中修改类名称为 E8AdoBridgeADO
(名称必须严格一致,区分大小写)
步骤4:编写VBA类代码
VBA类模块 E8AdoBridgeADO(需要先在VBA工程中存在)将以下代码完整复制到类模块中(不要遗漏任何部分):
' E8AdoBridgeADO.cls 类模块代码
' 完整的ADO数据库操作类(支持MySQL/SQL Server等)
Option Explicit
' 定义私有对象变量
Private m_Conn As Object ' ADODB.Connection
Private m_RS As Object ' ADODB.Recordset
Private m_Command As Object ' ADODB.Command
Private m_TransactionLevel As Long ' 事务嵌套层级
' ADO常量定义(避免引用ADO库也能运行)
Private Enum CursorTypeEnum
adOpenForwardOnly = 0
adOpenKeyset = 1
adOpenDynamic = 2
adOpenStatic = 3
End Enum
Private Enum LockTypeEnum
adLockReadOnly = 1
adLockPessimistic = 2
adLockOptimistic = 3
adLockBatchOptimistic = 4
End Enum
Private Enum ExecuteOptionEnum
adOptionUnspecified = -1
adAsyncExecute = &H10
adAsyncFetch = &H20
End Enum
' ████████████████████████████████████████████
' 类初始化与销毁
' ████████████████████████████████████████████
Private Sub Class_Initialize()
' 类初始化时创建基础对象
Set m_Conn = CreateObject("ADODB.Connection")
Set m_RS = CreateObject("ADODB.Recordset")
Set m_Command = CreateObject("ADODB.Command")
m_TransactionLevel = 0
m_Conn.CursorLocation = 3 ' adUseClient
End Sub
Private Sub Class_Terminate()
' 类销毁时确保释放所有资源
On Error Resume Next
If Not m_RS Is Nothing Then
If m_RS.State > 0 Then m_RS.Close
Set m_RS = Nothing
End If
If Not m_Conn Is Nothing Then
If m_Conn.State > 0 Then m_Conn.Close
Set m_Conn = Nothing
End If
Set m_Command = Nothing
End Sub
' ████████████████████████████████████████████
' 数据库连接管理
' ████████████████████████████████████████████
Public Sub Connect(ConnectionString As String, Optional Timeout As Integer = 30)
' 功能:建立数据库连接
' 参数:
' ConnectionString - ODBC连接字符串
' Timeout - 连接超时时间(秒)
On Error GoTo ErrorHandler
If m_Conn.State = adStateOpen Then Exit Sub
With m_Conn
.ConnectionTimeout = Timeout
.CommandTimeout = Timeout
.ConnectionString = ConnectionString
.Open
End With
Exit Sub
ErrorHandler:
Err.Raise vbObjectError + 1001, "E8AdoBridgeADO.Connect", _
"连接失败:" & Err.Description & vbCrLf & _
"连接字符串:" & ConnectionString
End Sub
Public Sub Disconnect()
' 功能:安全关闭数据库连接
On Error Resume Next
If m_Conn.State = adStateOpen Then
If m_TransactionLevel > 0 Then
m_Conn.RollbackTrans
End If
m_Conn.Close
End If
Set m_Conn = Nothing
End Sub
' ████████████████████████████████████████████
' 查询执行方法
' ████████████████████████████████████████████
Public Sub ExecuteQuery( _
SQL As String, _
Optional CursorType As CursorTypeEnum = adOpenStatic, _
Optional LockType As LockTypeEnum = adLockReadOnly, _
Optional CommandTimeout As Integer = 30 _
)
' 功能:执行查询并返回记录集
' 参数:
' SQL - 要执行的SQL语句
' CursorType - 游标类型(默认静态)
' LockType - 锁定类型(默认只读)
' CommandTimeout - 命令超时时间
On Error GoTo ErrorHandler
If m_RS.State = adStateOpen Then m_RS.Close
With m_RS
.CursorLocation = adUseClient
.CursorType = CursorType
.LockType = LockType
.Open SQL, m_Conn, , , adCmdText
End With
Exit Sub
ErrorHandler:
Err.Raise vbObjectError + 1002, "clsE8ADO.ExecuteQuery", _
"查询执行失败:" & Err.Description & vbCrLf & _
"SQL:" & Left(SQL, 200) & "..."
End Sub
Public Sub ExecuteNonQuery(SQL As String, Optional CommandTimeout As Integer = 30)
' 功能:执行非查询SQL(INSERT/UPDATE/DELETE)
On Error GoTo ErrorHandler
m_Conn.CommandTimeout = CommandTimeout
m_Conn.Execute SQL, , adExecuteNoRecords
Exit Sub
ErrorHandler:
Err.Raise vbObjectError + 1003, "clsE8ADO.ExecuteNonQuery", _
"非查询执行失败:" & Err.Description & vbCrLf & _
"SQL:" & Left(SQL, 200) & "..."
End Sub
' ████████████████████████████████████████████
' 事务管理
' ████████████████████████████████████████████
Public Sub BeginTrans()
' 开始事务(支持嵌套)
If m_TransactionLevel = 0 Then
m_Conn.BeginTrans
End If
m_TransactionLevel = m_TransactionLevel + 1
End Sub
Public Sub CommitTrans()
' 提交事务
If m_TransactionLevel > 0 Then
m_TransactionLevel = m_TransactionLevel - 1
If m_TransactionLevel = 0 Then
m_Conn.CommitTrans
End If
End If
End Sub
Public Sub RollbackTrans()
' 回滚事务
If m_TransactionLevel > 0 Then
m_TransactionLevel = 0
m_Conn.RollbackTrans
End If
End Sub
' ████████████████████████████████████████████
' 记录集操作方法
' ████████████████████████████████████████████
Public Function GetRecordCount() As Long
' 安全获取记录数(处理可能的-1返回值)
On Error Resume Next
If m_RS.State <> adStateOpen Then Exit Function
If m_RS.Supports(adApproxPosition) Then
m_RS.MoveLast
GetRecordCount = m_RS.RecordCount
m_RS.MoveFirst
Else
GetRecordCount = -1
End If
End Function
Public Sub MoveFirst()
If m_RS.State = adStateOpen Then m_RS.MoveFirst
End Sub
Public Sub MoveNext()
If m_RS.State = adStateOpen Then m_RS.MoveNext
End Sub
Public Function EOF() As Boolean
EOF = m_RS.EOF
End Function
' ████████████████████████████████████████████
' 属性访问器
' ████████████████████████████████████████████
Public Property Get Connection() As Object
Set Connection = m_Conn
End Property
Public Property Get Recordset() As Object
Set Recordset = m_RS
End Property
Public Property Get Fields() As Object
Set Fields = m_RS.Fields
End Property
Public Property Get State() As Integer
State = m_Conn.State
End Property
' ████████████████████████████████████████████
' 高级功能:参数化查询
' ████████████████████████████████████████████
Public Sub AddParameter( _
Name As String, _
Value As Variant, _
Optional DataType As DataTypeEnum = adVariant _
)
' 功能:添加命令参数
Dim param As Object
Set param = m_Command.CreateParameter(Name, DataType, adParamInput, , Value)
m_Command.Parameters.Append param
End Sub
Public Sub ExecuteParameterized( _
SQL As String, _
Optional CommandType As CommandTypeEnum = adCmdText _
)
' 功能:执行参数化查询
On Error GoTo ErrorHandler
With m_Command
.ActiveConnection = m_Conn
.CommandText = SQL
.CommandType = CommandType
.Execute
End With
Exit Sub
ErrorHandler:
Err.Raise vbObjectError + 1004, "dataADO.ExecuteParameterized", _
"参数化查询失败:" & Err.Description
End Sub
步骤5:设置VBA工程信任
- 在VBA编辑器中点击菜单:工具 → 引用
- 勾选以下两个库:
- Microsoft ActiveX Data Objects 6.1 Library
- Microsoft ActiveX Data Objects Recordset 6.0 Library
步骤6:创建JS宏模块
- 返回WPS表格界面,按 Alt + F11 打开宏编辑器
- 右键左侧的 "模块" → 选择 "插入" → "模块"
- 将默认生成的 Module1 重命名为 MainModule
步骤7:完整JSA代码
JAVASCRIPT
// ====== Part1:封装VBA调用 ======
class JSA_ADOWrapper {
constructor() {
try {
// 创建VBA类的实例
this.vbaADO = new ActiveXObject("VBAProject.E8AdoBridgeADO");
Console.log("[成功] VBA类已加载");
} catch (e) {
Console.log("[错误] 请检查:\n1. VBA类名是否正确\n2. 是否启用宏信任");
throw new Error("VBA类初始化失败");
}
}
// 连接数据库方法
Connect(server, user, pwd, dbName) {
const connStr =
`Driver={MySQL ODBC 8.0 Unicode Driver};` +
`Server=${server};` +
`Database=${dbName};` +
`Uid=${user};` +
`Pwd=${pwd};` +
`Option=3;`;
this.vbaADO.Connect(connStr);
Console.log(`已连接到数据库:${dbName}`);
}
// 执行SQL查询
ExecuteQuery(sql) {
this.vbaADO.ExecuteQuery(sql);
Console.log(`已执行SQL:${sql.substring(0, 50)}...`); // 显示前50字符
}
// 获取记录数
GetRecordCount() {
return this.vbaADO.GetRecordCount();
}
// 关闭连接
Close() {
this.vbaADO.CloseConn();
Console.log("数据库连接已关闭");
}
}
// ====== Part2:主程序 ======
function ImportMySQLToSheet() {
// 创建数据库操作对象
const db = new JSA_ADOWrapper();
try {
/************************ 配置区(根据实际情况修改) ************************/
const config = {
server: "localhost", // 如果是远程数据库填IP地址
user: "root", // 数据库用户名
pwd: "123456", // 数据库密码
dbName: "your_database_name",
tableName: "products" // 要导出的表名
};
/*************************************************************************/
// 1. 连接数据库
db.Connect(config.server, config.user, config.pwd, config.dbName);
// 2. 执行查询
const sql = `SELECT * FROM ${config.tableName}`;
db.ExecuteQuery(sql);
Console.log(`共查询到记录:${db.GetRecordCount()}条`);
// 3. 准备写入工作表
const sheet = Application.ActiveSheet;
sheet.Cells.Clear(); // 清空当前工作表
let rowIndex = 1;
// 4. 写入表头
const fields = db.vbaADO.RS.Fields;
for (let i = 0; i < fields.Count; i++) {
sheet.Cells(1, i + 1).Value = fields.Item(i).Name;
Console.log(`写入表头:${fields.Item(i).Name}`);
}
// 5. 写入数据
Console.log("开始写入数据...");
let recordCounter = 0;
while (!db.vbaADO.RS.EOF) {
rowIndex++;
for (let j = 0; j < fields.Count; j++) {
// 处理特殊值(如Null值)
const cellValue = fields.Item(j).Value === null ? "" : fields.Item(j).Value;
sheet.Cells(rowIndex, j + 1).Value = cellValue;
}
recordCounter++;
db.vbaADO.RS.MoveNext();
// 每100条显示进度
if (recordCounter % 100 === 0) {
Console.log(`已写入 ${recordCounter} 条...`);
}
}
Console.log(`数据导入完成!共导入 ${recordCounter} 条数据`);
} catch (e) {
Console.log("发生错误:" + e.message);
} finally {
// 确保关闭数据库连接
if (db.vbaADO.Conn && db.vbaADO.Conn.State !== 0) {
db.Close();
}
}
}
// ====== Part3:辅助函数 ======
function TestConnection() {
const db = new JSA_ADOWrapper();
db.Connect("localhost", "root", "123456", "test_db");
db.Close();
}步骤9:运行程序
- 保存文件为 .xlsm 格式(启用宏的工作簿)
- 按 Alt + F11 打开宏对话框
- 选择 ImportMySQLToSheet → 点击 运行
故障排除
常见问题1:找不到VBA类
- 症状:出现 ActiveXObject 创建失败 错误
- 解决方法:检查类名是否严格为E8AdoBridgeADO在VBA工程中右键类模块 → 查看属性确认名称重新编译VBA工程(按 Ctrl + S 保存)
常见问题2:数据库连接失败
- 症状:抛出 [Microsoft][ODBC 驱动程序管理器] 未发现数据源名称并且未指定默认驱动程序
- 解决方法:检查ODBC驱动是否安装成功打开控制面板 → 管理工具 → ODBC 数据源 → 查看驱动列表如果找不到 MySQL ODBC 8.0 Unicode Driver,重新安装驱动
常见问题3:字段显示不全
- 症状:某些列显示 [object] 或空白
- 解决方法:
在写入数据处添加类型判断:
let cellValue;
if (typeof fields.Item(j).Value === "object") {
cellValue = JSON.stringify(fields.Item(j).Value);
} else {
cellValue = fields.Item(j).Value === null ? "" : fields.Item(j).Value;
}扩展优化建议
添加进度条
在写入数据部分添加可视化进度:
// 在循环前添加
const totalRecords = db.GetRecordCount();
Application.StatusBar = "准备写入数据...";
// 在循环内更新
Application.StatusBar = `正在写入 ${recordCounter}/${totalRecords} (${Math.round((recordCounter/totalRecords)*100)}%)`;添加日期格式化
处理MySQL日期字段:
if (fields.Item(j).Type === 135) { // adDBTimeStamp
const rawDate = new Date(fields.Item(j).Value);
cellValue = rawDate.toLocaleDateString();
}分页查询(大数据量优化)
修改SQL语句实现分页:
const pageSize = 1000;
let page = 1;
while (true) {
const sql = `SELECT * FROM ${config.tableName} LIMIT ${pageSize} OFFSET ${(page-1)*pageSize}`;
db.ExecuteQuery(sql);
if (db.GetRecordCount() === 0) break;
// 写入数据...
page++;
}通过以上超过详细指导,您应该能够顺利完成从MySQL到WPS表格的数据导入。如果遇到任何问题,请随时提供具体错误信息留言!