跳到主要内容

3 篇博文 含有标签「js」

查看所有标签

什么是跨域

什么是同源策略及其限制内容

同源策略是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSRF等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。

同源策略限制了一下行为:

  • Cookie、LocalStorage 和 IndexDB 无法读取
  • DOM 和 JS 对象无法获取
  • Ajax请求发送不出去

跨域

当协议、域名、端口号中任意一个不相同时,都算作不同域。不同域之间相互请求资源,就算作“跨域”。

注意:跨域请求产生时,请求是发出去了,也是有响应的,仅仅是浏览器同源策略,认为不安全,拦截了结果,不将数据传递我们使用罢了

URL说明是否允许通信
http://www.a.com/a.js
http://www.a.com/b.js
同一域名下允许
http://www.a.com/lab/a.js
http://www.a.com/script/b.js
同一域名下不同文件夹允许
http://www.a.com:8000/a.js
http://www.a.com/b.js
同一域名,不同端口不允许
http://www.a.com/a.js
https://www.a.com/b.js
域名和域名对应ip不允许
http://www.a.com/a.js
http://script.a.com/b.js
主域相同,子域不同不允许
(cookie这种情况下也不允许访问)
http://www.a.com/a.js
http://a.com/b.js
同一域名,不同二级域名不允许
(cookie这种情况下也不允许访问)
http://www.cnblogs.com/a.js
http://www.a.com/b.js
不同域名不允许

解决方案如下

cors

概念

  • CORS 需要浏览器和后端同时支持
  • 浏览器会自动进行 CORS 通信,实现 CORS 通信的关键是后端。只要后端实现了 CORS,就实现了跨域。
  • 服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。
  • 虽然设置 CORS 和前端没什么关系,但是通过这种方式解决跨域问题的话,会在发送请求时出现两种情况,分别为简单请求复杂请求

简单请求

只要同时满足以下两大条件,就属于简单请求

  • 条件1:使用下列方法之一:

    • GET

    • HEAD

      • POST
  • 条件2:HTTP的头信息不超出以下几种字段

    • Accept

    • Accept-Language

    • Content-Language

    • Last-Event-ID

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

      • text/plain

      • multipart/form-data

      • application/x-www-form-urlencoded

复杂请求

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

JSONP

JSONP原理

利用 <script> 标签没有跨域限制,网页可以得到从其他来源动态产生的 JSON 数据。JSONP请求一定需要对方的服务器做支持才可以。

JSONP和AJAX对比

JSONP和AJAX相同,都是客户端向服务器端发送请求,从服务器端获取数据的方式。但AJAX属于同源策略,JSONP属于非同源策略(跨域请求)

JSONP缺点

JSONP优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅支持get方法具有局限性,不安全可能会遭受XSS攻击。

nginx反向代理

服务器配置代理服务器

#如果监听到请求接口地址是 www.xxx.com/api/page ,nginx就向http://www.yyy.com:9999/api/page这个地址发送请求
server {
listen 80;
server_name www.xxx.com;
#判过滤出含有api的请求
location /api/ {
proxy_pass http://www.yyy.com:9999; #真实服务器的地址
}
}

proxy代理服务器

// vue.config.js/webpack.config.js 
// 优点:可以配置多个代理,且可灵活控制请求是否走代理
// 缺点:配置繁琐,发起代理请求时必须加上配置好的前缀
module.exports={
devServer:{
proxy:{
'/api01':{
target:'http://xxx.xxx.xxx:5000',
changeOrigin:true,
// 重写请求,根据接口详情,判断是否需要
pathRewrite:{
'^/api01':''
}
},
'/api02':{
target:'http://xxx.xxx.xxx:5001',
changeOrigin:true,
// 重写请求,根据接口详情,判断是否需要
pathRewrite:{
'^/api02':''
}
}
}
}
}
// changeOrigin设置为true时,服务器收到的请求头的host与服务器地址相同
// changeOrigin设置为false时,服务器收到的请求头的host与前端地址相同

websocket

WebSocket 协议是一种基于 TCP 的通信协议,与 HTTP 协议不同,它不受同源策略的限制,没有使用HTTP响应头,因此也没有跨域的限制

window.name + iframe

window.name属性可设置或者返回存放窗口名称的一个字符串。他的神器之处在于name值在不同页面或者不同域下加载后依旧存在,没有修改就不会发生变化,并且可以存储非常长的name(2MB)

其中a.html和b.html是同域的,都是http://localhost:3000;而c.html是http://localhost:4000

 // a.html(http://localhost:3000/b.html)
<iframe src="http://localhost:4000/c.html" frameborder="0" onload="load()" id="iframe"></iframe>
<script>
let first = true
// onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
function load() {
if(first){
// 第1次onload(跨域页)成功后,切换到同域代理页面
let iframe = document.getElementById('iframe');
iframe.src = 'http://localhost:3000/b.html';
first = false;
}else{
// 第2次onload(同域b.html页)成功后,读取同域window.name中数据
console.log(iframe.contentWindow.name);
}
}
</script>

b.html为中间代理页,与a.html同域,内容为空。

 // c.html(http://localhost:4000/c.html)
<script>
window.name = '我不爱你'
</script>

location.hash + iframe

原理就是通过 url 带 hash ,通过一个非跨域的中间页面来传递数据

一开始 a.html 给 c.html 传一个 hash 值,然后 c.html 收到 hash 值后,再把 hash 值传递给 b.html,最后 b.html 将结果放到 a.html 的 hash 值中。 同样的,a.html 和 b.htm l 是同域的,都是 http://localhost:8000,而 c.html 是http://localhost:8080

// a.html
<iframe src="http://localhost:8080/hash/c.html#name1"></iframe>
<script>
  console.log(location.hash);
  window.onhashchange = function() {
    console.log(location.hash);
  };
</script>
// b.html
<script>
  window.parent.parent.location.hash = location.hash;
</script>
// c.html
<body></body>
<script>
  console.log(location.hash);
  const iframe = document.createElement("iframe");
  iframe.src = "http://localhost:8000/hash/b.html#name2";
  document.body.appendChild(iframe);
</script>

document.domain + iframe

该方式只能用于二级域名相同的情况下,比如 a.test.comb.test.com 适用于该方式。 只需要给页面添加 document.domain ='test.com' 表示二级域名都相同就可以实现跨域。

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

我们看个例子:页面a.zf1.cn:3000/a.html获取页面b.zf1.cn:3000/b.html中a的值

// a.html
<body>
helloa
<iframe src="http://b.zf1.cn:3000/b.html" frameborder="0" onload="load()" id="frame"></iframe>
<script>
document.domain = 'zf1.cn'
function load() {
console.log(frame.contentWindow.a);
}
</script>
</body>
// b.html
<body>
hellob
<script>
document.domain = 'zf1.cn'
var a = 100;
</script>
</body>

不允许iframe的设置

添加X-Frame-Options响应头,值为deny


鲸落知识点js阅读需 6 分钟

for 循环100000次

最直接的方式就是直接渲染出来,但是这样的做法肯定是不可取的,因为一次性渲染出10w个节点,是非常耗时间的

const renderList = async () => {
console.time('列表时间')
const list = await getList()
list.forEach(item => {
const div = document.createElement('div')
div.className = 'sunshine'
div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
container.appendChild(div)
})
console.timeEnd('列表时间')
}
renderList()

setTimeout分页渲染(时间分片)

这个方法就是,把10w按照每页数量limit分成总共Math.ceil(total / limit)页,然后利用setTimeout,每次渲染1页数据,这样的话,渲染出首页数据的时间大大缩减了

const renderList = async () => {
console.time('列表时间')
const list = await getList()
console.log(list)
const total = list.length
const page = 0
const limit = 200
const totalPage = Math.ceil(total / limit)

const render = (page) => {
if (page >= totalPage) return
setTimeout(() => {
for (let i = page * limit; i < page * limit + limit; i++) {
const item = list[i]
const div = document.createElement('div')
div.className = 'sunshine'
div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
container.appendChild(div)
}
render(page + 1)
}, 0)
}
render(page)
console.timeEnd('列表时间')
}

requestAnimationFrame

requestAnimationFrame 和 setTimeout 的区别:

  • requestAnimationFrame的调用频率通常为每秒60次。这意味着我们可以在每次重绘之前更新动画的状态,并确保动画流畅运行,而不会对浏览器的性能造成影响。
  • setIntervalsetTimeout它可以让我们在指定的时间间隔内重复执行一个操作,不考虑浏览器的重绘,而是按照指定的时间间隔执行回调函数,可能会被延迟执行,从而影响动画的流畅度。

使用requestAnimationFrame代替setTimeout,减少了重排的次数,极大提高了性能,建议大家在渲染方面多使用requestAnimationFrame

requestAnimationFrame不是定时器!不是定时器!!!只是一个用作定时器的帧函数

const renderList = async () => {
console.time('列表时间')
const list = await getList()
console.log(list)
const total = list.length
const page = 0
const limit = 200
const totalPage = Math.ceil(total / limit)

const render = (page) => {
if (page >= totalPage) return
// 使用requestAnimationFrame代替setTimeout
requestAnimationFrame(() => {
for (let i = page * limit; i < page * limit + limit; i++) {
const item = list[i]
const div = document.createElement('div')
div.className = 'sunshine'
div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
container.appendChild(div)
}
render(page + 1)
})
}
render(page)
console.timeEnd('列表时间')
}

文档碎片 + requestAnimationFrame

先解释一下什么是 DocumentFragment ,文献引用自MDN

DocumentFragment,文档片段接口,表示一个没有父级文件的最小文档对象。它被作为一个轻量版的`Document`使用,用于存储已排好版的或尚未打理好格式的XML片段。最大的区别是因为`DocumentFragment`不是真实DOM树的一部分,它的变化不会触发DOM树的(重新渲染) ,且不会导致性能等问题。可以使用`document.createDocumentFragment`方法或者构造函数来创建一个空的`DocumentFragment

文档碎片的好处

  • 之前都是每次创建一个div标签就appendChild一次,但是有了文档碎片可以先把1页的div标签先放进文档碎片中,然后一次性appendChildcontainer中,这样减少了appendChild的次数,极大提高了性能
  • 页面只会渲染文档碎片包裹着的元素,而不会渲染文档碎片
const renderList = async () => {
console.time('列表时间')
const list = await getList()
console.log(list)
const total = list.length
const page = 0
const limit = 200
const totalPage = Math.ceil(total / limit)

const render = (page) => {
if (page >= totalPage) return
requestAnimationFrame(() => {
// 创建一个文档碎片
const fragment = document.createDocumentFragment()
for (let i = page * limit; i < page * limit + limit; i++) {
const item = list[i]
const div = document.createElement('div')
div.className = 'sunshine'
div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
// 先塞进文档碎片
fragment.appendChild(div)
}
// 一次性appendChild
container.appendChild(fragment)
render(page + 1)
})
}
render(page)
console.timeEnd('列表时间')
}

懒加载

一句话解释:最开始不渲染所有数据,只展示视图上可见的数据,当滚动到页面底部时,加载更多数据

  • 位置计算 + 滚动事件 (Scroll) + DataSet API

    • clientTopoffsetTopclientHeight 以及 scrollTop 各种关于图片的高度作比对

    • 监听 window.scroll 事件

    • // 控制图片懒加载
      <img data-src="shanyue.jpg" />
      // 首先设置一个临时 Data 属性 data-src,控制加载时使用 src 代替 data-src,可利用 DataSet API 实现
      img.src = img.datset.src
  • getBoundingClientRect API + Scroll with Throttle + DataSet API

    • 如何判断图片出现在了当前视口?
    • Element.getBoundingClientRect() 方法返回元素的大小及其相对于视口的位置。img.getBoundingClientRect().top < document.documentElement.clientHeight;
  • IntersectionObserver api

    • 上一个方案使用的方法是: window.scroll 监听 Element.getBoundingClientRect() 并使用 _.throttle 节流
    • 浏览器出了一个三合一事件: IntersectionObserver API,一个能够监听元素是否到了当前视口的事件,一步到位!

虚拟列表

概念

虚拟列表其实是按需显示的一种实现,即只对可见区域进行渲染,对非可见区域中的数据不渲染或部分渲染的技术,从而达到极高的渲染性能。

假设有1万条记录需要同时渲染,我们屏幕的可见区域的高度为500px,而列表项的高度为50px,则此时我们在屏幕中最多只能看到10个列表项,那么在首次渲染的时候,我们只需加载10条即可。为了防止滑动过快导致的白屏现象,我们可以使用预加载的方式多加载一些数据出来。

image-20230810165500313

实现

实现屏加载的时候,只加载可视区域内需要的列表项,当滚动发生时,动态通过计算获得可视区域内的列表项,并将非可视区域内存在的列表项删除。

  • 计算当前可视区域起始数据索引(startIndex)
  • 计算当前可视区域结束数据索引(endIndex)
  • 计算当前可视区域的数据,并渲染到页面中
  • 计算startIndex对应的数据在整个列表中的偏移位置startOffset并设置到列表上

image-20230810170401342

由于只是对可视区域内的列表项进行渲染,所以为了保持列表容器的高度并可正常的触发滚动,将Html结构设计成如下结构:

<div class="infinite-list-container">
<div class="infinite-list-phantom"></div>
<div class="infinite-list">
<!-- item-1 -->
<!-- item-2 -->
<!-- ...... -->
<!-- item-n -->
</div>
</div>
  • infinite-list-container可视区域的容器
  • infinite-list-phantom 为容器内的占位,高度为总列表高度,用于形成滚动条
  • infinite-list 为列表项的渲染区域

接着,监听infinite-list-containerscroll事件,获取滚动位置scrollTop

  • 假定可视区域高度固定,称之为screenHeight
  • 假定列表每项高度固定,称之为itemSize
  • 假定列表数据称之为listData
  • 假定当前滚动位置称之为scrollTop

则可推算出:

  • 列表总高度listHeight = listData.length * itemSize
  • 可显示的列表项数visibleCount = Math.ceil(screenHeight / itemSize)
  • 数据的起始索引startIndex = Math.floor(scrollTop / itemSize)
  • 数据的结束索引endIndex = startIndex + visibleCount
  • 列表显示数据为visibleData = listData.slice(startIndex,endIndex)

当滚动后,由于渲染区域相对于可视区域已经发生了偏移,此时我需要获取一个偏移量startOffset,通过样式控制将渲染区域偏移至可视区域中。

  • 偏移量startOffset = scrollTop - (scrollTop % itemSize)

简易代码

最终的简易代码如下:

<template>
<div ref="list" class="infinite-list-container" @scroll="scrollEvent($event)">
<div class="infinite-list-phantom" :style="{ height: listHeight + 'px' }"></div>
<div class="infinite-list" :style="{ transform: getTransform }">
<div ref="items"
class="infinite-list-item"
v-for="item in visibleData"
:key="item.id"
:style="{ height: itemSize + 'px',lineHeight: itemSize + 'px' }"
>{{ item.value }}</div>
</div>
</div>
</template>

<script>
export default {
name:'VirtualList',
props: {
//所有列表数据
listData:{
type:Array,
default:()=>[]
},
//每项高度
itemSize: {
type: Number,
default:200
}
},
computed:{
//列表总高度
listHeight(){
return this.listData.length * this.itemSize;
},
//可显示的列表项数
visibleCount(){
return Math.ceil(this.screenHeight / this.itemSize)
},
//偏移量对应的style
getTransform(){
return `translate3d(0,${this.startOffset}px,0)`;
},
//获取真实显示列表数据
visibleData(){
return this.listData.slice(this.start, Math.min(this.end,this.listData.length));
}
},
mounted() {
this.screenHeight = this.$el.clientHeight;
this.start = 0;
this.end = this.start + this.visibleCount;
},
data() {
return {
//可视区域高度
screenHeight:0,
//偏移量
startOffset:0,
//起始索引
start:0,
//结束索引
end:null,
};
},
methods: {
scrollEvent() {
//当前滚动位置
let scrollTop = this.$refs.list.scrollTop;
//此时的开始索引
this.start = Math.floor(scrollTop / this.itemSize);
//此时的结束索引
this.end = this.start + this.visibleCount;
//此时的偏移量
this.startOffset = scrollTop - (scrollTop % this.itemSize);
}
}
};
</script>

多大量数据进行处理 — Web Worker

因为js是单线程运行的,在遇到一些需要处理大量数据的js时,可能会阻塞页面的加载,造成页面的假死。

在HTML5的新规范中,实现了 Web Worker 来引入 js 的 “多线程” 技术, 可以让我们在页面主运行的 js 线程中,加载运行另外单独的一个或者多个 js 线程

一句话: Web Worker专门处理复杂计算的,从此让前端拥有后端的计算能力

参考链接

后端一次给你10万条数据,如何优雅展示 - 掘金 (juejin.cn)

高性能渲染十万条数据(虚拟列表) - 掘金 (juejin.cn)


鲸落知识点js阅读需 8 分钟

写在前面

我们知道,HTTP 是无状态的。也就是说,HTTP 请求方和响应方间无法维护状态,都是一次性的,它不知道前后的请求都发生了什么。

但有的场景下,我们需要维护状态。最典型的,一个用户登陆微博,发布、关注、评论,都应是在登录后的用户状态下的。我们知道,HTTP 是无状态的。也就是说,HTTP 请求方和响应方间无法维护状态,都是一次性的,它不知道前后的请求都发生了什么。

但有的场景下,我们需要维护状态。最典型的,一个用户登陆微博,发布、关注、评论,都应是在登录后的用户状态下的。

Session 、 Cookie 和 token 的主要目的就是为了弥补 HTTP 的无状态特性。

什么是cookie

  • cookie 存储在客户端: cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。

  • cookie 是不可跨域的: 每个 cookie 都会绑定单一的域名,无法在别的域名下获取使用,一级域名和二级域名之间是允许共享使用的靠的是 domain)

    • Domain属性指定浏览器发出 HTTP 请求时,哪些域名要附带这个 Cookie。如果没有指定该属性,浏览器会默认将其设为当前 URL 的一级域名,比如 www.example.com 会设为 example.com,而且以后如果访问example.com的任何子域名,HTTP 请求也会带上这个 Cookie。如果服务器在Set-Cookie字段指定的域名,不属于当前域名,浏览器会拒绝这个 Cookie。

什么是session

  • session 是另一种记录服务器和客户端会话状态的机制
  • session 是基于 cookie 实现的,session 存储在服务器端,sessionId 会被存储到客户端的cookie 中

session验证流程

  • 用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的 Session
  • 请求返回时将此 Session 的唯一标识信息 SessionID 返回给浏览器
  • 浏览器接收到服务器返回的 SessionID 信息后,会将此信息存入到 Cookie 中,同时 Cookie 记录此 SessionID 属于哪个域名
  • 当用户第二次访问服务器的时候,请求会自动判断此域名下是否存在 Cookie 信息,如果存在自动将 Cookie 信息也发送给服务端,服务端会从 Cookie 中获取 SessionID,再根据 SessionID 查找对应的 Session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作。

根据以上流程可知,SessionID 是连接 Cookie 和 Session 的一道桥梁,大部分系统也是根据此原理来验证用户登录状态。

cookie和session的区别

  • 安全性: Session 比 Cookie 安全,Session 是存储在服务器端的,Cookie 是存储在客户端的。
  • 存取值的类型不同:Cookie 只支持存字符串数据,想要设置其他类型的数据,需要将其转换成字符串,Session 可以存任意数据类型。
  • 有效期不同: Cookie 可设置为长时间保持,比如我们经常使用的默认登录功能,Session 一般失效时间较短,客户端关闭(默认情况下)或者 Session 超时都会失效
  • 存储大小不同: 单个 Cookie 保存的数据不能超过 4K,Session 可存储数据远高于 Cookie,但是当访问量过多,会占用过多的服务器资源。

什么是token

Token是在身份验证和授权过程中广泛使用的一种机制,用于确认用户的身份并获得权限。它通常是一个字符串,由服务器生成并返回给客户端(例如,Web浏览器或移动应用程序)。Token在客户端和服务器之间进行传递,用于识别和验证用户。

token身份验证流程

  1. 客户端使用用户名跟密码请求登录
  2. 服务端收到请求,去验证用户名与密码
  3. 验证成功后,服务端会签发一个 token 并把这个 token 发送给客户端
  4. 客户端收到 token 以后,会把它存储起来,比如放在 cookie 里或者 localStorage 里
  5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 token
  6. 服务端收到请求,然后去验证客户端请求里面带着的 token ,如果验证成功,就向客户端返回请求的数据
  • 每一次请求都需要携带 token,需要把 token 放到 HTTP 的 Header 里
  • 基于 token 的用户认证是一种服务端无状态的认证方式,服务端不用存放 token 数据。用解析 token 的计算时间换取 session 的存储空间,从而减轻服务器的压力,减少频繁的查询数据库
  • token 完全由应用管理,所以它可以避开同源策略

token和session的区别

  1. 定义和作用:
    • Token(令牌):Token是在身份验证和授权过程中用于确认用户身份和获得权限的一种机制。它通常是一个字符串,由服务器生成并返回给客户端,用于在客户端和服务器之间传递身份验证和授权信息。
    • Session(会话):Session是服务器端用于维护用户状态和身份信息的一种机制。服务器为每个客户端创建一个会话,用于跟踪用户的状态,通过Session ID标识不同的会话。
  2. 存储位置:
    • Token:Token通常存储在客户端,比如浏览器的Cookie或本地存储中,以便客户端可以将其附加到后续请求中。
    • Session:Session数据通常存储在服务器端,而不是在客户端。服务器通过Session ID来标识每个客户端的会话状态。
  3. 状态维护:
    • Token:Token是无状态的,服务器不需要在后端存储Token的状态信息,因为所有必要的信息都包含在Token本身中。
    • Session:Session是有状态的,服务器需要在后端维护会话状态信息,以便跟踪用户状态和保存临时数据。
  4. 应用场景:
    • Token:Token在Web API、移动应用和分布式系统中广泛使用,特别适合跨服务器进行身份验证和授权。
    • Session:Session通常在传统的Web应用中使用,用于跟踪用户状态和保存用户相关的数据。
  5. 安全性:
    • Token:由于Token包含所有必要的验证信息,因此必须谨慎处理,确保其不被非法获取或篡改,通常通过加密和签名来增加安全性。
    • Session:Session数据存储在服务器端,相对较安全,但仍然需要采取措施防止会话劫持和其他安全漏洞。

Cookie 作为 HTTP 规范,其出现历史久远,因此存在一些历史遗留问题,比如跨域限制等,并且 Cookie 作为 HTTP 规范中的内容,其存在默认存储以及默认发送的行为,存在一定的安全性问题。相较于 Cookie,token 需要自己存储,自己进行发送,不存在跨域限制,因此 Token 更加的灵活,没有 Cookie 那么多的“历史包袱”束缚,在安全性上也能够做更多的优化。

Token 有什么 优势?

Token的无状态特性、安全性、可扩展性以及跨平台和跨语言的支持,使得它成为现代Web应用和API身份验证和授权的首选机制。


鲸落知识点js阅读需 7 分钟