打破次元壁!WPS + JSA + VBA混合编程实现数据库操作终极方案

deer332025-07-09技术文章32

一、技术背景与需求痛点

  1. JSA原生限制
    WPS JS宏(JSA)目前未内置ADO对象模型支持,无法直接进行数据库连接、记录集操作等底层交互。经研究可以通过VBA类模块来实现:
  2. 跨语言调用可行性验证
    通过研究WPS对象模型发现:JSA可通过
    ActiveXObject直接调用VBA工程中的类模块,实现跨语言对象互操作。

二、技术实现原理

核心机制

  1. ActiveX桥接技术
    JSA通过
    new ActiveXObject("VBAProject.ClassName")实例化VBA类,建立跨语言对象引用。
  2. 动态类型转换
    VBA与JSA数据类型自动映射:
  3. 基础类型(String/Number/Boolean)无缝转换
  4. 对象类型(Recordset/Connection)保持COM引用
  5. 错误冒泡机制
    VBA类抛出的错误会穿透到JSA环境,可通过
    try...catch统一捕获处理。

架构设计


三、实施步骤详解

第一阶段:VBA类开发

  1. 创建标准化数据库类
  2. 类名严格遵循E8AdoBridgeADO命名规范
  3. 封装连接/查询/事务等核心方法
  4. 添加完善的错误处理(Err.Raise
  5. 关键方法设计

四:JSA封装层开发

包装器类设计

class JSA_ADOWrapper {
    constructor() {
        this.vbaObj = new ActiveXObject("VBAProject.E8AdoBridgeADO");
    }
    
    connect(connConfig) { 
        // 转换配置为连接字符串
        this.vbaObj.Connect(fullConnStr);
    }
    
    query(sql) {
        // 执行查询并返回JS风格结果集
    }
}

五、方案优势分析

  1. VBA工程配置
  2. 引用Microsoft ActiveX Data Objects x.x Library
  3. 开启"信任对VBA工程对象模型的访问"

第一章:环境准备

步骤1:安装MySQL ODBC驱动

  1. 访问官网下载页面:
    https://dev.mysql.com/downloads/connector/odbc/

步骤2:启用WPS宏功能

  1. 打开WPS表格 → 顶部菜单栏选择 "开发工具"

步骤3:添加VBA类模块

  1. 在WPS表格中按 Alt + F11 打开VBA编辑器
  2. 右键左侧的 "VBAProject" → 选择 "插入" → "类模块"
  3. 在属性窗口(按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工程信任

  1. 在VBA编辑器中点击菜单:工具 → 引用
  2. 勾选以下两个库:
  3. Microsoft ActiveX Data Objects 6.1 Library
  4. Microsoft ActiveX Data Objects Recordset 6.0 Library

步骤6:创建JS宏模块

  1. 返回WPS表格界面,按 Alt + F11 打开宏编辑器
  2. 右键左侧的 "模块" → 选择 "插入" → "模块"
  3. 将默认生成的 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:运行程序

  1. 保存文件为 .xlsm 格式(启用宏的工作簿)
  2. Alt + F11 打开宏对话框
  3. 选择 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表格的数据导入。如果遇到任何问题,请随时提供具体错误信息留言!