简介:WPS演示催化剂插件项目代码包,面向希望了解或二次开发国产办公插件的中高级开发者,解决在WPS演示中内嵌HTML页面并实现实时交互的实际需求。整个压缩包体积仅6KB、共3个文件,包含HTML入口页面、inscode项目配置与gitignore忽略规则,文件结构精简,适合快速通读插件核心结构。目前已有281人学习或下载,对于评估插件实现思路有一定参考热度。通过源码可以掌握插件如何调用WPS演示的扩展机制、组织HTML渲染逻辑,并理解获奖插件的兼容性处理方式;开发者可据此复现功能、修改界面或移植到其他演示场景,也可借鉴其信创赛项获奖项目的代码组织方法。
1. WPS演示催化剂插件是什么:把几十步批量操作压成一次点击
做演示文稿最耗时间的其实不是想内容,而是改字体、清空段、统一行距这些重复劳动。某同事处理一份 80 页的工作汇报,光把全部页面从宋体改成微软雅黑就折腾了近两小时,中间还得一页页检查表格和流程图里漏掉的文字。这个叫“催化剂”的方向,就是要解决这类场景:它基于 WPS 加载项体系,给 WPS 演示加一个小而顺手的命令面板,把批量替换字体、清理多余空段落、统一行距等动作封装成一次点击。适合每天和 WPS 演示打交道的行政、教研、运维和文档岗;不适合指望它自动完成设计排版的人。项目代码的核心价值在于把逻辑拆成命令注册表、递归遍历和快照还原三层,后面我会按这个结构一步步落地。
2. 先选对插件形态:为什么加载项而不是 VBA 宏
2.1 三条路线对比:VBA 宏、JSA 宏、WPS 加载项
开发 WPS 演示的批处理工具,常见有三条路:VBA 宏、JSA 宏(WPS 的 JS 宏)和 WPS 加载项。先看对比,再讲为什么我最后选加载项。
| 路线 | 界面能力 | 个人版可用性 | 跨平台 | 主要坑 |
|---|---|---|---|---|
| VBA 宏 | 弱,靠窗体 | 默认不启用,需另装 VBA 兼容组件 | Windows 为主 | 不同版本行为差异大,常陷入“能跑就好”的玄学 |
| JSA 宏 | 弱,无复杂面板 | 内置,基本可用 | 随 WPS 版本 | API 覆盖不全,UI 能力有限,复杂交互不好做 |
| WPS 加载项 | 强,HTML+JS 面板 | 个人版可开发自用 | 看版本,Windows 最稳 | 入门门槛比宏高,桥接概念要理解 |
VBA 宏最大的问题是环境不确定性。个人版默认不带 VBA 程序能力,需要额外安装兼容组件,而组件的具体版本、位数、注册状态,只要有一个不匹配,就会出现“代码看着没问题但就是跑不起来”的情况。JSA 宏虽然内置,但它的定位更适合录一段脚本、做一次批处理,要做出一个带按钮、带参数选择的工具面板,开发成本全在 UI 上。
WPS 加载项的思路和 Office 的 Add-ins 同源:界面是一个本地网页,业务逻辑通过桥接层访问 WPS 文档对象。好处是界面能力拉满,前端技术可以复用,代码结构清晰;坏处是学习曲线比写宏稍陡,尤其要理解 renderer 和 main 的分工。对“催化剂”这种需要面板、需要组合命令、需要后续持续扩展的项目,加载项是正确选择。
2.2 搭开发环境:wpsjs 工具链三步跑通
WPS 官方提供了wpsjs这套加载项开发命令行工具,创建项目、本地调试、打包都在这里面。环境要求是 Node 和 npm,安装完成后三步走:
# 第一步:全局安装 wpsjs 命令行工具 npm install -g wpsjs # 第二步:创建加载项项目,按提示选择 wpp(WPS 演示)模板 wpsjs create catalyst-wpp # 第三步:进入项目并启动本地调试,WPS 演示会自动打开加载项入口 cd catalyst-wpp && wpsjs runwpsjs create会生成一套标准项目结构,包括加载项清单、后台逻辑目录和界面目录;wpsjs run会把当前项目注册到本机 WPS,并在本地起一个调试服务。跑完第三步后,打开 WPS 演示,在开发工具或加载项入口里能找到这个尚未命名的面板。
有个细节值得先说:wpsjs run默认使用的调试端口是固定的,如果被其他进程占用,启动会失败或面板空白。遇到这种情况,在命令后面手动换端口:
wpsjs run --port 8899另外,Windows 下建议用管理员身份打开 WPS 再执行wpsjs run,否则加载项清单可能注册不进去,表现就是“命令执行成功但 WPS 里什么都没有”。
2.3 项目目录与“双端”结构:renderer 只管界面,业务逻辑单独放
一个 WPS 加载项项目,核心目录结构大致如下:
catalyst-wpp/ ├─ manifest.json // 加载项清单,声明权限、入口和名称 ├─ main/ // 后台逻辑,桥接 WPS 文档对象 │ └─ index.js └─ renderer/ // 插件界面,本地 HTML/JS/CSS ├─ index.html ├─ style.css └─ app.js双端结构是加载项最容易理解偏的地方。renderer是用户看到的界面,它本质是一个网页,里面的 JS 只能操作界面元素,不能直接访问 WPS 的演示文稿对象;真正和文档打交道的是main。renderer 通过桥接层向 main 发消息,main 拿到消息后访问Application、Presentation、Slides这些对象再把结果返回。
新手常见的困惑是:“我在界面脚本里明明能拿到一个wpp对象,为什么对文档做的修改没生效?”十有八九是拿错对象了——界面层只能用来发命令,不要在里面写遍历文档的逻辑。排查时先在 renderer 的控制台打印日志确认命令已发出,再去 main 侧确认收到命令并执行了哪一步,这样能快速定位问题是出在“没发出去”还是“没执行”。
3. 催化剂的一键统一字体:递归 Shape 才是完整替换
3.1 从界面到桥接:一个按钮触发后台命令
统一字体是催化剂最基础的命令。界面很简单:一个字体输入框,一个范围选择,一个执行按钮。renderer 侧的核心代码:
// renderer/app.js —— 界面层只负责收集参数并发消息 const fontInput = document.getElementById('fontName'); const rangeSelect = document.getElementById('range'); const runBtn = document.getElementById('runReplace'); runBtn.addEventListener('click', function () { const fontName = fontInput.value.trim(); if (!fontName) return; bridge.postMessage({ type: 'replaceFont', font: fontName, range: rangeSelect.value // 'page' 当前页 | 'all' 全部页 }); });bridge.postMessage是加载项脚手架注入的通信能力,不同版本的封装名可能不同,但本质都是异步消息。type字段是命令名,font和range是业务参数。这里有个习惯值得养成:所有参数在界面层做一次校验,空值、非法值不要发到 main,省得后台每次都要防一手。
3.2 递归函数与表格、组合图形的兜底处理
main 侧拿到消息后,真正执行替换逻辑的是这个递归函数:
// main/index.js —— 遍历 Shape 集合,递归替换字体 function replaceFontInShapes(shapes, fontName) { for (let i = 1; i <= shapes.Count; i++) { const shape = shapes.Item(i); // 普通文本框和有文字的图形 if (shape.HasTextFrame && shape.TextFrame.HasText) { shape.TextFrame.TextRange.Font.Name = fontName; } // 表格:文字在 Cell 里,不在 TextFrame if (shape.HasTable) { const table = shape.Table; for (let r = 1; r <= table.Rows.Count; r++) { for (let c = 1; c <= table.Columns.Count; c++) { table.Cell(r, c).TextRange.Font.Name = fontName; } } } // 组合图形:WPS 对象模型里 Type 为 6 表示组合 if (shape.Type === 6) { replaceFontInShapes(shape.GroupItems, fontName); } } }这段代码是催化剂里最重要的一个函数,它处理了三个容易被漏掉的场景。第一,HasTextFrame只判断图形是否带文本框,但空文本框也会返回真,所以要再加HasText判断,避免把空对象写一遍徒增耗时。第二,表格里的文字不在 TextFrame 上,要逐行逐列取Cell里的TextRange,这块最容易漏。第三,组合图形是重灾区——从模板复制来的标题栏、流程图、装饰元素,很多是组合对象,里面的子 Shape 要递归进去处理。
shape.Type === 6这个判断来自通用对象模型里组合图形的枚举值,WPS 做了兼容。如果担心当前版本枚举不一致,可以打印每个 shape 的 Type 值对比一次再定。
3.3 三个必调参数:范围、日志开关、字体名
光有递归还不够,还得控制“改哪些页”。催化剂的替换命令我一般提供三个参数:
| 参数 | 可选值 | 说明 |
|---|---|---|
| font | 任意已安装字体名 | 不存在的字体不会报错,但替换不生效,需提前确认 |
| range | page / all | 建议先只做当前页和全部页,选中页依赖版本,后面再说 |
| debug | true / false | 打印每页命中 Shape 数量,排查哪一页没生效 |
遍历页面的逻辑按范围走:
function replaceFontByRange(fontName, range, debug) { const pres = wpp.ActivePresentation; const slides = pres.Slides; const targets = []; if (range === 'page') { targets.push(pres.SlideShowWindow.View.Slide); } else if (range === 'all') { for (let i = 1; i <= slides.Count; i++) { targets.push(slides.Item(i)); } } targets.forEach(function (slide, index) { let count = 0; const shapes = slide.Shapes; for (let i = 1; i <= shapes.Count; i++) { count += replaceFontInShapes(shapes, fontName); } if (debug) { console.log('第 ' + (index + 1) + ' 页,处理 Shape 数量:' + count); } }); }这里要提醒一个很现实的坑:WPS 演示里“当前选中的多页”这个对象集合,在不同版本中的获取方式不稳定,有的版本拿不到干净的选中集合。所以催化剂的初期版本只做“当前页”和“全部页”,把“选中页”留到拿到用户实际版本后再补,否则光这一个功能就能消耗大量调试时间。
日志开关看起来不起眼,但排错时极其有用。批量处理 100 页文稿,某几页没生效,没有日志就只能盲猜;打开 debug 后哪页漏了、漏了多少个 Shape 一目了然。这也是催化剂区别于一次性脚本的地方——它把可观测性做成了默认能力。
4. 把操作串成流水线:用命令面板组合清理与统一
4.1 命令注册表:每个操作都是一等公民
催化剂不能只有一个改字体的功能,否则谈不上“催化剂”。我把所有操作集中注册到一张命令表里,界面自动读取这张表生成按钮,新增功能不用改界面:
// renderer/commands.js —— 命令注册表 const commands = { replaceFont: { name: '统一字体', params: ['font', 'range'], run: (ctx) => ctx.exec('replaceFont', ctx.args) }, cleanParagraph: { name: '清理多余空段', params: ['range'], run: (ctx) => ctx.exec('cleanParagraph', ctx.args) }, unifyLineSpacing: { name: '统一行距', params: ['lineSpacing', 'range'], run: (ctx) => ctx.exec('unifyLineSpacing', ctx.args) }, cascadeClean: { name: '清理+统一字体', params: ['font', 'lineSpacing', 'range'], run: (ctx) => ctx.exec('cascadeClean', ctx.args) } };ctx是执行上下文,封装了 WPS 对象模型的代理、当前文稿引用、日志工具和参数校验。每个命令只描述“需要什么参数、执行哪个后台动作”,至于参数从哪个下拉框来、界面长什么样,由面板自动生成。这样后续加一个“统一行距”只需要注册表加一项,再在 main 侧实现同名方法,界面零改动。
注册表模式还有个好处:组合命令变成了一件自然的事。cascadeClean就是一条流水线——先清理多余空段落,再做字体统一,最后统一行距。用户不必理解中间步骤,点一次按钮全部完成。
4.2 清理空段落:换行符归一化是第一步
“清理多余空段”听起来简单,实现时坑不少。核心逻辑是读每段文本、修正多余换行、再把文本写回去:
// main/index.js —— 清理文本框里连续空行和多余半角空格 function cleanParagraph(textRange) { let text = textRange.Text; const before = text.length; // 换行符归一化:不同版本可能返回 \r\n 或 \r text = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); // 连续 3 个以上换行压成 2 个(段与段之间留一个空行) text = text.replace(/\n{3,}/g, '\n\n'); // 去掉行首连续半角空格,全角空格保留 text = text.replace(/^ {2,}/gm, ''); if (text.length !== before) { textRange.Text = text; } }这里“保留一个空行”是有意的。很多演示文本是从网页或 Word 直接贴进来的,每段之间夹了两三个空行,读起来像流水账;但全删掉又会糊成一团。压成“段间最多一个空行”是多数场景里最舒服的中间态。
\r\n归一化这步不要省。不同版本 WPS 在TextRange.Text里返回的换行符不完全一致,直接写/\n{3,}/去匹配,遇到\r\n会匹配不到;先统一成\n再处理,正则逻辑才能稳定。
4.3 组合命令与快照还原:给用户一颗后悔药
批量操作最让人心里没底的,是改完发现“有些地方不对”却难以精准撤销。加载项的批量修改在部分版本里不会合并成一步撤销记录,用户连按 Ctrl+Z 可能直接退回操作前,甚至把之前的手工修改也一并撤销掉。催化剂的做法是在自己的内存里做一份快照,提供独立的一键还原:
// main/index.js —— 快照与还原 let snapshot = []; function captureFontSnapshot(presentation) { snapshot = []; const slides = presentation.Slides; for (let i = 1; i <= slides.Count; i++) { const shapes = slides.Item(i).Shapes; for (let j = 1; j <= shapes.Count; j++) { const shape = shapes.Item(j); if (shape.HasTextFrame && shape.TextFrame.HasText) { snapshot.push({ slideIndex: i, shapeIndex: j, font: shape.TextFrame.TextRange.Font.Name }); } } } } function restoreFontSnapshot() { const pres = wpp.ActivePresentation; snapshot.forEach(function (item) { try { const shape = pres.Slides.Item(item.slideIndex).Shapes.Item(item.shapeIndex); shape.TextFrame.TextRange.Font.Name = item.font; } catch (err) { // 索引失效时跳过,不中断整个还原流程 console.warn('还原跳过:', item.slideIndex, item.shapeIndex); } }); }这段代码里最值得注意的,是还原时没有盲目相信快照里的索引。用户可能在操作后删了页、移动了 Shape,索引早已失效;所以还原函数要包一层try-catch,失败的跳过并警告,而不是让整个还原流程崩溃。这种“记录变更前状态”的思路,是催化剂这类批量插件该有的底线设计——效果可以再优化,但不能让用户没有后悔药吃。
5. 催化剂插件避坑清单:加载失败、漏改与撤销失效的 5 个现场
做了几个版本之后,我把最常见的翻车现场整理成清单。每条都是“现象 → 原因 → 解决”的结构,碰到类似情况可以直接对照。
现场一:WPS 演示里找不到加载项入口。
现象:wpsjs run执行成功,但打开 WPS 演示,开发工具或加载项区域一片空白。
原因:大部分情况不是代码问题,而是 WPS 版本不支持加载项,或者wpsjs run注册清单时权限不足。Linux 桌面版和部分精简版默认没有加载项入口。
解决:先确认当前 WPS 版本是否有开发工具入口;Windows 下用管理员身份重启 WPS 再运行wpsjs run;入口确实不存在的版本就换官方个人版,开发调试用官方个人版即可覆盖大多数场景。
现场二:同一份文档,有的页面改了字体,有的纹丝不动。
现象:标题页、目录页全部替换成功,流程图、数据图表里的文字还是原字体。
原因:流程图大多是组合图形,数据图表的文字存在数据标签里,不在普通文本框的TextFrame分支上。只处理HasTextFrame的循环,天然漏掉这些对象。
解决:对每个 Shape 先判断类型,组合图形递归进入GroupItems处理;图表的数据标签不要强行用插件改,强行写入容易破坏数据标签格式,图表里的文字建议在数据源里统一处理。
现场三:表格里的文字漏改,页面正文却全变了。
现象:全文替换后表格内文字保持原样,用户以为插件坏了。
原因:表格不是文本框,文字存于Table对象的Cell里,要走行、列双循环取TextRange。
解决:在递归函数里增加HasTable分支,逐行列遍历。注意合并单元格时行列计数可能越界,遇到异常要try-catch并打印警告,让批量任务继续跑下去。
现场四:Linux 桌面版功能灰化,点击按钮无效果。
现象:在一些 Linux 桌面环境上跑通加载项,按钮能点,文档没有任何变化,控制台也不报错。
原因:Linux 版 WPS 的 JS API 覆盖度不齐,部分调用静默失败,不抛异常也不生效。
解决:开发基线放在 Windows 版,Linux 环境只做查看和轻量验证。在 Linux 上测试时先用单页范围试跑,开着 debug 日志确认实际命中数量,别直接在几百页的文档上撞南墙。
现场五:批量操作后无法撤销,还原命令还报“索引无效”。
现象:用户改完全稿想撤销,按 Ctrl+Z 直接退回操作前状态;用插件还原,又提示找不到 Shape。
原因:加载项的批量写入不一定合并成一步撤销记录;而快照里记录的一次性索引,在用户增删页面后立刻失效。
解决:操作前给用户明确提示,重要文档先手动备份;快照记录页索引和 Shape 索引即可,但还原时必须包try-catch跳过失效项;更稳妥的做法是记录对象在页面上的唯一标识,还原时重新搜索定位。
这五条覆盖了加载、遍历、表格、跨平台和撤销五个层面,基本就是做这类插件的五个主要风险区。多数功能“看起来做完了”和“真的在所有环境上跑完”之间的差距,就是靠这几条填上的。
6. 把“催化剂”做成能交付的项目:打包、分发与往后扩展
6.1 打包与自用分发
开发调试期用wpsjs run跑热加载很方便,但给同事用的版本不能一直挂着调试服务。执行wpsjs pack会把当前项目打成加载项产物,把产物复制到本机 WPS 的加载项目录即可完成安装。不同版本 WPS 的加载项目录位置略有差别,最可靠的办法是打开加载项管理界面查看“已安装位置”再确认路径。自用和小范围分发,只要不做商业分发,这个流程已经完全够用。
6.2 两个值得继续做的扩展方向
第一个方向是模板化排版。很多教研岗用户的演示里都有一段段算法伪代码或数学公式,贴进来后字体和缩进全乱。催化剂可以在命令表里加一条“套用代码块样式”,把选中文本一次性设置成等宽字体、统一缩进和段距,能省下大量手工排版时间。
第二个方向是导出前置检查。与其在插件里实现导出功能,不如在导出前自动跑一遍“统一字体+清理空段+快照”,把问题消灭在导出动作之前。WPS 自身导出 PDF 和图片的能力已经很成熟,插件做导出反而容易踩 API 覆盖不全的坑,这属于典型的“不该重复造轮子”。
6.3 收尾
这套方案我最初是用 VBA 写的,改完一轮就发现环境兼容性太折腾,后来换成加载项重写,才把边界真正摸清楚。回想起来最值得坚持的两个习惯:一是任何批量操作都先开单页试跑并打印命中数量,确认当前版本 API 真的生效再全稿执行;二是给用户留还原路径,批量修改没有后悔药就谈不上可靠。希望这些思路能帮你在自己的 WPS 演示插件上少走一段弯路。
本文还有配套的精品资源,点击获取