init tab leader
This commit is contained in:
		
							
								
								
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					node_modules
 | 
				
			||||||
 | 
					dist
 | 
				
			||||||
 | 
					build
 | 
				
			||||||
 | 
					.cache
 | 
				
			||||||
 | 
					.DS_Store
 | 
				
			||||||
 | 
					*.log
 | 
				
			||||||
							
								
								
									
										4
									
								
								.npmrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.npmrc
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
 | 
				
			||||||
 | 
					@abearxiong:registry=https://npm.pkg.github.com
 | 
				
			||||||
 | 
					//registry.npmjs.org/:_authToken=${NPM_TOKEN}
 | 
				
			||||||
 | 
					@kevisual:registry=https://npm.xiongxiao.me
 | 
				
			||||||
							
								
								
									
										39
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "name": "@kevisual/tab-leader",
 | 
				
			||||||
 | 
					  "version": "0.0.1",
 | 
				
			||||||
 | 
					  "description": "",
 | 
				
			||||||
 | 
					  "main": "index.js",
 | 
				
			||||||
 | 
					  "scripts": {
 | 
				
			||||||
 | 
					    "build": "rm -rf dist && rollup -c",
 | 
				
			||||||
 | 
					    "watch": "rollup -c -w",
 | 
				
			||||||
 | 
					    "clean": "rm -rf dist"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "keywords": [],
 | 
				
			||||||
 | 
					  "author": "abearxiong <xiongxiao@xiongxiao.me>",
 | 
				
			||||||
 | 
					  "license": "MIT",
 | 
				
			||||||
 | 
					  "type": "module",
 | 
				
			||||||
 | 
					  "publishConfig": {
 | 
				
			||||||
 | 
					    "access": "public"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "dependencies": {},
 | 
				
			||||||
 | 
					  "devDependencies": {
 | 
				
			||||||
 | 
					    "@kevisual/router": "0.0.6-alpha-2",
 | 
				
			||||||
 | 
					    "@kevisual/store": "0.0.1-alpha.7",
 | 
				
			||||||
 | 
					    "eventemitter3": "^5.0.1",
 | 
				
			||||||
 | 
					    "nanoid": "^5.0.9",
 | 
				
			||||||
 | 
					    "@rollup/plugin-commonjs": "^28.0.1",
 | 
				
			||||||
 | 
					    "@rollup/plugin-node-resolve": "^15.3.0",
 | 
				
			||||||
 | 
					    "@rollup/plugin-replace": "^6.0.1",
 | 
				
			||||||
 | 
					    "@rollup/plugin-typescript": "^12.1.1",
 | 
				
			||||||
 | 
					    "rollup": "^4.28.1",
 | 
				
			||||||
 | 
					    "rollup-plugin-dts": "^6.1.1",
 | 
				
			||||||
 | 
					    "ts-lib": "^0.0.5"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "exports": {
 | 
				
			||||||
 | 
					    ".": {
 | 
				
			||||||
 | 
					      "import": "./dist/tab-leader.js",
 | 
				
			||||||
 | 
					      "types": "./dist/tab-leader.d.ts",
 | 
				
			||||||
 | 
					      "require": "./dist/tab-leader.js"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										9
									
								
								readme.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								readme.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					# 默认依赖模块
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```sh
 | 
				
			||||||
 | 
					import { nanoid } from 'nanoid';
 | 
				
			||||||
 | 
					import { QueryRouterServer } from '@kevisual/router/browser';
 | 
				
			||||||
 | 
					import { useContextKey } from '@kevisual/store/context';
 | 
				
			||||||
 | 
					import { useConfigKey } from '@kevisual/store/config';
 | 
				
			||||||
 | 
					import { EventEmitter } from 'eventemitter3';
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
							
								
								
									
										43
									
								
								rollup.config.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								rollup.config.mjs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
				
			|||||||
 | 
					// rollup.config.js
 | 
				
			||||||
 | 
					import typescript from '@rollup/plugin-typescript';
 | 
				
			||||||
 | 
					import resolve from '@rollup/plugin-node-resolve';
 | 
				
			||||||
 | 
					import commonjs from '@rollup/plugin-commonjs';
 | 
				
			||||||
 | 
					import { dts } from 'rollup-plugin-dts';
 | 
				
			||||||
 | 
					import replace from '@rollup/plugin-replace';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @type {import('rollup').RollupOptions}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export default [
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    input: 'src/index.ts',
 | 
				
			||||||
 | 
					    output: {
 | 
				
			||||||
 | 
					      file: 'dist/tab-leader.js',
 | 
				
			||||||
 | 
					      format: 'es',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    plugins: [
 | 
				
			||||||
 | 
					      replace({
 | 
				
			||||||
 | 
					        preventAssignment: true, // 必须设置为 true
 | 
				
			||||||
 | 
					        delimiters: ['', ''], // 确保完全匹配
 | 
				
			||||||
 | 
					        "import { nanoid } from 'nanoid'": "import { nanoid } from 'https://cdn.jsdelivr.net/npm/nanoid@4.0.0/nanoid.min.js'",
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					      ,
 | 
				
			||||||
 | 
					      resolve({ browser: true }),
 | 
				
			||||||
 | 
					      commonjs(),
 | 
				
			||||||
 | 
					      typescript(),
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    external: [
 | 
				
			||||||
 | 
					      'nanoid',
 | 
				
			||||||
 | 
					      //
 | 
				
			||||||
 | 
					      '@kevisual/router/browser',
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    input: 'src/index.ts',
 | 
				
			||||||
 | 
					    output: {
 | 
				
			||||||
 | 
					      file: 'dist/tab-leader.d.ts',
 | 
				
			||||||
 | 
					      format: 'es',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    plugins: [dts()],
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
							
								
								
									
										436
									
								
								src/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										436
									
								
								src/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,436 @@
 | 
				
			|||||||
 | 
					import { QueryRouterServer } from '@kevisual/router/browser';
 | 
				
			||||||
 | 
					import { useContextKey } from '@kevisual/store/config';
 | 
				
			||||||
 | 
					import { useConfigKey } from '@kevisual/store/config';
 | 
				
			||||||
 | 
					import { EventEmitter } from 'eventemitter3';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const emitter = useContextKey('emitter', () => {
 | 
				
			||||||
 | 
					  console.error('need create emitter, config.emitter');
 | 
				
			||||||
 | 
					  return {} as EventEmitter;
 | 
				
			||||||
 | 
					  // return new EventEmitter();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					export const app = useContextKey('app', () => {
 | 
				
			||||||
 | 
					  console.error('not found app, place set context app first');
 | 
				
			||||||
 | 
					  return {} as QueryRouterServer;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const generateRandom = () => {
 | 
				
			||||||
 | 
					  // return Math.random().toString(36).substring(8);
 | 
				
			||||||
 | 
					  return crypto.randomUUID();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export const tabId = useConfigKey('tabId', () => {
 | 
				
			||||||
 | 
					  return generateRandom();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					export const openTabs = useContextKey('openTabs', () => {
 | 
				
			||||||
 | 
					  return new Set<string>();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					export const tabConfig = useConfigKey('tabConfig', () => {
 | 
				
			||||||
 | 
					  const random = generateRandom();
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    title: 'tab random' + random,
 | 
				
			||||||
 | 
					    description: 'tab config',
 | 
				
			||||||
 | 
					    tabId: useConfigKey('tabId', () => {}),
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 控制 tab 启动
 | 
				
			||||||
 | 
					let load = false;
 | 
				
			||||||
 | 
					let openTime = Date.now();
 | 
				
			||||||
 | 
					let meIsMax = '1'; // 0: 未知, 1: 对比来源,我更大, 2: 不是
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type Msg = {
 | 
				
			||||||
 | 
					  requestId?: string;
 | 
				
			||||||
 | 
					  path: string;
 | 
				
			||||||
 | 
					  key: string;
 | 
				
			||||||
 | 
					  [key: string]: any;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					app
 | 
				
			||||||
 | 
					  .route({
 | 
				
			||||||
 | 
					    path: 'tab',
 | 
				
			||||||
 | 
					    key: 'introduce',
 | 
				
			||||||
 | 
					    description: '当多个tab打开的时候,找打哦最后打开的,确信那些tabs是有效和打开的',
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  .define(async (ctx) => {
 | 
				
			||||||
 | 
					    const source = ctx.query?.source;
 | 
				
			||||||
 | 
					    const sourceOpenTime = ctx.query?.openTime;
 | 
				
			||||||
 | 
					    const update = ctx.query?.update;
 | 
				
			||||||
 | 
					    if (!source) {
 | 
				
			||||||
 | 
					      ctx.throw?.(400, 'tabId is required');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    openTabs.add(source);
 | 
				
			||||||
 | 
					    if (update) {
 | 
				
			||||||
 | 
					      localStorage.setItem('tab-' + tabId, source);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!load) {
 | 
				
			||||||
 | 
					      // console.log('soucre time', sourceOpenTime, openTime, sourceOpenTime > openTime);
 | 
				
			||||||
 | 
					      if (openTime < sourceOpenTime) {
 | 
				
			||||||
 | 
					        meIsMax = '2'; // 来源的时间比我大
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    ctx.code = 999;
 | 
				
			||||||
 | 
					    ctx.body = 'ok';
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  .addTo(app);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app
 | 
				
			||||||
 | 
					  .route({
 | 
				
			||||||
 | 
					    path: 'tab',
 | 
				
			||||||
 | 
					    key: 'close',
 | 
				
			||||||
 | 
					    description: '当触发关闭事件的时候,清除tab',
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  .define(async (ctx) => {
 | 
				
			||||||
 | 
					    const source = ctx.query?.source;
 | 
				
			||||||
 | 
					    if (!source) {
 | 
				
			||||||
 | 
					      ctx.throw?.(400, 'tabId is required');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    openTabs.delete(source);
 | 
				
			||||||
 | 
					    localStorage.removeItem('tab-' + source);
 | 
				
			||||||
 | 
					    ctx.body = 'ok';
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  .addTo(app);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app
 | 
				
			||||||
 | 
					  .route({
 | 
				
			||||||
 | 
					    path: 'tab',
 | 
				
			||||||
 | 
					    key: 'setTabs',
 | 
				
			||||||
 | 
					    description: '已经知道最新的tabs了,通知所有的channel,设置打开的 tab',
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  .define(async (ctx) => {
 | 
				
			||||||
 | 
					    const tabs = ctx.query?.tabs || [];
 | 
				
			||||||
 | 
					    openTabs.clear();
 | 
				
			||||||
 | 
					    tabs.forEach((tab) => {
 | 
				
			||||||
 | 
					      openTabs.add(tab);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    console.log(`[${tabId}]Set open tabs: ${tabs.length}`, openTabs.keys());
 | 
				
			||||||
 | 
					    load = true;
 | 
				
			||||||
 | 
					    ctx.body = 'ok';
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  .addTo(app);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app
 | 
				
			||||||
 | 
					  .route({
 | 
				
			||||||
 | 
					    path: 'tab',
 | 
				
			||||||
 | 
					    key: 'getTabs',
 | 
				
			||||||
 | 
					    description: '获取所有的打开的 tab',
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  .define(async (ctx) => {
 | 
				
			||||||
 | 
					    ctx.body = Array.from(openTabs);
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  .addTo(app);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 操作私有 tab
 | 
				
			||||||
 | 
					app
 | 
				
			||||||
 | 
					  .route({
 | 
				
			||||||
 | 
					    path: 'tab',
 | 
				
			||||||
 | 
					    key: 'getMe',
 | 
				
			||||||
 | 
					    description: '获取自己的 tab 信息,比如tabId,openTime,tabConfig',
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  .define(async (ctx) => {
 | 
				
			||||||
 | 
					    ctx.body = { tabId, openTime, tabConfig };
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  .addTo(app);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app
 | 
				
			||||||
 | 
					  .route({
 | 
				
			||||||
 | 
					    path: 'tab',
 | 
				
			||||||
 | 
					    key: 'me',
 | 
				
			||||||
 | 
					    id: 'me',
 | 
				
			||||||
 | 
					    description: '验证是否是自己的标签页面,如果是自己则继续,否则返回错误',
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  .define(async (ctx) => {
 | 
				
			||||||
 | 
					    // check me
 | 
				
			||||||
 | 
					    const to = ctx.query?.to;
 | 
				
			||||||
 | 
					    if (to !== tabId) {
 | 
				
			||||||
 | 
					      // not me
 | 
				
			||||||
 | 
					      ctx.throw?.(840, 'not me');
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    ctx.body = 'ok';
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  .addTo(app);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app
 | 
				
			||||||
 | 
					  .route({
 | 
				
			||||||
 | 
					    path: 'channel',
 | 
				
			||||||
 | 
					    key: 'postAllTabs',
 | 
				
			||||||
 | 
					    description: '向所有的 tab 发送消息,如果payload有hasResponse,则等待所有的 tab 返回消息',
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  .define(async (ctx) => {
 | 
				
			||||||
 | 
					    const data = ctx.query?.data;
 | 
				
			||||||
 | 
					    const hasResponse = ctx.query?.hasResponse;
 | 
				
			||||||
 | 
					    if (!data?.path) {
 | 
				
			||||||
 | 
					      ctx.throw?.(400, 'path is required');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const res = await new Promise((resolve) => {
 | 
				
			||||||
 | 
					      if (hasResponse) {
 | 
				
			||||||
 | 
					        postAllTabs(data, (res) => {
 | 
				
			||||||
 | 
					          resolve(res);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        postAllTabs(data);
 | 
				
			||||||
 | 
					        resolve('ok');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    ctx.body = res || 'ok';
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  .addTo(app);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app
 | 
				
			||||||
 | 
					  .route({
 | 
				
			||||||
 | 
					    path: 'channel',
 | 
				
			||||||
 | 
					    key: 'postTab',
 | 
				
			||||||
 | 
					    description: '向指定的 tab 发送消息,如果payload有hasResponse,则等待指定的 tab 返回消息',
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  .define(async (ctx) => {
 | 
				
			||||||
 | 
					    const data = ctx.query?.data;
 | 
				
			||||||
 | 
					    const hasResponse = ctx.query?.hasResponse;
 | 
				
			||||||
 | 
					    if (!data?.path) {
 | 
				
			||||||
 | 
					      ctx.throw?.(400, 'path is required');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const res = await new Promise((resolve) => {
 | 
				
			||||||
 | 
					      if (hasResponse) {
 | 
				
			||||||
 | 
					        postTab(data, (res) => {
 | 
				
			||||||
 | 
					          resolve(res);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        postTab(data);
 | 
				
			||||||
 | 
					        resolve('ok');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    ctx.body = res || 'ok';
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  .addTo(app);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app
 | 
				
			||||||
 | 
					  .route({
 | 
				
			||||||
 | 
					    path: 'tab',
 | 
				
			||||||
 | 
					    key: 'setConfig',
 | 
				
			||||||
 | 
					    description: '设置 tab 的配置信息',
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  .define(async (ctx) => {
 | 
				
			||||||
 | 
					    const config = ctx.query?.config;
 | 
				
			||||||
 | 
					    if (config) {
 | 
				
			||||||
 | 
					      Object.assign(tabConfig, config);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    ctx.body = tabConfig;
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  .addTo(app);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app
 | 
				
			||||||
 | 
					  .route({
 | 
				
			||||||
 | 
					    path: 'tab',
 | 
				
			||||||
 | 
					    key: 'getConfig',
 | 
				
			||||||
 | 
					    description: '获取 tab 的配置信息',
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  .define(async (ctx) => {
 | 
				
			||||||
 | 
					    ctx.body = tabConfig;
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  .addTo(app);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const channel = useContextKey('channel', () => {
 | 
				
			||||||
 | 
					  return new BroadcastChannel('tab-channel');
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					channel.addEventListener('message', (event) => {
 | 
				
			||||||
 | 
					  const { requestId, to, source, responseId } = event.data;
 | 
				
			||||||
 | 
					  if (responseId && to === tabId) {
 | 
				
			||||||
 | 
					    // 有 id 的消息, 这是这个模块请求返回回来的消息,不被其他模块处理。
 | 
				
			||||||
 | 
					    emitter.emit(responseId, event.data);
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // console.log('channel message', event.data);
 | 
				
			||||||
 | 
					  if (to === 'all' || to === tabId) {
 | 
				
			||||||
 | 
					    const { path, key, payload, ...rest } = event.data;
 | 
				
			||||||
 | 
					    if (!path) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    app
 | 
				
			||||||
 | 
					      .run({
 | 
				
			||||||
 | 
					        path,
 | 
				
			||||||
 | 
					        key,
 | 
				
			||||||
 | 
					        payload: {
 | 
				
			||||||
 | 
					          ...rest,
 | 
				
			||||||
 | 
					          ...payload,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .then((res) => {
 | 
				
			||||||
 | 
					        const { data, message, code } = res;
 | 
				
			||||||
 | 
					        if (requestId) {
 | 
				
			||||||
 | 
					          if (code !== 999) {
 | 
				
			||||||
 | 
					            console.log('channel response', requestId, res);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          const msg = {
 | 
				
			||||||
 | 
					            // 返回数据一般没有 requestId,如果有,在res中自己放置
 | 
				
			||||||
 | 
					            responseId: requestId,
 | 
				
			||||||
 | 
					            to: source,
 | 
				
			||||||
 | 
					            source: tabId,
 | 
				
			||||||
 | 
					            data: data,
 | 
				
			||||||
 | 
					            message,
 | 
				
			||||||
 | 
					            code,
 | 
				
			||||||
 | 
					          };
 | 
				
			||||||
 | 
					          if (data?.requestId) {
 | 
				
			||||||
 | 
					            msg['requestId'] = data.requestId;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          channel.postMessage(msg);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .catch((err) => {
 | 
				
			||||||
 | 
					        console.error(tabId, requestId, err);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const postAllTabs = (msg: Msg, callback?: (data: any[], error?: any[]) => any, timeout = 3 * 60 * 1000) => {
 | 
				
			||||||
 | 
					  const action = {
 | 
				
			||||||
 | 
					    ...msg,
 | 
				
			||||||
 | 
					    to: 'all',
 | 
				
			||||||
 | 
					    source: tabId,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  if (callback) {
 | 
				
			||||||
 | 
					    const requestId = generateRandom();
 | 
				
			||||||
 | 
					    action.requestId = requestId;
 | 
				
			||||||
 | 
					    const res: any = [];
 | 
				
			||||||
 | 
					    const error: any = [];
 | 
				
			||||||
 | 
					    emitter.on(requestId, (data) => {
 | 
				
			||||||
 | 
					      res.push(data);
 | 
				
			||||||
 | 
					      // console.log('postAllTabs callback', data, res.length, openTabs.size);
 | 
				
			||||||
 | 
					      if (res.length >= openTabs.size - 1) {
 | 
				
			||||||
 | 
					        callback(res);
 | 
				
			||||||
 | 
					        emitter.off(requestId);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    setTimeout(() => {
 | 
				
			||||||
 | 
					      const tabs = [...openTabs];
 | 
				
			||||||
 | 
					      tabs.forEach((tab) => {
 | 
				
			||||||
 | 
					        if (tab === tabId) {
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (!res.find((r) => r.source === tab)) {
 | 
				
			||||||
 | 
					          error.push({ code: 500, message: 'timeout', tab, source: tab, to: tabId });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      callback(res, error);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      emitter.off(requestId);
 | 
				
			||||||
 | 
					    }, timeout);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  channel.postMessage(action);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const postTab = (msg: Msg, callback?: (data: any) => any, timeout = 3 * 60 * 1000) => {
 | 
				
			||||||
 | 
					  const action = {
 | 
				
			||||||
 | 
					    ...msg,
 | 
				
			||||||
 | 
					    to: msg.to,
 | 
				
			||||||
 | 
					    source: tabId,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  if (callback) {
 | 
				
			||||||
 | 
					    const requestId = generateRandom();
 | 
				
			||||||
 | 
					    action.requestId = requestId;
 | 
				
			||||||
 | 
					    emitter.on(requestId, (data) => {
 | 
				
			||||||
 | 
					      callback(data);
 | 
				
			||||||
 | 
					      emitter.off(requestId);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    setTimeout(() => {
 | 
				
			||||||
 | 
					      callback({ code: 500, message: 'timeout' });
 | 
				
			||||||
 | 
					      emitter.off(requestId);
 | 
				
			||||||
 | 
					    }, timeout);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  channel.postMessage(action);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					const channelFn = {
 | 
				
			||||||
 | 
					  postAllTabs,
 | 
				
			||||||
 | 
					  postTab,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					useConfigKey('channelMsg', () => {
 | 
				
			||||||
 | 
					  return channelFn;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getTabs = (filter = true) => {
 | 
				
			||||||
 | 
					  const allTabs = Object.keys(localStorage).filter((key) => key.startsWith('tab-'));
 | 
				
			||||||
 | 
					  const tabs = allTabs.filter((key) => {
 | 
				
			||||||
 | 
					    if (!filter) {
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const tab = localStorage.getItem(key);
 | 
				
			||||||
 | 
					    if (tab === tabId) {
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    localStorage.removeItem(key);
 | 
				
			||||||
 | 
					    return false;
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  return tabs;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getOtherTabs = async () => {
 | 
				
			||||||
 | 
					  const res = await app.run({
 | 
				
			||||||
 | 
					    path: 'channel',
 | 
				
			||||||
 | 
					    key: 'postAllTabs',
 | 
				
			||||||
 | 
					    payload: {
 | 
				
			||||||
 | 
					      data: {
 | 
				
			||||||
 | 
					        path: 'tab',
 | 
				
			||||||
 | 
					        key: 'getConfig',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      hasResponse: true,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  console.log('getOtherTabs', res);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// @ts-ignore
 | 
				
			||||||
 | 
					window.getOtherTabs = getOtherTabs;
 | 
				
			||||||
 | 
					const checkTabs = async () => {
 | 
				
			||||||
 | 
					  postAllTabs({ path: 'tab', key: 'introduce', payload: { openTime: openTime, update: true } });
 | 
				
			||||||
 | 
					  await sleep(1000);
 | 
				
			||||||
 | 
					  const tabs = getTabs();
 | 
				
			||||||
 | 
					  postAllTabs({ path: 'tab', key: 'setTabs', payload: { tabs: tabs.map((tab) => tab.replace('tab-', '')) } });
 | 
				
			||||||
 | 
					  openTabs.clear();
 | 
				
			||||||
 | 
					  tabs.forEach((tab) => {
 | 
				
			||||||
 | 
					    openTabs.add(tab.replace('tab-', ''));
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 初始化 tab
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					const init = async () => {
 | 
				
			||||||
 | 
					  localStorage.setItem('tab-' + tabId, tabId);
 | 
				
			||||||
 | 
					  // 向其他 tab 发送消息, 页面已经打开
 | 
				
			||||||
 | 
					  postAllTabs({ path: 'tab', key: 'introduce', payload: { openTime: openTime } });
 | 
				
			||||||
 | 
					  window.addEventListener('beforeunload', () => {
 | 
				
			||||||
 | 
					    // 向其他 tab 发送消息, 页面即将关闭
 | 
				
			||||||
 | 
					    localStorage.removeItem('tab-' + tabId);
 | 
				
			||||||
 | 
					    console.log('close tab', tabId);
 | 
				
			||||||
 | 
					    postAllTabs({ path: 'tab', key: 'close' });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  introduceMe();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					const introduceMe = () => {
 | 
				
			||||||
 | 
					  if (meIsMax === '2') {
 | 
				
			||||||
 | 
					    // 我知道我不是最大的了,我自己就不再发送消息了
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  postAllTabs({ path: 'tab', key: 'introduce', payload: { openTime: openTime } });
 | 
				
			||||||
 | 
					  if (!load) {
 | 
				
			||||||
 | 
					    const time = Date.now() - openTime;
 | 
				
			||||||
 | 
					    if (time > 2100) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    setTimeout(() => {
 | 
				
			||||||
 | 
					      introduceMe();
 | 
				
			||||||
 | 
					    }, 300);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const start = () => {
 | 
				
			||||||
 | 
					  init();
 | 
				
			||||||
 | 
					  setTimeout(() => {
 | 
				
			||||||
 | 
					    if (meIsMax === '1') {
 | 
				
			||||||
 | 
					      console.log('I am max', openTime);
 | 
				
			||||||
 | 
					      checkTabs();
 | 
				
			||||||
 | 
					      load = true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, 2000);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										40
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "compilerOptions": {
 | 
				
			||||||
 | 
					    "module": "NodeNext",
 | 
				
			||||||
 | 
					    "target": "esnext",
 | 
				
			||||||
 | 
					    "noImplicitAny": false,
 | 
				
			||||||
 | 
					    "outDir": "./dist",
 | 
				
			||||||
 | 
					    "sourceMap": false,
 | 
				
			||||||
 | 
					    "allowJs": true,
 | 
				
			||||||
 | 
					    "newLine": "LF",
 | 
				
			||||||
 | 
					    "baseUrl": "./",
 | 
				
			||||||
 | 
					    "typeRoots": [
 | 
				
			||||||
 | 
					      "node_modules/@types",
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    "declaration": false,
 | 
				
			||||||
 | 
					    "noEmit": true,
 | 
				
			||||||
 | 
					    "allowImportingTsExtensions": true,
 | 
				
			||||||
 | 
					    "moduleResolution": "NodeNext",
 | 
				
			||||||
 | 
					    "experimentalDecorators": true,
 | 
				
			||||||
 | 
					    "emitDecoratorMetadata": true,
 | 
				
			||||||
 | 
					    "esModuleInterop": true,
 | 
				
			||||||
 | 
					    "paths": {
 | 
				
			||||||
 | 
					      "@/*": [
 | 
				
			||||||
 | 
					        "src/*"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "*": [
 | 
				
			||||||
 | 
					        "types/*"
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "include": [
 | 
				
			||||||
 | 
					    "typings.d.ts",
 | 
				
			||||||
 | 
					    "src/**/*.ts"
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  "exclude": [
 | 
				
			||||||
 | 
					    "node_modules",
 | 
				
			||||||
 | 
					    "demo/simple/dist",
 | 
				
			||||||
 | 
					    "src/**/*.test.ts",
 | 
				
			||||||
 | 
					    "rollup.config.js",
 | 
				
			||||||
 | 
					  ]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user