今天抽空总结一下这段时间遇到的前端问题,以及总结的解决方法。

跨域

1、同源策略

浏览器同源策略限制请求

同源是指"​​协议+域名+端口​​"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。

限制以下行为

  1. ​Cookie​​​、​​LocalStorage​​​ 和​​IndexDB​​ 无法读取
  2. ​DOM​​ 和 Js对象无法获得
  3. ​AJAX​​ 请求不能发送

有三个标签是允许跨域加载资源

  • ​<img src=XXX>​
  • ​<link href=XXX>​
  • ​<script src=XXX>​

2、解决方案

1)通过​​jsonp​​跨域

​script​​标签不受策略影响,可以动态生成script去请求数据,但是仅限Get请求

原生实现

<script>
var script = document.createElement('script');
script.type = 'text/javascript';
// 传参并指定回调执行函数为onBack
script.src = 'http://www.domain2.com:8080/login?user=admin&callback=onBack';
document.head.appendChild(script);
// 回调执行函数
function onBack(res) {
alert(JSON.stringify(res));
}
</script>

vue实现

this.$http.jsonp(
'http://www.domain2.com:8080/login',
{
params: {},
jsonp: 'onBack'
}
).then(res => { console.log(res); }
)

2)​​document.domain + iframe​​跨域

仅限主域相同,子域不同的跨域应用场景

实现原理:两个页面都通过js强制设置​​document.domain​​为基础主域,就实现了同域

<!-- 父窗口:http://www.domain.com/a.html -->
<iframe id="iframe" src="http://child.domain.com/b.html">
</iframe>

<script>
document.domain = 'domain.com';
var user = 'admin';
</script>

<!-- 子窗口:http://child.domain.com/b.html -->
<script>
document.domain = 'domain.com';
// 获取父窗口中变量
alert('get js data from parent ---> '+ window.parent.user);
</script>

3)​​location.hash + iframe ​

实现原理: a欲与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用​​iframe​​​的​​location.hash​​传值,相同域之间直接js访问来通信。

具体实现:

A域:a.html -> B域:b.html -> A域:c.html,

a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过​​parent.parent​​访问a页面所有对象。

4)​​window.name + iframe​​跨域

​window.name​​属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。

通过​​iframe​​​的src属性由外域转向本地域,跨域数据即由​​iframe​​​的​​window.name​​从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。

5)​​postMessage​​跨域

​postMessage​​一般用于解决以下问题

a.) 页面和其打开的新窗口的数据传递

b.) 多窗口之间消息传递

c.) 页面与嵌套的iframe消息传递

d.) 上面三个场景的跨域数据传递

<!-- a页面:http://www.domain1.com/a.html -->
<iframe id="iframe"
src="http://www.domain2.com/b.html"
style="display:none;">
</iframe>
<script>
var iframe = document.getElementById('iframe');
iframe.onload = function() {
var data = {
name: 'aym'
};
// 向domain2传送跨域数据
iframe.contentWindow.postMessage
(JSON.stringify(data),
'http://www.domain2.com');
};
// 接受domain2返回数据
window.addEventListener
('message', function(e) {
alert('data from domain2 ---> ' + e.data);
},
false);
</script>

<!-- b页面:http://www.domain2.com/b.html -->
<script>
// 接收domain1的数据
window.addEventListener
('message', function(e) {
alert('data from domain1 ---> ' + e.data);
var data = JSON.parse(e.data);
if (data) {
data.number = 16;
// 处理后再发回domain1
window.parent.postMessage(JSON.stringify(data),
'http://www.domain1.com');
}
}, false);
</script>

6)跨域资源共享(CORS):主流的跨域解决方案

服务端设置​​Access-Control-Allow-Origin​​即可

若要带cookie请求:前后端都需要设置。

前端: : 检查前端设置是否带​​cookie​​​:​​xhr.withCredentials = true;​

通过这种方式解决跨域问题的话,会在发送请求时出现两种情况,分别为简单请求复杂请求

简单请求:

使用下列方法之一:

  • GET
  • HEAD
  • POST

​Content-Type ​​的值仅限于下列三者之一:

  • ​text/plain​
  • ​multipart/form-data​
  • ​ application/x-www-form-urlencoded​

不符合以上条件的请求就肯定是复杂请求了。 复杂请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求, 该请求是 option 方法的,通过该请求来知道服务端是否允许跨域请求。

OPTIONS预检请求

请求头:

  • ​Origin​​:当前请求源,和响应头里的Access-Control-Allow-Origin 对标, 是否允许当前源访问,Origin是不可修改的
  • ​Access-Control-Request-Headers​​:本次真实请求的额外请求头,和响应头里的Access-Control-Allow-Headers对标,是否允许真实请求的请求头
  • ​Access-Control-Request-Method​​:本次真实请求的额外方法,和响应头里的Access-Control-Allow-Methods对标,是否允许真实请求使用的请求方法


响应头

  • ​ Access-Control-Allow-Credentials​​:
    这里的​​Credentials​​(凭证)其意包括:​​Cookie​​ ,授权标头或 TLS 客户端证书,默认CORS请求是不带​​Cookies​​的,这与JSONP不同,JSONP每次请求都携带Cookies的,当然跨域允许带Cookies会导致CSRF漏洞。如果非要跨域传递Cookies,web端需要给ajax设置​​withCredentials​​为true,同时,服务器也必须使用​​Access-Control-Allow-Credentials​​头响应。此响应头true意味着服务器允许cookies(或其他用户凭据)包含在跨域请求中。另外,简单的GET请求是不预检的,即使请求的时候设置​​widthCrenditials​​为true,如果响应头不带​​Access-Control-Allow-Credentials​​,则会导致整个响应资源被浏览器忽略。
  • ​ Access-Control-Allow-Headers​
  • ​Access-Control-Allow-Methods​
  • ​Access-Control-Allow-Origin​
  • ​ Access-Control-Expose-Headers​​:在CORS中,默认的,只允许客户端读取下面六个响应头(在axios响应对象的headers里能看到):
  • ​Cache-Control​
  • ​Content-Language​
  • ​Content-Type​
  • ​Expires​
  • ​Last-Modified​
  • ​Pragma​

如果这六个以外的响应头要是想让客户端读取到,就需要设置​​Access-Control-Expose-Headers​​​这个为响应头名了,比如A​​ccess-Control-Expose-Headers: Token​

  • ​Access-Control-Max-Age​​:设置预检请求的有效时长,就是服务器允许的请求方法和请求头做个缓存。



7)​​nginx​​代理跨域

nginx配置解决iconfont跨域

浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加入以下配置。

location / {
add_header
Access-Control-Allow-Origin *;
}

反向代理

跨域原理: 同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨越问题。

通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。

#proxy服务器
server {
listen
81;
server_name www.domain1.com;
location / {
proxy_pass
http://www.domain2.com:8080;
#反向代理
proxy_cookie_domain
www.domain2.com www.domain1.com;
# 修改cookie里域名
index index.html index.htm;
# 用webpack-dev-server等中间件代理接口访问nignx时,
# 此时无浏览器参与,故没有同源限制,面的跨域配置可不启用
add_header Access-Control-Allow-Origin
http://www.domain1.com;
# 当前端只跨域不带cookie时,可为*
add_header Access-Control-Allow-Credentials true;
}
}

8)​​nodejs​​中间件代理跨域

node中间件实现跨域代理,原理大致与nginx相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。

9)​​WebSocket​​协议跨域

WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。

原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。

<div>user input:
<input type="text">
</div>

<script src="./socket.io.js"></script>
<script>var socket = io('
http://www.domain2.com:8080');
// 连接成功处理
socket.on('connect', function() {
// 监听服务端消息
socket.on('message', function(msg) {
console.log('data from server: ---> ' + msg);
});
// 监听服务端关闭
socket.on('disconnect', function() {
console.log('Server socket has closed.');
});
});
document.getElementsByTagName('input')
[0].onblur = function() {
socket.send(this.value);
};
</script>

图片懒加载

原理

优先加载可视区域的内容,其他部分等进入了可视区域再加载,从而提高性能

原理:

一张图片就是一个​​<img>​​​标签,浏览器是否发起请求图片是根据​​<img>​​​的​​src​​​属性,所以实现懒加载的关键就是,在图片没有进入可视区域时,先不给​​<img>​​​的src赋值,这样浏览器就不会发送请求了,等到图片进入可视区域再给​​src​​赋值。

实现思路:

  1. 加载​​loading​​图片
  2. 判断哪些图片要加载【重点】
    当图片距离顶部的距离top-height等于可视区域h和滚动区域高度s之和时说明图片马上就要进入可视区了
  3. 隐形加载图片
    创建一个临时图片,new Image() ,不会加载到页面上去,实现隐形加载
  4. 替换真图片
    替换src属性

页面布局位置基础知识


  • 网页可见区域宽:​​document.body.clientWidth​​;
  • 网页可见区域高:​​document.body.clientHeight;​
  • 网页可见区域宽:​​document.body.offsetWidth ​​(包括边线的宽);
  • 网页可见区域高:​​document.body.offsetHeight​​ (包括边线的高);



  • 网页正文全文宽:​​document.body.scrollWidth;​
  • 网页正文全文高:​​document.body.scrollHeight;​
  • 网页被卷去的高:​​document.body.scrollTop;​
  • 网页被卷去的左:​​document.body.scrollLeft;​



  • 网页正文部分上:​​window.screenTop;​
  • 网页正文部分左:​​window.screenLeft;​
  • 屏幕分辨率的高:​​window.screen.height;​
  • 屏幕分辨率的宽:​​window.screen.width;​
  • 屏幕可用工作区高度:​​window.screen.availHeight;​


  • 对块级元素来说,​​offsetTop​​​、​​offsetLeft​​​、​​offsetWidth ​​​及​​offsetHeight ​​​描述了元素相对于​​offsetParent ​​的边界框
  • ​HTMLElement.offsetParent​​​ 是一个只读属性,返回一个指向最近的(指包含层级上的最近)包含该元素的定位元素或者最近的​​table,td,th,body​​​元素。当元素的​​style.display​​​ 设置为 "none" 时,​​offsetParent ​​​返回​​null​​​。​​offsetParent ​​​很有用,因为​​offsetTop ​​​和​​offsetLeft ​​都是相对于其内边距边界的。
  • ​HTMLElement.offsetTop​​ 为只读属性,它返回当前元素相对于其 offsetParent 元素的顶部的距离。
  • ​window.innerHeight:​​浏览器窗口的视口(viewport)高度(以像素为单位);如果有水平滚动条,也包括滚动条高度。

代码实现

可以给​​img​​​标签统一自定义属性​​data-src='default.png'​​,当检测到图片出现在窗口之后再补充src属性,此时才会进行图片资源加载。

function lazyload() {
const imgs = document.getElementsByTagName('img');
const len = imgs.length;
// 视口的高度
const viewHeight = document.documentElement.clientHeight;
// 滚动条高度
const scrollHeight = document.documentElement.scrollTop || document.body.scrollTop;
for (let i = 0; i < len; i++) {
const offsetHeight = imgs[i].offsetTop;
if (offsetHeight < viewHeight + scrollHeight) {
const src = imgs[i].dataset.src;
imgs[i].src = src;
}
}
}

// 可以使用节流优化一下
window.addEventListener('scroll', lazyload);