75 Коммиты

Автор SHA1 Сообщение Дата
  links123.com bdaca15639 Initial commit 3 лет назад
  adam ea8aaa4f84 update:修正关闭的时候的自动重连&提供非单例模式 3 лет назад
  adam fc251f3f94 add:增加ws的BinaryType配置 3 лет назад
  adam 3d098b5d98 update 3 лет назад
  adam 95aeccc811 update:兼容RN的处理,尝试 3 лет назад
  Paul dfee2b206c update ts 5 лет назад
  Paul d72f7b8900 add eventemmit 5 лет назад
  Paul b60f31053b incremental 5 лет назад
  Paul 7aa23e963e update ts 5 лет назад
  Paul dd3d30c8c3 add miss retrun type 5 лет назад
  Paul 8ecf4bded5 add scripts 5 лет назад
  Paul 85aea4502c chore:update name 5 лет назад
  Paul b32d6010d6 new npm version 5 лет назад
  Paul 662fc61e40
Merge pull request #2 from wx-chevalier/master 5 лет назад
  wxyyxc1992 3678335f2f chore: update 5 лет назад
  Paul 6e1402ab85 chore:Singleton 5 лет назад
  Paul 73d14f0592 chron:refactor dir struct 5 лет назад
  Paul d6a28d9a93 fix build 5 лет назад
  Paul 1b27c1abaf fix merge error 5 лет назад
  Paul 8a97caf035
Merge pull request #1 from wx-chevalier/master 5 лет назад
  wxyyxc1992 82cc6206a0 chore: update client 5 лет назад
  Paul 062239b518 add close 5 лет назад
  Paul 48f4a81016 order method 5 лет назад
  Paul 5788c7740f add log 5 лет назад
  Paul b684458a83 promise 5 лет назад
  Paul ed0b48bd43 async 5 лет назад
  Paul 3c14833034 new version 5 лет назад
  Paul 5ec6403835 build es5 5 лет назад
  Paul bf1090c5fe add reademe 5 лет назад
  Paul 599f032578 eslint main.ts 5 лет назад
  Paul facaa0f38c eslint 5 лет назад
  Paul 76d47744dd add eslint 5 лет назад
  Paul f844eac74e fix build cmd 5 лет назад
  Paul 678d0a75e2 new version 5 лет назад
  Paul 28745364f6 change build mod 5 лет назад
  Paul 5c3b77b679 update version 5 лет назад
  Paul 7f7a86c58b add babel 5 лет назад
  Paul 1a8defa4ea fix json parse 5 лет назад
  Paul ee5f7126bc fix build 5 лет назад
  Paul 11f4f01ce9 add git url 5 лет назад
  Paul 4877c3485b delete types 5 лет назад
  Paul 6abfe33a94 delete max playlod 5 лет назад
  Paul bd13cc342f add doc 5 лет назад
  Paul 582c60a538 org dir 5 лет назад
  Paul 69a54de919 rename name 5 лет назад
  Paul 625288beb9 add getter 5 лет назад
  Paul 3294f9e0c3 getters/setters 5 лет назад
  Paul 93dfd8018b add access modifer 5 лет назад
  Paul 58c32f20f5 define const 5 лет назад
  Paul 10eee3bc35 split example from src 5 лет назад
  Paul 1e5be84b75 more type 5 лет назад
  Paul 7a234ce156 delete unused code 5 лет назад
  Paul c98396d5bc fix asynSend 5 лет назад
  Paul 46feffd905 fix ping 5 лет назад
  Paul 3f02a93936 delete version.ts 5 лет назад
  Paul 9781c6edf6 fix listeners 5 лет назад
  Paul b625123a2a delete _this 5 лет назад
  Paul 58139adac4 const replace let 5 лет назад
  Paul 6034ecb873 add types 5 лет назад
  Paul d8f65687a4 format 5 лет назад
  Paul df9231f1f0 fix style 5 лет назад
  Paul 43ece69ed8 strict null check 5 лет назад
  Paul 1ec05cd4a8 webpack as bundle tool 5 лет назад
  Paul adc0bdae14 add bundle tool 5 лет назад
  Paul 886f432be0 add example 5 лет назад
  Paul 49f2c07dfb export client 5 лет назад
  Paul 4a13df0968 declare global 5 лет назад
  Paul c72da8f193 add all file from old js 5 лет назад
  Paul 1c56d6c626 add more file 5 лет назад
  Paul 22275e3f0d code fmt 5 лет назад
  Paul be483f1f6a add test 5 лет назад
  Paul 86f6e02a10 rename 5 лет назад
  Paul 7a73fede20 rename test file 5 лет назад
  Paul 931dc023bc add jest 5 лет назад
  Paul 456cc2ea2e first commit 5 лет назад
22 измененных файлов: 20262 добавлений и 2 удалений
  1. 11
    0
      .babelrc
  2. 16
    0
      .editorconfig
  3. 14
    0
      .eslintrc.js
  4. 5
    0
      .gitignore
  5. 8
    0
      .prettierrc
  6. 30
    0
      .vscode/launch.json
  7. 7
    2
      README.md
  8. 44
    0
      example/main.ts
  9. 12205
    0
      package-lock.json
  10. 62
    0
      package.json
  11. 8
    0
      scripts/jest/jest.config.js
  12. 42
    0
      scripts/webpack/webpack.config.dev.js
  13. 34
    0
      scripts/webpack/webpack.config.umd.js
  14. 447
    0
      src/index.ts
  15. 68
    0
      src/packet.ts
  16. 5
    0
      src/types/callback.ts
  17. 32
    0
      src/types/index.ts
  18. 157
    0
      src/utils.ts
  19. 24
    0
      test/utils.test.ts
  20. 11
    0
      tsconfig.cjs.json
  21. 21
    0
      tsconfig.json
  22. 7011
    0
      yarn.lock

+ 11
- 0
.babelrc Просмотреть файл

@@ -0,0 +1,11 @@
1
+{
2
+  "presets": [
3
+    "@babel/env",
4
+    "@babel/typescript"
5
+  ],
6
+  "plugins": [
7
+    "@babel/plugin-transform-runtime",
8
+    "@babel/proposal-class-properties",
9
+    "@babel/proposal-object-rest-spread"
10
+  ]
11
+}

+ 16
- 0
.editorconfig Просмотреть файл

@@ -0,0 +1,16 @@
1
+# http://editorconfig.org
2
+root = true
3
+
4
+[*]
5
+indent_style = space
6
+indent_size = 2
7
+end_of_line = lf
8
+charset = utf-8
9
+trim_trailing_whitespace = true
10
+insert_final_newline = true
11
+
12
+[*.md]
13
+trim_trailing_whitespace = false
14
+
15
+[Makefile]
16
+indent_style = tab

+ 14
- 0
.eslintrc.js Просмотреть файл

@@ -0,0 +1,14 @@
1
+module.exports = {
2
+    "parser": "@typescript-eslint/parser",
3
+    "plugins": ["@typescript-eslint"],
4
+    "env": {
5
+        "browser": true,
6
+        "es6": true
7
+    },
8
+    "extends": [
9
+      "plugin:@typescript-eslint/recommended",
10
+      "prettier",
11
+      "prettier/@typescript-eslint"
12
+    ],
13
+    "rules": {}
14
+};

+ 5
- 0
.gitignore Просмотреть файл

@@ -0,0 +1,5 @@
1
+node_modules
2
+dist
3
+lib
4
+.cache
5
+tsconfig.cjs.tsbuildinfo

+ 8
- 0
.prettierrc Просмотреть файл

@@ -0,0 +1,8 @@
1
+{
2
+  "trailingComma": "all",
3
+  "tabWidth": 2,
4
+  "indentStyle": "es5",
5
+  "semi": true,
6
+  "arrowParens": "always",
7
+  "singleQuote": true
8
+}

+ 30
- 0
.vscode/launch.json Просмотреть файл

@@ -0,0 +1,30 @@
1
+{
2
+    // Use IntelliSense to learn about possible attributes.
3
+    // Hover to view descriptions of existing attributes.
4
+    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5
+    "version": "0.2.0",
6
+    "configurations": [
7
+        {
8
+            "type": "node",
9
+            "name": "vscode-jest-tests",
10
+            "request": "launch",
11
+            "args": [
12
+                "--runInBand"
13
+            ],
14
+            "cwd": "${workspaceFolder}",
15
+            "console": "integratedTerminal",
16
+            "internalConsoleOptions": "neverOpen",
17
+            "program": "${workspaceFolder}/node_modules/jest/bin/jest"
18
+        },
19
+        {
20
+            "type": "node",
21
+            "request": "launch",
22
+            "name": "Launch Program",
23
+            "program": "${workspaceFolder}/index.js",
24
+            "preLaunchTask": "tsc: build - tsconfig.json",
25
+            "outFiles": [
26
+                "${workspaceFolder}/dist/**/*.js"
27
+            ]
28
+        }
29
+    ]
30
+}

+ 7
- 2
README.md Просмотреть файл

@@ -1,3 +1,8 @@
1
-# ts-sdk
1
+开发环境
2 2
 
3
-ts-sdk
3
+运行 npm start可以打开浏览器在console里面看demo运行效果。example目录编写一些使用demo,发布npm包的时候不需要webpack打包
4
+
5
+发布npm包
6
+
7
+1. 运行 npm build
8
+2. npm publish

+ 44
- 0
example/main.ts Просмотреть файл

@@ -0,0 +1,44 @@
1
+import { Client } from '../src/';
2
+import { WebsocketError, WebSocketResp } from '../src/types';
3
+
4
+const url = 'ws://127.0.0.1:8085';
5
+const client = Client.getInstance(url, {
6
+  onOpen(): void {
7
+    client
8
+      .ping({})
9
+      .then(
10
+        (res: WebSocketResp): void => {
11
+          console.log('ping sucessful:', res);
12
+        },
13
+      )
14
+      .catch(
15
+        (reason: WebsocketError): void => {
16
+          console.log('ping error:', reason.code, reason.msg);
17
+        },
18
+      );
19
+
20
+    client
21
+      .request('/v1/healthy', {})
22
+      .then(
23
+        (res: WebSocketResp): void => {
24
+          console.log('request successful:', res);
25
+        },
26
+      )
27
+      .catch(
28
+        (reason: WebsocketError): void => {
29
+          console.log('request error:', reason.code, reason.msg);
30
+        },
31
+      );
32
+  },
33
+
34
+  onClose(ev: Event): void {
35
+    console.log('connection error', ev);
36
+    console.log(ev);
37
+  },
38
+
39
+  onError(): void {
40
+    console.log('close connection');
41
+  },
42
+});
43
+
44
+client.enableLogger = true;

+ 12205
- 0
package-lock.json
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 62
- 0
package.json Просмотреть файл

@@ -0,0 +1,62 @@
1
+{
2
+  "name": "@bilingo.com/ts-linker-sdk",
3
+  "version": "1.0.4",
4
+  "description": "linker's ts sdk",
5
+  "main": "dist/umd/index.js",
6
+  "types": "dist/types/index.d.ts",
7
+  "scripts": {
8
+    "type-check": "tsc --noEmit --project ./tsconfig.cjs.json",
9
+    "type-check:watch": "npm run type-check -- --watch",
10
+    "start": "webpack-dev-server --config scripts/webpack/webpack.config.dev.js",
11
+    "test": "jest --config=scripts/jest/jest.config.js",
12
+    "build": "npm run clean && npm run build:cjs && npm run build:umd",
13
+    "build:cjs": "tsc --project ./tsconfig.cjs.json",
14
+    "build:umd": "cross-env NODE_ENV=production webpack --config scripts/webpack/webpack.config.umd.js",
15
+    "clean": "rimraf dist"
16
+  },
17
+  "repository": {
18
+    "type": "git",
19
+    "url": "https://github.com/bilingo-com/ts-sdk"
20
+  },
21
+  "author": "adam_fu",
22
+  "license": "MIT",
23
+  "dependencies": {
24
+    "crypto-js": "^3.1.9-1",
25
+    "eventemitter3": "^4.0.0",
26
+    "node-int64": "^0.4.0"
27
+  },
28
+  "devDependencies": {
29
+    "cross-env": "^7.0.3",
30
+    "@babel/cli": "^7.4.4",
31
+    "@babel/core": "^7.4.4",
32
+    "@babel/plugin-proposal-class-properties": "^7.4.4",
33
+    "@babel/plugin-proposal-object-rest-spread": "^7.4.4",
34
+    "@babel/plugin-transform-runtime": "^7.4.4",
35
+    "@babel/preset-env": "^7.4.4",
36
+    "@babel/preset-typescript": "^7.3.3",
37
+    "@babel/runtime": "^7.4.4",
38
+    "@types/crypto-js": "^3.1.43",
39
+    "@types/jest": "^24.0.11",
40
+    "@types/node-int64": "^0.4.29",
41
+    "@typescript-eslint/eslint-plugin": "^1.7.0",
42
+    "@typescript-eslint/parser": "^1.7.0",
43
+    "babel-core": "^6.26.3",
44
+    "babel-loader": "^8.0.5",
45
+    "babel-preset-env": "^1.7.0",
46
+    "eslint": "^5.16.0",
47
+    "eslint-config-prettier": "^4.2.0",
48
+    "eslint-friendly-formatter": "^4.0.1",
49
+    "eslint-loader": "^2.1.2",
50
+    "html-webpack-plugin": "^3.2.0",
51
+    "jest": "^24.5.0",
52
+    "rimraf": "^2.6.3",
53
+    "ts-jest": "^24.0.0",
54
+    "typescript": "3.6",
55
+    "webpack": "^4.30.0",
56
+    "webpack-cli": "^3.3.2",
57
+    "webpack-dev-server": "^3.3.1"
58
+  },
59
+  "files": [
60
+    "dist/"
61
+  ]
62
+}

+ 8
- 0
scripts/jest/jest.config.js Просмотреть файл

@@ -0,0 +1,8 @@
1
+module.exports = {
2
+  transform: {
3
+    '^.+\\.tsx?$': 'ts-jest',
4
+  },
5
+  rootDir: '../../',
6
+  testRegex: '(/test/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
7
+  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
8
+};

+ 42
- 0
scripts/webpack/webpack.config.dev.js Просмотреть файл

@@ -0,0 +1,42 @@
1
+/* eslint-disable @typescript-eslint/no-var-requires */
2
+
3
+const path = require('path');
4
+const rootPath = process.cwd();
5
+const HtmlWebpackPlugin = require('html-webpack-plugin');
6
+
7
+module.exports = {
8
+  mode: 'development',
9
+  entry: './example/main.ts',
10
+  devtool: 'inline-source-map',
11
+  devServer: {
12
+    compress: true,
13
+    hot: true,
14
+  },
15
+  plugins: [
16
+    new HtmlWebpackPlugin({
17
+      title: 'index',
18
+    }),
19
+  ],
20
+  output: {
21
+    filename: '[name].bundle.js',
22
+    path: path.resolve(rootPath, 'dist'),
23
+  },
24
+  resolve: { extensions: ['.js', '.jsx', '.tsx', '.ts', '.json'] },
25
+  module: {
26
+    rules: [
27
+      {
28
+        // Include ts, tsx, js, and jsx files.
29
+        test: /\.(ts|js)x?$/,
30
+        exclude: /node_modules/,
31
+        loader: 'babel-loader',
32
+      },
33
+      {
34
+        test: /\.(ts|js)x?$/,
35
+        loader: 'eslint-loader',
36
+        options: {
37
+          formatter: require('eslint-friendly-formatter'),
38
+        },
39
+      },
40
+    ],
41
+  },
42
+};

+ 34
- 0
scripts/webpack/webpack.config.umd.js Просмотреть файл

@@ -0,0 +1,34 @@
1
+/* eslint-disable @typescript-eslint/no-var-requires */
2
+
3
+const path = require('path');
4
+const rootPath = process.cwd();
5
+
6
+module.exports = {
7
+  mode: 'production',
8
+  entry: './src/index.ts',
9
+  devtool: false,
10
+  output: {
11
+    filename: 'index.js',
12
+    path: path.resolve(rootPath, 'dist/umd'),
13
+    library: 'imSdk',
14
+    libraryTarget: 'umd',
15
+  },
16
+  resolve: { extensions: ['.js', '.jsx', '.tsx', '.ts', '.json'] },
17
+  module: {
18
+    rules: [
19
+      {
20
+        // Include ts, tsx, js, and jsx files.
21
+        test: /\.(ts|js)x?$/,
22
+        exclude: /node_modules/,
23
+        loader: 'babel-loader',
24
+      },
25
+      {
26
+        test: /\.(ts|js)x?$/,
27
+        loader: 'eslint-loader',
28
+        options: {
29
+          formatter: require('eslint-friendly-formatter'),
30
+        },
31
+      },
32
+    ],
33
+  },
34
+};

+ 447
- 0
src/index.ts Просмотреть файл

@@ -0,0 +1,447 @@
1
+import { Packet } from './packet';
2
+import { Utils } from './utils';
3
+import {
4
+  WebsocketError,
5
+  WebSocketResp,
6
+  ReadyStateCallback,
7
+} from './types/index';
8
+import EventEmitter from 'eventemitter3';
9
+
10
+const clientError = 400;
11
+
12
+/**
13
+ * 初始化链接以及收发数据
14
+ */
15
+class Client {
16
+  private _maxPayload: number;
17
+  private _enableLogger: boolean;
18
+  private static instance: Client;
19
+  private listeners: EventEmitter;
20
+  private binaryType: BinaryType;
21
+  private requestHeader: string;
22
+  private responseHeader: string;
23
+  private url: string;
24
+  private reconnectTimes: number;
25
+  private reconnectLock: boolean;
26
+  private socket: WebSocket;
27
+  private readyStateCallback: ReadyStateCallback;
28
+
29
+  /**
30
+   * 构造函数,初始化客户端链接
31
+   * @param url websocket链接地址
32
+   * @param readyStateCallback 链接状态回调,可以处理onOpen、onClose、onError
33
+   */
34
+  private constructor(url: string, readyStateCallback: ReadyStateCallback, binaryType?: BinaryType) {
35
+    this.binaryType = binaryType || 'blob';
36
+    this.listeners = new EventEmitter();
37
+    this.requestHeader = '';
38
+    this.requestHeader = '';
39
+    this._maxPayload = 1024 * 1024;
40
+    this.url = url;
41
+    this.reconnectTimes = 0;
42
+    this.readyStateCallback = readyStateCallback;
43
+    this._enableLogger = false;
44
+    this.socket = this.connect();
45
+  }
46
+
47
+  /**
48
+   * 通过单例模式获取客户端链接
49
+   * @param url websocket链接地址
50
+   * @param readyStateCallback 链接状态回调,可以处理onOpen、onClose、onError
51
+   * @param binaryType "arraybuffer" | "blob"
52
+   * @param allwaysNew 是否保持新建Client
53
+   */
54
+  public static getInstance(url: string, callback: ReadyStateCallback, binaryType?: BinaryType, allwaysNew?: boolean): Client {
55
+    if (allwaysNew || !Client.instance) {
56
+      Client.instance = new Client(url, callback, binaryType);
57
+    }
58
+
59
+    return Client.instance;
60
+  }
61
+
62
+  /**
63
+   * 设置可以处理的数据包上限
64
+   * @param maxPayload 最多可以处理的数据包大小
65
+   */
66
+  public set maxPayload(maxPayload: number) {
67
+    this._maxPayload = maxPayload;
68
+  }
69
+
70
+  /**
71
+   * 获取可以处理的数据包大小
72
+   */
73
+  public get maxPayload(): number {
74
+    return this._maxPayload;
75
+  }
76
+
77
+  /**
78
+   * 设置是否允许显示运行日志
79
+   */
80
+  public set enableLogger(enableLogger: boolean) {
81
+    this._enableLogger = enableLogger;
82
+  }
83
+
84
+  /**
85
+   * 获取是否显示日志的配置信息
86
+   */
87
+  public get enableLogger(): boolean {
88
+    return this._enableLogger;
89
+  }
90
+
91
+  /**
92
+   * 发送ping请求,来保持长连接
93
+   * @param param 请求参数,比如{"hello":"world"}
94
+   */
95
+  public async ping(param: object): Promise<WebSocketResp> {
96
+    return new Promise(
97
+      (
98
+        resolve: (data: WebSocketResp) => void,
99
+        reject: (err: WebsocketError) => void,
100
+      ): void => {
101
+        if (this.socket.readyState !== this.socket.OPEN) {
102
+          if (this._enableLogger) {
103
+            console.log('[ping]: connection refuse');
104
+          }
105
+
106
+          reject(new WebsocketError(clientError, 'connection refuse'));
107
+        }
108
+
109
+        const heartbeatOperator = 0;
110
+
111
+        this.listeners.addListener(
112
+          heartbeatOperator.toString(),
113
+          (data: WebSocketResp): void => {
114
+            const code = this.getResponseProperty('code');
115
+            if (code !== '') {
116
+              const message = this.getResponseProperty('message');
117
+              reject(new WebsocketError(Number(code), message));
118
+            } else {
119
+              resolve(data);
120
+            }
121
+          },
122
+        );
123
+
124
+        const p = new Packet();
125
+        this.send(
126
+          p.pack(
127
+            heartbeatOperator,
128
+            0,
129
+            this.requestHeader,
130
+            JSON.stringify(param),
131
+          ),
132
+        );
133
+
134
+        if (this._enableLogger) {
135
+          console.info(
136
+            '[send data packet]',
137
+            heartbeatOperator,
138
+            0,
139
+            this.requestHeader,
140
+            param,
141
+          );
142
+        }
143
+      },
144
+    );
145
+  }
146
+
147
+  /**
148
+   * 同步方式向服务端发送请求
149
+   * @param operator 路由地址
150
+   * @param param 请求参数,比如{"hello":"world"}
151
+   * @param callback 请求状态回调处理
152
+   */
153
+  public async request(
154
+    operator: string,
155
+    param: object,
156
+  ): Promise<WebSocketResp> {
157
+    return await this.asyncSend(operator, param);
158
+  }
159
+
160
+  /**
161
+   * 添加消息监听
162
+   * @description 添加消息监听器,比如operator是/v1/message/listener,那么从服务端推送到/v1/message/listener的消息会进入到定义的listener里面进行处理
163
+   * @param operator 消息监听地址
164
+   * @param listener 定义如何处理从服务端返回的消息
165
+   */
166
+  public addMessageListener(
167
+    operator: string,
168
+    listener: (data: WebSocketResp) => void,
169
+  ): void {
170
+    this.listeners.addListener(Utils.crc32(operator).toString(), listener);
171
+  }
172
+
173
+  /**
174
+   * 移除消息监听
175
+   * @param operator 消息监听地址
176
+   */
177
+  public removeMessageListener(operator: string): void {
178
+    this.listeners.removeListener(Utils.crc32(operator).toString());
179
+  }
180
+
181
+  /**
182
+   * 返回Websocket链接状态
183
+   * @returns Websocket的链接状态
184
+   */
185
+  public get readyState(): number {
186
+    return this.socket.readyState;
187
+  }
188
+
189
+  /**
190
+   * 添加请求属性,会携带在数据帧里面发送到服务端
191
+   * @param key 属性名
192
+   * @param value 属性值
193
+   */
194
+  public setRequestProperty(key: string, value: string): void {
195
+    let v = this.getRequestProperty(key);
196
+
197
+    this.requestHeader = this.requestHeader.replace(key + '=' + v + ';', '');
198
+    this.requestHeader = this.requestHeader + key + '=' + value + ';';
199
+  }
200
+
201
+  /**
202
+   * 获取请求属性
203
+   * @param key 属性名
204
+   */
205
+  public getRequestProperty(key: string): string {
206
+    if (this.requestHeader !== undefined) {
207
+      let values = this.requestHeader.split(';');
208
+      for (let index in values) {
209
+        let kv = values[index].split('=');
210
+        if (kv[0] === key) {
211
+          return kv[1];
212
+        }
213
+      }
214
+    }
215
+
216
+    return '';
217
+  }
218
+
219
+  /**
220
+   * 设置响应属性,客户端基本用不到,都是服务端来进行设置
221
+   * @param key 属性名
222
+   * @param value 属性值
223
+   */
224
+  public setResponseProperty(key: string, value: string): void {
225
+    let v = this.getResponseProperty(key);
226
+
227
+    this.responseHeader = this.responseHeader.replace(key + '=' + v + ';', '');
228
+    this.responseHeader = this.responseHeader + key + '=' + value + ';';
229
+  }
230
+
231
+  /**
232
+   * 获取从服务端返回的属性
233
+   * @param key 获取响应属性
234
+   */
235
+  public getResponseProperty(key: string): string {
236
+    if (this.responseHeader !== undefined) {
237
+      let values = this.responseHeader.split(';');
238
+      for (let index in values) {
239
+        let kv = values[index].split('=');
240
+        if (kv[0] === key) {
241
+          return kv[1];
242
+        }
243
+      }
244
+    }
245
+
246
+    return '';
247
+  }
248
+  /**
249
+   * 关闭客户端链接
250
+   */
251
+  public close(code?: number, reason?: string): void {
252
+    // 如果是主动关闭
253
+    this.reconnectLock = true;
254
+    this.socket.close(code, reason);
255
+  }
256
+  /**
257
+   * 创建websocket链接
258
+   */
259
+  private connect(): WebSocket {
260
+    const readyStateCallback = this.readyStateCallback;
261
+    let ws = new WebSocket(this.url);
262
+
263
+    ws.binaryType = this.binaryType;
264
+
265
+    ws.onopen = (ev): void => {
266
+      if (this._enableLogger) {
267
+        console.info('[websocket] open connection');
268
+      }
269
+
270
+      this.reconnectTimes = 0;
271
+
272
+      readyStateCallback.onOpen(ev);
273
+    };
274
+
275
+    ws.onclose = (ev): void => {
276
+      if (this._enableLogger) {
277
+        console.info('[websocket] close connection');
278
+      }
279
+
280
+      this.reconnect();
281
+
282
+      readyStateCallback.onClose(ev);
283
+    };
284
+
285
+    ws.onerror = (ev): void => {
286
+      if (this._enableLogger) {
287
+        console.info('[websocket] error');
288
+      }
289
+
290
+      this.reconnect();
291
+
292
+      readyStateCallback.onError(ev);
293
+    };
294
+
295
+    ws.onmessage = (ev): void => {
296
+      const handleData = (data: ArrayBuffer) => {
297
+        try {
298
+          let packet = new Packet().unPack(data);
299
+          let packetLength = packet.headerLength + packet.bodyLength + 20;
300
+          if (packetLength > this._maxPayload) {
301
+            throw new Error('the packet is big than ' + this._maxPayload);
302
+          }
303
+
304
+          let operator = Number(packet.operator) + Number(packet.sequence);
305
+          let listener = operator.toString();
306
+
307
+          this.listeners.listeners(listener).forEach((param): void => {
308
+            if (packet.body === '') {
309
+              packet.body = '{}';
310
+            }
311
+
312
+            this.responseHeader = packet.header;
313
+
314
+            (param as (data: WebSocketResp) => void)(JSON.parse(packet.body));
315
+          });
316
+
317
+          if (this._enableLogger) {
318
+            if (operator !== 0 && packet.body !== 'null') {
319
+              console.info('receive data packet', packet.body);
320
+            }
321
+          }
322
+        } catch (e) {
323
+          throw new Error(e);
324
+        }
325
+      }
326
+      // 区分不同类型数据的解析(暂时支持Blob和ArrayBuffer)
327
+      if (ev.data instanceof Blob) {
328
+        let reader = new FileReader();
329
+        reader.readAsArrayBuffer(ev.data);
330
+        reader.onload = (): void => {
331
+          handleData(reader.result as ArrayBuffer)
332
+        };
333
+      } else if(ev.data instanceof ArrayBuffer) {
334
+        handleData(ev.data);
335
+      } else {
336
+        throw new Error('unsupported data format');
337
+      }
338
+    };
339
+
340
+    return ws;
341
+  }
342
+
343
+  /**`
344
+   * 断线重连
345
+   */
346
+  private reconnect(): void {
347
+    if (!this.reconnectLock) {
348
+      this.reconnectLock = true;
349
+      if (this._enableLogger) {
350
+        console.info('websocket reconnect in ' + this.reconnectTimes + 's');
351
+      }
352
+
353
+      // 尝试重连
354
+      setTimeout((): void => {
355
+        this.reconnectTimes++;
356
+        this.socket = this.connect();
357
+        this.reconnectLock = false;
358
+      }, this.reconnectTimes * 1000);
359
+    }
360
+  }
361
+
362
+  /**
363
+   * 向服务端发送数据请求
364
+   * @param data 向服务端传送的数据
365
+   */
366
+  private send(data: ArrayBuffer): void {
367
+    if (this.socket.readyState !== this.socket.OPEN) {
368
+      if (this._enableLogger) {
369
+        console.error(
370
+          '[send] WebSocket is already in CLOSING or CLOSED state.',
371
+        );
372
+      }
373
+
374
+      return;
375
+    }
376
+
377
+    try {
378
+      this.socket.send(data);
379
+    } catch (e) {
380
+      throw new Error('send data error' + e);
381
+    }
382
+  }
383
+
384
+  /**
385
+   * 异步向服务端发送请求
386
+   * @param operator 路由地址
387
+   * @param param 请求参数,比如{"hello":"world"}
388
+   * @param callback 请求状态回调处理
389
+   */
390
+  private asyncSend(operator: string, param: object): Promise<WebSocketResp> {
391
+    return new Promise(
392
+      (
393
+        resolve: (data: WebSocketResp) => void,
394
+        reject: (err: WebsocketError) => void,
395
+      ): void => {
396
+        if (this.socket.readyState !== this.socket.OPEN) {
397
+          if (this._enableLogger) {
398
+            console.log('[ping]: connection refuse');
399
+          }
400
+
401
+          reject(
402
+            new WebsocketError(clientError, 'asyncSend: connection refuse'),
403
+          );
404
+        }
405
+
406
+        const sequence = new Date().getTime();
407
+        const listener = Utils.crc32(operator) + sequence;
408
+        this.listeners.addListener(
409
+          listener.toString(),
410
+          (data: WebSocketResp): void => {
411
+            const code = this.getResponseProperty('code');
412
+            if (code !== '') {
413
+              const message = this.getResponseProperty('message');
414
+              reject(new WebsocketError(Number(code), message));
415
+            } else {
416
+              resolve(data);
417
+            }
418
+
419
+            delete this.listeners[listener];
420
+          },
421
+        );
422
+
423
+        const p = new Packet();
424
+        this.send(
425
+          p.pack(
426
+            Utils.crc32(operator),
427
+            sequence,
428
+            this.requestHeader,
429
+            JSON.stringify(param),
430
+          ),
431
+        );
432
+
433
+        if (this._enableLogger) {
434
+          console.info(
435
+            '[send data packet]',
436
+            operator,
437
+            sequence,
438
+            this.requestHeader,
439
+            param,
440
+          );
441
+        }
442
+      },
443
+    );
444
+  }
445
+}
446
+
447
+export { Client };

+ 68
- 0
src/packet.ts Просмотреть файл

@@ -0,0 +1,68 @@
1
+import { Utils } from './utils';
2
+import Int64 from 'node-int64';
3
+
4
+export class Packet {
5
+  private key: string = 'b8ca9aa66def05ff3f24919274bb4a66';
6
+  public operator: number;
7
+  public sequence: number;
8
+  public headerLength: number;
9
+  public bodyLength: number;
10
+  public header: string;
11
+  public body: string;
12
+
13
+  public pack(
14
+    operator: number,
15
+    sequence: number,
16
+    header: string,
17
+    body: string,
18
+  ): ArrayBuffer {
19
+    header = Utils.encrypt(header, this.key, this.key);
20
+    body = Utils.encrypt(body, this.key, this.key);
21
+
22
+    const headerLength = header.length;
23
+    const bodyLength = body.length;
24
+
25
+    const buf = new ArrayBuffer(20 + headerLength + bodyLength);
26
+    const dataView = new DataView(buf);
27
+    const nsBuf = new Int64(sequence).toBuffer();
28
+
29
+    dataView.setUint32(0, operator);
30
+    dataView.setUint32(12, headerLength);
31
+    dataView.setUint32(16, bodyLength);
32
+
33
+    let bufView = new Uint8Array(buf);
34
+    for (var i = 0; i < 8; i++) {
35
+      bufView[4 + i] = nsBuf[i];
36
+    }
37
+    for (let i = 0; i < headerLength; i++) {
38
+      bufView[20 + i] = header.charCodeAt(i);
39
+    }
40
+
41
+    for (let i = 0; i < bodyLength; i++) {
42
+      bufView[20 + headerLength + i] = body.charCodeAt(i);
43
+    }
44
+
45
+    return buf;
46
+  }
47
+
48
+  public unPack(data: ArrayBuffer | SharedArrayBuffer): Packet {
49
+    const dataView = new DataView(data);
50
+
51
+    this.operator = dataView.getUint32(0, false);
52
+    this.sequence = new Int64(
53
+      new Uint8Array(dataView.buffer.slice(4, 12)),
54
+    ).toNumber();
55
+    this.headerLength = dataView.getUint32(12, false);
56
+    this.bodyLength = dataView.getUint32(16, false);
57
+
58
+    const header = Utils.ab2str(
59
+      dataView.buffer.slice(20, 20 + this.headerLength),
60
+    );
61
+    const body = Utils.ab2str(dataView.buffer.slice(20 + this.headerLength));
62
+
63
+    this.header = Utils.decrypt(header, this.key, this.key);
64
+    this.body = Utils.decrypt(body, this.key, this.key);
65
+
66
+    return this;
67
+  }
68
+}

+ 5
- 0
src/types/callback.ts Просмотреть файл

@@ -0,0 +1,5 @@
1
+export interface ReadyStateCallback {
2
+  onOpen(ev: Event): void;
3
+  onError(ev: Event): void;
4
+  onClose(ev: Event): void;
5
+}

+ 32
- 0
src/types/index.ts Просмотреть файл

@@ -0,0 +1,32 @@
1
+export interface WebSocketResp {
2
+  value: object | string;
3
+}
4
+
5
+export class WebsocketError {
6
+  private _code: number;
7
+  private _msg: string;
8
+
9
+  /**
10
+   * 构造函数
11
+   */
12
+  public constructor(code: number, msg: string) {
13
+    this._code = code;
14
+    this._msg = msg;
15
+  }
16
+
17
+  /**
18
+   * 返回错误码
19
+   */
20
+  public get code(): number {
21
+    return this._code;
22
+  }
23
+
24
+  /**
25
+   * 返回具体的错误信息
26
+   */
27
+  public get msg(): string {
28
+    return this._msg;
29
+  }
30
+}
31
+
32
+export { ReadyStateCallback } from './callback';

+ 157
- 0
src/utils.ts Просмотреть файл

@@ -0,0 +1,157 @@
1
+import { AES, enc, mode, pad } from 'crypto-js';
2
+
3
+declare global {
4
+  interface Window {
5
+    crcTable: number[];
6
+  }
7
+}
8
+
9
+class Utils {
10
+  private static code = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split(
11
+    '',
12
+  );
13
+
14
+  public static crc32(str: string): number {
15
+    const crcTable =
16
+      window.crcTable || (window.crcTable = Utils.makeCRCTable());
17
+    let crc = 0 ^ -1;
18
+
19
+    for (let i = 0; i < str.length; i++) {
20
+      crc = (crc >>> 8) ^ crcTable[(crc ^ str.charCodeAt(i)) & 0xff];
21
+    }
22
+
23
+    return (crc ^ -1) >>> 0;
24
+  }
25
+
26
+  // ArrayBuffer 转为字符串,参数为 ArrayBuffer 对象
27
+  public static ab2str(buf: ArrayBuffer): string {
28
+    // 注意,如果是大型二进制数组,为了避免溢出,必须一个一个字符地转
29
+    if (buf && buf.byteLength < 1024) {
30
+      return String.fromCharCode.apply(null, new Uint8Array(buf));
31
+    }
32
+
33
+    const bufView = new Uint8Array(buf);
34
+    const len = bufView.length;
35
+    const byteStr = new Array(len);
36
+
37
+    for (let i = 0; i < len; i++) {
38
+      byteStr[i] = String.fromCharCode.call(null, bufView[i]);
39
+    }
40
+
41
+    return byteStr.join('');
42
+  }
43
+
44
+  // 字符串转为 ArrayBuffer 对象,参数为字符串
45
+  public static str2ab(str: string): ArrayBuffer {
46
+    const buf = new ArrayBuffer(str.length); // 每个字符占用2个字节
47
+    const bufView = new Uint8Array(buf);
48
+
49
+    for (let i = 0, strLen = str.length; i < strLen; i++) {
50
+      bufView[i] = str.charCodeAt(i);
51
+    }
52
+
53
+    return buf;
54
+  }
55
+
56
+  // 解密服务端传递过来的字符串
57
+  public static decrypt(data: string, key: string, iv: string): string {
58
+    const binData = Utils.stringToBin(data);
59
+    const base64Data = Utils.binToBase64(binData);
60
+
61
+    const bytes = AES.decrypt(base64Data, enc.Latin1.parse(key), {
62
+      iv: enc.Latin1.parse(iv),
63
+      mode: mode.CBC,
64
+      padding: pad.Pkcs7,
65
+    });
66
+
67
+    return bytes.toString(enc.Utf8);
68
+  }
69
+
70
+  // 加密字符串以后传递到服务端
71
+  public static encrypt(data: string, key: string, iv: string): string {
72
+    const result = AES.encrypt(data, enc.Latin1.parse(key), {
73
+      iv: enc.Latin1.parse(iv),
74
+      mode: mode.CBC,
75
+      padding: pad.Pkcs7,
76
+    });
77
+
78
+    return Utils.binToString(Utils.base64ToBin(result.toString()));
79
+  }
80
+
81
+  // 字节数组转换为base64编码
82
+  public static binToBase64(bitString: string): string {
83
+    const tail = bitString.length % 6;
84
+    const bitStringTemp1 = bitString.substr(0, bitString.length - tail);
85
+
86
+    let result = '';
87
+    let bitStringTemp2 = bitString.substr(bitString.length - tail, tail);
88
+
89
+    for (let i = 0; i < bitStringTemp1.length; i += 6) {
90
+      let index = parseInt(bitStringTemp1.substr(i, 6), 2);
91
+      result += Utils.code[index];
92
+    }
93
+
94
+    bitStringTemp2 += new Array(7 - tail).join('0');
95
+    if (tail) {
96
+      result += Utils.code[parseInt(bitStringTemp2, 2)];
97
+      result += new Array((6 - tail) / 2 + 1).join('=');
98
+    }
99
+
100
+    return result;
101
+  }
102
+
103
+  // base64编码转换为字节数组
104
+  public static base64ToBin(str: string): string {
105
+    let bitString = '';
106
+    let tail = 0;
107
+
108
+    for (let i = 0; i < str.length; i++) {
109
+      if (str[i] !== '=') {
110
+        let decode = this.code.indexOf(str[i]).toString(2);
111
+        bitString += new Array(7 - decode.length).join('0') + decode;
112
+      } else {
113
+        tail++;
114
+      }
115
+    }
116
+
117
+    return bitString.substr(0, bitString.length - tail * 2);
118
+  }
119
+
120
+  // 字符串转换为字节数组
121
+  public static stringToBin(str: string): string {
122
+    let result = '';
123
+    for (let i = 0; i < str.length; i++) {
124
+      let charCode = str.charCodeAt(i).toString(2);
125
+      result += new Array(9 - charCode.length).join('0') + charCode;
126
+    }
127
+
128
+    return result;
129
+  }
130
+
131
+  // 字节数组转化为字符串
132
+  public static binToString(bin: string): string {
133
+    let result = '';
134
+    for (let i = 0; i < bin.length; i += 8) {
135
+      result += String.fromCharCode(parseInt(bin.substr(i, 8), 2));
136
+    }
137
+
138
+    return result;
139
+  }
140
+
141
+  private static makeCRCTable(): number[] {
142
+    let c: number;
143
+    let crcTable: number[] = [];
144
+
145
+    for (let n = 0; n < 256; n++) {
146
+      c = n;
147
+      for (let k = 0; k < 8; k++) {
148
+        c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
149
+      }
150
+      crcTable[n] = c;
151
+    }
152
+
153
+    return crcTable;
154
+  }
155
+}
156
+
157
+export { Utils };

+ 24
- 0
test/utils.test.ts Просмотреть файл

@@ -0,0 +1,24 @@
1
+import { Utils } from '../src/utils';
2
+
3
+test('utils crc32', (): void => {
4
+  const v = Utils.crc32('/v1/session/init');
5
+  expect(v).toBe(1897767088);
6
+});
7
+
8
+test('utils str2ab and ab2str', (): void => {
9
+  expect(Utils.ab2str(Utils.str2ab('test'))).toBe('test');
10
+});
11
+
12
+test('utils encrypt and decrypt', (): void => {
13
+  const key = 'b8ca9aa66def05ff3f24919274bb4a66';
14
+  const iv = key;
15
+  expect(Utils.decrypt(Utils.encrypt('test', key, iv), key, iv)).toBe('test');
16
+});
17
+
18
+test('utils binToBase64 and base64ToBin', (): void => {
19
+  expect(Utils.binToBase64(Utils.base64ToBin('test'))).toBe('test');
20
+});
21
+
22
+test('utils stringToBin and binToString', (): void => {
23
+  expect(Utils.binToString(Utils.stringToBin('test'))).toBe('test');
24
+});

+ 11
- 0
tsconfig.cjs.json Просмотреть файл

@@ -0,0 +1,11 @@
1
+{
2
+  "extends": "./tsconfig.json",
3
+  "compilerOptions": {
4
+    "module": "commonjs",
5
+    "rootDir": "src",
6
+    "outDir": "dist",
7
+    "declaration": true,
8
+    "declarationDir": "dist/types",
9
+    "sourceMap": false
10
+  }
11
+}

+ 21
- 0
tsconfig.json Просмотреть файл

@@ -0,0 +1,21 @@
1
+{
2
+  "compileOnSave": true,
3
+  "compilerOptions": {
4
+    // Target latest version of ECMAScript.
5
+    "target": "es5",
6
+    // Search under node_modules for non-relative imports.
7
+    "moduleResolution": "node",
8
+    // Process & infer types from .js files.
9
+    "allowJs": false,
10
+    // Import non-ES modules as default imports.
11
+    "esModuleInterop": true,
12
+    // Enable strictest settings like strictNullChecks & noImplicitAny.
13
+    "strictNullChecks": true,
14
+    "incremental": true,
15
+    "alwaysStrict": true,
16
+    "sourceMap": true,
17
+    "rootDirs": ["src", "example"],
18
+    "outDir": "lib/"
19
+  },
20
+  "include": ["src/**/*"]
21
+}

+ 7011
- 0
yarn.lock
Разница между файлами не показана из-за своего большого размера
Просмотреть файл