首页

你不知道的 WebSocket

seo达人

在最后的 阿宝哥有话说 环节,阿宝哥将介绍 WebSocket 与 HTTP 之间的关系、WebSocket 与长轮询有什么区别、什么是 WebSocket 心跳及 Socket 是什么等内容。


下面我们进入正题,为了让大家能够更好地理解和掌握 WebSocket 技术,我们先来介绍一下什么是 WebSocket。


一、什么是 WebSocket

1.1 WebSocket 诞生背景

早期,很多网站为了实现推送技术,所用的技术都是轮询。轮询是指由浏览器每隔一段时间向服务器发出 HTTP 请求,然后服务器返回的数据给客户端。常见的轮询方式分为轮询与长轮询,它们的区别如下图所示:




为了更加直观感受轮询与长轮询之间的区别,我们来看一下具体的代码:




这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而 HTTP 请求与响应可能会包含较长的头部,其中真正有效的数据可能只是很小的一部分,所以这样会消耗很多带宽资源。


比较新的轮询技术是 Comet)。这种技术虽然可以实现双向通信,但仍然需要反复发出请求。而且在 Comet 中普遍采用的 HTTP 长连接也会消耗服务器资源。


在这种情况下,HTML5 定义了 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。Websocket 使用 ws 或 wss 的统一资源标志符(URI),其中 wss 表示使用了 TLS 的 Websocket。如:


ws://echo.websocket.org

wss://echo.websocket.org

WebSocket 与 HTTP 和 HTTPS 使用相同的 TCP 端口,可以绕过大多数防火墙的限制。默认情况下,WebSocket 协议使用 80 端口;若运行在 TLS 之上时,默认使用 443 端口。


1.2 WebSocket 简介

WebSocket 是一种网络传输协议,可在单个 TCP 连接上进行全双工通信,位于 OSI 模型的应用层。WebSocket 协议在 2011 年由 IETF 标准化为 RFC 6455,后由 RFC 7936 补充规范。


WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。


介绍完轮询和 WebSocket 的相关内容之后,接下来我们来看一下 XHR Polling 与 WebSocket 之间的区别:




1.3 WebSocket 优点

较少的控制开销。在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。

更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于 HTTP 请求需要等待客户端发起请求服务端才能响应,延迟明显更少。

保持连接状态。与 HTTP 不同的是,WebSocket 需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。

更好的二进制支持。WebSocket 定义了二进制帧,相对 HTTP,可以更轻松地处理二进制内容。

可以支持扩展。WebSocket 定义了扩展,用户可以扩展协议、实现部分自定义的子协议。

由于 WebSocket 拥有上述的优点,所以它被广泛地应用在即时通信、实时音视频、在线教育和游戏等领域。对于前端开发者来说,要想使用 WebSocket 提供的强大能力,就必须先掌握 WebSocket API,下面阿宝哥带大家一起来认识一下 WebSocket API。


二、WebSocket API

在介绍 WebSocket API 之前,我们先来了解一下它的兼容性:




(图片来源:https://caniuse.com/#search=W...)


从上图可知,目前主流的 Web 浏览器都支持 WebSocket,所以我们可以在大多数项目中放心地使用它。


在浏览器中要使用 WebSocket 提供的能力,我们就必须先创建 WebSocket 对象,该对象提供了用于创建和管理 WebSocket 连接,以及可以通过该连接发送和接收数据的 API。


使用 WebSocket 构造函数,我们就能轻易地构造一个 WebSocket 对象。接下来我们将从 WebSocket 构造函数、WebSocket 对象的属性、方法及 WebSocket 相关的事件四个方面来介绍 WebSocket API,首先我们从 WebSocket 的构造函数入手:


2.1 构造函数

WebSocket 构造函数的语法为:


const myWebSocket = new WebSocket(url [, protocols]);

相关参数说明如下:


url:表示连接的 URL,这是 WebSocket 服务器将响应的 URL。

protocols(可选):一个协议字符串或者一个包含协议字符串的数组。这些字符串用于指定子协议,这样单个服务器可以实现多个 WebSocket 子协议。比如,你可能希望一台服务器能够根据指定的协议(protocol)处理不同类型的交互。如果不指定协议字符串,则假定为空字符串。

当尝试连接的端口被阻止时,会抛出 SECURITY_ERR 异常。


2.2 属性

WebSocket 对象包含以下属性:




每个属性的具体含义如下:


binaryType:使用二进制的数据类型连接。

bufferedAmount(只读):未发送至服务器的字节数。

extensions(只读):服务器选择的扩展。

onclose:用于指定连接关闭后的回调函数。

onerror:用于指定连接失败后的回调函数。

onmessage:用于指定当从服务器接受到信息时的回调函数。

onopen:用于指定连接成功后的回调函数。

protocol(只读):用于返回服务器端选中的子协议的名字。

readyState(只读):返回当前 WebSocket 的连接状态,共有 4 种状态:


CONNECTING — 正在连接中,对应的值为 0;

OPEN — 已经连接并且可以通讯,对应的值为 1;

CLOSING — 连接正在关闭,对应的值为 2;

CLOSED — 连接已关闭或者没有连接成功,对应的值为 3。

url(只读):返回值为当构造函数创建 WebSocket 实例对象时 URL 的绝对路径。

2.3 方法

close([code[, reason]]):该方法用于关闭 WebSocket 连接,如果连接已经关闭,则此方法不执行任何操作。

send(data):该方法将需要通过 WebSocket 链接传输至服务器的数据排入队列,并根据所需要传输的数据的大小来增加 bufferedAmount 的值 。若数据无法传输(比如数据需要缓存而缓冲区已满)时,套接字会自行关闭。

2.4 事件

使用 addEventListener() 或将一个事件监听器赋值给 WebSocket 对象的 oneventname 属性,来监听下面的事件。


close:当一个 WebSocket 连接被关闭时触发,也可以通过 onclose 属性来设置。

error:当一个 WebSocket 连接因错误而关闭时触发,也可以通过 onerror 属性来设置。

message:当通过 WebSocket 收到数据时触发,也可以通过 onmessage 属性来设置。

open:当一个 WebSocket 连接成功时触发,也可以通过 onopen 属性来设置。

介绍完 WebSocket API,我们来举一个使用 WebSocket 发送普通文本的示例。


2.5 发送普通文本



在以上示例中,我们在页面上创建了两个 textarea,分别用于存放 待发送的数据 和 服务器返回的数据。当用户输入完待发送的文本之后,点击 发送 按钮时会把输入的文本发送到服务端,而服务端成功接收到消息之后,会把收到的消息原封不动地回传到客户端。


// const socket = new WebSocket("ws://echo.websocket.org");

// const sendMsgContainer = document.querySelector("#sendMessage");

function send() {

 const message = sendMsgContainer.value;

 if (socket.readyState !== WebSocket.OPEN) {

   console.log("连接未建立,还不能发送消息");

   return;

 }

 if (message) socket.send(message);

}

当然客户端接收到服务端返回的消息之后,会把对应的文本内容保存到 接收的数据 对应的 textarea 文本框中。


// const socket = new WebSocket("ws://echo.websocket.org");

// const receivedMsgContainer = document.querySelector("#receivedMessage");    

socket.addEventListener("message", function (event) {

 console.log("Message from server ", event.data);

 receivedMsgContainer.value = event.data;

});

为了更加直观地理解上述的数据交互过程,我们使用 Chrome 浏览器的开发者工具来看一下相应的过程:




以上示例对应的完整代码如下所示:


<!DOCTYPE html>

<html>

 <head>

   <meta charset="UTF-8" />

   <meta name="viewport" content="width=device-width, initial-scale=1.0" />

   <title>WebSocket 发送普通文本示例</title>

   <style>

     .block {

       flex: 1;

     }

   </style>

 </head>

 <body>

   <h3>阿宝哥:WebSocket 发送普通文本示例</h3>

   <div style="display: flex;">

     <div class="block">

       <p>即将发送的数据:<button onclick="send()">发送</button></p>

       <textarea id="sendMessage" rows="5" cols="15"></textarea>

     </div>

     <div class="block">

       <p>接收的数据:</p>

       <textarea id="receivedMessage" rows="5" cols="15"></textarea>

     </div>

   </div>


   <script>

     const sendMsgContainer = document.querySelector("#sendMessage");

     const receivedMsgContainer = document.querySelector("#receivedMessage");

     const socket = new WebSocket("ws://echo.websocket.org");


     // 监听连接成功事件

     socket.addEventListener("open", function (event) {

       console.log("连接成功,可以开始通讯");

     });


     // 监听消息

     socket.addEventListener("message", function (event) {

       console.log("Message from server ", event.data);

       receivedMsgContainer.value = event.data;

     });


     function send() {

       const message = sendMsgContainer.value;

       if (socket.readyState !== WebSocket.OPEN) {

         console.log("连接未建立,还不能发送消息");

         return;

       }

       if (message) socket.send(message);

     }

   </script>

 </body>

</html>

其实 WebSocket 除了支持发送普通的文本之外,它还支持发送二进制数据,比如 ArrayBuffer 对象、Blob 对象或者 ArrayBufferView 对象:


const socket = new WebSocket("ws://echo.websocket.org");

socket.onopen = function () {

 // 发送UTF-8编码的文本信息

 socket.send("Hello Echo Server!");

 // 发送UTF-8编码的JSON数据

 socket.send(JSON.stringify({ msg: "我是阿宝哥" }));

 

 // 发送二进制ArrayBuffer

 const buffer = new ArrayBuffer(128);

 socket.send(buffer);

 

 // 发送二进制ArrayBufferView

 const intview = new Uint32Array(buffer);

 socket.send(intview);


 // 发送二进制Blob

 const blob = new Blob([buffer]);

 socket.send(blob);

};

以上代码成功运行后,通过 Chrome 开发者工具,我们可以看到对应的数据交互过程:




下面阿宝哥以发送 Blob 对象为例,来介绍一下如何发送二进制数据。


Blob(Binary Large Object)表示二进制类型的大对象。在数据库管理系统中,将二进制数据存储为一个单一个体的集合。Blob 通常是影像、声音或多媒体文件。在 JavaScript 中 Blob 类型的对象表示不可变的类似文件对象的原始数据。

对 Blob 感兴趣的小伙伴,可以阅读 “你不知道的 Blob” 这篇文章。


2.6 发送二进制数据



在以上示例中,我们在页面上创建了两个 textarea,分别用于存放 待发送的数据 和 服务器返回的数据。当用户输入完待发送的文本之后,点击 发送 按钮时,我们会先获取输入的文本并把文本包装成 Blob 对象然后发送到服务端,而服务端成功接收到消息之后,会把收到的消息原封不动地回传到客户端。


当浏览器接收到新消息后,如果是文本数据,会自动将其转换成 DOMString 对象,如果是二进制数据或 Blob 对象,会直接将其转交给应用,由应用自身来根据返回的数据类型进行相应的处理。


数据发送代码


// const socket = new WebSocket("ws://echo.websocket.org");

// const sendMsgContainer = document.querySelector("#sendMessage");

function send() {

 const message = sendMsgContainer.value;

 if (socket.readyState !== WebSocket.OPEN) {

   console.log("连接未建立,还不能发送消息");

   return;

 }

 const blob = new Blob([message], { type: "text/plain" });

 if (message) socket.send(blob);

 console.log(`未发送至服务器的字节数:${socket.bufferedAmount}`);

}

当然客户端接收到服务端返回的消息之后,会判断返回的数据类型,如果是 Blob 类型的话,会调用 Blob 对象的 text() 方法,获取 Blob 对象中保存的 UTF-8 格式的内容,然后把对应的文本内容保存到 接收的数据 对应的 textarea 文本框中。


数据接收代码


// const socket = new WebSocket("ws://echo.websocket.org");

// const receivedMsgContainer = document.querySelector("#receivedMessage");

socket.addEventListener("message", async function (event) {

 console.log("Message from server ", event.data);

 const receivedData = event.data;

 if (receivedData instanceof Blob) {

   receivedMsgContainer.value = await receivedData.text();

 } else {

   receivedMsgContainer.value = receivedData;

 }

});

同样,我们使用 Chrome 浏览器的开发者工具来看一下相应的过程:




通过上图我们可以很明显地看到,当使用发送 Blob 对象时,Data 栏位的信息显示的是 Binary Message,而对于发送普通文本来说,Data 栏位的信息是直接显示发送的文本消息。


以上示例对应的完整代码如下所示:


<!DOCTYPE html>

<html>

 <head>

   <meta charset="UTF-8" />

   <meta name="viewport" content="width=device-width, initial-scale=1.0" />

   <title>WebSocket 发送二进制数据示例</title>

   <style>

     .block {

       flex: 1;

     }

   </style>

 </head>

 <body>

   <h3>阿宝哥:WebSocket 发送二进制数据示例</h3>

   <div style="display: flex;">

     <div class="block">

       <p>待发送的数据:<button onclick="send()">发送</button></p>

       <textarea id="sendMessage" rows="5" cols="15"></textarea>

     </div>

     <div class="block">

       <p>接收的数据:</p>

       <textarea id="receivedMessage" rows="5" cols="15"></textarea>

     </div>

   </div>


   <script>

     const sendMsgContainer = document.querySelector("#sendMessage");

     const receivedMsgContainer = document.querySelector("#receivedMessage");

     const socket = new WebSocket("ws://echo.websocket.org");


     // 监听连接成功事件

     socket.addEventListener("open", function (event) {

       console.log("连接成功,可以开始通讯");

     });


     // 监听消息

     socket.addEventListener("message", async function (event) {

       console.log("Message from server ", event.data);

       const receivedData = event.data;

       if (receivedData instanceof Blob) {

         receivedMsgContainer.value = await receivedData.text();

       } else {

         receivedMsgContainer.value = receivedData;

       }

     });


     function send() {

       const message = sendMsgContainer.value;

       if (socket.readyState !== WebSocket.OPEN) {

         console.log("连接未建立,还不能发送消息");

         return;

       }

       const blob = new Blob([message], { type: "text/plain" });

       if (message) socket.send(blob);

       console.log(`未发送至服务器的字节数:${socket.bufferedAmount}`);

     }

   </script>

 </body>

</html>

可能有一些小伙伴了解完 WebSocket API 之后,觉得还不够过瘾。下面阿宝哥将带大家来实现一个支持发送普通文本的 WebSocket 服务器。


三、手写 WebSocket 服务器

在介绍如何手写 WebSocket 服务器前,我们需要了解一下 WebSocket 连接的生命周期。




从上图可知,在使用 WebSocket 实现全双工通信之前,客户端与服务器之间需要先进行握手(Handshake),在完成握手之后才能开始进行数据的双向通信。


握手是在通信电路创建之后,信息传输开始之前。握手用于达成参数,如信息传输率,字母表,奇偶校验,中断过程,和其他协议特性。 握手有助于不同结构的系统或设备在通信信道中连接,而不需要人为设置参数。


既然握手是 WebSocket 连接生命周期的第一个环节,接下来我们就先来分析 WebSocket 的握手协议。


3.1 握手协议

WebSocket 协议属于应用层协议,它依赖于传输层的 TCP 协议。WebSocket 通过 HTTP/1.1 协议的 101 状态码进行握手。为了创建 WebSocket 连接,需要通过浏览器发出请求,之后服务器进行回应,这个过程通常称为 “握手”(Handshaking)。


利用 HTTP 完成握手有几个好处。首先,让 WebSocket 与现有 HTTP 基础设施兼容:使得 WebSocket 服务器可以运行在 80 和 443 端口上,这通常是对客户端唯一开放的端口。其次,让我们可以重用并扩展 HTTP 的 Upgrade 流,为其添加自定义的 WebSocket 首部,以完成协商。


下面我们以前面已经演示过的发送普通文本的例子为例,来具体分析一下握手过程。


3.1.1 客户端请求

GET ws://echo.websocket.org/ HTTP/1.1

Host: echo.websocket.org

Origin: file://

Connection: Upgrade

Upgrade: websocket

Sec-WebSocket-Version: 13

Sec-WebSocket-Key: Zx8rNEkBE4xnwifpuh8DHQ==

Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

备注:已忽略部分 HTTP 请求头

字段说明


Connection 必须设置 Upgrade,表示客户端希望连接升级。

Upgrade 字段必须设置 websocket,表示希望升级到 WebSocket 协议。

Sec-WebSocket-Version 表示支持的 WebSocket 版本。RFC6455 要求使用的版本是 13,之前草案的版本均应当弃用。

Sec-WebSocket-Key 是随机的字符串,服务器端会用这些数据来构造出一个 SHA-1 的信息摘要。把 “Sec-WebSocket-Key” 加上一个特殊字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后计算 SHA-1 摘要,之后进行 Base64 编码,将结果做为 “Sec-WebSocket-Accept” 头的值,返回给客户端。如此操作,可以尽量避免普通 HTTP 请求被误认为 WebSocket 协议。

Sec-WebSocket-Extensions 用于协商本次连接要使用的 WebSocket 扩展:客户端发送支持的扩展,服务器通过返回相同的首部确认自己支持一个或多个扩展。

Origin 字段是可选的,通常用来表示在浏览器中发起此 WebSocket 连接所在的页面,类似于 Referer。但是,与 Referer 不同的是,Origin 只包含了协议和主机名称。

3.1.2 服务端响应

HTTP/1.1 101 Web Socket Protocol Handshake ①

Connection: Upgrade ②

Upgrade: websocket ③

Sec-WebSocket-Accept: 52Rg3vW4JQ1yWpkvFlsTsiezlqw= ④

备注:已忽略部分 HTTP 响应头

① 101 响应码确认升级到 WebSocket 协议。

② 设置 Connection 头的值为 "Upgrade" 来指示这是一个升级请求。HTTP 协议提供了一种特殊的机制,这一机制允许将一个已建立的连接升级成新的、不相容的协议。

③ Upgrade 头指定一项或多项协议名,按优先级排序,以逗号分隔。这里表示升级为 WebSocket 协议。

④ 签名的键值验证协议支持。

介绍完 WebSocket 的握手协议,接下来阿宝哥将使用 Node.js 来开发我们的 WebSocket 服务器。


3.2 实现握手功能

要开发一个 WebSocket 服务器,首先我们需要先实现握手功能,这里阿宝哥使用 Node.js 内置的 http 模块来创建一个 HTTP 服务器,具体代码如下所示:


const http = require("http");


const port = 8888;

const { generateAcceptValue } = require("./util");


const server = http.createServer((req, res) => {

 res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });

 res.end("大家好,我是阿宝哥。感谢你阅读“你不知道的WebSocket”");

});


server.on("upgrade", function (req, socket) {

 if (req.headers["upgrade"] !== "websocket") {

   socket.end("HTTP/1.1 400 Bad Request");

   return;

 }

 // 读取客户端提供的Sec-WebSocket-Key

 const secWsKey = req.headers["sec-websocket-key"];

 // 使用SHA-1算法生成Sec-WebSocket-Accept

 const hash = generateAcceptValue(secWsKey);

 // 设置HTTP响应头

 const responseHeaders = [

   "HTTP/1.1 101 Web Socket Protocol Handshake",

   "Upgrade: WebSocket",

   "Connection: Upgrade",

   `Sec-WebSocket-Accept: ${hash}`,

 ];

 // 返回握手请求的响应信息

 socket.write(responseHeaders.join("\r\n") + "\r\n\r\n");

});


server.listen(port, () =>

 console.log(`Server running at http://localhost:${port}`)

);

在以上代码中,我们首先引入了 http 模块,然后通过调用该模块的 createServer() 方法创建一个 HTTP 服务器,接着我们监听 upgrade 事件,每次服务器响应升级请求时就会触发该事件。由于我们的服务器只支持升级到 WebSocket 协议,所以如果客户端请求升级的协议非 WebSocket 协议,我们将会返回 “400 Bad Request”。


当服务器接收到升级为 WebSocket 的握手请求时,会先从请求头中获取 “Sec-WebSocket-Key” 的值,然后把该值加上一个特殊字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后计算 SHA-1 摘要,之后进行 Base64 编码,将结果做为 “Sec-WebSocket-Accept” 头的值,返回给客户端。


上述的过程看起来好像有点繁琐,其实利用 Node.js 内置的 crypto 模块,几行代码就可以搞定了:


// util.js

const crypto = require("crypto");

const MAGIC_KEY = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";


function generateAcceptValue(secWsKey) {

 return crypto

   .createHash("sha1")

   .update(secWsKey + MAGIC_KEY, "utf8")

   .digest("base64");

}

开发完握手功能之后,我们可以使用前面的示例来测试一下该功能。待服务器启动之后,我们只要对 “发送普通文本” 示例,做简单地调整,即把先前的 URL 地址替换成 ws://localhost:8888,就可以进行功能验证。


感兴趣的小伙们可以试试看,以下是阿宝哥本地运行后的结果:




从上图可知,我们实现的握手功能已经可以正常工作了。那么握手有没有可能失败呢?答案是肯定的。比如网络问题、服务器异常或 Sec-WebSocket-Accept 的值不正确。


下面阿宝哥修改一下 “Sec-WebSocket-Accept” 生成规则,比如修改 MAGIC_KEY 的值,然后重新验证一下握手功能。此时,浏览器的控制台会输出以下异常信息:


WebSocket connection to 'ws://localhost:8888/' failed: Error during WebSocket handshake: Incorrect 'Sec-WebSocket-Accept' header value

如果你的 WebSocket 服务器要支持子协议的话,你可以参考以下代码进行子协议的处理,阿宝哥就不继续展开介绍了。


// 从请求头中读取子协议

const protocol = req.headers["sec-websocket-protocol"];

// 如果包含子协议,则解析子协议

const protocols = !protocol ? [] : protocol.split(",").map((s) => s.trim());


// 简单起见,我们仅判断是否含有JSON子协议

if (protocols.includes("json")) {

 responseHeaders.push(`Sec-WebSocket-Protocol: json`);

}

好的,WebSocket 握手协议相关的内容基本已经介绍完了。下一步我们来介绍开发消息通信功能需要了解的一些基础知识。


3.3 消息通信基础

在 WebSocket 协议中,数据是通过一系列数据帧来进行传输的。为了避免由于网络中介(例如一些拦截代理)或者一些安全问题,客户端必须在它发送到服务器的所有帧中添加掩码。服务端收到没有添加掩码的数据帧以后,必须立即关闭连接。


3.3.1 数据帧格式

要实现消息通信,我们就必须了解 WebSocket 数据帧的格式:


0                   1                   2                   3

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1

+-+-+-+-+-------+-+-------------+-------------------------------+

|F|R|R|R| opcode|M| Payload len |    Extended payload length    |

|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |

|N|V|V|V|       |S|             |   (if payload len==126/127)   |

| |1|2|3|       |K|             |                               |

+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +

|     Extended payload length continued, if payload len == 127  |

+ - - - - - - - - - - - - - - - +-------------------------------+

|                               |Masking-key, if MASK set to 1  |

+-------------------------------+-------------------------------+

| Masking-key (continued)       |          Payload Data         |

+-------------------------------- - - - - - - - - - - - - - - - +

:                     Payload Data continued ...                :

+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +

|                     Payload Data continued ...                |

+---------------------------------------------------------------+

可能有一些小伙伴看到上面的内容之后,就开始有点 “懵逼” 了。下面我们来结合实际的数据帧来进一步分析一下:




在上图中,阿宝哥简单分析了 “发送普通文本” 示例对应的数据帧格式。这里我们来进一步介绍一下 Payload length,因为在后面开发数据解析功能的时候,需要用到该知识点。


Payload length 表示以字节为单位的 “有效负载数据” 长度。它有以下几种情形:


如果值为 0-125,那么就表示负载数据的长度。

如果是 126,那么接下来的 2 个字节解释为 16 位的无符号整形作为负载数据的长度。

如果是 127,那么接下来的 8 个字节解释为一个 64 位的无符号整形(最高位的 bit 必须为 0)作为负载数据的长度。

多字节长度量以网络字节顺序表示,有效负载长度是指 “扩展数据” + “应用数据” 的长度。“扩展数据” 的长度可能为 0,那么有效负载长度就是 “应用数据” 的长度。


另外,除非协商过扩展,否则 “扩展数据” 长度为 0 字节。在握手协议中,任何扩展都必须指定 “扩展数据” 的长度,这个长度如何进行计算,以及这个扩展如何使用。如果存在扩展,那么这个 “扩展数据” 包含在总的有效负载长度中。


3.3.2 掩码算法

掩码字段是一个由客户端随机选择的 32 位的值。掩码值必须是不可被预测的。因此,掩码必须来自强大的熵源(entropy),并且给定的掩码不能让服务器或者代理能够很容易的预测到后续帧。掩码的不可预测性对于预防恶意应用的作者在网上暴露相关的字节数据至关重要。


掩码不影响数据荷载的长度,对数据进行掩码操作和对数据进行反掩码操作所涉及的步骤是相同的。掩码、反掩码操作都采用如下算法:


j = i MOD 4

transformed-octet-i = original-octet-i XOR masking-key-octet-j

original-octet-i:为原始数据的第 i 字节。

transformed-octet-i:为转换后的数据的第 i 字节。

masking-key-octet-j:为 mask key 第 j 字节。

为了让小伙伴们能够更好的理解上面掩码的计算过程,我们来对示例中 “我是阿宝哥” 数据进行掩码操作。这里 “我是阿宝哥” 对应的 UTF-8 编码如下所示:


E6 88 91 E6 98 AF E9 98 BF E5 AE 9D E5 93 A5

而对应的 Masking-Key 为 0x08f6efb1,根据上面的算法,我们可以这样进行掩码运算:


let uint8 = new Uint8Array([0xE6, 0x88, 0x91, 0xE6, 0x98, 0xAF, 0xE9, 0x98,

 0xBF, 0xE5, 0xAE, 0x9D, 0xE5, 0x93, 0xA5]);

let maskingKey = new Uint8Array([0x08, 0xf6, 0xef, 0xb1]);

let maskedUint8 = new Uint8Array(uint8.length);


for (let i = 0, j = 0; i < uint8.length; i++, j = i % 4) {

 maskedUint8[i] = uint8[i] ^ maskingKey[j];

}


console.log(Array.from(maskedUint8).map(num=>Number(num).toString(16)).join(' '));

以上代码成功运行后,控制台会输出以下结果:


ee 7e 7e 57 90 59 6 29 b7 13 41 2c ed 65 4a

上述结果与 WireShark 中的 Masked payload 对应的值是一致的,具体如下图所示:




在 WebSocket 协议中,数据掩码的作用是增强协议的安全性。但数据掩码并不是为了保护数据本身,因为算法本身是公开的,运算也不复杂。那么为什么还要引入数据掩码呢?引入数据掩码是为了防止早期版本的协议中存在的代理缓存污染攻击等问题。


了解完 WebSocket 掩码算法和数据掩码的作用之后,我们再来介绍一下数据分片的概念。


3.3.3 数据分片

WebSocket 的每条消息可能被切分成多个数据帧。当 WebSocket 的接收方收到一个数据帧时,会根据 FIN 的值来判断,是否已经收到消息的最后一个数据帧。


利用 FIN 和 Opcode,我们就可以跨帧发送消息。操作码告诉了帧应该做什么。如果是 0x1,有效载荷就是文本。如果是 0x2,有效载荷就是二进制数据。但是,如果是 0x0,则该帧是一个延续帧。这意味着服务器应该将帧的有效负载连接到从该客户机接收到的最后一个帧。


为了让大家能够更好地理解上述的内容,我们来看一个来自 MDN 上的示例:


Client: FIN=1, opcode=0x1, msg="hello"

Server: (process complete message immediately) Hi.

Client: FIN=0, opcode=0x1, msg="and a"

Server: (listening, new message containing text started)

Client: FIN=0, opcode=0x0, msg="happy new"

Server: (listening, payload concatenated to previous message)

Client: FIN=1, opcode=0x0, msg="year!"

Server: (process complete message) Happy new year to you too!

在以上示例中,客户端向服务器发送了两条消息。第一个消息在单个帧中发送,而第二个消息跨三个帧发送。


其中第一个消息是一个完整的消息(FIN=1 且 opcode != 0x0),因此服务器可以根据需要进行处理或响应。而第二个消息是文本消息(opcode=0x1)且 FIN=0,表示消息还没发送完成,还有后续的数据帧。该消息的所有剩余部分都用延续帧(opcode=0x0)发送,消息的最终帧用 FIN=1 标记。


好的,简单介绍了数据分片的相关内容。接下来,我们来开始实现消息通信功能。


3.4 实现消息通信功能

阿宝哥把实现消息通信功能,分解为消息解析与消息响应两个子功能,下面我们分别来介绍如何实现这两个子功能。


3.4.1 消息解析

利用消息通信基础环节中介绍的相关知识,阿宝哥实现了一个 parseMessage 函数,用来解析客户端传过来的 WebSocket 数据帧。出于简单考虑,这里只处理文本帧,具体代码如下所示:


function parseMessage(buffer) {

 // 第一个字节,包含了FIN位,opcode, 掩码位

 const firstByte = buffer.readUInt8(0);

 // [FIN, RSV, RSV, RSV, OPCODE, OPCODE, OPCODE, OPCODE];

 // 右移7位取首位,1位,表示是否是最后一帧数据

 const isFinalFrame = Boolean((firstByte >>> 7) & 0x01);

 console.log("isFIN: ", isFinalFrame);

 // 取出操作码,低四位

 /**

  * %x0:表示一个延续帧。当 Opcode 为 0 时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片;

  * %x1:表示这是一个文本帧(text frame);

  * %x2:表示这是一个二进制帧(binary frame);

  * %x3-7:保留的操作代码,用于后续定义的非控制帧;

  * %x8:表示连接断开;

  * %x9:表示这是一个心跳请求(ping);

  * %xA:表示这是一个心跳响应(pong);

  * %xB-F:保留的操作代码,用于后续定义的控制帧。

  */

 const opcode = firstByte & 0x0f;

 if (opcode === 0x08) {

   // 连接关闭

   return;

 }

 if (opcode === 0x02) {

   // 二进制帧

   return;

 }

 if (opcode === 0x01) {

   // 目前只处理文本帧

   let offset = 1;

   const secondByte = buffer.readUInt8(offset);

   // MASK: 1位,表示是否使用了掩码,在发送给服务端的数据帧里必须使用掩码,而服务端返回时不需要掩码

   const useMask = Boolean((secondByte >>> 7) & 0x01);

   console.log("use MASK: ", useMask);

   const payloadLen = secondByte & 0x7f; // 低7位表示载荷字节长度

   offset += 1;

   // 四个字节的掩码

   let MASK = [];

   // 如果这个值在0-125之间,则后面的4个字节(32位)就应该被直接识别成掩码;

   if (payloadLen <= 0x7d) {

     // 载荷长度小于125

     MASK = buffer.slice(offset, 4 + offset);

     offset += 4;

     console.log("payload length: ", payloadLen);

   } else if (payloadLen === 0x7e) {

     // 如果这个值是126,则后面两个字节(16位)内容应该,被识别成一个16位的二进制数表示数据内容大小;

     console.log("payload length: ", buffer.readInt16BE(offset));

     // 长度是126, 则后面两个字节作为payload length,32位的掩码

     MASK = buffer.slice(offset + 2, offset + 2 + 4);

     offset += 6;

   } else {

     // 如果这个值是127,则后面的8个字节(64位)内容应该被识别成一个64位的二进制数表示数据内容大小

     MASK = buffer.slice(offset + 8, offset + 8 + 4);

     offset += 12;

   }

   // 开始读取后面的payload,与掩码计算,得到原来的字节内容

   const newBuffer = [];

   const dataBuffer = buffer.slice(offset);

   for (let i = 0, j = 0; i < dataBuffer.length; i++, j = i % 4) {

     const nextBuf = dataBuffer[i];

     newBuffer.push(nextBuf ^ MASK[j]);

   }

   return Buffer.from(newBuffer).toString();

 }

 return "";

}

创建完 parseMessage 函数,我们来更新一下之前创建的 WebSocket 服务器:


server.on("upgrade", function (req, socket) {

 socket.on("data", (buffer) => {

   const message = parseMessage(buffer);

   if (message) {

     console.log("Message from client:" + message);

   } else if (message === null) {

     console.log("WebSocket connection closed by the client.");

   }

 });

 if (req.headers["upgrade"] !== "websocket") {

   socket.end("HTTP/1.1 400 Bad Request");

   return;

 }

 // 省略已有代码

});

更新完成之后,我们重新启动服务器,然后继续使用 “发送普通文本” 的示例来测试消息解析功能。以下发送 “我是阿宝哥” 文本消息后,WebSocket 服务器输出的信息。


Server running at http://localhost:8888

isFIN:  true

use MASK:  true

payload length:  15

Message from client:我是阿宝哥

通过观察以上的输出信息,我们的 WebSocket 服务器已经可以成功解析客户端发送包含普通文本的数据帧,下一步我们来实现消息响应的功能。


3.4.2 消息响应

要把数据返回给客户端,我们的 WebSocket 服务器也得按照 WebSocket 数据帧的格式来封装数据。与前面介绍的 parseMessage 函数一样,阿宝哥也封装了一个 constructReply 函数用来封装返回的数据,该函数的具体代码如下:


function constructReply(data) {

 const json = JSON.stringify(data);

 const jsonByteLength = Buffer.byteLength(json);

 // 目前只支持小于65535字节的负载

 const lengthByteCount = jsonByteLength < 126 ? 0 : 2;

 const payloadLength = lengthByteCount === 0 ? jsonByteLength : 126;

 const buffer = Buffer.alloc(2 + lengthByteCount + jsonByteLength);

 // 设置数据帧首字节,设置opcode为1,表示文本帧

 buffer.writeUInt8(0b10000001, 0);

 buffer.writeUInt8(payloadLength, 1);

 // 如果payloadLength为126,则后面两个字节(16位)内容应该,被识别成一个16位的二进制数表示数据内容大小

 let payloadOffset = 2;

 if (lengthByteCount > 0) {

   buffer.writeUInt16BE(jsonByteLength, 2);

   payloadOffset += lengthByteCount;

 }

 // 把JSON数据写入到Buffer缓冲区中

 buffer.write(json, payloadOffset);

 return buffer;

}

创建完 constructReply 函数,我们再来更新一下之前创建的 WebSocket 服务器:


server.on("upgrade", function (req, socket) {

 socket.on("data", (buffer) => {

   const message = parseMessage(buffer);

   if (message) {

     console.log("Message from client:" + message);

     // 新增以下

停止犯下这5个JavaScript风格错误,使你的代码可读和可维护的快速提示

seo达人

使你的代码可读和可维护的快速提示。


有多少次,你打开一个旧的项目,发现混乱的代码,当你添加一些新的东西时,很容易崩溃?我们都有过这样的经历。


为了减少难以读懂的javascript的数量,我提供了以下示例。这些都是我过去所犯过的错误。


对具有多个返回值的函数使用数组解构

假设我们有一个返回多个值的函数。一种可能的实现是使用数组解构,如下所示:


const func = () => {

 const a = 1;

 const b = 2;

 const c = 3;

 const d = 4;

 return [a,b,c,d];

}

const [a,b,c,d] = func();

console.log(a,b,c,d); // 1,2,3,4

尽管上面的方法很好用,但确实引入了一些复杂性。


当我们调用函数并将值分配给 a,b,c,d 时,我们需要注意返回数据的顺序。这里的一个小错误可能会成为调试的噩梦。


此外,无法确切指定我们要从函数中获取哪些值,如果我们只需要 c 和 d 怎么办?


相反,我们可以使用对象解构。


const func = () => {

 const a = 1;

 const b = 2;

 const c = 3;

 const d = 4;

 return {a,b,c,d};

}

const {c,d} = func();

现在,我们可以轻松地从函数中选择所需的数据,这也为我们的代码提供了未来的保障,允许我们在不破坏东西的情况下增加额外的返回变量。


不对函数参数使用对象分解

假设我们有一个函数,该函数将一个对象作为参数并对该对象的属性执行一些操作。一种幼稚的方法可能看起来像这样:


// 不推荐

function getDaysRemaining(subscription) {

 const startDate = subscription.startDate;

 const endDate = subscription.endDate;

 return endDate - startDate;

}

上面的方法按预期工作,但是,我们创建了两个不必要的临时引用 startDate 和 endDate。


一种更好的实现是对 subscription 对象使用对象解构来在一行中获取 startDate 和 endDate。


// 推荐

function getDaysRemaining(subscription) {

 const { startDate, endDate } = subscription;

 return startDate - endDate;

}

我们可以更进一步,直接对参数执行对象析构。


// 更好

function getDaysRemaining({ startDate, endDate }) {

 return startDate - endDate;

}

更优雅,不是吗?


在不使用扩展运算符的情况下复制数组

使用 for循环遍历数组并将其元素复制到新数组是冗长且相当丑陋的。


可以以简洁明了的方式使用扩展运算符来达到相同的效果。


const stuff = [1,2,3];


// 不推荐

const stuffCopyBad = []

for(let i = 0; i < stuff.length; i++){

 stuffCopyBad[i] = stuff[i];

}


// 推荐

const stuffCopyGood = [...stuff];

使用var

使用 const 保证不能重新分配变量。这样可以减少我们代码中的错误,并使其更易于理解。


// 不推荐

var x = "badX";

var y = "baxY";


// 推荐

const x = "goodX";

const y = "goodX";

果你确实需要重新分配变量,请始终选择 let 而不是 var。


这是因为 let 是块作用域的,而 var 是函数作用域的。


块作用域告诉我们,只能在定义它的代码块内部访问变量,尝试访问块外部的变量会给我们提供ReferenceError。


for(let i = 0; i < 10; i++){

 //something

}

print(i) // ReferenceError: i is not defined

函数作用域告诉我们,只能在定义其的函数内部访问变量。


for(var i = 0; i < 10; i++){

 //something

}

console.log(i) // 10

let 和 const 都是块范围的。


不使用模板字面值

手动将字符串连接在一起相当麻烦,而且输入时可能会造成混淆。这是一个例子:


// 不推荐

function printStartAndEndDate({ startDate, endDate }) {

 console.log('StartDate:' + startDate + ',EndDate:' + endDate)

}

模板文字为我们提供了一种可读且简洁的语法,该语法支持字符串插值。


// 推荐

function printStartAndEndDate({ startDate, endDate }) {

 console.log(`StartDate: ${startDate}, EndDate: ${endDate}`)

}

模板文字也提供了嵌入新行的简便方法,你所需要做的就是照常按键盘上的Enter键。


// 两行打印

function printStartAndEndDate({ startDate, endDate }) {

 console.log(`StartDate: ${startDate}

 EndDate: ${endDate}`)

}

蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务

你可能不需要在 JavaScript 使用 switch 语句!

seo达人

没有 switch 就没有复杂的代码块

switch很方便:给定一个表达式,我们可以检查它是否与一堆case子句中的其他表达式匹配。 考虑以下示例:


const name = "Juliana";


switch (name) {

 case "Juliana":

   console.log("She's Juliana");

   break;

 case "Tom":

   console.log("She's not Juliana");

   break;

}

当 name 为“Juliana”时,我们将打印一条消息,并立即中断退出该块。 在switch函数内部时,直接在 case 块使用 return,就可以省略break。


当没有匹配项时,可以使用 default 选项:


const name = "Kris";


switch (name) {

 case "Juliana":

   console.log("She's Juliana");

   break;

 case "Tom":

   console.log("She's not Juliana");

   break;

 default:

   console.log("Sorry, no match");

}

switch在 Redux reducers 中也大量使用(尽管Redux Toolkit简化了样板),以避免产生大量的if。 考虑以下示例:


const LOGIN_SUCCESS = "LOGIN_SUCCESS";

const LOGIN_FAILED = "LOGIN_FAILED";


const authState = {

 token: "",

 error: "",

};


function authReducer(state = authState, action) {

 switch (action.type) {

   case LOGIN_SUCCESS:

     return { ...state, token: action.payload };

   case LOGIN_FAILED:

     return { ...state, error: action.payload };

   default:

     return state;

 }

}

这有什么问题吗?几乎没有。但是有没有更好的选择呢?


从 Python 获得的启示

来自 Telmo 的这条 Tweet引起了我的注意。 他展示了两种“switch”风格,其中一种非常接近Python中的模式。


Python 没有开关,它给我们一个更好的替代方法。 首先让我们将代码从 JavaScript 移植到Python:


LOGIN_SUCCESS = "LOGIN_SUCCESS"

LOGIN_FAILED = "LOGIN_FAILED"


auth_state = {"token": "", "error": ""}



def auth_reducer(state=auth_state, action={}):

   mapping = {

       LOGIN_SUCCESS: {**state, "token": action["payload"]},

       LOGIN_FAILED: {**state, "error": action["payload"]},

   }


   return mapping.get(action["type"], state)

在 Python 中,我们可以使用字典来模拟switch 。 dict.get() 可以用来表示 switch 的 default 语句。


当访问不存在的key时,Python 会触发一个 KeyError 错误:


>>> my_dict = {

   "name": "John",

   "city": "Rome",

   "age": 44

   }


>>> my_dict["not_here"]


# Output: KeyError: 'not_here'

.get()方法是一种更安全方法,因为它不会引发错误,并且可以为不存在的key指定默认值:


>>> my_dict = {

   "name": "John",

   "city": "Rome",

   "age": 44

   }


>>> my_dict.get("not_here", "not found")


# Output: 'not found'

因此,Pytho n中的这一行:


return mapping.get(action["type"], state)

等价于 JavaScript中的:


function authReducer(state = authState, action) {

 ...

   default:

     return state;

 ...

}

使用字典的方式替换 switch

再次思考前面的示例:


const LOGIN_SUCCESS = "LOGIN_SUCCESS";

const LOGIN_FAILED = "LOGIN_FAILED";


const authState = {

 token: "",

 error: "",

};


function authReducer(state = authState, action) {

 switch (action.type) {

   case LOGIN_SUCCESS:

     return { ...state, token: action.payload };

   case LOGIN_FAILED:

     return { ...state, error: action.payload };

   default:

     return state;

 }

}

如果不使用 switch 我们可以这样做:


function authReducer(state = authState, action) {

 const mapping = {

   [LOGIN_SUCCESS]: { ...state, token: action.payload },

   [LOGIN_FAILED]: { ...state, error: action.payload }

 };


 return mapping[action.type] || state;

}

这里我们使用 ES6 中的计算属性,此处,mapping的属性是根据两个常量即时计算的:LOGIN_SUCCESS 和 LOGIN_FAILED。

属性对应的值,我们这里使用的是对象解构,这里 ES9((ECMAScript 2018)) 出来的。


const mapping = {

 [LOGIN_SUCCESS]: { ...state, token: action.payload },

 [LOGIN_FAILED]: { ...state, error: action.payload }

}

你如何看待这种方法?它对 switch 来说可能还能一些限制,但对于 reducer 来说可能是一种更好的方案。


但是,此代码的性能如何?


性能怎么样?

switch 的性能优于字典的写法。我们可以使用下面的事例测试一下:


console.time("sample");

for (let i = 0; i < 2000000; i++) {

 const nextState = authReducer(authState, {

   type: LOGIN_SUCCESS,

   payload: "some_token"

 });

}

console.timeEnd("sample");

测量它们十次左右,


for t in {1..10}; do node switch.js >> switch.txt;done

for t in {1..10}; do node map.js >> map.txt;done

clipboard.png


人才们的 【三连】 就是小智不断分享的最大动力,如果本篇博客有任何错误和建议,欢迎人才们留言,最后,谢谢大家的观看。


原文:https://codeburst.io/alternat...


代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug。

蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务



vuex管理状态仓库详解

seo达人

一.什么是Vuex?

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 也集成到 Vue 的官方调试工具 devtools extension,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。采用了全局单例模式,将组件的共享状态抽离出来管理,使得组件树中每一个位置都可以获取共享的状态或者触发行为。
那么什么是状态呢?我把状态理解为在没有使用vuex时,在当前组件中data内需要共用的数据为状态。
vuex使得状态或行为成为了共享的状态,所共享的状态或行为可以在各个组件中都可以访问到,省去了子父或子子之间传递变量,提高了开发效率。

二.不使用vuex时与使用vuex时的差别

当我们不使用vuex时,对于组件之间传递信息会较为麻烦。

不使用vuex时

父子之间传递信息:

App.vue文件中:

<template>
  <div id="app">
      <Fruits :fruitList="fruitList"/>
  </div>
</template> 
<script> import Goods from './components/Goods'; export default { name: 'App',
  components:{
    Fruits,
    Goods
  }, data(){
    return{ goodList:[
      {
        name:'doll',
        price:12 },
      { name:'glass',
        price:10 }
    ],
    }
  }
}
</script>
<style>
</style>

Good.vue文件中:

<template>
  <div class="hello">
      <ul>
        <li v-for="(good,index) in goodList" :key="index"> name:{{good.name}} number: {{good.number}} {{index}}
        </li>
      </ul>
  </div>
</template>

<script> export default { props:['goodList'],
}
</script>
<style>

</style>

兄弟之间传递信息:

首先先创建一个js文件作为两兄弟之间传输的纽扣,这里起名为msg.js

//创建并暴露vue import Vue from 'vue';
export default new Vue

兄弟组件Goods:

<template>
  <div>
        <button @click="deliver">点击</button>
  </div>
</template>

<script> import MSG from '../msg';
export default {
  data(){ return{
      msg:'hahah' }
  },
  methods:{
    deliver() {
        MSG.$emit('showMsg',this.msg)
    }
  }

}
</script>
<style>

</style>

兄弟组件Fruits:

<template>
  <div>
      <div class="fruit">
          {{text}}
      </div>
  </div>
</template>
<script> import MSG from '../msg';
export default {
    data(){ return{
        text:'' }
    },
    created(){ this.getMsg()
    },
    methods:{
      getMsg(){
        MSG.$on('showMsg',(message)=>{ this.text = message
        })
      }
    }
}
</script>
<style>
</style>

在App组件中的代码:
在这里插入图片描述
点击按钮:


蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务

VUE-多文件断点续传、秒传、分片上传

seo达人

凡是要知其然知其所以然

文件上传相信很多朋友都有遇到过,那或许你也遇到过当上传大文件时,上传时间较长,且经常失败的困扰,并且失败后,又得重新上传很是烦人。那我们先了解下失败的原因吧!


据我了解大概有以下原因:


服务器配置:例如在PHP中默认的文件上传大小为8M【post_max_size = 8m】,若你在一个请求体中放入8M以上的内容时,便会出现异常

请求超时:当你设置了接口的超时时间为10s,那么上传大文件时,一个接口响应时间超过10s,那么便会被Faild掉。

网络波动:这个就属于不可控因素,也是较常见的问题。

基于以上原因,聪明的人们就想到了,将文件拆分多个小文件,依次上传,不就解决以上1,2问题嘛,这便是分片上传。 网络波动这个实在不可控,也许一阵大风刮来,就断网了呢。那这样好了,既然断网无法控制,那我可以控制只上传已经上传的文件内容,不就好了,这样大大加快了重新上传的速度。所以便有了“断点续传”一说。此时,人群中有人插了一嘴,有些文件我已经上传一遍了,为啥还要在上传,能不能不浪费我流量和时间。喔...这个嘛,简单,每次上传时判断下是否存在这个文件,若存在就不重新上传便可,于是又有了“秒传”一说。从此这"三兄弟" 便自行CP,统治了整个文件界。”

注意文中的代码并非实际代码,请移步至github查看代码

https://github.com/pseudo-god...


分片上传

HTML

原生INPUT样式较丑,这里通过样式叠加的方式,放一个Button.

 <div class="btns">

   <el-button-group>

     <el-button :disabled="changeDisabled">

       <i class="el-icon-upload2 el-icon--left" size="mini"></i>选择文件

       <input

         v-if="!changeDisabled"

         type="file"

         :multiple="multiple"

         class="select-file-input"

         :accept="accept"

         @change="handleFileChange"

       />

     </el-button>

     <el-button :disabled="uploadDisabled" @click="handleUpload()"><i class="el-icon-upload el-icon--left" size="mini"></i>上传</el-button>

     <el-button :disabled="pauseDisabled" @click="handlePause"><i class="el-icon-video-pause el-icon--left" size="mini"></i>暂停</el-button>

     <el-button :disabled="resumeDisabled" @click="handleResume"><i class="el-icon-video-play el-icon--left" size="mini"></i>恢复</el-button>

     <el-button :disabled="clearDisabled" @click="clearFiles"><i class="el-icon-video-play el-icon--left" size="mini"></i>清空</el-button>

   </el-button-group>

   <slot

   

//data 数据


var chunkSize = 10 * 1024 * 1024; // 切片大小

var fileIndex = 0; // 当前正在被遍历的文件下标


data: () => ({

   container: {

     files: null

   },

   tempFilesArr: [], // 存储files信息

   cancels: [], // 存储要取消的请求

   tempThreads: 3,

   // 默认状态

   status: Status.wait

 }),

   

一个稍微好看的UI就出来了。




选择文件

选择文件过程中,需要对外暴露出几个钩子,熟悉elementUi的同学应该很眼熟,这几个钩子基本与其一致。onExceed:文件超出个数限制时的钩子、beforeUpload:文件上传之前

fileIndex 这个很重要,因为是多文件上传,所以定位当前正在被上传的文件就很重要,基本都靠它


handleFileChange(e) {

 const files = e.target.files;

 if (!files) return;

 Object.assign(this.$data, this.$options.data()); // 重置data所有数据


 fileIndex = 0; // 重置文件下标

 this.container.files = files;

 // 判断文件选择的个数

 if (this.limit && this.container.files.length > this.limit) {

   this.onExceed && this.onExceed(files);

   return;

 }


 // 因filelist不可编辑,故拷贝filelist 对象

 var index = 0; // 所选文件的下标,主要用于剔除文件后,原文件list与临时文件list不对应的情况

 for (const key in this.container.files) {

   if (this.container.files.hasOwnProperty(key)) {

     const file = this.container.files[key];


     if (this.beforeUpload) {

       const before = this.beforeUpload(file);

       if (before) {

         this.pushTempFile(file, index);

       }

     }


     if (!this.beforeUpload) {

       this.pushTempFile(file, index);

     }


     index++;

   }

 }

},

// 存入 tempFilesArr,为了上面的钩子,所以将代码做了拆分

pushTempFile(file, index) {

 // 额外的初始值

 const obj = {

   status: fileStatus.wait,

   chunkList: [],

   uploadProgress: 0,

   hashProgress: 0,

   index

 };

 for (const k in file) {

   obj[k] = file[k];

 }

 console.log('pushTempFile -> obj', obj);

 this.tempFilesArr.push(obj);

}

分片上传

创建切片,循环分解文件即可


 createFileChunk(file, size = chunkSize) {

   const fileChunkList = [];

   var count = 0;

   while (count < file.size) {

     fileChunkList.push({

       file: file.slice(count, count + size)

     });

     count += size;

   }

   return fileChunkList;

 }

循环创建切片,既然咱们做的是多文件,所以这里就有循环去处理,依次创建文件切片,及切片的上传。

async handleUpload(resume) {

 if (!this.container.files) return;

 this.status = Status.uploading;

 const filesArr = this.container.files;

 var tempFilesArr = this.tempFilesArr;


 for (let i = 0; i < tempFilesArr.length; i++) {

   fileIndex = i;

   //创建切片

   const fileChunkList = this.createFileChunk(

     filesArr[tempFilesArr[i].index]

   );

     

   tempFilesArr[i].fileHash ='xxxx'; // 先不用看这个,后面会讲,占个位置

   tempFilesArr[i].chunkList = fileChunkList.map(({ file }, index) => ({

     fileHash: tempFilesArr[i].hash,

     fileName: tempFilesArr[i].name,

     index,

     hash: tempFilesArr[i].hash + '-' + index,

     chunk: file,

     size: file.size,

     uploaded: false,

     progress: 0, // 每个块的上传进度

     status: 'wait' // 上传状态,用作进度状态显示

   }));

   

   //上传切片

   await this.uploadChunks(this.tempFilesArr[i]);

 }

}

上传切片,这个里需要考虑的问题较多,也算是核心吧,uploadChunks方法只负责构造传递给后端的数据,核心上传功能放到sendRequest方法中

async uploadChunks(data) {

 var chunkData = data.chunkList;

 const requestDataList = chunkData

   .map(({ fileHash, chunk, fileName, index }) => {

     const formData = new FormData();

     formData.append('md5', fileHash);

     formData.append('file', chunk);

     formData.append('fileName', index); // 文件名使用切片的下标

     return { formData, index, fileName };

   });


 try {

   await this.sendRequest(requestDataList, chunkData);

 } catch (error) {

   // 上传有被reject的

   this.$message.error('亲 上传失败了,考虑重试下呦' + error);

   return;

 }


 // 合并切片

 const isUpload = chunkData.some(item => item.uploaded === false);

 console.log('created -> isUpload', isUpload);

 if (isUpload) {

   alert('存在失败的切片');

 } else {

   // 执行合并

   await this.mergeRequest(data);

 }

}

sendReques。上传这是最重要的地方,也是容易失败的地方,假设有10个分片,那我们若是直接发10个请求的话,很容易达到浏览器的瓶颈,所以需要对请求进行并发处理。


并发处理:这里我使用for循环控制并发的初始并发数,然后在 handler 函数里调用自己,这样就控制了并发。在handler中,通过数组API.shift模拟队列的效果,来上传切片。

重试: retryArr 数组存储每个切片文件请求的重试次数,做累加。比如[1,0,2],就是第0个文件切片报错1次,第2个报错2次。为保证能与文件做对应,const index = formInfo.index; 我们直接从数据中拿之前定义好的index。 若失败后,将失败的请求重新加入队列即可。


关于并发及重试我写了一个小Demo,若不理解可以自己在研究下,文件地址:https://github.com/pseudo-god... , 重试代码好像被我弄丢了,大家要是有需求,我再补吧!

   // 并发处理

sendRequest(forms, chunkData) {

 var finished = 0;

 const total = forms.length;

 const that = this;

 const retryArr = []; // 数组存储每个文件hash请求的重试次数,做累加 比如[1,0,2],就是第0个文件切片报错1次,第2个报错2次


 return new Promise((resolve, reject) => {

   const handler = () => {

     if (forms.length) {

       // 出栈

       const formInfo = forms.shift();


       const formData = formInfo.formData;

       const index = formInfo.index;

       

       instance.post('fileChunk', formData, {

         onUploadProgress: that.createProgresshandler(chunkData[index]),

         cancelToken: new CancelToken(c => this.cancels.push(c)),

         timeout: 0

       }).then(res => {

         console.log('handler -> res', res);

         // 更改状态

         chunkData[index].uploaded = true;

         chunkData[index].status = 'success';

         

         finished++;

         handler();

       })

         .catch(e => {

           // 若暂停,则禁止重试

           if (this.status === Status.pause) return;

           if (typeof retryArr[index] !== 'number') {

             retryArr[index] = 0;

           }


           // 更新状态

           chunkData[index].status = 'warning';


           // 累加错误次数

           retryArr[index]++;


           // 重试3次

           if (retryArr[index] >= this.chunkRetry) {

             return reject('重试失败', retryArr);

           }


           this.tempThreads++; // 释放当前占用的通道


           // 将失败的重新加入队列

           forms.push(formInfo);

           handler();

         });

     }


     if (finished >= total) {

       resolve('done');

     }

   };


   // 控制并发

   for (let i = 0; i < this.tempThreads; i++) {

     handler();

   }

 });

}

切片的上传进度,通过axios的onUploadProgress事件,结合createProgresshandler方法进行维护

// 切片上传进度

createProgresshandler(item) {

 return p => {

   item.progress = parseInt(String((p.loaded / p.total) * 100));

   this.fileProgress();

 };

}

Hash计算

其实就是算一个文件的MD5值,MD5在整个项目中用到的地方也就几点。

秒传,需要通过MD5值判断文件是否已存在。

续传:需要用到MD5作为key值,当唯一值使用。

本项目主要使用worker处理,性能及速度都会有很大提升.

由于是多文件,所以HASH的计算进度也要体现在每个文件上,所以这里使用全局变量fileIndex来定位当前正在被上传的文件

执行计算hash


正在上传文件


// 生成文件 hash(web-worker)

calculateHash(fileChunkList) {

 return new Promise(resolve => {

   this.container.worker = new Worker('./hash.js');

   this.container.worker.postMessage({ fileChunkList });

   this.container.worker.onmessage = e => {

     const { percentage, hash } = e.data;

     if (this.tempFilesArr[fileIndex]) {

       this.tempFilesArr[fileIndex].hashProgress = Number(

         percentage.toFixed(0)

       );

     }


     if (hash) {

       resolve(hash);

     }

   };

 });

}

因使用worker,所以我们不能直接使用NPM包方式使用MD5。需要单独去下载spark-md5.js文件,并引入


//hash.js


self.importScripts("/spark-md5.min.js"); // 导入脚本

// 生成文件 hash

self.onmessage = e => {

 const { fileChunkList } = e.data;

 const spark = new self.SparkMD5.ArrayBuffer();

 let percentage = 0;

 let count = 0;

 const loadNext = index => {

   const reader = new FileReader();

   reader.readAsArrayBuffer(fileChunkList[index].file);

   reader.onload = e => {

     count++;

     spark.append(e.target.result);

     if (count === fileChunkList.length) {

       self.postMessage({

         percentage: 100,

         hash: spark.end()

       });

       self.close();

     } else {

       percentage += 100 / fileChunkList.length;

       self.postMessage({

         percentage

       });

       loadNext(count);

     }

   };

 };

 loadNext(0);

};

文件合并

当我们的切片全部上传完毕后,就需要进行文件的合并,这里我们只需要请求接口即可

mergeRequest(data) {

  const obj = {

    md5: data.fileHash,

    fileName: data.name,

    fileChunkNum: data.chunkList.length

  };


  instance.post('fileChunk/merge', obj,

    {

      timeout: 0

    })

    .then((res) => {

      this.$message.success('上传成功');

    });

}

Done: 至此一个分片上传的功能便已完成

断点续传

顾名思义,就是从那断的就从那开始,明确思路就很简单了。一般有2种方式,一种为服务器端返回,告知我从那开始,还有一种是浏览器端自行处理。2种方案各有优缺点。本项目使用第二种。

思路:已文件HASH为key值,每个切片上传成功后,记录下来便可。若需要续传时,直接跳过记录中已存在的便可。本项目将使用Localstorage进行存储,这里我已提前封装好addChunkStorage、getChunkStorage方法。


存储在Stroage的数据




缓存处理

在切片上传的axios成功回调中,存储已上传成功的切片


instance.post('fileChunk', formData, )

 .then(res => {

   // 存储已上传的切片下标

+ this.addChunkStorage(chunkData[index].fileHash, index);

   handler();

 })

在切片上传前,先看下localstorage中是否存在已上传的切片,并修改uploaded


   async handleUpload(resume) {

+      const getChunkStorage = this.getChunkStorage(tempFilesArr[i].hash);

     tempFilesArr[i].chunkList = fileChunkList.map(({ file }, index) => ({

+        uploaded: getChunkStorage && getChunkStorage.includes(index), // 标识:是否已完成上传

+        progress: getChunkStorage && getChunkStorage.includes(index) ? 100 : 0,

+        status: getChunkStorage && getChunkStorage.includes(index)? 'success'

+              : 'wait' // 上传状态,用作进度状态显示

     }));


   }

构造切片数据时,过滤掉uploaded为true的


async uploadChunks(data) {

 var chunkData = data.chunkList;

 const requestDataList = chunkData

+    .filter(({ uploaded }) => !uploaded)

   .map(({ fileHash, chunk, fileName, index }) => {

     const formData = new FormData();

     formData.append('md5', fileHash);

     formData.append('file', chunk);

     formData.append('fileName', index); // 文件名使用切片的下标

     return { formData, index, fileName };

   })

}

垃圾文件清理

随着上传文件的增多,相应的垃圾文件也会增多,比如有些时候上传一半就不再继续,或上传失败,碎片文件就会增多。解决方案我目前想了2种

前端在localstorage设置缓存时间,超过时间就发送请求通知后端清理碎片文件,同时前端也要清理缓存。

前后端都约定好,每个缓存从生成开始,只能存储12小时,12小时后自动清理

以上2中方案似乎都有点问题,极有可能造成前后端因时间差,引发切片上传异常的问题,后面想到合适的解决方案再来更新吧。

Done: 续传到这里也就完成了。


秒传

这算是最简单的,只是听起来很厉害的样子。原理:计算整个文件的HASH,在执行上传操作前,向服务端发送请求,传递MD5值,后端进行文件检索。若服务器中已存在该文件,便不进行后续的任何操作,上传也便直接结束。大家一看就明白

async handleUpload(resume) {

   if (!this.container.files) return;

   const filesArr = this.container.files;

   var tempFilesArr = this.tempFilesArr;


   for (let i = 0; i < tempFilesArr.length; i++) {

     const fileChunkList = this.createFileChunk(

       filesArr[tempFilesArr[i].index]

     );


     // hash校验,是否为秒传

+      tempFilesArr[i].hash = await this.calculateHash(fileChunkList);

+      const verifyRes = await this.verifyUpload(

+        tempFilesArr[i].name,

+        tempFilesArr[i].hash

+      );

+      if (verifyRes.data.presence) {

+       tempFilesArr[i].status = fileStatus.secondPass;

+       tempFilesArr[i].uploadProgress = 100;

+      } else {

       console.log('开始上传切片文件----》', tempFilesArr[i].name);

       await this.uploadChunks(this.tempFilesArr[i]);

     }

   }

 }

 // 文件上传之前的校验: 校验文件是否已存在

 verifyUpload(fileName, fileHash) {

   return new Promise(resolve => {

     const obj = {

       md5: fileHash,

       fileName,

       ...this.uploadArguments //传递其他参数

     };

     instance

       .post('fileChunk/presence', obj)

       .then(res => {

         resolve(res.data);

       })

       .catch(err => {

         console.log('verifyUpload -> err', err);

       });

   });

 }

Done: 秒传到这里也就完成了。

后端处理

文章好像有点长了,具体代码逻辑就先不贴了,除非有人留言要求,嘻嘻,有时间再更新

Node版

请前往 https://github.com/pseudo-god... 查看

JAVA版

下周应该会更新处理

PHP版

1年多没写PHP了,抽空我会慢慢补上来

待完善

切片的大小:这个后面会做出动态计算的。需要根据当前所上传文件的大小,自动计算合适的切片大小。避免出现切片过多的情况。

文件追加:目前上传文件过程中,不能继续选择文件加入队列。(这个没想好应该怎么处理。)

更新记录

组件已经运行一段时间了,期间也测试出几个问题,本来以为没BUG的,看起来BUG都挺严重

BUG-1:当同时上传多个内容相同但是文件名称不同的文件时,出现上传失败的问题。


预期结果:第一个上传成功后,后面相同的问文件应该直接秒传


实际结果:第一个上传成功后,其余相同的文件都失败,错误信息,块数不对。


原因:当第一个文件块上传完毕后,便立即进行了下一个文件的循环,导致无法及时获取文件是否已秒传的状态,从而导致失败。


解决方案:在当前文件分片上传完毕并且请求合并接口完毕后,再进行下一次循环。


将子方法都改为同步方式,mergeRequest 和 uploadChunks 方法





BUG-2: 当每次选择相同的文件并触发beforeUpload方法时,若第二次也选择了相同的文件,beforeUpload方法失效,从而导致整个流程失效。

原因:之前每次选择文件时,没有清空上次所选input文件的数据,相同数据的情况下,是不会触发input的change事件。


解决方案:每次点击input时,清空数据即可。我顺带优化了下其他的代码,具体看提交记录吧。


<input

 v-if="!changeDisabled"

 type="file"

 :multiple="multiple"

 class="select-file-input"

 :accept="accept"

+  οnclick="f.outerHTML=f.outerHTML"

 @change="handleFileChange"/>

重写了暂停和恢复的功能,实际上,主要是增加了暂停和恢复的状态





之前的处理逻辑太简单粗暴,存在诸多问题。现在将状态定位在每一个文件之上,这样恢复上传时,直接跳过即可





封装组件

写了一大堆,其实以上代码你直接复制也无法使用,这里我将此封装了一个组件。大家可以去github下载文件,里面有使用案例 ,若有用记得随手给个star,谢谢!

偷个懒,具体封装组件的代码就不列出来了,大家直接去下载文件查看,若有不明白的,可留言。


组件文档

Attribute

参数 类型 说明 默认 备注

headers Object 设置请求头

before-upload Function 上传文件前的钩子,返回false则停止上传

accept String 接受上传的文件类型

upload-arguments Object 上传文件时携带的参数

with-credentials Boolean 是否传递Cookie false

limit Number 最大允许上传个数 0 0为不限制

on-exceed Function 文件超出个数限制时的钩子

multiple Boolean 是否为多选模式 true

base-url String 由于本组件为内置的AXIOS,若你需要走代理,可以直接在这里配置你的基础路径

chunk-size Number 每个切片的大小 10M

threads Number 请求的并发数 3 并发数越高,对服务器的性能要求越高,尽可能用默认值即可

chunk-retry Number 错误重试次数 3 分片请求的错误重试次数

Slot

方法名 说明 参数 备注

header 按钮区域 无

tip 提示说明文字 无

后端接口文档:按文档实现即可

蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务






初学者应该看的 Webpack 完整指南(2020)

seo达人

我们应该学习 webpack 吗 ?

如今,CLI工具(如create-react-app或Vue -cli)已经为我们抽象了大部分配置,并提供了合理的默认设置。


即使那样,了解幕后工作原理还是有好处的,因为我们迟早需要对默认值进行一些调整。


在本文中中,我们会知道 webpack可以做什么,以及如何配置它以满足我们的日常需求。


什么是 webpack?

作为前端开发人员,我们应该熟悉 module 概念。 你可能听说过 AMD模块,UMD,Common JS还有ES模块。


webpack是一个模块绑定器,它对模块有一个更广泛的定义,对于webpack来说,模块是:


Common JS modules

AMD modules

CSS import

Images url

ES modules

webpack 还可以从这些模块中获取依赖关系。


webpack 的最终目标是将所有这些不同的源和模块类型统一起来,从而将所有内容导入JavaScript代码,并最生成可以运行的代码。


entry

Webpack的 entry(入口点)是收集前端项目的所有依赖项的起点。 实际上,这是一个简单的 JavaScript 文件。


这些依赖关系形成一个依赖关系图。


Webpack 的默认入口点(从版本4开始)是src/index.js,它是可配置的。 webpack 可以有多个入口点。


Output

output是生成的JavaScript和静态文件的地方。


Loaders

Loaders 是第三方扩展程序,可帮助webpack处理各种文件扩展名。 例如,CSS,图像或txt文件。


Loaders的目标是在模块中转换文件(JavaScript以外的文件)。 文件成为模块后,webpack可以将其用作项目中的依赖项。


Plugins

插件是第三方扩展,可以更改webpack的工作方式。 例如,有一些用于提取HTML,CSS或设置环境变量的插件。


Mode

webpack 有两种操作模式:开发(development)和生产(production)。 它们之间的主要区别是生产模式自动生成一些优化后的代码。


Code splitting

代码拆分或延迟加载是一种避免生成较大包的优化技术。


通过代码拆分,开发人员可以决定仅在响应某些用户交互时加载整个JavaScript块,比如单击或路由更改(或其他条件)。


被拆分的一段代码称为 chunk。


Webpack入门

开始使用webpack时,先创建一个新文件夹,然后进入该文件中,初始化一个NPM项目,如下所示:


mkdir webpack-tutorial && cd $_


npm init -y

接着安装 webpack,webpack-cli和 webpack-dev-server:


npm i webpack webpack-cli webpack-dev-server --save-dev

要运行 webpack,只需要在 package.json 配置如下命令即可:


 "scripts": {

   "dev": "webpack --mode development"

 },

通过这个脚本,我们指导webpack在开发模式下工作,方便在本地工作。


Webpack 的第一步

在开发模式下运行 webpack:


npm run dev

运行完后会看到如下错误:


ERROR in Entry module not found: Error: Can't resolve './src'

webpack 在这里寻找默认入口点src/index.js,所以我们需要手动创建一下,并输入一些内容:


mkdir src


echo 'console.log("Hello webpack!")' > src/index.js

现在再次运行npm run dev,错误就没有了。 运行的结果生成了一个名为dist/的新文件夹,其中包含一个名为main.js的 JS 文件:


dist

└── main.js

这是我们的第一个webpack包,也称为output。


配置 Webpack

对于简单的任务,webpack无需配置即可工作,但是很快我们就会遇到问题,一些文件如果没有指定的 loader 是没法打包的。所以,我们需要对 webpack进行配置,对于 webpack 的配置是在 webpack.config.js 进行的,所以我们需要创建该文件:


touch webpack.config.js

Webpack 用 JavaScript 编写,并在无头 JS 环境(例如Node.js)上运行。 在此文件中,至少需要一个module.exports,这是的 Common JS 导出方式:


module.exports = {

 //

};

在webpack.config.js中,我们可以通过添加或修改来改变webpack的行为方式


entry point

output

loaders

plugins

code splitting

例如,要更改入口路径,我们可以这样做


const path = require("path");


module.exports = {

 entry: { index: path.resolve(__dirname, "source", "index.js") }

};

现在,webpack 将在source/index.js中查找要加载的第一个文件。 要更改包的输出路径,我们可以这样做:


const path = require("path");


module.exports = {

 output: {

   path: path.resolve(__dirname, "build")

 }

}

这样,webpack将把最终生成包放在build中,而不是dist.(为了简单起见,在本文中,我们使用默认配置)。


打包 HTML

没有HTML页面的Web应用程序几乎没有用。 要在webpack中使用 HTML,我们需要安装一个插件html-webpack-plugin:


npm i html-webpack-plugin --save-dev

一旦插件安装好,我们就可以对其进行配置:


const HtmlWebpackPlugin = require("html-webpack-plugin");

const path = require("path");


module.exports = {

 plugins: [

   new HtmlWebpackPlugin({

     template: path.resolve(__dirname, "src", "index.html")

   })

 ]

};

这里的意思是让 webpack,从 src/index.html 加载 HTML 模板。


html-webpack-plugin的最终目标有两个:


加载 html 文件

它将bundle注入到同一个文件中

接着,我们需要在 src/index.html 中创建一个简单的 HTML 文件:


<!DOCTYPE html>

<html lang="en">

<head>

   <meta charset="UTF-8">

   <title>Webpack tutorial</title>

</head>

<body>


</body>

</html>

稍后,我们会运行这个程序。


webpack development server

在本文第一部分中,我们安装了webpack-dev-server。如果你忘记安装了,现在可以运行下面命令安装一下:


npm i webpack-dev-server --save-dev

webpack-dev-server 可以让开发更方便,不需要改动了文件就去手动刷新文件。 配置完成后,我们可以启动本地服务器来提供文件。


要配置webpack-dev-server,请打开package.json并添加一个 “start” 命令:


"scripts": {

 "dev": "webpack --mode development",

 "start": "webpack-dev-server --mode development --open",

},

有了 start 命令,我们来跑一下:


npm start

运行后,默认浏览器应打开。 在浏览器的控制台中,还应该看到一个 script 标签,引入的是我们的 main.js。


clipboard.png


使用 webpack loader

Loader是第三方扩展程序,可帮助webpack处理各种文件扩展名。 例如,有用于 CSS,图像或 txt 文件的加载程序。


下面是一些 loader 配置介绍:


module.exports = {

 module: {

   rules: [

     {

       test: /\.filename$/,

       use: ["loader-b", "loader-a"]

     }

   ]

 },

 //

};

相关配置以module 关键字开始。 在module内,我们在rules内配置每个加载程序组或单个加载程序。


对于我们想要作为模块处理的每个文件,我们用test和use配置一个对象


{

   test: /\.filename$/,

   use: ["loader-b", "loader-a"]

}

test 告诉 webpack “嘿,将此文件名视为一个模块”。 use 定义将哪些 loaders 应用于些打包的文件。


打包 CSS

要 在webpack 中打包CSS,我们需要至少安装两个 loader。Loader 对于帮助 webpack 了解如何处理.css文件是必不可少的。


要在 webpack 中测试 CSS,我们需要在 src 下创建一个style.css文件:


h1 {

   color: orange;

}

另外在 src/index.html 添加 h1 标签


<!DOCTYPE html>

<html lang="en">

<head>

   <meta charset="UTF-8">

   <title>Webpack tutorial</title>

</head>

<body>

<h1>Hello webpack!</h1>

</body>

</html>

最后,在src/index.js 中加载 CSS:


在测试之前,我们需要安装两个 loader:


css-loader: 解析 css 代码中的 url、@import语法像import和require一样去处理css里面引入的模块

style-loader:帮我们直接将css-loader解析后的内容挂载到html页面当中

安装 loader:


npm i css-loader style-loader --save-dev

然后在webpack.config.js中配置它们


const HtmlWebpackPlugin = require("html-webpack-plugin");

const path = require("path");


module.exports = {

 module: {

   rules: [

     {

       test: /\.css$/,

       use: ["style-loader", "css-loader"]

     }

   ]

 },

 plugins: [

   new HtmlWebpackPlugin({

     template: path.resolve(__dirname, "src", "index.html")

   })

 ]

};

现在,如果你运行npm start,会看到样式表加载在HTML的头部:


clipboard.png


一旦CSS Loader 就位,我们还可以使用MiniCssExtractPlugin提取CSS文件


Webpack Loader 顺序很重要!

在webpack中,Loader 在配置中出现的顺序非常重要。以下配置无效:


//


module.exports = {

 module: {

   rules: [

     {

       test: /\.css$/,

       use: ["css-loader", "style-loader"]

     }

   ]

 },

 //

};

此处,“style-loader”出现在 “css-loader” 之前。 但是style-loader用于在页面中注入样式,而不是用于加载实际的CSS文件。


相反,以下配置有效:


module.exports = {

 module: {

   rules: [

     {

       test: /\.css$/,

       use: ["style-loader", "css-loader"]

     }

   ]

 },

 //

};

webpack loaders 是从右到左执行的。


打包 sass

要在 webpack 中测试sass,同样,我们需要在 src 目录下创建一个 style.scss 文件:


@import url("https://fonts.googleapis.com/css?family=Karla:weight@400;700&display=swap");


$font: "Karla", sans-serif;

$primary-color: #3e6f9e;


body {

 font-family: $font;

 color: $primary-color;

}

另外,在src/index.html中添加一些 Dom 元素:


<!DOCTYPE html>

<html lang="en">

<head>

   <meta charset="UTF-8">

   <title>Webpack tutorial</title>

</head>

<body>

 <h1>Hello webpack!</h1>

 <p>Hello sass!</p>

</body>

</html>

最后,将 sass 文件加载到src/index.js中:


import "./style.scss";

console.log("Hello webpack!");

在测试之前,我们需要安装几个 loader:


sass-loader:加载 SASS / SCSS 文件并将其编译为 CSS

css-loader: 解析 css 代码中的 url、@import语法像import和require一样去处理css里面引入的模块

style-loader:帮我们直接将css-loader解析后的内容挂载到html页面当中

安装 loader:


npm i css-loader style-loader sass-loader sass --save-dev

然后在webpack.config.js中配置它们:


const HtmlWebpackPlugin = require("html-webpack-plugin");

const path = require("path");


module.exports = {

 module: {

   rules: [

     {

       test: /\.scss$/,

       use: ["style-loader", "css-loader", "sass-loader"]

     }

   ]

 },

 plugins: [

   new HtmlWebpackPlugin({

     template: path.resolve(__dirname, "src", "index.html")

   })

 ]

};

注意loader的出现顺序:首先是sass-loader,然后是css-loader,最后是style-loader。


现在,运行npm start,你应该会在HTML的头部看到加载的样式表:


clipboard.png


打包现代 JavaScrip

webpack 本身并不知道如何转换JavaScript代码。 该任务已外包给babel的第三方 loader,特别是babel-loader。


babel是一个JavaScript编译器和“编译器”。 babel 可以将现代JS(es6, es7...)转换为可以在(几乎)任何浏览器中运行的兼容代码。


同样,要使用它,我们需要安装一些 Loader:


babel-core :把 js 代码分析成 ast ,方便各个插件分析语法进行相应的处理

babel-preset-env:将现代 JS 编译为ES5

babel-loader :用于 webpack

引入依赖关系


npm i @babel/core babel-loader @babel/preset-env --save-dev

接着,创建一个新文件babel.config.json配置babel,内容如下:


{

 "presets": [

   "@babel/preset-env"

 ]

}

最后在配置一下 webpack :


const HtmlWebpackPlugin = require("html-webpack-plugin");

const path = require("path");


module.exports = {

 module: {

   rules: [

     {

       test: /\.scss$/,

       use: ["style-loader", "css-loader", "sass-loader"]

     },

     {

       test: /\.js$/,

       exclude: /node_modules/,

       use: ["babel-loader"]

     }

   ]

 },

 plugins: [

   new HtmlWebpackPlugin({

     template: path.resolve(__dirname, "src", "index.html")

   })

 ]

};

要测试转换,可以在 src/index.js中编写一些现代语法:


import "./style.scss";

console.log("Hello webpack!");


const fancyFunc = () => {

 return [1, 2];

};


const [a, b] = fancyFunc();

现在运行npm run dev来查看dist中转换后的代码。 打开 dist/main.js并搜索“fancyFunc”:


\n\nvar fancyFunc = function fancyFunc() {\n return [1, 2];\n};\n\nvar _fancyFunc = fancyFunc(),\n _fancyFunc2 = _slicedToArray(_fancyFunc, 2),\n a = _fancyFunc2[0],\n b = _fancyFunc2[1];\n\n//# sourceURL=webpack:///./src/index.js?"

没有babel,代码将不会被转译:


\n\nconsole.log(\"Hello webpack!\");\n\nconst fancyFunc = () => {\n return [1, 2];\n};\n\nconst [a, b] = fancyFunc();\n\n\n//# sourceURL=webpack:///./src/index.js?");

注意:即使没有babel,webpack也可以正常工作。 仅在执行 ES5 代码时才需要进行代码转换过程。


在 Webpack 中使用 JS 的模块

webpack 将整个文件视为模块。 但是,请不要忘记它的主要目的:加载ES模块。


ECMAScript模块(简称ES模块)是一种JavaScript代码重用的机制,于2015年推出,一经推出就受到前端开发者的喜爱。在2015之年,JavaScript 还没有一个代码重用的标准机制。多年来,人们对这方面的规范进行了很多尝试,导致现在有多种模块化的方式。


你可能听说过AMD模块,UMD,或CommonJS,这些没有孰优孰劣。最后,在ECMAScript 2015中,ES 模块出现了。


我们现在有了一个“正式的”模块系统。


要在 webpack 使用 ES module ,首先创建 src/common/usersAPI.js 文件:


const ENDPOINT = "https://jsonplaceholder.typicode.com/users/";


export function getUsers() {

 return fetch(ENDPOINT)

   .then(response => {

     if (!response.ok) throw Error(response.statusText);

     return response.json();

   })

   .then(json => json);

}

在 src/index.js中,引入上面的模块:


import { getUsers } from "./common/usersAPI";

import "./style.scss";

console.log("Hello webpack!");


getUsers().then(json => console.log(json));

生产方式

如前所述,webpack有两种操作模式:开发(development )和(production)。 到目前为止,我们仅在开发模式下工作。


在开发模式中,为了便于代码调试方便我们快速定位错误,不会压缩混淆源代码。相反,在生产模式下,webpac k进行了许多优化:


使用 TerserWebpackPlugin 进行缩小以减小 bundle 的大小

使用ModuleConcatenationPlugin提升作用域

在生产模式下配 置webpack,请打开 package.json 并添加一个“ build” 命令:


现在运行 npm run build,webpack 会生成一个压缩的包。


Code splitting

代码拆分(Code splitting)是指针对以下方面的优化技术:


避免出现一个很大的 bundle

避免重复的依赖关系

webpack 社区考虑到应用程序的初始 bundle 的最大大小有一个限制:200KB。


在 webpack 中有三种激活 code splitting 的主要方法:


有多个入口点

使用 optimization.splitChunks 选项

动态导入

第一种基于多个入口点的技术适用于较小的项目,但是从长远来看它是不可扩展的。这里我们只关注第二和第三种方式。


Code splitting 与 optimization.splitChunks

考虑一个使用Moment.js 的 JS 应用程序,Moment.js是流行的时间和日期JS库。


在项目文件夹中安装该库:


npm i moment

现在清除src/index.js的内容,并引入 moment 库:


import moment from "moment";

运行 npm run build 并查看控制的输出内容:


main.js 350 KiB 0 [emitted] [big] main

整个 moment 库都绑定到了 main.js 中这样是不好的。借助optimization.splitChunks,我们可以从主包中移出moment.js。


要使用它,需要在 webpack.config.js 添加 optimization 选项:


const HtmlWebpackPlugin = require("html-webpack-plugin");

const path = require("path");


module.exports = {

 module: {

 // ...

 },

 optimization: {

   splitChunks: { chunks: "all" }

 },

 // ...

};

运行npm run build 并查看运行结果:


       main.js   5.05 KiB       0  [emitted]         main

vendors~main.js    346 KiB       1  [emitted]  [big]  vendors~main

现在,我们有了一个带有moment.js 的vendors〜main.js,而主入口点的大小更合理。


注意:即使进行代码拆分,moment.js仍然是一个体积较大的库。 有更好的选择,如使用luxon或date-fns。


Code splitting 与 动态导入

Code splitting的一种更强大的技术使用动态导入来有条件地加载代码。 在ECMAScript 2020中提供此功能之前,webpack 提供了动态导入。


这种方法在 Vue 和 React 之类的现代前端库中得到了广泛使用(React有其自己的方式,但是概念是相同的)。


Code splitting 可用于:


模块级别

路由级别

例如,你可以有条件地加载一些 JavaScript 模块,以响应用户的交互(例如单击或鼠标移动)。 或者,可以在响应路由更改时加载代码的相关部分。


要使用动态导入,我们先清除src/index.html,并写入下面的内容:


<!DOCTYPE html>

<html lang="en">

<head>

   <meta charset="UTF-8">

   <title>Dynamic imports</title>

</head>

<body>

<button id="btn">Load!</button>

</body>

</html>

在 src/common/usersAPI.js中:


const ENDPOINT = "https://jsonplaceholder.typicode.com/users/";


export function getUsers() {

 return fetch(ENDPOINT)

   .then(response => {

     if (!response.ok) throw Error(response.statusText);

     return response.json();

   })

   .then(json => json);

}

在 src/index.js 中


const btn = document.getElementById("btn");


btn.addEventListener("click", () => {

 //

});

如果运行npm run start查看并单击界面中的按钮,什么也不会发生。


现在想象一下,我们想在某人单击按钮后加载用户列表。 “原生”的方法可以使用静态导入从src/common /usersAPI.js加载函数:


import { getUsers } from "./common/usersAPI";


const btn = document.getElementById("btn");


btn.addEventListener("click", () => {

 getUsers().then(json => console.log(json));

});

问题在于ES模块是静态的,这意味着我们无法在运行时更改导入的内容。


通过动态导入,我们可以选择何时加载代码


const getUserModule = () => import("./common/usersAPI");


const btn = document.getElementById("btn");


btn.addEventListener("click", () => {

 getUserModule().then(({ getUsers }) => {

   getUsers().then(json => console.log(json));

 });

});

这里我们创建一个函数来动态加载模块


const getUserModule = () => import("./common/usersAPI");

现在,当你第一次使用npm run start加载页面时,会看到控制台中已加载 js 包:


clipboard.png


现在,仅在单击按钮时才加载/common/usersAPI:


clipboard.png


对应的 chunk 是 0.js


通过在导入路径前面加上魔法注释/ * webpackChunkName:“ name_here” * /,可以更改块名称:


const getUserModule = () =>

 import(/* webpackChunkName: "usersAPI" */ "./common/usersAPI");


const btn = document.getElementById("btn");


btn.addEventListener("click", () => {

 getUserModule().then(({ getUsers }) => {

   getUsers().then(json => console.log(json));

 });

});

蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务

iview按需引入,ie11不兼容,报无效字符问题解决

seo达人

准备工作

//借助插件

npm install babel-plugin-import --save-dev


// .babelrc

{

 "plugins": [["import", {

   "libraryName": "view-design",

   "libraryDirectory": "src/components"

 }]]

}

在main.js中引入

import "view-design/dist/styles/iview.css";

import { Button, Table } from "view-design";

const viewDesign = {

Button: Button,

Table: Table

};

Object.keys(viewDesign).forEach(element => {

Vue.component(element, viewDesign[element]);

});

先用google浏览器打开正常,以上操作猛如虎,IE浏览器打开250,好了不废话,下面是解决方案


解决方案

//vue.config.js中配置

chainWebpack: config => {

   //解决iview 按需引入babel转换问题

  config.module

     .rule("view-design")  //  我目前用的是新版本的iview ,旧版本的iview,用iview代替view-design

     .test(/view-design.src.*?js$/)

     .use("babel")

     .loader("babel-loader")

     .end();

}

问题原因

为什么会有如上问题呢? 这个就和babel转换问题有关了,按需引入时,那些组件里js文件未进行babel转换或转换不彻底就被引入了,ie11对es6+的语法支持是很差的,所以以上方法就是让引入文件前就对view-design的src下的所有js文件进行babel转换,举一反三,当按需引入第三方框架时出现这个问题,都可用这方法解决了,只要把规则和正则中view-design进行替换。


延伸扩展

//全局引入

import ViewUI from "view-design";

Vue.use(ViewUI);

import "view-design/dist/styles/iview.css";

tips:在全局引入时,一定要记住不要在.babelrc文件里配置按需导入,会导致冲突

蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务

ifram内嵌套tab选项卡以及iframe切换的一系列问题

前端达人

最近在项目中遇到这样一个问题

点击查看原图

当页面加载完毕后由于选项卡的另外两张属于display:none;状态  所以另外两张选项卡内echarts的宽高都会变成默认100*100


查阅了很多网上的案例,得出一下一些解决方案:

1:

原因很简单,在tab页中,图表的父容器div是隐藏的(display:none),图表在执行js初始化的时候找不到这个元素,所以自动将“100%”转成了“100”,最后计算出来的图表就成了100px

解决办法:

找一个在tab页的切换操作中不会隐藏的父容器,把它的宽度的具体值取出后在初始化图表之前直接赋给图表


1 $("#chartMain").css('width',$("#TabContent").width());//获取父容器的宽度具体数值直接赋值给图表以达到宽度100%的效果 2 var Chart = echarts.init(document.getElementById('chartMain')); 3 4 // 指定图表的配置项和数据 5 option = { ...配置项和数据 }; 6 7 // 使用刚指定的配置项和数据显示图表。 8 Chart.setOption(option);

2:mychart.resize() 重新渲染高度

3: 后来我想到了问题所在,既然高度是因为display:none;导致的 那大可不必设置这个属性,但是在页面渲染完毕后加上即可

所以取消了选项卡的display:none; 但在页面加载完毕后 
window.οnlοad=function(){

根基id在添加css display:none;

}

即可解决,

分割线

--------------------------------------------------------------------- 

接下来解决一下ifram内外通讯 互相通讯赋值ifram src 和高度问题 

如何用 JavaScript 来解析 URL

seo达人

统一资源定位符,缩写为URL,是对网络资源(网页、图像、文件)的引用。URL指定资源位置和检索资源的机制(http、ftp、mailto)。


举个例子,这里是这篇文章的 URL 地址:


https://dmitripavlutin.com/parse-url-javascript

很多时候你需要获取到一段 URL 的某个组成部分。它们可能是 hostname(例如 dmitripavlutin.com),或者 pathname(例如 /parse-url-javascript)。


一个方便的用于获取 URL 组成部分的办法是通过 URL() 构造函数。


在这篇文章中,我将给大家展示一段 URL 的结构,以及它的主要组成部分。


接着,我会告诉你如何使用 URL() 构造函数来轻松获取 URL 的组成部分,比如 hostname,pathname,query 或者 hash。


1. URL 结构

一图胜千言。不需要过多的文字描述,通过下面的图片你就可以理解一段 URL 的各个组成部分:


image


2. URL() 构造函数

URL() 构造函数允许我们用它来解析一段 URL:


const url = new URL(relativeOrAbsolute [, absoluteBase]);

参数 relativeOrAbsolute 既可以是绝对路径,也可以是相对路径。如果第一个参数是相对路径的话,那么第二个参数 absoluteBase 则必传,且必须为第一个参数的绝对路径。


举个例子,让我们用一个绝对路径的 URL 来初始化 URL() 函数:


const url = new URL('http://example.com/path/index.html');


url.href; // => 'http://example.com/path/index.html'

或者我们可以使用相对路径和绝对路径:


const url = new URL('/path/index.html', 'http://example.com');


url.href; // => 'http://example.com/path/index.html'

URL() 实例中的 href 属性返回了完整的 URL 字符串。


在新建了 URL() 的实例以后,你可以用它来访问前文图片中的任意 URL 组成部分。作为参考,下面是 URL() 实例的接口列表:


interface URL {

 href:     USVString;

 protocol: USVString;

 username: USVString;

 password: USVString;

 host:     USVString;

 hostname: USVString;

 port:     USVString;

 pathname: USVString;

 search:   USVString;

 hash:     USVString;


 readonly origin: USVString;

 readonly searchParams: URLSearchParams;


 toJSON(): USVString;

}

上述的 USVString 参数在 JavaScript 中会映射成字符串。


3. Query 字符串

url.search 可以获取到 URL 当中 ? 后面的 query 字符串:


const url = new URL(

 'http://example.com/path/index.html?message=hello&who=world'

);


url.search; // => '?message=hello&who=world'

如果 query 参数不存在,url.search 默认会返回一个空字符串 '':


const url1 = new URL('http://example.com/path/index.html');

const url2 = new URL('http://example.com/path/index.html?');


url1.search; // => ''

url2.search; // => ''

3.1 解析 query 字符串

相比于获得原生的 query 字符串,更实用的场景是获取到具体的 query 参数。


获取具体 query 参数的一个简单的方法是利用 url.searchParams 属性。这个属性是 URLSearchParams 的实例。


URLSearchParams 对象提供了许多用于获取 query 参数的方法,如get(param),has(param)等。


下面来看个例子:


const url = new URL(

 'http://example.com/path/index.html?message=hello&who=world'

);


url.searchParams.get('message'); // => 'hello'

url.searchParams.get('missing'); // => null

url.searchParams.get('message') 返回了 message 这个 query 参数的值——hello。


如果使用 url.searchParams.get('missing') 来获取一个不存在的参数,则得到一个 null。


4. hostname

url.hostname 属性返回一段 URL 的 hostname 部分:


const url = new URL('http://example.com/path/index.html');


url.hostname; // => 'example.com'

5. pathname

url. pathname 属性返回一段 URL 的 pathname 部分:


const url = new URL('http://example.com/path/index.html?param=value');


url.pathname; // => '/path/index.html'

如果这段 URL 不含 path,则该属性返回一个斜杠 /:


const url = new URL('http://example.com/');


url.pathname; // => '/'

6. hash

最后,我们可以通过 url.hash 属性来获取 URL 中的 hash 值:


const url = new URL('http://example.com/path/index.html#bottom');


url.hash; // => '#bottom'

当 URL 中的 hash 不存在时,url.hash 属性会返回一个空字符串 '':


const url = new URL('http://example.com/path/index.html');


url.hash; // => ''

7. URL 校验

当使用 new URL() 构造函数来新建实例的时候,作为一种副作用,它同时也会对 URL 进行校验。如果 URL 不合法,则会抛出一个 TypeError。


举个例子,http ://example.com 是一段非法 URL,因为它在 http 后面多写了一个空格。


让我们用这个非法 URL 来初始化 URL() 构造函数:


try {

 const url = new URL('http ://example.com');

} catch (error) {

 error; // => TypeError, "Failed to construct URL: Invalid URL"

}

因为 http ://example.com 是一段非法 URL,跟我们想的一样,new URL() 抛出了一个 TypeError。


8. 修改 URL

除了获取 URL 的组成部分以外,像 search,hostname,pathname 和 hash 这些属性都是可写的——这也意味着你可以修改 URL。


举个例子,让我们把一段 URL 从 red.com 修改成 blue.io:


const url = new URL('http://red.com/path/index.html');


url.href; // => 'http://red.com/path/index.html'


url.hostname = 'blue.io';


url.href; // => 'http://blue.io/path/index.html'

注意,在 URL() 实例中只有 origin 和 searchParams 属性是只读的,其他所有的属性都是可写的,并且会修改原来的 URL。


9. 总结

URL() 构造函数是 JavaScript 中的一个能够很方便地用于解析(或者校验)URL 的工具。


new URL(relativeOrAbsolute [, absoluteBase]) 中的第一个参数接收 URL 的绝对路径或者相对路径。当第一个参数是相对路径时,第二个参数必传且必须为第一个参数的基路径。


在新建 URL() 的实例以后,你就能很轻易地获得 URL 当中的大部分组成部分了,比如:


url.search 获取原生的 query 字符串

url.searchParams 通过 URLSearchParams 的实例去获取具体的 query 参数

url.hostname获取 hostname

url.pathname 获取 pathname

url.hash 获取 hash 值

那么你最爱用的解析 URL 的 JavaScript 工具又是什么呢?

蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务

TypeScript 运行时类型检查指南

seo达人

为什么需要额外的类型检查?

TypeScript 只在编译期执行静态类型检查!实际运行的是从 TypeScript 编译的 JavaScript,这些生成的 JavaScript 对类型一无所知。编译期静态类型检查在代码库内部能发挥很大作用,但对不合规范的输入(比如,从 API 处接收的输入)无能为力。


运行时检查的严格性

至少需要和编译期检查一样严格,否则就失去了编译期检查提供的保证。

如有必要,可以比编译期检查更严格,例如,年龄需要大于等于 0。

运行时类型检查策略

定制代码手动检查

灵活

可能比较枯燥,容易出错

容易和实际代码脱节

使用校验库手动检查

比如使用 joi:


import Joi from "@hapi/joi"const schema = Joi.object({    firstName: Joi.string().required(),    lastName: Joi.string().required(),    age: Joi.number().integer().min(0).required()});

灵活

容易编写

容易和实际代码脱节

手动创建 JSON Schema

例如:


{  "$schema": "http://json-schema.org/draft-07/schema#",  "required": [    "firstName",    "lastName",    "age"  ],  "properties": {    "firstName": {      "type": "string"    },    "lastName": {      "type": "string"    },    "age": {      "type": "integer",      "minimum": 0    }  }}

使用标准格式,有大量库可以校验。

JSON 很容易存储和复用。

可能会很冗长,手写 JSON Schema 可能会很枯燥。

需要确保 Schema 和代码同步更新。

自动创建 JSON Schema

基于 TypeScript 代码生成 JSON Schema

-- 比如 typescript-json-schema 这个工具就可以做到这一点(同时支持作为命令行工具使用和通过代码调用)。

-- 需要确保 Schema 和代码同步更新。

基于 JSON 输入示例生成

-- 没有使用已经在 TypeScript 代码中定义的类型信息。

-- 如果提供的 JSON 输入示例和实际输入不一致,可能导致错误。

-- 仍然需要确保 Schema 和代码同步更新。

转译

例如使用 ts-runtime。


这种方式会将代码转译成功能上等价但内置运行时类型检查的代码。


比如,下面的代码:


interface Person {    firstName: string;    lastName: string;    age: number;}const test: Person = {    firstName: "Foo",    lastName: "Bar",    age: 55}

会被转译为:


import t from "ts-runtime/lib";const Person = t.type(    "Person",    t.object(        t.property("firstName", t.string()),        t.property("lastName", t.string()),        t.property("age", t.number())    ));const test = t.ref(Person).assert({    firstName: "Foo",    lastName: "Bar",    age: 55});

这一方式的缺陷是无法控制在何处进行运行时检查(我们只需在输入输出的边界处进行运行时类型检查)。


顺便提一下,这是一个实验性的库,不建议在生产环境使用。


运行时类型派生静态类型

比如使用 io-ts 这个库。


这一方式下,我们定义运行时类型,TypeScript 会根据我们定义的运行时类型推断出静态类型。


运行时类型示例:


import t from "io-ts";const PersonType = t.type({  firstName: t.string,  lastName: t.string,  age: t.refinement(t.number, n => n >= 0, 'Positive')})

从中提取相应的静态类型:


interface Person extends t.TypeOf<typeof PersonType> {}

以上类型等价于:


interface Person {    firstName: string;    lastName: string;    age: number;}

类型总是同步的。

io-ts 很强大,比如支持递归类型。

需要将类型定义为 io-ts 运行时类型,这在定义类时不适用:

-- 有一种变通的办法是使用 io-ts 定义一个接口,然后让类实现这个接口。然而,这意味着每次给类增加属性的时候都要更新 io-ts 类型。

不容易复用接口(比如前后端之间使用同一接口),因为这些接口是 io-ts 类型而不是普通的 TypeScript 类型。

基于装饰器的类校验

比如使用 class-validator 这个库。


基于类属性的装饰器。

和 Java 的 JSR-380 Bean Validation 2.0 (比如 Hibernate Validator 就实现了这一标准)很像。

-- 此类 Java EE 风格的库还有 typeorm (ORM 库,类似 Java 的 JPA)和 routing-controllers (用于定义 API,类似 Java 的 JAX-RS)。

代码示例:


import { plainToClass } from "class-transformer";import {     validate, IsString, IsInt, Min } from "class-validator";class Person {    @IsString()    firstName: string;    @IsString()    lastName: string;    @IsInt()    @Min(0)    age: number;}const input: any = {    firstName: "Foo",    age: -1};const inputAsClassInstance = plainToClass(    Person, input as Person);validate(inputAsClassInstance).then(errors => {    // 错误处理代码});

类型总是同步的。

需要对类进行检查时很有用。

可以用来检查接口(定义一个实现接口的类)。

注意:class-validator 用于具体的类实例。在上面的代码中,我们使用它的姊妹库 class-transformer 将普通输入转换为 Person 实例。转换过程本身不进行任何类型检查。

蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务

日历

链接

个人资料

蓝蓝设计的小编 http://www.lanlanwork.com

存档