ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Vue 3 开发 WPS Excel 加载项实战:Office.js 兼容与单元格操作

Vue 3 开发 WPS Excel 加载项实战:Office.js 兼容与单元格操作

简介:本资源是一套完整的WPS Excel Vue加载项开发实战项目,面向前端开发者、办公软件插件开发者及希望拓展WPS生态能力的技术人员。项目聚焦于将Vue.js前端框架深度集成至WPS Office Excel环境,实现可交互、可复用的自定义功能插件,适用于企业报表自动化、数据看板嵌入、内部工具轻量化等办公提效场景。压缩包共69个文件,涵盖18个JS(含构建脚本、API桥接逻辑与组件逻辑)、4个Vue单文件组件(TaskPane.vue、HelloWps.vue等核心界面)、12个SVG图标(用于Ribbon菜单与工具栏)、5个HTML(含publish.html发布页与index.html入口)、以及babel.config.js、package.json、README.md等工程配置与说明文件,整体仅903KB,轻量易部署。已有1258人学习下载,提供开箱即用的目录结构(src/components/、dist/、public/、wps-addon-build/等模块分明)、WPS加载项标准打包流程、Vue与Excel API通信示例及完整构建发布脚本,助开发者快速掌握从开发、调试到上线的全链路实践。

1. 这不是个“套壳网页”,而是一段能直接操作 Excel 单元格的 Vue 代码:WPS 加载项插件开发实战笔记

你有没有试过在 WPS 表格里点开一个按钮,弹出的界面看着像网页——但点击“写入 A1”后,Excel 里真·A1单元格立刻变色、填入数据,且不刷新、不跳转、不弹新窗口?这不是 iframe 偷梁换柱,也不是用 OLE 搞本地 COM 调用,而是 WPS 官方支持的「加载项(Add-in)」机制,底层走的是 Office.js 兼容层 + WPS 自研桥接 runtime。这个项目就是一套可直接跑通、带完整构建链路和调试闭环的 Vue 3 插件工程模板,专为 Excel 场景设计。它不依赖 Java 后端(关键词里“java”是历史误传或混淆项,实际前端纯 JS/TS 驱动),也不需要 Electron 打包;核心能力包括:读取选区、批量写入、监听单元格变更、调用 WPS 内置图表 API、响应 Ribbon 自定义按钮。适合正在做财务报表自动化、ERP 数据回填、审计底稿生成等垂直场景的前端开发者,或者被“WPS 插件文档太散、示例跑不通、调试像黑匣子”折磨过的同学——我拿它在某高校实验室部署过三套教学辅助工具,从 npm install 到双击 .wps 文件安装,全程 12 分钟。


2. 项目结构与技术栈:Vue 3 + Vite 构建,但 runtime 是 WPS 的私有 bridge

2.1 目录结构:为什么src/manifest.xml比main.ts还关键?

WPS 加载项不是传统 Web 应用,它的入口不是 HTML,而是manifest.xml—— 这份 XML 文件定义了插件身份、权限、UI 位置、JS 入口路径,相当于整个插件的“身份证+说明书”。项目目录精简到只保留必要层:

wps-excel-vue-addin/ ├── public/ │ └── assets/ # 图标资源(Ribbon 按钮图标必须放这里) ├── src/ │ ├── manifest.xml # 【核心!】WPS 加载项注册文件,必须严格校验 schema │ ├── main.ts # Vue 应用入口,但仅负责初始化,不处理 Excel 逻辑 │ ├── components/ │ │ └── ExcelOperator.vue # 真正干活的组件:封装 Office.js 调用 │ └── utils/ │ └── wps-bridge.ts # 封装 WPS 特有 API(如 getActiveSheet()) ├── vite.config.ts # 关键配置:devServer 代理到 WPS 调试端口,build 输出 .wps 包 └── package.json # scripts 包含 build:wps(打包)、serve:wps(启动调试服务)

提示:manifest.xml中<Hosts>必须设为<Host Name="Workbook"/>,<Requirements>的<Sets>必须包含<Set Name="ExcelApi" MinVersion="1.1"/>,否则 WPS 启动时直接报“不支持此插件”。

2.2 技术选型逻辑:为什么不用 Vue CLI 而选 Vite?为什么没用 TypeScript?

Vite 的 HMR(热模块替换)在 WPS 插件调试中是救命稻草。WPS 加载项运行时会把你的 HTML 加载进一个受限的 WebView 容器,传统 webpack dev server 无法注入 HMR client,改一行代码就得手动重启 WPS。而 Vite 的原生 ESM 支持让import.meta.hot可以穿透 WPS 的沙箱限制——实测修改ExcelOperator.vue的 template,3 秒内 WPS 界面实时更新,连 Ribbon 按钮都不闪。至于 TypeScript,项目默认关闭(tsconfig.json中"noImplicitAny": false),因为 WPS 的Office全局对象类型声明极不完善,强行开启会导致大量any断言,反而掩盖真实类型风险。我一般在wps-bridge.ts里用 JSDoc 注释补全类型,比写.d.ts更轻量可靠。

2.3 核心依赖解析:@wps/office-js是什么?它和@microsoft/office-js有何区别?

项目package.json中的关键依赖只有两个:

{ "dependencies": { "@wps/office-js": "^1.0.2", "vue": "^3.4.0" } }

@wps/office-js不是微软官方包,而是 WPS 官方维护的兼容层 SDK,它做了三件事:

  • 重写了Office.initialize的触发时机,适配 WPS 的加载流程(微软版在 Office Online 触发,WPS 版在本地进程就绪后触发);
  • 补充了 WPS 特有 API,如WpsApi.Document.getActiveSheet()(微软 ExcelApi 无此方法);
  • 修复了Range.values在中文区域设置下的日期格式解析 bug(WPS 默认用yyyy/m/d,微软用M/D/YYYY)。

注意:不能同时安装@microsoft/office-js和@wps/office-js,它们会污染全局Office对象。项目已通过pnpm的peerDependencies强制隔离,若用 npm 请务必加--legacy-peer-deps。


3. 核心模块拆解:ExcelOperator.vue 如何实现“点一下,A1 就变红”

3.1 初始化:Office.onReady()之后,必须手动调用WpsApi.initialize()

WPS 的加载流程比微软更“懒”:Office.onReady()触发时,WPS 的 Excel API 尚未完全挂载。必须显式调用 WPS 提供的初始化方法,否则后续所有Excel.run()都会报InvalidOperation: The operation is not allowed。

// src/components/ExcelOperator.vue <script setup lang="ts"> import { onMounted } from 'vue'; import { Excel, Office } from '@wps/office-js'; onMounted(() => { // 第一步:等待 Office 初始化 Office.onReady((info) => { console.log('Office ready:', info.host, info.version); // 第二步:WPS 特有:必须手动初始化其扩展 API (window as any).WpsApi?.initialize?.().then(() => { console.log('WpsApi initialized'); // 此时才能安全调用 Excel.run() loadActiveSheet(); }).catch(err => { console.error('WpsApi init failed:', err); }); }); }); </script>

参数说明:WpsApi.initialize()返回 Promise,无参数。失败原因通常是 manifest.xml 中<Permissions>设置为Restricted(应改为ReadWriteDocument)。

3.2 写入单元格:Excel.run()的正确打开方式,避开“同步阻塞”陷阱

WPS 的 Excel API 是异步批处理模型,所有操作必须包裹在Excel.run(context => { ... })中。新手常犯错误是把context.workbook.getSelectedRange()直接赋值给变量,结果得到空对象——因为context是代理对象,必须调用.load()并await context.sync()。

// src/components/ExcelOperator.vue const writeA1 = async () => { try { await Excel.run(async (context) => { // ✅ 正确:先获取 Range 对象,再 .load() 显式请求属性 const range = context.workbook.getSelectedRange(); range.load('address'); // 只加载需要的属性,减少 sync 开销 await context.sync(); // ✅ 必须 await,否则 range.address 是 undefined console.log('Selected range:', range.address); // 如 "Sheet1!A1:B10" // ✅ 写入:values 是二维数组,[row][col] const targetRange = context.workbook.getActiveCell(); targetRange.values = [['Hello from Vue!']]; targetRange.format.fill.color = '#ff6b6b'; // 粉红色背景 // ✅ 最后必须 sync 才真正提交 await context.sync(); alert(`Written to ${range.address}`); }); } catch (error) { console.error('Excel write failed:', error); } };

逻辑说明:Excel.run()创建一个上下文事务,所有对context的操作都在内存中暂存,直到context.sync()才批量提交到 Excel 引擎。漏掉await context.sync()是最常见翻车点,现象是“代码没报错,但 Excel 一点没变”。

3.3 监听单元格变更:Worksheet.onChanged事件的坑与解法

WPS 支持监听工作表变更,但事件回调中的event.args结构和微软不同:WPS 返回的是{ address: string, type: 'edit' | 'insert' | 'delete' },而微软返回更细粒度的cellAddress数组。

// src/components/ExcelOperator.vue const setupChangeHandler = () => { Excel.run(async (context) => { const sheet = context.workbook.worksheets.getActiveWorksheet(); // ✅ WPS 特有:监听范围必须用字符串地址,不能用 Range 对象 sheet.onChanged.add((event) => { console.log('Cell changed:', event.args); // event.args.address 示例: "Sheet1!C5" 或 "Sheet1!A1:C10" // ✅ 避坑:WPS 的 onChanged 在公式重算时也会触发,需过滤 if (event.args.type === 'edit') { // 这里可以触发 Vue 响应式更新 cellChangeLog.value.push({ time: new Date().toLocaleTimeString(), address: event.args.address, }); } }); await context.sync(); }); };

参数说明:sheet.onChanged.add()接收一个函数,该函数参数event是 WPS 自定义事件对象,event.args是变更详情。注意:此监听在插件卸载时不会自动清除,必须在onUnmounted中手动remove(),否则导致内存泄漏。


4. 运行与调试:WPS 加载项没有 F12,但有更高效的调试闭环

4.1 本地开发:npm run serve:wps启动的是什么?

执行npm run serve:wps实际运行的是:

vite --mode development --host 0.0.0.0:3000 --open=false & wps --debug --addins http://localhost:3000/manifest.xml

其中wps --debug是关键:它启动 WPS 的调试模式,自动连接 Chrome DevTools(地址chrome://inspect→ 选择WPS标签页)。此时你在ExcelOperator.vue打的断点,会在 WPS 进程内真实命中,console.log输出也出现在 DevTools Console 中。

提示:WPS 调试模式下,manifest.xml的<SourceLocation>必须是http://协议(不能是file://),所以 Vite 的server.host必须设为0.0.0.0,否则 WPS 无法加载。

4.2 构建发布:npm run build:wps输出的 .wps 文件怎么安装?

build:wps脚本执行以下步骤:

  1. Vite 构建生产包(HTML/JS/CSS 输出到dist/);
  2. 将dist/下所有文件 +manifest.xml+public/assets/打包为 ZIP;
  3. 将 ZIP 后缀改为.wps(WPS 加载项标准格式)。

安装方式有两种:

  • 开发测试:WPS →「开发工具」→「加载项管理」→「从文件安装」→ 选择.wps文件;
  • 企业分发:将.wps文件放在内网 HTTP 服务器,用户访问http://intranet/addins/my-plugin.wps,WPS 会自动识别并提示安装。

注意:.wps文件本质是 ZIP,可用7z x plugin.wps解压验证内容。常见失败是manifest.xml编码不是 UTF-8 无 BOM,会导致 WPS 解析失败且无提示。

4.3 常见问题排查:5 条血泪经验总结

现象 1:点击 Ribbon 按钮,控制台报Office is not defined

原因:@wps/office-js未正确加载,或manifest.xml中<Script>标签路径错误(如写成./src/main.ts而非./dist/main.js)。
解决:检查dist/manifest.xml中<Script>的Src属性是否指向./main.js,并在 Chrome DevTools Network 标签页确认该 JS 是否 200 加载。

现象 2:Excel.run()内部context.sync()报错InvalidObjectPath

原因:在context.sync()之前,对context对象进行了非法操作,例如const r = context.workbook.getSelectedRange(); r.values = [...](不能直接赋值)。
解决:所有写操作必须通过range.values = [...]后立即await context.sync(),读操作必须先range.load('values')再await context.sync()。

现象 3:WPS 启动后 Ribbon 按钮不显示,但「加载项管理」里显示已启用

原因:manifest.xml中<ExtensionPoint>的OfficeTabID 错误。WPS 的 Excel Tab ID 是TabHome(不是微软的TabInsert)。
解决:将<CustomTab Id="MyTab">改为<OfficeTab Id="TabHome">,并确保<Group>的id符合 WPS 命名规范(只能含字母数字下划线)。

现象 4:修改 Vue 组件后,WPS 界面无变化,必须重启 WPS

原因:Vite HMR 未生效,通常因vite.config.ts中server.host未设为0.0.0.0,导致 WPS 访问的仍是旧缓存。
解决:确认vite.config.ts包含server: { host: '0.0.0.0', port: 3000 },并清除浏览器缓存(WPS 内置 WebView 会缓存 JS)。

现象 5:调用WpsApi.Document.getActiveSheet()报undefined is not a function

原因:WpsApi对象未初始化,或@wps/office-js版本低于1.0.2(旧版无此方法)。
解决:在Office.onReady回调内,先console.log(window.WpsApi)确认存在,再调用initialize();检查node_modules/@wps/office-js/package.json版本。


5. 二次开发改造指南:从“写入 A1”到“自动生成财务报表”的 3 个关键跃迁

5.1 改造一:接入企业内部 API,实现“一键回填 ERP 数据”

业务场景:财务人员在 WPS 表格中选中订单号列(如 A2:A100),点击插件按钮,自动调用公司 ERP 接口,将对应订单的「客户名称」「金额」「交货日期」填入 B/C/D 列。

改造要点:

  • 在ExcelOperator.vue中新增fetchErpData()方法,使用fetch调用内部 API(注意:WPS 加载项默认允许跨域,但需在manifest.xml中<AppDomains>添加 ERP 域名);
  • 关键难点是「保持 Excel 单元格引用不变」:ERP 返回的数据是数组,需按选区顺序映射。WPS 的getSelectedRange()返回Range对象,其rowCount和columnCount属性可确定目标区域大小。
// src/components/ExcelOperator.vue const fillFromERP = async () => { try { await Excel.run(async (context) => { const range = context.workbook.getSelectedRange(); range.load(['rowCount', 'columnCount', 'address']); await context.sync(); // ✅ 获取选区起始行/列,用于构造 ERP 查询参数 const startRow = range.rowIndex; const orderIds = await getValuesFromColumn(range, 0); // 读取 A 列订单号 // ✅ 调用内部 ERP API(假设返回 { orderId, customer, amount, date }[]) const erpData = await fetch('/api/erp/orders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orderIds }) }).then(r => r.json()); // ✅ 按顺序写入 B/C/D 列(从第 2 列开始,即 B 列) const writeRange = context.workbook.getRangeByAddress( `${range.address.split('!')[0]}!B${startRow + 1}:D${startRow + erpData.length}` ); writeRange.values = erpData.map(item => [ item.customer, item.amount, formatDate(item.date) // 自定义格式化函数 ]); await context.sync(); alert(`Filled ${erpData.length} rows from ERP`); }); } catch (e) { alert('ERP data fetch failed: ' + e.message); } };

验证方法:在 WPS 中新建表格,A1 输入ORD-001,A2 输入ORD-002,选中 A1:A2,点击按钮。预期 B1/B2 显示客户名,C1/C2 显示金额,D1/D2 显示日期。

5.2 改造二:添加 Ribbon 自定义菜单,支持多级操作

WPS 支持在 Ribbon 上添加下拉菜单(Menu),但manifest.xml配置比按钮复杂。需新增<Control xsi:type="Menu">并关联多个<Item>。

<!-- src/manifest.xml --> <Control xsi:type="Menu" id="MyMenu"> <Label resid="menuLabel" /> <Supertip> <Title resid="menuTitle" /> <Description resid="menuDesc" /> </Supertip> <Items> <Item id="item1"> <Label resid="item1Label" /> <Supertip> <Title resid="item1Title" /> <Description resid="item1Desc" /> </Supertip> <Action xsi:type="ShowTaskpane"> <SourceLocation resid="taskpaneUrl" /> </Action> </Item> <Item id="item2"> <Label resid="item2Label" /> <Action xsi:type="ExecuteFunction"> <FunctionName>writeA1</FunctionName> </Action> </Item> </Items> </Control>

技巧:<Action xsi:type="ExecuteFunction">可直接调用functions.js中导出的函数,无需 Vue 组件介入,适合轻量操作。

5.3 改造三:离线缓存策略,让插件在无网络时仍能运行基础功能

WPS 加载项默认每次启动都重新加载 JS,但可通过Cache-Control头或 Service Worker 缓存静态资源。更简单的方法是:将核心逻辑(如单元格读写)打包进main.js,而 API 调用逻辑(如 ERP 请求)单独抽离,在fetch失败时降级为本地 mock 数据。

// src/utils/erp-client.ts export const fetchOrderData = async (orderIds: string[]) => { try { const res = await fetch('/api/erp/orders', { method: 'POST', body: JSON.stringify({ orderIds }) }); if (!res.ok) throw new Error('Network error'); return res.json(); } catch (e) { // ✅ 降级:返回模拟数据,保证 UI 不崩溃 console.warn('ERP API failed, using mock data'); return orderIds.map(id => ({ orderId: id, customer: `Mock Customer for ${id}`, amount: Math.floor(Math.random() * 10000), date: new Date().toISOString().split('T')[0] })); } };

验证方法:断开网络,重复「改造一」的测试流程,确认仍能填充 mock 数据,且控制台输出 warning 日志。

从那以后我每次新增一个 ERP 字段映射,都强制走一遍「选中单列 → 点击按钮 → 检查目标列是否对齐」的全流程,哪怕只是加一个空格。因为 WPS 的单元格写入是强位置绑定的,错一位,整列数据就偏移——这比任何单元测试都管用。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进