feat: 添加query WS的功能

This commit is contained in:
xion 2024-09-28 18:35:50 +08:00
parent 632d164087
commit ed178ee4c6
11 changed files with 239 additions and 13 deletions

3
.gitignore vendored
View File

@ -1 +1,2 @@
node_modules/ node_modules
dist

View File

@ -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",

View File

@ -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": "",

View File

@ -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');
});
}; };

View File

@ -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: {

View File

@ -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"
}
} }
} }

View File

@ -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 文件
],
},
]; ];

View File

@ -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
View 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
View 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
View 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);