
很多人第一次搜 wpsjs脑子里想的是WPS 里那个写 JS 宏的地方,装完命令行工具之后却发现要敲 npm、要选模板、要起本地服务,完全不是一回事。这个误会我见过太多次,也解释过太多次,干脆一次性写清楚。wpsjs 是 WPS 官方开源的一套命令行工具链,用来开发和调试 WPS 加载项——也就是让你能在 WPS 文字、表格、演示里加一个自己的选项卡、按钮和面板,背后用 JavaScript 写逻辑。它和宏编辑器里写 JS是两条完全不同的路,解决的问题也不一样。这篇文章按我自己的上手顺序讲:先把两条路分清,再把环境搭起来,然后跑通第一个工程,接着把核心 API、功能区分发、以及我踩过的那些坑一个个说透。适合完全没接触过加载项开发、但会一点 JavaScript 的读者;如果你只想给自己做个小自动化,第一节能帮你省下好几天弯路。1. wpsjs 到底是什么先把宏和加载项两条路分清1.1 一个名字引发的误会WPS 里能写 JavaScript 的地方其实有两处,而且都被人叫过wpsjs。第一处是宏编辑器。在 WPS 表格里点工具 - 开发工具 - JS 宏编辑器,你就能看到一个跟 VBA 编辑器长得很像的界面,里面可以写 JavaScript,通过ActiveWorkbook、Range、Value2这类对象模型操作文档,写好的代码跟文档一起保存,发给同事,同事打开启用宏就能用。这条路的关键词是随文档走。第二处是加载项工程。这就是 wpsjs 这个命令行工具干的事:它在你的电脑上生成一套 HTML JavaScript 的前端工程,通过一个本地服务挂到 WPS 上,WPS 启动时把这个页面加载进去,于是你写的按钮就出现在功能区里了。这条路的关键词是随环境走。名字一样、API 风格也很像(都以 VBA 对象模型为蓝本),但生命周期、分发方式、界面能力完全不同。我自己刚开始就把这两件事混在一起想,结果在宏编辑器里找了半天怎么加按钮,白折腾。1.2 两条路的差异,用一张表说清维度WPS JS 宏宏编辑器WPS 加载项wpsjs 工程代码载体保存在宏文档里,如支持宏的工作簿格式独立工程目录,打包后安装/部署到 WPS入口方式打开文档、手动运行宏功能区选项卡、按钮、任务窗格、对话框界面能力基本没有,顶多弹提示框和输入框HTML/CSS 想怎么写就怎么写分发成本把文件发给每个使用者统一部署,可集中更新版本适合场景个人自用、一次性数据处理团队工具、对外交付的小产品运行环境WPS 内置 JS 引擎WPS 内嵌浏览器加载页面调试手段断点、逐语句、变量查看本地服务 日志,需要自己搭调试链路看到最后两行就知道为什么很多人最后会选加载项:只要你的工具需要给别人用、需要好看的界面、需要在发版后不回访每台电脑就能更新,宏就顶不住了。1.3 加载项到底跑在哪,能力边界在哪有个细节必须提前说清楚,否则后面写代码会一直别扭:加载项的逻辑运行在 WPS 内嵌的浏览器环境里,它既能用document、window这些前端 API,又能通过一个全局对象访问 WPS 的文档对象模型。也就是说,你是在一个网页里操作文档。这个设定带来几个直接后果。第一,你可以放心引入前端生态里的东西,日期选择器、图表库、表格组件都能用。第二,页面和文档是两套世界,页面刷新不会影响文档内容,但文档里发生的事也不会自动跑到你页面上,想联动就得靠事件监听。第三,一些传统桌面上很方便的能力(比如直接读本地文件路径)在加载项里会受限制,需要走专门的辅助对象,或者干脆让用户手动选文件。我一般会这样判断一个需求该不该做成加载项:如果它需要重复使用、需要界面、需要给别人装,就做加载项;如果只是这个表我今晚处理一下,打开宏编辑器十分钟写完收工。2. 环境准备Node、npm 与 wpsjs 的安装细节2.1 Node 版本这件事,别追新wpsjs 是通过 npm 全局安装的命令行工具,所以第一步是装 Node.js。这里是第一个坑:不要盲目装最新版。我本机的经验是 14.x 或 16.x 的 LTS 版本最省心,18 以上偶尔会在装依赖时报一些看起来莫名其妙的错。原因不难理解:这类脚手架工具内部会依赖一批构建期工具链,而这些依赖对 Node 版本比较敏感。追新版本本身没错,但你是来学加载项开发的,不是来给构建工具做兼容性测试的。真遇到安装报错,别急着怀疑自己,先用版本管理工具切回 16 再试一次,大概率就好了。装完打开终端验证:node -v npm -v两个命令都能打印出版本号,说明环境通了。如果提示不是内部或外部命令,基本是安装时没勾选加入环境变量,重新装一遍并勾上即可。2.2 换源与全局安装国内直连官方源装全局包,速度会很折磨人。建议先换源:npm config set registry https://registry.npmmirror.com npm config get registry第二条命令会打印当前源地址,确认一下改成功了。然后全局安装:npm install -g wpsjs wpsjs -hwpsjs -h会列出所有子命令。我印象里常用的有create、debug、build、publish这几个,具体命令名、参数和默认行为请以你本机wpsjs -h的输出为准,官方版本迭代时会调整细节,不要背命令,养成先看一眼帮助的习惯。如果全局安装报权限错误,Windows 上通常是没以管理员身份运行终端,或者 npm 的全局目录不在可写路径下。Linux 和 macOS 上更常见的是 prefix 权限问题,按报错提示处理就行,不要用sudo硬装,后面会埋下更麻烦的权限隐患。2.3 WPS 端的准备工具装好了,还得让 WPS 这边配合。几个点提前确认:版本:加载项能力需要相对较新的 WPS 版本,老版本可能根本没有加载项入口。不确定的话先把 WPS 升到当前较新的版本。登录状态:部分加载项能力与账号体系相关,开发调试时保持登录能少踩一些坑。开发者相关设置:WPS 的设置里通常有与加载项、开发者模式相关的开关,调试前建议确认没有被关闭。这一步没有代码,但特别容易卡人。我见过不止一位同学,前端工程跑得好好的,就是 WPS 里死活看不到选项卡,最后发现是版本太老。3. 第一个加载项从 wpsjs create 到 wpsjs debug 跑通3.1 创建项目与模板选择找一个纯英文、无空格的目录,这一点很重要,后面会解释原因。然后执行:mkdir wps-demo cd wps-demo wpsjs create hello-wps命令会以交互方式问你几个问题,通常是:给哪个组件做加载项(文字、表格还是演示)、要空白模板还是示例模板。第一次上手,直接选示例模板。空白模板干净是干净,但你要自己配功能区、自己写入口,对新手不友好;示例模板已经把该有的骨架都摆好了,改两行就能看到效果,学习效率完全不是一个量级。创建完成后进入项目目录,你会看到一堆文件。别急着写代码,先花十分钟把结构看明白,这十分钟能省你后面两小时。3.2 目录结构逐层拆解不同版本的模板细节会有差异,但核心就这几块:hello-wps/ ├── js/ │ ├── main.js # 主逻辑入口 │ ├── ribbon.js # 功能区回调 │ └── util.js # 工具函数 ├── ui/ │ ├── dialog.html # 对话框页面 │ └── taskpane.html # 任务窗格页面 ├── ribbon.xml # 功能区按钮定义 ├── index.html # 宿主页面 └── package.json逐层理解:ribbon.xml决定你在 WPS 功能区上看到什么。你想加一个我的工具选项卡,就在这个文件里写一个 tab;想加按钮,就写 button。这里的每个控件都有一个 id,后面 JS 里靠这个 id 区分是谁被点了。index.html是那个看不见的宿主页面。加载项启动时 WPS 会加载它,它主要负责把js/main.js引进来,通常不显示任何东西。js/main.js是你的主战场,初始化逻辑、事件注册都写在这。js/ribbon.js专门放功能区回调,按钮被点时先走到这里。ui/下面放真正给用户看的页面。理解这套结构的关键,是记住功能区描述和业务逻辑是分离的。ribbon.xml 只是画按钮,按钮点了之后干什么,全在 JS 里。这个分离设计很好,但新手容易在 ribbon.xml 里找逻辑,找不到就懵。3.3 debug 是怎么跑起来的执行:wpsjs debug这个命令做了两件事。第一,在本地起一个 HTTP 服务,把你的工程目录作为静态资源暴露出去,控制台会打印出服务地址和端口。第二,把加载项以调试模式注册到 WPS,指向刚才那个本地地址,然后拉起 WPS。WPS 启动后,你应该能在功能区看到模板自带的那张选项卡。点一下示例按钮,弹出一个提示框——恭喜,链路通了。这里的原理值得多说一句:因为 WPS 加载的是http://127.0.0.1:端口/...这样一个页面,每次改完代码刷新加载项就能生效,不需要你手动拷贝文件。这就是加载项开发体验比宏好得多的地方。有一点要注意:调试模式下,加载项的注册信息会写在 WPS 的加载项配置目录里(常见位置在用户配置目录下的 jsaddons 一类文件夹中,具体路径因版本和系统而异)。如果你手动改过这些配置,后面看到奇怪的现象,先怀疑是这里的残留。3.4 亲手改一行,确认它真的生效别满足于能弹出提示框。改一行代码验证热更新:// js/ribbon.js function OnAction(control) { const eleId control.Id; switch (eleId) { case btnHello: alert(第一个 wpsjs 加载项运行成功); break; } }把提示文字改掉,回到 WPS 重新触发一次调试(或按当前版本的刷新方式重载),看提示是不是变成了新内容。这一步是在确认改代码 - 看效果这条反馈闭环是通的。如果这一步不通,后面所有工作都是盲写,必须先把它解决掉。4. 核心 API 上手把文档对象模型用起来4.1 三种应用对象,别拿错加载项里访问文档,第一步是拿到应用对象。三个组件对应三个入口:const wpsApp wps.WpsApplication(); // 文字 const etApp wps.EtApplication(); // 表格 const wppApp wps.WppApplication(); // 演示拿到之后,后面的写法和 VBA 对象模型几乎是一致的:表格里有工作簿、工作表、区域;文字里有文档、段落、选区;演示里有演示文稿、幻灯片、形状。如果你以前写过 VBA,基本可以平移过来。这里有个新手最容易犯的错:给表格做的加载项,代码里却用了文字的应用对象。表现是代码不报语法错,但一执行就说对象不存在或者属性为空。排查时先确认三件事:你调试时打开的是哪个组件、你取的是哪个应用对象、你的控件是不是挂在对应组件的那张选项卡上。4.2 表格场景读写单元格和数组批量操作单格读写最直观:const app wps.EtApplication(); const sheet app.ActiveWorkbook.ActiveSheet; sheet.Range(A1).Value2 编号; sheet.Range(A1).Font.Bold true;批量读是加载项在表格场景里最常用的能力,也是性能分水岭:const data sheet.Range(A2:D1000).Value2; // 一次性读成二维数组 let sum 0; for (let r 1; r data.length; r) { sum Number(data[r][3]) || 0; } sheet.Range(F1).Value2 合计; sheet.Range(F2).Value2 sum;注意:区域返回的二维数组,下标习惯上是从 1 开始的,和 JavaScript 原生数组从 0 开始不一样。第一次用很容易越界或者漏掉第一行。我自己的习惯是先打印数组长度确认一下结构,再写循环。批量写同理,不要在循环里一格一格赋值。一千行数据用单格赋值可能要几十秒,组装好二维数组一次性写回,通常一秒以内。这个差距在实际项目里非常明显,尤其是数据量上去之后。4.3 文字场景段落、样式与内容写入文字这边我最常用的模式是往文档里追加内容:const app wps.WpsApplication(); const doc app.ActiveDocument; doc.Range(0, 0).Text 这是通过加载项写入的一段文字\n; const para doc.Paragraphs.Item(doc.Paragraphs.Count); para.Range.Font.Bold true; para.Range.Font.Size 14;思路是:先用范围的 Text 属性写内容,再从段落集合里把刚写进去的那段捞出来单独设样式。之所以不一步到位,是因为不同版本里某些属性组合的行为不完全一致,分两步写更稳、也更好调试。演示组件也是同一套逻辑:const app wps.WppApplication(); const pres app.ActivePresentation; const slide pres.Slides.Add(pres.Slides.Count 1, 12); // 版式常量按文档取值 slide.Shapes.Item(1).TextFrame.TextRange.Text 今日汇报;版式、颜色、对齐这些枚举常量,请以对象浏览器或官方文档为准。常量名和数值在不同组件里差别不小,凭记忆写基本会翻车,我现在的做法是把常用常量整理成一个小抄放在项目里。4.4 事件监听让加载项从按钮工具变成活的助手只会响应按钮点击的加载项,本质就是个外挂计算器。真正的效率提升来自事件。加载项体系里提供了事件注册入口:wps.ApiEvent.AddApiEventListener(WorkbookOpen, (data) { // 打开工作簿时做点什么 });事件能干什么?举几个我实际用过的场景:用户打开某个类型的表格时自动检查表头完整性;用户切换选区时在任务窗格里同步显示当前选中行的摘要;文档内容变化时自动更新面板上的统计数字。注意:事件名、回调参数结构在不同组件和版本里不完全相同,不要照着博客硬抄,先去文档里确认。另外,事件回调里不要做重活。它是在用户操作过程中同步触发的,你在里面跑一个几千行的循环,用户会明显感到卡顿。正确做法是回调里只做轻量判断和标记,重活丢到按钮或者定时逻辑里执行。还有一个常被忽略的点:注册了事件,就要考虑什么时候注销。长时间挂着一堆监听,项目复杂之后很容易出现链条断了但还在触发的诡异现象。4.5 功能区和界面ribbon.xml 加 HTML 的组合拳功能区定义大概长这样:customUI xmlnshttp://schemas.microsoft.com/office/2006/01/customui onLoadOnAddinLoad ribbon startFromScratchfalse tabs tab idmyTab label我的工具 group idgrpData label数据处理 button idbtnClean label清洗表头 sizelarge onActionOnAction/ button idbtnExport label导出报告 sizelarge onActionOnAction/ /group /tab /tabs /ribbon /customUI对应的 JS 侧:function OnAddinLoad(ribbonUI) { if (typeof wps.ribbonUI ! object) { wps.ribbonUI ribbonUI; // 存起来,后面动态改按钮状态要用 } return true; }把 ribbonUI 存下来这一步很关键。存了之后,你才能动态禁用按钮、改按钮文字、更新图标。比如某个按钮在当前文档状态下不该点,就把它的 enabled 置为 false。不存的话,你只能眼睁睁看着用户点了不该点的按钮。界面部分,加载项提供了弹出对话框和任务窗格两种方式:wps.ShowDialog(ui/dialog.html, 选择参数, 480 * window.devicePixelRatio, 320 * window.devicePixelRatio, false);任务窗格更适合边看边操作的场景,对话框适合填完就走的场景。选哪个不看技术难度,看用户的工作节奏:需要反复对照文档内容的,用任务窗格;一次配置长期生效的,用对话框。5. 分发落地构建、打包与部署5.1 build 产物里有什么开发跑通之后:wpsjs build构建会产出一个专门用于分发的目录,里面是压缩整理过的工程文件。开发目录里有源码、有备份文件、有一堆调试用的东西,分发目录里应该只有运行必需的资源。养成一个习惯:任何临时文件、测试脚本、本地路径配置,都不要出现在分发产物里。5.2 部署有哪几条路分发方式取决于你的使用规模:场景做法适合谁自己用保留调试模式,或者本地安装个人小团队打包后发给同事手动安装十来个人的小组组织内统一通过加载项管理渠道集中部署有 IT 管理的团队对外交付按平台要求打包并提供安装说明产品化交付这里有个必须注意的细节:加载项包的格式和安装方式,各版本要求不完全一致,以官方最新文档为准。我见过有人拿着两年前的教程打包,格式对不上,折腾了一下午。分发这件事上,文档比经验可靠。5.3 更新与缓存加载项最大的优势就是可更新:改了代码重新构建、重新部署,用户那边重启 WPS 就拿到新版本,不用挨个发文件。但缓存也会带来反效果。用户说我这边还是老界面,九成是缓存没刷新。处理顺序一般是:确认部署的是新版包、让用户重启 WPS、再不行就清理加载项缓存目录。把这几步写进你的交付说明里,能省掉大量重复沟通。6. 踩坑记录那些让我折腾半天的细节6.1 端口和服务起不来wpsjs debug有时会卡在启动阶段。常见原因按概率排序:端口被占用(上次的服务没退干净)、防火墙拦截了本地服务、杀毒软件把 Node 进程拦了。处理逻辑很简单:先看控制台输出的端口号,用系统命令查这个端口被谁占了,杀掉那个进程再重试。如果是防火墙或安全软件,加个白名单。不要用重启电脑大法跳过分析,因为这个问题会反复出现,你得知道原因。6.2 改了代码不生效这个问题我在不同阶段遇到过三次,原因完全不同:第一次是服务没重启,改的是构建前的源码,加载的还是旧文件;第二次是浏览器缓存,加了强制刷新参数才拿到新资源;第三次最隐蔽,是我改错了目录,一直在改一个没被引用的旧文件。排查顺序建议固定下来:先确认服务在跑,再确认改的文件被 index.html 引用了,最后清缓存重试。固定顺序的好处是,三次之内必然定位,不会东一榔头西一棒子。6.3 中文路径和空格路径这是我在 3.1 特意强调过的。工程路径里有中文或空格,某些构建环节会解析异常,报错信息还特别含糊,看起来像是依赖坏了。解决办法就一句话:开发目录用纯英文短路径,比如放在盘符根目录下的一个英文文件夹里。6.4 报错信息的正确读法加载项里的报错有时候不太友好,栈信息可能指向一个压缩过的文件。我的做法是:开发阶段始终跑调试模式(不压缩),把console输出和错误信息打印出来,同时在关键节点包上 try/catch,把上下文信息一起打出来。try { const sheet wps.EtApplication().ActiveWorkbook.ActiveSheet; // ...业务逻辑 } catch (e) { console.log(出错了:, e e.message ? e.message : e); }记住一点:在加载项环境里,没反应和报错是两回事。没反应往往是对象为空、静默返回;报错才是真异常。所以刚开始我会习惯性地把关键对象的类型和值都打一遍,确认拿到的不是空壳。6.5 性能与体验上的几个小技巧减少跨层调用次数。每次通过 JS 访问文档对象模型都有开销,能在 JS 侧算完的,就别反复跨过去读。批量读写代替循环单格。这个前面说过,收益最大。界面别做花活。任务窗格里的动画、大图、复杂图表,都会拖慢整体感受。工具的界面目标是清晰,不是炫。给耗时操作加反馈。超过一秒的操作,界面上要有个进度或者至少禁用按钮,否则用户会连点三次,然后你的逻辑跑三遍。6.6 最后分享两个我常用的判断第一,如果你发现某个需求在加载项里绕来绕去都做不顺,先退回去想想它是不是本来就该用宏,或者干脆在表格里用公式解决。技术选型的错误,靠努力是补不回来的。第二,保持一个最小可用版本的习惯。加载项很容易被做成一艘航空母舰,功能越堆越多,某天改一个按钮牵动三个模块。我现在的做法是:先做只有一个按钮、只干一件事的版本,跑通、发出去、收集反馈,再决定下一步加什么。这个节奏比一次性设计完美架构,靠谱得多。