Commit b7ae4581 authored by 杨靖's avatar 杨靖 😂
parents d7b8a735 0ed38f3e
Pipeline #16630 failed with stage
---
title: CSS隐藏元素及去除图片底部空白间隙的各种方式总结
date: 2022/8/30 22:42:20
tags:
- 前端
- CSS
author: 李燕
---
**一、CSS元素隐藏方式**
在某些场景下,让指定元素隐藏,是我们开发中常见的一个需求,其实现也很简单,光CSS方式就有好几种,可以简单总结如下:
<!-- more -->
1.设置元素的display为none
```
.hide {
 display:none;
}
```
将元素设置为display:none后,元素在页面上将彻底消失,元素本来占有的空间会被其他元素占有。
2.设置元素的visibility为hidden
```
.hide {
 visibility:hidden
}
```
和display:none的区别在于,元素在页面消失后,其占据的空间依旧会保留着。
3.将元素的透明度设置为0
```
.hide {
 opacity:0;
}
```
这种方法和visibility:hidden的一个共同点是元素隐藏后依旧占据着空间,而区别则在于元素只是隐身了没有消失,它依旧存在页面中。
4.设置height,width等盒模型属性为0
```
.hide{
 margin:0;
 border:0;
 padding:0;
 height:0;
 width:0;
 overflow:hidden;
}
```
5.设置元素的position与left、top、bottom、right等,将元素移出至屏幕外。
6.设置元素的position与z-index,将z-index设置成尽量小的负数
总结:隐藏元素最常用的方式为前2种,后面几种并不推荐,若有特殊场景需要则可适当选用。
**二、图片底部空白间隙去除方式**
在开发中经常会遇到这样一种情况:将一个图片放在一个盒子里,如果图片不做特殊处理,图片离盒子底部就会出现一段空白间隙,如下图:
![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a86a6dfc5e30449192553a4f0843fda5~tplv-k3u1fbpfcp-zoom-1.image)
这是因为,行内/行内块元素默认是与文字基线对齐的,而img是行内元素,它与文字基线对齐,但有的文字是有底线的(如下图中的"p"字母),基线与底线之间会有一段距离,所以造成了图片与盒子底部出现空白间隙。
![在这里插入图片描述](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7ca4bcb031ea4745854a49f033e5a0e1~tplv-k3u1fbpfcp-zoom-1.image)
#### 解决方法
1.修改img行内元素的垂直居中方式,让它不再以基线对齐。
```
img {
 vertical-align: top;
}
```
除了top值,还可以设置为middle或bottom 。
2.将img转为块元素,不在受行内基线的影响。
```
img {
 display: block;
}
```
3.修改img父级元素的行高或字体大小为0,这样基线下方的位置基本可以忽略。
```
div {
 font-size: 0px;
}
div {
 line-height: 0px;
}
```
---
title: JavaScript中Worker初探
date: 2022/8/30 17:20:00
author: 张顺
---
## 什么是Worker?他有什么作用?
因为js是单线程运行的,在遇到一些需要处理大量耗时的数据时,可能会阻塞页面的加载,造成面的假死。针对这个问题,H5出了新的功能worker,支持多线程,这时我们可以使用worker来开辟一个独立于主线程的子线程来进行一些耗时的大量运算。就可以用来解决因为大量耗时运算是造成页面假死的问题,详细内容查看[Worker](https://developer.mozilla.org/zh-CN/docs/Web/API/Worker)
### 如何使用Worker
```js
//首先创建 Worker 对象,第一个参数 worker 需要执行的 JS 文件的 URL。相对地址 URL 是相对于创建 Worker 的脚本所在文档(宿主文档)的访问地址。绝对地址 URL 必须要和宿主文档同源。
//第二个参数是可选参数,可以是一个对象,name属性,可以给当前Worker定义一个名称,可用来区分其他的worker
const worker = new Worker("worker.js",{name:'自定义的名称'})
//通过onmessage ,监听子线程的消息
worker.onmessage((e)=>`接收worker线程传递值:${e.data}`)
//通过postMessage ,向子线程发送消息
worker.postMessage('向子线程发送消息')
//worker.js 文件
//通过self.onmessage,来接收主线程传递的信息
self.onmessage((e)=>{
console.log(e)
})
//通过self.postMessage,来向主线程传递信息
self.postMessage('要传递的值')
```
### 方法和属性
主线程worker()对象,用来供主线程操作 Worker。worker 线程对象的属性和方法如下:
- worker.onerror:指定 error 事件的监听函数。
- worker.onmessage:指定 message 事件的监听函数,发送过来的数据在Event.data属性中。
- worker.onmessageerror:指定 messageerror 事件的监听函数。发送的数据无法序列化成字符串时,会触发这个事件。
- worker.postMessage():向 Worker 线程发送消息。
- worker.terminate():立即终止 Worker 线程。
worker 子线程全局属性和方法:
- self.onmessage:指定message事件的监听函数。
- self.onmessageerror:指定 messageerror 事件的监听函数。发送的数据无法序列化成字符串时,会触发这个事件。
- self.close():关闭 worker 线程。
- self.postMessage():向产生这个 worker 线程发送消息。
- self.importScripts():加载 JS 脚本。
- self.name: worker 的名字。该属性只读,由构造函数指定。
### worker 使用注意点
1. 同源限制
分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。
2. DOM 限制
worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用document、window、parent这些对象。但是,Worker 线程可以navigator对象和location对象。
3. 通信联系
worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息传递完成。
4. 脚本限制
worker 线程不能执行alert()方法和confirm()方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求。
5. 文件限制
worker 线程无法读取本地文件,即不能打开本机的文件系统(file://),它所加载的脚本,必须来自网络。
### 简单使用demo
模拟页面中插入耗时处理数据然后渲染元素,暂时不考虑页面渲染性能问题
```js
(() => {
let arrList = []  
let indexNum = 1000
for (let index = 0; index < indexNum; index++) {
let obj = {
name: index + 1
}
arrList.push(obj)
if (arrList.length == indexNum) {
//在子线程中,self代表worker子线程的全局对象,也可以用this代替self,或者省略也行
//this.postMessage(arrList);
//self.postMessage(arrList);
postMessage(arrList); //向主线程发送消息
}
onmessage = function(e) {
console.log(e.data)
console.log("我是worker线程接收到了你的信息,不客气,我就是做耗时操作的")
console.log("2妙后我还会给你发送个信息")
setTimeout(() => {
postMessage('如果还有耗时的操作,你还可以在开启一个worker线程呦')
}, 2000)
}
//worker自己关闭
// self.close()
}
})()
```
``` html
<!DOCTYPE html>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script>
<html>
<body>
<div id="bt">
<p style="color:red">模拟元素1</p>
<p style="color:red">模拟元素2</p>
<p style="color:red">模拟元素3</p>
<p style="color:red">模拟元素4</p>
<div class="ld" style="color:green">数据加载中。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。</div>
<div class="main"></div>
<p style="color:red">模拟元素5</p>
<p style="color:red">模拟元素6</p>
<p style="color:red">模拟元素7</p>
</div>
<script>
// 引入的js文件必须和当前页面同源,不可跨域
let w = new Worker("./worker2.js");
$(".ld").show()
// console.log("Worker线程处理完耗时操作后,主线程接会收到处理数据")
w.onmessage = function(e) {
let listData = e.data
console.log(listData)
let strs = ''
for (let j = 0; j < listData.length; j++) {
const element = listData[j];
strs += '<p>' + '这是接收到的数据' + j + '</p>'
}
$(".main").append(strs)
$(".ld").hide()
}
w.postMessage('你发送的数据我收到了,辛苦了')
//主线程关闭子线程
// w.terminate()
</script>
</body>
</html>
```
![](/yifu-study-front-share/images/wk/nostop.png)<br />![](/yifu-study-front-share/images/wk/wk_pic.png)
### SharedWorker一种特殊的worker,详细内容查看[SharedWorker](https://developer.mozilla.org/zh-CN/docs/Web/API/SharedWorker)
sharedWorker 顾名思义,是 worker 的一种,可以由所有同源的页面共享。同Worker的api一样,传入js的url,就可以注册一个 sharedWorker 实例,
sharedWorker通过port来发送和接收消息
```js
let myWorker = new SharedWorker('worker.js');
//主线程和共享线程都是通过 postMessage() 方法来发送消息
myWorker.port.postMessage('发送消息');
//接收消息都是使用onmessage = (e)=>{},或者 addEventListener('message', (e)=>{})
myWorker.port.onmessage = (e)=>{console.log(e.data)} ;
//关闭启动和worker是有点区别的
线程通过 worker.port.start() 启动
线程通过 worker.port.close() 关闭
SharedWorker使用注意要点同worKer一样
```
### 简单使用Demo
- shareWk.js 共享线程,里面存储计数器counter
- A.html 刷新页面,通知counter+1
- B.html 刷新页面,查看counter
``` shareWk.js
// 计时器
let counter = 0
// 监听连接
self.addEventListener('connect', (e) => {
const port = e.ports[0]
port.onmessage = (res) => {
console.log('A,B页面共享的信息:', res.data)
switch (res.data) {
case 'add':
counter++
break
}
console.log('counter:', counter)
port.postMessage(counter)
}
})
```
``` A.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>A页面</title>
</head>
<body>
<h1>A页面</h1>
<script>
// 兼容性判断
if (!SharedWorker) {
throw new Error('当前浏览器不支持SharedWorker')
}
// 创建共享线程
const worker = new SharedWorker('shareWk.js')
// 启动线程端口
worker.port.start()
// 向共享线程发送消息
worker.port.postMessage('add')
// 线程监听消息 2种方式都可以
// worker.port.addEventListener('message', (e) => {
// console.log('A页面共享线程counter值:', e.data)
// })
worker.port.onmessage = (e) => {
console.log('A页面取到counter值:', e.data)
}
console.log(worker.port)
</script>
</body>
</html>
```
``` B.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>B页面</title>
</head>
<body>
<h1>B页面</h1>
<script>
// 兼容性判断
if (!SharedWorker) {
throw new Error('当前浏览器不支持SharedWorker')
}
// 创建共享线程
const worker = new SharedWorker('shareWk.js')
// 启动线程端口
worker.port.start()
// 向共享线程发送消息
worker.port.postMessage('get counter')
// 线程监听消息
// 线程监听消息 2种方式都可以
// worker.port.addEventListener('message', (e) => {
// console.log('B页面共享线程counter值:', e.data)
// })
worker.port.onmessage = (e) => {
console.log('B页面取到counter值:', e.data)
}
</script>
</body>
</html>
```
![](/yifu-study-front-share/images/wk/shwk.png)
***SharedWorker调试注意点***
SharedWorker不能直接在页面调试面板查看,需要浏览器输入 chrome://inspect
然后点击inspect查看
![](/yifu-study-front-share/images/wk/point.png)
\ No newline at end of file
---
title: WebSocket
date: 2022/7/29 15:00:00
tags:
- WebSocket
author: 张芳利
---
<a name="d3db997e"></a>
## 一、什么是WebSocket
- WebSocket是一种通信协议,可在单个TCP连接上进行全双工通信。WebSocket使得客户端和服务器之间的数据交<br />换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就可以建立持久性的连接,并进行双向数据传输。
1. WebSocket是一种通信协议
2. 区别于HTTP协议,HTTP协议只能实现客户端请求,服务端响应的这种单项通<br />而WebSocket可以实现客户端与服务端的双向通讯,,最大也是最明显的区别就是可以做到服务端主动将消息推送给客户端。
<a name="554bf4d4"></a>
## 二、为什么需WebSocket?
- 因为 HTTP 协议有一个缺陷:通信只能由客户端发起。
> 由于http协议只能由客户端发起通信,如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用"轮询":每隔一段时候,就发出一个询问,了解服务器有没有新的信息。最典型的场景就是聊天室。Websocket在解决这样的需求上非常方便。<br />如果不使用WebSocket与服务器实时交互,一般有两种方法。AJAX轮询和Long Polling长轮询。
- 上边这两种方式都有个致命的弱点,开销太大,被动性。假设并发很高的话,这对服务端是个考验。<br />而WebSocket一次握手,持久连接,以及主动推送的特点可以解决上边的问题,又不至于损耗性能。
1. AJAX轮询
- AJAX轮询也就是定时发送请求,也就是普通的客户端与服务端通信过程,只不过是无限循环发送,这样,可以保证服务端一旦有最新消息,就可以被客户端获取。
2. Long Polling长轮询
- Long Polling长轮询是客户端和浏览器保持一个长连接,等服务端有消息返回,断开。<br />然后再重新连接,也是个循环的过程,无穷尽也。。。<br />客户端发起一个Long Polling,服务端如果没有数据要返回的话,<br />会hold住请求,等到有数据,就会返回给客户端。客户端又会再次发起一次Long Polling,再重复一次上面的过程。
<a name="bed9b70f"></a>
## 三、WebSocket特点
- 最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。
![websocket](/yifu-study-front-share/images/websocket/WebSocket1.png)
- 建立在 TCP 协议之上,服务器端的实现比较容易。
- 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
- 数据格式比较轻量,性能开销小,通信高效。
- 可以发送文本,也可以发送二进制数据。
- 没有同源限制,客户端可以与任意服务器通信。
- 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。
<a name="b15a1ba5"></a>
![websocket](/yifu-study-front-share/images/websocket/WebSocket2.png)
<a name="367b4b6d"></a>
## 四、WebSocket的API
1、WebSocket构造函数: 新建WebSocket实例后,会创建客户端与服务端的WebSocket连接
```jsx
let ws = new WebSocket(url)
```
2、 WebSocket实例的当前状态:
```jsx
ws.readyState
// 0: 'CONNECTING',表示正在连接
// 1: 'OPEN', 表示连接成功,可以通信了
// 2: 'CLOSING', 表示连接正在关闭
// 3: 'CLOSED', 表示连接已经关闭,或者打开连接失败。
```
3、WebSocket实例的事件:
```jsx
ws.onopen = function () {
// 指定连接成功后的回调函数
}
ws.onclose = function () {
// 指定连接关闭后的回调函数
}
ws.onmessage = function () {
// 指定收到服务器数据后的回调函数
}
ws.onerror = function () {
// 指定报错时的回调函数
}
```
4、WebSocket实例向后台推送消息的方法:
```jsx
ws.send(message) 向服务器发送数据
ws.close() 关闭当前连接
```
<a name="502a7fcf"></a>
## 五、WebSocket的解读
1、申请一个WebSocket对象,参数是需要连接的服务器端的地址,同http协议使用http://开头一样,WebSocket协议的URL使用ws://开头,另外安全的WebSocket协议使用wss://开头。
2、WebSocket对象一共支持四个消息 onopen, onmessage, onclose和onerror,<br />所有的操作都是采用消息的方式触发的,这样就不会阻塞UI,使得UI有更快的响应时间,得到更好的用户体验。<br />当Browser接收到WebSocketServer发送过来的数据时,就会触发onmessage消息,参数evt中包含server传输过来的数据;
```jsx
ws.onmessage = function(evt) {
};
```
3、WebSocket与TCP、HTTP的关系WebSocket与http协议一样都是基于TCP的,所以他们都是可靠的协议,Web开发者调用的WebSocket的send函数在browser的实现中最终都是通过TCP的系统接口进行传输的。
4、WebSocket和Http协议一样都属于应用层的协议,那么他们之间有没有什么关系呢?答案是肯定的,WebSocket在建立握手连接时,数据是通过http协议传输的,但是在建立连接之后,真正的数据传输阶段是不需要http协议参与的
![websocket](/yifu-study-front-share/images/websocket/WebSocket3.png)
<a name="14106cbc"></a>
## 六、WebSocket通讯详细解读
![websocket](/yifu-study-front-share/images/websocket/WebSocket4.png)
从图可以明显的看到,分三个阶段:
- 打开握手
- 数据传递
- 关闭握手
**reduce()** 方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。
reduce() 方法有两个参数,
- 第一个是回调函数,
- 第二个是初始值。
回调函数
回调函数在数组的每个元素上执行。回调函数的返回值是累加结果,并作为下一次调用回调函数的参数提供。回调函数带有四个参数。
accumulator 累计器 ——累加器累加回调函数的返回值。
currentValue 当前值——处理数组的当前元素。
currentIndex 当前索引 (可选)
array 数组 (可选)
初始值
如果指定了初始值,则将累加器设置为 initialValue 作为初始元素。否则,将累加器设置为数组的第一个元素作为初始元素。
arr.reduce(callback(accumulator, currentValue[,index[,array]])[, initialValue])
下面的代码片段中,第一个累加器(accumulator)被分配了初始值0。currentValue 是正在处理的 numbersArr 数组的元素。在这里,currentValue 被添加到累加器,在下次调用回调函数时,会将返回值作为参数提供。
```
const numbersArr = [67, 90, 100, 37, 60];
const total = numbersArr.reduce(function(accumulator, currentValue){
console.log("accumulator is " + accumulator + " current value is " + currentValue);
return accumulator + currentValue;
}, 0);
console.log("total : "+ total);
输出:
accumulator is 0 current value is 67
accumulator is 67 current value is 90
accumulator is 157 current value is 100
accumulator is 257 current value is 37
accumulator is 294 current value is 60
total : 354
```
**案例**
对数组的所有值求和
```
const studentResult = [67, 90, 100, 37, 60];
const total = studentResult.reduce((accumulator, currentValue) => accumulator +currentValue, 0);
console.log(total); // 354
```
代码中,studentResult 数组具有5个数字。使用 reduce() 方法,将数组减少为单个值,该值将 studentResult 数组的所有值和结果分配给 total。
对象数组中的数值之和
```
const studentResult = [
{ subject: '数学', marks: 78 },
{ subject: '物理', marks: 80 },
{ subject: '化学', marks: 93 }
];
const total = studentResult.reduce((accumulator, currentValue) => accumulator + currentValue.marks, 0);
console.log(total); // 251
```
我们从后端获取数据作为对象数组,因此,reduce() 方法有助于管理我们的前端逻辑。在下面的代码中,studentResult 对象数组有三个科目,这里,currentValue.marks 取了 studentResult 对象数组中每个科目的分数。
展平数组(去扁平化)
```
const twoDArr = [ [1,2], [3,4], [5,6], [7,8] , [9,10] ];
const oneDArr = twoDArr.reduce((accumulator, currentValue) => accumulator.concat(currentValue));
console.log(oneDArr);
// [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
```
“展平数组”是指将多维数组转换为一维。在下面的代码中,twoDArr 2维数组被转换为 oneDArr 一维数组。此处,第一个 [1,2] 数组分配给累加器 accumulator,然后 twoDArr 数组的其余每个元素都连接到累加器。
按属性分组对象
```
const result = [
{subject: '物理', marks: 41},
{subject: '化学', marks: 59},
{subject: '高等数学', marks: 36},
{subject: '应用数学', marks: 90},
{subject: '英语', marks: 64},
];
let initialValue = {
pass: [],
fail: []
}
const groupedResult = result.reduce((accumulator, current) => {
(current.marks >= 50) ? accumulator.pass.push(current) : accumulator.fail.push(current);
return accumulator;
}, initialValue);
console.log(groupedResult);
```
根据对象的属性,我们可以使用 reduce() 方法将对象数组分为几组。通过下面的代码片段,你可以清楚地理解这个概念。这里,result 对象数组有五个对象,每个对象都有 subject 和 marks 属性。如果分数大于或等于50,则该主题通过,否则,主题失败。reduce() 用于将结果分组为通过和失败。首先,将 initialValue 分配给累加器,然后 push() 方法在检查条件之后将当前对象添加到 pass 和 fail 属性中作为对象数组。
输出:
```
{
pass: [
{ subject: ‘化学’, marks: 59 },
{ subject: ‘应用数学’, marks: 90 },
{ subject: ‘英语’, marks: 64 }
],
fail: [
{ subject: ‘物理’, marks: 41 },
{ subject: ‘高等数学’, marks: 36 }
]
}
```
删除数组中的重复项
```
const duplicatedsArr = [1, 5, 6, 5, 7, 1, 6, 8, 9, 7];
const removeDuplicatedArr = duplicatedsArr.reduce((accumulator, currentValue) => {
if(!accumulator.includes(currentValue)){
accumulator.push(currentValue);
}
return accumulator;
}, []);
console.log(removeDuplicatedArr);
// [ 1, 5, 6, 7, 8, 9 ]
```
在下面的代码片段中,删除了 plicatedArr 数组中的重复项。首先,将一个空数组分配给累加器作为初始值。accumulator.includes() 检查 duplicatedArr 数组的每个元素是否已经在累加器中可用。如果 currentValue 在累加器中不可用,则使用 push() 将其添加。
\ No newline at end of file
---
title: 前端图片压缩
date: 2022/8/31 22:00:00
tags:
- 前端
- JavaScript
- 压缩
author: 王许晓
---
> 使用VUE或者React框架加载HTML大图时极容易导致前端页面卡死报out of memory错误,报出这个错误还好,如果没有报错浏览器可能就无法操作了。
<!-- more -->
# 前端压缩
为保前端页面正常渲染html大图,需要在html加载前压缩图片。前端解决这个问题有两种方案,一个是上传前压缩,一个是上传后压缩。上传前可以保证当前图片无论在哪里显示大小已经被压缩过,无需再去管理。上传后压缩可以保证服务器端拥有高清原图,显示前需要做一次压缩。
无论那种压缩,压缩方式基本一致
1. 获取图片尺寸
2. 计算压缩比率
3. 通过canvas.toDataUrl 参数转化压缩图片
4. 将base64字符转转成文件或者直接显示
## 上传前压缩
获取到本地文件后,通过FileReader将本地文件写入到内存中
```
// 获取文件图片的宽高
function getImageWHFromFile(file, fn, err) {
try {
// 读取上传之后的文件对象
const imageReader = new FileReader()
// 将图片的内存url地址添加到FileReader中
imageReader.readAsDataURL(file)
// 当图片完全加载到FileReader对象中后,触发下面的事件
imageReader.addEventListener('loadend', function(e) {
// 获取上传到本机内存中的图片的url地址
const imageSrc = e.target.result
// 调用计算图片大小的方法
calculateImageSize(imageSrc).then(function({ image, width, height }) {
// 得到图片的宽和高
fn(image, width, height)
}).catch(e => {
err()
})
})
// 可以做一些其他状态监听
} catch (e) {
err(file)
}
}
```
计算内存中图片的宽高
```
// 根据图片地址获取图片的宽和高
function calculateImageSize(src) {
return new Promise(function(resolve, reject) {
const image = new Image()
image.addEventListener('load', function(e) {
resolve({
image: image,
width: e.target.width,
height: e.target.height,
})
})
image.addEventListener('error', function(e) {
reject(Error('获取尺寸失败'))
})
// 将图片的url地址添加到图片地址中
image.setAttribute('crossOrigin', 'Anonymous')
image.src = src
})
}
```
得到图片宽高后就可以计算压缩比率,通过图片真水的宽高和期望的宽高计算压缩率,基本上可以得到合适的图片尺寸
```
// 计算压缩比率
function getRatio(oriW, oriH, width, height) {
const proW = (oriW / width).toFixed(5)
const proH = (oriH / height).toFixed(5)
return proW < proH ? proW : proH
}
```
得到压缩比率就可以执行压缩,压缩使用canvas实现
```
// 缩放img对象中的图片
function compressImageToBase64(image, quality) {
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
const imageWidth = image.width * quality
const imageHeight = image.height * quality
canvas.width = imageWidth
canvas.height = imageHeight
context.drawImage(image, 0, 0, imageWidth, imageHeight)
return canvas.toDataURL('image/jpeg', quality)
}
```
压缩完得到图片的base64(DataURL)数据,可以用这个数据直接输出到img标签显示,或者继续加工生成图片文件
将base64转成blob对象
```
// DataURL转Blob对象
function dataURLToBlob(dataurl) {
const arr = dataurl.split(',')
const mime = arr[0].match(/:(.*?);/)[1]
const bstr = atob(arr[1])
let n = bstr.length
const u8arr = new Uint8Array(n)
while (n--) {
u8arr[n] = bstr.charCodeAt(n)
}
return new Blob([u8arr], { type: mime })
}
```
将Blob转成File,调用接口用FormData的形式上传文件
```
// 对图片进行压缩并返回文件格式
function compressToFile(image, quality) {
const blob = dataURLToBlob(compressImageToBase64(image, quality))
return new File([blob], new Date().getTime() + '.jpg')
}
```
到此,就得到了一个压缩后的文件对象
## 上传后压缩
上传后压缩的意思是,服务器返回一个图片url后,如果这个url指向的图片尺寸很大,比如:10M,这时候在浏览器中显示这个图片会导致页面卡死。我的解决方案是压缩后再显示,压缩方式大概与上传前压缩类似,从calculateImageSize方法开始计算图片尺寸,然后进行压缩,得到base64数据后回调显示这个图片。这里存在一个问题:压缩开始后chrome的控制台无法打开。原因则是image加载图片后会在控制台的Network中输出base64格式的日志,这个日志占用了太大的内存。
# 服务器压缩
服务器压缩是最好的且能满足产品对大图要求的解决方案。前端上传原图,服务器压缩后生成两个地址,一个缩略图一个原图,缩略图供前端显示,原图供下载。
> 附全部代码供参考
```
/**
* @param {待压缩图片文件} file
* @param {异步回调} fn
* @param {压缩期望尺寸} size [width ,height]
*/
const fileCompressToFile = function(file, fn, size) {
const _size = getComperssSize(size)
if (_size === null) {
fn(file)
return
}
// 开启后执行压缩
getImageWHFromFile(file, (image, width, height) => {
const pro = getRatio(_size[0], _size[1], width, height)
if (pro >= 1) {
fn(file)
return
}
// 开始压缩
fn(compressToFile(image, pro))
}, () => {
fn(file)
})
}
// 通过url压缩图片,压缩成功返回base64
const urlCompressToBase64 = function(url, fn, size) {
const _size = getComperssSize(size)
if (_size === null) {
fn(url)
return
}
calculateImageSize(url).then(function({ image, width, height }) {
const pro = getRatio(_size[0], _size[1], width, height)
if (pro >= 1) {
fn(url)
return
}
// 开始压缩
fn(compressImageToBase64(image, pro))
}).catch(e => {
console.error(e)
fn(url)
})
}
// 格式化压缩尺寸
function getComperssSize(size) {
if (!size) {
return null
}
const defaultSize = 500
if (Array.isArray(size)) {
return [size[0] || defaultSize, size[1] || defaultSize]
}
const pf = parseFloat(size)
return [pf || defaultSize, pf || defaultSize]
}
// 计算压缩比率
function getRatio(oriW, oriH, width, height) {
const proW = (oriW / width).toFixed(5)
const proH = (oriH / height).toFixed(5)
return proW < proH ? proW : proH
}
// 缩放img对象中的图片
function compressImageToBase64(image, quality) {
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
const imageWidth = image.width * quality
const imageHeight = image.height * quality
canvas.width = imageWidth
canvas.height = imageHeight
context.drawImage(image, 0, 0, imageWidth, imageHeight)
return canvas.toDataURL('image/jpeg', quality)
}
// 对图片进行压缩并返回文件格式
function compressToFile(image, quality) {
const blob = dataURLToBlob(compressImageToBase64(image, quality))
return new File([blob], new Date().getTime() + '.jpg')
}
// DataURL转Blob对象
function dataURLToBlob(dataurl) {
const arr = dataurl.split(',')
const mime = arr[0].match(/:(.*?);/)[1]
const bstr = atob(arr[1])
let n = bstr.length
const u8arr = new Uint8Array(n)
while (n--) {
u8arr[n] = bstr.charCodeAt(n)
}
return new Blob([u8arr], { type: mime })
}
// 获取文件图片的宽高
function getImageWHFromFile(file, fn, err) {
try {
// 读取上传之后的文件对象
const imageReader = new FileReader()
// 将图片的内存url地址添加到FileReader中
imageReader.readAsDataURL(file)
// 当图片完全加载到FileReader对象中后,触发下面的事件
imageReader.addEventListener('loadend', function(e) {
// 获取上传到本机内存中的图片的url地址
const imageSrc = e.target.result
// 调用计算图片大小的方法
calculateImageSize(imageSrc).then(function({ image, width, height }) {
// 得到图片的宽和高
fn(image, width, height)
}).catch(e => {
err()
})
})
// 可以做一些其他状态监听
} catch (e) {
err(file)
}
}
// 根据图片地址获取图片的宽和高
function calculateImageSize(src) {
return new Promise(function(resolve, reject) {
const image = new Image()
image.addEventListener('load', function(e) {
resolve({
image: image,
width: e.target.width,
height: e.target.height,
})
})
image.addEventListener('error', function(e) {
reject(Error('获取尺寸失败'))
})
// 将图片的url地址添加到图片地址中
image.setAttribute('crossOrigin', 'Anonymous')
image.src = src
})
}
export {
fileCompressToFile,
urlCompressToBase64,
}
```
\ No newline at end of file
---
title: 浅谈跨域
date: 2022/8/30 10:54:30
tags:
- JSONP
- postMessage
- WebSocket
author: 张芳利
---
## 一、什么是跨域
要知道什么是跨域,需要先知道另一个名词【**同源政策**】。
同源政策:是一个重要的安全政策,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。
而什么才是同源呢?同源就是两个页面具有相同的协议,域名和端口 均一致。所以根据上面的了解,**源=协议+主机/域名+端口,当源不同的时候也就是跨域了**
例如:
![crossDomain](/yifu-study-front-share/images/crossDomain/img1.png)
| **当前页面url** | **被请求页面url** | **是否跨域** | **原因** |
| --- | --- | --- | --- |
| http://www.test.com/ | http://www.test.com/index.html | 否 | 同源 |
| http://www.test.com/ | https://www.test.com/index.html | 是 | 协议不同(http/https) |
| http://www.test.com/ | http://www.baidu.com/ | 是 | 主域名不同(test/baidu) |
| http://www.test.com/ | http://blog.test.com/ | 是 | 子域名不同(www/blog) |
| http://www.test.com:8080 | http://www.test.com:7001/ | 是 | 端口号不同(8080/7001) |
**注意:**
> 在默认情况下 http 可以省略端口 80, https 省略 443。
## 二、跨域的限制
- Cookie,LocalStorage,IndexDB 等存储性内容无法读取
- DOM 节点无法访问
- Ajax 请求发出去了,但是响应被浏览器拦截了
基本我们说的跨域是在第三点请求上面。
ajax请求报错:
```jsx
Access to XMLHttpRequest at 'xxx' from origin 'xxx' has been block by CORS,
policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
```
## 三、解决跨域
### 方法一:JSONP
JSONP 是 JSON with Padding 的缩写,主要就是利用了 script 标签没有跨域限制的这个特性来完成的。
思路:网页通过添加一个`<script>`元素,向服务器请求 JSON 数据,服务器收到请求后,将数据放在一个指定名字的回调函数的参数位置传回来。
缺点:只支持get请求,不支持post请求。
(下面的接口都是聚合数据里面的免费接口,如果想实验测试一下可以自己更换一下接口地址哈~)
**1、原生js**
```html
<script src="http://test.com/data.php?callback=dosomething"></script>
// 向服务器test.com发出请求,该请求的查询字符串有一个callback参数,用来指定回调函数的名字
// 处理服务器返回回调函数的数据
<script type="text/javascript">
function dosomething(res){
// 处理获得的数据
console.log(res.data)
}
</script>
//或
<script type="text/javascript">
window.jsonpCallback = function(res) {
console.log(res);
};
script>
<script
src="http://localhost:8080/api/jsonp?msg=hello&cb=jsonpCallback"
type="text/javascript"
>script>
```
**2、vue.js **
2.1 安装jsonp,利用jsonp插件
```jsx
yarn add jsonp
import jsonp from "jsonp";
// 这里的url 是聚合免费api
export const getTest = () => {
jsonp('http://v.juhe.cn/joke/content/list.php?key=79255ba6ab74a79d35edb174142c309c&page=2&pagesize=10&sort=asc&time=1418745237',{},(err,data)=>{
console.log(err)
console.log(data)
})
}
```
![crossDomain](/yifu-study-front-share/images/crossDomain/img2.png)
2.2 自己封装一下jsonp 函数方法
```jsx
const jsonpFun = ({ url, params })=>{
return new Promise((resolve, reject) => {
// 创建一个临时的 script 标签用于发起请求
const script = document.createElement('script');
// 将回调函数临时绑定到 window 对象,回调函数执行完成后,移除 script 标签
window['jsonCallBack'] =(result) => {
resolve(result)
document.body.removeChild(script);
}
// 构造 GET 请求参数,
const formatParams = { ...params, callback: 'jsonCallBack' };
const requestParams = Object.keys(formatParams)
.reduce((acc, cur) => {
return acc.concat([`${cur}=${formatParams[cur]}`]);
}, [])
.join('&');
// 构造 GET 请求的 url 地址
const src = `${url}?${requestParams}`;
script.setAttribute('src', src);
script.setAttribute('type','text/javascript');
document.body.appendChild(script);
});
}
// 使用
export const getTest = () => {
jsonpFun({
url:'http://v.juhe.cn/joke/content/list.php',
params:{
key:'79255ba6ab74a79d35edb174142c309c',
page:2,
pagesize:10,
sort:'asc',
time:1418745237
}
}).then(res=>{
console.log(res);
})
}
```
2.3 给axios 上面拓展一个jsonp
```jsx
// 给axios添加jsonp请求
axios.jsonp = (url) => {
if(!url){
console.error('Axios.JSONP 至少需要一个url参数!')
return;
}
return new Promise((resolve,reject) => {
window.jsonCallBack =(result) => {
resolve(result)
}
var JSONP=document.createElement("script");
JSONP.type="text/javascript";
JSONP.src=`${url}&callback=jsonCallBack`;
document.getElementsByTagName("head")[0].appendChild(JSONP);
setTimeout(() => {
document.getElementsByTagName("head")[0].removeChild(JSONP)
},500)
})
}
// 使用
export const getTest = () => {
axios.jsonp('http://v.juhe.cn/joke/content/list.php?key=79255ba6ab74a79d35edb174142c309c&page=2&pagesize=10&sort=asc&time=1418745237').then(res=>{
console.log(res);
})
}
```
### 方法二:CORS
CORS是基于http1.1的一种跨域解决方案,它的全称是**C**ross-**O**rigin **R**esource **S**haring,跨域资源共享。
**思路:如果浏览器要跨域访问服务器的资源,需要获得服务器的允许**
缺点:当你使用 IE<=9, Opera<12, or Firefox<3.5 或者更加老的浏览器,就不适合
在 cors 中会有 简单请求 和 复杂请求的概念。简单请求:当请求**同时满足**以下条件时,浏览器会认为它是一个简单请求:
情况一: 使用以下方法(意思就是以下请求意外的都是非简单请求)
- GET
- HEAD
- POST
情况二: 人为设置以下集合外的请求头
- Accept
- Accept-Language
- Content-Language
- Content-Type (需要注意额外的限制)
- DPR
- Downlink
- Save-Data
- Viewport-Width
- Width
情况三:Content-Type的值仅限于下列三者之一:(例如 application/json 为非简单请求)
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
情况四:
请求中的任意XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。
情况五:
请求中没有使用 ReadableStream 对象。
```jsx
// 简单请求
fetch('http://crossdomain.com/api/news');
// 请求方法不满足要求,不是简单请求
fetch('http://crossdomain.com/api/news', {
method: 'PUT',
});
// 加入了额外的请求头,不是简单请求
fetch('http://crossdomain.com/api/news', {
headers: {
a: 1,
},
});
// 简单请求
fetch('http://crossdomain.com/api/news', {
method: 'post',
});
// content-type不满足要求,不是简单请求
fetch('http://crossdomain.com/api/news', {
method: 'post',
headers: {
'content-type': 'application/json',
},
});
```
不满足上面的就是复杂请求。一般CORS的设置都是在后端的。
**1、**普通跨域请求:只需服务器端设置Access-Control-Allow-Origin
2、带cookie跨域请求:前后端都需要进行设置
**【前端设置】根据xhr.withCredentials字段判断是否带有cookie**
```jsx
// 原生ajax
var xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容
// 前端设置是否带cookie
xhr.withCredentials = true;
xhr.open('post', 'http://www.domain2.com:8080/login', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('user=admin');
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
alert(xhr.responseText);
}
};
//jQuery ajax
$.ajax({
url: 'http://www.test.com:8080/login',
type: 'get',
data: {},
xhrFields: {
withCredentials: true // 前端设置是否带cookie
},
crossDomain: true, // 会让请求头中包含跨域的额外信息,但不会含cookie
});
// vue-resource
Vue.http.options.credentials = true
// axios
axios.defaults.withCredentials = true
```
### 方法三:代理
**思路:利用服务端请求不会跨域的特性,让接口和当前站点同域。**
代理适用的场景是:生产环境不发生跨域,但开发环境发生跨域。
![crossDomain](/yifu-study-front-share/images/crossDomain/img3.png)
像是vue中,一般是这么处理。现在常用的vue等框架都有解决方案。
```jsx
// 在webpack中可以配置proxy来快速获得接口代理的能力。
devServer: {
port: 8000,
proxy: {
"/api": {
target: "http://localhost:8080"
}
}
},
// Vue-cli 2.x
// config/index.js
...
proxyTable: {
'/api': {
target: 'http://localhost:8080',
}
},
...
// Vue-cli 3.x
module.exports = {
devServer: {
port: 8000,
proxy: {
"/api": {
target: "http://localhost:8080"
}
}
}
};
// 等等
```
### 方法四:Nginx 反向代理
Nginx 实现原理类似于 Node 中间件代理,需要你搭建一个中转 nginx 服务器,用于转发请求。
使用 nginx 反向代理实现跨域,是最简单的跨域方式。只需要修改 nginx 的配置即可解决跨域问题,支持所有浏览器,支持 session,不需要修改任何代码,并且不会影响服务器性能。
主要是后端进行nginx配置,就不讲了。
### 方法五:websocket
Websocket 是 HTML5 的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。什么是全双工通信 ?简单来说,就是在建立连接之后,server 与 client 都能主动向对方发送或接收数据。
以第**三方库 ws** 为例:
```jsx
........
const WebSocket = require('ws');
const ws = new WebSocket('ws://www.host.com/path');
ws.on('open', function open() {
ws.send('something');
});
ws.on('message', function incoming(data) {
console.log(data);
});
... ...
```
### 方法六:window.postMessage()
此方法可以安全地实现跨源通信。window.postMessage() 方法允许来自一个文档的脚本可以传递文本消息到另一个文档里的脚本,而不用管是否跨域,可以用这种消息传递技术来实现安全的通信。这项技术称为“**跨文档消息传递**”,又称为“**窗口间消息传递**”或者“**跨域消息传递**”。
它可用于解决以下方面的问题:
- 页面和其打开的新窗口的数据传递
- 多窗口之间消息传递
- 页面与嵌套的iframe消息传递
- 上面三个场景的跨域数据传递
**发送消息**
```jsx
otherWindow.postMessage(message, targetOrigin, [transfer]);
```
> **otherWindow**:其他窗口的一个引用。比如 iframe 的 contentWindow 属性、执行 window.open 返回的窗口对象、或者是命名过或数值索引的 window.frames。
> message:要发送的消息。它将会被结构化克隆算法序列化,所以无需自己序列化,html5规范中提到该参数可以是JavaScript的任意基本类型或可复制的对象,然而并不是所有浏览器都做到了这点儿,部分浏览器只能处理字符串参数,所以我们在传递参数的时候需要使用JSON.stringify()方法对对象参数序列化。
> **targetOrigin:**“目标域“。URI(包括:协议、主机地址、端口号)。若指定为”*“,则表示可以传递给任意窗口,指定为”/“,则表示和当前窗口的同源窗口。当为URI时,如果目标窗口的协议、主机地址或端口号这三者的任意一项不匹配 targetOrigin 提供的值,那么消息就不会发送。
**接收消息**
如果指定的源匹配的话,那么当调用 postMessage() 方法的时候,在目标窗口的Window对象上就会触发一个 message 事件。
获取postMessage传来的消息:为页面添加onmessage事件。
```jsx
window.addEventListener('message',function(e) {
var origin = event.origin;
// 通常,onmessage()事件处理程序应当首先检测其中的origin属性,忽略来自未知源的消息
if (origin !== "http://example.org:8080") return;
// ...
}, false)
```
> event 的属性有:
> data: 从其他 window 传递过来的数据副本。
> origin: 调用 postMessage 时,消息发送窗口的 origin。例如:“http://example.com:8080”。
> source: 对发送消息的窗口对象的引用。可以使用此来在具有不同 origin 的两个窗口之间建立双向数据通信。
例如:
1. vue项目中,页面http://localhost:4000/=>http://localhost:4000/created 传数据
```jsx
// http://localhost:4000/
const gtoSend = () => {
// 发送
const targetWindow = window.open('http://localhost:4000/created');
setTimeout(()=>{
targetWindow.postMessage('你好,postMessage', 'http://localhost:4000/')
},100)
}
onMounted(() => {
window.addEventListener('message', (e) => {
console.log(e.data) //我收到信息了
})
})
//http://localhost:4000/created
// 接收
onMounted(() => {
window.addEventListener('message', (e) => {
console.log(e)
console.log(e.source); // e.source 发送消息的窗口 http://localhost:4000/
console.log(e.origin); // e.origin 消息发向的网址 http://localhost:4000/created
console.log(e.data); // e.data 发送的消息 你好,postMessage'
test.value = e.data
if (e.origin !== "http://localhost:4000") return;
e.source.postMessage('我收到消息了', e.origin)
},false)
})
```
2.iframe 窗口
```jsx
// http://localhost:4000/
<template>
<div class="home">
<button @click="changeFor" style="margin-top:20px">请求</button>
<div style="width:120%">
<iframe id="iframe"
src="http://localhost:4000/created"
style="width:100%;height:300px">
</iframe>
</div>
</template>
<script>
onMounted(() => {
window.addEventListener('message', (e) => {
console.log('4000页面',e.data) //4000页面 你好,我是created页面
})
})
var iframe = document.getElementById('iframe');
const changeFor = () => {
window.frames[0].postMessage('来首页的消息的消息', 'http://localhost:4000/created');
}
</script>
//http://localhost:4000/created
onMounted(() => {
window.addEventListener('message', (e) => {
console.log('created页面',e)
console.log(e.source); // e.source 发送消息的窗口
console.log(e.origin); // e.origin 消息发向的网址
console.log(e.data); // e.data 发送的消息
if (e.origin !== "http://localhost:4000") return;
e.source.postMessage('你好,我是created页面', e.origin)
},false)
})
```
![crossDomain](/yifu-study-front-share/images/crossDomain/img4.png)
其他方法暂时不提了,基本常用的就是上面6种了。💯
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment