feat: 添加query WS的功能
This commit is contained in:
parent
632d164087
commit
ed178ee4c6
3
.gitignore
vendored
3
.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
node_modules/
|
node_modules
|
||||||
|
dist
|
7
demo/package-lock.json
generated
7
demo/package-lock.json
generated
@ -16,9 +16,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"..": {
|
"..": {
|
||||||
"name": "@abearxiong/query",
|
"name": "@kevisual/query",
|
||||||
"version": "0.0.1",
|
"version": "0.0.2-alpha.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"zustand": "^4.5.5"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||||
"@rollup/plugin-typescript": "^11.1.6",
|
"@rollup/plugin-typescript": "^11.1.6",
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"dev": "vite"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
// console.log('Hello World');
|
// console.log('Hello World');
|
||||||
import { adapter, Query } from '@abearxiong/query';
|
import { adapter, Query } from '@abearxiong/query';
|
||||||
|
import { QueryWs } from '@abearxiong/query/ws';
|
||||||
|
|
||||||
window.onload = async () => {
|
window.onload = async () => {
|
||||||
// const res = await adapter({
|
// const res = await adapter({
|
||||||
@ -27,4 +28,13 @@ window.onload = async () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
console.log(res);
|
console.log(res);
|
||||||
|
|
||||||
|
const queryWs = new QueryWs({ url: '/api/router' });
|
||||||
|
// queryWs.conn
|
||||||
|
queryWs.listenConnect(() => {
|
||||||
|
console.log('Connected');
|
||||||
|
});
|
||||||
|
queryWs.listenConnect(() => {
|
||||||
|
console.log('Connected2');
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@ -9,10 +9,17 @@ export default defineConfig({
|
|||||||
port: 6102,
|
port: 6102,
|
||||||
// host: '::',
|
// host: '::',
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api/router': {
|
'/api': {
|
||||||
target: 'http://127.0.0.1:3003',
|
target: 'http://127.0.0.1:4000',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
|
'/api/router': {
|
||||||
|
target: 'ws://localhost:4000',
|
||||||
|
changeOrigin: true,
|
||||||
|
ws: true,
|
||||||
|
rewriteWsOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/api/, '/api'),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// define: {
|
// define: {
|
||||||
|
21
package.json
21
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@kevisual/query",
|
"name": "@kevisual/query",
|
||||||
"version": "0.0.2-alpha.0",
|
"version": "0.0.3",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.js",
|
"module": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
@ -21,17 +21,28 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||||
"@rollup/plugin-typescript": "^11.1.6",
|
"@rollup/plugin-typescript": "^11.1.6",
|
||||||
"@types/jest": "^29.5.12",
|
|
||||||
"jest": "^29.7.0",
|
|
||||||
"jest-config": "^29.7.0",
|
|
||||||
"rollup": "^4.21.2",
|
"rollup": "^4.21.2",
|
||||||
"ts-jest": "^29.2.5",
|
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tslib": "^2.7.0",
|
"tslib": "^2.7.0",
|
||||||
|
"zustand": "^4.5.5",
|
||||||
"typescript": "^5.5.4"
|
"typescript": "^5.5.4"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@1.22.19+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447",
|
"packageManager": "yarn@1.22.19+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"require": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"./node": {
|
||||||
|
"import": "./dist/node-adapter.js",
|
||||||
|
"require": "./dist/node-adapter.js"
|
||||||
|
},
|
||||||
|
"./ws": {
|
||||||
|
"import": "./dist/ws.js",
|
||||||
|
"require": "./dist/ws.js"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -35,4 +35,18 @@ export default [
|
|||||||
}), // 使用 @rollup/plugin-typescript 处理 TypeScript 文件
|
}), // 使用 @rollup/plugin-typescript 处理 TypeScript 文件
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
input: 'src/ws.ts', // TypeScript 入口文件
|
||||||
|
output: {
|
||||||
|
file: 'dist/ws.js', // 输出文件
|
||||||
|
format: 'es', // 输出格式设置为 ES 模块
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
resolve(), // 使用 @rollup/plugin-node-resolve 解析 node_modules 中的模块
|
||||||
|
typescript({
|
||||||
|
allowImportingTsExtensions: true,
|
||||||
|
noEmit: true,
|
||||||
|
}), // 使用 @rollup/plugin-typescript 处理 TypeScript 文件
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { adapter } from './adapter.ts';
|
import { adapter } from './adapter.ts';
|
||||||
|
export {QueryWs} from './ws.ts'
|
||||||
type Fn = (opts: {
|
type Fn = (opts: {
|
||||||
url?: string;
|
url?: string;
|
||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
|
24
src/utils.ts
Normal file
24
src/utils.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
export const parseUrl = (url: string) => {
|
||||||
|
try {
|
||||||
|
new URL(url);
|
||||||
|
} catch (e) {
|
||||||
|
const _url = new URL(url, location.origin);
|
||||||
|
return _url.href;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseWsUrl = (url: string) => {
|
||||||
|
try {
|
||||||
|
new URL(url);
|
||||||
|
return url;
|
||||||
|
} catch (e) {
|
||||||
|
const _url = new URL(url, location.origin);
|
||||||
|
if (_url.protocol === 'http:') {
|
||||||
|
_url.protocol = 'ws:';
|
||||||
|
}
|
||||||
|
if (_url.protocol === 'https:') {
|
||||||
|
_url.protocol = 'wss:';
|
||||||
|
}
|
||||||
|
return _url.href;
|
||||||
|
}
|
||||||
|
};
|
143
src/ws.ts
Normal file
143
src/ws.ts
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import { createStore, StoreApi } from 'zustand/vanilla';
|
||||||
|
import { parseWsUrl } from './utils.ts';
|
||||||
|
|
||||||
|
type QueryWsStore = {
|
||||||
|
connected: boolean;
|
||||||
|
status: 'connecting' | 'connected' | 'disconnected';
|
||||||
|
setConnected: (connected: boolean) => void;
|
||||||
|
setStatus: (status: QuerySelectState) => void;
|
||||||
|
};
|
||||||
|
export type QuerySelectState = 'connecting' | 'connected' | 'disconnected';
|
||||||
|
export type QueryWsStoreListener = (newState: QueryWsStore, oldState: QueryWsStore) => void;
|
||||||
|
type QueryWsOpts = {
|
||||||
|
url?: string;
|
||||||
|
store?: StoreApi<QueryWsStore>;
|
||||||
|
ws?: WebSocket;
|
||||||
|
};
|
||||||
|
export type WsSend<T = any, U = any> = (data: T, opts?: { isJson?: boolean; wrapper?: (data: T) => U }) => any;
|
||||||
|
export type WsOnMessage<T = any, U = any> = (fn: (data: U, event: MessageEvent) => void, opts?: { isJson?: boolean; selector?: (data: T) => U }) => any;
|
||||||
|
|
||||||
|
export class QueryWs {
|
||||||
|
url: string;
|
||||||
|
store: StoreApi<QueryWsStore>;
|
||||||
|
ws: WebSocket;
|
||||||
|
constructor(opts?: QueryWsOpts) {
|
||||||
|
const url = opts?.url || '/api/router';
|
||||||
|
if (opts?.store) {
|
||||||
|
this.store = opts.store;
|
||||||
|
} else {
|
||||||
|
const store = createStore<QueryWsStore>((set) => ({
|
||||||
|
connected: false,
|
||||||
|
status: 'connecting',
|
||||||
|
setConnected: (connected) => set({ connected }),
|
||||||
|
setStatus: (status) => set({ status }),
|
||||||
|
}));
|
||||||
|
this.store = store;
|
||||||
|
}
|
||||||
|
const wsUrl = parseWsUrl(url);
|
||||||
|
if (opts?.ws && opts.ws instanceof WebSocket) {
|
||||||
|
this.ws = opts.ws;
|
||||||
|
} else {
|
||||||
|
this.ws = new WebSocket(wsUrl);
|
||||||
|
}
|
||||||
|
this.connect();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 连接 WebSocket
|
||||||
|
*/
|
||||||
|
connect() {
|
||||||
|
const store = this.store;
|
||||||
|
const connected = store.getState().connected;
|
||||||
|
if (connected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ws = this.ws || new WebSocket(this.url);
|
||||||
|
ws.onopen = () => {
|
||||||
|
store.getState().setConnected(true);
|
||||||
|
store.getState().setStatus('connected');
|
||||||
|
};
|
||||||
|
ws.onclose = () => {
|
||||||
|
store.getState().setConnected(false);
|
||||||
|
this.ws = null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
listenConnect(callback: () => void) {
|
||||||
|
const store = this.store;
|
||||||
|
const { connected } = store.getState();
|
||||||
|
if (connected) {
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const subscriptionOne = (selector: (state: QueryWsStore) => QueryWsStore['connected'], listener: QueryWsStoreListener) => {
|
||||||
|
const unsubscribe = store.subscribe((newState: any, oldState: any) => {
|
||||||
|
if (selector(newState) !== selector(oldState)) {
|
||||||
|
listener(newState, oldState);
|
||||||
|
unsubscribe();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return unsubscribe;
|
||||||
|
};
|
||||||
|
const cancel = subscriptionOne(
|
||||||
|
(state) => state.connected,
|
||||||
|
() => {
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return cancel;
|
||||||
|
}
|
||||||
|
onMessage<T = any, U = any>(
|
||||||
|
fn: (data: U, event: MessageEvent) => void,
|
||||||
|
opts?: {
|
||||||
|
isJson?: boolean;
|
||||||
|
selector?: (data: T) => U;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const ws = this.ws;
|
||||||
|
const isJson = opts?.isJson ?? true;
|
||||||
|
const selector = opts?.selector;
|
||||||
|
const parseIfJson = (data: string) => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(data);
|
||||||
|
} catch (e) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const listener = (event: MessageEvent) => {
|
||||||
|
const received = parseIfJson(event.data);
|
||||||
|
if (typeof received === 'string' && !isJson) {
|
||||||
|
fn(received as any, event);
|
||||||
|
} else if (typeof received === 'object' && isJson) {
|
||||||
|
fn(selector ? selector(received) : received, event);
|
||||||
|
} else {
|
||||||
|
// 过滤掉的数据
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ws.addEventListener('message', listener);
|
||||||
|
return () => {
|
||||||
|
ws.removeEventListener('message', listener);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
close() {
|
||||||
|
const ws = this.ws;
|
||||||
|
const store = this.store;
|
||||||
|
ws?.close?.();
|
||||||
|
this.ws = null;
|
||||||
|
store.getState().setConnected(false);
|
||||||
|
store.getState().setStatus('disconnected');
|
||||||
|
}
|
||||||
|
send<T = any, U = any>(data: T, opts?: { isJson?: boolean; wrapper?: (data: T) => U }) {
|
||||||
|
const ws = this.ws;
|
||||||
|
const isJson = opts?.isJson ?? true;
|
||||||
|
const wrapper = opts?.wrapper;
|
||||||
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||||
|
console.error('WebSocket is not open');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isJson) {
|
||||||
|
ws.send(JSON.stringify(wrapper ? wrapper(data) : data));
|
||||||
|
} else {
|
||||||
|
ws.send(data as string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
test/ws.test.ts
Normal file
13
test/ws.test.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { QueryWs } from '../src/ws';
|
||||||
|
|
||||||
|
const queryWs = new QueryWs({ url: '/api/ws' });
|
||||||
|
|
||||||
|
queryWs.listenConnect(() => {
|
||||||
|
console.log('Connected');
|
||||||
|
});
|
||||||
|
|
||||||
|
queryWs.store.getState().setConnected(true);
|
||||||
|
queryWs.store.getState().setConnected(false);
|
||||||
|
setTimeout(() => {
|
||||||
|
queryWs.store.getState().setConnected(true);
|
||||||
|
}, 1000);
|
Loading…
x
Reference in New Issue
Block a user