无尘阁日记

无尘阁日记

我想用wpsjs对wps进行插件开发,并使用vue,那么问题来了,多个taskpane与多个dialog之间是否符合vue的单面应用,还是它们是多页的
2026-03-23

结论先说:

在 WPS JS 插件开发里,多个 taskpane 和多个 dialog,整体上更接近“多页面 / 多 WebView 实例”,而不是浏览器里那种严格意义上的“一个 Vue 单页应用统一接管所有界面”。
每一个 taskpane 页面、每一个 dialog 页面内部,你都完全可以各自使用 Vue SPA。也就是说:

单个窗体内部可以是 SPA;多个窗体之间本质上是多个独立页面实例。 (WPS开放平台)


一、为什么我说它更像多页,而不是一个大 SPA

WPS 官方对加载项的描述里,有两个很关键的信息:

第一,WPS 加载项本质上是基于 Web 技术的,每个加载项对应打开了一个网页
第二,taskpane 和 dialog 也都是展示网页内容:task pane 是通过 CreateTaskPane 创建并指定网页路径;dialog 是通过 ShowDialog 根据 URL 创建,内容也是一个 web 页面。(WPS开放平台)

这几句话其实已经把架构本质说透了:

1. taskpane 是一个网页

官方文档摘要写得很明确:调用 CreateTaskPane 时,参数就是 web page path,也就是网页路径。(WPS开放平台)

2. dialog 也是一个网页

官方文档摘要同样明确:ShowDialog 是根据给定 URL 创建对话框,而对话框里的内容是一个 web page。(WPS开放平台)

3. 所以它们不是 Vue Router 在一个浏览器 tab 里切视图

而是更像:

  • 打开一个 taskpane.html

  • 再打开一个 dialog.html

  • 再打开另一个 taskpane2.html

这些页面各自独立加载,各自有自己的 JS 上下文、组件树、生命周期。
因此从前端架构上看,它们更接近多页应用 MPA,只是页面容器不是普通浏览器 tab,而是 WPS 宿主里的不同窗格/对话框。(WPS开放平台)


二、那 Vue 的“单页应用”还能不能用

能用,而且非常适合。

但要分清楚“单页应用”的作用范围。

1. 单个 taskpane 内部:可以是标准 Vue SPA

例如你的 taskpane.html 挂一个 #app,里面:

  • Vue Router

  • Pinia

  • 组件拆分

  • 页面切换

  • 状态管理

这些都没问题。

你可以把一个 taskpane 做成:

  • 首页

  • 设置页

  • 历史记录页

  • 结果页

这些都由一个 Vue SPA 接管。

2. 单个 dialog 内部:也可以是一个 Vue SPA

比如一个弹窗里放:

  • 登录页

  • 授权页

  • 选择器

  • 预览页

  • 确认页

这也完全可以用 Vue 来做。

3. 但“多个 taskpane + 多个 dialog”加在一起,不是同一个 SPA

原因很简单:

它们不是在同一个 DOM、同一个 JS 运行时里切路由,
而是多个网页实例
每打开一个 taskpane / dialog,通常就相当于新建了一个独立前端运行环境。这个边界不是 Vue Router 能跨过去的。(WPS开放平台)


三、最通俗的理解

你可以这样想:

浏览器里的标准 SPA

像一个大型商场,只有一个大门。
你进去以后,去 A 区、B 区、C 区,都是商场内部走动。
这就是 Vue Router 切页面。

WPS 里的多个 taskpane / dialog

更像是:

  • 一个侧边办公室

  • 一个弹出会议室

  • 另一个浮动小房间

它们都属于同一栋楼,但不是同一间屋子
每间屋子都可以自己装修成 Vue SPA,
但几间屋子合在一起,不是一个 SPA。


四、从技术上看,它们之间为什么不是一个 Vue 单页应用

核心有四个原因。

1. 入口文件不同

很多 WPS 加载项示例里,本身就是 taskpane.htmldialog.html 分开的。相关文章也描述了点击按钮后分别生成 dialog.htmltaskpane.html 页面。(效率符号)

这意味着你天然就是多个页面入口。

2. 生命周期不同

taskpane 可能常驻在侧边。
dialog 往往是打开、关闭、销毁。
两者生命周期并不统一。

3. JS 上下文不同

一个 dialog 里的 window,通常不是 taskpane 里的那个 window
所以:

  • 组件实例不共享

  • 内存状态不共享

  • router 不共享

  • Pinia store 默认也不共享

4. 路由机制也不是同一套路由树

你不能指望 taskpane 里的 /setting 直接切成 dialog 里的 /login,因为那不是一个页面上下文。


五、那到底该按“单页”来做,还是按“多页”来做

正确答案是:

WPS 插件整体按“多页面容器架构”设计;每个容器内部按“Vue SPA”设计。

这是最稳、最符合实际的方式。


六、推荐的架构思路

下面这套是最适合你现在这个场景的。

方案一:多个独立页面入口,各自挂 Vue

这是最常规、最清晰的。

例如:

src/
  taskpane-main/
    main.ts
    App.vue
  taskpane-settings/
    main.ts
    App.vue
  dialog-login/
    main.ts
    App.vue
  dialog-picker/
    main.ts
    App.vue

对应打包出:

  • taskpane-main.html

  • taskpane-settings.html

  • dialog-login.html

  • dialog-picker.html

这个方案的优点

结构清晰

哪个页面干什么,一眼就懂。

生命周期清楚

dialog 销毁就销毁,不会影响 taskpane。

容易排查问题

WPS 这种宿主环境里,清晰往往比“极致统一”更重要。

适合场景

  • 业务界面较多

  • taskpane 和 dialog 职责不同

  • 你准备长期维护


方案二:同一个 HTML 入口,不同参数决定渲染哪个 Vue 页面

例如只有一个 app.html,然后:

  • taskpane 打开 app.html?scene=taskpaneMain

  • dialog 打开 app.html?scene=loginDialog

再由 Vue 根据 query 参数决定挂哪个根组件。

这个方案的本质

看起来统一,但注意:

仍然不是一个真正共享运行时的 SPA。
只是多个独立页面实例,恰好都加载了同一个入口文件。

优点

代码复用高

入口统一,公共逻辑容易抽。

维护一套基础设施

比如统一:

  • axios

  • UI 组件

  • i18n

  • 工具函数

  • 权限判断

缺点

容易把边界搞乱

你会误以为自己做成了“一个 SPA”,但其实实例仍然是分开的。

后期排查问题时更绕

尤其是 dialog 多起来以后。

适合场景

  • 页面不多

  • 团队前端能力强

  • 你特别强调代码统一


七、多个 taskpane / dialog 之间如何共享状态

这恰恰是你最需要提前想清楚的。

因为既然它们不是同一个 SPA,那默认就不能直接共享 Vue 状态

你可以考虑以下几种方式。

1. 共享“业务数据”,不要共享“Vue 内存状态”

这是第一原则。

比如不要想着:

“taskpane 里的 pinia state 直接给 dialog 用”

更稳的是:

  • 把当前文档信息写入本地存储

  • 把当前用户选择结果写入宿主可读位置

  • 或者通过接口拿最新数据

也就是说,共享数据源,不共享组件内存。


2. 用本地存储做轻量同步

如果宿主 WebView 支持正常的 Web Storage,可以用:

  • localStorage

  • sessionStorage

适合存:

  • 当前 token

  • 当前用户配置

  • 最近一次操作参数

  • 主题色/语言

但不要把它当成实时总线。


3. 用 URL 参数传递初始化信息

这个在 dialog 特别常见。

例如打开 dialog 时带上:

dialog.html?docId=123&type=seal&mode=preview

dialog 初始化时再读参数。

这很稳,也很符合“独立页面实例”的思路。


4. 用后端接口做真正的数据中心

如果插件业务稍微复杂一点,这是最靠谱的。

taskpane 改了数据,写到服务端。
dialog 打开时再从服务端取。
这样不会被前端页面实例边界困住。


5. 如果 WPS 提供页面间通信能力,也尽量只做事件通知

比如通知:

  • “已完成登录”

  • “已选择某模板”

  • “请刷新列表”

而不要直接耦合成“一个页面操纵另一个页面内部组件”。


八、开发时最容易踩的坑

1. 误把多个 WebView 当成一个 Vue 应用

这是最大坑。

表现为:

  • 想共用 router

  • 想共用 pinia 实例

  • 想直接调用对方组件方法

  • 想直接访问另一个页面的 DOM

这些思路都不稳。


2. 把 dialog 当成 taskpane 的“子组件”

从视觉上它像“子弹窗”,
但从技术上它更像“另开了一个网页容器”。

所以别按组件通信思路设计,要按“页面通信”思路设计。


3. 入口过度合并

一开始为了“优雅”,把所有页面都塞成一个超级入口。
后面你会发现:

  • 打包配置复杂

  • 场景判断越来越乱

  • 初始化逻辑越来越重

  • 宿主兼容问题更难排查


九、我给你的明确建议

如果你准备用 WPSJS + Vue 正经做插件,我建议你这样设计:

1. 整体认知上,按“多页”理解

也就是:

  • 多个 taskpane

  • 多个 dialog

本质上是多个页面实例,不要强行理解成一个总 SPA。

2. 单个 taskpane / dialog 内部,按“单页”实现

即:

  • 一个 taskpane = 一个 Vue SPA

  • 一个 dialog = 一个 Vue SPA

3. 公共部分抽成共享层

例如:

  • components/

  • composables/

  • utils/

  • api/

  • store-factory/

  • styles/

这样既保留了多页边界,又不浪费代码复用。

4. 页面之间靠“参数 + 存储 + 接口”通信

不要靠“同一个 Vue 实例”的幻想来设计。


十、给你一个最实战的项目结构建议

推荐目录结构

src/
  shared/
    api/
    components/
    composables/
    utils/
    styles/
  pages/
    taskpane-home/
      main.ts
      App.vue
    taskpane-settings/
      main.ts
      App.vue
    dialog-login/
      main.ts
      App.vue
    dialog-picker/
      main.ts
      App.vue

这样做的好处

对 WPS 宿主模型友好

因为 taskpane / dialog 本来就是网页入口。

对 Vue 也友好

每个页面都可以是完整 Vue 应用。

后期扩展不痛苦

你新增一个 dialog,只是新增一个页面入口,不会污染全局。


十一、一句话判断法

你以后只要记住这句就行:

在 WPS 插件里,taskpane 和 dialog 是“页面级容器”;Vue SPA 应该运行在每个容器内部,而不是跨多个容器做成一个总 SPA。


十二、最后给你一个最简短答案

你的问题如果只要一句话:

多个 taskpane 与多个 dialog,不应当理解成一个统一的 Vue 单页应用;它们更接近多个独立页面实例。你可以让每个 taskpane / dialog 各自用 Vue SPA,但它们彼此之间总体上属于多页架构。 (WPS开放平台)

接下来你要是愿意,我可以直接给你出一版:

“WPSJS + Vue 的推荐工程目录结构 + Vite 多入口配置示例”
我会按你这个场景直接写到可以开工。