无尘阁日记

无尘阁日记

我想用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.html、dialog.html 分开的。相关文章也描述了点击按钮后分别生成 dialog.html 和 taskpane.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 多入口配置示例”
我会按你这个场景直接写到可以开工。