├── .browserslistrc ├── commitlint.config.js ├── babel.config.js ├── .editorconfig ├── .prettierrc.json ├── PUBLISH.md ├── .gitignore ├── webstorm.config.js ├── vue.config.js ├── .eslintrc.js ├── .eslintrc.json ├── src ├── type │ └── PluginsType.ts ├── socket-server │ ├── Emitter.ts │ └── Observer.ts └── Main.ts ├── tsconfig.json ├── package.json ├── CHANGELOG.md ├── README.md └── README-EN.md /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ["@commitlint/config-angular"] }; 2 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/cli-plugin-babel/preset"] 3 | }; 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | # 对所有文件有效 4 | [*] 5 | charset = utf-8 6 | tab_width = 2 7 | indent_style = space 8 | indent_size = 2 -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "endOfLine": "auto", 5 | "singleQuote": false, 6 | "semi": true, 7 | "trailingComma": "none", 8 | "bracketSpacing": true 9 | } 10 | -------------------------------------------------------------------------------- /PUBLISH.md: -------------------------------------------------------------------------------- 1 | ## 新版本推送规范 2 | 3 | - 对插件进行修改 4 | 5 | - 执行 `yarn build` 来生成打包后的文件 6 | 7 | - 修改`package.json`中的版本号 8 | 9 | - 提交你的修改 10 | 11 | - 运行`package.json`中的`changelog`命令来生成更新记录 12 | 13 | - 最后将项目推送到你的仓库,然后为主仓库创建一个Pull request 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /webstorm.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // eslint-disable-next-line @typescript-eslint/no-var-requires 3 | const path = require("path"); 4 | 5 | function resolve(dir) { 6 | return path.join(__dirname, ".", dir); 7 | } 8 | 9 | module.exports = { 10 | context: path.resolve(__dirname, "./"), 11 | resolve: { 12 | extensions: [".js", ".vue", ".json"], 13 | alias: { 14 | "@": resolve("src"), 15 | "@views": resolve("src/views"), 16 | "@comp": resolve("src/components"), 17 | "@core": resolve("src/core"), 18 | "@utils": resolve("src/utils") 19 | } 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 强制css内联,不然会导致样式失效问题 3 | css: { extract: false }, 4 | chainWebpack: config => { 5 | if (process.env.NODE_ENV === "production") { 6 | config.module.rule("ts").uses.delete("cache-loader"); 7 | config.module 8 | .rule("ts") 9 | .use("ts-loader") 10 | .loader("ts-loader") 11 | .tap(opts => { 12 | opts.transpileOnly = false; 13 | opts.happyPackMode = false; 14 | return opts; 15 | }); 16 | } 17 | }, 18 | parallel: false, 19 | // 禁用生产环境的 Source Map 文件 20 | productionSourceMap: false 21 | }; 22 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: [ 7 | "plugin:vue/vue3-essential", 8 | "eslint:recommended", 9 | "@vue/typescript/recommended", 10 | "@vue/prettier", 11 | "@vue/prettier/@typescript-eslint" 12 | ], 13 | parserOptions: { 14 | ecmaVersion: 2020 15 | }, 16 | rules: { 17 | "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", 18 | "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", 19 | "prettier/prettier": "error", // prettier标记的地方抛出错误信息 20 | "spaced-comment": [2, "always"], // 注释后面必须写两个空格 21 | "@typescript-eslint/no-explicit-any": ["off"] // 关闭any校验 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, // 开启浏览器 4 | "es2020": true, // 开启es2020语法 5 | "node": true // 开启node 6 | }, 7 | "extends": [ 8 | "eslint:recommended", // 使用eslint的标准验证规则 9 | "plugin:@typescript-eslint/recommended" // 使用typescript插件推荐的标准验证规则 10 | ], 11 | "parser": "@typescript-eslint/parser", // 使用typescript作为ESLint的解析器 12 | "parserOptions": { 13 | "ecmaVersion": 11, // ecmaVersion对应ES2020 14 | "sourceType": "module" // ES6+语法必须用module 15 | }, 16 | "plugins": [ // 用到的插件 17 | "@typescript-eslint", 18 | "prettier" 19 | ], 20 | "rules": { 21 | "prettier/prettier": "error", // prettier标记的地方抛出错误信息 22 | "spaced-comment": [2,"always"],// 注释后面必须写两个空格 23 | "@typescript-eslint/no-explicit-any": ["off"] // 关闭any校验 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/type/PluginsType.ts: -------------------------------------------------------------------------------- 1 | // 插件内用到的类型进行统一定义 | Unified definition of the types used in the plugin 2 | 3 | // 传输数据时的处理函数类型定义 | Type definition of processing function when transferring data 4 | export type storeHandler = ( 5 | eventName: string, 6 | event: { 7 | data: string; 8 | mutation: string; 9 | namespace: string; 10 | action: string; 11 | }, 12 | opts?: T 13 | ) => void; 14 | 15 | // 插件调用者可以传的参数类型定义 | The parameter type definition that the plug-in caller can pass 16 | export type websocketOpts = { 17 | format: string; 18 | reconnection?: boolean; 19 | reconnectionAttempts?: number; 20 | reconnectionDelay?: number; 21 | connectManually?: boolean; 22 | passToStoreHandler?: storeHandler; 23 | store?: T; 24 | mutations?: T; 25 | protocol?: string; 26 | WebSocket?: WebSocket; 27 | $setInstance?: (event: EventTarget) => void; 28 | }; 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "skipLibCheck": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "baseUrl": ".", 14 | "types": [ 15 | "webpack-env" 16 | ], 17 | "paths": { 18 | "@/*": [ 19 | "src/*" 20 | ] 21 | }, 22 | "declaration": true,// 是否生成声明文件 23 | "declarationDir": "dist/lib",// 声明文件打包的位置 24 | "lib": [ 25 | "esnext", 26 | "dom", 27 | "dom.iterable", 28 | "scripthost" 29 | ] 30 | }, 31 | "include": [ 32 | "src/**/*.ts", 33 | "src/**/*.tsx", 34 | "src/**/*.vue", 35 | "tests/**/*.ts", 36 | "tests/**/*.tsx" 37 | ], 38 | "exclude": [ 39 | "node_modules" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-native-websocket-vue3", 3 | "version": "3.1.8", 4 | "description": "支持vue3和vuex、pinia的websocket插件", 5 | "main": "dist/vueNativeWebsocket.common.js", 6 | "types": "dist/lib/Main.d.ts", 7 | "private": false, 8 | "publisher": "magicalprogrammer@qq.com", 9 | "scripts": { 10 | "build": "vue-cli-service build --target lib --name vueNativeWebsocket src/Main.ts", 11 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", 12 | "commit": "git-cz" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/likaia/vue-native-websocket-vue3.git" 17 | }, 18 | "keywords": [ 19 | "vuejs", 20 | "socket", 21 | "vue", 22 | "websocket", 23 | "realtime", 24 | "flux", 25 | "vuex", 26 | "pinia" 27 | ], 28 | "author": "likaia", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/likaia/vue-native-websocket-vue3/issues" 32 | }, 33 | "homepage": "https://github.com/likaia/vue-native-websocket-vue3#readme", 34 | "devDependencies": { 35 | "@commitlint/cli": "^11.0.0", 36 | "@commitlint/config-angular": "^11.0.0", 37 | "@typescript-eslint/eslint-plugin": "^2.33.0", 38 | "@typescript-eslint/parser": "^2.33.0", 39 | "@vue/cli-plugin-babel": "~4.5.0", 40 | "@vue/cli-plugin-eslint": "~4.5.0", 41 | "@vue/cli-plugin-typescript": "~4.5.0", 42 | "@vue/cli-service": "~4.5.0", 43 | "@vue/compiler-sfc": "^3.1.5", 44 | "@vue/eslint-config-prettier": "^6.0.0", 45 | "@vue/eslint-config-typescript": "^5.0.2", 46 | "commitizen": "^4.2.2", 47 | "cz-conventional-changelog": "^3.3.0", 48 | "eslint": "^6.7.2", 49 | "eslint-plugin-prettier": "^3.1.3", 50 | "eslint-plugin-vue": "^7.0.0-0", 51 | "husky": "^4.3.0", 52 | "prettier": "^1.19.1", 53 | "sass": "^1.26.5", 54 | "sass-loader": "^8.0.2", 55 | "typescript": "~3.9.3" 56 | }, 57 | "peerDependencies": { 58 | "core-js": "^3.6.5", 59 | "vue": "^3.0.0" 60 | }, 61 | "config": { 62 | "commitizen": { 63 | "path": "./node_modules/cz-conventional-changelog" 64 | } 65 | }, 66 | "husky": { 67 | "hooks": { 68 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 69 | } 70 | }, 71 | "dependencies": { 72 | "file-entry-cache": "^6.0.1" 73 | }, 74 | "files": [ 75 | "dist" 76 | ], 77 | "volta": { 78 | "node": "14.16.0", 79 | "yarn": "1.22.17" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 3.2.0 (2022-04-25) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * 更新插件文档 ([4d8fd98](https://github.com/likaia/vue-native-websocket-vue3/commit/4d8fd988efcbdb3786bffbca0bf8c96a6b86254b)) 7 | * 添加消息监听与发送消息的示例代码 ([ab7fb4c](https://github.com/likaia/vue-native-websocket-vue3/commit/ab7fb4cd58a35ab66f812b9b214c5325d3d55f59)) 8 | * **修复部分类型错误:** 添加英文注释 | add English Comment ([a439a0a](https://github.com/likaia/vue-native-websocket-vue3/commit/a439a0a27a25fcb7823fe2d20d22c8f695aa979c)) 9 | * 修复无法打包问题 ([3b2a703](https://github.com/likaia/vue-native-websocket-vue3/commit/3b2a703eb9f468498aa4f9fcff34dc7ed7c67ecd)) 10 | * **修复git commit规范不生效问题:** 使用新的包指定commit规范 ([93a7c1c](https://github.com/likaia/vue-native-websocket-vue3/commit/93a7c1c4a1a6c0ea3252e333e73b799364a1d169)) 11 | * 在globalProperties中添加消息监听,适配CompositionAPI ([a9ca073](https://github.com/likaia/vue-native-websocket-vue3/commit/a9ca073d066ecd4d5f3dc230462f56f6984b8acc)) 12 | * update .gitignore ([a154d53](https://github.com/likaia/vue-native-websocket-vue3/commit/a154d5336363913685f45a710d85e3f081316a3d)) 13 | 14 | 15 | ### Features 16 | 17 | * **更新版本号:** 更新日志记录 ([bb5555b](https://github.com/likaia/vue-native-websocket-vue3/commit/bb5555b49260358b6d591c64b7388ef076b08ebe)) 18 | * **使用vue3+typescript重构原来的插件:** 删除代码内已经废弃的vue2方法,替换为vue3支持的写法 ([d21f3b6](https://github.com/likaia/vue-native-websocket-vue3/commit/d21f3b6596db29a3c68d6ded71220636347a095d)) 19 | * 添加VueCLI作为打包工具 ([7475000](https://github.com/likaia/vue-native-websocket-vue3/commit/7475000d353102681edd00a2edf56764cd658231)) 20 | * add pinia support ([7298187](https://github.com/likaia/vue-native-websocket-vue3/commit/7298187cfb649bc4b8c007f7289cc6dae2a103ee)) 21 | * add sample code for message monitoring and message sending ([fa5f37a](https://github.com/likaia/vue-native-websocket-vue3/commit/fa5f37ae684fac833ffff7d48e33a2de9f5aaec4)) 22 | 23 | 24 | 25 | ## 3.0.2 (2020-11-20) 26 | 27 | 28 | ### Bug Fixes 29 | 30 | * **修复部分类型错误:** 添加英文注释 | add English Comment ([a439a0a](https://github.com/likaia/vue-native-websocket-vue3/commit/a439a0a27a25fcb7823fe2d20d22c8f695aa979c)) 31 | 32 | 33 | ## 3.0.1 (2020-11-04) 34 | 35 | 36 | ### Bug Fixes 37 | 38 | * **修复git commit规范不生效问题:** 使用新的包指定commit规范 ([93a7c1c](https://github.com/likaia/vue-native-websocket-vue3/commit/93a7c1c4a1a6c0ea3252e333e73b799364a1d169)) 39 | 40 | # 3.0.0 (2020-10-29) 41 | 42 | 43 | ### Features 44 | 45 | * **使用vue3+typescript重构原来的插件:** 删除代码内已经废弃的vue2方法,替换为vue3支持的写法 ([d21f3b6](https://github.com/nathantsoi/vue-native-websocket/commit/d21f3b6596db29a3c68d6ded71220636347a095d)) 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/socket-server/Emitter.ts: -------------------------------------------------------------------------------- 1 | export class Emitter { 2 | private listeners: Map; 3 | constructor() { 4 | this.listeners = new Map(); 5 | } 6 | /** 7 | * 添加事件监听 | Add event listener 8 | * @param label 事件名称 | Event name 9 | * @param callback 回调函数 | Callback 10 | * @param vm this对象 | this object 11 | * @return {boolean} 12 | */ 13 | addListener(label: T, callback: (...params: T[]) => void, vm: T): boolean { 14 | if (typeof callback === "function") { 15 | // label不存在就添加 | add if label does not exist 16 | this.listeners.has(label) || this.listeners.set(label, []); 17 | // 向label添加回调函数 | Add callback function to label 18 | this.listeners.get(label).push({ callback: callback, vm: vm }); 19 | return true; 20 | } 21 | return false; 22 | } 23 | 24 | /** 25 | * 移除监听 Remove monitor 26 | * @param label 事件名称 | Event name 27 | * @param callback 回调函数 | Callback 28 | * @param vm this对象 | this object 29 | * @return {boolean} 30 | */ 31 | removeListener(label: T, callback: () => void, vm: T): boolean { 32 | // 从监听列表中获取当前事件 | Get the current event from the listener list 33 | const listeners = this.listeners.get(label); 34 | let index; 35 | 36 | if (listeners && listeners.length) { 37 | // 寻找当前事件在事件监听列表的位置 | Find the position of the current event in the event monitoring list 38 | index = listeners.reduce((i: number, listener: any, index: number) => { 39 | if ( 40 | typeof listener.callback === "function" && 41 | listener.callback === callback && 42 | listener.vm === vm 43 | ) { 44 | i = index; 45 | } 46 | return i; 47 | }, -1); 48 | 49 | if (index > -1) { 50 | // 移除事件 | Remove event 51 | listeners.splice(index, 1); 52 | this.listeners.set(label, listeners); 53 | return true; 54 | } 55 | } 56 | return false; 57 | } 58 | 59 | /** 60 | * 触发监听 | Trigger monitor 61 | * @param label 事件名称 | Event name 62 | * @param args 参数 | parameter 63 | * @return {boolean} 64 | */ 65 | emit(label: string, ...args: T[]): boolean { 66 | // 获取事件列表中存储的事件 | Get events stored in the event list 67 | const listeners = this.listeners.get(label); 68 | 69 | if (listeners && listeners.length) { 70 | listeners.forEach( 71 | (listener: { callback: (...params: T[]) => void; vm: T }) => { 72 | // 扩展callback函数,让其拥有listener.vm中的方法 | Extend the callback function to have methods in listener.vm 73 | listener.callback.call(listener.vm, ...args); 74 | } 75 | ); 76 | return true; 77 | } 78 | return false; 79 | } 80 | } 81 | 82 | export default new Emitter(); 83 | -------------------------------------------------------------------------------- /src/Main.ts: -------------------------------------------------------------------------------- 1 | import Observer from "./socket-server/Observer"; 2 | import Emitter from "./socket-server/Emitter"; 3 | import { websocketOpts } from "./type/PluginsType"; 4 | // eslint-disable-next-line @typescript-eslint/ban-ts-ignore 5 | // @ts-ignore 6 | import { App } from "vue"; 7 | 8 | export default { 9 | install( 10 | app: App, 11 | connection: string, 12 | opts: websocketOpts = { format: "" } 13 | ): void { 14 | // 没有传入连接,抛出异常 | No incoming connection, throw an exception 15 | if (!connection) { 16 | throw new Error("[vue-native-socket] cannot locate connection"); 17 | } 18 | 19 | let observer: Observer; 20 | 21 | opts.$setInstance = (wsInstance: EventTarget) => { 22 | // 全局属性添加$socket | Add $socket to global properties 23 | app.config.globalProperties.$socket = wsInstance; 24 | }; 25 | 26 | // 配置选项中启用手动连接 | Enable manual connection in configuration options 27 | if (opts.connectManually) { 28 | app.config.globalProperties.$connect = ( 29 | connectionUrl = connection, 30 | connectionOpts = opts 31 | ) => { 32 | // 调用者传入的参数中添加set实例 | Add a set instance to the parameters passed by the caller 33 | connectionOpts.$setInstance = opts.$setInstance; 34 | // 创建Observer建立websocket连接 | Create Observer to establish websocket connection 35 | observer = new Observer(connectionUrl, connectionOpts); 36 | // 全局添加$socket | Add $socket globally 37 | app.config.globalProperties.$socket = observer.WebSocket; 38 | }; 39 | 40 | // 全局添加连接断开处理函数 | Globally add disconnection processing functions 41 | app.config.globalProperties.$disconnect = () => { 42 | if (observer && observer.reconnection) { 43 | // 重新连接状态改为false | Change the reconnection status to false 44 | observer.reconnection = false; 45 | // 移除重新连接计时器 | Remove the reconnection timer 46 | clearTimeout(observer.reconnectTimeoutId); 47 | } 48 | // 如果全局属性socket存在则从全局属性移除 | If the global attribute socket exists, remove it from the global attribute 49 | if (app.config.globalProperties.$socket) { 50 | // 关闭连接 | Close the connection 51 | app.config.globalProperties.$socket.close(); 52 | delete app.config.globalProperties.$socket; 53 | } 54 | }; 55 | } else { 56 | // 未启用手动连接 | Manual connection is not enabled 57 | observer = new Observer(connection, opts); 58 | // 全局添加$socket属性,连接至websocket服务器 | Add the $socket attribute globally to connect to the websocket server 59 | app.config.globalProperties.$socket = observer.WebSocket; 60 | } 61 | const hasProxy = 62 | typeof Proxy !== "undefined" && 63 | typeof Proxy === "function" && 64 | /native code/.test(Proxy.toString()); 65 | 66 | app.mixin({ 67 | created() { 68 | // eslint-disable-next-line @typescript-eslint/no-this-alias 69 | const vm = this; 70 | const sockets = this.$options["sockets"]; 71 | 72 | if (hasProxy) { 73 | this.$options.sockets = new Proxy( 74 | {}, 75 | { 76 | set(target: any, key: any, value: any): boolean { 77 | // 添加监听 | Add monitor 78 | Emitter.addListener(key, value, vm); 79 | target[key] = value; 80 | return true; 81 | }, 82 | deleteProperty(target: { key: any }, key: any): boolean { 83 | // 移除监听 | Remove monitor 84 | Emitter.removeListener(key, vm.$options.sockets[key], vm); 85 | delete target.key; 86 | return true; 87 | } 88 | } 89 | ); 90 | app.config.globalProperties.sockets = new Proxy( 91 | {}, 92 | { 93 | set(target: any, key: any, value: any): boolean { 94 | // 添加监听 | Add monitor 95 | Emitter.addListener(key, value, vm); 96 | target[key] = value; 97 | return true; 98 | }, 99 | deleteProperty(target: { key: any }, key: any): boolean { 100 | // 移除监听 | Remove monitor 101 | Emitter.removeListener(key, vm.$options.sockets[key], vm); 102 | delete target.key; 103 | return true; 104 | } 105 | } 106 | ); 107 | if (sockets) { 108 | Object.keys(sockets).forEach((key: string) => { 109 | // 给$options中添加sockets中的key | Add the key in sockets to $options 110 | this.$options.sockets[key] = sockets[key]; 111 | app.config.globalProperties.sockets[key] = sockets[key]; 112 | }); 113 | } 114 | } else { 115 | // 将对象密封,不能再进行改变 | Seal the object so that it cannot be changed 116 | Object.seal(this.$options.sockets); 117 | Object.seal(app.config.globalProperties.sockets); 118 | if (sockets) { 119 | Object.keys(sockets).forEach((key: string) => { 120 | // 添加监听 | Add monitor 121 | Emitter.addListener(key, sockets[key], vm); 122 | }); 123 | } 124 | } 125 | }, 126 | beforeUnmount() { 127 | if (hasProxy) { 128 | const sockets = this.$options["sockets"]; 129 | 130 | if (sockets) { 131 | Object.keys(sockets).forEach((key: string) => { 132 | // 销毁前如果代理存在sockets存在则移除$options中给sockets添加过的key | If the proxy has sockets before destruction, remove the keys added to sockets in $options 133 | delete this.$options.sockets[key]; 134 | delete app.config.globalProperties.sockets; 135 | }); 136 | } 137 | } 138 | } 139 | }); 140 | } 141 | }; 142 | -------------------------------------------------------------------------------- /src/socket-server/Observer.ts: -------------------------------------------------------------------------------- 1 | import Emitter from "./Emitter"; 2 | import { websocketOpts } from "@/type/pluginsType"; 3 | 4 | export default class { 5 | private readonly format: string; // 数据传输格式 | Data transmission format 6 | private readonly connectionUrl: string; // 连接url | Connection url 7 | private readonly opts: websocketOpts; // 调用者可以传入的自定义参数 | Custom parameters that the caller can pass in 8 | public reconnection: boolean; // 是否开启重连 | Whether to enable reconnection 9 | private readonly reconnectionAttempts: number; // 最大重连次数 | Maximum number of reconnections 10 | private readonly reconnectionDelay: number; // 重连间隔时间 | Reconnection interval 11 | public reconnectTimeoutId = 0; // 重连超时id | Reconnect timeout id 12 | private reconnectionCount = 0; // 已重连次数 | Reconnected times 13 | private readonly passToStoreHandler: any; // 传输数据时的处理函数 | Processing function when transferring data 14 | private readonly store: any; // 启用vuex时传入vuex的store | Pass in vuex store when vuex is enabled 15 | private readonly mutations: any; // 启用vuex时传入vuex中的mutations | Pass in the mutations in vuex when vuex is enabled 16 | public WebSocket: WebSocket | undefined; // websocket连接 | websocket connection 17 | /** 18 | * 观察者模式, websocket服务核心功能封装 | Observer mode, websocket service core function package 19 | * @param connectionUrl 连接的url 20 | * @param opts 其它配置项 | Other configuration items 21 | */ 22 | constructor(connectionUrl: string, opts: websocketOpts = { format: "" }) { 23 | // 获取参数中的format并将其转成小写 | Get the format in the parameter and convert it to lowercase 24 | this.format = opts.format && opts.format.toLowerCase(); 25 | 26 | // 如果url以//开始对其进行处理添加正确的websocket协议前缀 | If the URL starts with // to process it, add the correct websocket protocol prefix 27 | if (connectionUrl.startsWith("//")) { 28 | // 当前网站如果为https请求则添加wss前缀否则添加ws前缀 | If the current website is an https request, add the wss prefix, otherwise add the ws prefix 29 | const scheme = window.location.protocol === "https:" ? "wss" : "ws"; 30 | connectionUrl = `${scheme}:${connectionUrl}`; 31 | } 32 | // 将处理好的url和opts赋值给当前类内部变量 | Assign the processed url and opts to the internal variables of the current class 33 | this.connectionUrl = connectionUrl; 34 | this.opts = opts; 35 | this.reconnection = this.opts.reconnection || false; 36 | this.reconnectionAttempts = this.opts.reconnectionAttempts || Infinity; 37 | this.reconnectionDelay = this.opts.reconnectionDelay || 1000; 38 | this.passToStoreHandler = this.opts.passToStoreHandler; 39 | 40 | // 建立连接 | establish connection 41 | this.connect(connectionUrl, opts); 42 | 43 | // 如果配置参数中有传store就将store赋值 | If store is passed in the configuration parameters, store will be assigned 44 | if (opts.store) { 45 | this.store = opts.store; 46 | } 47 | // 如果配置参数中有传vuex的同步处理函数就将mutations赋值 | If there is a synchronization processing function that passes vuex in the configuration parameters, assign mutations 48 | if (opts.mutations) { 49 | this.mutations = opts.mutations; 50 | } 51 | // 事件触发 52 | this.onEvent(); 53 | } 54 | 55 | // 连接websocket | Connect websocket 56 | connect( 57 | connectionUrl: string, 58 | opts: websocketOpts = { format: "" } 59 | ): WebSocket { 60 | // 获取配置参数传入的协议 | Get the protocol passed in the configuration parameter 61 | const protocol = opts.protocol || ""; 62 | // 如果没传协议就建立一个正常的websocket连接否则就创建带协议的websocket连接 | If no protocol is passed, establish a normal websocket connection, otherwise, create a websocket connection with protocol 63 | this.WebSocket = 64 | opts.WebSocket || 65 | (protocol === "" 66 | ? new WebSocket(connectionUrl) 67 | : new WebSocket(connectionUrl, protocol)); 68 | // 启用json发送 | Enable json sending 69 | if (this.format === "json") { 70 | // 如果websocket中没有senObj就添加这个方法对象 | If there is no sen Obj in websocket, add this method object 71 | if (!("sendObj" in (this.WebSocket as WebSocket))) { 72 | // 将发送的消息转为json字符串 | Convert the sent message into a json string 73 | (this.WebSocket as WebSocket).sendObj = (obj: JSON) => 74 | (this.WebSocket as WebSocket).send(JSON.stringify(obj)); 75 | } 76 | } 77 | return this.WebSocket; 78 | } 79 | // 重新连接 | reconnect 80 | reconnect(): void { 81 | // 已重连次数小于等于设置的连接次数时执行重连 | Reconnect when the number of reconnections is less than or equal to the set connection times 82 | if (this.reconnectionCount <= this.reconnectionAttempts) { 83 | this.reconnectionCount++; 84 | // 清理上一次重连时的定时器 | Clear the timer of the last reconnection 85 | window.clearTimeout(this.reconnectTimeoutId); 86 | // 开始重连 87 | this.reconnectTimeoutId = window.setTimeout(() => { 88 | // 如果启用vuex就触发vuex中的重连方法 | If vuex is enabled, the reconnection method in vuex is triggered 89 | if (this.store) { 90 | this.passToStore("SOCKET_RECONNECT", this.reconnectionCount); 91 | } 92 | // 重新连接 | reconnect 93 | this.connect(this.connectionUrl, this.opts); 94 | // 触发WebSocket事件 | Trigger Web Socket events 95 | this.onEvent(); 96 | }, this.reconnectionDelay); 97 | } else { 98 | // 如果启用vuex则触发重连失败方法 | If vuex is enabled, the reconnection failure method is triggered 99 | if (this.store) { 100 | this.passToStore("SOCKET_RECONNECT_ERROR", true); 101 | } 102 | } 103 | } 104 | 105 | // 事件分发 | Event distribution 106 | onEvent(): void { 107 | ["onmessage", "onclose", "onerror", "onopen"].forEach( 108 | (eventType: string) => { 109 | // eslint-disable-next-line @typescript-eslint/ban-ts-ignore 110 | // @ts-ignore 111 | (this.WebSocket as WebSocket)[eventType] = (event: any) => { 112 | Emitter.emit(eventType, event); 113 | 114 | // 调用vuex中对应的方法 | Call the corresponding method in vuex 115 | if (this.store) { 116 | this.passToStore("SOCKET_" + eventType, event); 117 | } 118 | 119 | // 处于重新连接状态切事件为onopen时执行 | Execute when the event is onopen in the reconnect state 120 | if (this.reconnection && eventType === "onopen") { 121 | // 设置实例 | Setting example 122 | this.opts.$setInstance && 123 | this.opts.$setInstance(event.currentTarget); 124 | // 清空重连次数 | Empty reconnection times 125 | this.reconnectionCount = 0; 126 | } 127 | 128 | // 如果处于重连状态且事件为onclose时调用重连方法 | If in the reconnect state and the event is onclose, call the reconnect method 129 | if (this.reconnection && eventType === "onclose") { 130 | this.reconnect(); 131 | } 132 | }; 133 | } 134 | ); 135 | } 136 | 137 | /** 138 | * 触发vuex中的方法 | Trigger methods in vuex 139 | * @param eventName 事件名称 140 | * @param event 事件 141 | */ 142 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types 143 | passToStore(eventName: string, event: any): void { 144 | // 如果参数中有传事件处理函数则执行自定义的事件处理函数,否则执行默认的处理函数 | If there is an event processing function in the parameter, the custom event processing function is executed, otherwise the default processing function is executed 145 | if (this.passToStoreHandler) { 146 | this.passToStoreHandler( 147 | eventName, 148 | event, 149 | this.defaultPassToStore.bind(this) 150 | ); 151 | } else { 152 | this.defaultPassToStore(eventName, event); 153 | } 154 | } 155 | 156 | /** 157 | * 默认的事件处理函数 | The default event handler 158 | * @param eventName 事件名称 159 | * @param event 事件 160 | */ 161 | defaultPassToStore( 162 | eventName: string, 163 | event: { 164 | data: string; 165 | mutation: string; 166 | namespace: string; 167 | action: string; 168 | } 169 | ): void { 170 | // 事件名称开头不是SOCKET_则终止函数 | If the beginning of the event name is not SOCKET_ then terminate the function 171 | if (!eventName.startsWith("SOCKET_")) { 172 | return; 173 | } 174 | let method = "commit"; 175 | // 事件名称字母转大写 | Turn the letter of the event name to uppercase 176 | let target = eventName.toUpperCase(); 177 | // 消息内容 | Message content 178 | let msg = event; 179 | // data存在且数据为json格式 | data exists and the data is in json format 180 | if (this.format === "json" && event.data) { 181 | // 将data从json字符串转为json对象 | Convert data from json string to json object 182 | msg = JSON.parse(event.data); 183 | // 判断msg是同步还是异步 | Determine whether msg is synchronous or asynchronous 184 | if (msg.mutation) { 185 | target = [msg.namespace || "", msg.mutation] 186 | .filter((e: string) => !!e) 187 | .join("/"); 188 | } else if (msg.action) { 189 | method = "dispatch"; 190 | target = [msg.namespace || "", msg.action] 191 | .filter((e: string) => !!e) 192 | .join("/"); 193 | } 194 | } 195 | if (this.mutations) { 196 | target = this.mutations[target] || target; 197 | } 198 | // 触发store中的方法 | Trigger the method in the store 199 | if (this.store._p) { 200 | // pinia 201 | target = eventName.toUpperCase(); 202 | this.store[target](msg); 203 | } else { 204 | // vuex 205 | this.store[method](target, msg); 206 | } 207 | } 208 | } 209 | 210 | // 扩展全局对象 | Extended global object 211 | declare global { 212 | // 扩展websocket对象,添加sendObj方法 | Extend websocket object, add send Obj method 213 | interface WebSocket { 214 | sendObj(obj: JSON): void; 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-native-websocket-vue3 · [![npm version](https://img.shields.io/badge/npm-v3.1.8-2081C1)](https://www.npmjs.com/package/vue-native-websocket-vue3) [![yarn version](https://img.shields.io/badge/yarn-v3.1.8-F37E42)](https://classic.yarnpkg.com/zh-Hans/package/vue-native-websocket-vue3) 2 | 仅支持vue3的websocket插件 | Only supports vue3 websocket plugin 3 | 4 | English documents please move: [README-EN.md](README-EN.md) 5 | 6 | 对Pinia进行兼容处理(感谢[@chuck](https://github.com/FrontToEnd)所提供的兼容代码) 7 | 8 | ## 插件安装 9 | ```bash 10 | yarn add vue-native-websocket-vue3 11 | 12 | # or 13 | 14 | npm install vue-native-websocket-vue3 --save 15 | ``` 16 | ## 插件使用 17 | 如果你的项目启用了TypeScript,则在`main.ts`文件中导入并使用插件。 18 | 19 | 没有启用就在`main.js`中导入并使用。 20 | 21 | 使用插件时,第二个参数为必填项,是你的`websocket`服务端连接地址。 22 | 23 | ```typescript 24 | import VueNativeSock from "vue-native-websocket-vue3"; 25 | 26 | // 使用VueNativeSock插件,并进行相关配置 27 | app.use(VueNativeSock,""); 28 | ``` 29 | 30 | > 注意:插件依赖于Vuex,你的项目一定要安装vuex才可以使用本插件。vuex的相关配置请查阅文档后面的插件配置项中的内容。 31 | 32 | > 同样的,插件也支持pinia,vuex与pinia任选其一即可。pinia的相关使用配置请请查阅文档后面的插件配置项中的内容。 33 | 34 | ### 插件配置项 35 | 插件提供了一些配置选项,提高了插件的灵活度,能更好的适配开发者的业务需求。 36 | 37 | #### 启用Vuex集成 38 | 在`main.ts | main.js`中导入`vuex`的配置文件,在使用插件时,第三个参数就是用户可以传配置项,他为一个对象类型,在对象中加入`store`属性,值为导入的vuex。 39 | 40 | ```typescript 41 | import store from "./store"; 42 | 43 | app.use(VueNativeSock,"",{ 44 | store: store 45 | }); 46 | ``` 47 | > 如果你仍然不知道怎么用,可以去参考我的另一个开源项目[chat-system](https://github.com/likaia/chat-system/blob/master/src/main.ts)。 48 | 49 | 如果启用了vuex集成,就需要在其配置文件中定义state以及mutations方法。mutations中定义的方法为websocket的6个监听,你可以在这几个监听中做相应的操作。 50 | ```typescript 51 | import { createStore } from "vuex"; 52 | import main from "../main"; 53 | 54 | export default createStore({ 55 | state: { 56 | socket: { 57 | // 连接状态 58 | isConnected: false, 59 | // 消息内容 60 | message: "", 61 | // 重新连接错误 62 | reconnectError: false, 63 | // 心跳消息发送时间 64 | heartBeatInterval: 50000, 65 | // 心跳定时器 66 | heartBeatTimer: 0 67 | } 68 | }, 69 | mutations: { 70 | // 连接打开 71 | SOCKET_ONOPEN(state, event) { 72 | main.config.globalProperties.$socket = event.currentTarget; 73 | state.socket.isConnected = true; 74 | // 连接成功时启动定时发送心跳消息,避免被服务器断开连接 75 | state.socket.heartBeatTimer = setInterval(() => { 76 | const message = "心跳消息"; 77 | state.socket.isConnected && 78 | main.config.globalProperties.$socket.sendObj({ 79 | code: 200, 80 | msg: message 81 | }); 82 | }, state.socket.heartBeatInterval); 83 | }, 84 | // 连接关闭 85 | SOCKET_ONCLOSE(state, event) { 86 | state.socket.isConnected = false; 87 | // 连接关闭时停掉心跳消息 88 | clearInterval(state.socket.heartBeatTimer); 89 | state.socket.heartBeatTimer = 0; 90 | console.log("连接已断开: " + new Date()); 91 | console.log(event); 92 | }, 93 | // 发生错误 94 | SOCKET_ONERROR(state, event) { 95 | console.error(state, event); 96 | }, 97 | // 收到服务端发送的消息 98 | SOCKET_ONMESSAGE(state, message) { 99 | state.socket.message = message; 100 | }, 101 | // 自动重连 102 | SOCKET_RECONNECT(state, count) { 103 | console.info("消息系统重连中...", state, count); 104 | }, 105 | // 重连错误 106 | SOCKET_RECONNECT_ERROR(state) { 107 | state.socket.reconnectError = true; 108 | } 109 | }, 110 | modules: {} 111 | }); 112 | ``` 113 | ##### 自定义方法名 114 | 你也可以自定义`mutations`中自定义websocket的默认监听事件名。 115 | ```typescript 116 | // mutation-types.ts 117 | const SOCKET_ONOPEN = '✅ Socket connected!' 118 | const SOCKET_ONCLOSE = '❌ Socket disconnected!' 119 | const SOCKET_ONERROR = '❌ Socket Error!!!' 120 | const SOCKET_ONMESSAGE = 'Websocket message received' 121 | const SOCKET_RECONNECT = 'Websocket reconnected' 122 | const SOCKET_RECONNECT_ERROR = 'Websocket is having issues reconnecting..' 123 | 124 | export { 125 | SOCKET_ONOPEN, 126 | SOCKET_ONCLOSE, 127 | SOCKET_ONERROR, 128 | SOCKET_ONMESSAGE, 129 | SOCKET_RECONNECT, 130 | SOCKET_RECONNECT_ERROR 131 | } 132 | 133 | // store.ts 134 | import { createStore } from "vuex"; 135 | import main from "../main"; 136 | import { 137 | SOCKET_ONOPEN, 138 | SOCKET_ONCLOSE, 139 | SOCKET_ONERROR, 140 | SOCKET_ONMESSAGE, 141 | SOCKET_RECONNECT, 142 | SOCKET_RECONNECT_ERROR 143 | } from "./mutation-types" 144 | 145 | export default createStore({ 146 | state: { 147 | socket: { 148 | isConnected: false, 149 | message: '', 150 | reconnectError: false, 151 | } 152 | }, 153 | mutations: { 154 | [SOCKET_ONOPEN](state, event) { 155 | state.socket.isConnected = true 156 | }, 157 | [SOCKET_ONCLOSE](state, event) { 158 | state.socket.isConnected = false 159 | }, 160 | [SOCKET_ONERROR](state, event) { 161 | console.error(state, event) 162 | }, 163 | // default handler called for all methods 164 | [SOCKET_ONMESSAGE](state, message) { 165 | state.socket.message = message 166 | }, 167 | // mutations for reconnect methods 168 | [SOCKET_RECONNECT](state, count) { 169 | console.info(state, count) 170 | }, 171 | [SOCKET_RECONNECT_ERROR](state) { 172 | state.socket.reconnectError = true; 173 | } 174 | }, 175 | modules: {} 176 | }); 177 | 178 | // main.ts 179 | import store from './store' 180 | import { 181 | SOCKET_ONOPEN, 182 | SOCKET_ONCLOSE, 183 | SOCKET_ONERROR, 184 | SOCKET_ONMESSAGE, 185 | SOCKET_RECONNECT, 186 | SOCKET_RECONNECT_ERROR 187 | } from './mutation-types' 188 | 189 | const mutations = { 190 | SOCKET_ONOPEN, 191 | SOCKET_ONCLOSE, 192 | SOCKET_ONERROR, 193 | SOCKET_ONMESSAGE, 194 | SOCKET_RECONNECT, 195 | SOCKET_RECONNECT_ERROR 196 | } 197 | 198 | app.use(VueNativeSock,"",{ 199 | store: store, 200 | mutations: mutations 201 | }); 202 | ``` 203 | 204 | #### 启用pinia集成 205 | 在`main.js | main.ts`中导入`pinia`的配置文件。 206 | ```typescript 207 | // useSocketStore为pinia的socket配置文件 208 | import { useSocketStoreWithOut } from './useSocketStore'; 209 | 210 | const store = useSocketStoreWithOut(); 211 | 212 | app.use(VueNativeSock, "", { 213 | store: store 214 | }); 215 | ``` 216 | > 我专门写了一个demo用来演示pinia的集成,如果你需要参考的话请移步:[pinia-websocket-project](https://github.com/likaia/pinia-websocket-project) 217 | 218 | pinia的socket配置文件代码如下: 219 | ```typescript 220 | import { defineStore } from 'pinia'; 221 | import { store } from '/@/store'; 222 | import main from '/@/main'; 223 | 224 | interface SocketStore { 225 | // 连接状态 226 | isConnected: boolean; 227 | // 消息内容 228 | message: string; 229 | // 重新连接错误 230 | reconnectError: boolean; 231 | // 心跳消息发送时间 232 | heartBeatInterval: number; 233 | // 心跳定时器 234 | heartBeatTimer: number; 235 | } 236 | 237 | export const useSocketStore = defineStore({ 238 | id: 'socket', 239 | state: (): SocketStore => ({ 240 | // 连接状态 241 | isConnected: false, 242 | // 消息内容 243 | message: '', 244 | // 重新连接错误 245 | reconnectError: false, 246 | // 心跳消息发送时间 247 | heartBeatInterval: 50000, 248 | // 心跳定时器 249 | heartBeatTimer: 0, 250 | }), 251 | actions: { 252 | // 连接打开 253 | SOCKET_ONOPEN(event) { 254 | main.config.globalProperties.$socket = event.currentTarget; 255 | this.isConnected = true; 256 | // 连接成功时启动定时发送心跳消息,避免被服务器断开连接 257 | this.heartBeatTimer = window.setInterval(() => { 258 | const message = '心跳消息'; 259 | this.isConnected && 260 | main.config.globalProperties.$socket.sendObj({ 261 | code: 200, 262 | msg: message, 263 | }); 264 | }, this.heartBeatInterval); 265 | }, 266 | // 连接关闭 267 | SOCKET_ONCLOSE(event) { 268 | this.isConnected = false; 269 | // 连接关闭时停掉心跳消息 270 | window.clearInterval(this.heartBeatTimer); 271 | this.heartBeatTimer = 0; 272 | console.log('连接已断开: ' + new Date()); 273 | console.log(event); 274 | }, 275 | // 发生错误 276 | SOCKET_ONERROR(event) { 277 | console.error(event); 278 | }, 279 | // 收到服务端发送的消息 280 | SOCKET_ONMESSAGE(message) { 281 | this.message = message; 282 | }, 283 | // 自动重连 284 | SOCKET_RECONNECT(count) { 285 | console.info('消息系统重连中...', count); 286 | }, 287 | // 重连错误 288 | SOCKET_RECONNECT_ERROR() { 289 | this.reconnectError = true; 290 | }, 291 | }, 292 | }); 293 | 294 | // Need to be used outside the setup 295 | export function useSocketStoreWithOut() { 296 | return useSocketStore(store); 297 | } 298 | ``` 299 | 300 | 为了方便在组件外面使用pinia,这里额外导出了`useSocketStoreWithOut`,否则pinia会报错,提示找不到pinia实例。 301 | 302 | pinia的store配置代码如下: 303 | ```typescript 304 | import type { App } from 'vue'; 305 | import { createPinia } from 'pinia'; 306 | 307 | const store = createPinia(); 308 | 309 | export function setupStore(app: App) { 310 | app.use(store); 311 | } 312 | 313 | export { store }; 314 | ``` 315 | 316 | 317 | #### 其它配置 318 | > 下述方法,均为插件的可传参数,可以和`store`搭配使用 319 | 320 | * 设置websocket子协议默,认为空字符串。 321 | ```json 322 | { 323 | "protocol": "my-protocol" 324 | } 325 | ``` 326 | * 启用JSON消息传递,开启后数据发送与接收均采用json作为数据格式。 327 | ```json 328 | { 329 | "format": "json" 330 | } 331 | ``` 332 | 333 | > 如果你没启用JSON消息传递,只能使用`send`方法来发送消息. 334 | 335 | * 启用自动重连`reconnection`,启用时可配置重连次数`reconnectionAttempts`与重连间隔时长`reconnectionDelay` 336 | ```json 337 | { 338 | "reconnection": true, 339 | "reconnectionAttempts": 5, 340 | "reconnectionDelay": 3000 341 | } 342 | ``` 343 | 344 | * 手动管理连接 345 | ```json 346 | { 347 | "connectManually": true 348 | } 349 | ``` 350 | 启用手动管理连接后,项目启动时则不会自动连接,你可以在项目的特定组件调用连接方法来进行连接。在组件销毁时调用关闭方法来关闭连接。 351 | > 如果你启用了手动连接,必须要要启用vuex,否则此设置将不会生效。 352 | ```typescript 353 | // 连接websocket服务器,参数为websocket服务地址 354 | this.$connect(""); 355 | // 关闭连接 356 | this.$disconnect(); 357 | 358 | // CompositionAPI 359 | proxy.$connect(""); 360 | proxy.$disconnect(""); 361 | ``` 362 | * 自定义socket事件处理 363 | 触发vuex里的mutations事件时,你可以选择自己写函数处理,做你想做的事情,在使用插件时传入`passToStoreHandler`参数即可,如果你没有传则走默认的处理函数,默认函数的定义如下: 364 | ```typescript 365 | export default class { 366 | /** 367 | * 默认的事件处理函数 368 | * @param eventName 事件名称 369 | * @param event 事件 370 | */ 371 | defaultPassToStore( 372 | eventName: string, 373 | event: { 374 | data: string; 375 | mutation: string; 376 | namespace: string; 377 | action: string; 378 | } 379 | ): void { 380 | // 事件名称开头不是SOCKET_则终止函数 381 | if (!eventName.startsWith("SOCKET_")) { 382 | return; 383 | } 384 | let method = "commit"; 385 | // 事件名称字母转大写 386 | let target = eventName.toUpperCase(); 387 | // 消息内容 388 | let msg = event; 389 | // data存在且数据为json格式 390 | if (this.format === "json" && event.data) { 391 | // 将data从json字符串转为json对象 392 | msg = JSON.parse(event.data); 393 | // 判断msg是同步还是异步 394 | if (msg.mutation) { 395 | target = [msg.namespace || "", msg.mutation].filter((e: string) => !!e).join("/"); 396 | } else if (msg.action) { 397 | method = "dispatch"; 398 | target = [msg.namespace || "", msg.action].filter((e: string) => !!e).join("/"); 399 | } 400 | } 401 | if (this.mutations) { 402 | target = this.mutations[target] || target; 403 | } 404 | // 触发store中的方法 | Trigger the method in the store 405 | if (this.store._p) { 406 | // pinia 407 | target = eventName.toUpperCase(); 408 | this.store[target](msg); 409 | } else { 410 | // vuex 411 | this.store[method](target, msg); 412 | } 413 | } 414 | } 415 | ``` 416 | 当你要自定义一个函数时,这个函数接收3个参数: 417 | * event name 事件名 418 | * event 事件 419 | * 默认事件处理,这使你可以选择将事件移交给原始处理程序之前进行一些基本的预处理 420 | 421 | 下面是一个例子 422 | ```typescript 423 | app.use(VueNativeSock, "", { 424 | passToStoreHandler: function (eventName, event, next) { 425 | event.data = event.should_have_been_named_data 426 | next(eventName, event) 427 | } 428 | }) 429 | ``` 430 | 431 | ### 插件暴露的函数 432 | * `send` 发送非json类型的数据(使用插件时不能启用JSON消息传递) 433 | * `sendObj` 发送json类型的数据(必须在使用插件时启用JSON消息传递) 434 | * `$connect` 连接websocket服务器(必须在使用插件时启用手动管理连接选项) 435 | * `onmessage` 收到服务端推送消息时的监听 436 | * `$disconnect` 断开websocket连接 437 | 438 | > 注意:上述方法均支持在optionsAPI与CompositionAPI中使用,具体的用法请查阅相关函数的文档。 439 | 440 | ### 在组件中使用 441 | 做完上述配置后,就可以在组件中使用了,如下所示为发送数据的例子。 442 | ```typescript 443 | export default defineComponent({ 444 | methods: { 445 | clickButton: function(val) { 446 | // 调用send方法,以字符串形式发送数据 447 | this.$socket.send('some data'); 448 | // 如果fomat配置为了json,即可调用sendObj方法来发送数据 449 | this.$socket.sendObj({ awesome: 'data'} ); 450 | } 451 | } 452 | }) 453 | ``` 454 | 455 | > 注意:`sendObj`方法必须在你启用JSON消息传递时才可以使用,不然只能使用`send`方法。 456 | 457 | 消息监听,即接收websocket服务端推送的消息,如下所示为消息监听的示例代码。 458 | ```typescript 459 | // optionsAPI用法 460 | this.$options.sockets.onmessage = (res: { data: string }) => { 461 | console.log(data); 462 | } 463 | 464 | // CompositionAPI用法 465 | import { getCurrentInstance } from "vue"; 466 | const { proxy } = getCurrentInstance() as ComponentInternalInstance; 467 | proxy.$socket.onmessage = (res: { 468 | data: string; 469 | }) => { 470 | console.log(data); 471 | } 472 | ``` 473 | 474 | 发送消息,向服务端推送消息 475 | ```typescript 476 | // optionsAPI用法 477 | this.$socket.sendObj({msg: '消息内容'}); 478 | 479 | // compositionAPI写法 480 | const internalInstance = data.currentInstance; 481 | internalInstance?.proxy.$socket.sendObj({ 482 | msg: "消息内容" 483 | }); 484 | ``` 485 | > compositionAPI写法由于在setup中无法拿到vue实例,因此需要在页面挂载后将实例存储到全局对象中,用的时候再将实例取出来。详细使用方法可以参考我的chat-system中的写法:[InitData.ts#L91](https://github.com/likaia/chat-system/blob/cacf587061f3a56198ade33a2c5bebeacec004a5/src/module/message-display/main-entrance/InitData.ts#L91) 、[EventMonitoring.ts#L50](https://github.com/likaia/chat-system/blob/db35173c8e54834a117ac8cb5a3753e75d9b1161/src/module/message-display/main-entrance/EventMonitoring.ts#L50) 、[SendMessage.ts#L73](https://github.com/likaia/chat-system/blob/db35173c8e54834a117ac8cb5a3753e75d9b1161/src/module/message-display/components-metords/SendMessage.ts#L73) 、[contact-list.vue#L620](https://github.com/likaia/chat-system/blob/91fe072a20d0928ff2af6c1bf56cedd0e545d0d5/src/views/contact-list.vue#L620) 486 | 487 | 移除消息监听 488 | ```typescript 489 | delete this.$options.sockets.onmessage 490 | // compositionAPI写法 491 | delete proxy.$socket.onmessage 492 | ``` 493 | 494 | ## 写在最后 495 | 至此,插件的所有使用方法就介绍完了。 496 | 497 | 想进一步了解插件源码的请移步项目的GitHub仓库:[vue-native-websocket-vue3](https://github.com/likaia/vue-native-websocket-vue3) 498 | 499 | Vue2版本请移步原插件:[vue-native-websocket](https://github.com/nathantsoi/vue-native-websocket) 500 | -------------------------------------------------------------------------------- /README-EN.md: -------------------------------------------------------------------------------- 1 | # vue-native-websocket-vue3 · [![npm version](https://img.shields.io/badge/npm-v3.1.6-2081C1)](https://www.npmjs.com/package/vue-native-websocket-vue3) [![yarn version](https://img.shields.io/badge/yarn-v3.1.6-F37E42)](https://classic.yarnpkg.com/zh-Hans/package/vue-native-websocket-vue3) 2 | Only supports vue3 websocket plugin 3 | 4 | Chinese documents please move: [README.md](README.md) 5 | 6 | 7 | 8 | Compatible handling for Pinia(Thanks to [@chuck](https:github.com Front To End) for the compatible code) 9 | 10 | ## Plug-in installation 11 | ```bash 12 | yarn add vue-native-websocket-vue3 13 | 14 | # or 15 | 16 | npm install vue-native-websocket-vue3 --save 17 | ``` 18 | 19 | ## Plug-in use 20 | If Type Script is enabled in your project, import and use the plugin in the `main.ts` file. 21 | 22 | Import and use it in `main.js` if it is not enabled. 23 | 24 | When using the plug-in, the second parameter is required and is your `websocket` server connection address. 25 | 26 | ```typescript 27 | import VueNativeSock from "vue-native-websocket-vue3"; 28 | 29 | // Use the Vue Native Sock plug-in and perform related configuration 30 | app.use(VueNativeSock,""); 31 | ``` 32 | 33 | > payAttention:The plugin depends on Vuex, your project must install vuex to use this plugin. For the relevant configuration of vuex, please refer to the content in the plug-in configuration item at the back of the document. 34 | 35 | > Similarly, the plugin also supports pinia, you can choose one of vuex and pinia. Please refer to the content in the plug-in configuration item at the back of the document for the related usage configuration of pinia. 36 | 37 | ### Plug-in configuration items 38 | The plug-in provides some configuration options, which improves the flexibility of the plug-in and better adapts to the business needs of developers. 39 | 40 | #### Enable Vuex integration 41 | Import the configuration file of `vuex` in `main.ts | main.js`. When using the plug-in, the third parameter is that the user can pass the configuration item. It is an object type, and the `store` attribute is added to the object. Value is imported vuex. 42 | 43 | ```typescript 44 | import store from "./store"; 45 | 46 | app.use(VueNativeSock,"",{ 47 | store: store 48 | }); 49 | ``` 50 | > If you still don't know how to use it, you can refer to my other open source project[chat-system](https://github.com/likaia/chat-system/blob/master/src/main.ts)。 51 | 52 | If vuex integration is enabled, state and mutations methods need to be defined in its configuration file. The methods defined in mutations are 6 monitors of websocket, and you can do corresponding operations in these monitors. 53 | ```typescript 54 | import { createStore } from "vuex"; 55 | import main from "../main"; 56 | 57 | export default createStore({ 58 | state: { 59 | socket: { 60 | // Connection Status 61 | isConnected: false, 62 | // Message content 63 | message: "", 64 | // Reconnect error 65 | reconnectError: false, 66 | // Heartbeat message sending time 67 | heartBeatInterval: 50000, 68 | // Heartbeat timer 69 | heartBeatTimer: 0 70 | } 71 | }, 72 | mutations: { 73 | // Connection open 74 | SOCKET_ONOPEN(state, event) { 75 | main.config.globalProperties.$socket = event.currentTarget; 76 | state.socket.isConnected = true; 77 | // When the connection is successful, start sending heartbeat messages regularly to avoid being disconnected by the server 78 | state.socket.heartBeatTimer = setInterval(() => { 79 | const message = "Heartbeat message"; 80 | state.socket.isConnected && 81 | main.config.globalProperties.$socket.sendObj({ 82 | code: 200, 83 | msg: message 84 | }); 85 | }, state.socket.heartBeatInterval); 86 | }, 87 | // Connection closed 88 | SOCKET_ONCLOSE(state, event) { 89 | state.socket.isConnected = false; 90 | // Stop the heartbeat message when the connection is closed 91 | clearInterval(state.socket.heartBeatTimer); 92 | state.socket.heartBeatTimer = 0; 93 | console.log("The line is disconnected: " + new Date()); 94 | console.log(event); 95 | }, 96 | // An error occurred 97 | SOCKET_ONERROR(state, event) { 98 | console.error(state, event); 99 | }, 100 | // Receive the message sent by the server 101 | SOCKET_ONMESSAGE(state, message) { 102 | state.socket.message = message; 103 | }, 104 | // Auto reconnect 105 | SOCKET_RECONNECT(state, count) { 106 | console.info("Message system reconnecting...", state, count); 107 | }, 108 | // Reconnect error 109 | SOCKET_RECONNECT_ERROR(state) { 110 | state.socket.reconnectError = true; 111 | } 112 | }, 113 | modules: {} 114 | }); 115 | ``` 116 | ##### Custom method name 117 | You can also customize the default listener event name of custom websocket in `mutations`. 118 | ```typescript 119 | // mutation-types.ts 120 | const SOCKET_ONOPEN = '✅ Socket connected!' 121 | const SOCKET_ONCLOSE = '❌ Socket disconnected!' 122 | const SOCKET_ONERROR = '❌ Socket Error!!!' 123 | const SOCKET_ONMESSAGE = 'Websocket message received' 124 | const SOCKET_RECONNECT = 'Websocket reconnected' 125 | const SOCKET_RECONNECT_ERROR = 'Websocket is having issues reconnecting..' 126 | 127 | export { 128 | SOCKET_ONOPEN, 129 | SOCKET_ONCLOSE, 130 | SOCKET_ONERROR, 131 | SOCKET_ONMESSAGE, 132 | SOCKET_RECONNECT, 133 | SOCKET_RECONNECT_ERROR 134 | } 135 | 136 | // store.ts 137 | import { createStore } from "vuex"; 138 | import main from "../main"; 139 | import { 140 | SOCKET_ONOPEN, 141 | SOCKET_ONCLOSE, 142 | SOCKET_ONERROR, 143 | SOCKET_ONMESSAGE, 144 | SOCKET_RECONNECT, 145 | SOCKET_RECONNECT_ERROR 146 | } from "./mutation-types" 147 | 148 | export default createStore({ 149 | state: { 150 | socket: { 151 | isConnected: false, 152 | message: '', 153 | reconnectError: false, 154 | } 155 | }, 156 | mutations: { 157 | [SOCKET_ONOPEN](state, event) { 158 | state.socket.isConnected = true 159 | }, 160 | [SOCKET_ONCLOSE](state, event) { 161 | state.socket.isConnected = false 162 | }, 163 | [SOCKET_ONERROR](state, event) { 164 | console.error(state, event) 165 | }, 166 | // default handler called for all methods 167 | [SOCKET_ONMESSAGE](state, message) { 168 | state.socket.message = message 169 | }, 170 | // mutations for reconnect methods 171 | [SOCKET_RECONNECT](state, count) { 172 | console.info(state, count) 173 | }, 174 | [SOCKET_RECONNECT_ERROR](state) { 175 | state.socket.reconnectError = true; 176 | } 177 | }, 178 | modules: {} 179 | }); 180 | 181 | // main.js 182 | import store from './store' 183 | import { 184 | SOCKET_ONOPEN, 185 | SOCKET_ONCLOSE, 186 | SOCKET_ONERROR, 187 | SOCKET_ONMESSAGE, 188 | SOCKET_RECONNECT, 189 | SOCKET_RECONNECT_ERROR 190 | } from './mutation-types' 191 | 192 | const mutations = { 193 | SOCKET_ONOPEN, 194 | SOCKET_ONCLOSE, 195 | SOCKET_ONERROR, 196 | SOCKET_ONMESSAGE, 197 | SOCKET_RECONNECT, 198 | SOCKET_RECONNECT_ERROR 199 | } 200 | 201 | app.use(VueNativeSock,"",{ 202 | store: store, 203 | mutations: mutations 204 | }); 205 | ``` 206 | 207 | 208 | #### Enable Pinia integration 209 | Import `pinia`'s configuration file in `main.js|main.ts`. 210 | ```typescript 211 | // use Socket Store is pinia's socket configuration file 212 | import { useSocketStoreWithOut } from './useSocketStore'; 213 | 214 | const store = useSocketStoreWithOut(); 215 | 216 | app.use(VueNativeSock, "", { 217 | store: store 218 | }); 219 | ``` 220 | > I specially wrote a demo to demonstrate the integration of pinia, if you need a reference, please go to: [pinia-websocket-project](https://github.com/likaia/pinia-websocket-project) 221 | 222 | The code of pinia's socket configuration file is as follows: 223 | ```typescript 224 | import { defineStore } from 'pinia'; 225 | import { store } from '/@/store'; 226 | import main from '/@/main'; 227 | 228 | interface SocketStore { 229 | // Connection Status 230 | isConnected: boolean; 231 | // Message content 232 | message: string; 233 | // Reconnect error 234 | reconnectError: boolean; 235 | // Heartbeat message sending time 236 | heartBeatInterval: number; 237 | // Heartbeat timer 238 | heartBeatTimer: number; 239 | } 240 | 241 | export const useSocketStore = defineStore({ 242 | id: 'socket', 243 | state: (): SocketStore => ({ 244 | // Connection Status 245 | isConnected: false, 246 | // Message content 247 | message: '', 248 | // Reconnect error 249 | reconnectError: false, 250 | // Heartbeat message sending time 251 | heartBeatInterval: 50000, 252 | // Heartbeat timer 253 | heartBeatTimer: 0, 254 | }), 255 | actions: { 256 | // Connection open 257 | SOCKET_ONOPEN(event) { 258 | main.config.globalProperties.$socket = event.currentTarget; 259 | this.isConnected = true; 260 | // When the connection is successful, start sending heartbeat messages regularly to avoid being disconnected by the server 261 | this.heartBeatTimer = window.setInterval(() => { 262 | const message = 'Heartbeat message'; 263 | this.isConnected && 264 | main.config.globalProperties.$socket.sendObj({ 265 | code: 200, 266 | msg: message, 267 | }); 268 | }, this.heartBeatInterval); 269 | }, 270 | // Connection closed 271 | SOCKET_ONCLOSE(event) { 272 | this.isConnected = false; 273 | // Stop the heartbeat message when the connection is closed 274 | window.clearInterval(this.heartBeatTimer); 275 | this.heartBeatTimer = 0; 276 | console.log('The line is disconnected: ' + new Date()); 277 | console.log(event); 278 | }, 279 | // An error occurred 280 | SOCKET_ONERROR(event) { 281 | console.error(event); 282 | }, 283 | // Receive the message sent by the server 284 | SOCKET_ONMESSAGE(message) { 285 | this.message = message; 286 | }, 287 | // Auto reconnect 288 | SOCKET_RECONNECT(count) { 289 | console.info('Message system reconnecting...', count); 290 | }, 291 | // Reconnect error 292 | SOCKET_RECONNECT_ERROR() { 293 | this.reconnectError = true; 294 | }, 295 | }, 296 | }); 297 | 298 | // Need to be used outside the setup 299 | export function useSocketStoreWithOut() { 300 | return useSocketStore(store); 301 | } 302 | ``` 303 | 304 | In order to facilitate the use of pinia outside the component, useSocketStoreWithOut is additionally exported here, otherwise pinia will report an error indicating that the pinia instance cannot be found. 305 | 306 | The store configuration code of pinia is as follows: 307 | ```typescript 308 | import type { App } from 'vue'; 309 | import { createPinia } from 'pinia'; 310 | 311 | const store = createPinia(); 312 | 313 | export function setupStore(app: App) { 314 | app.use(store); 315 | } 316 | 317 | export { store }; 318 | ``` 319 | 320 | #### Other configuration 321 | > The following methods are all passable parameters of the plug-in and can be used with `store` 322 | 323 | * Set the websocket sub-protocol default, consider it as an empty string. 324 | ```json 325 | { 326 | "protocol": "my-protocol" 327 | } 328 | ``` 329 | * Enable JSON messaging. After enabling, data sending and receiving will use json as the data format. 330 | ```json 331 | { 332 | "format": "json" 333 | } 334 | ``` 335 | 336 | * Enable automatic reconnection `reconnection`, when enabled, you can configure the number of reconnections `reconnection Attempts` and the reconnection interval duration `reconnection Delay` 337 | ```json 338 | { 339 | "reconnection": true, 340 | "reconnectionAttempts": 5, 341 | "reconnectionDelay": 3000 342 | } 343 | ``` 344 | 345 | * Manually manage connections 346 | ```json 347 | { 348 | "connectManually": true 349 | } 350 | ``` 351 | After enabling manual connection management, the connection will not be automatically connected when the project starts. You can call the connection method on a specific component of the project to connect. Call the close method when the component is destroyed to close the connection. 352 | > If you enable manual connection, you must enable vuex, otherwise this setting will not take effect. 353 | ```typescript 354 | // Connect to the websocket server, the parameter is the websocket service address 355 | this.$connect(""); 356 | // Close the connection 357 | this.$disconnect(); 358 | 359 | // CompositionAPI 360 | proxy.$connect(""); 361 | proxy.$disconnect(""); 362 | ``` 363 | * Custom socket event handling 364 | When triggering the mutations event in vuex, you can choose to write your own function processing, do what you want to do, pass in the `pass To Store Handler` parameter when using the plug-in, and if you don’t pass it, use the default processing function. The definition of the default function is as follows: 365 | ```typescript 366 | export default class { 367 | /** 368 | * The default event handler 369 | * @param eventName 370 | * @param event 371 | */ 372 | defaultPassToStore( 373 | eventName: string, 374 | event: { 375 | data: string; 376 | mutation: string; 377 | namespace: string; 378 | action: string; 379 | } 380 | ): void { 381 | // If the beginning of the event name is not SOCKET_ then terminate the function 382 | if (!eventName.startsWith("SOCKET_")) { 383 | return; 384 | } 385 | let method = "commit"; 386 | // Turn the letter of the event name to uppercase 387 | let target = eventName.toUpperCase(); 388 | // Message content 389 | let msg = event; 390 | // data exists and the data is in json format 391 | if (this.format === "json" && event.data) { 392 | // Convert data from json string to json object 393 | msg = JSON.parse(event.data); 394 | // Determine whether msg is synchronous or asynchronous 395 | if (msg.mutation) { 396 | target = [msg.namespace || "", msg.mutation].filter((e: string) => !!e).join("/"); 397 | } else if (msg.action) { 398 | method = "dispatch"; 399 | target = [msg.namespace || "", msg.action].filter((e: string) => !!e).join("/"); 400 | } 401 | } 402 | if (this.mutations) { 403 | target = this.mutations[target] || target; 404 | } 405 | // Trigger methods in storm 406 | if (this.store._p) { 407 | // pinia 408 | target = eventName.toUpperCase(); 409 | this.store[target](msg); 410 | } else { 411 | // vuex 412 | this.store[method](target, msg); 413 | } 414 | } 415 | } 416 | ``` 417 | When you want to customize a function, this function receives 3 parameters: 418 | * event name 419 | * event 420 | * Default event handling, which gives you the option to perform some basic preprocessing before handing over the event to the original handler 421 | 422 | Below is an example 423 | ```typescript 424 | app.use(VueNativeSock, "", { 425 | passToStoreHandler: function (eventName, event, next) { 426 | event.data = event.should_have_been_named_data 427 | next(eventName, event) 428 | } 429 | }) 430 | ``` 431 | 432 | ### functions exposed by the plugin 433 | * `send` Send non-json type data (JSON messaging cannot be enabled when using plugins) 434 | * `sendObj` Send data of type json (JSON messaging must be enabled when using the plugin) 435 | * `$connect` Connect to the websocket server (manually manage connections option must be enabled when using the plugin) 436 | * `onmessage` listening when receiving server push messages 437 | * `$disconnect` disconnectWebsocketConnection 438 | 439 | > payAttention: The above methods are supported in the options API and Composition API. For specific usage, please refer to the documentation of the related functions. 440 | 441 | ### Use in components 442 | After finishing the above configuration, it can be used in the component. The following shows an example of sending data. 443 | ```typescript 444 | export default defineComponent({ 445 | methods: { 446 | clickButton: function(val) { 447 | // Call the send method to send data as a string 448 | this.$socket.send('some data'); 449 | // If fomat is configured as json, you can call the send Obj method to send data 450 | this.$socket.sendObj({ awesome: 'data'} ); 451 | } 452 | } 453 | }) 454 | ``` 455 | 456 | Message monitoring means receiving messages pushed by the websocket server. The sample code for message monitoring is shown below. 457 | ```typescript 458 | // optionsAPI 459 | this.$options.sockets.onmessage = (res: { data: string }) => { 460 | console.log(data); 461 | } 462 | 463 | // CompositionAPI 464 | import { getCurrentInstance } from "vue"; 465 | const { proxy } = getCurrentInstance() as ComponentInternalInstance; 466 | proxy.$socket.onmessage = (res: { 467 | data: string; 468 | }) => { 469 | console.log(data); 470 | } 471 | ``` 472 | 473 | Send messages, push messages to the server 474 | ```typescript 475 | // optionsAPI 476 | this.$socket.sendObj({msg: 'msgText'}); 477 | 478 | // compositionAPI 479 | const internalInstance = data.currentInstance; 480 | internalInstance?.proxy.$socket.sendObj({ 481 | msg: "msgText" 482 | }); 483 | ``` 484 | > The composition API is written because the vue instance cannot be obtained in the setup, so the instance needs to be stored in the global object after the page is mounted, and then the instance is taken out when it is used. For detailed usage, please refer to the writing in my chat-system: [InitData.ts#L91](https://github.com/likaia/chat-system/blob/cacf587061f3a56198ade33a2c5bebeacec004a5/src/module/message-display/main-entrance/InitData.ts#L91) 、[EventMonitoring.ts#L50](https://github.com/likaia/chat-system/blob/db35173c8e54834a117ac8cb5a3753e75d9b1161/src/module/message-display/main-entrance/EventMonitoring.ts#L50) 、[SendMessage.ts#L73](https://github.com/likaia/chat-system/blob/db35173c8e54834a117ac8cb5a3753e75d9b1161/src/module/message-display/components-metords/SendMessage.ts#L73) 、[contact-list.vue#L620](https://github.com/likaia/chat-system/blob/91fe072a20d0928ff2af6c1bf56cedd0e545d0d5/src/views/contact-list.vue#L620) 485 | 486 | 487 | Remove message monitoring 488 | ```typescript 489 | delete this.$options.sockets.onmessage 490 | // compositionAPI writing 491 | delete proxy.$socket.onmessage 492 | ``` 493 | 494 | ## Write at the end 495 | So far, all the methods of using the plug-in have been introduced. 496 | 497 | If you want to know more about the plug-in source code, please move to the project's Git Hub repository:[vue-native-websocket-vue3](https://github.com/likaia/vue-native-websocket-vue3) 498 | 499 | Please move to the original plugin for Vue 2 version: [vue-native-websocket](https://github.com/nathantsoi/vue-native-websocket) 500 | --------------------------------------------------------------------------------