引论

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)源码,有些则是自己的思考所得。如有错漏之处请多指点

需要思考的问题

  1. 我们需要编写哪些通信方式?这些通信方式的上到下的兼容顺序是什么?

  2. 浏览器怎么选择最优的通信方式呢?

  3. 服务端怎么知道当前发出请求的浏览器,它最高支持哪一种通信方式?

  4. 编写的服务端代码怎么和当前的业务代码衔接?

  5. 如何使用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.服务端怎么知道当前发出请求的浏览器,它最高支持哪一种通信方式?

因为服务端需要处理不同的浏览器发出的请求,这些请求的方式可能是不一样的。

我的思路是:

  1. 对于websocket请求,可通过检测connection首部字段是否包含'upgrade',同时upgrade首部字段是否为 'websocket'这两个条件进行判断

  2. 对于event-source和AJAX轮询,让前端选择方式后,传URL路径过去告知后端就可以了,路径分别为host:/eventsource和host:/polling

  3. 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");
})

后端

 
定义一个Socket类,每个请求会对应创建一个Socket对象(对于AJAX轮询时候考虑复用Socket对象)
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");
});
Server对象会根据每请求创建相应Socket对象(AJAX轮询中Socket对象可能持久化并复用
并且是继承自events.EventEmitter,它会在适当的时刻触发connect事件,并且把请求对应的Socket对象传过去

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请求报文和响应报文开始说起

1.这是我的ws请求报文
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]))
2.这是我的ws响应报文
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: WLZzo5hbAQgXJ24D0mE3u3nj1Fo=

...

WebSocket的握手流程和代码

要在后端完成基本的握手,你需要做这三件事情:

1.监听server对象的upgrade方法,从回调中接收请求对象req和socket对象,接下来通过req判断是否该请求是否是一个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篇」

  1. httpServer的Upgrade事件并不是Upgrade成功时触发的,而是包含Upgrade首部的请求报文到达服务端时触发的,也即每次服务器响应升级请求时发出。我们可以在这里确认请求是否为Websocket升级请求并进行握手

  2. 在simple-socket-server中,是将其附加到已有的server实例中根据其自有的请求和响应进行处理,而不是另外启动一个server,这样是为了避免产生跨域的问题,因为simple-socket-client的JS代码和项目本身的服务端代码是同域的,simple-socket-server自然也要和原有的服务端代码同域

  3. 可以通过httpserver对象的request事件监听请求和响应,从外部附加socket-server的业务代码

「WebSocket篇」

  1. websocket不是永久连接的。一段时间就会断开,websocket需要手写定时心跳连接的代码(待会填上去)

  2. 服务端接收Websocket数据需手动解析WebSocket帧。当你尝试接收前端的数据时,即在服务端获取到连接的socket后,通过socket.on('data', payload => { ... })获取的payload。这个payload是一个Buffer类型, 然而蛋疼的是你也不能直接通过Buffer.toString拿到这个字符串数据,如果直接toString输出将会得到一串乱码!!因为收到的这个Buffer是一个被封装后的帧,需要进行解析

  3. 服务端发送Websocket数据需手动封装WebSocket帧。 正如上一条所示,在websocket的服务端,你不能直接通过socket.write(String)或者socket.write(Buffer)去写数据,而是要手动先把数据封装成帧,才能发送过去

  4. 在服务端发送websocket数据帧时,要确保FIN为1(表示最后一个帧)。前端onmessage才能收到响应!否则无法响应。

  5. 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篇)的更多相关文章

  1. 【JavaScript】论一个低配版Web实时通信库是如何实现的之二( EventSource篇)

    前情提要 「 话说上回说到!那WebSocket大侠,巧借http之内力,破了敌阵的双工鸳鸯锁,终于突出重围. 然而玄难未了,此时web森林中飞出一只银头红缨枪,划破夜色. "莫非!?&qu ...

  2. 【Java】利用注解和反射实现一个"低配版"的依赖注入

    在Spring中,我们可以通过 @Autowired注解的方式为一个方法中注入参数,那么这种方法背后到底发生了什么呢,这篇文章将讲述如何用Java的注解和反射实现一个“低配版”的依赖注入. 下面是我们 ...

  3. 搭建一个低配版的Mock Server

    mock翻译过来是模仿的意思,Server是服务器.粗暴点直译就是模仿服务器. 写在前面 通过阅读本文,你将对Mock的使用有一定的了解,对前后端分离的概念有了更深一步的认识,对Koa的使用有一定的了 ...

  4. 基于canvas和web audio实现低配版MikuTap

    导言 最近发掘了一个特别happy的网页小游戏--MikuTap.打开之后沉迷了一下午,导致开发工作没做完差点就要删库跑路了,还好boss瞥了我一眼就没下文了.于是第二天我就继续沉迷,随着一阵抽搐,这 ...

  5. Jenkins 结合 Docker 为 .NET Core 项目实现低配版的 CI&CD

    随着项目的不断增多,最开始单体项目手动执行 docker build 命令,手动发布项目就不再适用了.一两个项目可能还吃得消,10 多个项目每天让你构建一次还是够呛.即便你的项目少,每次花费在发布上面 ...

  6. 搭建react项目(低配版)

    react项目低配版,可作为react相关测试的基础环境,方便快速进行测试. git clone git@github.com:whosMeya/simple-react-app.git git ch ...

  7. 用node.js写一个jenkins发版脚本

    背景 每次到网页里手动发版有点烦,写个脚本来提高开发效率. CFG 在 jenkins 设置里获取 API TOKEN. 把 host 和账号密码拼接起来就可以通过鉴权. const token = ...

  8. java线程学习第一天__低配版的卖面包机

    package Thread;import javax.xml.bind.ValidationEvent;class snacks{    private int  SaledSnacks=0;   ...

  9. unittest框架(惨不忍睹低配版)

    根据我上个随笔的unittest框架优化得来,虽然对于smtp模块还是有点迷糊,不过还是勉强搭建运行成功了,还是先上代码: #login_test.py import requests class L ...

随机推荐

  1. Python解题技巧

    Python解题技巧 一直都是用C++和C解题,某题简单解完后便心血来潮想用Python解一次,发现一些问题,特写此篇随笔来记录. 一. 输入格式: 例:输入第1行给出正整数n和整数m:第2行给出n个 ...

  2. 【模拟】(正解树状数组)-校长的问题-C++-计蒜客

    描述 学校中有 n 名学生,学号分别为 1 - n.再一次考试过后,学校按照学生的分数排了一个名次(分数一样,按照名字的字典序排序).你是一名老师,你明天要和校长汇报这次考试的考试情况,校长询问的方式 ...

  3. 鸽巢原理及其扩展——Ramsey定理

    第一部分:鸽巢原理 咕咕咕!!! 然鹅大家还是最熟悉我→ a数组:but 我也很重要 $:我好像也出现不少次 以上纯属灌水 文章简叙:鸽巢原理对初赛时的问题求解以及复赛的数论题目都有启发意义.直接的初 ...

  4. 洛谷 P1635 跳跃

    题目: 题目背景 NOIP即将迎来周年华诞.在这一个春秋的历程里,NOIP领导全国oier,建设高效.稳定.快捷.开放的社会主义现代化OI.在新的一年里,YZOJ将再接再厉,积极探寻成长之路,更好地为 ...

  5. android值类型转换

    各种数字类型转换成字符串型: String s = String.valueOf( value); // 其中 value 为任意一种数字类型. 字符串型转换成各种数字类型: String s = & ...

  6. 校园表白墙、微信表白墙、校园墙 微信小程序 JAVA 开发记录与分享

    目录 最新版表白墙博客地址 1.微信小程序前台展示 2.功能介绍 3.后台管理 4.后端语言采用 JAVA 开发 5.体验此微信小程序 扫描下方二维码 6.如何联系我或需要源码进行联系 最新版表白墙博 ...

  7. echarts在react项目中的使用

    数据可视化在前端开发中经常会遇到,万恶的图表,有时候总是就差一点,可是怎么也搞不定. 别慌,咱们一起来研究. 引入我就不多说了 npm install echarts 对于基础的可视化组件,我一般采用 ...

  8. Android App安装包瘦身计划

    Android App安装包瘦身计划 Android App安装包体积优化: 理由, 指标和可以采用的方法. 本文内容归纳如下图: 为什么要安装包瘦身 安装包需要瘦身吗? 不需要吗? 安装包要瘦身的主 ...

  9. jQuery表单校验

    主要特性: 表单提交前对所有数据进行校验,不符合不让提交(validate) 如果表单校验不通过,自动focus到第一个错误的域 自动在控件后面显示错误提示内容(error message) 支持根据 ...

  10. Jmeter之CSS选择器/JQuery选择器关联

    选择器: CSS选择器或JQuery选择器是Jmeter支持的两种语法,下面对其两种语法进行简单介绍 CSS选择器 JQuery选择器 Chrome - 复制CSS选择器 Google Chrome在 ...