Webpack核心机制与构建优化:从依赖分析到性能调优
在社区答疑和面试辅导里被问得最多的前端工具Webpack绝对排前三。不是因为它有多难而是因为它太底层了——几乎所有现代前端项目都跑在它上面但大多数人只在报错的时候才想起它。这篇算是我这几年在项目里反复折腾Webpack的一个汇总从核心概念到实际配置从构建提速到产物瘦身再到和Vite的选型对比按我自己的理解串一遍。适合刚接触前端工程的初学者也适合准备面试或者在项目里做构建优化的人。先说一个反直觉的结论Webpack真正难的从来不是配置项本身而是理解它背后那套依赖分析和模块转换的体系。配置项记不住可以查文档体系不理解改配置全靠试错运气好能跑通运气不好就掉进各种莫名其妙的坑里。所以这篇文章我不打算写成API手册而是按一条主线走Webpack解决了什么问题 → 它是怎么解决的 → 实际配置怎么写 → 怎么优化 → 怎么选型 → 面试怎么考。这条主线理清楚知识点自然就串起来了。1. 回顾一下没有Webpack的前端代码组织方式在没有Webpack成为主流的年代前端项目组织代码的方式大致是这样一个页面引入多个script标签靠全局变量互相通信引入顺序错了就报错要么undefined要么重复声明依赖关系完全靠人肉维护上线前没人敢保证没问题。后来出现了RequireJS、SeaJS这类按AMD/CMD规范加载模块的工具再后来Node.js把CommonJS带火了前端才开始认真思考模块化这件事。但浏览器本身不支持CommonJS的require语法也不支持把ES Module直接在生产环境跑于是就需要一个东西把各种模块规范翻译成浏览器能识别的代码同时把散落的资源组织成结构化的产物——这就是Webpack登场的时机。Webpack做的事情说穿了就是三件从一个或多个入口出发分析代码里的import/require递归构建出一张完整的依赖关系图根据这张依赖图把各种类型的文件JS、CSS、图片、字体交给对应的loader做转换交给插件做加工最终合并输出成浏览器可以直接加载的静态资源同时支持代码拆分、按需加载、缓存优化这些工程能力注意第二条很重要。各种类型的文件意味着Webpack不只管JS。CSS要被当成模块引入图片要被压缩或转base64字体要按需加载这些全都是Webpack生态要解决的。这也是后来一切皆模块这个说法的来源。理解这一点你就明白为什么很多面试官喜欢从Webpack的工作原理问起它不是某个配置文件里的几个字段而是一整套围绕依赖分析和模块转换的执行体系。后面所有知识点都能挂在这条主线上理解。我个人的体会是学Webpack最忌讳一上来就抄配置。抄来的配置能跑但出了问题完全不知道从哪里查起。反而是先把这张依赖图在脑子里画出来再看配置每个字段都对应得上记忆成本低得多。2. 五个核心角色Entry、Output、Loader、Plugin、Module是怎么配合的2.1 Entry和Output打包的起点和终点Entry入口告诉Webpack从哪里开始分析。最简单的配置是一个字符串module.exports { entry: ./src/index.js }但实际项目里入口往往不是单个。我见过不少项目把入口写成对象就是为了做多页面应用MPA或者单独抽第三方库module.exports { entry: { app: ./src/index.js, vendor: [react, react-dom] } }Output输出定义了打包结果写到哪、叫什么名字。这里有几个字段需要认真对待path输出目录的绝对路径一般用path.resolve(__dirname, dist)filenameJS文件名可以带hash比如[name].[contenthash:8].jspublicPath静态资源的公共URL前缀这个字段在部署到CDN或者子路径的时候特别关键配置错了页面直接白屏关于hash很多人分不清hash、chunkhash、contenthash三者的区别这里直接放一个对比表类型生成依据特点适用场景hash整次构建任意文件变化所有文件名都变基本不用chunkhashchunk内容属于同一个chunk的资源共用一个hash多入口区分时用contenthash单个文件内容文件内容不变hash就不变长效缓存的最优解实际项目中我几乎都推荐用contenthash配合强缓存能达到改哪个文件只更新哪个文件的效果。这个细节在面试里也常被问到顺手记一下。2.2 Loader文件翻译官Loader的本质是一个转换函数把Webpack不认识的资源转成Webpack可以处理的模块。它最大的特点是可串联每个loader只做一件事多个loader从右到左依次执行。module.exports { module: { rules: [ { test: /\.jsx?$/, exclude: /node_modules/, use: [babel-loader] }, { test: /\.scss$/, use: [ style-loader, css-loader, postcss-loader, sass-loader ] } ] } }以scss为例执行顺序是sass-loader把scss编译成csspostcss-loader做autoprefixer加浏览器前缀css-loader解析css里的url()和import并变成JS模块style-loader把样式以style标签的方式插入页面。这个链条从右往左走每一步的产物都是下一步的输入。这里有个非常容易踩的坑顺序写反。如果把use数组写成[sass-loader, css-loader, style-loader]Webpack会从右往左先执行style-loader它拿到的是原始scss内容根本没法处理报错报得莫名其妙。所以记住一句话loader顺序和执行的直觉相反数组最后一个最先执行。还有一个常见问题是loader和plugin的区别。我一般这么解释loader是文件级的转换器在模块加载阶段处理单个文件plugin是构建级的增强器可以在Webpack整个生命周期里挂载钩子做任何loader做不到的全局操作比如生成HTML、提取CSS、压缩代码。一个是翻译一个是项目管理。2.3 Plugin在构建生命周期里插一脚Webpack的整个构建过程是事件驱动的Plugin本质就是往这些事件节点上挂回调函数。以HtmlWebpackPlugin为例它的作用是在构建结束后自动生成一个HTML文件并把你打包出来的JS和CSS按顺序注入进去。没有它的话你每次新增一个入口就得手动去改HTML里的script引用项目稍微一复杂就是灾难。const HtmlWebpackPlugin require(html-webpack-plugin); module.exports { plugins: [ new HtmlWebpackPlugin({ template: ./public/index.html, minify: true }) ] };类似的常用插件还有MiniCssExtractPlugin把CSS从JS里抽出来生成独立的CSS文件生产环境通常必配DefinePlugin注入全局常量比如process.env.NODE_ENVCleanWebpackPlugin构建前清理输出目录避免旧文件残留理解Plugin的关键在于理解Webpack的生命周期钩子。每个钩子代表构建过程中的一个时间节点比如emit表示生成资源到输出目录之前done表示构建完成。一个最简单的插件大概长这样class MyPlugin { apply(compiler) { compiler.hooks.emit.tap(MyPlugin, (compilation) { console.log(即将输出文件当前资源数量, Object.keys(compilation.assets).length); }); } }讲到这里可以顺带提一个面试高频题Webpack构建流程。用文字描述就是下面这个顺序初始化参数读取配置合并命令行参数加载插件执行插件的apply方法注册钩子确定入口从entry开始编译模块根据入口递归解析依赖匹配loader规则逐个转换模块完成模块编译得到每个模块的内容和依赖关系输出资源根据入口和模块之间的依赖关系组装成一个个chunk输出完成写入磁盘整个过程概括起来就是初始化、编译、输出三个阶段插件就是在这些阶段的不同节点上做文章。2.4 Module解析Webpack怎么找到文件的除了module.rules里配置loaderresolve配置也是日常会碰到的。它控制Webpack怎么解析模块路径最常见的有module.exports { resolve: { extensions: [.js, .jsx, .ts, .tsx, .vue, .json], alias: { : path.resolve(__dirname, src) }, modules: [node_modules, path.resolve(__dirname, src)] } };extensions自动补全扩展名配置后import ./App能自动找到App.jsxalias配置路径别名import /components/Button不用写一长串相对路径modules告诉Webpack去哪些目录下找node_modules这些配置看起来简单但对构建速度和可维护性影响很大。extensions不要配太多每多一个模块解析时就要多尝试一次文件查找全项目积累下来就是可观的耗时。alias配合modules能减少Webpack在node_modules里向上递归查找的层级这是最基础的构建加速手段之一后面讲优化的时候再展开。3. 从零写一份能上生产的Webpack配置3.1 为什么要拆环境而不是一个配置文件打天下新手最容易犯的错误是把所有配置堆在一个webpack.config.js里然后用process.env.NODE_ENV判断环境在配置对象里写一堆三目表达式。这样不是不能跑但配置一多这个文件会变得极其难读而且dev和prod的很多配置是互斥的——比如dev需要devServer和热更新prod需要代码压缩和文件指纹混在一起配置起来很痛苦。我推荐的做法是拆三个文件webpack.base.js公共配置entry、output、resolve、loader这些两边都要的webpack.dev.js开发环境专属devServer、热更新、eval类型的sourceMapwebpack.prod.js生产环境专属代码压缩、contenthash、CSS提取、资源优化然后用webpack-merge把公共配置合并进去// webpack.dev.js const { merge } require(webpack-merge); const base require(./webpack.base.js); module.exports merge(base, { mode: development, devServer: { port: 8080, hot: true, open: true }, devtool: eval-cheap-module-source-map });这个结构最大的好处是每个文件的职责单一改dev配置不会影响prod反之亦然。新的团队成员上手也快看文件名就知道改哪里。我自己带项目的时候凡是看到单文件里用process.env.NODE_ENV塞满条件判断的都会建议拆开重写后面维护成本真的差很多。3.2 devServer和热更新开发环境下Webpack Dev Server的作用是监听文件变化、自动编译、通过WebSocket通知浏览器更新。它的核心配置项有port服务端口hot开启HMR热模块替换historyApiFallback解决前端路由的history模式刷新404问题proxy开发环境代理解决跨域很多人分不清热更新和自动刷新的区别。自动刷新是文件变了浏览器整个页面刷新状态全丢HMR是只替换变更的模块比如改了CSS内容页面样式瞬间更新但不会刷新React的state、Vue的data都还在。HMR在体验上的差距非常大尤其是调试复杂的表单页面时自动刷新一次要重新走一遍登录、导航、填表单能把人逼疯。devServer底层做的事简单说就是编译出更新后的模块以JSON格式传给浏览器浏览器端接收到更新信号根据模块类型调用对应的处理逻辑比如style-loader会直接替换新样式react-refresh会重渲染组件树如果HMR没生效排查顺序一般是这样先确认hot: true再确认框架层是否配了对应的热更新插件React要用react-refreshVue有自带的热更新然后看浏览器的Console有没有报错。这是很多项目改了代码但页面没反应的常见原因不一定是Webpack的问题是框架层的热更新适配没接上。3.3 sourceMap选型sourceMap是用来在浏览器开发者工具里还原源码的没有它线上报错显示的是打包后那一长串压缩代码根本没法调试。但sourceMap也有成本和讲究。devtool值构建速度还原程度推荐场景eval最快只有文件信息基本不推荐eval-cheap-module-source-map快行级、源码友好开发环境推荐source-map慢完整生产排查用hidden-source-map慢完整、不在页面暴露配错误监控平台nosources-source-map慢只有行列、无源码想保护源码又需要堆栈dev环境推荐eval-cheap-module-source-map它生成速度快定位到行级别的信息足够用。prod环境推荐hidden-source-map或nosources-source-map——前者不暴露源码能配合错误监控平台还原堆栈后者是sourceMap里不包含源码内容避免源码泄露。核心思路是开发环境要速度生产环境要安全和可排查不要一套配置走天下。关于sourceMap我还想多提醒一句线上如果开了sourceMap一定要做好访问控制否则你的源码等于裸奔。这也是很多公司强制要求生产环境必须关闭或使用hidden-source-map的原因。4. 构建提速三板斧缓存、多线程、缩小解析范围构建慢是Webpack项目做大了之后绕不开的问题。我的建议是先用工具量化再针对性地改别凭感觉瞎调。4.1 先量化时间都花在哪了webpack 5内置了--progress可以看进度但不够细。我喜欢用speed-measure-webpack-plugin它能把每个loader和plugin的耗时列出来一眼就能看出瓶颈在babel-loader还是压缩阶段。如果用的是webpack 5也可以直接用内置的缓存和统计信息配合webpack-bundle-analyzer做体积分析——注意这个工具是看体积的和测耗时的不是一回事两者搭配用。4.2 缓存让重复工作不再重复最大的优化收益通常来自缓存。Webpack 5推出了开箱即用的持久化缓存module.exports { cache: { type: filesystem, buildDependencies: { config: [__filename] } } };开启后没变过的模块编译结果会缓存在node_modules/.cache目录里二次构建速度可以快好几倍。在webpack 4时代这个能力需要靠cache-loader或者babel-loader的cacheDirectory选项实现配置分散还容易踩坑Webpack 5直接内置这是它非常值得升级的一个点。需要注意的是buildDependencies的作用——告诉Webpack哪些文件变化时需要失效缓存。最常见的就是配置文件本身和package-lock.json、package.json。如果不配你改了配置它可能还在用旧缓存折腾半天发现没生效。4.3 多线程thread-loader的适用场景thread-loader可以做的事把它放在某个loader之前Webpack就会开启一个worker池把这个loader和后面的loader放到worker线程里去跑不阻塞主进程。module.exports { module: { rules: [ { test: /\.jsx?$/, exclude: /node_modules/, use: [ { loader: thread-loader, options: { workers: 2 } }, babel-loader ] } ] } };但这里要泼一盆冷水thread-loader不是加了就一定快。开线程本身有通信开销如果项目很小或者单个loader本身不耗时开了反而更慢。我个人的经验是当babel-loader处理大量文件构建明显卡在编译模块阶段时才值得上thread-loader。而且线上CI环境要注意CPU核数worker数量配多了会拖垮整个CI机器。4.4 缩小解析范围把力气花在刀刃上这个方向往往被忽视但其实是最便宜的优化。核心就两条用exclude/include缩小loader的处理范围node_modules不做babel转换尽量减少resolve里需要尝试的路径module.exports { module: { rules: [ { test: /\.jsx?$/, include: path.resolve(__dirname, src), exclude: /node_modules/, use: [babel-loader] } ] }, resolve: { alias: { : path.resolve(__dirname, src) }, modules: [path.resolve(__dirname, node_modules)] } };还有一个很实用的点是如果有一些库不需要走完整解析流程可以配合resolve.alias直接指向它的压缩版文件。不过这个属于非常规操作只在你确实了解库的产物结构时才建议用否则可能引发奇怪的问题。一般项目用include、alias、modules这几个就够了收益已经很明显。5. 产物瘦身Tree Shaking、Code Splitting和Scope Hoisting的配合构建变快只是优化的一头另一头是产物体积。体积累积到一定程度即使有contenthash用户首次打开页面的白屏时间也会变长。这一节讲三个核心手段。5.1 Tree Shaking把没用到的代码摇掉Tree Shaking这个词源自RollupWebpack 2之后也支持了。它的原理是基于ES Module的静态语法import和export在代码编译阶段就能确定依赖关系所以Webpack可以分析出哪些导出是没被用到的在打包时丢掉。要让Tree Shaking生效有几个条件代码必须使用ES Module语法import/exportCommonJS的require无法静态分析在package.json里声明sideEffects: false或者列出有副作用的文件告诉Webpack除了这些文件其他模块没有副作用可以安全删除生产模式下自动开启usedExports和minimize压缩开发环境下不会生效我踩过的坑是样式文件被当成副作用被抖掉了。比如import ./styles.css它的作用是往页面注入样式模块本身没有导出任何东西但Webpack如果认为它没副作用可能直接不去加载它。解决办法就是在sideEffects里放行{ sideEffects: [ **/*.css, **/*.scss ] }5.2 Code Splitting拆分的目的不是变小是按需加载Code Splitting代码分割说的是把代码切成多个chunk让浏览器按需加载。这里有两个层次。第一层是入口分割。比如多页面应用每个页面一个入口公共代码抽成公共chunk。第二层是动态导入。用import()语法把一个模块变成异步加载的chunkconst { default: _ } await import(lodash-es);Webpack会把动态导入的部分单独打成chunk在运行时才加载。这个在路由懒加载场景下特别常见首屏只加载当前路由的代码其他路由的代码等用户跳到那个页面再加载。自动化的拆分主要靠optimization.splitChunks这是webpack 4之后替代CommonsChunkPlugin的配置。一个比较通用的生产配置是module.exports { optimization: { splitChunks: { chunks: all, cacheGroups: { vendors: { test: /[\\/]node_modules[\\/]/, priority: -10, name: vendors }, common: { minChunks: 2, priority: -20, name: common } } } } };简单说就是node_modules里的第三方库单独打成vendors被多个入口引用的公共模块打成common。这样做的原因是利用浏览器缓存第三方库不常变contenthash稳定用户访问过一次之后可以直接命中缓存只有业务代码变了才需要重新下载。5.3 Scope Hoisting和CSS、图片处理Scope Hoisting作用域提升是把能合并的模块函数体直接平铺进同一个作用域减少包裹函数和运行时代码。Webpack打包后的代码每个模块默认会被一个函数包裹住保证作用域隔离模块多了这些包裹函数本身就是不小的体积而且运行时还要逐层调用。webpack 4之后生产模式默认开启optimization.concatenateModules但同样要求使用ES Module才能生效。开启后打包体积能小一些运行效率也能提升一点算是一个白拿的优化。CSS方面Webpack 5里推荐用MiniCssExtractPlugin抽离CSS再用CssMinimizerWebpackPlugin压缩。图片方面webpack 5内置了Asset Modules可以替代老旧的file-loader和url-loadermodule.exports { module: { rules: [ { test: /\.(png|jpe?g|gif|webp)$/i, type: asset, parser: { dataUrlCondition: { maxSize: 8 * 1024 // 小于8KB的图片转base64内联 } } } ] } };asset模块可以自动决定是把小图片内联成base64减少HTTP请求还是输出成独立文件大图保留文件加载更快。这个maxSize阈值建议结合项目实际情况调整不是越大越好base64过多会增加HTML/JS的体积反而拖慢首屏。最后提一个压箱底的工具CompressionWebpackPlugin。它可以在构建时提前生成.gz文件部署后配合Nginx的gzip_static直接返回压缩文件不用服务器实时压缩能省下不少CPU。压缩级别别调到太高我一般设threshold: 10240只压大于10KB的文件压缩用时和体积的平衡点最优。6. Webpack和Vite不是替代关系是不同阶段的选型最近两年Webpack和Vite选谁的话题越来越热面试也经常被问到。我的看法是这俩不是简单的谁替代谁而是面对不同场景的选择。6.1 核心差异开发模式的工作方式不同Webpack在开发时也要把整个项目打包成一个或多个bundle项目越大这个打包动作越慢启动Dev Server和热更新都要等。Vite的思路完全不同它利用浏览器原生ES Module能力开发模式下根本不做整体打包而是把文件直接发给浏览器浏览器自己通过import去加载。只有启动时预构建一下第三方依赖而且这个构建是用esbuild做的快得离谱。所以Vite的冷启动能做到毫秒级代码改动后的热更新时间基本在100ms以内而Webpack大型项目冷启动常常要几十秒甚至一分钟。这是我亲身体会最深的一点维护一个上百个路由的大项目时Webpack每次启动都要先编译完所有模块中间改一行代码热更新也要等个两三秒切到Vite之后那种改完保存立即刷新的体验确实是革命性的。6.2 生产构建策略和生态成熟度的对比Vite在开发模式用esbuild做依赖预构建和转译但在生产模式用的是Rollup做打包。为什么不用esbuild直接打包因为esbuild虽然快但在tree shaking、代码分割这些精细活上还不够成熟Rollup在这方面更可靠。这个开发用esbuild、生产用Rollup的组合既保证了开发体验又保证了产物质量算是Vite比较务实的策略。Webpack 5虽然也内置了持久化缓存开发体验比webpack 4时代好了不少但它的架构决定了每一次构建都要走完整的模块图构建流程和Vite的按需编译在底层效率上不是一个量级的。下面这个对比表是我在项目选型时常用的参考维度WebpackVite开发启动速度慢大型项目明显快毫秒级冷启动热更新bundle级大型项目有延迟模块级即时反馈生产构建Terser压缩成熟稳定Rollup打包产物质量高生态插件海量loader/plugin兼容Rollup插件生态较新复杂度配置细粒度高上手有门槛配置简单开箱即用适用场景存量项目、强定制需求新项目、体验优先6.3 什么时候选Webpack什么时候选Vite按我自己的项目经验判断标准大概是这几条如果是从零开始的新项目团队熟悉Vue或React的现代生态优先考虑Vite开发体验好是实打实的如果项目是存量的大型Webpack工程迁移成本高且构建问题可以通过缓存、并行等手段缓解那没必要为了追新而重构如果项目深度依赖某些webpack插件生态里的特殊插件比如一些老的loader、自定义插件、复杂的多入口配置Vite的插件兼容性可能不支持这时候留在Webpack更稳妥如果是做组件库或SDKRollup/Vite在产物格式和tree shaking上往往更合适我的真实感受是Vite很好但Webpack的生态沉淀太深了它背后有海量的loader和plugin解决各种现实问题。很多复杂场景比如细粒度的自定义缓存策略、高度定制化的构建流程Webpack依然是最灵活的。与其问Webpack是不是过时了不如问我这个项目当前最缺的是什么。开发效率选Vite生态兼容和深度定制选Webpack两者会在很长时间里并存。7. 面试里反复出现的Webpack问题其实都在考这套底层逻辑看热搜词里有webpack面试题我觉得与其背八股文不如把问题归一下类。面试官问来问去本质上都在考你对依赖图构建和生命周期钩子这两件事的理解。7.1 loader和plugin的区别为什么总被问这个问题的本质是考察你知不知道Webpack的两层扩展机制。我上面讲过loader处理文件转换在模块编译阶段起作用plugin处理构建流程在整个生命周期里都能介入。但更深一层的理解是loader的输入输出都是单个文件它的写法和普通转换函数没什么两样而plugin面对的是Compiler和Compilation这两个核心对象你操作的是整个构建上下文可以读取所有模块、所有资源、所有状态。面试时能答到这个层面基本就过关了。7.2 如何写一个自定义loader写loader本质就是导出一个函数接收源文件内容返回处理后的内容。如果要用到异步操作可以调this.async()module.exports function (source) { // 同步写法直接返回字符串 return source.replace(/hello/g, world); }; module.exports function (source) { // 异步写法 const callback this.async(); setTimeout(() { callback(null, source.replace(/hello/g, world)); }, 100); };注意loader里的this是Webpack注入的上下文有很多方法和属性比如this.resourcePath可以拿到当前处理的文件路径this.cacheable()可以告诉Webpack这个文件的结果能否缓存。写loader时一个常见错误是忘记处理sourceMap或者schema校验不过这个在普通项目里其实很少用到面试问到的话能把函数签名和同步异步两种处理方式讲清楚就够了。7.3 构建流程的深挖tapable是什么如果你被追问Webpack为什么能支持这么多插件那大概率会提到tapable。tapable是Webpack内部的钩子机制库可以理解为它提供了一套发布订阅模型但有更强的类型和流程控制能力。Plugin做的那件事——compiler.hooks.emit.tap(PluginName, fn)——本质上就是往tapable的钩子实例上注册一个订阅函数。tapable有不同的钩子类型比如SyncHook同步钩子按注册顺序执行AsyncSeriesHook异步串行前一个完成后再执行下一个AsyncParallelHook异步并行所有并发的回调都完成后再继续Webpack的编译流程里大量用到这些钩子这也是机制层面的东西。面试能讲到这已经超过很多人了。但如果背不住这些名词也没关系核心是理解Webpack通过钩子对外暴露生命周期节点插件通过tap注册回调这套思路。7.4 HMR的原理值得花时间看一遍源码HMR热模块替换可能是面试里最会被问到原理的问题。它的完整链路大致是开发模式下devServer监听文件变化触发重新编译编译完成后通过WebSocket向浏览器发送更新消息包括更新后的模块hash浏览器端收到消息通过HotModuleReplacement.runtime去请求更新后的模块代码新模块加载后调用模块的accept回调触发框架层的更新逻辑React的react-refresh或Vue的热更新API如果某个模块没有accept处理HMR会向上冒泡最终找不到accept就回退到整页刷新这个知识点考察的是你对开发时链路的完整理解能把每一步对应的代码位置说出来说明你真的研究过而不是背了概念。7.5 从优化问题反推面试官想听什么面试里还有一类常见问题Webpack打包太慢怎么办打包体积太大怎么办。这类问题没有标准答案面试官想听的是你有没有一套分析问题的思路。我的答题框架是固定的先说量化通过speed-measure-webpack-plugin或bundle-analyzer定位问题再提速缓存Webpack 5 filesystem cache、多线程thread-loader、缩小解析范围include/exclude/alias再瘦身Tree Shaking的生效条件、代码分割、按需加载、图片压缩、gzip预压缩最后说取舍优化方案都有成本要权衡收益和复杂度这其实不是背答案而是一套实际工作中会用到的排查路径。能把先测量再优化这个思路讲出来比背一堆插件名要有说服力得多。把这些知识点串起来再看Webpack并不神秘。它就是一套以入口为起点、以依赖图为骨架、以loader和plugin为两个扩展维度、以缓存和拆分为优化手段的构建体系。如果你正在学或者正在准备面试我给你的建议是别急着背API先在自己项目里跑一遍从零配置到能上线的完整流程亲自踩几个坑比看十遍文档都有用。就我自己而言真正把这些概念融会贯通不是在读文档的时候而是在一个大型项目里排查了一个星期构建性能问题之后。那些配置项和钩子机制在问题面前会自动长进你的脑子里以后面试不用背也讲得出来。
