【Node/JavaScript】论一个低配版Web实时通信库是如何实现的( WebSocket篇)
引论
simple-socket是我写的一个"低配版"的Web实时通信工具(相对于Socket.io),在参考了相关源码和资料的基础上,实现了前后端实时互通的基本功能
选用了WebSocket ->server-sent-event -> AJAX轮询这三种方式做降级兼容,分为simple-socket-client和simple-socket-server两套代码,
并实现了最简化的API:
前后端各自通过connect事件触发,获取各自的socket对象
前端通过socket.emit('message', "data")发送消息; 服务端通过socket.on('message', function (data) { //... })接收
服务端通过socket.emit('message', "data")发送消息; 前端通过socket.on('message', function (data) { //... })接收
为方便细节的理解,未直接引用ws,eventsource,sockjs,http://engine.io等已有的工具库
下面把编码的过程和细节,以及代码予以记录
github仓库地址
https://github.com/penghuwan/simple-socket
npm命令
npm i simple-socket-serve (服务端npm包)
npm i simple-socket-client (客户端npm包)
使用方式(模仿Socket.io)
前端
var client = require('simple-socket-client');
var client = new Client();
client.on('connect', socket => {
socket.on('reply', function (data) {
console.log(data)
})
socket.emit('message', "pppppppp");
})
服务端
const SocketServer = require('simple-socket-serve');
const http = require('http'); const server = http.createServer(function (request, response) {
// 你的其他代码~~
}) // Usage start
const ss = new SocketServer({
httpSrv: server, // 需传入Server对象
});
ss.on('connect', socket => {
socket.on('message', data => {
console.log(data);
});
setTimeout(() => {
socket.emit('reply', "aaaa");
}, 3000);
});
// Usage end server.listen(3000);
Output
前端: 约3秒后输出aaaa
服务端:输出pppppp
下面梳理了我在编码过程中的思路,其中有些是借鉴于已有的工具库(如Socket.io)源码,有些则是自己的思考所得。如有错漏之处请多指点
需要思考的问题
我们需要编写哪些通信方式?这些通信方式的上到下的兼容顺序是什么?
浏览器怎么选择最优的通信方式呢?
服务端怎么知道当前发出请求的浏览器,它最高支持哪一种通信方式?
编写的服务端代码怎么和当前的业务代码衔接?
如何使用WebSocket实现通讯?
Q1. 我们需要编写哪些通信方式?这些通信方式的上到下的兼容顺序是什么?
首先要先梳理一下可供选择的实现双向通信的方式,以及它们的浏览器兼容性 (兼容性数据来源于 can i use)
WebSocket: IE10以上才支持,Chrome16, FireFox11,Safari7以及Opera12以上完全支持,移动端形势大
event-source: IE完全不支持(注意是任何版本都不支持),Edge76,Chrome6,Firefox6,Safari5和Opera以上支持, 移动端形势大好
AJAX轮询: 用于兼容低版本的浏览器
永久帧( forever iframe)可用于兼容低版本的浏览器
flash socket 可用于兼容低版本的浏览器
那么它们的优缺点各是怎样的呢?
1.WebSocket
优点:WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议,可从HTTP升级而来,浏览器和服务器只需要一次握手,就可以进行持续的,双向的数据传输,因此能显著节约资源和带宽
缺点:1. 兼容性问题:不支持较低版本的IE浏览器(IE9及以下)2.不支持断线重连,需要手写心跳连接的逻辑 3.通信机制相对复杂
2. server-sent-event(event-source)
优点:(1)只需一次请求,便可以stream的方式多次传送数据,节约资源和带宽 (2)相对WebSocket来说简单易用 (3)内置断线重连功能(retry)
缺点: (1)是单向的,只支持服务端->客户端的数据传送,客户端到服务端的通信仍然依靠AJAX,没有”一家人整整齐齐“的感觉(2)兼容性令人担忧,IE浏览器完全不支持
3. AJAX轮询
优点:兼容性良好,对标低版本IE
缺点:请求中有大半是无用的请求,浪费资源
4.Flash Socket(这个感觉得先说缺点2333)
缺点:(1)浏览器开启时flash需要用户确认,(2)加载时间长,用户体验较差 (3)大多数移动端浏览器不支持flash,为重灾区
优点: 兼容低版本浏览器
5. 永久帧( forever iframe)
缺点: iframe会产生进度条一直存在的问题,用户体验差
优点:兼容低版本IE浏览器
综上,综合兼容性和用户体验的问题,我在项目中选用了WebSocket ->server-sent-event -> AJAX轮询这三种方式做从上到下的兼容
Q2: 浏览器端怎么选择最优的通信方式呢?
很简单,做一下能力检测就可以了,对于支持WebSocket的浏览器,window顶层对象可以检测到WebSocket属性,而支持SSE的浏览器,则可以检测到window.EventSource属性,这便可以作为判断依据。对三种方式做从上到下的判断即可。
// 备注: 此为前端代码
function Client() {
this.ws = null;
this.es = null;
init.call(this);
}
function init() {
// 采用WebSocket作为通信方式
if (window.WebSocket) {
this.type = 'websocket';
this.ws = new WebSocket(`ws://${url}`);
return;
}
// 采用server-sent-event作为通信方式
if (window.EventSource) {
this.type = 'eventsource';
this.es = new EventSource(`http://${url}/eventsource?connection=true`)
return;
}
// 采用Ajax轮询作为通信方式
this.type = 'polling';
}
Q3.服务端怎么知道当前发出请求的浏览器,它最高支持哪一种通信方式?
因为服务端需要处理不同的浏览器发出的请求,这些请求的方式可能是不一样的。
我的思路是:
对于websocket请求,可通过检测connection首部字段是否包含'upgrade',同时upgrade首部字段是否为 'websocket'这两个条件进行判断
对于event-source和AJAX轮询,让前端选择方式后,传URL路径过去告知后端就可以了,路径分别为host:/eventsource和host:/polling
event-source我觉得也可以在前端设置accept:'text/event-stream'的方式告知后端,这个待会改改
// 备注:Node.js服务端代码
var url = require('url');
module.exports = {
// 判断请求的浏览器是否选择了websocket进行通信
isWebSocket(req) {
var connection = req.headers.connection || '';
var upgrade = req.headers.upgrade || '';
return connection.toLowerCase().indexOf('upgrade') >= 0 &&
upgrade.toLowerCase() === 'websocket';
},
// 判断请求的浏览器是否选择了event-source(SSE)进行通信
isEventSource(req) {
var pathname = url.parse(req.url).pathname;
return pathname === '/eventsource';
},
// 判断请求的浏览器是否选择了AJAX轮询进行通信
isPolling(req) {
var pathname = url.parse(req.url).pathname;
return pathname === '/polling';
},
}
Q4. 编写的服务端代码怎么和当前的业务代码衔接?
我们定义一个SocketServer类,并在contructor中接收业务代码中已有的server实例,并监听其request事件去处理请求和响应。如下所示
// 备注: Node.js服务端代码
class SocketServer {
constructor (opt) {
super();
// 以构造函数参数的方式接收业务代码里面已有的Server实例
this.httpSrv = opt.httpSrv;
this._initHttp();
}
_initHttp() {
// 监听外部Server实例的request事件,并处理请求和响应
this.httpSrv.on('request', (req,res) => {
// ...
} );
}
}
使用方式
const server = http.createServer(function (request, response) { }) // 原有的业务代码
const ss = new SocketServer({
httpSrv: server, // 需传入Server对象
});
ss.on('connect', socket => { });
这样做有两个好处:
一方面,对原有的代码没有过多的侵入性
避免了创建新的server实例或监听不同的端口,保持和原server同域,避免了前后端代码产生跨域的问题
前后端组织逻辑概述
前端
1.定义构造函数Client
function Client(host) {
this.type = null; // 通信方式
this.ws = null; // WebSocket对象
this.es = null; // EventSource对象
this.ajax = null;
init.call(this); // 通过能力检测, 设置this.type,初始化相关API对象
listen.call(this); // 监听相关连接打开或消息接收的事件(例如ws.onpen/ws.onmessage;
}
Client.prototype.on = function (event,cb){
emitter.on(event, cb)
}
2.在连接打开时触发connect事件,把client对象自身给传进去
this.ws.onopen = function () {
emitter.emit('connect', this);
} var client = new Client();
// 下面的写法中,socket和client其实是同一个对象
client.on('connect', socket => {
socket.on('reply', function (data) {
console.log(data)
})
socket.emit('message', "pppppppp");
})
后端
class Socket extends events.EventEmitter {
constructor(socketId) {
super();
this.transport = null; // 标记通信方式
this.id = socketId; // SocketId
this.netSocket = null // updrage时获取的net.socket的实例,供WebSocket通信使用
this.eventStream = null // Stream.readable实例,供Event-Source通信使用
this.toSendMes = []; // 待发送的信息,HTTP轮询时使用
}
// 其他代码 ...
on (event,cb) {
// 接收前端传送的信息
}
emit (event,data) {
// 发送信息给前端
}
}
并且定义Server类如下:
class Server extends events.EventEmitter {
constructor(opt) {
super();
this.httpSrv = opt.httpSrv;
// ...
}
// 其他代码 ...
} // 使用Server对象
const ss = new Server({
httpSrv: server, // 需传入Server对象
});
ss.on('connect', socket => {
socket.on('message', data => {
console.log(data);
});
socket.emit('reply', "aaaa");
});
Q5.如何实现WebSocket实时通信?
关于如何在前端利用WS发送和接收消息,MDN文档里说得很详细了请看 https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket这里不再赘述,主要是用了这几个API:
- 创建websocket对象:var ws = new WebSocket(url);
- 发送消息 ws.send("XXXX");
- 接收消息:ws.onmessage = function (payload) { console.log(payload.data) };
WebSocket前端代码
前端接收消息
// 一开始能力检测的时候判断过通信类型并初始化
this.ws = new WebSocket(`ws://${url}`);
// ... 中间隔了其他代码
this.ws.onmessage = function (payload) {
var dataObj = JSON.parse(payload.data);
emitter.emit(dataObj.event, dataObj.data); // 触发事件
}
前端发送消息
// 一开始能力检测的时候判断过通信类型并初始化
this.ws = new WebSocket(`ws://${url}`);
// ... 中间隔了其他代码
this.ws.send(JSON.stringify({
event: event,
data: data
}));
WebSocket服务端代码(Node.js)
WebSocket的报文结构
接下来要讲的是后端怎么进行websocket消息的发送和接收。这首先要先从websocet请求报文和响应报文开始说起
Connection: Upgrade // 表示请求从HTTP升级为其他协议
Upgrade: websocket // 表示升级的协议是webSocket
Sec-WebSocket-Key: VCKjclrCsM3LpMkEngmVhA== // 这个参数需要在服务端拼接后返回
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits // WebSocket的扩展字段
Sec-WebSocket-Version: 13 // WebSocket版本
Sec-websocket-protocol //这个字段我的报文里没有,它是前端webSocket构造函数指定的第二个参数(new WebSocket(url,[protocol]))
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: WLZzo5hbAQgXJ24D0mE3u3nj1Fo=
...
WebSocket的握手流程和代码
要在后端完成基本的握手,你需要做这三件事情:
2. 把下面这三行字段原封不动地写入响应报文里,准备返回去给前端~~
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: websocket
3. 从前端请求报文中获取Sec-WebSocket-Key,拼接上服务端自己定义的ID字符串,然后用sha1加密,再然后转为base64编码格式。最后放在Sec-WebSocket-Accept这个响应报文字段中返回给前端。返回数据的方法是调用socket.write方法
上面三件事完成了,基本的握手流程就可以跑通了
如果你想进一步知道怎么对Sec-WebSocket-Extensions,Sec-websocket-protocol这几个请求字段做处理,你可以看看这里,这个是ws模块的代码 https://github.com/websockets/ws/blob/master/lib/websocket-server.js ,对,就是这个文件
class SocketServer {
constructor (opt) {
super();
// 以构造函数参数的方式接收业务代码里面已有的Server实例
this.httpSrv = opt.httpSrv;
this._initWebSocket();
} _initWebSocket() {
// 监听upgrade事件,判断是否请求是websocket,若是则进行握手
this.httpSrv.on('upgrade', (req, netSocket) => {
// ... other code
this._handleWShandShake(req, netSocket, () => {
const socket = new Socket(null);
// 握手成功后触发onConnection事件, 同时传递socket对象进去
this.emit('connect', socket);
})
});
}
}
上面的_handleWShandShake方法代码如下:
handleWShandShake(req, netSocket, cb) {
if (!detect.isWebSocket(req)) {
return;
}
const key =
req.headers['sec-websocket-key'] !== undefined
? req.headers['sec-websocket-key'].trim()
: '';
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
const digest = createHash('sha1')
.update(key + GUID)
.digest('base64');
const headers = [
'HTTP/1.1 101 Switching Protocols',
'Upgrade: websocket',
'Connection: Upgrade',
`Sec-WebSocket-Accept: ${digest}`
]; netSocket.write(headers.concat('\r\n').join('\r\n'));
cb();
}
上面讲了websocket的握手过程,下面讲一下怎么进行server端消息的发送和接收
服务端接收消息
我们上回说到,监听server对象的upgrade事件可以获取socket对象,我们可以通过监听socket对象的data方法,获取前端通过websocket.send传来的数据 。
但是这里有一个坑!上面data的回调里接收的payload是一个Buffer类型的对象,那我们能否通过Buffer.string去获得前端传来的JSON字符串呢?
答案是
因为传来的—— 是一个封装好的帧的数据,你需要把它手动解析出来,才能取出我们想要的那部分数据。
(如果你发现报了failed: One or more reserved bits are on: reserved1 = 1, reserved2 = 1, reserved3 = 1 这个错误,恭喜你!踩中坑了)
WebSocket帧的编码和解码
在介绍帧的编码和解码之前,让我们先看看WebSocket的帧的格式是怎样的
WebSocket的帧格式
详细介绍参考Websocket的RFC文档:https://tools.ietf.org/html/rfc6455 (在page27处)
了解了websocket帧的格式后,这里介绍一下几个非(jin)常(chang)有(keng)用(ren)的字段
FIN: 表示是否是最后一个帧,1代表是,0不是 // 返回数据帧给前端的时候FIN一定要为1,不然前端收不到
Opcode:帧类型,1代表文本数据,2代表二进制数据 // 这个影响前端onmessage接收的数据类型到底是String还是Blob
RSV 1 RSV2 RSV3 留以后备用 //也就是。。现在还没有卵用,如果控制台报了这个有错八成是没有解析帧数据
其他一些字段
Mask :1bit 掩码,是否加密数据,默认必须置为1
Payload len : 7bit,表示数据的长度
Payload data :为数据内容
解析数据帧的代码
OK!介绍完了帧的格式,下面show一下(别人的)解析帧的代码
// 解析Socket数据帧的方法
// 作者:龙恩0707
// 参考地址: https://www.cnblogs.com/tugenhua0707/p/8542890.html
function decodeFrame(e) {
var i = 0, j, s, arrs = [],
frame = {
// 解析前两个字节的基本数据
FIN: e[i] >> 7,
Opcode: e[i++] & 15,
Mask: e[i] >> 7,
PayloadLength: e[i++] & 0x7F
}; // 处理特殊长度126和127
if (frame.PayloadLength === 126) {
frame.PayloadLength = (e[i++] << 8) + e[i++];
}
if (frame.PayloadLength === 127) {
i += 4; // 长度一般用4个字节的整型,前四个字节一般为长整型留空的。
frame.PayloadLength = (e[i++] << 24) + (e[i++] << 16) + (e[i++] << 8) + e[i++];
}
// 判断是否使用掩码
if (frame.Mask) {
// 获取掩码实体
frame.MaskingKey = [e[i++], e[i++], e[i++], e[i++]];
// 对数据和掩码做异或运算
for (j = 0, arrs = []; j < frame.PayloadLength; j++) {
arrs.push(e[i + j] ^ frame.MaskingKey[j % 4]);
}
} else {
// 否则的话 直接使用数据
arrs = e.slice(i, i + frame.PayloadLength);
}
// 数组转换成缓冲区来使用
arrs = new Buffer.from(arrs);
// 如果有必要则把缓冲区转换成字符串来使用
if (frame.Opcode === 1) {
arrs = arrs.toString();
}
// 设置上数据部分
frame.PayloadLength = arrs;
// 返回数据帧
return frame;
}
帧解码后接收前端传来的消息
帧解码
借助于上面的decodeFrame方法,我们就可以愉快地通过WebSocket从前端接收消息啦!
this.httpSrv.on('upgrade', (req, netSocket) => {
// ... other code
netSocket.on('data', payload => {
// 对接收的WebSocket帧数据进行解析,对应前端调用ws.send方法发来的数据
const str = decodeFrame(payload).PayloadLength;
});
});
通过WebSocket向前端发送消息
根据上文容易联想,既然接收消息要解析帧,那么发送消息也肯定要把数据封装成帧再发送对不对~~ 看代码
WebSocket帧的封装
// 接收数据并返回Socket数据帧的方法
// 作者:小胡子哥
// 参考地址: https://www.cnblogs.com/hustskyking/p/websocket-with-node.html
function encodeFrame(e) {
var s = [], o = Buffer.from(e.PayloadData), l = o.length;
//输入第一个字节
s.push((e.FIN << 7) + e.Opcode);
//输入第二个字节,判断它的长度并放入相应的后续长度消息
//永远不使用掩码
if (l < 126) s.push(l);
else if (l < 0x10000) s.push(126, (l & 0xFF00) >> 8, l & 0xFF);
else s.push(
127, 0, 0, 0, 0, //8字节数据,前4字节一般没用留空
(l & 0xFF000000) >> 6, (l & 0xFF0000) >> 4, (l & 0xFF00) >> 8, l & 0xFF
);
//返回头部分和数据部分的合并缓冲区
return Buffer.concat([new Buffer(s), o]);
}
好的大伙,故事到这里就讲完了,祝大家 。。。
等等!!
好像还有什么重要的事情要说。
WebSocket编码的技术总结
下面开始WebSocket编码的技术总结~(美食作家王刚的口音)
「Node篇」
httpServer的Upgrade事件并不是Upgrade成功时触发的,而是包含Upgrade首部的请求报文到达服务端时触发的,也即每次服务器响应升级请求时发出。我们可以在这里确认请求是否为Websocket升级请求并进行握手
在simple-socket-server中,是将其附加到已有的server实例中根据其自有的请求和响应进行处理,而不是另外启动一个server,这样是为了避免产生跨域的问题,因为simple-socket-client的JS代码和项目本身的服务端代码是同域的,simple-socket-server自然也要和原有的服务端代码同域
可以通过httpserver对象的request事件监听请求和响应,从外部附加socket-server的业务代码
「WebSocket篇」
websocket不是永久连接的。一段时间就会断开,websocket需要手写定时心跳连接的代码(待会填上去)
服务端接收Websocket数据需手动解析WebSocket帧。当你尝试接收前端的数据时,即在服务端获取到连接的socket后,通过socket.on('data', payload => { ... })获取的payload。这个payload是一个Buffer类型, 然而蛋疼的是你也不能直接通过Buffer.toString拿到这个字符串数据,如果直接toString输出将会得到一串乱码!!因为收到的这个Buffer是一个被封装后的帧,需要进行解析
服务端发送Websocket数据需手动封装WebSocket帧。 正如上一条所示,在websocket的服务端,你不能直接通过socket.write(String)或者socket.write(Buffer)去写数据,而是要手动先把数据封装成帧,才能发送过去
在服务端发送websocket数据帧时,要确保FIN为1(表示最后一个帧)。前端onmessage才能收到响应!否则无法响应。
WebSocket的onmessage = (event) =>{ event.data }中前端接收的event.data的类型取决于服务端返回的数据帧的opcode这一字段, event.data可能为Blob (opcode = 2,代表发送过去的是二进制数据) 或者字符串(opcode = 1,表示字符串数据)
本文完,完整代码请参考
github仓库地址
https://github.com/penghuwan/simple-socket
【Node/JavaScript】论一个低配版Web实时通信库是如何实现的( WebSocket篇)的更多相关文章
- 【JavaScript】论一个低配版Web实时通信库是如何实现的之二( EventSource篇)
前情提要 「 话说上回说到!那WebSocket大侠,巧借http之内力,破了敌阵的双工鸳鸯锁,终于突出重围. 然而玄难未了,此时web森林中飞出一只银头红缨枪,划破夜色. "莫非!?&qu ...
- 【Java】利用注解和反射实现一个"低配版"的依赖注入
在Spring中,我们可以通过 @Autowired注解的方式为一个方法中注入参数,那么这种方法背后到底发生了什么呢,这篇文章将讲述如何用Java的注解和反射实现一个“低配版”的依赖注入. 下面是我们 ...
- 搭建一个低配版的Mock Server
mock翻译过来是模仿的意思,Server是服务器.粗暴点直译就是模仿服务器. 写在前面 通过阅读本文,你将对Mock的使用有一定的了解,对前后端分离的概念有了更深一步的认识,对Koa的使用有一定的了 ...
- 基于canvas和web audio实现低配版MikuTap
导言 最近发掘了一个特别happy的网页小游戏--MikuTap.打开之后沉迷了一下午,导致开发工作没做完差点就要删库跑路了,还好boss瞥了我一眼就没下文了.于是第二天我就继续沉迷,随着一阵抽搐,这 ...
- Jenkins 结合 Docker 为 .NET Core 项目实现低配版的 CI&CD
随着项目的不断增多,最开始单体项目手动执行 docker build 命令,手动发布项目就不再适用了.一两个项目可能还吃得消,10 多个项目每天让你构建一次还是够呛.即便你的项目少,每次花费在发布上面 ...
- 搭建react项目(低配版)
react项目低配版,可作为react相关测试的基础环境,方便快速进行测试. git clone git@github.com:whosMeya/simple-react-app.git git ch ...
- 用node.js写一个jenkins发版脚本
背景 每次到网页里手动发版有点烦,写个脚本来提高开发效率. CFG 在 jenkins 设置里获取 API TOKEN. 把 host 和账号密码拼接起来就可以通过鉴权. const token = ...
- java线程学习第一天__低配版的卖面包机
package Thread;import javax.xml.bind.ValidationEvent;class snacks{ private int SaledSnacks=0; ...
- unittest框架(惨不忍睹低配版)
根据我上个随笔的unittest框架优化得来,虽然对于smtp模块还是有点迷糊,不过还是勉强搭建运行成功了,还是先上代码: #login_test.py import requests class L ...
随机推荐
- Python解题技巧
Python解题技巧 一直都是用C++和C解题,某题简单解完后便心血来潮想用Python解一次,发现一些问题,特写此篇随笔来记录. 一. 输入格式: 例:输入第1行给出正整数n和整数m:第2行给出n个 ...
- 【模拟】(正解树状数组)-校长的问题-C++-计蒜客
描述 学校中有 n 名学生,学号分别为 1 - n.再一次考试过后,学校按照学生的分数排了一个名次(分数一样,按照名字的字典序排序).你是一名老师,你明天要和校长汇报这次考试的考试情况,校长询问的方式 ...
- 鸽巢原理及其扩展——Ramsey定理
第一部分:鸽巢原理 咕咕咕!!! 然鹅大家还是最熟悉我→ a数组:but 我也很重要 $:我好像也出现不少次 以上纯属灌水 文章简叙:鸽巢原理对初赛时的问题求解以及复赛的数论题目都有启发意义.直接的初 ...
- 洛谷 P1635 跳跃
题目: 题目背景 NOIP即将迎来周年华诞.在这一个春秋的历程里,NOIP领导全国oier,建设高效.稳定.快捷.开放的社会主义现代化OI.在新的一年里,YZOJ将再接再厉,积极探寻成长之路,更好地为 ...
- android值类型转换
各种数字类型转换成字符串型: String s = String.valueOf( value); // 其中 value 为任意一种数字类型. 字符串型转换成各种数字类型: String s = & ...
- 校园表白墙、微信表白墙、校园墙 微信小程序 JAVA 开发记录与分享
目录 最新版表白墙博客地址 1.微信小程序前台展示 2.功能介绍 3.后台管理 4.后端语言采用 JAVA 开发 5.体验此微信小程序 扫描下方二维码 6.如何联系我或需要源码进行联系 最新版表白墙博 ...
- echarts在react项目中的使用
数据可视化在前端开发中经常会遇到,万恶的图表,有时候总是就差一点,可是怎么也搞不定. 别慌,咱们一起来研究. 引入我就不多说了 npm install echarts 对于基础的可视化组件,我一般采用 ...
- Android App安装包瘦身计划
Android App安装包瘦身计划 Android App安装包体积优化: 理由, 指标和可以采用的方法. 本文内容归纳如下图: 为什么要安装包瘦身 安装包需要瘦身吗? 不需要吗? 安装包要瘦身的主 ...
- jQuery表单校验
主要特性: 表单提交前对所有数据进行校验,不符合不让提交(validate) 如果表单校验不通过,自动focus到第一个错误的域 自动在控件后面显示错误提示内容(error message) 支持根据 ...
- Jmeter之CSS选择器/JQuery选择器关联
选择器: CSS选择器或JQuery选择器是Jmeter支持的两种语法,下面对其两种语法进行简单介绍 CSS选择器 JQuery选择器 Chrome - 复制CSS选择器 Google Chrome在 ...