笔试

第1题

铺设光缆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
from collections import deque
import sys

def calculate_min_execution_time():
# 读取任务数量 N
N = int(sys.stdin.readline())

# 读取每个任务的执行时间,times[i] 表示任务 i 的执行时间
times = list(map(int, sys.stdin.readline().split()))

# 初始化存储依赖关系的列表
dependencies = []
for _ in range(N):
line = sys.stdin.readline().strip()
# 如果输入为 -1,表示该任务没有依赖,加入空列表
if line == '-1':
dependencies.append([])
else:
# 否则将依赖的任务编号转为 0 基索引并存储
deps = list(map(int, line.split()))
dependencies.append([d-1 for d in deps])

# 构建任务依赖图和入度数组
graph = [[] for _ in range(N)] # graph[i] 表示任务 i 依赖的其他任务
in_degree = [0] * N # in_degree[i] 表示任务 i 的前置依赖任务数
for i in range(N):
for dep in dependencies[i]:
graph[dep].append(i) # 如果 i 依赖 dep, 那么 dep 需要完成 i 才能开始
in_degree[i] += 1 # 每找到一个依赖,入度加 1

# 初始化队列,存储没有依赖的任务(入度为 0)
queue = deque()
earliest_finish = [0] * N # 存储每个任务的最早完成时间
for i in range(N):
if in_degree[i] == 0:
queue.append(i) # 入度为 0 的任务可以立即开始
earliest_finish[i] = times[i] # 它们的最早完成时间就是它们的执行时间

# 处理队列中的任务,使用拓扑排序计算每个任务的最早完成时间
while queue:
u = queue.popleft() # 取出一个没有依赖的任务
for v in graph[u]: # 遍历所有依赖于任务 u 的任务 v
# 更新任务 v 的最早完成时间,确保任务 u 完成后任务 v 才能开始
if earliest_finish[v] < earliest_finish[u] + times[v]:
earliest_finish[v] = earliest_finish[u] + times[v]
# 减少任务 v 的入度,如果入度为 0,加入队列
in_degree[v] -= 1
if in_degree[v] == 0:
queue.append(v)

# 检查是否存在循环依赖
if any(in_degree):
print("存在循环依赖,无法完成所有步骤") # 如果有任务的入度不为 0,表示有循环依赖
else:
print(max(earliest_finish)) # 输出最早完成时间中的最大值,即总的最短执行时间

if __name__ == "__main__":
calculate_min_execution_time()

这段代码实现了一个基于拓扑排序的算法,用来计算多个任务的最短完成时间。任务之间有依赖关系,任务必须按顺序执行。本质上,这个问题涉及有向无环图(DAG)的处理,任务依赖关系可以看作图中的边,任务是图中的节点。代码的目标是找到满足依赖条件的所有任务的最早完成时间。接下来,我将逐步解释代码的各个部分。

1. 导入库

1
2
from collections import deque
import sys
  • deque:双端队列,用于高效地在队列的两端进行操作(如插入和删除),拓扑排序的队列处理需要它。
  • sys:用于从标准输入(stdin)读取数据。在命令行环境下,sys.stdin.readline() 会比 input() 更高效。

2. 读取任务数量和任务执行时间

1
2
N = int(sys.stdin.readline())
times = list(map(int, sys.stdin.readline().split()))
  • N:表示任务的数量,从标准输入中读取第一个值。
  • times:表示每个任务的执行时间,从第二行输入读取,使用 split() 函数将其拆分为整数列表。times[i] 表示任务 i 的执行时间。

3. 读取任务的依赖关系

1
2
3
4
5
6
7
8
dependencies = []
for _ in range(N):
line = sys.stdin.readline().strip()
if line == '-1':
dependencies.append([])
else:
deps = list(map(int, line.split()))
dependencies.append([d-1 for d in deps])
  • dependencies:这是一个二维列表,每个任务的依赖关系被存储在这里。dependencies[i] 表示任务 i 依赖的任务列表。
    • 如果某个任务没有依赖(输入为 -1),则将空列表加入 dependencies
    • 如果有依赖(如输入 “1 3” 表示任务依赖于任务1和任务3),则这些依赖任务编号从 1 开始,我们将其转换为从 0 开始的索引。

4. 构建依赖图和入度数组

1
2
3
4
5
6
graph = [[] for _ in range(N)]
in_degree = [0] * N
for i in range(N):
for dep in dependencies[i]:
graph[dep].append(i)
in_degree[i] += 1
  • graph:用来表示任务之间的依赖关系,构建成一个图。graph[i] 表示任务 i 是其他任务的前置条件,也就是说,如果 i 依赖任务 dep,则任务 dep 的列表中应该添加任务 i
  • in_degree:用来存储每个任务的入度。入度表示有多少其他任务依赖当前任务。初始值为 0,遍历依赖关系时,每当任务 i 依赖某个任务 dep,就将 i 的入度加 1。

5. 初始化队列与最早完成时间

1
2
3
4
5
6
queue = deque()
earliest_finish = [0] * N
for i in range(N):
if in_degree[i] == 0:
queue.append(i)
earliest_finish[i] = times[i]
  • queue:一个双端队列,用来存储入度为 0 的任务,即没有任何其他任务依赖的任务,这些任务可以立即开始执行。
  • earliest_finish:一个数组,用于存储每个任务的最早完成时间。对于入度为 0 的任务,最早完成时间就是其自身的执行时间。

6. 拓扑排序与任务时间计算

1
2
3
4
5
6
7
8
while queue:
u = queue.popleft()
for v in graph[u]:
if earliest_finish[v] < earliest_finish[u] + times[v]:
earliest_finish[v] = earliest_finish[u] + times[v]
in_degree[v] -= 1
if in_degree[v] == 0:
queue.append(v)
  • 这是代码的核心部分,使用拓扑排序的方式来计算每个任务的最早完成时间:
    • 取出队列中的任务 u,并更新所有依赖于 u 的任务 v 的最早完成时间。
    • 计算依赖任务 v 的完成时间。如果当前计算的 v 的完成时间比之前记录的最早时间长,则更新 earliest_finish[v]
    • 同时,将任务 v 的入度减少 1。如果 v 的入度变为 0,说明它的所有依赖都已完成,可以加入队列中进行进一步处理。

7. 检测循环依赖与输出结果

1
2
3
4
if any(in_degree):
print("存在循环依赖,无法完成所有步骤")
else:
print(max(earliest_finish))
  • 在拓扑排序完成后,代码会检查 in_degree 数组是否有非零值。如果存在非零值,说明某些任务没有被处理,意味着任务存在循环依赖(死锁),无法完成所有任务。
  • 如果没有循环依赖,则打印 earliest_finish 中的最大值,表示所有任务的最短完成时间。

程序入口

1
2
if __name__ == "__main__":
calculate_min_execution_time()
  • 这段代码是标准的程序入口,确保在直接运行脚本时调用 calculate_min_execution_time() 函数。

代码总结

该代码实现了一个基于拓扑排序的算法,用来解决有依赖关系的任务调度问题。具体步骤如下:

  1. 读取任务数量、执行时间和依赖关系。
  2. 构建任务之间的依赖图和入度数组。
  3. 使用拓扑排序处理入度为 0 的任务,并逐步更新每个任务的最早完成时间。
  4. 最终输出所有任务完成的最短时间。如果存在循环依赖,则输出错误提示。

这个代码的关键在于如何处理任务之间的依赖关系,使用拓扑排序可以有效地解决任务调度问题,同时检测循环依赖情况。

第2题

软件安装工具

c++

在C++面试中,常见的问题通常会考察候选人的基础知识、面向对象编程(OOP)概念、STL的使用、内存管理、并发编程、以及算法与数据结构的实现能力。以下是一些常见的C++面试问题和解释:

基础知识

  1. C++中指针和引用的区别?

    • 指针是变量,存储的是内存地址,可以重新指向不同的对象,也可以为空。引用是一个别名,必须在声明时初始化,并且之后不能更改它指向的对象,不能为null。
  2. 深拷贝和浅拷贝的区别是什么?

    • 浅拷贝:只复制对象的地址或基本数据类型,多个对象共享同一份资源。
    • 深拷贝:除了复制地址或基本数据类型外,还会复制指向的资源,创建一份新的资源,使得每个对象都有独立的资源。
  3. 虚函数和纯虚函数的区别?

    • 虚函数:基类声明为virtual,可以在派生类中重写,但基类仍然可以提供实现。
    • 纯虚函数:基类中没有实现,必须在派生类中实现,基类含有纯虚函数时,类成为抽象类,不能实例化。
  4. 什么是多态?如何实现?

    • 多态允许通过基类指针或引用来调用派生类的函数。C++中的多态有两种类型:
      • 编译时多态:通过函数重载和运算符重载实现。
      • 运行时多态:通过虚函数和继承关系实现。
  5. 为什么需要析构函数是虚函数?

    • 当通过基类指针删除派生类对象时,如果析构函数不是虚函数,可能只会调用基类的析构函数,而不会调用派生类的析构函数,导致内存泄漏。

STL(Standard Template Library)

  1. STL容器的种类及使用场景?

    • 序列容器vector, deque, list,用于存储有序集合。
    • 关联容器set, map, multiset, multimap,用于存储关联键值对,支持高效的查找。
    • 无序关联容器unordered_set, unordered_map,类似于哈希表,提供常量时间的查找。
  2. 如何选择合适的STL容器?

    • 如果需要动态数组,使用vector
    • 如果频繁插入和删除操作,使用listdeque
    • 如果需要键值对并且需要排序,使用map;如果不需要排序,使用unordered_map

内存管理

  1. 智能指针(Smart Pointer)的类型及区别?

    • **std::unique_ptr**:独占所有权,不能共享,不能复制。
    • **std::shared_ptr**:可以共享所有权,引用计数管理资源的生命周期。
    • **std::weak_ptr**:配合shared_ptr使用,弱引用,不增加引用计数,防止循环引用。
  2. 什么是内存泄漏?如何避免?

    • 内存泄漏是指程序在动态分配内存后未释放,导致内存资源不可用。避免的方法包括使用智能指针和确保每个new都有对应的delete,或者使用RAII(资源获取即初始化)原则。

并发编程

  1. 如何在C++中实现多线程?

    • 使用C++11提供的std::thread类。
      1
      2
      3
      4
      5
      6
      7
      8
      void threadFunc() {
      std::cout << "Hello from thread!" << std::endl;
      }
      int main() {
      std::thread t(threadFunc);
      t.join(); // 等待线程结束
      return 0;
      }
  2. 如何解决线程安全问题?

    • 使用互斥锁(std::mutex)来保护共享数据。
      1
      2
      3
      4
      5
      std::mutex mtx;
      void printData() {
      std::lock_guard<std::mutex> guard(mtx); // 自动锁定和解锁
      // 访问共享数据
      }
  3. 条件变量(Condition Variable)的用途?

    • 条件变量用于线程间同步,允许一个线程等待另一个线程发出信号。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      std::condition_variable cv;
      std::mutex mtx;
      bool ready = false;

      void waitForSignal() {
      std::unique_lock<std::mutex> lock(mtx);
      cv.wait(lock, []{ return ready; });
      // 当ready为true时执行后续操作
      }

      void sendSignal() {
      std::lock_guard<std::mutex> lock(mtx);
      ready = true;
      cv.notify_one();
      }

算法和数据结构

  1. 如何实现单例模式(Singleton)?

    • 单例模式确保一个类只有一个实例,并提供一个全局访问点。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      class Singleton {
      private:
      Singleton() {}
      Singleton(const Singleton&) = delete;
      Singleton& operator=(const Singleton&) = delete;

      public:
      static Singleton& getInstance() {
      static Singleton instance;
      return instance;
      }
      };
  2. 常见排序算法的实现(如快排、归并排序)?

    • 快速排序(Quick Sort):基于分治法,通过选取一个基准点,将数组分为两部分,递归地排序。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      int partition(int arr[], int low, int high) {
      int pivot = arr[high];
      int i = low - 1;
      for (int j = low; j < high; ++j) {
      if (arr[j] < pivot) {
      ++i;
      std::swap(arr[i], arr[j]);
      }
      }
      std::swap(arr[i + 1], arr[high]);
      return i + 1;
      }

      void quickSort(int arr[], int low, int high) {
      if (low < high) {
      int pi = partition(arr, low, high);
      quickSort(arr, low, pi - 1);
      quickSort(arr, pi + 1, high);
      }
      }

进阶问题

  1. 模板元编程是什么?

    • 模板元编程(Template Metaprogramming,TMP)是一种编写可以在编译时执行计算的C++代码的技巧,常用于编译时优化和代码生成。
  2. C++11/14/17的新特性?

    • C++11:智能指针、lambda表达式、auto关键字、范围for循环、std::thread多线程库、nullptr
    • C++14:泛型lambda、decltype(auto)std::make_unique
    • C++17:结构化绑定、if constexpr、并行STL算法。

1.前端如何实现截图?
2.当OPS达到峰值时,该如何处理?
3.js 超过 Number 最大值的数怎么处理?
4.使用同一个链接,如何实现 PC 打开是 web 应用、手机打
开是一个 H5 应用?
5.如何保证用户的使用体验
6.如何解决页面请求接口大规模并发问题
7.设计一套全站请求耗时统计工具
8.大文件上传了解多少
9.H5 如何解决移动端适配问题
10.站点一键换肤的实现方式有哪些?
11.如何实现网页加载进度条?
12.常见图片懒加载方式有哪些?
13.cookie 构成部分有哪些
14.扫码登录实现方式
15.DNS 协议了解多少
16.函数式编程了解多少?
17.前端水印了解多少?
18.什么是领域模型
19.一直在 window 上面挂东西是否有什么风险
20.深度 SEO 优化的方式有哪些,从技术层面来说
21.小程序为什么会有两个线程
22.web 应用中如何对静态资源加载失败的场景做降级处理23.html中前缀为 data- 开头的元素属性是什么?
24.移动端如何实现上拉加载,下拉刷新?
25.如何判断dom元素是否在可视区域

28.如果不使用脚手架,如果用 webpack 构建一个自己的react 应用
29.用 nodejs 实现一个命令行工具,统计输入目录下面指定代码的行数
30.package.json 里面 sideEffects 属性的作用是啥31.script 标签上有那些属性,分别作用是啥?
32.为什么 SPA应用都会提供一个 hash 路由,好处是什么?
33.React如何进行路由变化监听
34.单点登录是是什么,具体流程是什么
35.web 网页如何禁止别人移除水印
36.用户访问页面白屏了,原因是啥,如何排查?
37.[代码实现1JS 中如何实现大对象深度对比
38.如何理解数据驱动视图,有哪些核心要素?
39.vue-cli都做了哪些事儿,有哪些功能?
40.JS 执行 100 万个任务,如何保证浏览器不卡顿?
41.JS 放在 head 里和放在 body 里有什么区别?
42.Eslint 代码检查的过程是啥?
43.虚拟混动加载原理是什么,用JS 代码简单实现一个虚拟滚动加加载。

44.[Reactlreact-router和原生路由区别

45.html的行内元素和块级元素的区别

46.介绍一下 requestldleCallback api

47.documentFragmentapi是什么,有哪些使用场景?

48.git pull 和 git fetch 有啥区别?

49.前端如何做 页面主题色切换

得物

多端兼容,有哪些方式?

前端的多端兼容,主要解决的是一个应用需要在多种平台(如Web、iOS、Android、小程序等)上运行的问题。为了减少多端开发的成本和复杂度,目前有多种技术和策略来实现多端兼容。以下是主要的方式:

1. 响应式设计(Responsive Design)

适用场景:主要针对不同尺寸和分辨率的设备,如PC端和移动端。

  • 核心技术

    • 媒体查询(Media Query):根据屏幕宽度加载不同的CSS样式。
    • 弹性布局(Flexbox)和 Grid布局:处理页面的自动适配。
    • 视口单位(vh、vw):动态控制尺寸。
    • 图片优化:根据屏幕大小加载不同分辨率的图片(srcset)。
  • 优点:简单实现不同尺寸屏幕的适配,易于维护。

  • 缺点:对于需要高度个性化界面或复杂交互逻辑的应用,Web适配可能不够。

2. 跨平台框架

适用场景:需要兼容移动端和Web端,且希望代码重用率高。

(1) React Native + React Web

  • 技术原理:使用React代码,同时支持Web和移动端,通过react-native-web适配不同平台。
  • 特点
    • 使用一套组件,跨Web、iOS、Android开发。
    • 部分逻辑和UI代码复用,但需要根据平台做部分定制。

(2) Flutter

  • 技术原理:Google推出的UI框架,通过同一套Dart代码生成iOS、Android和Web应用。
  • 特点
    • 完全一致的UI和性能体验。
    • Web支持还在不断完善中。

(3) Taro

  • 技术原理:基于React的多端开发框架,支持小程序(微信、支付宝等)、H5 和 原生APP。
  • 特点
    • 一次开发,多端运行。
    • 适用于有小程序和H5需求的场景。

3. Hybrid 混合开发

适用场景:已有Web项目,需要快速扩展到移动端。

(1) Cordova / PhoneGap

  • 技术原理:将HTML、CSS、JavaScript封装在一个WebView中,打包成移动APP。
  • 特点:开发简单,但性能较差。

(2) Ionic

  • 技术原理:基于Web技术(Angular/React),通过WebView运行在移动设备上。
  • 特点:更好的UI组件和体验,但依然存在性能瓶颈。

4. 小程序框架的多端适配

适用场景:需要兼容多个小程序平台(如微信、支付宝、抖音)。

  • uni-app:一套代码,支持微信、支付宝、百度、QQ、字节跳动等小程序。
  • 快应用:针对国产手机生态(如华为、小米)的小程序技术。

5. PWA(Progressive Web App)

适用场景:希望Web应用具备接近Native应用的体验。

  • 技术原理:通过Service Worker、Web App Manifest等技术,让Web应用支持离线缓存、通知推送等功能。
  • 优点:无需下载,跨平台能力强。
  • 缺点:部分设备和浏览器支持不完全。

6. API层的兼容与适配

适用场景:前端多端开发时,需要和后端通信逻辑统一。

  • GraphQL:通过灵活的查询,减少多端开发时的数据冗余。
  • REST API:通过版本管理或设备标识符实现针对不同终端的定制化数据。

7. 组件库和样式层的适配

适用场景:希望UI组件在多个平台保持一致性。

  • Ant Design / Ant Design Mobile:同一设计规范的组件库,分别用于Web和移动端。
  • WeUI:微信团队出品的小程序和H5组件库。

8. 自动化构建与编译工具

适用场景:根据平台不同进行代码的动态编译和打包。

  • Webpack:通过配置多入口,支持针对不同平台输出不同版本的代码。
  • Vite:更快的构建工具,支持多端开发优化。

总结

根据项目需求,可以选择不同的多端兼容方式:

  • 简单适配:响应式设计(PC和移动端的Web兼容)。
  • 高代码重用率:React Native、Flutter、Taro。
  • Web为主、移动端补充:Hybrid开发(Cordova、Ionic)。
  • 小程序生态兼容:uni-app、Taro。
  • 提升Web体验:PWA。

选择合适的方案需要综合考虑性能、开发成本、维护难度以及未来的扩展性。如果你的项目需要支持多个平台,推荐优先选择跨平台框架(如Taro或Flutter),以提高开发效率。

px转vm,是如何实现的?基础的视口宽度如何获取.

px 转换为 vw 是如何实现的?

前端开发中,将 px 单位转换为 vw 是实现响应式设计的重要步骤。vw 是基于视口宽度的相对单位,可以让元素根据屏幕宽度自动调整大小。

  • px(像素):表示具体的固定长度。
  • vw(视口宽度):表示视口宽度的百分比,即 1vw = 视口宽度的 1%

转换公式

假设我们希望将一个 px 值转为 vw,视口宽度为 baseWidth(如:375px、750px 等)。转换的公式为:

$$
vw = \frac{px\ 值}{视口宽度} \times 100
$$

  • 举例:如果设计稿的宽度是 375px,我们要将 100px 转换为 vw
    $$
    100px \to \frac{100}{375} \times 100 = 26.67vw
    $$

如何获取基础视口宽度?

前端开发中,基础视口宽度是指当前页面的浏览器视口(Viewport)的宽度,一般通过 JavaScript 获取。

JavaScript 获取视口宽度的方法

1
2
const viewportWidth = window.innerWidth; // 获取当前视口宽度(单位:px)
console.log(`视口宽度: ${viewportWidth}px`);
  • **window.innerWidth**:浏览器窗口的宽度(包括滚动条)。
  • **document.documentElement.clientWidth**:HTML 根元素的宽度,不包含滚动条。

在 CSS 中动态设置视口宽度

如果需要在 CSS 中以 vw 进行动态设置,可以通过 JavaScript 修改 html 标签的样式:

1
2
3
4
5
const baseWidth = 375; // 设计稿的宽度
const viewportWidth = window.innerWidth; // 当前视口宽度

// 计算缩放比例,并设置到HTML根元素
document.documentElement.style.fontSize = (viewportWidth / baseWidth) * 100 + 'px';

这样,可以使用 rem 单位来进行自适应。例如,在 CSS 中:

1
2
3
4
/* 1rem 将等于设计稿中 100px */
body {
font-size: 1rem;
}

实际项目中的应用:px 到 vw 的自动转换

在项目中,尤其是使用 PostCSS 或类似工具时,可以配置自动将 px 转换为 vw

PostCSS 配置示例

postcss.config.js 中配置:

1
2
3
4
5
6
7
8
9
10
module.exports = {
plugins: {
'postcss-px-to-viewport': {
viewportWidth: 375, // 设计稿的宽度
viewportUnit: 'vw', // 使用vw作为单位
minPixelValue: 1, // 小于1px的值不转换
mediaQuery: false, // 允许在媒体查询中转换
}
}
};

适用场景

  • 移动端开发:一般使用设计稿宽度为 375px 或 750px。
  • PC 端开发:设计稿宽度常为 1440px 或 1920px。

总结

  1. px 到 vw 的公式
    $vw = \frac{px\ 值}{设计稿宽度} \times 100$。

  2. 获取基础视口宽度:使用 window.innerWidthdocument.documentElement.clientWidth

  3. 动态设置字体大小或缩放比例:可以通过 JavaScript 将根元素的 font-size 设置为基于视口的比例。

  4. 工具支持:在构建工具中使用 postcss-px-to-viewport 等插件可以自动完成 px 到 vw 的转换。

rem使用场景

rem 的使用场景和优势

remRoot EM)是一种相对单位,表示相对于 HTML 根元素的字体大小。相比 pxemrem 提供了更好的响应式布局可维护性,广泛用于前端开发中的多种场景。

1. 响应式设计

rem 常用于响应式布局中,因为它可以通过调整根元素的字体大小来统一改变整个页面的比例,从而实现自适应效果。

1
2
3
4
5
6
7
8
9
10
11
html {
font-size: 10px; /* 1rem = 10px */
}

body {
font-size: 1.6rem; /* 16px */
}

.container {
width: 30rem; /* 300px */
}

在移动端开发时,动态调整根元素的 font-size 可以让整个页面的内容根据视口宽度进行缩放。

动态调整 rem

1
2
3
const baseWidth = 375; // 设计稿宽度
const scale = window.innerWidth / baseWidth;
document.documentElement.style.fontSize = `${scale * 10}px`;

优势:通过调整根元素的字体大小,整个页面的布局可以随视口变化进行缩放,避免了手动计算每个元素的尺寸。

2. 字体大小的统一控制

使用 rem 进行字体大小控制时,可以确保在不同设备上保持一致性,并且通过一次调整根元素的大小来影响整个页面的文字。

1
2
3
4
5
6
7
8
9
10
11
html {
font-size: 12px;
}

h1 {
font-size: 2rem; /* 24px */
}

p {
font-size: 1rem; /* 12px */
}

适用于需要大面积文字排版的场景,如博客、资讯类网站,方便字体在不同设备间的一致性管理。

3. 组件库开发中的尺寸标准化

组件库开发中,使用 rem 可以避免组件的样式与用户页面的全局样式发生冲突。开发者通过设置组件的 rem 单位,可以确保组件的大小可控且灵活适应不同页面的根元素字体设置。

1
2
3
4
.button {
padding: 0.8rem 1.2rem; /* 8px 12px */
font-size: 1rem; /* 10px */
}

在组件库开发时,rem 确保组件可以在不同项目中保持一致的尺寸比例,同时让使用者通过改变 html 根元素的大小调整组件的整体大小。

4. 替代 px 避免手动计算

在开发跨平台多屏适配项目时,手动计算 px 对应的尺寸会增加工作量。使用 rem 结合 PostCSS 等工具可以自动将设计稿中的 px 转换为 rem,减少重复工作。

1
2
3
4
5
6
7
8
9
10
module.exports = {
plugins: {
'postcss-px-to-rem': {
rootValue: 16, // 1rem = 16px
unitPrecision: 5, // 保留5位小数
propList: ['*'], // 所有属性都进行转换
minPixelValue: 1 // 小于1px的值不转换
}
}
};

用于设计稿转换,减少开发时的繁琐计算,并确保不同尺寸屏幕上的布局比例一致。

5. SEO 和无障碍支持

使用 rem 替代 px 进行字体设置,有助于提升用户体验,特别是对于需要无障碍支持的网站。当用户通过浏览器设置字体缩放比例时,使用 rem 的页面会自动响应调整,而 px 不会。

  • 示例
    1
    2
    3
    4
    5
    6
    7
    html {
    font-size: 100%; /* 用户可以调整的基础字体大小 */
    }

    p {
    font-size: 1rem; /* 相当于浏览器默认字体大小 */
    }
    用于需要支持高可访问性(Accessibility)的网站,如政府、教育、医疗类站点,确保用户通过系统和浏览器调整字体大小时页面能正常显示。

媒体查询中的灵活使用

rem 也可以用于媒体查询中,通过调整根元素大小来实现页面布局的自适应,而不是对每个元素进行单独处理。

1
2
3
4
5
6
7
8
9
10
11
@media (min-width: 600px) {
html {
font-size: 12px; /* 1rem = 12px */
}
}

@media (min-width: 1200px) {
html {
font-size: 16px; /* 1rem = 16px */
}
}

在响应式布局中,使用 rem 作为基础单位,确保不同屏幕尺寸下页面的比例和布局风格一致。

总结

rem 的使用适用于响应式设计、字体大小控制、组件开发、多屏适配和无障碍支持等场景。相比 pxemrem 更加灵活且易于维护。在开发中,结合动态根元素设置和工具链(如 PostCSS),可以显著提升开发效率,并确保良好的用户体验。

css样式优先级?!important是如何实现的

CSS 样式优先级规则

CSS 优先级由四个层级决定:

  1. 行内样式(Inline styles)
  2. ID 选择器#id
  3. 类选择器、伪类(:hover)、属性选择器([type="text"]
  4. 标签选择器、伪元素(::before::after
  5. 通配符*)、继承的样式、浏览器默认样式

优先级权重的计算

每个选择器的优先级用四位数来表示:
(内联样式, ID, 类/伪类, 标签/伪元素)

  • 内联样式1000
  • ID 选择器0100
  • 类选择器 / 伪类 / 属性选择器0010
  • 标签选择器 / 伪元素0001
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 优先级为 0010 */
.container {
color: red;
}

/* 优先级为 0100 */
#main {
color: blue;
}

/* 优先级为 0001 */
div {
color: green;
}
  • 如果同一个元素应用了 #main.containerdiv 选择器,最终会显示蓝色,因为 ID 选择器的优先级更高。

继承和层叠(Cascading Order)

  • 继承:一些属性(如 colorfont-family)会从父元素继承,如果没有明确定义,则使用继承的样式。
  • 层叠:当优先级相同时,靠后定义的规则会生效。
1
2
3
4
5
6
.container {
color: red; /* 先定义 */
}
.container {
color: green; /* 后定义,生效 */
}

使用 !important 关键字

!important 用于强制覆盖其他样式,无论其优先级如何。它会直接提升该声明的优先级,优先于内联样式和其他规则。

1
2
3
4
5
6
7
.container {
color: red !important; /* 强制生效 */
}

#main {
color: blue;
}

尽管 #main 的 ID 选择器优先级更高,但由于 .container 使用了 !important,所以最终颜色会显示为红色

!important 的实现机制:

  1. 当 CSS 解析器遇到 !important,会将该声明的优先级提升至最高
  2. 即使有多个 !important 声明,CSS 仍会比较选择器的优先级来决定最终生效的样式。

多个 !important 规则的冲突

1
2
3
4
5
6
7
.container {
color: red !important;
}

#main {
color: blue !important;
}

由于两个属性都使用了 !important,此时比较选择器的优先级。
最终 #main 的规则会生效,颜色为蓝色

如何正确使用 !important

虽然 !important 可以解决样式冲突,但不建议频繁使用,因为:

  • 降低代码的可维护性:后续样式难以覆盖或更改。
  • 破坏样式的层叠逻辑:容易导致样式混乱。

推荐的使用场景

  1. 紧急修复样式:需要快速调整某个样式的优先级。
  2. 第三方样式覆盖:如 UI 库自带的样式需要被自定义样式覆盖时。
  3. 内嵌广告或动态内容:避免被外部样式影响。

如何避免过度使用 !important

  • 提高选择器的优先级:使用更精确的选择器替代 !important
  • 模块化 CSS:如使用 BEM 规范,将样式逻辑清晰分离。
  • 调整 CSS 规则顺序:确保后定义的样式优先生效。

总结

CSS 优先级计算顺序:

  1. 内联样式 style=""(1000)。
  2. ID 选择器(0100)。
  3. 类选择器 / 伪类 / 属性选择器(0010)。
  4. 标签选择器 / 伪元素(0001)。

!important 的作用

  • 强制覆盖其他样式,无视优先级规则。
  • 当多个 !important 存在时,选择器优先级仍会决定最终样式。

虽然 !important 是一个有力的工具,但应谨慎使用,尽量通过合理的样式结构和优先级管理来避免样式冲突。

媒体查询是否会改变样式优先级?

严格来说,媒体查询(Media Queries)不会改变 CSS 样式的优先级。媒体查询只是对 特定条件下的样式进行定义,比如根据设备宽度、屏幕分辨率等,仅当条件满足时这些样式才会被应用。但其优先级依旧遵循标准 CSS 选择器的优先级规则,即不会因为在媒体查询中定义而自动提升或降低其权重。

  • 媒体查询本身不会改变选择器的优先级。CSS 选择器的优先级依然按照标准规则计算。
  • 层叠顺序(Cascade Order):当多个选择器优先级相同时,最后定义的规则会覆盖前面的规则。
  • **!important**:可以在媒体查询内部或外部使用,强制提升某个样式的优先级。
  • 媒体查询主要用于在特定条件下覆盖样式,而不是提升或降低样式的优先级。

最佳实践:尽量通过选择器的合理设计和层叠顺序控制样式覆盖,避免频繁使用 !important,以提高代码的可维护性和可预测性。

sessionStorage能够在多个标签页共享数据吗?

不会

为什么要写React hooks,不直接写函数?

React 引入 Hooks 是为了更好地解决组件开发中的一些复杂性限制。虽然 React 组件本质上就是函数,但 Hooks 为函数组件提供了更强大的功能,比如状态管理、生命周期控制等。在 Hooks 出现之前,这些功能只能在类组件中实现。

下面是详细的原因和 Hooks 的优势,以及为什么直接使用普通函数不够。

函数组件的局限性(Hooks 出现前的问题)

1.1 没有状态管理的能力

  • 在 React Hooks 出现前,函数组件无状态的。所有带有状态(state)的逻辑必须用 类组件 来实现。
  • 这导致开发者在写 React 组件时不得不在函数组件类组件之间来回切换,代码风格不统一,增加了维护成本。
1
2
3
4
5
6
7
8
function Counter() {
let count = 0;
const increment = () => {
count += 1;
console.log(count); // 页面不会重新渲染
};
return <button onClick={increment}>Click me</button>;
}

在上面的代码中,虽然 count 可以递增,但因为没有 state,React 不知道什么时候要重新渲染页面。

1.2 复杂组件逻辑难以拆分和复用

使用类组件时,状态逻辑通常集中在 生命周期方法(如 componentDidMountcomponentDidUpdate 中。随着逻辑的复杂化,这些方法会变得臃肿且难以维护

示例:类组件的状态逻辑容易混乱

1
2
3
4
5
6
7
8
9
10
11
12
13
class Counter extends React.Component {
componentDidMount() {
// 数据请求逻辑
}

componentDidUpdate() {
// 依赖状态的逻辑更新
}

render() {
// 视图层逻辑
}
}

当多个逻辑需要在生命周期函数中处理时,代码会变得难以维护,逻辑容易纠缠在一起,不方便复用。

Hooks 的解决方案和优势

Hooks 让函数组件拥有状态(useState)

useState 是 React 的一个 Hook,它为函数组件提供了状态管理能力,从而不需要类组件也能管理状态。

使用 useState 管理状态
1
2
3
4
5
6
7
8
9
10
11
import React, { useState } from 'react';

function Counter() {
const [count, setCount] = useState(0); // 使用 useState 管理状态

return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}

优势

  • 简洁明了:不需要类组件,直接在函数中管理状态。
  • 即时更新setCount 调用后页面自动重新渲染。

逻辑拆分和复用(useEffect 和自定义 Hook)

通过 useEffect,可以将副作用逻辑(如数据请求、DOM 操作)清晰地拆分。并且,开发者可以将状态逻辑抽取为自定义 Hooks,在多个组件之间共享。

使用 useEffect 处理副作用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { useState, useEffect } from 'react';

function Counter() {
const [count, setCount] = useState(0);

// 使用 useEffect 处理副作用
useEffect(() => {
console.log(`Count is ${count}`);
}, [count]); // 只有 count 变化时才执行

return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}

优势

  • 代码更易读、更简洁:将逻辑分散到不同的 useEffect 中,而不是集中在生命周期方法中。
  • 灵活复用:可以抽离为自定义 Hooks,实现逻辑复用。
自定义 Hook 实现逻辑复用
1
2
3
4
5
6
7
8
9
10
11
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(count + 1);
return [count, increment];
}

function Counter() {
const [count, increment] = useCounter(0);

return <button onClick={increment}>Clicked {count} times</button>;
}

避免类组件的复杂性(简化生命周期管)

在类组件中,不同逻辑混杂在生命周期方法里。而 Hooks 则通过 useEffect 将不同逻辑清晰地拆分出来,避免了复杂的生命周期管理。

  • 类组件问题:多个逻辑集中在 componentDidMountcomponentDidUpdate 中。
  • Hooks 解决方案:每个 useEffect 针对特定逻辑,不同逻辑不会互相干扰。

为什么不直接使用普通函数,而需要 Hooks?

虽然 React 组件看起来就是函数,但普通的 JavaScript 函数无法保存状态或处理副作用。React 需要一种机制来跟踪函数组件的状态和副作用,这就是 Hooks 的核心作用。

  • Hooks 是 React 内部机制的一部分
    • React 会在每次渲染时跟踪 useStateuseEffect 等 Hook 的调用顺序。
    • 普通函数不能达到这种效果,因为它们没有与 React 的状态管理和渲染流程相结合的能力。

总结:为什么 React 使用 Hooks?

  • 函数组件也需要状态管理能力useState 让函数组件像类组件一样管理状态。
  • 更简洁的逻辑拆分useEffect 和自定义 Hooks 让副作用逻辑清晰且易于复用。
  • 减少类组件的复杂性:无需再处理复杂的生命周期函数和状态逻辑。
  • 提高代码的可维护性和一致性:避免在类组件和函数组件之间切换,统一开发风格。

React Hooks 并不是普通的函数,而是与 React 渲染机制紧密结合的工具。它们解决了函数组件无法管理状态和副作用的缺陷,使得开发更加高效和清晰。

React hooks中useRef是什么?怎么用?应用Dom,保存一个引用值,方便自行处理副作用

useRef 在 React Hooks 中是什么?怎么用?

useRef 是 React 提供的一个 Hook,主要用于创建一个可变的引用对象。它可以在组件的整个生命周期内存储某个值,并且不会因组件的重新渲染而丢失。

useRef 的两个核心应用场景:

  1. 访问和操作 DOM 元素(如获取元素的引用)。
  2. 存储可变数据(如保存状态的引用而不触发重新渲染)。

1. useRef 的语法

1
const myRef = useRef(initialValue);
  • **myRef**:这是一个包含 .current 属性的对象。
  • **.current**:初始值为 initialValue,可以随时更改。修改 .current 不会触发组件的重新渲染

2. 场景一:获取 DOM 元素的引用

在 React 中,通常不建议直接操作 DOM,但有些场景(如表单输入框聚焦、滚动控制)需要手动访问 DOM 元素。useRef 可以用来存储某个 DOM 元素的引用。

示例:控制输入框的聚焦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React, { useRef } from 'react';

function InputFocus() {
const inputRef = useRef(null); // 创建一个引用

const handleFocus = () => {
inputRef.current.focus(); // 使用引用手动获取焦点
};

return (
<div>
<input ref={inputRef} type="text" placeholder="点击按钮自动聚焦" />
<button onClick={handleFocus}>聚焦输入框</button>
</div>
);
}

export default InputFocus;

解释

  • inputRef 是一个对象,其 .current 属性指向 <input> 元素。
  • 调用 inputRef.current.focus() 手动聚焦输入框,而无需重新渲染组件。

3. 场景二:保存一个可变值的引用

在 React 中,每次组件重新渲染时,普通变量会被重新初始化。但使用 useRef 可以在渲染之间保存一个可变值,即使组件重新渲染,useRef 中存储的值也不会丢失。

示例:保存计时器的引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import React, { useRef, useState } from 'react';

function Timer() {
const [count, setCount] = useState(0);
const timerRef = useRef(null); // 存储计时器的引用

const startTimer = () => {
if (!timerRef.current) {
timerRef.current = setInterval(() => {
setCount((prev) => prev + 1);
}, 1000);
}
};

const stopTimer = () => {
clearInterval(timerRef.current);
timerRef.current = null; // 清空计时器引用
};

return (
<div>
<h1>计数:{count}</h1>
<button onClick={startTimer}>开始计时</button>
<button onClick={stopTimer}>停止计时</button>
</div>
);
}

export default Timer;

解释

  • 计时器的引用保存在 timerRef 中,不会因组件重新渲染而丢失。
  • 这样我们可以确保组件在每次点击按钮时,不会创建多个计时器实例。

4. 场景三:避免不必要的副作用

有时,我们希望保存上一次的值来对比当前的值,或者跟踪某个状态的变化。useRef 是一个非常方便的方式来保存这种引用值,而不触发重新渲染。

示例:保存前一个状态值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, { useEffect, useRef, useState } from 'react';

function PreviousStateExample() {
const [count, setCount] = useState(0);
const prevCountRef = useRef(); // 保存前一个状态值

useEffect(() => {
prevCountRef.current = count; // 在每次渲染后保存当前值
});

const prevCount = prevCountRef.current;

return (
<div>
<h1>当前计数:{count}</h1>
<h2>上一次计数:{prevCount}</h2>
<button onClick={() => setCount(count + 1)}>增加计数</button>
</div>
);
}

export default PreviousStateExample;

解释

  • prevCountRef 存储了上一次的计数值,它不会因为组件的重新渲染而重置。
  • 每次组件重新渲染时,useEffect 会更新 prevCountRef.current 的值。

5. useRef vs. useState 区别

特性 useRef useState
是否触发重新渲染 不会触发重新渲染 修改状态时会触发重新渲染
存储的数据 可变引用(如 DOM 元素、计时器) 状态变量(与 UI 渲染绑定)
常见使用场景 操作 DOM、存储计时器、保存上次值 管理 UI 状态(如计数、表单数据)

6. 总结:useRef 的典型应用场景

  1. 操作 DOM 元素:如手动聚焦、滚动位置控制等。
  2. 存储可变引用值:如计时器 ID,避免组件重复创建副作用。
  3. 保存上一次的状态值:避免多次渲染时状态的丢失或混乱。

useRef 是一个强大的工具,主要用于需要在渲染之间保持数据的一致性且不需要触发组件更新的场景。它与 useState 搭配使用,可以使 React 组件更加简洁、高效。

在useEffect的第二个参数中添加useRef对象,修改对象的current属性会执行吗?

useEffect 的第二个参数中添加 useRef 对象,修改其 current 属性是否会触发副作用?

简短答案:

不会,因为 useRef 的引用对象不会触发组件的重新渲染,也不会影响 useEffect 的依赖更新。即使你修改了 ref.current 的值,React 不会认为这是一个依赖变化,因此不会重新执行 useEffect

详细解释

1. 为什么 useRef 的修改不会触发重新渲染或执行副作用?

  • useRef.current 属性是一个可变的引用,它的值在组件生命周期内保持不变,但修改 .current 不会触发组件的重新渲染。
  • useEffect 的第二个参数是一个依赖数组,当数组中的某个依赖发生变化时,useEffect 的副作用函数才会被重新执行。但 useRef 本质上是一个引用对象,即使它的 .current 属性发生变化,其引用地址不变,所以 React 认为依赖没有变化。

2. 示例:尝试在 useEffect 的依赖中使用 useRef

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import React, { useEffect, useRef, useState } from 'react';

function Example() {
const myRef = useRef(0); // 创建一个 ref 对象
const [count, setCount] = useState(0);

useEffect(() => {
console.log('useEffect 执行了:', myRef.current);
}, [myRef]); // 尝试将 myRef 作为依赖

const incrementRef = () => {
myRef.current += 1; // 修改 ref 的 current 值
console.log('myRef.current:', myRef.current);
};

return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>增加 Count</button>
<button onClick={incrementRef}>增加 Ref</button>
</div>
);
}

export default Example;

预期结果

  1. 当你点击 “增加 Ref” 按钮时,myRef.current 的值会递增。
  2. 但是,尽管 myRef.current 的值变化了,useEffect 不会重新执行,因为 myRef 的引用没有变。

控制台输出

1
2
3
useEffect 执行了: 0
myRef.current: 1
myRef.current: 2

解释:即使 myRef.current 的值更新了,React 认为 myRef 引用对象没有变,因此不会重新执行 useEffect

3. 如何触发 useEffect 执行?

如果你希望当 ref.current 的值发生变化时触发 useEffect,你可以将 **ref.current 的值保存到 useState**,因为 useState 会触发重新渲染。

改进示例:使用 useState 触发更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React, { useEffect, useRef, useState } from 'react';

function Example() {
const myRef = useRef(0); // 创建 ref 对象
const [refValue, setRefValue] = useState(0); // 使用 state 保存 ref 值

useEffect(() => {
console.log('useEffect 执行了:', refValue);
}, [refValue]); // 当 refValue 变化时重新执行 useEffect

const incrementRef = () => {
myRef.current += 1; // 修改 ref 的 current 值
setRefValue(myRef.current); // 将 ref 的值同步到 state 中
};

return (
<div>
<h1>Ref Value: {refValue}</h1>
<button onClick={incrementRef}>增加 Ref</button>
</div>
);
}

export default Example;

输出

1
2
useEffect 执行了: 1
useEffect 执行了: 2

解释:通过将 myRef.current 的值同步到 state,我们确保了 useEffect 会在值变化时执行。

4. 总结:useRefuseEffect 依赖中的行为

  • 直接将 useRef 对象作为依赖不会触发 useEffect 重新执行,因为 useRef 引用地址不变。
  • 修改 ref.current 不会导致组件重新渲染,也不会触发依赖更新。
  • 解决方法:如果你需要在 ref.current 的值变化时触发副作用,建议将其同步到 useState

flex属性

Flexbox 的属性详解

Flexbox 是 CSS 中的一种布局模型,专门用于创建灵活、响应式的布局。它包含两个主要角色:

  1. 容器(父元素):通过设定 display: flex; 激活 Flex 布局。
  2. 子项(直接子元素):在 Flex 容器内的项目受布局规则影响。

1. Flex 容器的属性(父元素属性)

这些属性用于设置Flex 容器的布局规则,影响其内部子项的排列方式。

1.1 display

1
2
3
4
.container {
display: flex; /* 激活 Flex 布局 */
display: inline-flex; /* 创建行内级 Flex 布局 */
}
  • flex:块级 Flex 容器。
  • inline-flex:行内级 Flex 容器。

1.2 flex-direction

用于定义子项排列的主轴方向

1
2
3
4
5
6
.container {
flex-direction: row; /* 默认值,主轴为水平方向(从左到右) */
flex-direction: row-reverse; /* 主轴为水平方向(从右到左) */
flex-direction: column; /* 主轴为垂直方向(从上到下) */
flex-direction: column-reverse; /* 主轴为垂直方向(从下到上) */
}

1.3 justify-content

用于设置子项在主轴上的对齐方式

1
2
3
4
5
6
7
8
.container {
justify-content: flex-start; /* 默认值,从主轴的起点对齐 */
justify-content: flex-end; /* 从主轴的终点对齐 */
justify-content: center; /* 子项居中对齐 */
justify-content: space-between; /* 子项之间平均分布,第一个和最后一个子项贴合两端 */
justify-content: space-around; /* 子项平均分布,两端有间距 */
justify-content: space-evenly; /* 子项平均分布,间距相等 */
}

1.4 align-items

用于设置子项在交叉轴上的对齐方式

1
2
3
4
5
6
7
.container {
align-items: stretch; /* 默认值,子项拉伸以适应容器 */
align-items: flex-start; /* 子项在交叉轴起点对齐 */
align-items: flex-end; /* 子项在交叉轴终点对齐 */
align-items: center; /* 子项在交叉轴居中对齐 */
align-items: baseline; /* 子项的文本基线对齐 */
}

1.5 align-content

用于多行内容在交叉轴上的对齐方式,仅在有多行时生效。

1
2
3
4
5
6
7
8
.container {
align-content: flex-start; /* 各行在交叉轴起点对齐 */
align-content: flex-end; /* 各行在交叉轴终点对齐 */
align-content: center; /* 各行在交叉轴居中对齐 */
align-content: space-between; /* 各行平均分布,两端贴合 */
align-content: space-around; /* 各行平均分布,两端有间距 */
align-content: stretch; /* 各行拉伸填满容器 */
}

1.6 flex-wrap

用于控制子项是否换行

1
2
3
4
5
.container {
flex-wrap: nowrap; /* 默认值,不换行 */
flex-wrap: wrap; /* 子项换行,溢出部分另起一行 */
flex-wrap: wrap-reverse; /* 子项换行,但新行排在上面 */
}

1.7 gap / row-gap / column-gap

用于设置子项之间的间距

1
2
3
4
5
.container {
gap: 10px; /* 行和列之间的统一间距 */
row-gap: 10px; /* 设置行间距 */
column-gap: 20px; /* 设置列间距 */
}

2. Flex 子项的属性(子元素属性)

这些属性应用于 Flex 容器内的子项,用于控制它们的大小、排列和伸缩。


2.1 order

用于控制子项的排列顺序。数值越小,排列越靠前。

1
2
3
.item {
order: 1; /* 默认值为 0 */
}

2.2 flex-grow

用于定义子项在主轴方向上的扩展比例。当容器有剩余空间时,flex-grow 决定子项占用剩余空间的比例。

1
2
3
.item {
flex-grow: 1; /* 子项会根据剩余空间进行扩展 */
}
  • 如果所有子项的 flex-grow 都为 1,它们将平分剩余空间
  • 如果一个子项的 flex-grow2,而其他子项为 1,那么该子项将占据双倍的剩余空间。

2.3 flex-shrink

用于定义子项在主轴方向上的收缩比例。当容器空间不足时,flex-shrink 决定子项如何缩小。

1
2
3
.item {
flex-shrink: 1; /* 默认值为 1 */
}
  • 如果 flex-shrink0,则子项不会收缩。

2.4 flex-basis

用于定义子项的初始尺寸,即它在伸缩前的大小。

1
2
3
.item {
flex-basis: 100px; /* 子项的初始大小为 100px */
}
  • flex-basis 优先级高于 widthheight

2.5 flex(简写属性)

flexflex-growflex-shrinkflex-basis 的简写。

1
2
3
4
5
6
7
.item {
flex: 1 1 100px; /* 等同于:
flex-grow: 1;
flex-shrink: 1;
flex-basis: 100px;
*/
}
  • 常见简写
    1
    2
    flex: 1; /* 等同于 flex: 1 1 0%; */
    flex: none; /* 等同于 flex: 0 0 auto; */

2.6 align-self

用于单独设置某个子项在交叉轴上的对齐方式,它会覆盖父容器的 align-items 设置。

1
2
3
4
5
6
.item {
align-self: flex-start; /* 该子项在交叉轴起点对齐 */
align-self: flex-end; /* 该子项在交叉轴终点对齐 */
align-self: center; /* 该子项在交叉轴居中对齐 */
align-self: stretch; /* 该子项在交叉轴拉伸 */
}

3. 总结

父元素属性(容器) 作用
display 激活 Flex 布局
flex-direction 设置主轴方向
justify-content 控制主轴上的对齐方式
align-items 控制交叉轴上的对齐方式
align-content 控制多行内容的对齐方式
flex-wrap 是否允许子项换行
gap / row-gap / column-gap 设置子项之间的间距
子元素属性(子项) 作用
order 控制子项的排列顺序
flex-grow 控制子项的扩展比例
flex-shrink 控制子项的收缩比例
flex-basis 设置子项的初始大小
flex 简写属性,用于同时设置扩展、收缩和初始大小
align-self 设置单个子项在交叉轴上的对齐方式

Flexbox 让布局更灵活和高效,适用于一维布局(如水平或垂直排列)。在复杂布局场景下(如网格布局),可以与 CSS Grid 结合使用,获得更强大的布局能力。

vite和webpack

Vite 与 Webpack 的区别,以及为什么 Vite 更快

Vite 和 Webpack 都是现代前端开发中的构建工具,但它们的架构设计工作方式有显著差异。Vite 解决了 Webpack 在开发时的性能瓶颈,并提供了更快的开发体验。下面详细讲解它们的主要区别以及 Vite 快速的原因。


1. Vite 与 Webpack 的主要区别

特性 Vite Webpack
构建方式 即时按需加载(ESM 模式) 打包构建(Bundle-based)
开发服务器启动 极快(无需打包,直接启动) 较慢(必须打包所有资源后才能启动)
热更新(HMR) 模块级别更新,更新更快 整体模块更新,更新粒度较大
配置复杂度 简单,内置很多默认配置 复杂,需要详细配置
支持现代浏览器 基于 原生 ESM 支持 需要使用 polyfill 等兼容性代码
首次打包速度 (开发模式无打包) ,打包所有模块
生产构建 使用 Rollup 进行优化打包 使用自身的打包逻辑
生态和插件 生态系统快速发展,插件可复用(Rollup 插件) 丰富且成熟的生态

2. Vite 为什么比 Webpack 快

2.1 架构设计不同

  • Webpack:在启动开发服务器前,需要打包所有资源(JavaScript、CSS、图片等),然后通过一个大的 Bundle 文件在浏览器中运行。这在项目复杂度增加时会显著变慢。

  • Vite:Vite 依赖浏览器原生的 ESM(ES Modules)支持,在开发模式中按需加载文件。它不需要打包,浏览器会直接请求模块文件并解析依赖。

2.2 直接使用 ESM 模块

  • Vite 的开发模式不会进行传统的打包,而是利用浏览器的 ESM 支持,直接将每个模块文件作为一个独立请求加载。浏览器只会请求实际需要的模块,避免了大文件的打包。

示例

在 Vite 中,浏览器会直接加载:

1
import { someFunction } from './module.js';

而 Webpack 会将所有依赖打包为一个 Bundle。

2.3 更快的开发服务器启动

  • Webpack:启动开发服务器前需要先完成所有文件的打包。随着项目规模增大,首次打包的时间可能会显著增加。

  • Vite:启动时不打包整个项目,而是直接响应浏览器的模块请求。这让 Vite 的开发服务器可以瞬间启动,即使在大型项目中也能显著提高启动速度。

2.4 模块级热更新(HMR)

  • Webpack:HMR(Hot Module Replacement,热模块替换)机制会重新打包部分文件,然后更新浏览器。这在大型项目中仍可能导致延迟。

  • Vite:HMR 更快,因为它直接替换受影响的 ESM 模块,而不是重新打包整个模块依赖树。


3. 为什么 Vite 的架构更适合现代开发?

3.1 现代浏览器的原生支持

Vite 的开发模式依赖于现代浏览器的 ES Modules(原生模块支持)。这意味着:

  • 无需额外的打包和编译,浏览器能直接理解 JavaScript 模块。
  • 不需要处理像 Webpack 那样复杂的模块解析和打包逻辑。

3.2 面向渐进式的模块加载

Vite 的 ESM 模块加载是渐进式的,即:

  • 只有在浏览器请求某个模块时,Vite 才会编译并返回该模块。
  • 未被请求的模块不会被预先编译和加载,避免了不必要的工作。

4. Vite 的生产构建与 Webpack 的对比

  • Vite 在开发模式中避免了打包,但在生产模式下,它使用 Rollup 进行打包优化,确保输出的代码体积小且性能高
  • Webpack 则在生产构建中继续使用自己的打包逻辑,并通过 Tree Shaking、代码分割等优化手段减小包体积。

5. 哪些项目适合使用 Vite?

  • 中小型项目或个人项目:Vite 的极速启动和简洁配置非常适合。
  • Vue、React 等框架的现代项目:Vite 有非常好的官方支持,且能与这些框架无缝结合。
  • 需要高频热更新的开发场景:Vite 的模块级 HMR 能提供更流畅的开发体验。

6. 总结:Vite 为什么更快?

  1. 即时按需加载:Vite 不需要在开发模式下打包整个项目,依赖浏览器的 ESM 支持,按需加载模块。
  2. 更快的启动速度:Vite 直接启动开发服务器,无需预先打包所有模块。
  3. 模块级 HMR:Vite 的 HMR 更新粒度更小,更加高效。
  4. 简单的配置:Vite 默认配置良好,减少了复杂的配置需求。

Vite 的架构设计使其成为现代前端开发的理想选择,特别适合需要快速开发、频繁热更新的项目。相比之下,Webpack 仍然适合大型复杂项目的构建,但 Vite 正逐渐成为新一代构建工具的主流选择。

防止cookie被篡改

由于 Cookie 存储在客户端(用户的浏览器),它有被篡改的风险。如果攻击者可以篡改 Cookie 中的数据,比如会话标识或用户角色信息,就可能导致严重的安全问题。为了防止 Cookie 被篡改,我们可以结合多种安全策略来保护数据的完整性和安全性。


1. 使用 HTTP-only 属性

  • 防止 JavaScript 访问 Cookie,减少 XSS(跨站脚本攻击)的风险。
1
Set-Cookie: session_id=abc123; HttpOnly
  • 解释
    • HttpOnly 属性设置后,Cookie 无法通过 document.cookie 在客户端读取或修改,只能通过 HTTP 请求头自动发送。

2. 使用 Secure 属性

  • 强制 Cookie 只能通过 HTTPS 传输,防止数据在传输过程中被窃取或篡改。
1
Set-Cookie: session_id=abc123; Secure
  • 解释
    • Secure 属性要求 Cookie 只能通过 HTTPS 连接传输,避免中间人攻击(MITM)时 Cookie 被劫持或修改。

3. 使用 SameSite 属性

  • 防止 CSRF(跨站请求伪造)攻击,通过限制第三方网站发送 Cookie 的能力。
1
Set-Cookie: session_id=abc123; SameSite=Strict
  • SameSite 取值:
    • Strict:完全阻止第三方网站发送 Cookie。
    • Lax:只在部分情况下(如 GET 请求)发送 Cookie。
    • None:允许跨站发送,但必须搭配 Secure

4. 使用加密和签名

  • 加密:将敏感信息加密后存入 Cookie,只有服务器能解密查看。
  • 签名:为 Cookie 添加签名,防止数据被篡改。

示例:使用 HMAC 签名防篡改

  1. 服务器端生成 Cookie(使用 HMAC 算法签名):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const crypto = require('crypto');
    const secret = 'your-secret-key';
    const value = 'user_id=123';

    // 使用 HMAC 生成签名
    const signature = crypto
    .createHmac('sha256', secret)
    .update(value)
    .digest('hex');

    const cookie = `${value}; signature=${signature}`;
    res.setHeader('Set-Cookie', cookie);
  2. 服务器端验证 Cookie

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const [value, signature] = req.cookies.session_id.split('; signature=');

    // 验证签名是否匹配
    const validSignature = crypto
    .createHmac('sha256', secret)
    .update(value)
    .digest('hex');

    if (signature !== validSignature) {
    throw new Error('Cookie 被篡改');
    }
  • 解释
    • 服务器在生成 Cookie 时使用 HMAC 签名,客户端无法伪造或篡改。
    • 服务器在读取 Cookie 时验证签名,发现不匹配就拒绝请求。

  • 将 Cookie 的 max-ageexpires 设置为较短时间,即使被窃取也能减少其有效时间。

示例:设置短生命周期

1
Set-Cookie: session_id=abc123; Max-Age=600
  • 解释Max-Age=600 表示该 Cookie 仅在 10 分钟内有效。

6. 验证服务器端会话数据

  • 在服务器端为每个会话维护必要的状态信息,而不是完全依赖客户端的 Cookie。

示例:服务端会话验证

1
2
3
4
5
6
7
8
9
10
app.use((req, res, next) => {
const sessionId = req.cookies.session_id;

// 验证 session_id 是否存在于服务器会话存储中
if (!sessionStore[sessionId]) {
res.status(401).send('无效的会话');
} else {
next();
}
});
  • 解释:即使攻击者伪造了一个 Cookie,服务器也会通过会话存储验证它的合法性。

  1. HttpOnly:防止 JavaScript 操作 Cookie,避免 XSS 攻击。
  2. Secure:只允许通过 HTTPS 传输 Cookie,防止中间人攻击。
  3. SameSite:限制第三方站点使用 Cookie,避免 CSRF 攻击。
  4. 加密与签名:为 Cookie 数据加密或添加 HMAC 签名,防止篡改。
  5. 短生命周期:设置合理的过期时间,降低 Cookie 被滥用的风险。
  6. 服务端验证会话:不要完全依赖客户端 Cookie,需在服务端验证会话数据。

通过以上策略的组合,你可以显著提高 Cookie 的安全性,防止其被恶意篡改和滥用。

async和await的原理

async / await 的原理详解

async / await 是基于 Promise 的语法糖,主要用于简化 异步代码的书写和管理。它的本质是利用 Promise 进行异步操作,并通过 await 将异步代码转为同步的书写形式,从而提高代码的可读性和可维护性。

下面我们从 工作原理事件循环机制代码执行流程 来分析 async / await 的实现原理。


1. async / await 的基本概念

  • async:用于定义一个异步函数,其返回值会被自动转换为 Promise
  • await:只能在 async 函数中使用,它会暂停函数的执行,直到 await 的 Promise 解析完成(resolve),然后继续执行。

示例:基本用法

1
2
3
4
5
6
async function fetchData() {
const data = await fetch('https://api.example.com/data');
console.log('数据已获取:', data);
}

fetchData();

解释

  1. await fetch() 会暂停 fetchData 函数的执行,直到 fetch 返回的 Promise 被解析。
  2. 一旦 Promise 被 resolvedata 会接收到结果,函数继续执行。

2. async / await 的底层原理

async / await 的底层是基于 Promise 和生成器(Generator) 实现的。它将复杂的异步逻辑拆分为同步的结构,但并不阻塞 JavaScript 的 事件循环(Event Loop)

底层原理:基于 Promise 的语法糖

  1. async 函数会自动将返回值包装成一个 Promise
  2. await 会暂停当前异步函数的执行,等待 Promise 被resolvereject
  3. 事件循环机制确保在 Promise 完成后,函数的剩余部分会在微任务队列(Microtask Queue)中执行。

模拟实现:async / await 的原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function asyncFunction(generatorFn) {
const generator = generatorFn();

function handleResult(result) {
if (result.done) return Promise.resolve(result.value); // 完成时返回结果
return Promise.resolve(result.value).then(
res => handleResult(generator.next(res)), // 递归执行
err => handleResult(generator.throw(err)) // 处理异常
);
}

return handleResult(generator.next());
}

asyncFunction(function* () {
const data = yield fetch('https://api.example.com/data');
console.log('数据:', data);
});

解释:

  • 上面的代码模拟了 async / await 的实现原理。
  • 使用 Generator 函数来暂停和恢复执行,并通过 Promise 进行异步操作。

3. 事件循环与 await 的执行顺序

await 会将异步操作放入 微任务队列(Microtask Queue),并在当前同步任务执行完毕后立刻执行微任务。这是因为 JavaScript 是单线程的await 只能在同步任务完成后继续执行。

示例:await 执行顺序

1
2
3
4
5
6
7
8
9
10
11
console.log('Start');

async function asyncFunc() {
console.log('Inside asyncFunc');
await Promise.resolve();
console.log('After await');
}

asyncFunc();

console.log('End');

执行顺序

  1. console.log('Start') 立即执行。
  2. asyncFunc() 调用,打印 **Inside asyncFunc**。
  3. await Promise.resolve()微任务加入微任务队列
  4. console.log('End') 立即执行(同步任务)。
  5. 执行 微任务,打印 **After await**。

输出

1
2
3
4
Start
Inside asyncFunc
End
After await

4. 错误处理机制

async 函数中,错误可以使用 try...catch 捕获,就像同步代码一样。

示例:错误处理

1
2
3
4
5
6
7
8
9
10
11
async function fetchData() {
try {
const response = await fetch('https://api.invalid-url.com');
const data = await response.json();
console.log(data);
} catch (error) {
console.error('发生错误:', error);
}
}

fetchData();
  • 如果 fetchresponse.json() 抛出错误,catch 会捕获并处理错误。

5. async / await 的优缺点

优点

  1. 代码结构清晰:避免了回调地狱,使异步逻辑看起来像同步代码。
  2. 错误处理简单:支持 try...catch,使得错误处理更加直观。
  3. 提升可读性:代码更易于理解和维护。

缺点

  1. 依赖于 Promise:本质上还是基于 Promise,只是语法简化。
  2. 阻塞异步代码await 会暂停当前 async 函数的执行,如果滥用可能会导致性能问题。

6. 总结:async / await 的原理

  • async / awaitPromise 的语法糖,简化了异步操作的书写方式。
  • async 函数会自动返回一个 Promise,使得代码更易于管理。
  • await 会暂停当前函数的执行,等待 Promise 完成,然后恢复执行。
  • 它与 事件循环(Event Loop) 配合,使得代码不会阻塞主线程。

通过 async / await,JavaScript 的异步代码变得更具可读性和可维护性,这是现代 JavaScript 中处理异步逻辑的推荐方式。