开发桌面壁纸
在 Widget.js 中,桌面壁纸是组件包里的一个专用网页。你负责页面、动画和数据;宿主负责把页面放到 Windows 桌面图标下面,并管理启动恢复、关闭和异常重试。
目前只支持 Windows 主屏,一次运行一张壁纸。壁纸接收鼠标移动和左键事件,不接收右键、滚轮或键盘,不获得输入焦点。需要输入文字的功能应放到独立设置窗口。
准备项目
可以在已有 Vite 组件项目中添加壁纸,也可以先按创建项目建立组件包。React、Vue 或普通 TypeScript 页面都可以,下面用 React 演示最小接入。
需要包含 WallpaperWidget、WallpaperApi 的 core SDK,以及支持壁纸的 Widget.js 宿主。不要仅根据 npm 的 latest 判断这些接口是否已发布:工作区开发可以使用 "@widget-js/core": "workspace:*",先构建本地 core;独立项目需使用实际包含这些导出的发布版本。
项目需要 @widget-js/vite-plugin-widget。在现有 React Vite 配置中加入插件:
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import widget from '@widget-js/vite-plugin-widget'
export default defineConfig({
base: './',
plugins: [react(), widget()],
})插件生成 widget.json,开发时向本地 Widget.js 注册组件包。先启动宿主,再启动开发服务,查看注册结果。相对资源路径有助于 ZIP 离线加载;不要把开发服务器绝对地址写进页面资源。
声明专用壁纸
在项目根目录创建或扩展 widget.package.ts:
import { WallpaperWidget, WidgetPackage } from '@widget-js/core'
export const wallpaper = new WallpaperWidget({
name: 'cn.example.wallpaper.ripple',
lang: 'zh-CN',
title: { 'zh-CN': '鼠标互动壁纸' },
description: { 'zh-CN': '随鼠标移动的桌面场景' },
path: '/wallpaper',
previewImage: 'preview.png',
keywords: [],
configPage: {
path: '/settings',
width: 720,
height: 780,
minWidth: 380,
minHeight: 560,
resizable: true,
frame: true,
titleBarStyle: 'hidden',
transparent: false,
},
})
export default new WidgetPackage({
name: 'cn.example.wallpaper',
author: '你的名字',
title: { 'zh-CN': '互动壁纸包' },
description: { 'zh-CN': '我的网页壁纸' },
entry: 'index.html',
hash: true,
widgets: [wallpaper],
})将预览图放到 public/preview.png。没有设置页面时省略 configPage。WallpaperWidget 自动指定专用部署模式,不需要网格宽高,也不会显示普通组件的“桌面”“悬浮”按钮。
页面路由与就绪
声明中的 path 必须能被前端路由识别。使用 hash 路由,让离线包也能打开 index.html#/wallpaper 和 index.html#/settings。不要给壁纸页面套用依赖普通组件 IPC 的初始化逻辑。
下面是一个没有外部资源的最小页面。它安装鼠标监听器后才报告就绪,离开页面时清理监听器;普通浏览器也可预览。
// src/WallpaperPage.tsx
import { useEffect, useRef } from 'react'
import { ElectronUtils, WallpaperApi } from '@widget-js/core'
export function WallpaperPage() {
const dot = useRef<HTMLDivElement>(null)
const sent = useRef(false)
useEffect(() => {
const move = (event: MouseEvent) => {
if (dot.current) {
dot.current.style.transform = `translate(${event.clientX}px, ${event.clientY}px)`
}
}
window.addEventListener('mousemove', move)
if (ElectronUtils.hasElectronApi() && !sent.current) {
sent.current = true
void WallpaperApi.ready().catch(console.error)
}
return () => window.removeEventListener('mousemove', move)
}, [])
return <main style={{ position: 'fixed', inset: 0, background: '#102532' }}>
<div ref={dot} style={{ position: 'absolute', width: 24, height: 24,
borderRadius: '50%', background: '#78e4d0', pointerEvents: 'none' }} />
</main>
}例如已有 react-router-dom 时,在入口使用:
<HashRouter>
<Routes>
<Route path="/wallpaper" element={<WallpaperPage />} />
<Route path="/settings" element={<SettingsPage />} />
</Routes>
</HashRouter>HashRouter、Routes、Route 从 react-router-dom 导入,两个页面从项目自己的文件导入;SettingsPage 是你实现的设置表单。
有图片、视频或模型时,应等必要资源可用后再调用 ready(),不能仅以组件已挂载作为完成。资源失败时可以显示已加载的降级背景后报告就绪;无法展示内容则不要报告成功。页面 ready 加上原生桌面挂载成功后才算启用完成,整个准备阶段最多 30 秒,每次恢复都会重新计时。
设置页面
壁纸里的按钮可以通过 WallpaperApi.openConfigPage() 打开已声明的设置页面。设置窗口可以正常输入、滚动,并与壁纸使用同一组件包的持久化分区。卡片在组件可配置时也会显示“设置”。
// 壁纸里的设置按钮
await WallpaperApi.openConfigPage()
// 独立设置页保存
localStorage.setItem('my-wallpaper-settings', JSON.stringify({ color: '#78e4d0' }))
// 壁纸监听另一窗口的修改;卸载时移除监听器
const onStorage = (event: StorageEvent) => {
if (event.key === 'my-wallpaper-settings') {
// 校验 JSON 后更新画面;首次加载也要主动读取一次。
}
}
window.addEventListener('storage', onStorage)
// window.removeEventListener('storage', onStorage)相同分区仍要求相同网页来源才能共享 localStorage。浏览器预览与 Electron 不共享数据,localhost 与 127.0.0.1 也不是同一来源。
优先使用 configPage,不要同时依赖旧 configPagePath 查询参数。两者并存时优先 configPage.path,窗口选项直接来自 configPage。titleBarStyle: 'hidden' 用于隐藏原生标题栏;页面需要自行提供可拖动区域及窗口控制按钮。修改窗口尺寸、边框或标题栏后,等待组件包重新注册,关闭旧设置窗口再打开,已有窗口不会自动重建。
环境变化与性能
宿主提供 locked、suspended、displayOff 和 fullscreen 通知,不强制暂停或静音。页面自己决定降低帧率、暂停视频或停止动画;恢复时重新读取当前时间,避免收入、时钟等内容停留在旧值。
let receivedEvent = false
const apply = (state: { locked: boolean; suspended: boolean; displayOff: boolean; fullscreen: boolean }) => {
const shouldPause = state.locked || state.suspended || state.displayOff || state.fullscreen
// 根据 shouldPause 控制自己的动画和音频。
}
const unsubscribe = WallpaperApi.onEnvironmentChanged(state => {
receivedEvent = true
apply(state)
})
const initial = await WallpaperApi.getEnvironment()
if (!receivedEvent) apply(initial)
// 页面卸载时调用 unsubscribe();异步初始化也应检查页面是否已卸载。使用 requestAnimationFrame 驱动动画,闲置时停止无用绘制。使用当前窗口尺寸布局,并处理 resize;不要硬编码主屏分辨率。主屏切换时宿主尽量调整同一窗口保留页面状态,异常恢复重建页面时则需要从持久化存储恢复业务数据。
调试、打包与启用
- 启动支持壁纸的 Widget.js,再运行项目的开发脚本,确认插件报告组件包注册成功。
- 在组件卡片点击“壁纸”。加载完成后出现“关闭”,运行中可用 DevTools 检查控制台、网络和资源加载。可配置的组件显示“设置”。
- 使用项目构建脚本生成包含
widget.json、index.html、预览图和所有资源的离线包。标准插件支持 ZIP 生成;不要只打包 HTML,也不要把额外父目录包在 ZIP 根层。 - 通过管理端
WidgetPackageApi.install(zipPath)安装 ZIP,再在卡片启用。壁纸页自身无权部署、关闭或打开 DevTools。
初次加载失败或超时会通知用户并露出系统壁纸,不自动重试初次启用。运行中异常最多尝试恢复 3 次,稳定运行 60 秒后清零计数。正常退出宿主保留恢复选择;主动关闭或进入失败终态则清除选择。替换失败不会回滚旧网页壁纸。
验收与排查
- 浏览器预览正常不等于桌面挂载成功:实际检查图标优先、左键与鼠标移动、桌面右键菜单、设置窗口输入和 DevTools。
- 超过 30 秒:检查入口路由、资源路径、控制台异常,以及是否在必要资源就绪后调用了
ready()。 - 修改设置窗口样式未生效:检查生成的
widget.json和宿主注册结果,关闭旧设置窗口再打开。 - 壁纸不响应输入框或滚轮:这是壁纸模式的输入限制,应改用设置窗口。
- 测试主屏切换、分辨率变化、锁屏恢复和应用重启;系统桌面的正常点击、框选可能与壁纸点击同时发生。
完整接口见 WallpaperApi,Windows 宿主架构和历史验证边界见设计与实现记录。仓库内的 packages/@widget-js/wallpaper 是蟹堡王壁纸接入示例。