前端面经第一篇
最后编写于2025年8月1日
前端如何实现打字机的效果,说一下它实现的思路或者是一个原理
打字机效果的核心原理是模拟人类敲击键盘的过程——文字逐个(或逐段)显示,并可配合光标闪烁增强真实感。实现思路主要围绕“控制文字显示节奏”和“模拟输入反馈”展开,具体可拆解为以下步骤:
一、核心原理
通过定时器(控制时间间隔) 和字符串截取(控制显示内容),让目标文本从空字符串开始,按固定节奏逐步拼接字符,最终完整显示。同时可添加光标动画(如“|”的闪烁),模拟打字时的光标状态。
二、实现思路(以原生JS为例)
假设要在页面中实现对文本 Hello, 这是打字机效果! 的打字动画,步骤如下:
1. 准备基础结构
在HTML中定义显示文本的容器和光标(可选):
<div class="typewriter-text"></div>
<span class="cursor">|</span> <!-- 光标元素 -->2. 核心逻辑:控制文字逐个显示
存储目标文本:定义需要显示的完整字符串。
记录当前显示进度:用一个变量(如
index)记录已经显示到第几个字符。定时器逐字拼接:每间隔一段事件(如200ms),将目标文本的第
index个字符拼接到容器中,并递增index,直到显示完整文本后清除定时器。
const text = "Hello, 这是打字机效果!"; // 目标文本
const container = document.querySelector('.typewriter-text');
let index = 0; // 当前显示到第几个字符
// 打字函数:每200ms显示一个字符
function type() {
if (index < text.length) {
// 拼接当前字符到容器(从0到index的 substring)
container.textContent = text.substring(0, index + 1);
index++;
// 递归调用定时器,控制打字速度
setTimeout(type, 200); // 200ms打一个字,可调整速度
} else {
// 打字结束,可隐藏光标或做其他处理
clearTimeout(timer);
}
}
// 启动打字
let timer = setTimeout(type, 200);3. 增强体验:添加光标闪烁效果
通过CSS动画让光标元素(如“|”)周期性显示/隐藏,模拟打字时的光标:
.cursor {
opacity: 1;
animation: blink 1s infinite; /* 1秒闪烁一次 */
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; } /* 中间时刻隐藏 */
}三、变种场景:更贴近真实的细节优化
控制打字速度:可给不同字符设置不同间隔(如标点符号后停顿更长),更像人类打字习惯。
javascript// 示例:遇到逗号/句号时停顿500ms,其他字符停顿200ms function getDelay(char) { return [',', ',', '.', '。'].includes(char) ? 500 : 200; } // 在type()中使用动态延迟:setTimeout(type, getDelay(text[index]));支持换行/HTML标签:如果文本包含换行或HTML(如
<br>),需在拼接时保留标签(需用innerHTML而非textContent)。暂停/继续功能:通过控制定时器的启动和清除,实现打字过程的暂停与恢复。
四、框架中的实现(以Vue3为例)
在Vue中可通过 ref 记录文本和进度,配合 onMounted 启动定时器,逻辑与原生一致,只是用响应式变量管理状态:
<template>
<div>{{ displayText }}</div>
<span class="cursor" v-if="isTyping">|</span>
</template>
<script setup>
import { ref, onMounted } from 'vue';
const targetText = "Hello, Vue打字机效果!";
const displayText = ref(''); // 响应式显示文本
const index = ref(0);
const isTyping = ref(true); // 控制光标显示
function type() {
if (index.value < targetText.length) {
displayText.value = targetText.substring(0, index.value + 1);
index.value++;
setTimeout(type, 200);
} else {
isTyping.value = false; // 结束后隐藏光标
}
}
onMounted(() => {
setTimeout(type, 500); // 页面加载后延迟启动
});
</script>总结
打字机效果的本质是时间间隔 + 内容递增:通过定时器控制节奏,通过字符串截取控制显示范围,再配合光标动画增强真实感。核心逻辑简单,可根据需求调整速度、添加交互(如暂停、变速),适配不同场景(文字、数字、甚至代码高亮显示)。
TCP为什么是三次握手而不是两次?
TCP 为什么采用三次握手而非两次,核心原因是确保双方的“发送能力”和“接收能力”都能被对方确认,同时避免“已失效的连接请求报文段”干扰新连接。以下从原理和风险两方面详细说明:
一、TCP 三次握手的核心目的
TCP 是“面向连接”的可靠传输协议,在传输数据前必须通过握手建立连接。握手的本质是双方交换“初始序列号(ISN)”并确认对方能正常收发数据。三次握手的每一步都有明确作用:
- 第一次握手(客户端 → 服务器):客户端发送连接请求(SYN 报文),携带自己的初始序列号(ISNc),表示“我能发送数据”。
- 第二次握手(服务器 → 客户端):服务器收到请求后,回复确认(SYN+ACK 报文),携带自己的初始序列号(ISNs),并确认收到客户端的 ISNc(即 ACK=ISNc+1),表示“我能接收也能发送数据”。
- 第三次握手(客户端 → 服务器):客户端收到回复后,再发送一个确认(ACK 报文),确认收到服务器的 ISNs(即 ACK=ISNs+1),表示“我已确认你能正常收发,连接可以建立”。
二、为什么不能是两次握手?
两次握手无法完成“双向确认”,会导致两个关键问题:
1. 无法确认客户端是否能接收服务器的数据
- 两次握手的流程只能是:客户端发送 SYN → 服务器回复 SYN+ACK(此时服务器认为连接已建立)。
- 但服务器无法确定“自己的 SYN+ACK 报文是否被客户端收到”。如果这个报文丢失,客户端会认为连接未建立,而服务器会一直等待客户端发送数据,最终造成服务器资源浪费(如保留连接缓存、端口等)。
- 三次握手的第三次 ACK 则能让服务器确认:“客户端已收到我的消息,且能正常回复”,此时服务器才会确认连接建立。
2. 无法处理“已失效的连接请求”
- 实际网络中,客户端可能因网络延迟发送“旧的 SYN 报文”(比如第一次发送的 SYN 超时后,客户端重发了新的 SYN 并建立连接,但旧的 SYN 后来又到达服务器)。
- 如果是两次握手:服务器收到旧的 SYN 后,会直接回复 SYN+ACK 并建立连接,但客户端此时早已不需要这个连接,服务器会一直维持无效连接,导致资源泄露。
- 三次握手则能避免:旧的 SYN 到达服务器后,服务器回复 SYN+ACK,但客户端会识别这是“无效请求”(因为已建立新连接),不会发送第三次 ACK。服务器等不到 ACK,就会释放该连接,避免无效资源占用。
三、三次握手 vs 两次握手:核心差异总结
| 场景 | 三次握手 | 两次握手 |
|---|---|---|
| 双向能力确认 | 客户端和服务器都能确认对方“收发正常” | 服务器无法确认客户端是否能接收自己的消息 |
| 失效请求处理 | 能识别并丢弃无效连接请求 | 可能建立无效连接,浪费服务器资源 |
| 连接可靠性 | 确保连接建立的双向可靠性 | 存在连接状态不一致的风险 |
四、为什么不是四次握手?
三次握手已经能完成所有必要的确认(交换 ISN、双向确认收发能力),四次握手会增加一次不必要的交互,降低连接建立效率,因此三次是“最低且最优”的次数。
总结
TCP 三次握手的核心是通过三次交互完成“双向收发能力确认”,既确保连接建立的可靠性,又避免资源浪费。两次握手因缺少客户端对服务器的最终确认,无法解决“报文丢失”和“无效请求”问题,因此 TCP 必须采用三次握手。
前端路由的两种模式在实际开发中怎么去选?
在前端路由中,history 模式(Web History API)和 hash 模式(Hash History)的选择需结合 项目部署环境、URL 美观需求、功能场景 等核心因素。以下从两者的底层差异出发,结合实际开发场景给出选择建议。
一、先明确两种模式的核心差异
| 对比维度 | hash 模式 | history 模式 |
|---|---|---|
| URL 表现 | 带 # (如 http://xxx.com/#/page) | 无 #(如 http://xxx.com/page),更接近传统 URL |
| 底层原理 | 依赖 URL 中 # 后的哈希值变化(hashchange 事件监听) | 依赖 HTML5 的 History API(pushState/replaceState) |
| 后端依赖 | 无需后端配置(刷新页面时浏览器只解析 # 前的路径) | 必须后端配合(刷新页面时浏览器会请求完整 URL,需后端返回前端页面) |
| 路径长度限制 | 部分浏览器对 # 后的哈希值长度有限制(较旧场景) | 无特殊长度限制(依赖浏览器对 URL 整体长度的限制) |
| SEO 友好性 | 较差(部分搜索引擎可能忽略 # 后的内容) | 更好(URL 结构更符合搜索引擎对静态 URL 的偏好) |
| 支持的路径类型 | 仅支持同一域名下的路由(# 不触发跨域) | 可模拟跨域路径(但实际仍受同源策略限制) |
二、实际开发中的选择标准(核心场景)
1. 优先选 hash 模式的场景
如果项目符合以下任一条件,hash 模式是更稳妥的选择:
部署环境无后端控制权
例如:静态页面部署在纯静态服务器(如 Nginx 静态托管、GitHub Pages、Netlify 等),且无法修改后端配置。- 原因:
hash模式下,URL 中#后的内容不会被浏览器当作“路径”发送给服务器,刷新页面时浏览器只会请求#前的域名(如http://xxx.com),只要服务器能返回前端入口页面(如index.html),路由就能正常工作,无需后端额外配置。
- 原因:
快速原型开发或小型项目
例如:内部工具、H5 活动页、无需 SEO 的演示项目。- 原因:
hash模式实现简单(框架默认支持,无需额外配置),能避免因后端配置不当导致的“刷新 404”问题,降低开发和部署成本。
- 原因:
对 URL 美观度要求不高
例如:后台管理系统(用户更关注功能而非 URL 样式)。- 原因:
#虽然不美观,但后台系统用户通常不会在意 URL 格式,且hash模式的稳定性(无后端依赖)更重要。
- 原因:
2. 优先选 history 模式的场景
如果项目符合以下条件,history 模式更合适:
对 URL 美观和 SEO 有要求
例如:官网、营销页、需要被搜索引擎收录的内容页。- 原因:无
#的 URL 更符合用户认知(接近传统网站),且搜索引擎对无#的 URL 解析更友好(虽然现代搜索引擎对hash模式的支持有所提升,但history模式仍是更优解)。
- 原因:无
有后端配合能力
例如:项目部署在自有服务器(如 Node.js、Java 后端),可修改后端路由配置。- 原因:
history模式的核心痛点是“刷新页面时会请求完整 URL”,需后端配置“所有路由都指向前端入口页面(index.html)”(如 Nginx 配置try_files $uri $uri/ /index.html;)。只要后端支持,就能避免刷新 404 问题。
- 原因:
需要使用“嵌套路径”或“复杂路由结构”
例如:电商网站的http://xxx.com/category/123/product/456这类多层级路径。- 原因:
hash模式的 URL 带#,多层级路径会显得冗余(如http://xxx.com/#/category/123/product/456),而history模式的 URL 结构更清晰,便于用户识别和记忆。
- 原因:
3. 特殊场景的补充判断
是否需要“分享链接”更友好?
若链接需频繁在社交平台、邮件中传播(如活动页),history模式的无#URL 更易被用户信任(用户可能对#产生“链接不正规”的感知)。是否兼容极低版本浏览器?
hash模式兼容 IE8 及以上(hashchange事件支持),而history模式依赖 HTML5 API,不兼容 IE9 及以下。若需支持古老浏览器,优先hash模式。
三、总结:一句话选择法则
- 能控制后端配置 + 在意 URL 美观/SEO → 选 history 模式
- 无后端控制权 + 优先稳定性/低成本 → 选 hash 模式
实际开发中,多数中小型项目(尤其是静态部署的前端项目)会优先用 hash 模式(避免后端依赖);而官网、大型应用(有后端团队配合)更倾向 history 模式(提升用户体验和 SEO)。
此外,主流框架(Vue、React)的路由库(如 Vue Router、React Router)都支持两种模式的无缝切换,可根据项目后期需求调整(例如先用水印模式快速上线,后续对接后端后切换为 history 模式)。
说说JS中闭包的实现原理
闭包的通俗解释
想象一下,你有一个神奇的口袋,这个口袋可以记住你放进去的东西,而且无论你走到哪里,口袋里的东西都不会消失。即使你离开了放东西的那个房间,口袋里的东西依然存在。
在 JavaScript 里,闭包就是这个神奇的口袋。当一个函数内部创建了另一个函数,并且内部函数引用了外部函数的变量时,闭包就形成了。内部函数会"记住"这些变量,即使外部函数已经执行完毕。
为什么闭包很有用?
闭包最核心的作用是让变量的值始终保持在内存中,不会随着外部函数的执行结束而被销毁。这在很多场景下非常有用,比如:
- 实现私有变量:外部无法直接访问函数内部的变量,但可以通过闭包提供的接口间接访问。
- 函数工厂:创建带有特定"记忆"的函数。
- 事件处理:在异步操作中保持变量的状态。
闭包的实现原理
闭包的实现依赖于 JavaScript 的词法作用域和垃圾回收机制:
词法作用域:函数在定义时会"记住"它周围的变量环境(即它的作用域链)。即使函数在其他地方被调用,它仍然可以访问定义时的作用域链。
垃圾回收:通常,函数执行完毕后,其内部变量会被垃圾回收机制清除。但如果有闭包引用这些变量,垃圾回收器会认为这些变量仍然"有用",从而保留它们在内存中。
一个简单的例子
function outer() {
let count = 0; // 这个变量会被闭包"记住"
function inner() {
count++; // 内部函数可以访问外部函数的变量
console.log(count);
}
return inner; // 返回内部函数(形成闭包)
}
const counter = outer(); // 调用outer(),返回inner函数
counter(); // 输出 1
counter(); // 输出 2
counter(); // 输出 3解释:
outer()函数执行后返回了inner函数,此时outer的执行上下文本应被销毁。- 但由于
inner函数引用了count变量,JavaScript 会为inner创建一个闭包,保留对count的引用。 - 每次调用
counter()(即inner函数)时,count的值都会被累加,因为闭包一直保持着对它的引用。
再看一个实际场景:事件处理
function setupButton() {
let count = 0;
const button = document.createElement('button');
button.textContent = `点击了 ${count} 次`;
button.addEventListener('click', function() {
count++; // 闭包记住了count变量
button.textContent = `点击了 ${count} 次`;
});
return button;
}
document.body.appendChild(setupButton());解释:
- 每次点击按钮时,事件处理函数(闭包)会访问并修改
count变量。 - 即使
setupButton函数已经执行完毕,count变量也不会被销毁,因为闭包一直引用着它。
闭包的注意事项
内存管理:闭包会保留变量的引用,可能导致内存占用增加。如果闭包不再需要,应手动解除引用(如设置为
null)。循环中的闭包:在循环中创建闭包时要特别小心,因为它们可能共享同一个变量。通常需要使用立即执行函数或
let块级作用域来解决。性能考量:过度使用闭包可能影响性能,特别是在创建大量闭包的情况下。
总结
闭包就是一个能够记住并访问其定义时的词法环境的函数。即使外部函数已经执行完毕,闭包仍然可以访问和修改外部函数的变量。它是 JavaScript 中非常强大的特性,但也需要谨慎使用以避免内存问题。
项目打包出来的vender文件过大如何优化解决
在Vue项目打包时,vender文件(通常命名为vender.[hash].js)是一个重要的优化产物,它主要用于集中存放项目中引用的第三方依赖库(如vue,axios等)。
vender文件的作用
代码分割:将第三方依赖和业务代码分离,避免重复打包。
缓存优化:第三方库一般比较稳定,单独打包后可以利用浏览器缓存(内容不变hash不变),无需用户重新下载该js文件。
提升加载性能:并行加载
vender和业务代码,加快页面渲染速度。
如何解决过大问题
拆分代码:使用动态import按需加载业务组件(搭配Vue Router)。
JScomponent: () => import("@/views/test/index.vue")优化依赖:将一些大型的第三方库替换成该第三方库的小型版本,比如
lodash换成lodash-es使用CDN导入第三方库:通过vite的
external配置指定某些包不打包进vender文件,手动在index.html中导入cdn地址。使用
Tree Shakeing技术,只打包实际使用的代码。
请解释 Apache 和 Nginx 的区别,并描述各自的应用场景
Apache 与 Nginx 的核心区别及适用场景
Apache 和 Nginx 是目前最流行的两款 Web 服务器软件,它们在架构设计、性能特性和适用场景上有显著差异。以下是通俗解释和对比:
一、核心区别
1. 架构模型
Apache:多进程/多线程模型(如 Prefork、Worker 模式)
- 每个请求由一个独立进程/线程处理,开销大但兼容性好。
- 适合处理少量并发的复杂请求(如动态脚本)。
Nginx:事件驱动(异步非阻塞)模型
- 单线程处理多个连接(通过 epoll/kqueue),内存占用极低。
- 擅长处理大量并发的静态请求(如静态文件、反向代理)。
2. 性能表现
Apache:
- 高并发时性能下降明显(进程/线程创建开销大)。
- 适合处理 CPU 密集型请求(如 PHP、Python 脚本)。
Nginx:
- 轻松支持 5 万+并发连接(内存占用仅几十 MB)。
- 静态文件处理速度比 Apache 快 30%~50%。
3. 功能侧重
Apache:
- 功能全面:支持 .htaccess 动态配置、大量模块(如 mod_rewrite)。
- 适合需要复杂 URL 重写、认证、自定义模块的场景。
Nginx:
- 轻量级:专注高性能反向代理、负载均衡、静态文件服务。
- 适合作为前端服务器(如负载均衡器、静态资源缓存)。
4. 配置复杂度
- Apache:配置文件复杂(基于文本的 .conf 文件),修改后需重启服务。
- Nginx:配置简洁(模块化设计),支持热加载(无需重启服务)。
二、典型应用场景
Apache 适用场景
动态脚本为主的网站(如 WordPress、Django 应用)
- 对 PHP、Python 等脚本语言支持更原生,兼容性好。
需要复杂 URL 规则的网站
- 依赖 .htaccess 文件实现灵活的 URL 重写和访问控制。
安全性要求高的企业应用
- 丰富的安全模块(如 mod_security)和成熟的访问控制机制。
功能扩展性需求
- 需要集成 LDAP、DAV 等特殊模块的场景。
Nginx 适用场景
高并发静态资源服务(如图片、JS、CSS)
- 作为静态文件服务器,性能远超 Apache。
反向代理与负载均衡
- 处理大量并发请求,将流量分发到多个后端服务器。
微服务网关
- 作为 API 网关,提供路由、限流、鉴权等功能。
前端项目部署(如 Vue、React 应用)
- 轻量级部署,高效处理 SPA(单页应用)的路由请求。
流媒体服务
- 支持 RTMP、HLS 等协议,适合直播、视频网站。
三、对比表格
| 特性 | Apache | Nginx |
|---|---|---|
| 架构 | 多进程/多线程 | 事件驱动(异步非阻塞) |
| 并发处理 | 低(数千连接) | 极高(5 万+连接) |
| 内存占用 | 高(每个进程/线程占用 MB 级) | 低(单进程几十 KB) |
| 静态文件处理 | 较慢 | 极快 |
| 动态脚本支持 | 优秀(原生模块支持) | 需依赖 FastCGI 等外部进程 |
| 配置灵活性 | 高(支持 .htaccess) | 低(需修改主配置文件) |
| 热加载 | 不支持(需重启服务) | 支持(无需中断服务) |
| 典型场景 | 动态网站、企业应用 | 静态服务、反向代理、负载均衡 |
四、如何选择?
- 选 Apache:如果你的应用以动态脚本为主,需要复杂配置和扩展功能。
- 选 Nginx:如果需要高性能处理大量并发请求(如静态资源、API 网关)。
常见组合:
- Nginx + Apache:Nginx 作为前端负载均衡和静态资源服务器,Apache 处理动态请求。
- Nginx + Node.js/Python:Nginx 作为反向代理,后端使用 Node.js/Python 微服务。
五、性能测试参考
在相同硬件条件下(8核16GB):
- Apache:处理 1 万并发请求时,CPU 使用率达 80%,响应延迟约 500ms。
- Nginx:处理 10 万并发请求时,CPU 使用率仅 30%,响应延迟 < 100ms。
(数据来源:Benchmarking Apache vs Nginx, TechEmpower Framework Benchmarks)
总结
Apache 是“全能型选手”,适合功能复杂的传统应用;Nginx 是“性能王者”,适合高并发场景。实际项目中,两者常结合使用,发挥各自优势。
H5页面怎么在移动端做适配的?具体实操中怎么解决移动端显示不同的字体和元素大小?在项目中如何实现px和rem之间的转换?
在移动端H5开发中,“适配”本质就是让同一个页面在不同尺寸的手机上(比如小屏的iPhone SE、大屏的安卓机)都能正常显示——文字不模糊、按钮不跑偏、布局不混乱。下面用通俗的话讲清楚适配思路、字体/元素大小问题,以及px和rem的转换实操。
一、H5移动端适配的核心思路:“按比例缩放”
手机屏幕尺寸太多(比如宽度从320px到428px不等),不可能为每个屏幕写一套样式。核心解法是:以某个“标准屏幕”为基准,让页面元素的大小、间距等按屏幕宽度的比例自动缩放。
比如:你在设计稿上(假设设计稿宽度是375px)画了一个100px宽的按钮,在实际手机上(比如宽度414px),这个按钮的宽度应该自动变成“100px × (414/375)”,这样在任何屏幕上,按钮占屏幕的比例都是一样的。
二、具体实操:解决字体和元素大小适配
最常用、最简单的方案是 “rem适配”(配合flex/grid布局),步骤分3步:
1. 先理解两个单位:px和rem
- px:固定像素(比如“font-size: 16px”,就是文字占16个像素宽),写死的话,在不同屏幕上大小不变(小屏显大,大屏显小)。
- rem:相对单位,大小由根元素(html标签)的font-size决定。比如:
如果html的font-size是100px,那么“1rem = 100px”;
如果html的font-size是50px,那么“1rem = 50px”。
核心:只要动态改变html的font-size,所有用rem写的元素就会自动缩放。
2. 动态设置html的font-size(关键一步)
目标:让html的font-size和手机屏幕宽度成比例。
比如设计稿宽度是375px(常见的手机设计稿尺寸),我们可以约定:
屏幕宽度为375px时,html的font-size = 100px(这个100是随便定的,方便计算)。
那么在其他屏幕上,html的font-size就按比例算:html的font-size = (手机实际宽度 / 375) × 100px
举例:
- 手机宽度375px → html.fontSize = 100px
- 手机宽度414px → html.fontSize = (414/375)×100 ≈ 110.4px
- 手机宽度320px → html.fontSize = (320/375)×100 ≈ 85.3px
这样一来,rem就会随屏幕宽度自动变化,用rem写的元素自然就适配了。
实操代码(在入口JS或全局JS里加):
// 监听屏幕尺寸变化,动态计算html的font-size
function setRemUnit() {
const designWidth = 375; // 设计稿宽度
const baseFontSize = 100; // 设计稿下的根字体大小
const actualWidth = document.documentElement.clientWidth; // 手机实际宽度
// 计算当前根字体大小(按比例)
const currentFontSize = (actualWidth / designWidth) * baseFontSize;
// 设置到html标签
document.documentElement.style.fontSize = currentFontSize + 'px';
}
// 初始化时执行一次
setRemUnit();
// 屏幕旋转或 resize 时重新计算(比如横屏变竖屏)
window.addEventListener('resize', setRemUnit);3. 写样式时,用rem代替px
设计稿上的尺寸,直接除以100(因为我们约定设计稿下1rem=100px),得到rem值。
举例:
- 设计稿上按钮宽200px → 200 / 100 = 2rem →
width: 2rem - 设计稿上文字大小16px → 16 / 100 = 0.16rem →
font-size: 0.16rem - 设计稿上间距30px → 30 / 100 = 0.3rem →
margin: 0.3rem
这样写出来的元素,在任何屏幕上都会按设计稿的比例显示,不会变形。
三、补充:避免踩坑的细节
不要混用px和rem
适配的元素(宽度、高度、字体、间距)用rem;不需要适配的(比如边框1px)用px。限制最大/最小宽度
大屏手机(比如平板)可能显示太宽,可加个最大宽度:cssbody { max-width: 750px; /* 比如最大宽度是设计稿的2倍 */ margin: 0 auto; /* 居中 */ }用flex/grid布局配合rem
rem解决“大小适配”,flex/grid解决“位置适配”(比如元素自动换行、居中),两者结合效果更好。框架工具可以简化操作
如果用Vue/React,可配合插件自动转换(比如postcss-pxtorem):写px时自动转成rem,不用手动算除法。
总结
移动端H5适配核心就是“按屏幕比例缩放”,用rem实现最简单:
- 动态计算html的font-size(随屏幕宽度变);
- 写样式时,设计稿px值除以100(或你定的基准值)得到rem;
- 配合flex布局,搞定所有屏幕。
这样做的好处是:一套样式适配所有手机,不用为不同屏幕写多套代码。
RBAC与ABAC的核心区别及适用场景
RBAC(基于角色的访问控制)和ABAC(基于属性的访问控制)是两种主流的权限控制模型,它们在设计理念、实现复杂度和适用场景上有本质区别。以下是对比分析和实际场景建议:
核心概念对比
| 维度 | RBAC(Role-Based Access Control) | ABAC(Attribute-Based Access Control) |
|---|---|---|
| 控制单元 | 角色(Role) | 属性(Attribute) |
| 权限分配 | 用户→角色→权限(多层级授权) | 基于用户属性、资源属性、环境属性动态计算权限 |
| 决策逻辑 | 预定义规则(静态) | 动态规则引擎(实时计算) |
| 典型场景 | 组织架构相对稳定的企业系统(如ERP、OA) | 复杂多变的权限场景(如云计算、物联网、金融风控) |
| 实现复杂度 | 低(数据库设计简单) | 高(需规则引擎支持) |
RBAC详解与场景
1. 核心组件
- 用户(User):系统中的个体
- 角色(Role):如"管理员"、"财务"、"普通用户"
- 权限(Permission):如"创建订单"、"查看财务报表"
- 关系:用户→角色→权限(多对多)
2. 层级结构
- RBAC0(基础模型):最基本的用户-角色-权限关联
- RBAC1(层级模型):角色可继承(如"高级管理员"继承"普通管理员"权限)
- RBAC2(约束模型):增加角色互斥(如财务和审计不能由同一人担任)
- RBAC3(综合模型):合并RBAC1和RBAC2
3. 适用场景
- 企业管理系统:按部门和职位划分权限(如人力资源系统)
- 内容管理系统(CMS):编辑→审核→发布的角色流程
- 电商后台:运营→客服→财务的分层权限控制
4. 优势与局限
- 优势:简单直观,易于理解和实现
- 局限:灵活性差,难以应对复杂动态的权限需求(如"工作日上午可审批,周末不可")
ABAC详解与场景
1. 核心组件
- 主体属性(User Attributes):如用户ID、部门、职位、信誉等级
- 资源属性(Resource Attributes):如文件机密等级、金额大小、创建时间
- 环境属性(Environment Attributes):如访问时间、IP地址、设备类型
- 规则引擎:定义属性之间的逻辑关系(如"部门=财务 AND 时间=工作日")
2. 决策流程
用户请求资源 → 收集用户属性、资源属性、环境属性 →
规则引擎评估 → 返回允许/拒绝结果3. 适用场景
- 云计算平台:根据资源使用量、付费状态动态分配权限
- 物联网系统:基于设备状态、位置、时间控制访问
- 金融风控系统:根据交易金额、用户历史行为、地理位置动态授权
- 个性化推荐系统:根据用户偏好、浏览历史控制内容可见性
4. 优势与局限
- 优势:极高的灵活性,可应对复杂多变的权限需求
- 局限:实现复杂度高,性能开销大,规则维护困难
实际项目选型建议
1. 选型决策树
项目需求 → 是否需要动态权限? →
否 → 选择RBAC(简单场景用RBAC0,复杂层级用RBAC1)
是 → 是否需要基于多维度属性组合? →
否 → 扩展RBAC(如添加用户组、资源标签)
是 → 选择ABAC(需接受较高实现成本)2. 混合模型方案
在实际项目中,可结合两者优势:
- 粗粒度权限用RBAC:如部门级访问控制
- 细粒度权限用ABAC:如特定数据的访问限制
示例场景:
// 某银行系统权限规则
1. 基础权限(RBAC):
- 柜员角色:可处理10万以下的普通业务
- 经理角色:可处理10万以上的特殊业务
2. 动态权限(ABAC):
- 规则1:如果交易金额 > 50万 AND 时间不在工作日9:00-17:00 → 需要双因素认证
- 规则2:如果用户IP不在常用地址列表 AND 交易金额 > 20万 → 拒绝交易
- 规则3:如果用户信誉等级 < 60分 → 限制高风险操作| 场景类型 | 推荐模型 | 典型案例 |
|---|---|---|
| 企业内部系统(权限稳定) | RBAC | ERP、OA、CRM系统 |
| 互联网平台(权限多变) | ABAC或混合模型 | 电商平台(促销活动权限)、社交平台(内容可见性控制) |
| 物联网/工业系统 | ABAC | 智能工厂(设备状态动态授权) |
| 金融风控系统 | ABAC | 交易风险实时评估 |
在实际项目中,约80%的场景可以通过RBAC解决,只有真正复杂且动态的权限需求才需要引入ABAC。建议优先使用RBAC,当遇到无法解决的场景时再考虑扩展或引入ABAC。
为什么react需要fiber架构,而vue不需要?
要理解这个问题,我们可以从“浏览器的脾气”和“两个框架的做事风格”入手,用生活化的例子来讲清楚。
先搞懂一个前提:浏览器怕“累着”
浏览器的主线程(处理渲染、用户操作的线程)很“娇气”,一次只能干一件事,而且如果一件事干太久(超过16ms,大概是屏幕刷新一帧的时间),就会“卡住”——比如你点按钮没反应、页面滚动不流畅,这就是所谓的“卡顿”。(单线程)
所以,任何框架操作DOM时,都得尽量别让主线程“加班”太久。
React为什么需要Fiber?因为它以前“太实在”
React早期的渲染逻辑是“一根筋”:当数据变化需要更新页面时,它会从根组件开始,像“剥洋葱”一样一层层对比新旧组件树(这个过程叫“协调”),找出需要更新的部分。这个过程是“一口气干完”的——一旦开始,就必须等到整个组件树对比完才能停,中间哪怕用户点了按钮、滚动了页面,也得憋着,等它干完才能响应。
这就出问题了:如果组件树特别大(比如一个页面有几百个组件),“剥洋葱”可能要花几十甚至上百毫秒,远超16ms。这时候浏览器就会卡顿,用户体验就差了。
Fiber架构就是来解决这个问题的:
它把“剥洋葱”的大任务,拆成了无数个“小任务”(比如处理一个组件就是一个小任务)。每个小任务干完后,先问问浏览器:“我干得差不多了,你有没有更急的事(比如用户点了按钮)?有的话你先干,我等会儿再继续。”
这样一来,主线程就不会被长时间霸占,卡顿问题就解决了。简单说,Fiber就是给React加了个“暂停/继续”的功能,让它学会“见缝插针”地干活,不耽误浏览器处理更急的事。
Vue为什么不需要Fiber?因为它“更机灵”
Vue的做事风格和React完全不同,它从根上就避免了“干重活”的情况,自然就不需要Fiber这种“拆分任务”的机制。
具体来说,Vue有两个“小聪明”:
它知道“具体哪里要改”
Vue通过“数据劫持”(比如用Proxy监听数据变化),能精确追踪到“哪个数据被改了,这个数据被哪些组件用了”。当数据变化时,Vue直接就能定位到需要更新的组件,不会像React那样“从根开始剥洋葱”。
打个比方:React更新时像“全楼排查”,从1楼到顶楼每个房间都看一遍;Vue则像“精准导航”,直接告诉你“3楼2号房的灯坏了,去修那里就行”。
因为需要处理的范围小,任务本身就轻,很少出现“干太久卡主线程”的情况。它会“合并任务”
Vue更新时会“攒一波再干”:如果短时间内多个数据变化(比如连续改了3个数据),Vue不会立刻更新3次,而是等所有数据改完,合并成一次更新。这样又减少了主线程的工作量,进一步降低了卡顿风险。
总结:核心差异在“更新范围”和“调度需求”
- React早期的更新是“大范围、一口气干到底”,容易累着主线程,所以需要Fiber来“拆分任务、灵活调度”;
- Vue的更新是“小范围、精准定位+合并操作”,本身工作量就小,不需要Fiber这种复杂的调度机制,也能保证不卡顿。
简单说:React是因为“以前太实在,容易干过头”才需要Fiber救场;Vue因为“天生机灵,活儿干得少”,所以用不上。