很多朋友都希望再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