2025年突然发现少了很多前端小伙伴,身边也有很多人陆续转行,但是行业竞争一点没变。
今天给一些已经入坑还想再试试的前端人整理一些经验。
首先 前端学习内容复杂、网上资料良莠不齐,想要靠自己梳理清楚确实不容易,为了帮助别管是目标大厂,还是目标中小厂的你通过前端面试,所以总结出这「 300 道前端工程师常考必考面试题+详细解答。于是我翻箱倒柜,把这份字节大牛总结的前端开发归纳笔记找出来。
传说有小伙伴靠这份笔记顺利进入 BAT 哦,所以一定要好好学习这份资料!做下一个靠这份笔记进入BAT的人。干巴dei
正文
怎么在前端页面中添加水印?
参考答案
在前端页面中添加水印可以通过以下几种方法实现:
1. 使用 CSS 实现
使用 CSS 伪元素和 background
属性来添加水印。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Watermark Example</title>
<style>
.watermarked {
position: relative;
}
.watermarked::before {
content: "Watermark";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) rotate(-30deg);
font-size: 3rem;
color: rgba(0, 0, 0, 0.1);
pointer-events: none;
z-index: 1000;
}
</style>
</head>
<body>
<div class="watermarked">
<!-- Your content here -->
<p>Some content with a watermark.</p>
</div>
</body>
</html>
2. 使用 Canvas
通过在 canvas
上绘制水印来实现。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas Watermark Example</title>
</head>
<body>
<canvas id="watermarkCanvas" width="600" height="400"></canvas>
<script>
const canvas = document.getElementById('watermarkCanvas');
const ctx = canvas.getContext('2d');
// Draw background
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw watermark text
ctx.font = '48px Arial';
ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.save();
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.rotate(-Math.PI / 6);
ctx.fillText('Watermark', 0, 0);
ctx.restore();
</script>
</body>
</html>
3. 使用 HTML 的 background
属性
将水印作为背景图像设置到页面的某个容器上。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Background Watermark Example</title>
<style>
.watermarked {
background-image: url('watermark.png');
background-repeat: no-repeat;
background-size: 200px 100px;
background-position: center;
}
</style>
</head>
<body>
<div class="watermarked">
<!-- Your content here -->
<p>Some content with a watermark.</p>
</div>
</body>
</html>
4. 使用 JavaScript 动态生成水印
通过 JavaScript 在 DOM 中添加水印元素。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dynamic Watermark Example</title>
<style>
.watermark {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 9999;
text-align: center;
font-size: 3rem;
color: rgba(0, 0, 0, 0.1);
transform: rotate(-30deg);
}
</style>
</head>
<body>
<div id="content">
<!-- Your content here -->
<p>Some content with a watermark.</p>
</div>
<script>
const watermark = document.createElement('div');
watermark.className = 'watermark';
watermark.textContent = 'Watermark';
document.body.appendChild(watermark);
</script>
</body>
</html>
如何封装一个请求,让其多次调用的时候,实际只发起一个请求的时候,返回同一份结果?
参考答案
封装一个请求使其在多次调用时只发起一次请求,并返回相同结果,通常是通过请求去重(debouncing)来实现的。这种功能对于避免重复的网络请求、提高性能和减少不必要的负载非常有用。
同时,我们需要确保在请求完成之前,对相同请求的重复调用都会共享相同的请求 Promise。避免出现连续发出相同的请求,在第一个请求尚未完成时,那么可能会发出多个请求的情况。
可以通过以下步骤来实现这个功能:
1. 使用一个缓存机制
我们可以使用 JavaScript 对象或 Map 来缓存已经发起的请求,并在 subsequent 请求中返回缓存的结果。缓存的关键是确保相同的请求参数对应同一个缓存条目。
2. 创建请求缓存封装
以下是一个基于 axios
的请求去重的封装示例:
import axios from 'axios';
// 请求缓存
const requestCache = new Map();
async function fetchData(url, params) {
// 生成缓存 key
const cacheKey = `${url}?${new URLSearchParams(params).toString()}`;
// 检查缓存中是否已有数据
if (requestCache.has(cacheKey)) {
return requestCache.get(cacheKey);
}
// 创建请求 Promise
const requestPromise = axios.get(url, { params })
.then(response => {
// 请求成功,存储结果
requestCache.delete(cacheKey); // 请求完成后,移除缓存
return response.data;
})
.catch(error => {
// 请求失败,清除缓存
requestCache.delete(cacheKey);
throw error;
});
// 存储请求 Promise
requestCache.set(cacheKey, requestPromise);
// 返回 Promise
return requestPromise;
}
export default fetchData;
注意事项:
- 缓存请求 Promise:每个请求的 Promise 被缓存到 requestCache 中。后续的相同请求会返回这个缓存的 Promise。
- 请求完成后移除缓存:请求成功或失败后,删除缓存,以防止缓存中的 Promise 长时间存在,避免内存泄漏。
- 请求失败处理:如果请求失败,清理缓存并抛出错误,以便后续调用可以重新发起请求。
3. 使用请求缓存
使用封装好的 fetchData
函数来发起请求。多次调用相同的请求 URL 和参数只会发起一次网络请求,并返回相同的结果。
import fetchData from './fetchData';
// 使用示例
fetchData('https://api.example.com/data', { id: 1 })
.then(data => console.log(data))
.catch(error => console.error(error));
// 再次调用相同的请求
fetchData('https://api.example.com/data', { id: 1 })
.then(data => console.log(data)) // 共享相同的请求结果
.catch(error => console.error(error));
如何解决页面请求接口大规模并发问题
参考答案
主要包括以下几种策略:
1. 使用防抖和节流
-
防抖(Debouncing) :在一段时间内只执行最后一次请求,适用于用户输入场景。
function debounce(func, delay) { let timer; return function (...args) { clearTimeout(timer); timer = setTimeout(() => func.apply(this, args), delay); }; }
-
节流(Throttling) :限制在一定时间内执行请求,适用于限制频繁的请求。
function throttle(func, limit) { let lastFunc; let lastRan; return function (...args) { const context = this; if (!lastRan) { func.apply(context, args); lastRan = Date.now(); } else { clearTimeout(lastFunc); lastFunc = setTimeout(function () { if ((Date.now() - lastRan) >= limit) { func.apply(context, args); lastRan = Date.now(); } }, limit - (Date.now() - lastRan)); } }; }
2. 请求合并
-
请求去重:避免同一请求被重复发起,通过设置请求唯一标识来防止重复请求。
const pendingRequests = new Map(); async function fetchData(url, options) { const key = `${url}_${JSON.stringify(options)}`; if (pendingRequests.has(key)) { return pendingRequests.get(key); } const requestPromise = fetch(url, options).finally(() => { pendingRequests.delete(key); }); pendingRequests.set(key, requestPromise); return requestPromise; }
-
批量请求:将多个请求合并为一个请求,通过接口支持批量请求功能来减少请求数量。
3. 后端处理
- 负载均衡:使用负载均衡器将请求分发到多个服务器,避免单台服务器过载。
- 缓存:在后端使用缓存(如 Redis)存储重复请求的数据,减少数据库压力。
- 限流:在后端实施限流策略,控制每秒处理的请求数量,防止系统过载。
4. 异步处理
-
并发控制:限制并发请求的数量,通过实现并发队列来控制请求的并发数。
class Queue { constructor(limit) { this.queue = []; this.activeCount = 0; this.limit = limit; } add(fn) { return new Promise((resolve, reject) => { this.queue.push(() => fn().then(resolve).catch(reject)); this.processQueue(); }); } processQueue() { if (this.activeCount < this.limit && this.queue.length > 0) { const fn = this.queue.shift(); this.activeCount++; fn().finally(() => { this.activeCount--; this.processQueue(); }); } } }
5. 前端缓存
- 使用浏览器缓存:设置缓存头(如
Cache-Control
和ETag
)来缓存请求结果,减少重复请求。 - 本地存储:使用
localStorage
或sessionStorage
来缓存数据,避免重复请求。
6. 使用 Web Workers
- Web Workers:将一些计算密集型的请求处理任务放到 Web Worker 中,避免阻塞主线程,提升用户体验。
前端单页应用 History 路由模式, 需要如何配置 nginx?
参考答案
前端单页应用使用 History 路由模式,需要将路由的控制交给前端,而不是后端处理,因此需要配置 nginx 让其将所有请求都指向 index.html 页面,让前端来处理路由。
具体配置可以参考以下 nginx 配置:
bashCopy codeserver {
listen 80;
server_name example.com; # 域名或 IP 地址
location / {
root /path/to/your/app; # 项目根目录
index index.html;
try_files $uri $uri/ /index.html; # 将所有请求都指向 index.html 页面
}
}
以上配置中,try_files
指令会尝试将请求映射到对应的文件,如果找不到,则会将请求转发到 /index.html
页面,由前端来处理路由。同时,root
指令需要指向前端应用的根目录。
web 网页如何禁止别人移除水印
参考答案
防止DOM被删除
为了防止水印被删除,可以利用 MutationObserver API
监听 DOM 变化。MutationObserver
可以监控 DOM 树的变化并触发回调函数。回调函数可以用于检测水印是否被移除,并采取相应的措施进行恢复。
以下是一个示例代码,演示了如何使用 MutationObserver
监听 DOM 变化并检测水印的删除:
// 目标节点
const targetNode = document.body;
// 创建 MutationObserver 实例
const observer = new MutationObserver(mutationsList => {
mutationsList.forEach(mutation => {
// 检查是否有子节点被删除
if (mutation.removedNodes.length > 0) {
// 检查被删除的节点是否为水印
// 如果是,则重新插入水印元素
// targetNode.appendChild(watermarkElement);
}
});
});
// 配置 MutationObserver
const config = { childList: true, subtree: true };
// 开始观察目标节点
observer.observe(targetNode, config);
在上述代码中,我们创建了一个 MutationObserver
实例,并通过 observe
方法绑定到目标节点。在回调函数中,使用 mutation.removedNodes
检测子节点删除情况。如果发现水印被删除,可以在此处重新插入水印元素。
需要注意的是,MutationObserver
是现代浏览器的特性,可能不兼容老旧浏览器。因此,实际应用中应考虑浏览器兼容性。
此外,为了确保水印能迅速恢复,可以在检测到水印被删除时立即执行插入操作。
防止DOM被隐藏
除了防止DOM被删除,还要考虑DOM被隐藏的情况。
要检测到水印DOM被设置为 display: none
隐藏,可以通过 MutationObserver
观察元素的属性变化,而不是仅仅关注子节点的删除。监听 attributes
类型的变化,以检测到 display
样式属性的改变。
以下示例展示了如何监控 display
属性的变化:
// 目标节点(假设水印元素是一个特定的节点)
const watermarkElement = document.querySelector('.watermark');
// 创建 MutationObserver 实例
const observer = new MutationObserver(mutationsList => {
mutationsList.forEach(mutation => {
if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
// 检查水印的 display 属性是否被设置为 none
if (getComputedStyle(watermarkElement).display === 'none') {
// 如果水印被隐藏,重新显示水印
watermarkElement.style.display = 'block';
}
}
});
});
// 配置 MutationObserver
const config = { attributes: true, subtree: true, attributeFilter: ['style'] };
// 开始观察目标节点
observer.observe(document.body, config);
说明
- 目标节点:在代码中,
watermarkElement
代表水印元素。请确保选择器正确。 - MutationObserver 实例:观察属性变化 (
attributes
) 和特定的属性style
。 - 属性变化检测:在回调函数中,使用
getComputedStyle
检查display
属性的值。如果水印被设置为display: none
,则将其恢复为display: block
。
web 应用中如何对静态资源加载失败的场景做降级处理?
参考答案
在 Web 应用中,对静态资源加载失败的场景进行降级处理是确保应用稳定性和用户体验的关键。以下是一些常见的策略和方法:
1. 使用备用资源
-
图片:
- 在
<img>
标签中使用onerror
事件处理程序,当图片加载失败时,替换为备用图片。
<img src="main-image.jpg" alt="Image" onerror="this.src='fallback-image.jpg';">
- 在
-
CSS:
- 对于 CSS 文件,可以在
<link>
标签中设置备用 CSS 文件,但这种方式不如 JavaScript 的处理直接。通常建议使用 JavaScript 进行处理。
<link rel="stylesheet" href="main.css" onerror="this.href='fallback.css';">
- 对于 CSS 文件,可以在
2. JavaScript 动态加载
-
动态脚本加载:
- 使用 JavaScript 动态加载资源并处理加载失败情况。例如,可以使用
fetch
或XMLHttpRequest
加载 JavaScript 文件或其他资源,并在失败时加载备用资源。
function loadScript(url, fallbackUrl) { const script = document.createElement('script'); script.src = url; script.onerror = () => { script.src = fallbackUrl; document.head.appendChild(script); }; document.head.appendChild(script); } loadScript('main-script.js', 'fallback-script.js');
- 使用 JavaScript 动态加载资源并处理加载失败情况。例如,可以使用
-
动态样式表加载:
- 类似地,可以动态加载样式表并处理加载失败。
function loadStylesheet(url, fallbackUrl) { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = url; link.onerror = () => { link.href = fallbackUrl; }; document.head.appendChild(link); } loadStylesheet('main-styles.css', 'fallback-styles.css');
3. 利用Service Workers
-
缓存和离线处理:
- 使用 Service Workers 来缓存静态资源,并提供离线访问或备用资源。在资源加载失败时,服务工作者可以提供缓存中的版本或备用资源。
// Example service worker script self.addEventListener('install', (event) => { event.waitUntil( caches.open('my-cache').then((cache) => { return cache.addAll([ '/main.css', '/fallback.css', '/main.js', '/fallback.js' ]); }) ); }); self.addEventListener('fetch', (event) => { event.respondWith( caches.match(event.request).then((response) => { return response || fetch(event.request).catch(() => { return caches.match('/fallback.css'); // Or fallback.js }); }) ); });
4. Graceful Degradation
-
功能降级:
- 设计时考虑功能降级,确保核心功能在静态资源加载失败时仍然可用。例如,提供基本功能和备用界面,即使某些样式或脚本没有加载成功。
5. 用户提示
- 在静态资源加载失败时,向用户显示提示信息或错误页面,以告知他们发生了问题并提供解决方案或备用操作。
什么是单点登录,以及如何进行实现?
参考答案
一、是什么
单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一
SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统
SSO 一般都需要一个独立的认证中心(passport),子系统的登录均得通过passport
,子系统本身将不参与登录操作
当一个系统成功登录以后,passport
将会颁发一个令牌给各个子系统,子系统可以拿着令牌会获取各自的受保护资源,为了减少频繁认证,各个子系统在被passport
授权以后,会建立一个局部会话,在一定时间内可以无需再次向passport
发起认证
上图有四个系统,分别是Application1
、Application2
、Application3
、和SSO
,当Application1
、Application2
、Application3
需要登录时,将跳到SSO
系统,SSO
系统完成登录,其他的应用系统也就随之登录了
举个例子
淘宝、天猫都属于阿里旗下,当用户登录淘宝后,再打开天猫,系统便自动帮用户登录了天猫,这种现象就属于单点登录
二、如何实现
同域名下的单点登录
cookie
的domin
属性设置为当前域的父域,并且父域的cookie
会被子域所共享。path
属性默认为web
应用的上下文路径
利用 Cookie
的这个特点,没错,我们只需要将Cookie
的 domain
属性设置为父域的域名(主域名),同时将 Cookie
的path
属性设置为根路径,将 Session ID
(或 Token
)保存到父域中。这样所有的子域应用就都可以访问到这个Cookie
不过这要求应用系统的域名需建立在一个共同的主域名之下,如 tieba.baidu.com
和 map.baidu.com
,它们都建立在 baidu.com
这个主域名之下,那么它们就可以通过这种方式来实现单点登录
不同域名下的单点登录(一)
如果是不同域的情况下,Cookie
是不共享的,这里我们可以部署一个认证中心,用于专门处理登录请求的独立的 Web
服务
用户统一在认证中心进行登录,登录成功后,认证中心记录用户的登录状态,并将 token
写入 Cookie
(注意这个 Cookie
是认证中心的,应用系统是访问不到的)
应用系统检查当前请求有没有 Token
,如果没有,说明用户在当前系统中尚未登录,那么就将页面跳转至认证中心
由于这个操作会将认证中心的 Cookie
自动带过去,因此,认证中心能够根据 Cookie
知道用户是否已经登录过了
如果认证中心发现用户尚未登录,则返回登录页面,等待用户登录
如果发现用户已经登录过了,就不会让用户再次登录了,而是会跳转回目标 URL
,并在跳转前生成一个 Token
,拼接在目标 URL
的后面,回传给目标应用系统
应用系统拿到 Token
之后,还需要向认证中心确认下 Token
的合法性,防止用户伪造。确认无误后,应用系统记录用户的登录状态,并将 Token
写入 Cookie
,然后给本次访问放行。(注意这个 Cookie
是当前应用系统的)当用户再次访问当前应用系统时,就会自动带上这个 Token
,应用系统验证 Token 发现用户已登录,于是就不会有认证中心什么事了
此种实现方式相对复杂,支持跨域,扩展性好,是单点登录的标准做法
不同域名下的单点登录(二)
可以选择将 Session ID
(或 Token
)保存到浏览器的 LocalStorage
中,让前端在每次向后端发送请求时,主动将LocalStorage
的数据传递给服务端
这些都是由前端来控制的,后端需要做的仅仅是在用户登录成功后,将 Session ID
(或 Token
)放在响应体中传递给前端
单点登录完全可以在前端实现。前端拿到 Session ID
(或 Token
)后,除了将它写入自己的 LocalStorage
中之外,还可以通过特殊手段将它写入多个其他域下的 LocalStorage
中
关键代码如下:
// 获取 token
var token = result.data.token;
// 动态创建一个不可见的iframe,在iframe中加载一个跨域HTML
var iframe = document.createElement("iframe");
iframe.src = "http://app1.com/localstorage.html";
document.body.append(iframe);
// 使用postMessage()方法将token传递给iframe
setTimeout(function () {
iframe.contentWindow.postMessage(token, "http://app1.com");
}, 4000);
setTimeout(function () {
iframe.remove();
}, 6000);
// 在这个iframe所加载的HTML中绑定一个事件监听器,当事件被触发时,把接收到的token数据写入localStorage
window.addEventListener('message', function (event) {
localStorage.setItem('token', event.data)
}, false);
前端通过 iframe
+postMessage()
方式,将同一份 Token
写入到了多个域下的 LocalStorage
中,前端每次在向后端发送请求之前,都会主动从 LocalStorage
中读取Token
并在请求中携带,这样就实现了同一份 Token
被多个域所共享
此种实现方式完全由前端控制,几乎不需要后端参与,同样支持跨域
三、流程
单点登录的流程图如下所示:
- 用户访问系统1的受保护资源,系统1发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数
- sso认证中心发现用户未登录,将用户引导至登录页面
- 用户输入用户名密码提交登录申请
- sso认证中心校验用户信息,创建用户与sso认证中心之间的会话,称为全局会话,同时创建授权令牌
- sso认证中心带着令牌跳转会最初的请求地址(系统1)
- 系统1拿到令牌,去sso认证中心校验令牌是否有效
- sso认证中心校验令牌,返回有效,注册系统1
- 系统1使用该令牌创建与用户的会话,称为局部会话,返回受保护资源
- 用户访问系统2的受保护资源
- 系统2发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数
- sso认证中心发现用户已登录,跳转回系统2的地址,并附上令牌
- 系统2拿到令牌,去sso认证中心校验令牌是否有效
- sso认证中心校验令牌,返回有效,注册系统2
- 系统2使用该令牌创建与用户的局部会话,返回受保护资源
用户登录成功之后,会与sso
认证中心及各个子系统建立会话,用户与sso
认证中心建立的会话称为全局会话
用户与各个子系统建立的会话称为局部会话,局部会话建立之后,用户访问子系统受保护资源将不再通过sso
认证中心
全局会话与局部会话有如下约束关系:
- 局部会话存在,全局会话一定存在
- 全局会话存在,局部会话不一定存在
- 全局会话销毁,局部会话必须销毁
SPA首屏加载速度慢的怎么解决
参考答案
单页面应用(SPA)首屏加载速度慢的问题可能由多种因素造成。以下是一些优化首屏加载速度的常见方法:
1. 代码分割(Code Splitting)
-
描述:将代码拆分成多个小块,只加载当前页面所需的代码。
-
实现:使用 Webpack 或其他打包工具进行动态导入,按需加载代码模块。
-
示例:使用
React.lazy
和Suspense
进行组件级别的代码分割。import React, { Suspense, lazy } from 'react'; const LazyComponent = lazy(() => import('./LazyComponent')); function App() { return ( <Suspense fallback={<div>Loading...</div>}> <LazyComponent /> </Suspense> ); }
2. 懒加载(Lazy Loading)
-
描述:只在需要时加载资源(如图片、组件)。
-
实现:使用
IntersectionObserver
或第三方库(如react-lazyload
)。 -
示例:
import React from 'react'; const LazyImage = React.lazy(() => import('./LazyImage')); function App() { return ( <React.Suspense fallback={<div>Loading...</div>}> <LazyImage /> </React.Suspense> ); }
3. 减少初始加载资源
- 描述:减少首屏渲染时需要加载的资源量(如 JS、CSS 文件)。
- 实现:合并和压缩 CSS 和 JS 文件,删除未使用的 CSS,减少 HTTP 请求数量。
4. 服务器端渲染(SSR)
- 描述:将页面的初始内容在服务器端生成,以减少客户端的渲染时间。
- 实现:使用框架(如 Next.js、Nuxt.js)支持 SSR,优化首屏加载时间。
5. 使用 Service Workers
-
描述:使用 Service Workers 缓存资源和页面,加速后续的访问。
-
实现:在应用中注册 Service Worker,缓存静态资源和 API 请求。
if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/service-worker.js') .then(registration => { console.log('ServiceWorker registration successful:', registration); }) .catch(error => { console.log('ServiceWorker registration failed:', error); }); }); }
6. 优化数据请求
- 描述:优化初始数据请求,减少请求时间。
- 实现:将请求数据分为初始必要的数据和后续加载的数据,减少首屏渲染时的数据请求。
7. CDN
静态资源走CDN,缩短下载时间。
8. 使用性能分析工具
- 描述:分析应用性能,找到瓶颈。
- 实现:使用 Chrome DevTools、Lighthouse 等工具来分析和优化首屏加载时间。
通过结合这些方法,可以显著改善 SPA 的首屏加载速度,提高用户体验。
react 中怎么实现下拉菜单场景,要求点击区域外能关闭下拉组件
参考答案
涉及以下几个步骤:
- 创建下拉菜单组件
- 监听点击事件
- 判断点击事件是否在下拉菜单外部
步骤说明
1. 创建下拉菜单组件
import React, { useState, useRef, useEffect } from 'react';
const Dropdown = () => {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
const toggleDropdown = () => {
setIsOpen(!isOpen);
};
// 监听点击事件
useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
return (
<div ref={dropdownRef}>
<button onClick={toggleDropdown}>
Toggle Dropdown
</button>
{isOpen && (
<div className="dropdown-menu">
<p>Menu Item 1</p>
<p>Menu Item 2</p>
<p>Menu Item 3</p>
</div>
)}
</div>
);
};
export default Dropdown;
2. 监听点击事件
- 在
useEffect
钩子中,添加一个全局的mousedown
事件监听器,用于检测点击是否发生在下拉菜单外部。 handleClickOutside
函数会检查点击事件的目标是否在下拉菜单外部,如果是则关闭下拉菜单。
3. 判断点击事件是否在下拉菜单外部
- 使用
useRef
钩子获取下拉菜单组件的引用(dropdownRef
)。 handleClickOutside
函数中使用dropdownRef.current.contains(event.target)
来判断点击的目标是否在下拉菜单的 DOM 结构内。
注意事项
- 确保
ref
正确设置在包含下拉菜单的最外层容器上。 - 在组件卸载时移除事件监听器,以避免内存泄漏。
- 在大型应用中,可以考虑使用更复杂的事件处理库或工具来处理全局点击事件。
通过上述步骤,你可以实现点击区域外关闭下拉菜单的功能。
Redux 和 Vuex 的设计思想是什么?
参考答案
Redux 和 Vuex 都是用于状态管理的库,分别用于 React 和 Vue 框架。它们的设计思想在许多方面有相似之处,但也有各自的独特之处。
下面是对 Redux 和 Vuex 设计思想的详细比较:
Redux
设计思想:
-
单一数据源:
- Redux 强调应用的所有状态都集中在一个单一的
store
中。这个设计简化了状态的管理和调试。
- Redux 强调应用的所有状态都集中在一个单一的
-
状态不可变性:
- Redux 采用不可变数据结构,每次状态的变化都会创建一个新的状态对象,而不是直接修改原有状态。这有助于追踪状态的变化并实现高效的 UI 渲染。
-
纯函数(Reducers) :
- 状态的更新由纯函数(称为 reducers)负责。纯函数意味着相同的输入始终产生相同的输出,不依赖于外部状态或副作用。
-
单向数据流:
- Redux 遵循单向数据流的原则。数据流动的方向是:
action
->reducer
->store
-> 视图。通过这种方式,可以明确跟踪数据的变化和流动。
- Redux 遵循单向数据流的原则。数据流动的方向是:
-
中间件:
- Redux 支持中间件的机制,可以在
dispatch
和reducer
之间插入逻辑。这对于处理异步操作和其他副作用很有用。
- Redux 支持中间件的机制,可以在
-
可预测的状态管理:
- Redux 的设计使得状态变化变得可预测。状态变化的逻辑集中在 reducers 中,可以通过 action 和 reducer 组合跟踪状态的变化。
适用场景:
- 适用于大型应用或复杂状态管理场景。适合需要高度可控和可预测状态的应用。
Vuex
设计思想:
-
集中式存储:
- Vuex 提供集中式的状态管理,所有组件的状态都存储在一个全局的
store
中,保证应用的状态集中管理。
- Vuex 提供集中式的状态管理,所有组件的状态都存储在一个全局的
-
状态、变更和行动分离:
- Vuex 将状态(state)、变更(mutations)、和行动(actions)进行明确的分离。状态是存储的状态,变更通过同步的 mutation 进行,异步操作通过 actions 进行。
-
Mutation 必须是同步的:
- Vuex 强调 mutation 函数必须是同步的,所有状态的更改都必须通过 mutation,这保证了状态更改的可追踪性和调试性。
-
状态可追踪:
- 通过 Vuex 的
store
结构,可以清晰地跟踪状态的变化和应用的状态。
- 通过 Vuex 的
-
Getter 函数:
- Vuex 提供 getter 函数,允许计算状态的派生数据。它类似于 Vue 的计算属性,用于从 store 的状态派生出新的数据。
-
插件系统:
- Vuex 支持插件,可以扩展 Vuex 的功能,例如日志记录、持久化存储等。
适用场景:
- 适用于 Vue.js 应用,特别是中大型应用。适合需要集中式状态管理的场景。
如何搭建一套灰度系统?
参考答案
搭建一套灰度发布系统涉及多个方面的技术和流程,目的是在发布新版本时,能够逐步、部分地将新功能或改动推送给用户,以降低发布的风险。
以下是搭建的一般步骤和要点:
1. 明确灰度发布的需求和目标
- 降低风险:避免一次性发布导致的全局性错误影响所有用户。
- 收集反馈:逐步推出新功能,观察用户行为和收集反馈,及时进行调整。
- 验证性能和稳定性:在小范围内测试新版本的性能和稳定性。
2. 架构设计
- 服务分层:将应用分成多个服务或模块,每个服务独立发布,方便单独进行灰度发布。
- 支持多版本并存:确保系统能同时运行多个版本的新旧功能,以便不同用户访问到不同版本。
3. 用户分组策略
- 按用户分组:根据用户的特征(如地域、设备类型、用户等级等)或随机分配,决定哪些用户先接收到新版本。
- 流量分配:通过配置逐步增加新版本的流量比例。例如,先让 5% 的用户使用新版本,然后观察反馈,逐步增加到 10%、20% 等。
4. 灰度发布系统的功能
- 流量调度:能够动态调整不同版本的流量占比,通常由一个流量调度模块控制。
- 用户分组管理:可以管理用户分组,并将用户分配到对应的版本。
- 监控和日志收集:实时监控系统的性能指标(如请求响应时间、错误率、资源使用情况等),并收集用户行为日志。
- 自动回滚:当检测到新版本出现问题时,系统可以自动回滚到稳定的旧版本。
- A/B 测试:结合 A/B 测试工具,进行功能对比测试,进一步细化灰度策略。
5. 工具与技术栈
- 负载均衡器:使用负载均衡器(如 Nginx、HAProxy)进行流量分配,决定哪些请求应该分配到新版本。
- 微服务架构:使用 Kubernetes、Docker 等技术支持微服务架构,方便独立部署和灰度发布。
- CI/CD 工具:结合 Jenkins、GitLab CI/CD、GitHub Actions 等工具进行自动化构建、测试和部署。
- 监控系统:使用 Prometheus、Grafana、ELK 等工具进行系统监控和日志分析。
- A/B 测试工具:如 Google Optimize、Optimizely,结合灰度发布进行用户体验的差异化测试。
6. 实施灰度发布
- 部署基础设施:搭建好灰度发布的基础设施,包括流量调度、监控、日志收集等模块。
- 制定灰度策略:根据业务需求,制定详细的灰度发布策略,包括用户分组、流量占比、回滚条件等。
- 逐步推进:从小规模用户开始,逐步扩大灰度范围,并观察各项指标以确保系统稳定。
- 持续监控与反馈:在整个灰度发布期间持续监控系统表现,收集用户反馈,及时作出调整或回滚。
7. 回滚策略
- 快速回滚机制:在发布过程中出现问题时,灰度系统应能快速回滚到上一稳定版本。
- 保持数据一致性:确保新版本的数据格式与旧版本兼容,回滚时不会导致数据丢失或不一致。
8. 总结与优化
- 记录发布过程:详细记录每次灰度发布的过程、问题和解决方案,为后续发布积累经验。
- 优化灰度系统:根据发布过程中的经验,不断优化灰度系统,提升系统的健壮性和可操作性。
怎么使用 webpack,将 JS 文件中的 css 提取到单独的样式文件中?
参考答案
在前端项目中使用 Webpack 提取 JS 文件中的 CSS,可以通过以下步骤实现:
1. 安装必要的依赖
首先,需要安装以下依赖:
npm install --save-dev webpack webpack-cli style-loader css-loader mini-css-extract-plugin
2. 配置 Webpack
在 Webpack 配置文件 webpack.config.js
中,配置 MiniCssExtractPlugin
来提取 CSS。
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
entry: './src/index.js', // 入口文件
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /.css$/, // 匹配 CSS 文件
use: [
MiniCssExtractPlugin.loader, // 提取 CSS
'css-loader', // 处理 CSS
],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: 'styles.css', // 输出的 CSS 文件名
}),
],
};
3. 在 JavaScript 中引入 CSS
在你的 JS 文件中,引入 CSS 文件:
import './styles.css'; // 引入 CSS 文件
console.log('Hello, World!');
4. 运行 Webpack
使用以下命令构建项目:
npx webpack --config webpack.config.js
5. 输出结果
构建完成后,生成的 dist
文件夹中会包含 bundle.js
和 styles.css
,CSS 被成功提取到单独的文件中。
站点一键换肤的实现方式有哪些?
参考答案
实现站点一键换肤功能,可以通过以下几种方式:
1. 切换 CSS 样式表
方式:
- 通过 JavaScript 动态切换不同的 CSS 样式表文件。
实现步骤:
-
在
<head>
标签中引入多个样式表,使用不同的id
来标识。<link id="theme-style" rel="stylesheet" href="default-theme.css">
-
使用 JavaScript 动态更改
href
属性来切换样式表。function changeTheme(theme) { const linkElement = document.getElementById('theme-style'); linkElement.href = theme + '-theme.css'; }
2. 使用 CSS 变量
方式:
- 定义 CSS 变量来控制主题样式,通过 JavaScript 动态修改这些变量的值。
实现步骤:
-
在 CSS 文件中定义主题变量。
:root { --primary-color: #3498db; --background-color: #ecf0f1; } .dark-theme { --primary-color: #2c3e50; --background-color: #34495e; }
-
使用 JavaScript 动态切换主题类。
function changeTheme(theme) { document.documentElement.className = theme; }
3. 动态加载样式
方式:
- 利用 JavaScript 动态加载不同的样式文件或 CSS 规则。
实现步骤:
-
创建多个样式文件,并使用 JavaScript 动态加载。
function loadTheme(theme) { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = theme + '-theme.css'; document.head.appendChild(link); }
-
移除当前样式以避免重复加载。
function changeTheme(theme) { const existingLink = document.querySelector('link[href*="-theme.css"]'); if (existingLink) { existingLink.remove(); } loadTheme(theme); }
4. 使用 JavaScript 插件
方式:
- 利用现成的 JavaScript 插件或库来实现换肤功能。
实现步骤:
-
使用插件,例如 ThemeSwitcher 或类似的工具,根据插件文档进行配置。
-
插件通常提供了简单的 API 来更换主题。
$('#theme-switcher').on('change', function() { const selectedTheme = $(this).val(); themeSwitcher.change(selectedTheme); });
5. 存储用户设置
方式:
- 保存用户选择的主题,并在用户重新访问时应用相同的主题。
实现步骤:
-
在用户选择主题时,将其存储到
localStorage
或sessionStorage
。function setTheme(theme) { localStorage.setItem('theme', theme); changeTheme(theme); }
-
在页面加载时,读取存储的主题并应用。
document.addEventListener('DOMContentLoaded', function() { const savedTheme = localStorage.getItem('theme') || 'default'; changeTheme(savedTheme); });
因为本文确实全部写下来太长,确实是篇幅有限,完整版本可以扣【传送门】!!!
也希望还没有转行的小伙伴能够在2025年取得心仪的offer,手动比心,886