js 插件从开发到发布至 npm 的全过程

639 阅读6分钟

如果您希望在 npmjs 社区发布自己的插件开发包,那么您可以认为这是一篇完整的教程,其中不乏实现思路,开发规范,开发技巧,注意事项,质量控制的全过程,此文以本人最近发布的插件的全过程,作为依据来进行解析实践。

该插件已发布到 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 兼容性

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
}

打包构建

目前为止功能代码实际已经开发完成,但这并非结束,为了保证使用场景、便捷、质量等目标,余量工作反而更为繁杂

目标

从本质上来说,因为 JavaScript 语言,需要在多端(brower、node、typescript 等)场景下使用,在实际解决加载逻辑上,远比其他语言更为复杂,我们至少要实现兼容浏览器

构建工具

这里选择构建工具: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