在前端展示1000张图片时,如果一次性加载,可能会导致页面加载缓慢,用户体验下降。以下是一些可以应用的优化策略:
<img data-src="image.jpg" alt="description" loading="lazy">
如果你希望使用JavaScript来实现懒加载,可以使用Intersection Observer API。
let images = document.querySelectorAll('img[data-src]'); let imgObserver = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { let img = entry.target; img.src = img.getAttribute('data-src'); img.removeAttribute('data-src'); observer.unobserve(img); } }); }); images.forEach(img => { imgObserver.observe(img); });
以下是一个简单的无限滚动的实现:
let page = 0; let inScroll = false; function loadImages() { if (inScroll) { return; } //然后,我们定义一个布尔变量inScroll,用于防止在加载图片时重复发送请求。 inScroll = true; fetch(`/api/images?page=${page}`) .then(response => response.json()) .then(data => { page++; data.forEach(image => { let img = document.createElement('img'); img.src = image.url; document.body.appendChild(img); }); inScroll = false; }); } window.onscroll = function() { if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) { loadImages(); } }; loadImages();
注意,这是一个基础的实现,实际的应用可能需要进行错误处理、API防抖、空数据处理等。
axios 是基于 http (基于 tcp 传输层)的网络请求库。
拦截器执行顺序:
请求的发送需要在请求拦截器之后,在响应拦截器之前,所以数组先放入request,接着在数组的前后分别加入请求和响应拦截器,由于加入请求拦截器的方法是unshift,所以最后通过promise进行请求的链式调用的时候,我们可以看到执行顺序是从左往右的,所以最后注册的请求拦截器会最先执行,而响应拦截的执行顺序和注册顺序是一样的。
fetch和axios区别:
data
属性里,以对象的方式进行传递,而Fetch则是需要放在body
属性中,以字符串的方式进行传递timeout
属性就可以了,而fetch需要通过new AbortController()然后设置settimeouthttps://juejin.cn/post/6934155066198720519#heading-2
如何取消请求:
1 利用防抖
2 利用request.cancel
通过 cancel 属性来取消请求 另一种方法是直接在请求对象上设置 cancel 属性,该属性是一个函数。当您需要取消请求时,只需调用此函数即可。
3 利用CancelToken()
我们首先创建了一个名为 source 的 CancelToken 实例,并将其传递给请求的 config 对象中。然后,在需要取消请求的位置,我们通过调用 source.cancel() 方法来发送取消请求信号。如果请求已经被取消,则会抛出一个包含取消原因的错误,并且您可以在 catch 块中检查这个错误并处理它。
4 利用signal
参数中携带signal:controller.signal, 设置一个全局变量controller= null,第一行进行判断controller && controller.abort(),第二行controller = new AbortController()
你可以在 Vue.js 应用中创建一个单独的服务文件(例如 httpService.js
),在这个文件中封装你的 Axios 请求。下面是一个简单的示例:
// 引入axios库 import axios from 'axios'; // 创建axios实例 const http = axios.create({ baseURL: 'http://api.example.com', // API服务器的基础URL timeout: 1000, // 设置请求超时时间,时间内就接受,时间外就catch }); // 添加请求拦截器 http.interceptors.request.use(config => { // 在发送请求之前,可以在这里做些什么,例如添加Token到header config.headers['Authorization'] = 'Bearer token'; return config; }, error => { // 对请求错误做些什么 return Promise.reject(error); }); // 添加响应拦截器 http.interceptors.response.use(response => { // 对响应数据做点什么,例如处理不同的HTTP状态码 if (response.status === 200) { return response.data; } else { return Promise.reject(response); } }, error => { // 对响应错误做点什么 return Promise.reject(error); }); // 导出http对象 export default http;
然后你可以在 Vue.js 组件中这样使用这个封装好的服务:
import http from './httpService'; export default { data() { return { posts: [], }; }, async created() { try { this.posts = await http.get('/posts'); } catch (error) { console.error(error); } }, };
这个封装的服务包含了请求拦截器和响应拦截器,可以方便你在请求之前或响应之后执行某些操作,例如添加认证信息到请求头,或处理不同的 HTTP 状态码。注意,这只是一个基本的例子,实际的需求可能更复杂。
axios底层原理XHR
class Axios { constructor() { } request(config) { return new Promise(resolve => { //利用promise const {url = '', method = 'get', data = {}} = config; // 发送ajax请求 const xhr = new XMLHttpRequest(); xhr.open(method, url, true); xhr.onload = function() { console.log(xhr.responseText) resolve(xhr.responseText); } xhr.serRequestHeader(k,v)//发送额外配置的头部字段 xhr.send(data);//如果data为空需要传null }) } } // 最终导出axios的方法,即实例的request方法 function CreateAxiosFn() { let axios = new Axios(); let req = axios.request.bind(axios); return req; } // 得到最后的全局变量axios let axios = CreateAxiosFn(); //接收到数据后xhr对象上的一下属性会被填充 ``` reponseText:作为响应主体被返回的文本。 responseXML:如果响应的内容类型是“text/xml”或“application/xml”,这个属性中将保存包含着响应数据的XML DOM文档。 status : 响应的HTTP状态。 statusText:HTTP状态的说明。 ``` //XHR头部信息:setRequestHeader ``` Accept:浏览器能够处理的内容类型。 Accept-Charset:浏览器能够处理的字符集。 Accept-Encoding:浏览器能够处理的压缩编码。 Accept-Language:浏览器当前设置的语言。 Connection:浏览器与服务器之间连接的类型。 Cookie:当前页面设置的任何Cookie。 Host:发出请求的页面所在的域。 Referer:发出请求的页面的URL。注意,HTTP规范将这个头部字段拼写错了, 而为保证与规范一致,也只能讲错就错了。(这个英文单词的正确拼法应该是 referrer。) User-Agent:浏览器的用户代理字符串。 ```
// 定义get,post...方法,挂在到Axios原型上 axios.method() const methodsArr = ['get', 'delete', 'head', 'options', 'put', 'patch', 'post']; methodsArr.forEach(met => { Axios.prototype[met] = function() { console.log('执行'+met+'方法'); // 处理单个方法 if (['get', 'delete', 'head', 'options'].includes(met)) { // 2个参数(url[, config]) return this.request({ method: met, url: arguments[0], ...arguments[1] || {} }) } else { // 3个参数(url[,data[,config]]) return this.request({ method: met, url: arguments[0], data: arguments[1] || {}, ...arguments[2] || {} }) } } })
axios
实例: 当你调用 axios.method()
函数时,Axios 首先会创建一个新的 axios
实例。这个实例包含了 Axios 的所有功能,包括拦截器、转换函数、取消功能等。get
方法。合并后的配置将被应用到新创建的 axios
实例上。method()
函数返回的是一个 Promise 对象。这个 Promise 代表了 HTTP 请求的结果,你可以使用 .then
或 .catch
来处理这个 Promise。如果请求成功,Promise 将被解析并返回响应数据;如果请求失败,Promise 将被拒绝并返回错误信息。Express优点:线性逻辑,通过中间件形式把业务逻辑细分、简化,一个请求进来经过一系列中间件处理后再响应给用户,清晰明了。 缺点:基于 callback 组合业务逻辑,业务逻辑复杂时嵌套过多,异常捕获困难。
Koa优点:首先,借助 co 和 generator,很好地解决了异步流程控制和异常捕获问题。其次,Koa 把 Express 中内置的 router、view 等功能都移除了,使得框架本身更轻量。 缺点:社区相对较小
KOA启动服务的流程
koa 主要的启动流程就是下面的 4 步:引入 koa 包 => 实例化 koa => 编写中间件 => 监听服务器
实例化koa:
执行 constructor ,将 ctx、response、request 等对象封装在 koa 实例中;
编写中间件:
首先判断 fn 的类型,不是方法直接抛错 => 是生成器函数的话用 co 封装 => 是 async 函数的话直接放入中间件数组中 => 如果是普通函数的话,1.X 版本会报错,2.X 版本可以执行,但是由于没有 next,只能执行第一个
koa 的中间件机制巧妙的运用了闭包和 async await 的特点,形成了一个洋葱式的流程,和 JS 的事件流 (捕获 -> target -> 冒泡) 相似
const koa = require("koa"); const app = new koa(); app.use(function 1(){}) //use 的作用就是把中间件函数依次放入 ctx.middleware 中,等待请求到来的时候顺序调用 app.listen(port,function(){}) //封装原生的 node sever 监听 //封装 koa.callback(),并且为这个服务器设置了 node 的 request 事件,这意味着当每一个请求到来时就会执行 koa.callback() 方法,这是极为关键的一步,是 koa 中间件原理的基础
普通函数采用 dispatch 算法也能取得洋葱式的流程,为何要使用 async ?
因为next()采用的异步算法。
为何要用 Promise.resolve 返回
因为他是洋葱式的层级,如果用普通的 Boolean 返回的话,只能返回到上一层,没法全局获取,对错误的把控难以控制。Promise 任何一层报错,都能用 catch 捕获
https://www.ucloud.cn/yun/94307.html
过期闭包就是闭包中的变量获取的是过期的取值。解决过期闭包最好的方法就是在useEffect中合理管理依赖变量,或者是在useState中使用函数更新状态。 当然,解决过期闭包最关键的一点就是保证闭包中的变量能够及时获取最新的数值。
一个最简单的 case 就是一个组件依赖了父组件的 callback,同时内部 useffect 依赖了这个 callback,每次 Parent 重渲染都会生成一个新的 fetchData,因为 fetchData 是 Child 的 useEffect 的 dep,每次 fetchData 变动都会导致子组件重新触发 effect,一方面这会导致性能问题,假如 effect 不是幂等的这也会导致业务问题(如果在 effect 里上报埋点怎么办)
解决思路1:不再 useEffect 里监听 fetchData: 导致 stale closure 问题 和页面 UI 不一致。此时一方面父组件 query 更新,但是子组件的搜索并未更新但是子组件的 query 显示却更新了,这导致了子组件的 UI 不一致。
解决思路2:在思路 1 的基础上加强刷 token
解决思路3:useCallback 包裹 fetchData, 这实际上是把 effect 强刷的控制逻辑从 callee 转移到了 caller
解决思路4:使用 useEventCallback 作为逃生舱,
解决思路5:拥抱 mutable,实际上这种做法就是放弃 react 的快照功能(变相放弃了 concurrent mode ),达到类似 vue3 的编码风格。实际上我们发现 hook + mobx === vue3, vue3 后期的 api 实际上能用 mobx + hook 进行模拟。
解决思路6:useReducer 这也是官方推荐的较为正统的做法我们仔细看看我们的代码,parent 里的 fetchData 为什么每次都改变,因为我们父组件每次 render 都会生成新的函数,为什每次都会生成新的函数,我们依赖了 query 导致没法提取到组件外,除了使用 useCallback 我们还可以将 fetchData 的逻辑移动至 useReducer 里。因为 useReducer 返回的 dispatch 永远是不变的,我们只需要将 dispatch 传递给子组件即可,然而 react 的 useReducer 并没有内置对异步的处理,所以需要我们自行封装处理, 幸好有一些社区封装可以直接拿来使用,比如 zustand, 这也是我目前觉得较好的方案,尤其是 callback 依赖了多个状态的时候。
https://juejin.cn/post/6916792895055855623
https://juejin.cn/post/7083481223384793096
<div> <!-- 小图与遮罩 --> <div id="small"> <img src="images/189602.jpg" class="small-img" alt="" > <div id="mark"></div> </div> <!-- 等比例放大的大图 --> <div id="big"> <img src="images/189602.jpg" alt="" id="bigimg"> </div> </div>
window.addEventListener("load", function() { // 获取小图和遮罩、大图、大盒子 var small = document.getElementById("small") var mark = document.getElementById("mark") var big = document.getElementById("big") var bigimg = document.getElementById("bigimg") // 在小图区域内获取鼠标移动事件;遮罩跟随鼠标移动 small.onmousemove = function (e) { // 得到遮罩相对于小图的偏移量(鼠标所在坐标-小图相对于body的偏移-遮罩本身宽度或高度的一半) var s_left = e.pageX - mark.offsetWidth / 2 - small.offsetLeft var s_top = e.pageY - mark.offsetHeight / 2 - small.offsetTop // 遮罩仅可以在小图内移动,所以需要计算遮罩偏移量的临界值(相对于小图的值) var max_left = small.offsetWidth - mark.offsetWidth; var max_top = small.offsetHeight - mark.offsetHeight; // 遮罩移动右侧大图也跟随移动(遮罩每移动1px,图片需要向相反对的方向移动n倍的距离) var n = big.offsetWidth / mark.offsetWidth // 遮罩跟随鼠标移动前判断:遮罩相对于小图的偏移量不能超出范围,超出范围要重新赋值(临界值在上边已经计算完成:max_left和max_top) // 判断水平边界 if (s_left < 0) { s_left = 0 } else if (s_left > max_left) { s_left = max_left } //判断垂直边界 if (s_top < 0) { s_top = 0 } else if (s_top > max_top) { s_top = max_top } // 给遮罩left和top赋值(动态的?因为e.pageX和e.pageY为变化的量),动起来! mark.style.left = s_left + "px"; mark.style.top = s_top + "px"; // 计算大图移动的距离 var levelx = -n * s_left; var verticaly = -n * s_top; // 让图片动起来 bigimg.style.left = levelx + "px"; bigimg.style.top = verticaly + "px"; } // 鼠标移入小图内才会显示遮罩和跟随移动样式,移出小图后消失 small.onmouseenter = function () { mark.style.display = "block" big.style.display= "block" } small.onmouseleave = function () { mark.style.display = "none" big.style.display= "none" } })
* { margin: 0; padding: 0; } #small { width: 500px; height: 320px; float: left; position: relative; } #big { /* background-color: seagreen; */ width: 768px; height: 768px; float: left; /* 超出取景框的部分隐藏 */ overflow: hidden; margin-left: 20px; position: relative; display: none; } #bigimg { /* width: 864px; */ position: absolute; left: 0; top: 0; } #mark { width: 220px; height: 220px; background-color: #fff; opacity: .5; position: absolute; left: 0; top: 0; /* 鼠标箭头样式 */ cursor: move; display: none; } .small-img { width: 100%; height:100%; }
RTSP和HLS的特点
RTSP,是目前三大流媒体协议之一,即实时流传输协议。它本身并不传输数据,传输数据的动作可以让UDP/TCP协议完成,而且RTSP可以选择基于RTP协议传输。RTSP对流媒体提供了诸如暂停,快进等控制,它不仅提供了对于视频流的控制还定义了流格式,如TS、 mp4 格式。最大的特点除了控制视频操作外还具有低延时的特点,通常可实现毫秒级的延时,但是也存在一些弊端,如该视频流技术实现复杂,而且对浏览器很挑剔,且flash插件播不了,这也极大的限制了它的发展。
HLS,由苹果公司提出,它是基于Http的流媒体网络传输协议,主要传输TS格式流,最大的特点是安卓、苹果都能兼容,通用性强,而且码流切换流畅,满足不同网络、不同画质的用户播放需要,但是因为该种视频流协议也存在较为致命的缺陷,那就是网络延时太高。本质上HLS视频流传输是将整个视频流分成一个个小切片,可理解为切土豆片,这些小片都是基于HTTP文件来下载——先下载,后观看。用户观看视频实际上是下载这些小的视频切片,每次只下载一些,苹果官方建议是请求到3个片之后才开始播放,若是直播,时延将超10秒,所以比较适合于点播。因此HLS视频的切片一般建议10s,时间间隔太短就切容易造成碎片化太严重不方便数据存储和处理,太长容易造成时延加重。
前端加载RTSP视频流:
//<video src="" poster=""></video> autoplay:视频会马上自动开始播放,不会停下来等着数据载入结束 autobuffer(preload):视频会自动开始缓存 crossorigin:该枚举属性指明抓取相关图片是否必须用到CORS。不加这个属性时,抓取资源不会走CORS请求(即,不会发送 Origin: HTTP 头),保证其在 <canvas> 元素中使用时不会被污染。 width|height
RTSP视频流:
1 rtsp2web 是一个依赖 ffmpeg,能实时将传入的 rtsp 视频流转码成图像数据并通过 ws 推送到前端的智能工具包。 优点: 高性能,配置丰富。 并发,支持同时播放多路视频。 合并同源,多个视频窗口同时播放同一个rtsp视频源时,只会创建一个转码进程,不会创建多个。 智能释放资源,智能检测当前没有在使用的转码进程,将其关闭,并释放电脑资源。 2 将RTSP视频流在后端进行转码通过IPB视频压缩进行传输(base64),然后通过websocket进行发送,要使用这种帧间压缩技术,通常你需要选择一个具有这种功能的视频编码器。例如,H.264(AVC)、H.265(HEVC)、VP8、VP9和AV1等,这些都是支持这种压缩技术的视频编码器。使用这些编码器进行视频压缩的过程通常是由具体的编码库(例如FFmpeg)或者工具(例如HandBrake)实现的。 1. I帧也被称为关键帧,这是一个独立的帧,不依赖于任何其他帧进行解码。I帧包含了一帧完整的图像数据,就像一张静态的图片。视频播放时,通常从最近的I帧开始。由于I帧包含完整的图像数据,所以它的大小一般比P帧和B帧要大。但是,I帧的存在使得视频具有了“随机访问”(seek)的功能,也就是说,你可以任意跳转到视频的任何位置开始播放。 2.P帧依赖于前面最近的I帧或者P帧进行解码。P帧只存储与前一帧的差异信息,而不是完整的图像信息。这使得P帧的大小通常小于I帧。 3. B帧既依赖于前面的帧(I帧或P帧),也依赖于后面的帧(I帧或P帧)进行解码。B帧只存储与前后帧的差异信息,所以它的大小通常是这三种帧类型中最小的。然而,由于B帧依赖于后面的帧,所以在处理B帧时需要更多的计算资源。
除了Base64,传输视频数据的其他一些常见方式包括:
这些方式各有优点和适用的场景,你可以根据你的具体需求选择合适的方式。
点云数据进行播放:
点云数据的实时播放在前端通常通过WebGL库来实现,比如Three.js或者Potree。
<template> <div id="canvas-container"></div> </template> <script> import * as THREE from 'three'; export default { mounted() { this.initThree(); }, methods: { //进行初始化 initThree() { }, // 创建xyz点云 createPointCloud() { const geometry = new THREE.BufferGeometry(); }, // 进行动画渲染 animate() { requestAnimationFrame(this.animate); this.renderer.render(this.scene, this.camera); }, }, }; </script>
点云数据前端渲染,和rtsp帧同步
在前端进行实时播放点云数据和RTSP流数据可能会面临几个挑战:
如果你发现大量的微任务阻塞了DOM的渲染,下面有一些可能的解决方案:
如果你的应用突然收到了大量的数据并且导致了卡顿,这里有一些可能的解决方案:
当你在HTML的 <img>
标签中使用URL(通常是HTTP或HTTPS URL)来指定图片时,浏览器将会发起一个新的HTTP或HTTPS请求到该URL以获取图片数据。
这个过程大致如下:
<img>
标签并读取 src
属性。src
URL。这个请求会被浏览器的网络堆栈处理,通过DNS查找,TCP连接,发送HTTP请求,接收HTTP响应,到最后的数据接收。image/jpeg
, image/png
或者其他图片格式。alt
文本),以及错误处理(例如如果图片加载失败,显示一个错误标记)。 所以,使用URL来渲染图片实际上是浏览器内部进行了一系列的网络请求和图片解码处理。
解析图片数据和将图片渲染到屏幕上都可能成为性能瓶颈,这取决于许多因素,包括图片的大小、解码效率、渲染性能、设备硬件性能等。因此,很难做出一个泛泛的判断。以下是一些可能影响性能的因素:
在现代浏览器和设备上,浏览器通常会优化图片的下载和解码过程,并尽可能地使用GPU来加速渲染。然而,在低性能的设备或者网络条件差的情况下,用户可能会感受到图片加载的延迟。
为了提高性能和用户体验,开发者可以采取一些优化策略,例如使用更高效的图片格式(如WebP),提供适应不同网络条件和设备性能的响应式图片,使用图片懒加载等技术。
1 使用Pinia的坑
// 使用Pinia时,不可以使用解构赋值。 import { useCategoryStore } from '@/stores/category'; const CategoryStore = useCategoryStore() const { getCategory, categoryList} = useCategoryStore() 这是由于pinia在使用解构赋值时会丢失响应信息,此时的axios由于是异步发送会有延迟。导致axios接收后不会去更新这个解构完的pinia了。可以通过 Pinia 提供的响应式方法,storeToRefs 来处理。 storeToRefs接收一个store参数。 //首先判断是否为vue2环境,如果是vue2环境,直接使用toRefs将store转换为一个普通对象;如果不是vue2环境,首先获取store的原始对象,然后遍历原始对象的键值,在遍历过程中,只会处理ref(ref类型的值包括store中的state与getter,getter会被转为计算属性)与reactive类型的值,对于符合条件的值,会将这些值转为ref类型的值,然后将其复制到一个新的对象中refs中,最后返回refs。 https://juejin.cn/post/7088709186766241822
2 Router缓存
//使用带有参数的路由时需要注意的是,当用户从 /users/johnny 导航到 /users/jolyne 时,相同的组件实例将被重复使用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会被调用。 解决思路: 1.让组件实例不复用,强制销毁重建 (给Routerview :key="$router.fullpath") 2.监听路由变化,变化后执行数据的更新
https://blog.csdn.net/liusuyun_1/article/details/123956581 react项目之菜单,按钮权限的实现方案
https://juejin.cn/post/7097914569179267102 React中基于Router-V6实现配置式路由与路由鉴权详细过程
1 购物车流程&&sku
封装carStore(state + action) --> 组件点击添加按钮 --> 是否选择了规格(是否有sku.id) --> 调用action添加(通过匹配skuId找到了就是添加过)
sku组件的作用是为了让用户能够选择商品的规格,在选择的过程中,组件的选中状态要进行更新,还要提示用户当前规格是否禁用,每次选择都要产出对应的Sku数据。
通过axios请求后端返回数据,在specs属性中得到[{},...],进行双重遍历
绑定@cllick,给每个规格项添加selected决定是否激活,配合动态class属性(:class="{}")激活对应类名
首先利用全连接生成有效路径字典(方便查看库存) --> 删除库存为0的字段 --> 通过powerSet子集算法得到所有的子集 --> 通过arr.join('-')将数组转为字符串作为对象的key --> 根据子集生成路径字典对象。
初始化规格禁用,遍历每一个规格对象,使用name作为key和路径字典进行匹配,匹配不上则禁用。
点击规格更新禁用状态,首先按顺序得到规格项中选择的数组,将name进行填充,过滤undefined使用join得到有效的key,和路径字典匹配。
通过已选择的Sku的数组进行判断,然后通过key进行匹配。通过通信传给父组件。
2 合并前后端购物车
登陆时调用合并购物车接口-->获取最新的购物车列表-->覆盖本地购物车列表
3 支付功能
客户端通过get请求跳转支付地址(订单id+回跳地址url) --> 后端请求支付响应结果 --> 跳转到回跳地址url(参数支付成功和订单id)
绑定地址需要encodeURIComponent(uriComponent) 接收 string、number、boolean、null,undefined 或者任何 object。在编码之前,uriComponent 会被转化为字符串。
Web Workers 可以让你在后台线程中运行 JavaScript,这样就不会阻塞主线程,从而提升页面性能。然而,Web Workers 主要用于执行复杂和耗时的计算任务,如大量数据的处理和计算。
至于图片渲染,它是由浏览器的渲染引擎负责的,通常在主线程上执行,并且不能在 Web Worker 中完成。浏览器会使用 GPU 加速来提升渲染性能。
但是,在某些情况下,Web Workers 可以帮助提升图片处理的性能:
因此,Web Workers 不直接加速图片的渲染,但它们可以在处理和准备图片数据时提供帮助,这样可以减少主线程的负载,从而提高总体性能。
//worker.js self.onmessage = async function(event) { const { id, buffer } = event.data; try { const imageBitmap = await createImageBitmap(new Blob([buffer])); postMessage({ id, imageBitmap }, [imageBitmap]); } catch (error) { console.error('Error in worker:', error); postMessage({ id, error }); } };
//main.js // 创建 worker const worker = new Worker('imageWorker.js'); worker.onmessage = function(event) { const { id, imageBitmap, error } = event.data; if (error) { console.error('Error from worker:', error); return; } const imgElement = document.getElementById(id); imgElement.src = URL.createObjectURL(imageBitmap); }; // 假设我们有一个包含图像数据的 ArrayBuffer const imageArrayBuffer = ...; // 发送图像数据到 worker 进行解码 worker.postMessage({ id: 'myImage', buffer: imageArrayBuffer });
注意,这个例子需要在支持 createImageBitmap
和 Blob
API 的浏览器环境中运行。另外,因为跨域问题,Web Worker 只能在服务器环境中使用,不能直接在本地文件系统中使用。
在这个例子中,我们创建了一个 Web Worker,并使用 postMessage
将图像数据发送到 Worker。Worker 接收到数据后,使用 createImageBitmap
进行解码,并将解码后的 ImageBitmap 对象发送回主线程。主线程接收到 ImageBitmap 对象后,将其转换为 Blob URL,并设置为图像元素的 src
属性,从而显示图像。
URL.createObjectURL()
静态方法会创建一个 DOMString
,其中包含一个表示参数中给出的对象的URL。这个 URL 的生命周期和创建它的窗口中的 document
绑定。这个新的URL 对象表示指定的 File
对象或 Blob
对象。
注:
简单的理解一下就是将一个file
或Blob
类型的对象转为UTF-16
的字符串,并保存在当前操作的document
下。扩展1:UTF-8
与UTF-16
与GBK
到底有啥区别, 都是可变长度的编码方式通过对Unicode
码值进行对应规则转换后,编码保持到内存/文件中
在每次调用 createObjectURL()
方法时,都会创建一个新的 URL 对象,即使你已经用相同的对象作为参数创建过。当不再需要这些 URL 对象时,每个对象必须通过调用 URL.revokeObjectURL()
方法来释放。浏览器在 document 卸载的时候,会自动释放它们,但是为了获得最佳性能和内存使用状况,你应该在安全的时机主动释放掉它们。
SharedWorker
WebWorker只属于某个页面,不会和其他页面的Render进程(浏览器内核进程)共享,所以Chrome在Render进程中(每一个Tab页就是一个render进程)创建一个新的线程来运行Worker中的JavaScript程序。
SharedWorker是浏览器所有页面共享的,不能采用与Worker同样的方式实现,因为它不隶属于某个Render进程,可以为多个Render进程共享使用,所以Chrome浏览器为SharedWorker单独创建一个进程来运行JavaScript程序,在浏览器中每个相同的JavaScript只存在一个SharedWorker进程,不管它被创建多少次。
看到这里,应该就很容易明白了,本质上就是进程和线程的区别。SharedWorker由独立的进程管理,WebWorker只是属于render进程下的一个线程
在Web前端上传文件的时候,通常采用的是HTTP协议来传输文件数据。这个过程一般可以分为以下步骤:
在具体的实现中,可能会使用到各种前端库和框架来简化这个过程,比如jQuery, Axios等。但是这个基本的过程是不变的。
axios
是一个基于Promise的HTTP库,可以用于浏览器和node.js中。以下是一个使用axios在浏览器中上传文件的例子:
let fileInput = document.querySelector('input[type="file"]'); let file = fileInput.files[0]; let formData = new FormData(); formData.append('file', file); axios.post('/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' } }) .then(response => { console.log(response); }) .catch(error => { console.error(error); });
在这个例子中,我们首先获取用户选择的文件,然后创建一个FormData
对象,并将文件添加到这个对象中。之后,我们使用axios.post
方法向服务器发送一个POST请求,请求的URL是/upload
,请求的数据是我们刚才创建的FormData
对象。
axios.post
方法返回一个Promise对象,这个对象代表了服务器的响应。当服务器的响应到达时,我们使用.then
方法来处理这个响应。如果在发送请求或处理响应时发生了错误,我们使用.catch
方法来处理这个错误。
axios
接收文件与上传文件的流程有一些不同。以下是一个使用axios
在浏览器中接收文件的例子:
axios({ url: '/download', method: 'GET', responseType: 'blob', // 注意这里的响应类型是 'blob',还有 base64 (利用 fileReader 而不是 formData) }) .then(response => { // 创建一个新的 Blob 对象 const blob = new Blob([response.data], { type: response.headers['content-type'] }); // 创建一个链接 let downloadUrl = window.URL.createObjectURL(blob); // 创建一个隐藏的 'a' 标签,设置其链接并触发点击事件,从而下载文件 let link = document.createElement('a'); link.href = downloadUrl; link.setAttribute('download', 'file'); document.body.appendChild(link); link.click(); link.remove(); }) .catch(error => { console.error(error); });
在这个例子中,我们发送了一个GET请求到/download
来获取文件。注意我们设置responseType
为blob
,这样axios
会将响应的数据处理为一个Blob对象,我们可以将其直接用于文件下载。
服务器响应到达后,我们创建一个新的Blob对象,然后使用window.URL.createObjectURL
方法将这个Blob对象转化为一个URL。然后我们创建一个新的a
元素,将这个URL设置为其href
属性,然后模拟用户点击这个a
元素,从而开始文件下载。
请注意,这是一个简化的例子,实际使用中可能需要处理一些额外的问题,例如错误处理和兼容性问题。
大文件上传
断点续传
后端
刷新页面
如果你希望在前端刷新页面时保留已上传文件的状态,你可以尝试以下几种方法:
请注意,以上方法在某些情况下可能不适用,例如在浏览器的隐私模式下或用户禁用了Cookies等。因此,需要根据你的具体需求和环境来选择最适合的方法。
有关刷新页面续传,其实没必要把文件内容存到浏览器storage里,只需要根据每一片slice切片大小计算出每片在文件流当中的偏移量,即可在每次上传时按这个偏移量读取相应分片内容。设计好上传本身各个动作形成的这个状态机,只需要在状态机里维护好当前上传到第几个分片即可,没必要记住分片内容,因为内容本身就存在磁盘上了。另外,上传的话最好先做md5校验,确认这个文件确实是你上次上传到一半的那个。刷新页面后,用户可以根据之前的操作状态提示用户重新选择文件,文件名提示一下这个文件是可以支持断点续传的,引导用户选中,然后按照刷新页面前最后一次的上传分片偏移量去做续传就好,上传完成后服务端校验分片重组后文件的完整性。(但是根据H5的标准,在没有用户操作的前提下浏览器环境的js是不可能主动去访问磁盘上的文件的)
**客户端标识:**也就是 UUID,这是贯穿整个流程的纽带,一个闭环登录过程,每一步业务处理都是围绕该次的 UUD 进行处理的。UUID 的生成有根据 session_id 的也有根据客户端 ip 地址的。个人还是建议每个二维码都有单独的 UUID,适用场景更广一些!
**前端和服务器通讯:**前端肯定是要和服务器保持一直通讯的,用以获取登录结果和二维码状态。看了下网上的一些实现方案,基本各个方案都有用的:轮询、长轮询、长链接、websocket。也不能肯定的说哪个方案好哪个方案不好,只能说哪个方案更适用于当前应用场景。个人比较建议使用长轮询、websocket 这种比较节省服务器性能的方案。
总结下核心流程:
https://juejin.cn/post/7056544865647067172
时间切片
利用requestAnimationFrame(),原理是由于它会在浏览器渲染完去执行。在执行完一次会等待浏览器渲染完继续执行。
虚拟列表(只渲染当前可视区域)
item 高度固定
自定义一个组件,传递 3 个值给 VirtualList 组件: size:每一项的高度 keeps:希望展示几条数据 arrayData:列表数据 VirtualList 组件里得有 3 个部分: 最外层容器区域。高度固定,超出区域出现滚动条,高度为传入的 size 乘上 keeps; 列表本应该有的高度区域,也就是列表如果全部渲染的总高度。因为只渲染 keeps 指定的条数的数据,就会导致没有滚动条或滚动条无法起到预告总的列表长度的功能,所以要用一个高度为列表总长度的 div 让滚动条正确显示; 要展示的内容。展示的数据应该是总数据 arrayData 的某一部分。展示的数据 item 还得传给父组件,在父组件进行使用,这里就用到了插槽。 当滚动列表时(handleScroll 触发),我们要及时的根据滚动的距离更新应该显示的数据: onscroll 处理的是对象内部内容区的滚动事件,所以是对最外部固定高度的 wrap 容器进行监听。 如下图所示:蓝色矩形为可视区域,假设传入的 keeps 为 3 ,当滚动列表(红色矩形)时,渲染的列表区域,也就是 3 个 item(深蓝绿色矩形) 占据的区域也会跟着滚动,如果仅仅改变渲染的内容,也就是根据滚动距离从 item1 开始渲染,那么此时这个 item1 就会替换下图的 item0 ,位于可视区域之外,无法被看见。
item 高度不固定
原来对于列表如果全部渲染应该有的高度的计算 this.arrayData.length * this.size + 'px' 显然不合适了,因为每项 item 的高度 size 不确定了。 [ 在二分法中,计算中间项的索引时用的是 mid = start + (end - start) / 2 而不是直接使用更简单的公式 mid = (start + end) / 2,是为了防止值溢出的情况,因为 start + end 的值可能会大于 js 最大的能表示的数。(如果 start < 0 或 end < 0时,end - start 也可能会溢出)] 在页面加载完毕后,对数据数组里每一项的 height, top 和 bottom 的值做个缓存(此时的 size 为我们预估的,滚动条的高度并不准确),存放在数组 positionListArr 里; 用二分法开始查找,我们页面滚动的距离 scrollTop 对应于 positionListArr 里的哪一项的 bottom 的值。之所以用二分法是因为后面会根据真实 dom 重行计算每一项的 height, top 和 bottom,到时候每一项的 size 就可能不一样了; 之后对于 end 和 offset 计算原理就跟 item 高度固定的情况一样了。
利用 css
最近看到一个新的 css 属性 content-visibility
,利用这个属性(大概率还得配合 contain-intrinsic-size
)就可以实现只渲染当前可视窗口区域内的内容,跳过不在屏幕上的内容渲染。但目前兼容性极差,只能说未来可期~
https://juejin.cn/post/6979865534166728711
利用html的contentEditable
属性,可以让我们对任意网页上的标签内容进行编辑。PS:对禁止复制文字或者需要登录才能复制文字的网页,用这招即可完美破解:
这是浏览器的一个api,当元素进入编辑模式的时候,document 对象暴露出一个 execCommand 方法去操纵当前的可编辑区域 。
打开一个有文字的网页,先让页面变成可编辑,控制台运行document.body.contentEditable = true 然后选中页面一些文字,控制台运行document.execCommand('bold', false),即可使选中区域加粗
https://juejin.cn/post/7018355368996634638
指在多系统应用群中登录一个系统,便可在其他所有系统中得到授权而无需再次登录,包括单点登录与单点注销两部分。
假如公司有两个不同域名或IP下的管理网站,http://a.com,http://b.com,我们想要输入用户名和密码登录a.com, 即可自动登录http://b.com,怎么实现呢?
登录http://a.com后会在a平台产生会话信息,如果我们登录b.com,怎么判断该用户已经登录a平台了,然后自己登录?这里我们需要借助第三方平台来做授权验证,即c.com,该授权中心维护一套共有的账号和密码,当用户访问 http://a.com 后,我们会跳转到 c.com,并带上http://a.com 这个来源访问地址,在http://c.com 中判断是否有登录,如果未登录,则给出登录界面,登录成功后,则产生会话信息,同时生成一个授权Token,保存在http://c.com cookie 中,然后返回给原地址,如果已经登录(即已经授权过了),则拿到cookie中保存的Token信息然后回跳到原来的访问地址,http://a.com 判断有Token信息,则拿着这个Token发送http请求到c.com,判断该Token是否有效,如果有效,则返回给用户信息,然后http://a.com拿到信息后,自动登录a.com;
当访问 http://b.com 时,也会去 http://c.com 中判断是否已经授权过了,因为http://a.com 已经授过权了,则会给http://b.com 返回授权Token,然后http://b.com 判断该Token是否有效,如果有效,则会返回给用户信息,http://b.com 自动登录,这就是单点登录的一个简单的流程。
用户登录成功之后,会与sso认证中心及访问的子系统建立会话,用户与sso认证中心建立的会话称为全局会话,用户与各个子系统建立的会话称为局部会话,局部会话建立之后,用户访问子系统受保护资源将不再通过sso认证中心,全局会话与局部会话有如下约束关系
https://juejin.cn/post/7044328327762411534
优点:
缺点:
模块之间如何进行解耦呢:
除了全局变量,我能在组件里面维护自己的状态吗?当然是可以的,我们可以新增一个 state 属性,来维护组件自身的一些状态,并且我们可以通过维护原型链的方式来层层向上获取父元素数据,比如这样:state.__proto__ = parent.state
**怎么支持跨平台:**低代码平台搭建好后最终得到的是一个 json,而这个 json 本身就是以一种通用的语言来描述页面结构、表现和行为的,它和平台无关。要想跨平台、跨框架,只需要做一个适配层去解析这些 json,导出成各平台、各框架需要的样子即可。举个例子来说,我们在解析的过程中肯定会需要创建元素,而 vue 和 react 都有各自的 createElement 方法,那到时候只需要把创建元素的方法换成各框架各自的方法就行了。思路听起来很简单,但是每一种适配起来都很繁琐
实现:
通常情况下,低码平台自身会有个物料管理平台对组件进行统一的管理和操作,简单点做的话我们可以直接把开发好的组件发到 npm 上,把 npm 当做物料平台用。
分类:
事实上,低码平台都秉持着数据驱动视图的思想(和我们现在用的 vue 和 react 框架如出一辙),也是通过递归解析 componentTree 这个全局组件树来动态生成页面。通过转换器、渲染器或者 render 函数,通常开发完成之后,这个渲染器是不用改的,我们只需要单纯的修改数据,渲染器自然会帮我们解析。
这个思想很重要,也是解耦的核心:就是我们所有的操作,不管拖拽也好,修改元素属性也好,还是调整元素位置,都是对 componentTree 这个数据进行修改,单纯的对数据进行操作,比如追加元素,就往 componentTree 的 children 里面 push 一个元素即可;如果要修改一个元素的属性值,只需要找到对应元素的数据修改其 props 值即可。画布编排的本质就是操作组件节点和属性。
另外为了让每个组件都能直接获取到这个 componentTree,我们可以把这个 componentTree 弄成全局的(全局数据能够让整体流程更加清晰),比如放在 window、vuex、redux 上,这样每个模块就能共享同一份数据,也能随时随地更改同一份数据(平台会暴露公共的修改方法),而这个渲染器只是单纯的根据这个数据来渲染,并不处理其他事情。
这里我们还要注意一个问题,就是画布区本身也是个组件,是个组件那它就会受到父元素和全局的影响,最简单的比如样式,可能受到外部样式作用,导致你这个画布区和最终呈现的页面可能有一丢丢的不同,所以要排除这些影响,就是把这个画布区搞成一个独立的 iframe,这样环境就比较纯了,完美隔离,只不过增加了通信的成本。现在,物料区只负责渲染组件列表,以及触发拖拽放下的事件,之后就是触发修改全局的 componentTree,再之后就是触发画布区的重新渲染。这样一来,你就会发现画布区和物料区就很好的解耦了。到目前为止画布区只负责单纯的渲染。
流程:
首先如果我们修改了属性设置区的表单项,我们实际上是去修改全局的 componentTree,然后画布区自然就会根据这个新的 componentTree 自动渲染,有点单向数据流的意思(就是修改数据的入口只有一个),也方便排查问题。把数据放到全局上,很多通信的过程就可以省掉了
一个常常提到的问题就是如何实现联动,比如字段 2 的显隐依赖于字段 1 的值,类似这种功能通常有两种实现方式:
{{ globalData.field1 && ... && globalData.fieldN }}
,因为数据是全局的所以很方便能够直接获取到,在实际渲染的过程中就会动态执行上面那个表达式来确定组件渲不渲染。此外,因为数据是全局的,跨组件或者跨页面共享数据也会变得轻而易举 再一个常常提到的问题就是如何处理点击事件?如果做的开放点、简单点,我们可以直接让用户自己写函数,然后运行的时候用 eval
或者 new Function
执行一下就行。但是这样会有个问题,就是安全性、稳定性和效率不够,所以我们需要进行一些限制,这个通常有两种方法:
一种是暴露固定方法,只接收参数,比如我这个点击的结果就是跳转到某个页面,也就是执行 window.open
这个方法,那我们就不允许用户直接书写这个代码,而是先内置一个全局封装好的 jumpToPage(url)
方法,然后在属性设置的时候只允许输入 url 并进行简单校验
但是固定方法是很难满足我们的一些需求的,最终还是得支持让用户可以自己写脚本,于是乎我们就得让这个脚本具有良好的隔离性,也就是沙箱或者对代码进行校验等,这里就简单说一下沙箱的方式:
网易云低代码平台
Tango 低代码引擎不依赖私有搭建协议和 DSL,而是直接使用源代码驱动,引擎内部将源码转为 AST,用户的所有的搭建操作转为对 AST 的遍历和修改,进而将 AST 重新生成为代码,将代码同步给在线沙箱执行。与传统的 基于 Schema 驱动的低代码方案 相比,不受私有 DSL(领域特定语言,如 sql、.vue等) 和协议的限制,能够完美的实现低代码搭建与源码开发的无缝集成。
https://juejin.cn/post/7276837017231835136
https://mp.weixin.qq.com/s?__biz=Mzg4MjE5OTI4Mw==&mid=2247491681&idx=1&sn=387027420642ca4043f8bfd920d0badf&scene=21#wechat_redirect
https://mp.weixin.qq.com/s/pPBJPAyv5e8H4OFAHIiFWQ
https://juejin.cn/post/7202877900239077432 沙箱隔离
https://cloud.tencent.com/developer/article/1965378
微前端架构具备以下几个核心价值:
qiankun特点
使用:
bootstrap
、mount
、unmount
三个生命周期钩子,以供主应用在适当的时机调用。https://qiankun.umijs.org/zh/guide/getting-started
BFF是一种Web架构,全名为Backends For Frontends,即为服务于前端的后端。
问题提出: 在系统一开始开发的时候只考虑了PC网页端的设计,服务器端API是为了PC网页端而服务的。但是后来随着移动互联网的兴起,移动端开始流行,决定在原有服务端的基础上开发移动端App,复用之前的API,但是原有API是为了PC端设计的,并不符合移动端的需求。
BFF的优势: