• 基本按照Real time communication with WebRTC搭建(下面简称该网站为官方tutorial)
  • 本文重视WebRTC的基于同页面通信的代码实现,主要讲述顺序是WebRTC的三大API顺序,一些原理、拓展的部分在链接和后续中

基本环境搭建

已有环境

  • Mac OS 10 & Windows 10 & Ubuntu 18.04 (均实现,WebRTC支持跨平台)
  • Chrome 76 & Firefox
  • Webstorm IDE

搭建需要环境

下载源码

git clone https://github.com/googlecodelabs/webrtc-web

getUserMedia

  • 源码的Step01跑一下,浏览器获取前置摄像头就能成功,不展示具体效果了,看看源码和一些其他的应用

源码分析

  • 源码项目所给的代码结构,多是如下图,所以常会看到js/main.js css/main.css这种src

  • 分析源码关键调用部分
<!-- core src code of index.html -->
<head>
<title>Realtime communication with WebRTC</title>
<link rel="stylesheet" href="css/main.css" />
</head> <body>
<h1>Realtime communication with WebRTC</h1>
<!-- add video and script element in this .html file -->
<video autoplay playsinline></video>
<script src="js/main.js"></script>
</body>
/* core src code of main.css */
body {
font-family: sans-serif;
} video {
max-width: 100%;
width: 800px;
}
  • html css作为标记型语言,了解其基本语法特征与调用(我是通过阅读DOM Sripting的前三章后比较清楚的,阅读这部分还有一个好处是,把我不理解的简洁代码到页面奇幻效果的转化,推锅给了DOM和浏览器厂商~),上面的两个代码就不难理解,着重分析下面js代码
'use strict';

// On this codelab, you will be streaming only video (video: true).
const mediaStreamConstraints = {
video: true,
}; // Video element where stream will be placed.
const localVideo = document.querySelector('video'); // Local stream that will be reproduced on the video.
let localStream; // Handles success by adding the MediaStream to the video element.
function gotLocalMediaStream(mediaStream) {
localStream = mediaStream;
localVideo.srcObject = mediaStream;
} // Handles error by logging a message to the console with the error message.
function handleLocalMediaStreamError(error) {
console.log('navigator.getUserMedia error: ', error);
} // Initializes media stream.
navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
.then(gotLocalMediaStream).catch(handleLocalMediaStreamError);
  • 在官方的tutorial中,对于代码第一行就有解释ECMAScript 5 Strict Mode, JSON, and More,可以认为是一种语法、异常检查更严格的模式

  • 对于第3行之后的代码部分,功能上可以看作

    • 一个constraint只读变量
    • gotLocalMediaStream() 处理视频流函数
    • handleLocalMediaStreamError() 异常处理函数
    • getUserMedia() 调用
  • MediaDevices.getUserMedia()的API调用规则,也就大体明白了上面接近30行代码的架构

navigator.mediaDevices.getUserMedia(constraints)
/* produces a MediaStream */
.then(function(stream) {
/* use the stream */
})
.catch(function(err) {
/* handle the error */
});
  • 在看第14-18行,how to use the mediaStream?

    • 先看mediaStream 的相关API

    The MediaStream interface represents a stream of media content. A stream consists of several tracks such as video or audio tracks. Each track is specified as an instance of MediaStreamTrack

    • 看代码,从整个main.js 文件中,我没有看出let localStream 有什么特殊的用途,这一行注释掉对网页也没有什么影响(也许在之后的源码中有用)

    • 但17行的代码就相当关键了(可以把这一行的代码注释看看是个什么效果~获取了媒体流,但是网页上没有视频显示)

    • 从第9行的const localVideo = document.querySelector('video') 说起

      • const 只读变量
      • Document.querySelector() 理解这个函数,需要对DOM有一些认识
      • DOM(Document Object Model),既然是model就会有一定的逻辑表达形式,DOM文档的表示就是一棵家谱树
      • querySelector(selectors) 也正是基于树形数据结构,来对document 中的 object 进行深度优先的前序遍历,来获取document 中符合selectorsHTMLElement 并返回

      The matching is done using depth-first pre-order traversal of the document's nodes starting with the first element in the document's markup and iterating through sequential nodes by order of the number of child nodes.

    • 17行的HTMLMediaElement.srcObject 则是对'video' 流媒体的赋值,使页面显示video

getUserMedia()++

  • 在step01的demo里,前置摄像头的调用非常成功,但要刨根问底,step01中的代码并没有说明要调什么摄像头,什么类型的视频流(constraints里面只要求video: true
  • 在官方tutorial里面有Bonus points,回答理解这些问题来加深对getUserMedia() 的理解
  • 由于不想把这篇博文写的太长,上面两个问题,都会在基于浏览器的WebRTC的getUserMedia()相关补充中补充说明

RTCPeerConnection

  • Let's move on to Step-02

源码分析

HTML

<body>
<h1>Realtime communication with WebRTC</h1> <video id="localVideo" autoplay playsinline></video>
<video id="remoteVideo" autoplay playsinline></video> <div>
<button id="startButton">Start</button>
<button id="callButton">Call</button>
<button id="hangupButton">Hang Up</button>
</div> <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<script src="js/main.js"></script>
</body>
  • 在HTML文档中<head> 以及调用main.css 的部分和Step-01相比几乎没有改变
  • 新加入的video button scriptid & src命名都有很好的解释说明效果,在下文对main.js 的分析中,相关内容会有更清楚的解释
  • 代码接近300行的样子,按页面的操作顺序,分析一下相关代码

三个button

  • 从三个button 开始,这是代码183-192行
// Define and add behavior to buttons.

// Define action buttons.
const startButton = document.getElementById('startButton');
const callButton = document.getElementById('callButton');
const hangupButton = document.getElementById('hangupButton'); // Set up initial action buttons status: disable call and hangup.
callButton.disabled = true;
hangupButton.disabled = true;
  • 上面的代码和querySelector()有类似功能,比较清晰
  • 代码259-262行
// Add click event handlers for buttons.
startButton.addEventListener('click', startAction);
callButton.addEventListener('click', callAction);
hangupButton.addEventListener('click', hangupAction);

startAction()

  • startAction 开始
// Handles start button action: creates local MediaStream.
function startAction() {
startButton.disabled = true;
navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
.then(gotLocalMediaStream).catch(handleLocalMediaStreamError);
trace('Requesting local stream.');
}
  • 一经页面开启startButton 只能click一次,之后获取getUserMedia()
  • mediaStreamConstraints() 函数几乎没有变化,gotLocalMediaStream() & handleLocalMediaStreamError() 有些许变化,在19-43部分行
// Define peer connections, streams and video elements.
const localVideo = document.getElementById('localVideo');
const remoteVideo = document.getElementById('remoteVideo'); let localStream;
let remoteStream; // Define MediaStreams callbacks. // Sets the MediaStream as the video element src.
function gotLocalMediaStream(mediaStream) {
localVideo.srcObject = mediaStream;
localStream = mediaStream;
trace('Received local stream.');
callButton.disabled = false; // Enable call button.
} // Handles error by logging a message to the console.
function handleLocalMediaStreamError(error) {
trace(`navigator.getUserMedia error: ${error.toString()}.`);
}
  • 代码的整体逻辑非常清晰,唯独新加入的trace() 函数比较新颖,看看
// Logs an action (text) and the time when it happened on the console.
function trace(text) {
text = text.trim();
const now = (window.performance.now() / 1000).toFixed(3); console.log(now, text);
}

callAction()

  • 再看callAction() ,代码203-246行
// Code from line 16-17
// Define initial start time of the call (defined as connection between peers).
let startTime = null; // Code from line19-27
// Define peer connections
let localPeerConnection;
let remotePeerConnection; // Handles call button action: creates peer connection.
function callAction() {
callButton.disabled = true; // disenable call button
hangupButton.disabled = false; // enable hangup button trace('Starting call.');
startTime = window.performance.now(); // assign startTime with concrete time // Get local media stream tracks.
const videoTracks = localStream.getVideoTracks();
const audioTracks = localStream.getAudioTracks();
if (videoTracks.length > 0) {
trace(`Using video device: ${videoTracks[0].label}.`);
}
if (audioTracks.length > 0) {
trace(`Using audio device: ${audioTracks[0].label}.`);
} const servers = null; // Allows for RTC server configuration. // Create peer connections and add behavior.
localPeerConnection = new RTCPeerConnection(servers);
trace('Created local peer connection object localPeerConnection.'); localPeerConnection.addEventListener('icecandidate', handleConnection);
localPeerConnection.addEventListener(
'iceconnectionstatechange', handleConnectionChange); remotePeerConnection = new RTCPeerConnection(servers);
trace('Created remote peer connection object remotePeerConnection.'); remotePeerConnection.addEventListener('icecandidate', handleConnection);
remotePeerConnection.addEventListener(
'iceconnectionstatechange', handleConnectionChange);
remotePeerConnection.addEventListener('addstream', gotRemoteMediaStream); // Add local stream to connection and create offer to connect.
localPeerConnection.addStream(localStream);
trace('Added local stream to localPeerConnection.'); trace('localPeerConnection createOffer start.');
localPeerConnection.createOffer(offerOptions)
.then(createdOffer).catch(setSessionDescriptionError);
}
  • 按上文代码的函数,从第28行开始,就是极其关键的RTCPeerConnection,解析下面所说的三个步骤,以建立连接时序展开

Setting up a call between WebRTC peers involves three tasks:

  • Create a RTCPeerConnection for each end of the call and, at each end, add the local stream from getUserMedia().
  • Get and share network information: potential connection endpoints are known as ICE candidates.
  • Get and share local and remote descriptions: metadata about local media in SDP format.

RTCPeerConnection关键部分——Local & Remote peer建立

  • getUserMedia() 部分,不再赘述
  let localPeerConnection;
const servers = null; // Allows for RTC server configuration. This is where you could specify STUN and TURN servers. // Create peer connections and add behavior.
localPeerConnection = new RTCPeerConnection(servers); remotePeerConnection = new RTCPeerConnection(servers);
  • 关于servers,官网tutorial给了一篇说明WebRTC in the real world: STUN, TURN and signaling,这个我也会在随着项目系统通信搭建的深入,学习实践到servers层面再记录
  • 可以认为在上述代码之后,一个RTCPeerConnection的端就实例化成功了
// Add local stream to connection and create offer to connect.
localPeerConnection.addStream(localStream);
trace('Added local stream to localPeerConnection.');
  • addStream() 之后,可以认为Local & Remote Peer已经全部建好(RTCPeerConnection实例化成功,media传输也可以开始进行)

RTCPeerConnection关键部分——ICE candidate建立

  localPeerConnection.addEventListener('icecandidate', handleConnection);
localPeerConnection.addEventListener(
'iceconnectionstatechange', handleConnectionChange);
  • addEventListener() method在button相关中已经了解,关于'icecandidate' Event,看RTCPeerConnection: icecandidate event,而其中的setLocalDescription()在下面一个section中有介绍
  • 这一部分,需要对计算机网络有一些了解,以及对WebRTC signaling的过程烂熟于心,我初学是非常费解的,探索后其中内容解释在WebRTC的RTCPeerConnection()原理探析(链接中文章重原理,这篇重视基础的代码实现)
  • 同样Remote Peer的建立也是类似
  remotePeerConnection.addEventListener('icecandidate', handleConnection);
remotePeerConnection.addEventListener(
'iceconnectionstatechange', handleConnectionChange);
remotePeerConnection.addEventListener('addstream', gotRemoteMediaStream);
  • 继续看ICE candidate建立过程中用到的三个函数
// Connects with new peer candidate.
function handleConnection(event) {
const peerConnection = event.target;
const iceCandidate = event.candidate; if (iceCandidate) {
const newIceCandidate = new RTCIceCandidate(iceCandidate);
const otherPeer = getOtherPeer(peerConnection); otherPeer.addIceCandidate(newIceCandidate)
.then(() => {
handleConnectionSuccess(peerConnection);
}).catch((error) => {
handleConnectionFailure(peerConnection, error);
}); trace(`${getPeerName(peerConnection)} ICE candidate:\n` +
`${event.candidate.candidate}.`);
}
} // Logs changes to the connection state.
function handleConnectionChange(event) {
const peerConnection = event.target;
console.log('ICE state change event: ', event);
trace(`${getPeerName(peerConnection)} ICE state: ` +
`${peerConnection.iceConnectionState}.`);
} // Handles remote MediaStream success by adding it as the remoteVideo src.
function gotRemoteMediaStream(event) {
const mediaStream = event.stream;
remoteVideo.srcObject = mediaStream;
remoteStream = mediaStream;
trace('Remote peer connection received remote stream.');
}
  • 我猜测前两个函数,是针对于本机连本机的特殊应用搭建的,不具有普遍性,所以不具体分析
  • gotRemoteMediaStream() 函数,最终将Local Peer的addStream() 显示
  • 还有一个API值得看一下,就是RTCPeerConnection.addIceCandidate()

RTCPeerConnection关键部分——Get and share local and remote descriptions

  • 开启一个SDP offer,以进行远程连接
  trace('localPeerConnection createOffer start.');
localPeerConnection.createOffer(offerOptions)
.then(createdOffer).catch(setSessionDescriptionError);
// Set up to exchange only video.
const offerOptions = {
offerToReceiveVideo: 1,
}; // Logs offer creation and sets peer connection session descriptions.
function createdOffer(description) {
trace(`Offer from localPeerConnection:\n${description.sdp}`); trace('localPeerConnection setLocalDescription start.');
localPeerConnection.setLocalDescription(description)
.then(() => { // The parameter list for a function with no parameters should be written with a pair of parentheses.
setLocalDescriptionSuccess(localPeerConnection);
// just logs successful info on the console
}).catch(setSessionDescriptionError);
} // Logs error when setting session description fails.
function setSessionDescriptionError(error) {
trace(`Failed to create session description: ${error.toString()}.`);
}
  • 解释一下createOffer()函数
  • 先看setLocalDescriptionAPI中也没有讲的特别清楚,简单的说,可以认为这个函数经过调用后,Local Peer的offer就发送成功(可参见RTCPeerConnection.signalingState),但实际上发送的信息是什么、向谁发...等一系列问题,都是在官方教程中的源码里面未涉及的,这部分我写在了WebRTC的RTCPeerConnection()原理探析
  • Local Peer已经提供了offer,来而不往非礼也,下面就是Remote Peer的回应了
  trace('remotePeerConnection setRemoteDescription start.');
remotePeerConnection.setRemoteDescription(description)
.then(() => {
setRemoteDescriptionSuccess(remotePeerConnection);
}).catch(setSessionDescriptionError); trace('remotePeerConnection createAnswer start.');
remotePeerConnection.createAnswer()
.then(createdAnswer)
.catch(setSessionDescriptionError); function createdAnswer(description) {
trace(`Answer from remotePeerConnection:\n${description.sdp}.`); trace('remotePeerConnection setLocalDescription start.');
remotePeerConnection.setLocalDescription(description)
.then(() => {
setLocalDescriptionSuccess(remotePeerConnection);
}).catch(setSessionDescriptionError); trace('localPeerConnection setRemoteDescription start.');
localPeerConnection.setRemoteDescription(description)
.then(() => {
setRemoteDescriptionSuccess(localPeerConnection);
}).catch(setSessionDescriptionError);
}
  • Remote Peer的createAnswer()的API以及setRemoteDescriptionAPI,Local Peer与Remote Peer之间的互相通信基本建立了
  • 在Google Dev Tools Console里面截取了一张SDP的图片,感觉比较复杂,之前有做WebRTC底层优化的准备,现在觉得...可能在十分十分需要的时候才会去做QAQ

hangupAction()

  • 最后来看hangup button对应什么函数
// Handles hangup action: ends up call, closes connections and resets peers.
function hangupAction() {
localPeerConnection.close();
remotePeerConnection.close();
localPeerConnection = null;
remotePeerConnection = null;
hangupButton.disabled = true;
callButton.disabled = false;
trace('Ending call.');
}
  • 非常清晰易懂,不解释

如何PC 2 PC

  • 源码分析终于分析完了~
  • 但还有一些问题,源码中的网页本地P2P通信如何改为PC 2 PC的通信?这个我记录在原理一文中

RTCDataChannel

  • RTCPeerConnection部分需要写的实在太多了,到这里,全文长度已经超过3000.orz...这部分脚步代码量略少一些,也尽量写的简洁一点,其余拓展见补充
  • 还是从源码分析开始

源码分析

HTML

  • HTML代码部分较RTCPeerConnection部分,增加了两个文本区
  <textarea id="dataChannelSend" disabled
placeholder="Press Start, enter some text, then press Send."></textarea>
<textarea id="dataChannelReceive" disabled></textarea>
  • 标记性语言,语法、效果非常容易理解,可见
  • 在HTML语言中,我们也看到了和上一节类似的三个button,还是按button顺序来分析

三个button

  • 这次三个button的写法较上一节的有比较新奇的改变
var startButton = document.querySelector('button#startButton');
var sendButton = document.querySelector('button#sendButton');
var closeButton = document.querySelector('button#closeButton'); startButton.onclick = createConnection;
sendButton.onclick = sendData;
closeButton.onclick = closeDataChannels;
  • 首先是querySelector()括号里面的内容非常有范式,所以查到一个参考链接CSS 选择器,然后onclick method也是一种非常简洁的写法
  • 下面看三个button对应的功能

startButton

var localConnection;
var remoteConnection;
var sendChannel;
var dataConstraint;
var dataChannelSend = document.querySelector('textarea#dataChannelSend'); // Offerer side
function createConnection() {
dataChannelSend.placeholder = '';
var servers = null;
pcConstraint = null;
dataConstraint = null;
trace('Using SCTP based data channels');
// For SCTP, reliable and ordered delivery is true by default.
// Add localConnection to global scope to make it visible
// from the browser console.
window.localConnection = localConnection =
new RTCPeerConnection(servers, pcConstraint); // constructor
trace('Created local peer connection object localConnection'); sendChannel = localConnection.createDataChannel('sendDataChannel',
dataConstraint);
trace('Created send data channel'); localConnection.onicecandidate = iceCallback1;
sendChannel.onopen = onSendChannelStateChange;
sendChannel.onclose = onSendChannelStateChange; // Add remoteConnection to global scope to make it visible
// from the browser console.
window.remoteConnection = remoteConnection =
new RTCPeerConnection(servers, pcConstraint);
trace('Created remote peer connection object remoteConnection'); remoteConnection.onicecandidate = iceCallback2;
remoteConnection.ondatachannel = receiveChannelCallback; localConnection.createOffer().then(
gotDescription1,
onCreateSessionDescriptionError
);
startButton.disabled = true;
closeButton.disabled = false;
} function iceCallback1(event) {
trace('local ice callback');
if (event.candidate) {
remoteConnection.addIceCandidate(
event.candidate
).then(
onAddIceCandidateSuccess,
onAddIceCandidateError
);
trace('Local ICE candidate: \n' + event.candidate.candidate);
}
} function iceCallback2(event) {
trace('remote ice callback');
if (event.candidate) {
localConnection.addIceCandidate(
event.candidate
).then(
// print out info on the console
onAddIceCandidateSuccess,
onAddIceCandidateError
);
trace('Remote ICE candidate: \n ' + event.candidate.candidate);
}
} function onSendChannelStateChange() {
var readyState = sendChannel.readyState;
trace('Send channel state is: ' + readyState);
if (readyState === 'open') {
dataChannelSend.disabled = false;
dataChannelSend.focus();
sendButton.disabled = false;
closeButton.disabled = false;
} else {
dataChannelSend.disabled = true;
sendButton.disabled = true;
closeButton.disabled = true;
}
}

The createDataChannel() method on the RTCPeerConnection interface creates a new channel linked with the remote peer, over which any kind of data may be transmitted. This can be useful for back-channel content such as images, file transfer, text chat, game update packets, and so forth.

This happens whenever the local ICE agent needs to deliver a message to the other peer through the signaling server. This lets the ICE agent perform negotiation with the remote peer without the browser itself needing to know any specifics about the technology being used for signaling; simply implement this method to use whatever messaging technology you choose to send the ICE candidate to the remote peer.

The read-only property candidate on the RTCIceCandidate interface returns a DOMString describing the candidate in detail. Most of the other properties of RTCIceCandidate are actually extracted from this string.

When a web site or app using RTCPeerConnection receives a new ICE candidate from the remote peer over its signaling channel, it delivers the newly-received candidate to the browser's ICE agent by calling RTCPeerConnection.addIceCandidate(). This adds this new remote candidate to the RTCPeerConnection's remote description, which describes the state of the remote end of the connection.

The RTCDataChannel.onopen property is an EventHandler which specifies a function to be called when the open event is fired; this is a simple Event which is sent when the data channel's underlying data transport—the link over which the RTCDataChannel's messages flow—is established or re-established.

  • HTMLElement.focus(),这个功能比我想的有趣,而且我们在平时使用浏览器的时候,遇见的特别多~但是,有一点问题,就是基于DOM的Web前端开发太“高级”了,以至于我看不到底层的实现...

The HTMLElement.focus() method sets focus on the specified element, if it can be focused. The focused element is the element which will receive keyboard and similar events by default.

  • 之后的部分就是和RTCPeerConnection相类似的createOffer() createAnswer()部分,在学习完RTCPeerConnection之后,是非常容易的,所以不赘述

sendButton

function sendData() {
var data = dataChannelSend.value;
sendChannel.send(data);
trace('Sent Data: ' + data);
}
  • 基于上一步建立的连接上,实现data传输功能

closeButton

function closeDataChannels() {
trace('Closing data channels');
sendChannel.close();
trace('Closed data channel with label: ' + sendChannel.label);
receiveChannel.close();
trace('Closed data channel with label: ' + receiveChannel.label);
localConnection.close();
remoteConnection.close();
localConnection = null;
remoteConnection = null;
trace('Closed peer connections');
startButton.disabled = false;
sendButton.disabled = true;
closeButton.disabled = true;
dataChannelSend.value = '';
dataChannelReceive.value = '';
dataChannelSend.disabled = true;
disableSendButton();
enableStartButton();
} function enableStartButton() {
startButton.disabled = false;
} function disableSendButton() {
sendButton.disabled = true;
}
  • 这部分基本上也是明晰的,不赘述

And then...

  • 关于WebRTC官网代码三大模块同一网页实现的分析,就写到这个地方,也写了很多很多了,换一个文本继续写跨PC的WebRTC实现,请见PC 2 PC的WebRTC实现

同网页的WebRTC实现与源码分析的更多相关文章

  1. gRPC源码分析0-导读

    gRPC是Google开源的新一代RPC框架,官网是http://www.grpc.io.正式发布于2016年8月,技术栈非常的新,基于HTTP/2,netty4.1,proto3.虽然目前在工程化方 ...

  2. YII 的源码分析(二)

    上一篇简单分析了一下yii的流程,从创建一个应用,到屏幕上输出结果.这一次我来一个稍复杂一点的,重点在输出上,不再是简单的一行"hello world",而是要经过view(视图) ...

  3. Struts2 源码分析——DefaultActionInvocation类的执行action

    本章简言 上一章讲到关于拦截器的机制的知识点,让我们对拦截器有了一定的认识.我们也清楚的知道在执行用户action类实例之前,struts2会先去执行当前action类对应的拦截器.而关于在哪里执行a ...

  4. Struts2 源码分析——Action代理类的工作

    章节简言 上一章笔者讲到关于如何加载配置文件里面的package元素节点信息.相信读者到这里心里面对struts2在启动的时候加载相关的信息有了一定的了解和认识.而本章将讲到关于struts2启动成功 ...

  5. Struts2 源码分析——调结者(Dispatcher)之执行action

    章节简言 上一章笔者写关于Dispatcher类如何处理接受来的request请求.当然读者们也知道他并非正真的执行action操作.他只是在执行action操作之前的准备工作.那么谁才是正真的执行a ...

  6. 《深入理解Spark:核心思想与源码分析》(第2章)

    <深入理解Spark:核心思想与源码分析>一书前言的内容请看链接<深入理解SPARK:核心思想与源码分析>一书正式出版上市 <深入理解Spark:核心思想与源码分析> ...

  7. wifidog源码分析 - 用户连接过程

    引言 之前的文章已经描述wifidog大概的一个工作流程,这里我们具体说说wifidog是怎么把一个新用户重定向到认证服务器中的,它又是怎么对一个已认证的用户实行放行操作的.我们已经知道wifidog ...

  8. soundtouch源码分析__based on csdn :

    1. soundtouch介绍和相关资源 The SoundTouch Library Copyright © Olli Parviainen 2001-2014 SoundTouch is an o ...

  9. jQuery 源码分析和使用心得 - 序

    众所周知, jQuery (个人简称为jq) 在前端开发中占有着非常重要的地位, 可以说jQuery的存在大大降低了学习网页设计和交互的门槛, 他的简单的语法和顺畅的使用逻辑激发了人们强烈的学习兴趣, ...

随机推荐

  1. [洛谷P3369] 普通平衡树 Treap & Splay

    这个就是存一下板子...... 题目传送门 Treap的实现应该是比较正经的. 插入删除前驱后继排名什么的都是平衡树的基本操作. #include<cstdio> #include< ...

  2. 签章新的pom文件

    <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://mave ...

  3. javasc-正则表达式

    匹配中文字符的正则表达式: [\u4e00-\u9fa5]评注:匹配中文还真是个头疼的事,有了这个表达式就好办了 匹配双字节字符(包括汉字在内):[^\x00-\xff]评注:可以用来计算字符串的长度 ...

  4. (七)spring+druid多数据源配置

    druid多数据源配置 一.druid简介 Druid首先是一个数据库连接池,但它不仅仅是一个数据库连接池,它还包含一个ProxyDriver,一系列内置的JDBC组件库,一个SQL Parser. ...

  5. python标准库-builtin 模块之compile,execfile

    eval函数仅仅允许执行简单的表达式.对于更大的代码块时,使用compile和exec函数. 例子:使用 compile函数验证语法 NAME = "script.py" BODY ...

  6. linux find命令格式及find命令详解

    本文详细介绍了linux find命令格式及find命令案例,希望对您的学习有所帮助.1.find命令的一般形式为:find pathname -options [-print -exec -ok . ...

  7. 前进中的人工智能——聚焦Faculty Summit 2015人工智能主题研讨会

    Summit 2015人工智能主题研讨会" title="前进中的人工智能--聚焦Faculty Summit 2015人工智能主题研讨会"> 在近几年上映的科幻大 ...

  8. H2O theme for Jekyll

    正如我在微博上所说的,使用Jekyll半年以来一直没有令我满意的主题模板,所以开始计划自己写一套好看又好用的主题模板.设计之初就明确了极简主义,风格采用扁平化了,通过卡片式设计来进行区块分明的布局,参 ...

  9. Python获取内网IP

    Python 获取本机内网IP 本文记录使用Python获取本机IP的两种方法. 通过hostname来获取本机IP import socket print(socket.gethostbyname( ...

  10. gitlab配置邮箱

    邮件测试Notify.test_email('xx@qq.com', 'Message Subject', 'Message Body').deliver_now 参考 https://www.cen ...