前端难点(面经)
目录
9.1 多图加载方案
9.2 axios
9.3 Koa
9.4 React坑
9.4.1 过期闭包
9.4.2 父子引用函数
9.5 登录流程
9.6 图片放大镜
9.7 视频播放
9.7.1 图片渲染流程
9.8-些坑
9.9 鉴权
9.10 小免鲜
9.11 Web worker
9.12 文件上传
9.13 扫码登陆
9.14 列表优化
9.15 富文本编辑器
9.16 SSO
9.17 低代码
9.18 小程序
9.19 qiankun
9.18 BFF中间层
9.1 多图加载方案
在前端展示1000张图片时,如果一次性加载,可能会导致页面加载缓慢,用户体验下降。以下是一些可以应用的优化策略:
- 懒加载(Lazy Loading):只有当图片进入或即将进入视口时,才加载图片。这可以减少初始页面加载的时间。在最新的HTML标准中,可以直接使用loading="lazy"属性。
<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); });
- 图片压缩:对图片进行压缩可以减少图片的大小,从而减少加载时间。可以使用工具或库如imagemin,或者在服务器端进行图片压缩。
- 使用CDN:通过使用内容分发网络(CDN),可以将图片缓存到距离用户更近的服务器上,从而减少加载时间。
- 使用缩略图:如果图片的细节不重要,可以先加载缩略图,当用户点击时再加载完整的图片。
- 分页加载/无限滚动:不是一次性加载所有图片,而是当用户滚动到页面底部时,加载更多图片。可以使用分页(Pagination)或无限滚动(Infinity Scrolling)的方式来实现。
- 虚拟滚动。同懒加载,可以使用ElementUI和vue-virtual-scroller进行显示。
以下是一个简单的无限滚动的实现:
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防抖、空数据处理等。
9.2 axios
axios 是基于 http (基于 tcp 传输层)的网络请求库。
拦截器执行顺序:
请求的发送需要在请求拦截器之后,在响应拦截器之前,所以数组先放入request,接着在数组的前后分别加入请求和响应拦截器,由于加入请求拦截器的方法是unshift,所以最后通过promise进行请求的链式调用的时候,我们可以看到执行顺序是从左往右的,所以最后注册的请求拦截器会最先执行,而响应拦截的执行顺序和注册顺序是一样的。
fetch和axios区别:
- Axios可以兼容IE浏览器,而Fetch在IE浏览器和一些老版本浏览器上没有受到支持
- 传递数据的方式不同,Axios是放到
data
属性里,以对象的方式进行传递,而Fetch则是需要放在body
属性中,以字符串的方式进行传递 - Axios的相应超时设置是非常简单的,直接设置
timeout
属性就可以了,而fetch需要通过new AbortController()然后设置settimeout - Axios还有非常好的一点就是会自动对数据进行转化,而Fetch则不同,它需要使用者进行手动转化。
- Axios的一大卖点就是它提供了拦截器,可以统一对请求或响应进行一些处理,使用它可以为请求附加token、为请求增加时间戳防止请求缓存,以及拦截响应,一旦状态码不符合预期则直接将响应消息通过弹框的形式展示在界面上,比如密码错误、服务器内部错误、表单验证不通过等问题。而Fetch没有拦截器功能,但是要实现该功能并不难,直接重写全局Fetch方法就可以办到。
- Fetch唯一碾压Axios的一点就是现代浏览器的原生支持。
https://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 的所有功能,包括拦截器、转换函数、取消功能等。 - 合并配置参数: Axios 会将你提供的配置参数与默认配置合并。例如,如果你提供了一个 URL,但没有提供方法,那么 Axios 会使用默认的
get
方法。合并后的配置将被应用到新创建的axios
实例上。 - 创建一个新的 HTTP 请求: Axios 会使用配置中的 URL、方法、头部信息、数据等,创建一个新的 HTTP 请求。这个请求是由 XMLHttpRequest 对象(浏览器)或 http 模块(Node.js)创建的,这取决于你在哪个环境中使用 Axios。
- 发送 HTTP 请求: Axios 会将创建好的 HTTP 请求发送到服务器。发送请求的过程可能会触发请求拦截器,拦截器可以修改请求或添加额外的功能,例如日志记录。
- 处理响应: 当服务器返回响应时,Axios 会接收到这个响应。Axios 会解析响应的状态码、头部信息和数据,然后将其封装到一个新的 Promise 对象中。这个过程可能会触发响应拦截器,拦截器可以修改响应或添加额外的功能。
- 返回 Promise: Axios 的
method()
函数返回的是一个 Promise 对象。这个 Promise 代表了 HTTP 请求的结果,你可以使用.then
或.catch
来处理这个 Promise。如果请求成功,Promise 将被解析并返回响应数据;如果请求失败,Promise 将被拒绝并返回错误信息。
9.3 Koa
Express优点:线性逻辑,通过中间件形式把业务逻辑细分、简化,一个请求进来经过一系列中间件处理后再响应给用户,清晰明了。 缺点:基于 callback 组合业务逻辑,业务逻辑复杂时嵌套过多,异常捕获困难。
Koa优点:首先,借助 co 和 generator,很好地解决了异步流程控制和异常捕获问题。其次,Koa 把 Express 中内置的 router、view 等功能都移除了,使得框架本身更轻量。 缺点:社区相对较小
- express采取回调方式解决异步问题,koa采取promise方式解决异步问题
- express 内置许多中间件,koa只提供了核心代码,没有扩展其他中间件
- express中间件与koa中间件又差异
- express只能通过回调的方式处理错误,koa可以通过监听 on("error") 处理错误
- koa中请求与响应都扩展到了ctx上,express是直接对请求req与响应res进行扩展
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
9.4 React坑
9.4.1 过期闭包
过期闭包就是闭包中的变量获取的是过期的取值。解决过期闭包最好的方法就是在useEffect中合理管理依赖变量,或者是在useState中使用函数更新状态。 当然,解决过期闭包最关键的一点就是保证闭包中的变量能够及时获取最新的数值。
9.4.2 父子引用函数
一个最简单的 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
- 如果子组件的 effect 较多,需要建立 refreshToken 和 effect 的映射关系
- 触发 eslint-hook 的 warning,进一步的可能触发 eslint-hook 的 auto fix 功能,导致 bug
- fetchData 仍然可能获取的是旧的闭包?
解决思路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
9.5 登录流程
https://juejin.cn/post/7083481223384793096
9.6 图片放大镜
- 黄色的遮挡层跟随鼠标功能。
- 把鼠标坐标给遮挡层不合适。因为遮挡层坐标以父盒子为准。
- 首先是获得鼠标在盒子的坐标。
- 之后把数值给遮挡层做为left 和top值。
- 此时用到鼠标移动事件,但是还是在小图片盒子内移动。
- 发现,遮挡层位置不对,需要再减去盒子自身高度和宽度的一半。
- 遮挡层不能超出小图片盒子范围。
- 如果小于零,就把坐标设置为0
- 如果大于遮挡层最大的移动距离,就把坐标设置为最大的移动距离
- 遮挡层的最大移动距离:小图片盒子宽度 减去 遮挡层盒子宽度
<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%; }
9.7 视频播放
RTSP和HLS的特点
RTSP,是目前三大流媒体协议之一,即实时流传输协议。它本身并不传输数据,传输数据的动作可以让UDP/TCP协议完成,而且RTSP可以选择基于RTP协议传输。RTSP对流媒体提供了诸如暂停,快进等控制,它不仅提供了对于视频流的控制还定义了流格式,如TS、 mp4 格式。最大的特点除了控制视频操作外还具有低延时的特点,通常可实现毫秒级的延时,但是也存在一些弊端,如该视频流技术实现复杂,而且对浏览器很挑剔,且flash插件播不了,这也极大的限制了它的发展。
HLS,由苹果公司提出,它是基于Http的流媒体网络传输协议,主要传输TS格式流,最大的特点是安卓、苹果都能兼容,通用性强,而且码流切换流畅,满足不同网络、不同画质的用户播放需要,但是因为该种视频流协议也存在较为致命的缺陷,那就是网络延时太高。本质上HLS视频流传输是将整个视频流分成一个个小切片,可理解为切土豆片,这些小片都是基于HTTP文件来下载——先下载,后观看。用户观看视频实际上是下载这些小的视频切片,每次只下载一些,苹果官方建议是请求到3个片之后才开始播放,若是直播,时延将超10秒,所以比较适合于点播。因此HLS视频的切片一般建议10s,时间间隔太短就切容易造成碎片化太严重不方便数据存储和处理,太长容易造成时延加重。
前端加载RTSP视频流:
- 使用媒体服务器进行转码:你可以设置一个媒体服务器,如 Wowza、GStreamer、FFmpeg 或 Red5,将 RTSP 流转码为浏览器支持的流格式,如 HTTP Live Streaming(HLS)或者 MPEG-DASH。然后,你可以使用 video 标签或者一些 JavaScript 库(如 video.js、hls.js)在前端播放转码后的流。
- 使用 WebRTC:使用WebRTC播放RTSP视频流的过程中,你需要一个媒体服务器来做转码,比如使用GStreamer或者FFmpeg,将RTSP流转成WebRTC流。这需要后端的支持。在前端,你需要用到JavaScript的WebRTC API来播放视频。WebRTC并不是将视频流转换为图片流进行发送的。它处理的是压缩编码的音视频数据流,这是一种更有效的数据传输方式。具体来说,RTSP(实时流协议)视频流通常包含H.264或其他格式的编码视频数据。这些数据是连续的帧序列,每一帧都是图像的一部分,但并非直接的图像流。这些帧包括I帧(关键帧,包含完整的图像信息)和P帧(预测帧,只包含与前一帧的差异信息)。当你使用像GStreamer这样的媒体服务器接收RTSP流时,你是在接收这些编码的视频数据,而不是逐帧图像。然后,GStreamer可以将这些编码的数据重新打包为WebRTC可以理解的格式,然后通过WebRTC协议发送出去。
- 使用插件或者特定的浏览器:一些浏览器插件和特定的浏览器(如 VLC 插件、QuickTime 插件、IE 浏览器等)可以直接播放 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进行发送,要使
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
前端校招面经分享,包含CSS、JS、Vue、React、计算机网络、难点项目、手撕题等。这份面经总结了几乎大厂所有的面试题与牛客近几年公开的面经,可以说面试不会超出范围。 因为我只负责总结加一些个人见解,所以一开始免费开源。但互联网戾气真的重,很多人拿着面经还一副理所应当的样子质问我要语雀,还说网上同类的有很多??唉,分享不易,那我只好收费了233。当然也欢迎直接来找我要语雀,语雀会多一些内容。