上传到npm仓库

发布 npm 包不仅仅是把代码放上去,还需要考虑 构建规范 (ESM/CJS)类型定义 (TypeScript .d.ts)Peer Dependencies 以及 扩展性

既然我们要把它做成一个通用的 pinia-plugin-electron-share,我们需要让它支持配置(比如自定义 Channel 名称),并且具备完善的工程化结构。

这里是可以直接 npm publish 的完整工程代码结构


1. 项目目录结构

你需要创建一个新的文件夹(例如 pinia-plugin-electron-share),结构如下:

1
2
3
4
5
6
7
8
pinia-plugin-electron-share/
├── src/
│ └── index.ts # 核心源码
├── dist/ # 构建产物 (自动生成)
├── package.json # 依赖与元数据
├── tsconfig.json # TS 配置
├── vite.config.ts # 构建脚本 (使用 Vite 库模式)
└── README.md # 说明文档

2. 核心代码 (src/index.ts)

这里我做了一些增强:

  1. 支持插件级配置:可以在 pinia.use() 时传入全局配置。
  2. 类型声明增强:完美的 TypeScript 类型提示。
  3. 序列化保护:增加了对序列化异常的基础捕获。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
// src/index.ts
import { PiniaPluginContext } from 'pinia'

// 1. 扩展 Pinia 类型定义
declare module 'pinia' {
export interface DefineStoreOptionsBase<S, Store> {
/**
* Enable state synchronization across Electron windows.
* @default false
*/
share?: boolean
}
}

// 2. 插件配置接口
export interface ElectronShareOptions {
/**
* Custom name for BroadcastChannel.
* Useful if you have multiple apps running on the same domain/protocol.
* @default 'pinia-electron-share'
*/
channelName?: string
}

// 3. 消息载荷定义
type SyncPayload = {
type: 'UPDATE' | 'REQUEST_INIT' | 'RESPONSE_INIT'
storeId: string
data?: any
timestamp: number
}

// 4. 插件主函数
export function createElectronSharePlugin(globalOptions: ElectronShareOptions = {}) {
const CHANNEL_NAME = globalOptions.channelName || 'pinia-electron-share'

// 创建频道 (由于是广播,可以在插件层级复用一个实例,也可以在 Store 级创建,这里为了简单和性能复用一个)
const channel = new BroadcastChannel(CHANNEL_NAME)

return ({ store, options }: PiniaPluginContext) => {
// 如果 Store 没有开启 share,直接跳过
if (!options.share) return

let isExternalUpdate = false

// --- 消息接收逻辑 ---
channel.addEventListener('message', (event: MessageEvent<SyncPayload>) => {
const { type, storeId, data } = event.data

// 忽略不属于当前 Store 的消息
if (storeId !== store.$id) return

switch (type) {
case 'UPDATE':
if (!isExternalUpdate) {
isExternalUpdate = true
store.$patch(data)
// 宏任务延迟释放锁,确保 Vue 响应式队列清空
setTimeout(() => { isExternalUpdate = false }, 0)
}
break

case 'REQUEST_INIT':
// 收到新窗口的请求,发送当前状态
channel.postMessage({
type: 'RESPONSE_INIT',
storeId: store.$id,
data: JSON.parse(JSON.stringify(store.$state)),
timestamp: Date.now(),
})
break

case 'RESPONSE_INIT':
// 收到初始化数据,应用到当前 Store
isExternalUpdate = true
store.$patch(data)
setTimeout(() => { isExternalUpdate = false }, 0)
break
}
})

// --- 状态变更监听 ---
store.$subscribe((mutation, state) => {
if (isExternalUpdate) return

try {
const rawState = JSON.parse(JSON.stringify(state))

channel.postMessage({
type: 'UPDATE',
storeId: store.$id,
data: rawState,
timestamp: Date.now(),
})
} catch (e) {
console.error(`[pinia-plugin-electron-share] Failed to serialize state for store "${store.$id}". Ensure state is JSON-serializable.`, e)
}
})

// --- 初始化握手 ---
// 延迟极短时间发送请求,确保监听器已挂载
setTimeout(() => {
channel.postMessage({
type: 'REQUEST_INIT',
storeId: store.$id,
timestamp: Date.now(),
})
}, 50)
}
}

3. 构建配置 (vite.config.ts)

使用 Vite 的库模式(Library Mode)是目前打包现代前端库的最佳实践。我们需要同时输出 ESM (给 Vite/Webpack 用) 和 CJS (给旧 Node 环境用,虽然后者在 Electron 前端不常用,但为了规范还是加上)。

你需要安装开发依赖:
npm install -D vite vite-plugin-dts typescript vue pinia

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// vite.config.ts
import { defineConfig } from 'vite'
import dts from 'vite-plugin-dts'
import { resolve } from 'path'

export default defineConfig({
plugins: [
// 自动生成 .d.ts 类型文件
dts({
include: ['src/**/*.ts'],
insertTypesEntry: true,
})
],
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'PiniaPluginElectronShare',
fileName: (format) => `index.${format}.js` // index.es.js, index.umd.js
},
rollupOptions: {
// 确保外部化处理那些你不想打包进库的依赖
external: ['pinia', 'vue'],
output: {
// 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
globals: {
vue: 'Vue',
pinia: 'Pinia'
}
}
}
}
})

4. package.json 配置

这里有几个关键点:

  1. peerDependencies:明确告诉使用者,你必须先安装 pinia
  2. exports:现代 npm 包的导出规范。
  3. types:指向生成的类型文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
{
"name": "pinia-plugin-electron-share",
"version": "1.0.0",
"description": "Sync Pinia state across Electron windows (Renderer processes) using BroadcastChannel. Ideal for Dockview.",
"main": "./dist/index.umd.js",
"module": "./dist/index.es.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.es.js",
"require": "./dist/index.umd.js",
"types": "./dist/index.d.ts"
}
},
"files": [
"dist"
],
"scripts": {
"dev": "vite build --watch",
"build": "vite build",
"prepublishOnly": "npm run build"
},
"keywords": [
"pinia",
"electron",
"vue",
"share",
"sync",
"multi-window",
"dockview"
],
"author": "Your Name",
"license": "MIT",
"peerDependencies": {
"pinia": "^2.0.0"
},
"devDependencies": {
"pinia": "^2.0.0",
"typescript": "^5.0.0",
"vite": "^5.0.0",
"vite-plugin-dts": "^3.0.0",
"vue": "^3.0.0"
}
}

5. tsconfig.json

确保能正确编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"strict": true,
"sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"skipLibCheck": true,
"declaration": true
},
"include": ["src"]
}

6. 使用说明 (README.md 片段)

你可以直接把这段贴到文档里:

1
2
3
4
5
6
7
8
# pinia-plugin-electron-share

Sync Pinia state across multiple Electron windows (Renderer Processes).

## Installation

```bash
npm install pinia-plugin-electron-share

Usage

1. Register the Plugin

1
2
3
4
5
6
7
8
9
10
11
12
// store/index.ts
import { createPinia } from 'pinia'
import { createElectronSharePlugin } from 'pinia-plugin-electron-share'

const pinia = createPinia()

pinia.use(createElectronSharePlugin({
// Optional: Custom channel name
channelName: 'my-app-sync'
}))

export default pinia

2. Enable in Store

1
2
3
4
5
6
7
8
9
10
11
// store/user.ts
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
state: () => ({ count: 0 }),
actions: {
increment() { this.count++ }
},
// Enable sync for this store
share: true
})
1
2
3
4
5
6
7
8
9
10

---

### 7. 发布流程

作为高级工程师,你应该知道发布流程,但为了完整性我还是列一下:

这里提醒一下 npm 官网更新了验证机制,你需要去在你的账户下创建一个access token,然后在命令行中使用 `npm login --registry=https://registry.npmjs.org/` 登录。

在项目下创建一个.npmrc 文件,内容如下:

registry=https://registry.npmjs.com/
//registry.npmjs.com/:_authToken=YOUR_ACCESS_TOKEN

1
2
3
4

1. **构建**:
```bash
npm run build
*(确保 `dist` 目录下生成了 `index.es.js` 和 `index.d.ts`)*
  1. 发布

    1
    2
    npm login
    npm publish --access public
  2. 在你的项目中使用
    在你的 Electron 项目中 npm install pinia-plugin-electron-share,然后按照 README 配置即可。

为什么这样做更好?

  1. 解耦:你的业务代码库不会混入一堆复杂的 IPC 通信逻辑。
  2. 复用:如果你有下一个 Electron 项目,直接 install 即可。
  3. 类型安全:通过 declare module,你的同事在写 defineStore 时,输入 share: 会自动获得代码提示,这才是高级开发体验。

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器