講述寫websocket 前端控件併發布至npm的全過程

很多朋友都希望再npmjs社區發佈自己的插件開發包,那麼您可以認爲這是一篇完整的教程,其中不乏實現思路,開發規範,開發技巧,注意事項,質量控制得全過程,此文以我最近發佈的一個websocket 插件的過程,作爲載體來拋磚引玉,希望可以幫助到大家

該插件實際已發佈到npm社區,歡迎使用

npm install tank-websocket.js
// or
yarn add tank-websocket.js

創建項目

mkdir tank-websocket
npm init -y

目錄結構配置

  • src/
  • test/
    • package.json

兼容性確認

我們在實際生產代碼中會用到瀏覽器API WebSocket 和 class 語法

WebSocket 兼容性

class 兼容性

Set 兼容性

結論

主流瀏覽器已支持

編寫代碼

其實編寫代碼只佔用開發過程的1/3的工作量

創建一個class

class SocketClient {
	    constructor() {}
}

提供創建webSocket 的構造函數

class SocketClient {
	   ws = null
	    constructor() {
			 this.url = url
        	 this.ws = new WebSocket(url)
		}
}

提供事件模式

作爲一個websocket 客戶端 事件監聽是最重要的事務,我們提供了完整的事件機制,封裝目的是爲了提高開發體驗,提高易用性,屏蔽原始事件系統的寫法

//聲明事件處理集合
 events = {
        open: new Set(),
        message: new Set(),
        error: new Set(),
        close: new Set(),
    }
//提供事件註冊機制
onOpen(func) {
        if (typeof func === "function") {
            this.events.open.add(func)
        }
    }
//提供事件驅動,下劃線開頭規約爲private 函數,僅提供內部調用
_emitOpen(event) {
        this.events.open.forEach((func) => {
            func(event)
        })
    }
 //原始事件橋接
  constructor() {
			 this.url = url
        	 this.ws = new WebSocket(url)
			this.ws.addEventListener('open', (event) => {
				this.lastReConnTime = new Date().getTime()
				this._emitOpen.call(this, event)
			});
		}

//提供關閉事件的方法
offOpenEvent() {
        this.events.open.clear()
    }

以上代碼僅爲拋磚引玉,實際代碼中海必須提供 onMessage/onClose/onError的事件驅動

提供send 方法

   /**
     * data a text string, ArrayBuffer or Blob
     * @param data {string|any}
     */
    send(data) {
        try {
            this.ws.send(data)
        } catch (e) {
            console.log(e)
        }
    }

實現重連機制

class SocketClient {
	   ws = null
		lastReConnTime = new Date().getTime();//用於記錄上一次連接時間
		interval = 1000;//重試時間間隔
		useReConn = true //這是一個重連開關,在用戶主動斷開連接的情況下,我們需要關閉重連機制
	    constructor() {
			 this.url = url
        	 this.ws = new WebSocket(url)
			 this.checkConn()
		}

//定時遞歸重連
  checkConn() {
        setTimeout(() => {
            if (this.useReConn && this.ws && this.ws.readyState > 1 && (new Date().getTime() - this.lastReConnTime) > this.interval) {
                console.log("reconnect socket=》》》", this.url)
                this.ws = new WebSocket(this.url)
            }
            if (this.ws) {
                this.checkConn()
            }
        }, this.interval)
    }
}

編寫 主動關閉方法

    /**
     * Actively disconnect
     */
    close() {
        this.useReConn = false//不再重連
        this.ws.close()//關閉原始實例
        this.ws = null
		//清除所有事件
        this.events = {
            open: new Set(),
            message: new Set(),
            error: new Set(),
            close: new Set(),
        }
    }

至此我們的主類已經基本寫完了

導出

主類

遵循 ES6+ 規範

export default SocketClient

入口

實現多例單例組合Api導出

import SocketClient from "./socketClient"
let socketClient = null
const useSocketClient = (url = "") => {
    if (!url && socketClient === null) {
        throw new Error("must param at `url`")
    }
    return socketClient = socketClient ? socketClient : new SocketClient(url);
}
export default {
    SocketClient, useSocketClient
}

打包構建

至此代碼開發已經完成,後邊的工作反而更爲複雜 目前我們需要實現兼容瀏覽器 <script> tag 導入、es6+ import、commonjs require,這裏用到rollup.js

umd 兼容性

所謂UMD (Universal Module Definition),就是一種javascript通用模塊定義規範,讓你的模塊能在javascript所有運行環境中發揮作用。

  • 安裝插件
npm install  @babel/core rollup @rollup/plugin-babel @rollup/plugin-commonjs @rollup/plugin-node-resolve --save-dev
  • 創建配置文件 rollup.config.js 內容爲:
const  resolve =require('@rollup/plugin-node-resolve');
const  commonjs =require('@rollup/plugin-commonjs');
const  babel =require('@rollup/plugin-babel');

module.exports= {
    input: 'src/index.js',
    output: {
        file: 'lib/index.js',//輸出目錄
        format: 'umd',//使用umd規範
        name: 'TankWebSocket'//淺顯的理解爲暴露給window對象的名稱
    },
    plugins: [
        resolve(),
        commonjs(),
        babel({ babelHelpers: 'bundled' })
    ]
};

  • 打包 在package.json script 字段增加命令
  "scripts": {
    "build": "npx rollup -c rollup.config.js",
  },
  • 執行
npm run build
  • 結果輸出 構建完成後,自動會生成一個lib目錄

types支持

	爲了提供友好的開發提示,我們需要引入typescript 工具

全局安裝

npm install -g typescript

項目中初始化

tsc --init

會看到目錄中創建了 tsconfig.json 修改內容如下

{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "rootDir": "./src",
    "allowJs": true,
    "checkJs": true,
    "declaration": true,
    "emitDeclarationOnly": true,
    "sourceMap": false,
    "outDir": "./types",
    "inlineSources": false,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": false,
    "skipLibCheck": true
  },
  "files": [
    "src/index.js",
    "src/socketClient.js"
  ]
}

構建types

tsc

根目錄會出現以下types/目錄,如圖:

此時我們的代碼就會給ide提供良好的開發提示

E2e 測試 && Unit 測試

  • 由於websocket 是基於瀏覽器和服務端運行的環境,所以不同以往的單元測試直接調用就可以了,我們需要給其提供一個良好的瀏覽器沙箱供其運行,這裏我們採用的karma+mocha+chai的組合
  • 此外必須提供臨時的websocket server 才能保證其連通性,保證測試真實準確

創建server

  • 安裝依賴 ws 提供服務,pm2 提供了服務的管理啓停託管能力

npm install pm2 ws --save-dev

- 創建服務代碼 文件 /test/mockServer.js
```javascript
const WebSocket = require('ws');
const wss = new WebSocket.Server({port: 19198});
wss.on('connection', function connection(ws) {
    ws.on('message', function incoming(message) {
        if (message.toString() === "please disconnect me!") {
            ws.close()
            return
        }
        console.log('Received message:', message.toString());
        ws.send(message.toString());
    });
});
  • 增加package.json 的運行命令
	  "scripts": {
    "start-socket-serve": "pm2 start ./test/mockServer.js", //啓動服務
    "stop-socket-serve": "pm2 stop ./test/mockServer.js",//停止服務
  }

編寫單元測試

  • 安裝插件
npm install karma karma-chai chai karma-chrome-launcher mocha karma-mocha --save-dev
  • 創建配置文件 karma.conf.js
module.exports = function(config) {
    config.set({
        frameworks: ['mocha','chai'],
        browsers: ['Chrome'],
        files: [
            'test/**/*.test.js',
            'lib/**/*.js',
        ],
        captureTimeout: 30000,
        browserDisconnectTimeout: 10000,
        reporters: ['progress'],
        singleRun: true
    })
}
  • 編寫測試用例(示例爲測試重連機制是否正常工作)client.reconn.test.js
describe('tank-websocket-client test', function () {
    let conn = null
    beforeEach(() => {
        const TankWebSocket = window.TankWebSocket
        conn = new TankWebSocket.SocketClient('ws://127.0.0.1:19198');
        conn.setDebug(false)
    })
    describe('reconnect ws server', function () {
        it('disconnect', function (done) {
            conn.onOpen(()=>{
                conn.send("please disconnect me!")
            })
            conn.onClose((event) => {
                console.log("____disconnect____time___readyState", new Date(), conn.ws.readyState)
                assert.equal(conn.ws.readyState, WebSocket.CLOSED)
                done()
            })
        });
        it('reconnect', function (done) {
            conn.onOpen(()=>{
                console.log("____onOpen____time___readyState", new Date(),conn.ws.readyState)
                assert.equal(conn.ws.readyState, WebSocket.OPEN)
                done()
            })
        });
    });
    afterEach(()=>{
        conn.disconnect()
    })
});
  • package.json 添加測試命令
 "scripts": {
    "test": "npm run start-socket-serve && karma start && npm run stop-socket-serve"
}

命令的執行順序爲1、啓動webSocket服務 2、啓動瀏覽器沙箱,並執行mocha單元測試,用chai作爲瀏覽器端斷言 工具 3、停止webSocket服務

  • 運行
npm run test

以下爲整個運行過程

自述文件編寫

整體開發測試已經完成,通常我們需要開始編寫自述文檔,一般文件默認名稱爲README.MD ,此文件受到所有git環境和npm服務的支持,使用的是markdown文件格式,我建議的大綱格式爲: - 簡介 - 特性 - 安裝 - 導入 - 示例 - api

發佈

配置

  • package.json 中編輯files 設置要上傳的文件
  • package.json 中編輯version 字段設置版本號
  • package.json 中編輯 keywords字段作爲搜索關鍵字
  • 其他根據實際情況編寫

git tag

創建一個git tag便於後期版本維護
git tag 1.0.1
git push --tag

publish

npm publish
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章