记录一次糟糕的面试

26 阅读16分钟

先介绍下本人情况,base 西安,20 年民本金融学毕业,在教培晃荡两年被优化,遂在某培训班转型前端开发,包了两年经验,后面入职了一家初创自研,成功混了两年后项目停住被优化,从去年 11 月到现在躺了一年多,期间入职一家小公司天天加班到 11 点,一周后老板跑路,目前正在仲裁,最近在重拾面试题,前几天面试了软通推的一个项目,回答的非常糟糕,期间自己都没绷住,把回忆起的面试题分享给大家,希望能帮助到和我一样在求职的小伙伴;下面是我回忆的面试题,后面我会给出答案。

/…

flex 布局怎么实现左边固定,右边自适应?

react 中常见的 hook

webpack 中 loader 和 plugin 的区别

写一个深拷贝

写一个数组去重

后台项目中怎么做权限

im 通讯中怎么做心跳检测

defer 和 async 的区别

什么是 treeshake

js 事件循环机制

ts 中的 interface 和 type 的区别和使用场景

什么是宏任务?什么是微任务?

讲讲 promise?

怎么让元素页面居中

…/

1. flex 布局怎么实现左边固定,右边自适应?

Flex 布局实现该效果核心是利用 flex 属性的分配规则,有两种常用方案:

方案 1:基础版(推荐)

/* 容器 */
.container {
  display: flex;
  width: 100%;
  height: 500px;
}
/* 左侧固定 */
.left {
  width: 200px; /* 固定宽度 */
  flex: none; /* 禁止伸缩 */
  background: #eee;
}
/* 右侧自适应 */
.right {
  flex: 1; /* 占据剩余所有空间 */
  background: #ddd;
}

方案 2:进阶版(兼容特殊场景)

.container {
  display: flex;
  width: 100%;
}
.left {
  flex-basis: 200px; /* 固定基准宽度 */
  flex-shrink: 0; /* 禁止缩小 */
}
.right {
  flex-grow: 1; /* 允许放大占满剩余空间 */
  flex-shrink: 1; /* 允许缩小(适配容器不足场景) */
}

核心原理​:flex:1 等价于 flex-grow:1; flex-shrink:1; flex-basis:0%,表示右侧元素可伸缩并占据剩余空间;左侧通过固定宽度 + 禁止伸缩保证尺寸不变。

2. React 中常见的 Hook

React Hook 是 React 16.8 新增特性,用于在函数组件中使用状态和生命周期等特性,核心常用 Hook 如下:

Hook 名称核心作用使用场景
useState声明状态变量,返回[状态, 修改状态的方法]管理组件内部简单状态(如输入框值、开关状态)
useEffect处理副作用(DOM 操作、请求、订阅),替代类组件生命周期数据请求、监听窗口大小、清理定时器
useRef创建可变的引用对象,值更新不触发组件重渲染获取 DOM 元素、保存定时器 ID、跨渲染周期保存值
useContext接收上下文对象,跨组件共享数据(无需 props 逐层传递)全局状态(如主题、用户信息)共享
useReducer替代 useState,处理复杂状态逻辑(类似 Redux)状态逻辑复杂、多个子值的状态管理
useMemo缓存计算结果,避免重复计算复杂数据处理、避免子组件不必要重渲染
useCallback缓存函数引用,避免函数重复创建传递给子组件的回调函数(配合 memo 使用)
useLayoutEffect同步执行副作用,在 DOM 更新后、浏览器绘制前触发需要同步修改 DOM 的场景(如获取 DOM 尺寸)

3. Webpack 中 loader 和 plugin 的区别

Loader 和 Plugin 是 Webpack 核心扩展机制,但定位和作用完全不同:

| 维度 | Loader | Plugin | 扩展 Webpack 生命周期功能(打包优化、资源管理) | | 处理对象 | 单个文件(模块),专注“文件转换” | 整个打包流程,专注“流程控制” | | 执行时机 | 模块解析阶段(module 阶段) | 打包的各个生命周期钩子(compiler/hook) | | 使用方式 | 配置在 module.rules 数组中 | 配置在 plugins 数组中(需实例化) | | 示例 | css-loader(解析 CSS)、babel-loader(转译 ES6+)、file-loader(处理静态资源) | HtmlWebpackPlugin(生成 HTML)、MiniCssExtractPlugin(提取 CSS)、CleanWebpackPlugin(清理输出目录) | | 开发方式 | 导出函数,接收源码返回处理后代码 | 基于 Tapable 钩子,通过 compiler 对象注册事件 | | ​核心总结​:Loader 解决“文件怎么转”,Plugin 解决“打包流程怎么扩展”。 | | |

4. 写一个深拷贝

完整版深拷贝(支持常见类型 + 循环引用)

function deepClone(target, map = new WeakMap()) {
  // 1. 处理基本类型和null
  if (target === null || typeof target !== 'object') {
    return target;
  }

  // 2. 处理循环引用
  if (map.has(target)) {
    return map.get(target);
  }

  // 3. 处理日期/正则
  if (target instanceof Date) {
    const newDate = new Date(target);
    map.set(target, newDate);
    return newDate;
  }
  if (target instanceof RegExp) {
    const newReg = new RegExp(target.source, target.flags);
    map.set(target, newReg);
    return newReg;
  }

  // 4. 处理数组/对象
  const cloneTarget = Array.isArray(target) ? [] : {};
  map.set(target, cloneTarget); // 缓存当前对象,解决循环引用

  // 5. 递归拷贝属性
  Reflect.ownKeys(target).forEach(key => {
    cloneTarget[key] = deepClone(target[key], map);
  });

  return cloneTarget;
}

// 测试示例
const obj = { a: 1, b: [1, 2], c: { d: 3 } };
obj.self = obj; // 循环引用
const newObj = deepClone(obj);
console.log(newObj); // 完整拷贝,无循环引用问题

核心特性​:支持基本类型、数组、对象、日期、正则,解决循环引用问题,兼容 Symbol 键名。

5. 写一个数组去重

方案 1:ES6 简洁版(推荐,性能优)

function uniqueArr(arr) {
  // 利用Set的唯一性,支持基本类型去重
  return Array.isArray(arr) ? [...new Set(arr)] : [];
}

方案 2:兼容版(支持复杂场景,可自定义去重规则)

function uniqueArr(arr) {
  if (!Array.isArray(arr)) return [];
  const result = [];
  const map = new Map();

  arr.forEach(item => {
    // 处理引用类型:JSON.stringify转换(或自定义唯一标识)
    const key = typeof item === 'object' ? JSON.stringify(item) : item;
    if (!map.has(key)) {
      map.set(key, true);
      result.push(item);
    }
  });
  return result;
}

// 测试
console.log(uniqueArr([1, 2, 2, 3, '3', { a: 1 }, { a: 1 }]));
// 输出:[1, 2, 3, '3', { a: 1 }]

适用场景​:方案 1 适合基本类型数组;方案 2 适合包含引用类型的数组。

6. 后台项目中怎么做权限

后台系统权限控制需覆盖路由、菜单、按钮、接口四层,核心方案如下:

1. 权限设计思路

采用“RBAC(基于角色的访问控制)”模型:用户 → 角色 → 权限(菜单/按钮/接口)。

2. 具体实现步骤

(1)登录鉴权

  • 登录后获取用户信息 + 权限列表(如 permissions: ['user:list', 'user:add']),存储在 localStorage/Vuex/Redux 中。
  • 路由守卫(如 Vue 的 router.beforeEach)校验登录状态,未登录跳转登录页。

(2)路由权限

  • 静态路由​:通用路由(登录页、404)直接注册。
  • 动态路由​:根据权限列表过滤路由表,通过 router.addRoute 动态添加可访问路由。
// Vue示例:动态注册路由
const asyncRoutes = [/* 所有需要权限的路由 */];
// 过滤有权限的路由
const accessibleRoutes = asyncRoutes.filter(route => {
  return store.getters.permissions.includes(route.meta.permission);
});
// 动态添加
accessibleRoutes.forEach(route => router.addRoute(route));

(3)菜单权限

  • 菜单渲染时根据权限列表过滤,无权限的菜单不渲染。
<!-- Vue示例 -->
<template>
  <el-menu>
    <el-menu-item
      v-for="menu in menuList"
      :key="menu.id"
      v-if="hasPermission(menu.permission)"
    >
      {{ menu.name }}
    </el-menu-item>
  </el-menu>
</template>
<script>
export default {
  methods: {
    hasPermission(permission) {
      return this.$store.getters.permissions.includes(permission);
    }
  }
}
</script>

(4)按钮权限

  • 封装权限指令,无权限的按钮隐藏/禁用。
// Vue自定义指令示例
Vue.directive('permission', {
  inserted(el, binding) {
    const { value } = binding;
    const permissions = store.getters.permissions;
    if (value && !permissions.includes(value)) {
      el.parentNode.removeChild(el); // 移除按钮
    }
  }
});
// 使用:<button v-permission="'user:delete'">删除</button>

(5)接口权限

  • 请求拦截器携带 Token,后端校验 Token 及权限,无权限返回 403,前端跳转无权限页。

7. IM 通讯中怎么做心跳检测

心跳检测用于维持 IM 长连接、检测连接状态,核心是“定时双向通信”,实现方案如下:

1. 核心原理

客户端定时发送“心跳包” → 服务端接收后立即回复“心跳响应” → 客户端超时未收到响应则判定连接断开,触发重连。

2. 具体实现(WebSocket 示例)

class IMHeartbeat {
  constructor(wsUrl) {
    this.ws = null;
    this.wsUrl = wsUrl;
    this.heartbeatInterval = 30000; // 30秒发一次心跳
    this.heartbeatTimer = null; // 心跳定时器
    this.reconnectTimer = null; // 重连定时器
    this.reconnectMaxTimes = 5; // 最大重连次数
    this.reconnectCount = 0; // 已重连次数
  }

  // 初始化连接
  init() {
    this.ws = new WebSocket(this.wsUrl);
    this.bindEvent();
  }

  // 绑定事件
  bindEvent() {
    // 连接成功
    this.ws.onopen = () => {
      console.log('连接成功');
      this.startHeartbeat(); // 启动心跳
      this.reconnectCount = 0; // 重置重连次数
    };

    // 接收消息
    this.ws.onmessage = (e) => {
      const data = JSON.parse(e.data);
      // 收到心跳响应,重置定时器
      if (data.type === 'heartbeat') {
        this.resetHeartbeat();
      }
    };

    // 连接关闭
    this.ws.onclose = () => {
      console.log('连接关闭,开始重连');
      this.stopHeartbeat();
      this.reconnect();
    };

    // 连接错误
    this.ws.onerror = (err) => {
      console.error('连接错误', err);
      this.ws.close();
    };
  }

  // 启动心跳
  startHeartbeat() {
    this.heartbeatTimer = setInterval(() => {
      if (this.ws.readyState === WebSocket.OPEN) {
        // 发送心跳包
        this.ws.send(JSON.stringify({ type: 'heartbeat', time: Date.now() }));
      }
    }, this.heartbeatInterval);
  }

  // 重置心跳定时器(收到响应后)
  resetHeartbeat() {
    clearInterval(this.heartbeatTimer);
    this.startHeartbeat();
  }

  // 停止心跳
  stopHeartbeat() {
    clearInterval(this.heartbeatTimer);
    this.heartbeatTimer = null;
  }

  // 重连逻辑
  reconnect() {
    if (this.reconnectCount >= this.reconnectMaxTimes) {
      console.log('重连次数耗尽,停止重连');
      return;
    }
    this.reconnectTimer = setTimeout(() => {
      this.reconnectCount++;
      console.log(`第${this.reconnectCount}次重连`);
      this.init();
    }, 5000); // 5秒重连一次
  }
}

// 使用
const im = new IMHeartbeat('ws://localhost:8080/im');
im.init();

3. 服务端配合

  • 服务端接收心跳包后,立即返回 { type: 'heartbeat', code: 200 }
  • 服务端也可主动检测:超时未收到客户端心跳则主动断开连接,避免无效连接占用资源。

4. 优化点

  • 心跳间隔根据网络环境动态调整(弱网增大间隔)。
  • 重连采用指数退避策略(第一次 5 秒,第二次 10 秒,第三次 20 秒)。
  • 断线时缓存未发送的消息,重连后补发。

8. defer 和 async 的区别

deferasync<script> 标签的属性,用于异步加载 JS 文件,核心区别如下:

维度deferasync
执行时机文档解析完成后、DOMContentLoaded 事件触发前执行脚本下载完成后立即执行(暂停文档解析)
执行顺序按脚本在页面中的顺序执行谁先下载完谁先执行(无序)
适用场景依赖 DOM、需按顺序执行的脚本(如 jQuery 插件)独立无依赖的脚本(如统计、广告脚本)
加载特性异步加载,不阻塞文档解析异步加载,不阻塞文档解析

可视化流程

// 正常脚本(阻塞)
解析HTML → 下载脚本 → 执行脚本 → 继续解析HTML

// defer
解析HTML → 异步下载脚本 → 解析完成 → 按顺序执行脚本 → DOMContentLoaded

// async
解析HTML → 异步下载脚本 → 下载完成 → 暂停解析 → 执行脚本 → 继续解析HTML

代码示例

<!-- defer:按顺序执行,DOM解析完执行 -->
<script src="a.js" defer></script>
<script src="b.js" defer></script> <!-- 一定在a.js之后执行 -->

<!-- async:无序执行,下载完立即执行 -->
<script src="a.js" async></script>
<script src="b.js" async></script> <!-- 可能先于a.js执行 -->

9. 什么是 Tree Shaking

Tree Shaking(摇树优化)是 Webpack/Rollup 等打包工具的优化技术,核心是​**剔除代码中未被使用的死代码(dead code)**​,减少打包体积。

1. 核心原理

  • 基于 ES6 模块的静态分析特性(import/export 是静态的,编译时确定依赖关系)。
  • 打包时遍历所有模块依赖,标记未被引用的代码,最终从打包产物中删除。

2. 启用条件

  • 模块系统:必须使用 ES6 模块(import/export),CommonJS(require)不支持(动态加载,无法静态分析)。
  • 模式:Webpack 需设置 mode: 'production'(默认开启 Tree Shaking)。
  • 配置:package.json 中声明 sideEffects(标记无副作用的文件,可安全删除)。
// package.json
{
  "sideEffects": [
    "*.css", // CSS文件有副作用(样式生效),不删除
    "*.less"
  ]
}

3. 示例

// utils.js(源码)
export const add = (a, b) => a + b;
export const minus = (a, b) => a - b;

// index.js(业务代码)
import { add } from './utils.js'; // 仅引用add
console.log(add(1, 2));

// 打包后(Tree Shaking后)
// 仅保留add函数,minus被剔除
const add = (a, b) => a + b;
console.log(add(1, 2));

4. 注意事项

  • 副作用代码(如修改全局变量、DOM 操作)需标记 sideEffects,否则会被误删。
  • 仅对顶层导出有效,嵌套对象的未使用属性无法剔除。

10. JS 事件循环机制

JS 事件循环(Event Loop)是 JS 处理异步任务的核心机制,用于解决单线程下的异步执行问题,核心规则如下:

1. 核心概念

  • 执行栈​:同步任务按顺序进入执行栈执行。
  • 任务队列​:异步任务完成后,回调函数进入任务队列(分宏任务队列、微任务队列)。
  • 事件循环​:执行栈为空时,先清空所有微任务队列,再从宏任务队列取一个任务执行,重复此过程。

2. 任务分类

任务类型包含内容执行优先级
宏任务(Macrotask)script 整体代码、setTimeout、setInterval、I/O、UI 渲染、WebSocket低(微任务执行完才执行)
微任务(Microtask)Promise.then/catch/finally、async/await、queueMicrotask、MutationObserver高(宏任务执行完立即执行)

3. 执行流程(可视化)

1. 执行同步代码(属于宏任务)→ 执行栈清空
2. 执行所有微任务 → 微任务队列清空
3. 执行一次UI渲染(可选)
4. 从宏任务队列取第一个任务执行 → 回到步骤1

4. 示例(理解执行顺序)

console.log('同步1'); // 同步代码,立即执行

setTimeout(() => { // 宏任务
  console.log('setTimeout');
}, 0);

Promise.resolve().then(() => { // 微任务
  console.log('Promise.then');
});

console.log('同步2'); // 同步代码,立即执行

// 执行结果:同步1 → 同步2 → Promise.then → setTimeout

11. TS 中的 interface 和 type 的区别和使用场景

interface(接口)和 type(类型别名)是 TS 中定义类型的核心方式,核心区别及场景如下:

1. 核心区别

特性interfacetype
定义方式只能定义对象/类的形状可定义任意类型(基本类型、联合类型、交叉类型等)
扩展方式extends 扩展,支持多继承用交叉类型 & 扩展
合并特性支持重复声明自动合并不支持重复声明(重复定义会报错)
实现方式类可通过 implements 实现类只能实现对象类型的 type(基本类型/联合类型不行)
索引签名支持更灵活的索引签名支持但语法稍繁琐

2. 代码示例

(1)interface 示例

// 定义对象类型
interface User {
  name: string;
  age: number;
}

// 扩展(继承)
interface Admin extends User {
  role: string;
}

// 重复声明自动合并
interface User {
  gender: string;
}
// 最终User:{ name: string; age: number; gender: string }

// 类实现
class Person implements User {
  name = '张三';
  age = 20;
  gender = '男';
}

(2)type 示例

// 定义基本类型
type Str = string;

// 定义联合类型
type Status = 'success' | 'error' | 'loading';

// 定义对象类型
type User = {
  name: string;
  age: number;
};

// 扩展(交叉类型)
type Admin = User & { role: string };

// 重复声明报错
// type User = { gender: string }; // 错误:标识符“User”重复

3. 使用场景

  • 优先用 interface​:定义对象/类的形状、需要类型合并、需要被类实现的场景(如组件 Props、接口返回值)。
  • 优先用 type​:定义基本类型、联合类型、交叉类型、元组,或需要给类型起别名的场景(如状态枚举、工具类型)。

12. 什么是宏任务?什么是微任务?

宏任务和微任务是 JS 异步任务的两大分类,是事件循环的核心,具体定义如下:

1. 宏任务(Macrotask)

  • 定义​:执行时间较长、优先级较低的异步任务,会完整占用一次事件循环的执行周期。
  • 包含类型​:
    • script 整体代码(最顶层的宏任务)
    • setTimeout/setInterval
    • setImmediate(Node.js 特有)
    • I/O 操作(文件读取、网络请求)
    • UI 渲染(浏览器特有)
    • WebSocket 回调

2. 微任务(Microtask)

  • 定义​:执行时间短、优先级高的异步任务,宏任务执行完后会立即执行所有微任务,不会阻塞 UI 渲染。
  • 包含类型​:
    • Promise.then/catch/finally
    • async/await(本质是 Promise 语法糖)
    • queueMicrotask(手动创建微任务)
    • MutationObserver(浏览器特有)
    • process.nextTick(Node.js 特有,优先级最高)

3. 核心区别

  • 执行优先级:微任务 > 宏任务(同一次事件循环中)。
  • 执行时机:微任务在当前宏任务执行完后立即执行;宏任务需等待下一轮事件循环。
  • 阻塞性:微任务执行完才会触发 UI 渲染,大量微任务会阻塞渲染;宏任务执行间隔可能触发渲染。

13. 讲讲 Promise?

Promise 是 JS 解决异步回调地狱的核心方案,用于表示异步操作的最终完成(或失败)及其结果值。

1. 核心特性

  • 状态不可逆​:Promise 有三种状态,状态一旦改变无法回退:
    • pending(进行中)→ fulfilled(已成功)/ rejected(已失败)。
  • 链式调用​:通过 then()/catch()/finally() 实现异步操作串行执行,避免回调嵌套。
  • 错误冒泡​:链式调用中任意环节出错,可通过末尾的 catch() 统一捕获。

2. 基本使用

// 创建Promise
const promise = new Promise((resolve, reject) => {
  // 异步操作(如请求)
  setTimeout(() => {
    const res = { code: 200, data: '成功' };
    if (res.code === 200) {
      resolve(res.data); // 成功,触发then
    } else {
      reject('失败'); // 失败,触发catch
    }
  }, 1000);
});

// 调用
promise
  .then(res => {
    console.log('成功:', res);
    return res + '处理后'; // 传递给下一个then
  })
  .then(res => console.log('链式调用:', res))
  .catch(err => console.error('错误:', err))
  .finally(() => console.log('无论成功失败都执行'));

3. 常用静态方法

方法作用场景
Promise.resolve()返回一个已成功的 Promise快速创建成功的 Promise
Promise.reject()返回一个已失败的 Promise快速创建失败的 Promise
Promise.all()接收 Promise 数组,全部成功才返回,一个失败则立即失败并行执行多个异步任务(如同时请求多个接口)
Promise.race()接收 Promise 数组,第一个完成的 Promise 决定结果超时控制(如请求 5 秒未响应则超时)
Promise.allSettled()接收 Promise 数组,等待所有任务完成(无论成败),返回所有结果需知道所有异步任务的执行结果(如批量上传)
Promise.any()接收 Promise 数组,第一个成功的 Promise 决定结果多个备选接口,取第一个成功的

4. 解决的问题

  • 回调地狱:将嵌套的回调转换为链式调用,代码更清晰。
  • 异步操作统一管理:标准化异步操作的成功/失败处理。

14. 怎么让元素页面居中

元素居中是前端高频需求,分​水平居中​、​垂直居中​、​水平垂直居中​,以下是常用方案:

1. 水平居中

元素类型实现方案代码示例
行内元素(文字/span)父元素设置 text-align: center.parent { text-align: center; }
块级元素(固定宽度)元素设置 margin: 0 auto.child { width: 200px; margin: 0 auto; }
块级元素(不定宽度)Flex 布局:父元素 display: flex; justify-content: center.parent { display: flex; justify-content: center; }

2. 垂直居中

实现方案代码示例适用场景
Flex 布局(推荐).parent { display: flex; align-items: center; }任意元素,兼容性好
定位 + transform.parent { position: relative; } .child { position: absolute; top: 50%; transform: translateY(-50%); }不定高度元素
行高(单行文字).parent { line-height: 50px; height: 50px; }单行文字垂直居中

3. 水平垂直居中(常用方案)

方案 1:Flex 布局(推荐,简洁)

.parent {
  display: flex;
  justify-content: center; /* 水平居中 */
  align-items: center; /* 垂直居中 */
  width: 100vw;
  height: 100vh;
}
.child {
  width: 200px;
  height: 200px;
  background: #eee;
}

方案 2:定位 + transform(兼容旧浏览器)

.parent {
  position: relative;
  width: 100vw;
  height: 100vh;
}
.child {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%); /* 回移自身50% */
  width: 200px;
  height: 200px;
}

方案 3:Grid 布局(现代浏览器)

.parent {
  display: grid;
  place-items: center; /* 水平+垂直居中 */
  width: 100vw;
  height: 100vh;
}

总结

核心关键点回顾

  1. Flex 实现左固定右自适应:左侧固定宽度 + flex: none,右侧 flex: 1 占满剩余空间。
  2. React Hook 核心:useState(状态)、useEffect(副作用)、useRef(引用)、useContext(上下文)是高频考点。
  3. Webpack:Loader 处理文件转换,Plugin 扩展打包流程。
  4. 深拷贝需处理:基本类型、引用类型、循环引用、特殊对象(日期/正则)。
  5. 事件循环核心:同步代码 → 微任务 → 宏任务,微任务优先级高于宏任务。
  6. TS 类型定义:interface 适合对象/类,type 适合基本类型/联合类型。
  7. 元素居中:Flex 布局是最简洁通用的方案,定位 +transform 兼容性更好。 | Complex data processing to avoid unnecessary re-rendering of subcomponents | | useCallback | Cache function references to avoid duplicate function creation | Callback function passed to a child component (used with memo) | | useLayoutEffect | Synchronously execute side effects, triggered after the DOM update, before the browser draws | Scenarios where you need to modify the DOM synchronously (e.g., get DOM size) |

3. Difference between loader and plugin in Webpack

Loader and Plugin are the core extension mechanisms of Webpack, but they are positioned and function completely differently:

| Dimension | Loader | Plugin | Complex data processing to avoid unnecessary re-rendering of subcomponents | | useCallback | Cache function references to avoid duplicate function creation | Callback function passed to a child component (used with memo) | | useLayoutEffect | Synchronously execute side effects, triggered after the DOM update, before the browser draws | Scenarios where you need to modify the DOM synchronously (e.g., get DOM size) |

3. Difference between loader and plugin in Webpack

Loader and Plugin are the core extension mechanisms of Webpack, but they are positioned and function completely differently:

| useCallback | Cache function references to avoid duplicate function creation | Callback function passed to a child component (used with memo) | | useLayoutEffect | Synchronously execute side effects, triggered after the DOM update, before the browser draws | Scenarios where you need to modify the DOM synchronously (e.g., get DOM size) |

3. Difference between loader and plugin in Webpack

Loader and Plugin are the core extension mechanisms of Webpack, but they are positioned and function completely differently:

| Dimension | Loader | Plugin