普通视图

发现新文章,点击刷新页面。
昨天以前首页

Webpack vs Vite:核心差异、选型建议

2026年4月24日 17:35

Vite 与 Webpack 深度对比:特性、短板与选型建议

1. 为什么需要前端构建工具?

现代前端开发中,我们常常使用 TypeScript、SCSS、JSX 等非原生语法,以及 npm 包、图片、字体等多种资源。浏览器无法直接运行这些内容,因此需要构建工具进行转译、打包、优化。Webpack 和 Vite 是目前最主流的两款构建工具,它们代表了两种不同的构建哲学。

简单说:构建工具帮助我们把“高级代码”变成浏览器能懂的代码,还能自动处理文件依赖、压缩体积、提供开发服务器(热更新)。没有它,我们就要手动做很多重复工作。

2. 核心差异速览

维度 Webpack Vite
构建方式 全量打包(bundle) 开发时按需编译 + 生产打包
启动速度 随项目增大而变慢 极快(几乎与项目规模无关)
热更新 需重新构建变化模块 基于 ESM 的即时 HMR
配置复杂度 较高,需要配置 loader/plugin 开箱即用,配置简洁
生产优化 成熟强大(代码分割、Tree Shaking) 基于 Rollup,基本够用
生态 海量 loader/plugin 兼容 Rollup 插件,逐渐丰富
旧浏览器支持 通过 polyfill 可兼容 IE11 需额外配置(如 @vitejs/plugin-legacy

热更新的定义:热更新就是修改代码后,不刷新页面直接更新模块,保持页面状态。Vite 的热更新比 Webpack 更快,因为它是基于浏览器的原生 ES Module 按需替换。

3. Webpack:功能强大的模块打包器

3.1 核心理念

Webpack 将所有资源(JS、CSS、图片等)视为模块,从入口开始递归构建依赖图,最终打包成一个或多个 bundle 文件。它强调配置化可扩展性

3.2 工作流程

  1. 读取 webpack.config.js 配置。
  2. 从入口(entry)开始,通过 loader 转换非 JS 文件。
  3. 使用 plugin 在构建生命周期中执行任务(如生成 HTML、压缩代码)。
  4. 输出打包后的文件到 dist 目录。

如下图:

3.3 关键配置示例

// webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  mode: 'development',        // 或 'production'
  entry: './src/index.js',    // 入口
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
    clean: true               // 每次打包前清空 dist
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']   // 顺序:从右到左
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env']
        }
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({ template: './src/index.html' })
  ],
  devServer: {
    port: 8080,
    hot: true,
    open: true
  }
}

3.4 优势

  • 打包能力全面:支持几乎所有资源类型,通过 loader 体系无限扩展。
  • 生态丰富:有大量官方和社区 plugin,能满足各种复杂需求(如代码分割、资源内联、PWA 等)。
  • 生产优化成熟:Tree Shaking、代码分割、缓存控制等经过多年考验。
  • 高度可定制:几乎可以控制构建的每一个环节。

3.5 局限性

问题 说明
开发启动慢 每次启动都需要全量打包,项目越大越慢
热更新慢 修改代码后需要重新编译受影响的模块,大项目可能等待数秒
配置复杂 新手容易迷失在 loader 和 plugin 的组合中,配置错误难以排查
生产构建耗时长 大型项目打包可能耗时数分钟

4. Vite:面向现代浏览器的极速构建工具

4.1 核心理念

Vite 利用浏览器原生 ES Module 支持,在开发环境下不打包,直接按需编译请求的模块;生产环境则使用 Rollup 进行打包。它强调开发体验优先

4.2 工作流程

  1. 启动开发服务器,预构建依赖(使用 esbuild,极快)。
  2. 浏览器请求 main.js 时,Vite 拦截请求,实时编译 Vue/JSX/TS 等文件。
  3. 返回浏览器可执行的 ES Module 代码。
  4. 生产构建时调用 Rollup 打包,并进行优化。 如下图:

image.png

4.3 配置示例

// vite.config.js
import { defineConfig } from 'vite'
import legacy from '@vitejs/plugin-legacy'

export default defineConfig({
  plugins: [
    legacy({ targets: ['ie 11'] })   // 可选:兼容旧浏览器
  ],
  server: {
    port: 5173,
    open: true,
    proxy: { '/api': 'http://localhost:3000' }
  },
  build: {
    outDir: 'dist',
    sourcemap: true
  }
})

4.4 优势

  • 极速启动:无需打包,启动时间与项目规模无关,通常小于 1 秒。
  • 即时的热更新:基于 ESM 的 HMR 非常快,修改后浏览器几乎瞬间更新。
  • 开箱即用:支持 TypeScript、CSS、静态资源等,无需配置 loader。
  • 配置简洁:API 设计清晰,上手门槛低。
  • 现代工具链:使用 esbuild 预构建依赖,速度极快。

4.5 局限性

问题 说明
生产优化不如 Webpack 对于极大型项目,Vite 的打包结果可能比 Webpack 略大或优化不够细致
旧浏览器兼容麻烦 依赖 ES Module,要支持 IE11 必须引入 @vitejs/plugin-legacy,会增加构建复杂度
生态相对年轻 虽然兼容 Rollup 插件,但部分 Webpack 专属插件(如某些针对特殊资源的 loader)无法直接使用
开发环境与生产环境行为不一致 开发时使用 esbuild 转译,生产时使用 Rollup,可能导致细微差异

5. 如何选择?

5.1 适合 Webpack 的场景

  • 项目历史悠久,已经使用了大量 Webpack 专属插件(如某些特殊 loader)。
  • 需要极度精细的生产环境优化(如微前端架构、自定义代码分割策略)。
  • 团队对 Webpack 非常熟悉,迁移成本高。
  • 需要兼容非常古老的浏览器(如 IE11)且不希望额外配置。

5.2 适合 Vite 的场景

  • 新项目,希望快速启动和热更新,提升开发效率。
  • 使用 Vue 3 / React 18 + 现代浏览器为目标。
  • 项目以现代 JavaScript 为主,不依赖太多 Webpack 特有功能。
  • 希望配置简单,降低新手维护成本。

目前 Vite 已是 Vue 官方推荐工具,并广泛应用于 React、Svelte 等生态。对于绝大多数新项目,Vite 是更高效的选择。

6. 总结

维度 Webpack Vite
核心哲学 模块化打包,高度可控 开发体验优先,按需编译
启动速度 ⭐⭐ ⭐⭐⭐⭐⭐
热更新速度 ⭐⭐ ⭐⭐⭐⭐⭐
生产优化能力 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐
配置复杂度 ⭐⭐⭐⭐
生态丰富度 ⭐⭐⭐⭐⭐ ⭐⭐⭐
最佳适用 大型复杂项目、遗留系统 新项目、现代浏览器应用

如果你追求极致的开发体验和快速启动,选 Vite;如果你需要极度精细的生产优化和丰富的生态,或者维护老项目,继续用 Webpack。两者并非互斥,也可以根据项目模块逐步迁移。


7. 面试常见问题与回答思路

Q1:什么是构建工具?为什么需要它?

参考答案
“构建工具可以帮助我们把现代前端代码(比如 TypeScript、JSX、SCSS)转译成浏览器能识别的 JavaScript、CSS,同时还能合并文件、压缩代码、处理图片等。它可以自动化很多重复工作,提升开发效率,并且提供开发服务器支持热更新。”

Q2:Vite 和 Webpack 的核心区别是什么?

参考答案(抓住两点即可):
“Webpack 在开发模式下会全量打包整个项目,所以项目越大启动越慢;而 Vite 利用浏览器原生 ES Module,开发时不打包,只按需编译请求的文件,因此启动非常快,热更新也更快。另外,Webpack 配置复杂但生态成熟,Vite 开箱即用但生产优化略逊一筹。”

Q3:你用过 Vite 或 Webpack 吗?怎么搭建一个简单的 Vite 项目?

参考答案
“用过 Vite。搭建非常简单,只需要三行命令:

npm create vite@latest my-app
cd my-app
npm install
npm run dev

启动后就能看到页面。Vite 默认支持 CSS、TypeScript、静态资源等,不用额外配置。”

Q4:如果让你选一个构建工具,你会选哪个?为什么?

参考答案
“如果是新项目,我会选 Vite。因为它启动快、热更新快、配置简单,能显著提高开发效率,而且 Vue/React 官方都推荐。但如果项目需要兼容 IE11,或者已经用了很多 Webpack 特有插件,那我会选 Webpack。”

Q5:Vite 和 Webpack 在生产环境打包上有什么区别?

参考答案
“Webpack 的生产优化更成熟,比如代码分割的策略更精细,Tree Shaking 效果更好,适合大型复杂项目。Vite 生产环境使用 Rollup 打包,基本够用,但对于极大型项目可能打包结果略大或优化不够细致。”

Q6:你遇到过 Vite 或 Webpack 的问题吗?怎么解决的?

参考答案(如果没有实际遇到过,可以这样说):
“我用 Vite 时遇到过端口被占用的问题,通过配置 server.port 改成其他端口解决。另外,Vite 默认不支持 IE11,如果需要兼容,要安装 @vitejs/plugin-legacy 插件。”

Promise原理、手写与 async、await

2026年4月21日 17:46

Promise原理、手写与 async、await

1. 为什么需要 Promise?

JavaScript 是单线程语言,为了避免阻塞 UI,大量操作(网络请求、定时器、文件读写)被设计为异步。早期使用回调函数,但多个异步任务嵌套会导致“回调地狱”:

getUser(1, (user) => {
  getPosts(user.id, (posts) => {
    getComments(posts[0].id, (comments) => {
      console.log(comments);
    });
  });
});

小结:回调函数的缺点

  • 嵌套复杂(回调地狱问题)。
  • 难以管理错误(多个异步嵌套后,错误处理困难)。
  • 可读性差(功能逻辑分散,难以理解代码流程)。

Promise 通过状态机链式调用,将异步代码写得像同步一样清晰,解决了回调地狱、错误处理困难和难以组合的问题。


2. Promise 核心概念

2.1 三种状态

状态 含义 触发方式 是否可逆
pending 初始状态,未完成 new Promise 可变为 fulfilled/rejected
fulfilled 操作成功 调用 resolve(value) 不可变
rejected 操作失败 调用 reject(reason) 不可变

小结:Promise 的状态特性

  • pending 转为 fulfilledrejected 后状态不能再变。
  • resolve(value) → 将状态变为 fulfilled
  • reject(reason) → 将状态变为 rejected

2.2 实例方法

const p = new Promise((resolve, reject) => {
  setTimeout(() => resolve('成功'), 1000);
});

p.then(
  value => console.log(value),  // 成功回调
  reason => console.error(reason) // 失败回调
).catch(error => {
  // 捕获链上任何未处理的错误
}).finally(() => {
  // 无论成败都执行
});

2.3 promise执行流程图

小结:实例方法
  1. then: 接收 fulfilledrejected 回调,返回新的 Promise。
  2. catch: 用于捕获链中的失败(代替 then(null, onRejected))。
  3. finally: 无论成功或失败都执行,不改变返回值。

3. JS事件循环与Promise

Promise 的回调是异步执行的,会被放入微任务队列(Microtask Queue),优先级高于宏任务(如 setTimeout)。

示例:

console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// 输出顺序:1 4 3 2
小结:事件循环与 Promise 的关系
  1. 同步代码 → 微任务队列 → 宏任务队列。
  2. Promise 的 then 回调属于微任务,优先级高于宏任务。

4. 链式调用与错误处理

4.1 值传递与穿透

每个 then 返回一个新 Promise,上一个 then 的返回值会传递给下一个 then

Promise.resolve(1)
  .then(x => x + 1)       // 返回 2
  .then(x => { throw x }) // 抛出错误
  .catch(err => err + 1)  // 捕获错误,返回 3
  .then(console.log);     // 输出 3

如果 then 没有传入回调,值会穿透:

Promise.resolve(1).then().then(v => console.log(v)); // 输出 1
场景示例:异步加载资源
Promise.resolve('开始加载')
  .then(() => fetch('/user')) // 模拟异步数据请求
  .then(res => res.json())
  .then(data => console.log('加载完成:', data))
  .catch(err => {
    console.error('加载失败:', err);
  });

4.2 错误处理最佳实践

  • 始终使用 .catch 而不是then的第二个参数:
// 推荐做法(catch 更全面)
Promise.resolve()
  .then(() => { throw new Error('失败'); })
  .catch(err => console.log('捕获错误:', err));
  • .catch放在链式末尾,捕获前面所有错误。

5. 静态方法:组合多个 Promise

常见方法与场景:

方法 行为 典型场景
Promise.all 全部成功 → 结果数组;任一失败 → 立即 reject 多个请求必须全部成功
Promise.allSettled 等待所有 settled,返回每个的状态和值/原因 不关心个别失败,只要全部完成
Promise.race 返回最先 settled 的结果(成功或失败) 超时控制
Promise.any 返回第一个成功的结果;全部失败 → AggregateError 多个备用接口
Promise.resolve 将值转为 resolved Promise 统一异步处理
Promise.reject 返回 rejected Promise 快速抛出异步错误
示例:请求超时控制(Promise.race
function fetchWithTimeout(url, timeout = 3000) {
  const timeoutPromise = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('请求超时')), timeout)
  );
  return Promise.race([fetch(url), timeoutPromise]);
}

fetchWithTimeout('/data')
  .then(res => res.json())
  .then(data => console.log(data))
  .catch(err => console.error(err));

6. 手写 Promise

小结:手写 Promise 的核心要点

  1. 状态管理:pendingfulfilled/rejected
  2. 链式调用:then 返回新的 Promise,并传递返回值。
  3. 微任务队列:通过 queueMicrotask 模拟异步执行。
  4. 错误处理:支持 catch 和错误冒泡。
class MyPromise {
  constructor(executor) {
    this.state = 'pending';
    this.value = undefined;
    this.reason = undefined;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];

    const resolve = (value) => {
      if (this.state !== 'pending') return;
      this.state = 'fulfilled';
      this.value = value;
      this.onFulfilledCallbacks.forEach(fn => fn());
    };

    const reject = (reason) => {
      if (this.state !== 'pending') return;
      this.state = 'rejected';
      this.reason = reason;
      this.onRejectedCallbacks.forEach(fn => fn());
    };

    try { executor(resolve, reject); } catch (err) { reject(err); }
  }

  then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;
    onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err; };

    const promise2 = new MyPromise((resolve, reject) => {
      const runFulfilled = () => {
        queueMicrotask(() => {
          try {
            const x = onFulfilled(this.value);
            resolve(x);
          } catch (err) { reject(err); }
        });
      };
      const runRejected = () => {
        queueMicrotask(() => {
          try {
            const x = onRejected(this.reason);
            resolve(x);
          } catch (err) { reject(err); }
        });
      };
      if (this.state === 'fulfilled') runFulfilled();
      else if (this.state === 'rejected') runRejected();
      else {
        this.onFulfilledCallbacks.push(runFulfilled);
        this.onRejectedCallbacks.push(runRejected);
      }
    });
    return promise2;
  }

  catch(onRejected) { return this.then(null, onRejected); }

  static resolve(value) { return new MyPromise(resolve => resolve(value)); }
  static reject(reason) { return new MyPromise((_, reject) => reject(reason)); }

  static all(promises) {
    return new MyPromise((resolve, reject) => {
      const results = [];
      let count = 0;
      if (promises.length === 0) return resolve(results);
      promises.forEach((p, i) => {
        MyPromise.resolve(p).then(
          val => { results[i] = val; count++; if (count === promises.length) resolve(results); },
          reject
        );
      });
    });
  }

  static race(promises) {
    return new MyPromise((resolve, reject) => {
      promises.forEach(p => MyPromise.resolve(p).then(resolve, reject));
    });
  }
}

7. Async/Await:更优雅的异步方案

示例:用户数据加载与嵌套调用

async function getData(id) {
  try {
    const user = await fetch(`/user/${id}`).then(res => res.json());
    const posts = await fetch(`/posts/${user.id}`).then(res => res.json());
    console.log(`用户:${user.name} 的文章:`, posts);
  } catch (err) {
    console.error('加载失败:', err);
  }
}

小结:async/await 的优势

  1. 语法糖,异步代码像同步代码。
  2. 内置错误处理(try/catch),更自然。
  3. 避免 Promise 链嵌套问题。

8. 常见追问与扩展

  • then为什么返回新 Promise? 保证状态不可变,支持链式调用,避免修改原 Promise 的值。
  • 微任务如何模拟? 可用 queueMicrotaskPromise.resolve().then();手写时也可用 setTimeout 但需说明那是宏任务。
  • allrace 的区别? all 等待全部成功或任一失败;race 返回最先 settled 的结果。
  • 如何实现 finally? 无论成败都执行,且不改变返回值,可用 then 链式调用实现。
  • async/await相比 Promise 有什么优势? 代码更简洁、可读性更高,错误处理使用 try/catch 更自然;但本质上仍是 Promise 的语法糖。
  • await后面的代码何时执行? await表达式本身会立即执行其右侧的 Promise(或非 Promise 值),然后暂停当前 async 函数的执行,将后续代码包装为一个微任务。当 await 的 Promise 决议后,该微任务被推入微任务队列,在所有同步代码执行完毕后执行。

错误处理案例:未捕获承诺错误的危害

async function bad() {
  const data = await fetch('/api'); // 如果 fetch 失败,错误会被吞掉
  return data;
}

解决方案:

  • 准确使用 try/catch 或全局事件:
window.addEventListener('unhandledrejection', (event) => {
  console.error('未捕获的 Promise 错误:', event.reason);
});

9. 全文总结与面试技巧

小结:主要知识点

知识点 核心要点
Promise 状态机 pendingfulfilled/rejected,不可逆。
链式调用 每个 then 返回新 Promise,值穿透,错误冒泡。
微任务 then/catch/finally 回调进入微任务队列,优先级高于宏任务。
async/await 更简洁的异步处理,避免嵌套。
❌
❌