JavaScript依赖管理核心是代码加载时机与方式,浏览器原生ESM限制多(需type="module"、带后缀路径、不支持node_modules直接引入),故真实项目必须用Webpack或Vite等打包工具处理模块解析、tree-shaking等。

JavaScript 依赖管理的核心不是“装多少包”,而是“谁在什么时候、以什么方式加载了什么代码”。现代项目几乎必须用模块打包工具,否则 import 会直接报错,node_modules 中的包也无法被浏览器执行。
为什么 import 在浏览器里直接写会报错?
原生浏览器只支持 ES 模块(ESM),但有硬性限制:必须显式声明 type="module",且所有 import 路径必须带后缀(如 ./utils.js),不能省略;同时不支持 import 从 node_modules 直接引入(例如 import _ from 'lodash' 会 404)。
- 浏览器原生 ESM 不解析
package.json的exports或main - 没有自动路径别名、无
node_modules查找逻辑 - 无法处理 CommonJS(
require())或 JSON 导入等 Node 特性
所以,光靠浏览器跑 import 只适用于极简静态模块,真实项目必须走打包流程。
npm install 装的是什么?devDependencies 和 dependencies 怎么分?
npm install 把包下载到 node_modules,但只是“存着”,不会自动接入代码。是否真正参与构建/运行,取决于你是否在源码中 import 或 require 它,以及打包工具是否将其纳入输出。
立即学习“Java免费学习笔记(深入)”;
-
dependencies:运行时必需,比如react、axios—— 打包后仍需存在于最终 JS 中 -
devDependencies:仅开发期用,比如webpack、eslint、@vitejs/plugin-react—— 不会进入生产产物 - 混淆点:
typescript是devDependency,但@types/react也是,因为类型只用于编译检查,不产出 JS
Vite 和 Webpack 做的事本质一样,但启动和构建逻辑完全不同
Vite 利用浏览器原生 ESM,在开发时按需编译单个文件(冷启动快);Webpack 则是先全量构建依赖图,再启动服务(启动慢,但兼容性更可控)。两者都解决同一个问题:把 import 'lodash' 这种语句,转成浏览器能加载的真实路径,并处理循环依赖、tree-shaking、代码分割等。
import { debounce } from 'lodash-es';
export function setupSearch() {
const input = document.getElementById('search');
input.addEventListener('input', debounce(handleSearch, 300));
}
上面这段代码在 Vite 中可直接运行;在 Webpack 中也行,但若你用的是 lodash(非 lodash-es),Webpack 需额外配置 babel-plugin-lodash 或 alias 才能避免打包整个库 —— 这就是模块格式(CJS vs ESM)对打包结果的直接影响。
不打包也能跑 import?可以,但代价是放弃灵活性
用 deno run 或 node --experimental-modules 确实能直接执行 ESM,但它们不处理:
- 路径别名(
@/components→src/components) - CSS / 图片等非 JS 资源导入(
import './style.css') - 环境变量注入(
import.meta.env.VITE_API_URL) - 动态
import()的代码分割与预加载提示
也就是说,跳过打包工具 = 主动放弃工程化能力。小脚本可以试,中大型项目会卡在第 3 天的样式加载失败或环境变量读不到上。










