前端工程化项目初始化实战:从零搭建可扩展架构
导读:项目能否长期维护,七成取决于初始化阶段的架构决策。本文以一个真实「广告后台管理系统」为载体,系统讲解前端工程化项目从无到有的搭建过程——多环境 Webpack 配置的分层哲学、
webpack-merge的智能合并机制、HtmlWebpackPlugin的模板引擎与资源注入、CopyWebpackPlugin的静态资源策略、单页应用(SPA)前端路由的底层原理(hash 与 history 两种模式)、以及 EJS 模板引擎「视图即函数」的编译范式。每一项技术都从「为什么需要、底层如何运作、何时使用」三个维度展开,结合 MDN、Webpack 官方文档与工程实践经验,适合希望建立「项目架构思维」的中级前端工程师。
目录
- 一、工程化项目的目录架构哲学
- 二、多环境配置与 webpack-merge
- 三、HtmlWebpackPlugin 的模板引擎
- 四、静态资源策略与 CopyWebpackPlugin
- 五、单页应用与前端路由原理
- 六、EJS 模板引擎:视图即函数
- 七、html-loader 与媒体资源处理
- 八、生产环境 CSS 抽离与压缩
- 总结
一、工程化项目的目录架构哲学
1.1 为什么要分层
名词解释:
- 工程化(Engineering):用统一的工具链与规范,把开发、构建、部署组织成可复用、可协作的流程。
- 关注点分离(Separation of Concerns):把不同职责的代码物理隔离,使每一部分可独立理解与修改。
概念与底层原理:
「会写业务代码」和「会做项目」是两种能力。前者关注「这个功能怎么实现」,后者关注「这个系统如何组织才能让十个人协作三年而不崩溃」。工程化的本质,就是用确定性的结构对抗业务的无序增长。
一个新人加入项目时会问三个问题:构建配置在哪、业务源码在哪、不参与编译的静态资源在哪。一个好的项目架构应当让这三个问题的答案一目了然:
config/—— 构建配置,按环境分文件。src/—— 参与 webpack 打包的业务源码。public/—— 不参与编译、需原样拷贝到产物的静态资源。
【代码注释】这种三分法的价值在于「让结构自我解释」——任何人打开项目都能凭目录名推断职责,无需口口相传。市面应用:Vue CLI、Create React App、Vite 创建的项目都遵循这套结构(public/ + src/ + 隐藏的构建配置),它已经成为前端社区的事实标准。
1.2 最小依赖与标准骨架
概念与底层原理:
一个 Webpack 工程的最小依赖只有五个包,每个都对应一项不可或缺的能力:
npm install webpack webpack-cli webpack-dev-server html-webpack-plugin webpack-merge -D
【代码注释】五个包各司其职:webpack 是打包核心、webpack-cli 提供命令行入口、webpack-dev-server 提供本地开发服务(文件监听 + WebSocket 推送 + 代理)、html-webpack-plugin 生成入口 HTML 并注入资源、webpack-merge 用于合并多环境配置。全部装在 devDependencies(-D)——它们只在「构建阶段」需要,不会出现在线上运行的代码里。市面应用:所有企业级前端项目都本地安装构建工具(而非全局),并通过 package.json + package-lock.json 锁定版本,确保团队成员构建环境一致。
标准骨架结构:

【代码注释】这张图展示了 public/ 与 dist/ 之间的两条处理路径:除 HTML 外的静态资源由 CopyWebpackPlugin 原样拷贝,HTML 由 HtmlWebpackPlugin 经模板渲染并注入打包产物。两条路径分工明确、互不干扰。市面应用:这正是「Vue CLI 风格」项目目录的工作原理——public/ 里的东西「原样搬运」,src/ 里的东西「编译打包」。
权威参考:
- Webpack 生产指南:https://webpack.docschina.org/guides/production/
- webpack-merge 仓库:https://github.com/survivejs/webpack-merge
【实战要点】
- 经典应用场景:所有基于 webpack 的中大型前端项目;多人协作场景下结构约定尤其重要。
- 常见坑:把第三方库 import 进
src/让 webpack 重新打包,既慢又容易因路径解析失败报错——稳定的第三方产物应放public/。 - 性能与最佳实践:开发期开启
cache: { type: 'filesystem' }持久化缓存,二次构建可显著提速。
【本章小结】
| 目录 | 职责 |
|---|---|
config/ | 按环境分离的构建配置 |
src/ | 参与打包的业务源码 |
public/ | 原样拷贝的静态资源 |
记忆口诀:「Config 管构建,src 走打包,public 原样搬」。
【面试考点】
Q1:为什么前端项目要区分 public/ 与 src/?
A:核心判断标准是「资源是否需要随业务逻辑被处理」。src/ 里的资源走 webpack 编译——做 Tree Shaking、模块分析、压缩、contenthash 命名;public/ 里的资源原样拷贝——适合已经是稳定压缩产物的第三方库(jQuery、AdminLTE,可走 CDN 缓存)以及「URL 不能变」的元数据(favicon、robots.txt、manifest.json)。这种分离让构建更快、部署路径更稳定。
二、多环境配置与 webpack-merge
2.1 base/dev/prod 的分层逻辑
概念与底层原理:
开发环境和生产环境的需求是对立的:开发期要 SourceMap、热更新、代理,追求「调试体验」;生产期要压缩、Hash 命名、CSS 抽离,追求「运行性能」。把两套需求塞进一份配置,结果必然是满屏的 if (isProduction) 条件分支——难读、难维护、易出错。
工程上的标准解法是「三段式分层」:
webpack.base.config.js—— 三套环境共用的部分(入口、出口、loader 规则、通用插件)。webpack.dev.config.js—— 开发专属(mode: development、devServer、快速 SourceMap)。webpack.prod.config.js—— 生产专属(mode: production、CSS 抽离、压缩)。
// config/webpack.base.config.js —— 公共配置
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: path.resolve(__dirname, '../src/app.js'),
output: {
path: path.resolve(__dirname, '../dist'),
filename: 'js/[name].[contenthash:12].js',
clean: true,
publicPath: '/' // 资源运行时 URL 前缀
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, '../public/index.html'),
inject: 'body'
})
]
};
【代码注释】path.resolve(__dirname, '../src/app.js') 是工程配置的标准写法——__dirname 是配置文件所在目录的绝对路径,.. 回到项目根,拼出稳定的绝对路径,无论从哪里执行命令都不出错。publicPath: '/' 决定运行时资源 URL 的前缀:部署在网站根用 '/'、部署在子路径 /admin/ 用 '/admin/'、CDN 部署用完整域名——它直接影响线上资源能否加载。市面应用:所有企业项目都会精心调整 publicPath,这是「本地能跑、上线白屏」类问题的高频根因。
2.2 webpack-merge 的智能合并机制
名词解释:
- 浅合并(Shallow Merge):
Object.assign的行为,同名属性直接覆盖。 - 深合并(Deep Merge):递归合并嵌套对象,数组追加而非覆盖。
概念与底层原理:
为什么不能用 Object.assign(base, dev) 合并配置?因为它是浅合并——如果 base 有 plugins: [HtmlPlugin]、dev 有 plugins: [HMRPlugin],合并结果会变成 plugins: [HMRPlugin],HtmlPlugin 被整个覆盖丢失。
webpack-merge 理解 webpack 配置的语义,做「智能合并」:数组字段追加、对象字段深合并、loader 规则按 key 合并。

【代码注释】这张图揭示了 webpack-merge 与 Object.assign 的本质差异:它会按字段类型分别处理——plugins 数组合并而非覆盖、output 对象深合并、scalar 值(如 mode)直接覆盖。正是这种「懂 webpack」的合并策略,让 base 的插件与 dev 的插件能共存。市面应用:当项目从 3 套环境扩展到 6+ 套(dev/test/staging/prod,再叠加多终端),webpack-merge 几乎是必需品。
// config/webpack.dev.config.js
const { merge } = require('webpack-merge');
const baseConfig = require('./webpack.base.config');
module.exports = merge(baseConfig, {
mode: 'development',
devServer: {
host: 'localhost',
port: 80,
open: true,
historyApiFallback: true // SPA history 模式必备
}
});
【代码注释】merge(baseConfig, devOverrides) 把公共配置与开发专属配置融合。historyApiFallback: true 是 SPA 必备——用户刷新 /admin 时 DevServer 会返回 index.html 让前端路由接管(详见第五章)。市面应用:所有 SPA 项目的 dev 配置都会开启这一项。
package.json 通过 --config 指定配置文件:
{
"scripts": {
"start": "webpack serve --config ./config/webpack.dev.config.js",
"build": "webpack --config ./config/webpack.prod.config.js"
}
}
【代码注释】--config 让 webpack 加载指定配置而非默认 ./webpack.config.js。npm start 是 npm 内置的免 run 快捷脚本之一。市面应用:npm start 启动开发、npm run build 打包发布,是整个前端社区的命令约定。
【实战要点】
- 经典应用场景:所有中大型项目;多环境(dev/test/staging/prod)部署。
- 常见坑:用
Object.assign合并配置导致插件/loader 数组被覆盖丢失。 - 性能与最佳实践:进一步可用
mergeWithRules精细控制单条 loader 规则的合并策略(如替换 base 中某条 rule 的 loader)。
【本章小结】
| 文件 | 职责 |
|---|---|
| base | entry / output / loader / 通用 plugin |
| dev | mode:development + devServer |
| prod | mode:production + 抽离 + 压缩 |
| webpack-merge | 智能合并三者 |
记忆口诀:「Base 共用,Dev 调试,Prod 上线,Merge 巧合并」。
【面试考点】
Q1:webpack-merge 和 Object.assign 的区别?
A:Object.assign 是浅合并——对数组/嵌套对象会覆盖而非合并,plugins 数组会被整个替换;webpack-merge 理解 webpack 配置语义——数组追加、对象深合并、loader 规则按 key 合并。一句话:Object.assign 不懂 webpack,webpack-merge 懂。
Q2:为什么不用环境变量在单文件里切换配置?
A:理论可行(env => ({...}) + process.env.NODE_ENV 判断),但条件分支多了配置就难读难维护——每次修改都要在脑子里跑一遍「这条 if 走不走」。文件拆分把分支显式化,新人能一眼看出某项配置只在哪个环境生效,是工程上更优雅的解法。
三、HtmlWebpackPlugin 的模板引擎
3.1 模板变量注入原理
名词解释:
- 模板变量:HTML 模板中以
<%= ... %>形式嵌入的占位符,构建时被求值替换。 - 资源注入(Injection):把打包产物的
<script>/<link>自动插入 HTML 的过程。
概念与底层原理:
直接打包的 main.[hash].js 无法在浏览器单独运行,必须嵌入 HTML。但手工维护 HTML 有两个无法回避的痛点:产物文件名带 contenthash、每次构建都变;多入口时要写多个 <script> 标签。
HtmlWebpackPlugin 内置 EJS 模板引擎,它订阅 webpack 的 emit 钩子,在「写文件前」把整个插件 options 作为模板上下文传入 EJS 渲染,再注入打包产物:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title><%= htmlWebpackPlugin.options.title %></title>
<link rel="icon" href="./favicon.ico" />
</head>
<body>
<div id="app"></div>
<!-- 不要手写 <script>,插件会自动注入 -->
</body>
</html>
【代码注释】<%= htmlWebpackPlugin.options.title %> 之所以能取到值,是因为插件把整个 options 对象作为 EJS 上下文传入——你在插件配置里写的任何字段(title、description、gtmId)都能在模板里访问。注意不要在模板里手写 <script src>,否则会与自动注入冲突导致重复加载。市面应用:脚手架生成的 public/index.html 里那些 <%= htmlWebpackPlugin.options.xxx %> 语法,正是利用这套机制注入标题、CSP nonce、环境变量等元信息。
3.2 站标与页面元信息
new HtmlWebpackPlugin({
template: path.resolve(__dirname, '../public/index.html'),
inject: 'body',
minify: {
removeComments: true,
collapseWhitespace: true
},
title: '广告管理系统' // 自定义 options,模板里可访问
})
【代码注释】inject: 'body' 把 <script> 注入 body 尾部(不阻塞 head 解析);minify 在生产模式压缩 HTML;title 是自定义字段,模板通过 htmlWebpackPlugin.options.title 读取。这套机制让「一份模板服务多个站点」成为可能——同一模板配不同 title 即可输出不同品牌的页面。市面应用:SEO 项目常用此机制注入 meta 描述、Open Graph 协议等元数据。
【实战要点】
- 经典应用场景:SPA 用单个 HtmlWebpackPlugin;MPA 多次实例化生成多页。
- 常见坑:模板里手写
<script>与自动注入冲突——保持模板纯净。 - 性能与最佳实践:生产开
minify压缩 HTML;配合[contenthash]实现「永久缓存 + 引用始终正确」。
【本章小结】
| 能力 | 配置 |
|---|---|
| 模板变量 | <%= htmlWebpackPlugin.options.xxx %> |
| 资源注入 | inject: 'body' |
| 压缩 | minify |
记忆口诀:「模板占位插件填,脚本自动注 body」。
【面试考点】
Q1:HtmlWebpackPlugin 解决了什么根本问题?
A:1)产物文件名带 [contenthash]、每次构建都变,手工维护 <script src> 不现实;2)多入口/拆 Chunk 后要正确注入多个脚本。该插件订阅 emit 钩子,自动读取本次构建的 Chunk 列表并注入对应资源,配合 contenthash 实现「长效缓存 + 引用始终正确」。
四、静态资源策略与 CopyWebpackPlugin
4.1 编译资源与拷贝资源的边界
概念与底层原理:
favicon.ico、第三方稳定库(jQuery、AdminLTE 的压缩 CSS/JS)这类资源不需要 webpack 编译。把它们 import 进 src/ 反而有害——webpack 会尝试解析其中所有 url(...)、做依赖分析,既慢又容易因路径问题报错。正确做法是放进 public/,构建时由 CopyWebpackPlugin 整体拷贝到 dist/:
const CopyWebpackPlugin = require('copy-webpack-plugin');
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve(__dirname, '../public'),
to: path.resolve(__dirname, '../dist'),
globOptions: {
ignore: ['**/*.html'] // 排除 HTML,由 HtmlWebpackPlugin 处理
}
}
]
})
【代码注释】from / to 是源/目标绝对路径;globOptions.ignore: ['**/*.html'] 排除所有 HTML——因为 HtmlWebpackPlugin 已用模板生成最终 HTML,再拷一遍会重复覆盖。** 是 glob 的「任意层目录」通配符,与只匹配本层的 * 不同。这体现了「插件协作」的工程思想:CopyWebpackPlugin 与 HtmlWebpackPlugin 各管一摊,靠 ignore 划清边界。市面应用:所有「Vue CLI 风格」项目都有 public/ 目录及类似的拷贝逻辑。
4.2 第三方 UI 框架的接入方式
AdminLTE 是基于 Bootstrap 4 的开源后台 UI 框架。把它的 CSS / JS 放进 public/,在 HTML 里直接外链引入:
<head>
<link rel="stylesheet" href="css/fontawesome.min.css" />
<link rel="stylesheet" href="css/adminlte.min.css" />
</head>
<body>
<div id="app"></div>
<script src="js/jquery.min.js"></script>
<script src="js/bootstrap.bundle.min.js"></script>
<script src="js/adminlte.min.js"></script>
</body>
【代码注释】这些第三方资源不走 webpack 打包,原因有三:1)它们已是压缩、稳定的 UMD/IIFE 产物,再打一遍只是浪费构建时间;2)放 public/ 由浏览器直接加载,可利用 HTTP/2 并行;3)稳定库的 URL 不变,利于浏览器长期缓存。市面应用:许多企业项目进一步把通用库(jQuery、Vue 运行时)改用 CDN 外链,配合 webpack 的 externals 把它们从 bundle 排除,减小产物体积。
【实战要点】
- 经典应用场景:管理后台、电商后台、政企系统常用 AdminLTE / Ant Design Pro 类成品 UI 框架。
- 常见坑:把第三方 CSS 放
src/然后 import,webpack 解析其中url(...)时常因路径失败报错。 - 性能与最佳实践:稳定第三方库放
public/+ CDN;用externals把它们从 bundle 排除。
【本章小结】
| 资源类型 | 处理方式 |
|---|---|
| 业务 JS/CSS | import 到 src,走 webpack |
| 第三方稳定库 | 放 public,CopyWebpackPlugin 拷贝 |
| 入口 HTML | 放 public,HtmlWebpackPlugin 模板化 |
| favicon/robots | 放 public,原样拷贝 |
记忆口诀:「稳定库不编译,HTML 模板化,业务码走打包」。
【面试考点】
Q1:HtmlWebpackPlugin 与 CopyWebpackPlugin 都涉及 HTML,会冲突吗?
A:不会,但要靠 globOptions.ignore: ['**/*.html'] 避免双重处理——HtmlWebpackPlugin 已把模板渲染为最终 HTML,CopyWebpackPlugin 若再拷贝原始模板会覆盖产物。这是典型的「插件分工 + 边界划清」案例。
五、单页应用与前端路由原理
5.1 SPA 的运行机制
名词解释:
- SPA(Single Page Application):浏览器只加载一次 HTML,之后通过 JS 动态切换视图、操作 DOM、调接口。
- 前端路由:在浏览器内部维护「当前 URL → 对应视图」的映射,跳转时不再向服务器请求新页面。
概念与底层原理:
传统多页应用(MPA)每次点击链接都向服务器请求一份新 HTML——网络延迟可感知、页面状态丢失。SPA 的革命在于:首屏加载一次 HTML 与 bundle,此后所有「跳转」都由前端 JS 在内存中完成。

【代码注释】这张时序图揭示了 SPA 的核心——首次访问加载 HTML + bundle,之后点击 /admin 链接时,前端路由用 history.pushState 拦截跳转、在内存中切换视图,全程不向服务器请求新页面、不刷新。这就是 SPA「丝滑」体验的来源。市面应用:Gmail、Notion、GitHub 的大部分交互都是 SPA;Vue Router、React Router 都是这套机制的工业化封装。
5.2 hash 模式与 history 模式
名词解释:
- hash 模式:用 URL 中
#后的部分表示路由,变化不触发浏览器导航请求。 - history 模式:基于 HTML5 History API(
pushState/popstate),URL 形如传统多页但不刷新。
概念与底层原理:
前端路由有两种实现,差异源于浏览器对 URL 不同部分的处理:
- hash 模式:URL 里
#之后的部分(hash)变化时,浏览器永远不会发请求,只触发hashchange事件。监听它即可实现路由。 - history 模式:用
history.pushState(state, '', '/admin')修改 URL 但不刷新,监听popstate处理前进/后退。
import SMERouter from 'sme-router';
// 第二个参数 'html5' 启用 history 模式,默认是 hash 模式
const router = new SMERouter('app', 'html5');
router.route('/index', (req, res) => res.render('<h1>首页</h1>'));
router.route('/admin', (req, res) => res.render('<h1>管理员列表</h1>'));
router.route('*', (req, res) => res.redirect('/index')); // 兜底
【代码注释】SMERouter('app', 'html5') 把 id 为 app 的 DOM 作为视图容器,'html5' 启用 history 模式。router.route(path, handler) 注册路径与处理函数,写法刻意模仿 Express 中间件 (req, res) => {},便于熟悉后端的人快速上手。'*' 是兜底路由,处理未匹配的路径。市面应用:Vue Router 的 createWebHashHistory 与 createWebHistory、React Router 的 HashRouter 与 BrowserRouter,正是这两种模式的对应实现。
权威参考:MDN History API:https://developer.mozilla.org/zh-CN/docs/Web/API/History_API
5.3 history 模式的服务端 fallback
概念与底层原理:
history 模式有个绕不开的工程问题:URL http://x.com/admin 看起来像真实路径,但服务器磁盘上根本没有 /admin 这个文件。用户直接访问或刷新时,服务器返回 404。
解决方案是「服务端 fallback」——把所有未匹配的路径都返回 index.html,让前端路由接管:

【代码注释】这是 history 模式能正常工作的「魔法」所在——服务器对任何路径都返回同一份 index.html,浏览器拿到 HTML 后执行 bundle,前端路由读取 location.pathname 渲染对应视图。开发期由 DevServer 的 historyApiFallback: true 实现,生产期由 Nginx 的 try_files $uri /index.html 实现。市面应用:所有用 history 模式的 SPA 上线时都必须配置这一项,否则「首页能进、刷新子页面 404」是最常见的上线事故。
【实战要点】
- 经典应用场景:后台管理系统、应用类站点用 history 模式;纯静态托管(GitHub Pages)用 hash 模式。
- 常见坑:history 模式上线后刷新子页面 404——记得配服务端 fallback。
- 性能与最佳实践:用
replaceState替代pushState可避免污染浏览器历史栈(如重定向场景)。
【本章小结】
| 模式 | URL | 实现 | 服务端要求 |
|---|---|---|---|
| hash | /#/admin | 监听 hashchange | 无 |
| history | /admin | pushState + popstate | fallback 到 index.html |
记忆口诀:「Hash 简单不刷新,History 干净要 fallback」。
【面试考点】
Q1:hash 模式与 history 模式的区别?
A:1)URL 形式——hash 带 #,history 不带;2)实现机制——hash 监听 hashchange,history 用 history.pushState/popstate;3)SEO——history 的 URL 干净更利于搜索引擎;4)部署——hash 模式服务器零配置,history 模式必须把所有未匹配路径 fallback 到 index.html。业务系统一般选 history,纯静态 demo 选 hash。
Q2:为什么 history 模式刷新会 404?怎么解决?
A:history 模式的 URL(如 /admin)是前端虚拟路径,服务器磁盘上没有对应文件,刷新时服务器找不到资源返回 404。解决方案是服务端 fallback——把所有 404 请求返回 index.html:开发期用 DevServer historyApiFallback,生产期用 Nginx try_files $uri $uri/ /index.html。
六、EJS 模板引擎:视图即函数
6.1 模板引擎的本质
名词解释:
- 模板引擎:把「带占位符的字符串」+「数据对象」编译为最终 HTML 的库;EJS、Handlebars、Pug 均属此类。
- EJS(Embedded JavaScript):可在 HTML 中直接嵌入 JS 表达式的模板引擎。
概念与底层原理:
把视图写在 JS 里有三种方式,痛苦逐级升高:字符串拼接('<div>' + name + '</div>')最痛苦;模板字符串(`<div>${name}</div>`)稍好但仍把 HTML 拌进 JS;模板引擎把 HTML 与 JS 彻底解耦——.ejs 文件里写完整 HTML,只在需要的地方嵌入少量 <%= name %>。
EJS 的核心语法:
| 语法 | 作用 |
|---|---|
<%= expr %> | 输出转义后的值(防 XSS) |
<%- expr %> | 输出原始 HTML(信任来源时) |
<% js %> | 执行 JS 但不输出(控制结构) |
<%# comment %> | 注释 |
【代码注释】<%= %> 默认对 & < > 做 HTML 转义——如果用户输入 <script>steal()</script>,转义后会作为纯文本显示而非执行,这是模板引擎的内置 XSS 防护。只有完全信任来源时(如后端 markdown 编译结果)才用不转义的 <%- %>。市面应用:所有模板引擎都默认转义防 XSS,这是 Web 安全的基础设施。
6.2 ejs-loader 的编译范式
概念与底层原理:
配合 ejs-loader,webpack 可以把 .ejs 文件预编译成一个「接受数据、返回 HTML 字符串」的 JS 函数:

【代码注释】这张图揭示了一个关键事实:import indexV from './index.ejs' 拿到的不是字符串,而是一个函数——ejs-loader 在构建期把模板编译成 (data) => HTML 的函数,所以必须调用 indexV(data) 才能得到 HTML。「视图 = 函数(数据 → HTML)」这个范式极其重要——它正是 React 组件「(props) => UI」思想的雏形。市面应用:Vue 单文件组件的 template 被编译成 render 函数、React 的 JSX 被编译成 createElement 调用,本质都是「模板/视图编译为函数」这一思路的延伸。
const { merge } = require('webpack-merge');
// config/webpack.base.config.js 中的 loader 配置
module.exports = {
module: {
rules: [
{
test: /\.ejs$/,
loader: 'ejs-loader',
options: { variable: 'data' } // 模板内用 data.xxx 访问
}
]
}
};
【代码注释】variable: 'data' 让模板里的数据访问统一走 data. 前缀(data.title、data.list),避免与全局变量名冲突,也让模板更易读。市面应用:许多团队规定模板内所有数据访问都加 data. 前缀,作为编码规范。
6.3 视图模块化组织
把视图按页面拆分,每个页面一个 .ejs 文件:
import SMERouter from 'sme-router';
import indexV from '@/views/index'; // 拿到的是函数
import adminV from '@/views/admin';
import loginV from '@/views/login';
const router = new SMERouter('app', 'html5');
router.route('/index', (req, res) => {
// 调用模板函数,传入数据,得到 HTML
res.render(indexV({ title: '硅谷', message: '欢迎来到管理控制台' }));
});
router.route('/admin', (req, res) => res.render(adminV())); // 无数据直接调用
router.route('/login', (req, res) => res.render(loginV()));
router.route('*', (req, res) => res.redirect('/index'));
【代码注释】indexV({ title, message }) 调用模板函数注入数据得到 HTML,再交给 res.render 写入路由容器。这种「数据驱动视图」的思想,几年后被 Vue / React 用响应式系统推到极致——但底层逻辑一脉相承:视图是数据的投影。市面应用:AdminLTE + EJS 模板是 2018 年前后中小后台项目的流行搭配,理解它有助于读懂大量遗留项目,也能反衬出现代框架「响应式」的价值。
【实战要点】
- 经典应用场景:中小型后台、官网、营销页;不需要响应式但要清晰组织 HTML 的项目。
- 常见坑:忘记
options: { variable: 'data' },模板里写<%= title %>不报错却拿不到值。 - 性能与最佳实践:模板只负责展示,数据处理逻辑放 JS;升级到 Vue/React 后视图与逻辑分离会更彻底。
【本章小结】
| 步骤 | 关键 |
|---|---|
| 安装 | npm i ejs-loader -D |
| 配置 | module.rules 加 .ejs 规则 |
| 使用 | import view(函数)→ view(data)(HTML) |
记忆口诀:「Ejs 即函数,Import 加调用」。
【面试考点】
Q1:模板引擎与 Vue/React 组件的本质区别?
A:模板引擎(EJS、Handlebars)只解决「数据 + 模板 → HTML 字符串」的一次性问题,没有响应式——数据变化后必须手动重新调用模板函数并替换 DOM。Vue/React 在此之上增加了响应式系统——数据变化自动触发重新渲染,并通过虚拟 DOM 高效 diff 出最小 DOM 操作。所以模板引擎适合「一次性视图生成」「服务端渲染」,现代动态交互优先用框架。
七、html-loader 与媒体资源处理
7.1 html-loader 的作用与原理
名词解释:
- html-loader:一个 webpack loader,专门解析 HTML 文件中对资源的引用(
<img src>、<audio src>、<source src>等),将这些引用转换为 webpack 可以追踪和处理的模块依赖。 - Asset Module:webpack 5 内置的资源处理机制,分为
asset/resource(输出单独文件)、asset/inline(Base64 内联)、asset(按大小自动选择)三种类型。
概念与底层原理:
webpack 本质上是一个 JS 模块打包工具——它只认识 import / require。当 HTML 里的 img 标签 src 属性指向本地路径(如 ./logo.png)时,webpack 看不见这个依赖:logo.png 不会被打包,部署后路径也会失效。
html-loader 的职责就是把 HTML 文件里的资源引用「翻译」成 webpack 能处理的模块依赖:
// html-loader 对图片引用的转换示意(伪代码)
// 原 HTML 中:img 标签的 src 指向 ./avatar.png
// loader 内部转换为:
import __img0__ from './avatar.png'; // webpack 追踪并处理这张图片
// 输出时该路径自动替换为 hash 命名:static/images/${contentHash}.png
【代码注释】html-loader 把 HTML 中的 src、href 等静态路径当作 JS import 语句处理,触发对应的 asset rule 重命名、压缩、输出。转换后 img 的 src 属性值变为经过 contenthash 命名的真实路径,浏览器能正确加载。市面应用:在 webpack 项目中于 HTML 模板里直接写相对路径引用图片,是最符合直觉的写法,html-loader 让这种写法在工程化场景下正常工作。
// config/webpack.base.config.js — 处理 HTML 文件内部的资源引用
module.exports = {
module: {
rules: [
{
test: /\.html$/i,
use: ['html-loader'] // 解析 HTML 中 img/audio/video 等标签的资源引用
}
]
}
};
【代码注释】/\.html$/i 的 i 标志让匹配大小写不敏感——工程里存在 .HTML(Windows 常见)也能匹配。html-loader 默认处理 <img src>、<source src>、<link href> 等属性;可通过 sources 选项精细控制哪些属性要追踪。市面应用:在 HTML 模板里直接用相对路径引用图片是最自然的写法,html-loader 让这种写法在 webpack 工程里可行;Vue 单文件组件的 template 处理中,vue-loader 内部也有类似的资源路径转换逻辑。
【代码注释】这张流程图展示了 html-loader 在 webpack 依赖图中的位置:它不处理图片本身,只负责「发现引用」并把图片注册为模块,让后续 asset rule 统一处理(压缩、重命名、输出)。理解这个流程有助于排查「打包后图片路径失效」的问题——根因往往是资源引用没有被 html-loader 发现、游离在依赖图之外。市面应用:Webpack 5 官方文档的 Asset Modules 指南详细描述了这一机制。
7.2 音频与媒体等媒体资源的 loader 配置
除图片之外,项目里还会遇到音频(mp3、ogg)和影像(mp4、webm)。webpack 5 的 Asset Module 统一处理这些二进制资源:
// config/webpack.base.config.js — 图片、字体、媒体的完整 asset 配置
module.exports = {
module: {
rules: [
// 图片:小于 20KB 内联 Base64,否则输出独立文件
{
test: /\.(png|jpe?g|gif|webp)$/,
type: 'asset',
parser: {
dataUrlCondition: { maxSize: 20 * 1024 } // 20KB 阈值
},
generator: {
filename: 'static/images/[hash:20][ext][query]'
}
},
// 字体文件:始终输出独立文件
{
test: /\.(ttf|woff2?|woff|svg|eot)$/,
type: 'asset/resource',
generator: {
filename: 'static/fonts/[hash:12][ext][query]'
}
},
// 媒体文件:始终输出独立文件(音频与媒体体积大,不宜内联)
{
test: /\.(mp3|mp4|webm|ogg)$/,
type: 'asset/resource',
generator: {
filename: 'static/medias/[hash:12][ext][query]'
}
}
]
}
};
【代码注释】三条规则体现了不同资源的处理策略差异:图片用 asset(自动选择)因为小图内联可减少请求数;字体用 asset/resource 是因为浏览器需要独立 URL 才能加载字体;媒体同样用 asset/resource——音频 mp3 几百 KB 到几 MB,Base64 编码后体积增加 33% 且无法利用浏览器缓存,输出独立文件是正确选择。[hash:20] 对图片用 20 位哈希保证唯一性;[ext] 保留原始扩展名便于浏览器识别 MIME 类型。市面应用:大厂项目(如 B 站 Web 端、淘宝)对图片有专门 CDN 处理,开发期通常直接用 asset/resource 输出文件,上线前再替换 CDN 路径。
| 类型 | webpack type | 适用场景 |
|---|---|---|
| 图片(小) | asset(自动内联) | icon、小图 < 20KB |
| 图片(大) | asset/resource | banner、背景图 |
| 字体 | asset/resource | woff2 / ttf / eot |
| 音频与媒体 | asset/resource | mp3 / mp4 / webm |
【实战要点】
- 经典应用场景:任何在 HTML/CSS/JS 中引用本地媒体资源的项目都需要这套配置。
- 常见坑:忘加
html-loader时,HTML 模板里img标签的本地src路径在开发环境能显示(webpack-dev-server 回退文件系统),打包后路径却失效——典型的「开发没问题上线就崩」。 - 性能与最佳实践:图片 20KB 阈值是经验值,SPA 首屏 icon 可调高到 8KB,避免首次加载阻塞;大图走 CDN,不应打进 bundle。
【本章小结】
| 资源 | 处理方案 | 关键点 |
|---|---|---|
| HTML 内图片/媒体引用 | html-loader | 让 webpack 「看见」HTML 内部依赖 |
| 图片(< 阈值) | asset(自动内联) | 减少小图请求数 |
| 字体 / 媒体 | asset/resource | 保持独立 URL,利于缓存 |
记忆口诀:「Html-loader 发现引用,asset 规则处理资源」。
【面试考点】
Q1:webpack 处理 HTML 中 img 标签的本地路径引用需要什么配置?
A:需要 html-loader——它把 img 标签 src 属性中的本地路径变成 webpack 模块依赖,触发对应的 asset rule 处理(重命名、压缩、输出)。如果只配了 asset rule 而没有 html-loader,HTML 里的引用不会被 webpack 追踪,打包后路径失效。
Q2:type: 'asset' 与 type: 'asset/resource' 的区别?
A:asset 是自动模式——小于 maxSize 阈值的文件转 Base64 内联进 JS/CSS,大于的输出独立文件;asset/resource 始终输出独立文件;asset/inline 始终 Base64 内联。选择取决于资源大小与缓存策略:小图内联减请求数,大文件独立输出利于 CDN 缓存。
7.3 DevServer 进阶:自定义域名与 ESLint 集成
开发期的完整 devServer 配置远不止 port: 3000。以下是工程中常用的进阶选项:
// config/webpack.dev.config.js — 开发环境完整配置
const { merge } = require('webpack-merge');
const baseConfig = require('./webpack.base.config');
const ESLintPlugin = require('eslint-webpack-plugin');
module.exports = merge(baseConfig, {
mode: 'development',
devtool: 'cheap-module-source-map',
devServer: {
host: 'shirly.com', // 自定义域名(需在 hosts 文件中添加 127.0.0.1 shirly.com)
port: 8080,
open: true, // 启动后自动打开浏览器
historyApiFallback: true, // history 模式必备:所有 404 返回 index.html
},
plugins: [
new ESLintPlugin({
context: path.resolve(__dirname, '../src') // 只检查 src/ 下的文件
})
]
});
【代码注释】host: 'shirly.com' 代替 localhost 的常见原因:某些 Cookie 策略(SameSite)要求域名而非 IP;多人联调时可在 hosts 中统一配置相同域名;部分第三方 OAuth 回调只接受域名。historyApiFallback: true 让 DevServer 对「找不到文件」的请求统一返回 index.html,确保刷新 /admin 时前端路由能接管。ESLintPlugin 在每次文件变动时自动 lint,把语法问题暴露在编译期而非运行期。市面应用:大厂的 webpack 开发配置中,ESLint 集成、CORS 代理(proxy)、https 配置是三个最常用的 devServer 进阶选项。
【实战要点】
- 经典应用场景:多人协作项目强制 ESLint;需要与后端联调时配置 devServer proxy 转发请求。
- 常见坑:修改
host后忘记在系统 hosts 文件加映射(127.0.0.1 shirly.com),导致浏览器找不到服务器。 - 性能与最佳实践:开发期
cheap-module-source-map比source-map快 2~5 倍,只记录行映射、省略列映射,已够用于定位问题。
【面试考点】
Q1:devServer 的 historyApiFallback 为什么必须和 history 模式路由配套使用?
A:history 模式下 URL 形如 /admin,刷新时浏览器向 DevServer 请求 /admin 对应的资源——但磁盘上只有 index.html,没有 admin 文件,DevServer 默认返回 404。historyApiFallback: true 让 DevServer 对所有 「找不到文件」的 GET 请求统一返回 index.html,前端路由读到 /admin 后再渲染正确视图。
八、生产环境 CSS 抽离与压缩
8.1 为什么生产环境要抽离 CSS
名词解释:
- style-loader:把 CSS 字符串动态注入
<style>标签,依赖 JS 执行——只适合开发环境。 - MiniCssExtractPlugin:把 CSS 从 JS bundle 中抽离,生成独立的
.css文件,可被浏览器并行加载、独立缓存。 - PostCSS:一个 CSS 处理平台,通过插件(如
autoprefixer)为 CSS 属性自动添加浏览器厂商前缀(-webkit-、-moz-)。
概念与底层原理:
开发环境用 style-loader 把 CSS 注入 <style> 标签有一个致命缺陷:CSS 要等 JS 执行完才能生效——用户看到的是白屏或无样式的裸 HTML,直到 bundle 加载、解析、执行完毕。这个「CSS 闪烁(FOUC)」在开发期可以忍,生产环境不可接受。
生产环境的正确做法:
- MiniCssExtractPlugin 把 CSS 从 JS 里分离为独立文件(
main.[hash].css) - 浏览器可以并行请求 JS 和 CSS,互不阻塞
- CSS 文件拥有独立的 contenthash,业务逻辑改变不影响 CSS 缓存(反之亦然)
【代码注释】这张对比图清楚呈现了两种环境对 CSS 的不同处理策略。开发期动态注入 <style> 标签支持热模块替换(HMR),CSS 改动无需刷新页面;生产期独立 .css 文件利于缓存分层——纯样式改动不破坏 JS 缓存,业务逻辑改动不破坏 CSS 缓存。「开发快迭代,生产优缓存」是工程化配置的基本思路。市面应用:Vue CLI、Create React App 都是开发期用 style-loader / CSS-in-JS,生产期用 MiniCssExtractPlugin 导出独立 CSS。
8.2 MiniCssExtractPlugin 的工作原理
// config/webpack.prod.config.js — 生产环境 CSS 抽离配置
const { merge } = require('webpack-merge');
const baseConfig = require('./webpack.base.config');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
module.exports = merge(baseConfig, {
mode: 'production',
devtool: 'source-map', // 生产期:完整 source-map,供错误追踪
module: {
rules: [
// 覆盖 base 中的 css 规则:用 MiniCssExtractPlugin.loader 替换 style-loader
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
},
{
test: /\.less$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader']
}
]
},
optimization: {
minimizer: [
'...', // 保留 webpack 内置的 JS 压缩(terser)
new CssMinimizerPlugin() // 追加 CSS 压缩
]
},
plugins: [
new MiniCssExtractPlugin({
filename: 'static/css/main-[contenthash:12].css' // contenthash 实现 CSS 独立缓存
})
]
});
【代码注释】几个关键设计决策值得注意:① MiniCssExtractPlugin.loader 替换 style-loader——两者不能共存,前者提取到文件、后者注入 <style>;② '...' 是 ES5 spread 的 webpack 语法糖,等同于 ...webpack.default.minimizer——它保留 terser(JS 压缩),否则自定义 minimizer 会把 JS 压缩覆盖掉;③ contenthash 而非 hash——前者只随该 CSS 文件内容变化,实现 CSS 与 JS 缓存独立失效。市面应用:几乎所有生产级 webpack 配置都是这套:开发 style-loader,生产 MiniCssExtractPlugin,这是行业标准模式。
8.3 CssMinimizerPlugin 与 PostCSS 自动前缀
PostCSS 与 autoprefixer:
现代 CSS 属性(如 display: flex、user-select、appearance)在旧版浏览器中需要厂商前缀(-webkit-、-moz-、-ms-)。手写前缀繁琐且难维护——PostCSS + autoprefixer 在构建时自动注入:
/* 源码(你写的) */
.container {
display: flex;
user-select: none;
}
/* postcss + autoprefixer 处理后(webpack 输出)*/
.container {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
【代码注释】postcss-loader 在 css-loader 之前运行,它调用 PostCSS 插件链处理 CSS 字符串,autoprefixer 查询 Can I Use 数据库为需要前缀的属性自动补全。postcss.config.js 是 PostCSS 的配置文件(与 webpack.config.js 平级),配置 browserslist 决定要兼容的浏览器范围:
// postcss.config.js
module.exports = {
plugins: [
require('autoprefixer') // 读取 package.json 的 browserslist 决定兼容范围
]
};
【代码注释】PostCSS 生态极为丰富——除 autoprefixer 外,postcss-preset-env 可把未来 CSS 语法降级、purgecss 可删除未使用的 CSS class,这些插件都在 postcss.config.js 中配置,loader 链不变。市面应用:Tailwind CSS 本身就是一个 PostCSS 插件;Create React App、Next.js 内置 PostCSS 支持,用户可在项目根目录放 postcss.config.js 扩展。
CssMinimizerPlugin 压缩原理:
// optimization.minimizer 中追加 CSS 压缩
new CssMinimizerPlugin()
// 等效于运行 cssnano:
// .container { display: flex; color: #ff0000; }
// → .container{display:flex;color:red} ← 删空格、简化颜色、合并规则
【代码注释】cssnano(CssMinimizerPlugin 的底层)不只是删空格——它还会合并重复规则、简化 #ff0000 → red、去掉冗余选择器。在大型项目中,CSS 压缩通常能减少 20~40% 体积。webpack 5 默认只压缩 JS(terser),显式配置 minimizer 时必须用 '...' 保留内置 JS 压缩器,否则 JS 压缩会被关掉。
【实战要点】
- 经典应用场景:所有要上线的 SPA/MPA 项目的生产配置;SSR 项目(Next.js / Nuxt.js)同样内置了这套流程。
- 常见坑:在
optimization.minimizer里漏写'...',导致生产包 JS 没有被压缩、体积膨胀数倍——这是新人最常犯的错误。 - 性能与最佳实践:CSS 抽离 + contenthash 是「缓存分层」的基础。业务逻辑改 JS 不会破坏 CSS 缓存,纯样式改动不破坏 JS 缓存,两者的缓存策略可以独立设置 max-age。
【本章小结】
| 环境 | CSS 处理 | 优势 |
|---|---|---|
| 开发 | style-loader | 支持 HMR,实时看效果 |
| 生产 | MiniCssExtractPlugin | 独立文件、并行加载、独立缓存 |
| 生产(压缩) | CssMinimizerPlugin | 去冗余、删注释、减体积 |
| 生产(前缀) | postcss-loader + autoprefixer | 自动兼容旧浏览器 |
记忆口诀:「开发 style 注入,生产 Extract 抽离,PostCSS 加前缀,Minimizer 压缩」。
【面试考点】
Q1:style-loader 和 MiniCssExtractPlugin.loader 为什么不能同时使用?
A:两者目的互斥——style-loader 把 CSS 转为 JS 模块,运行时动态创建 <style> 注入 DOM;MiniCssExtractPlugin.loader 把 CSS 收集到独立文件、通过 <link> 加载。webpack 在 loader 链处理 CSS 时只能走一条路:注入 DOM 或输出文件,选哪个由环境决定,所以两者只能二选一。
Q2:生产环境 minimizer 里为什么要写 '...'?
A:webpack 5 内置了 TerserPlugin(JS 压缩),它默认存在于 optimization.minimizer 里。一旦你自定义 minimizer 数组,webpack 认为你要接管全部压缩逻辑,内置的 Terser 就被移除了。'...' 是 webpack 5 新增的语法,表示「展开内置 minimizer」,等于把 TerserPlugin 加回来,确保 JS 继续被压缩。
附录:可运行 Demo
为了把前述抽象原理落到可触摸的代码,下面给出两个完整、零依赖、可直接运行的 HTML——把它们保存为本目录下的 .html 文件双击打开即可,无需 npm 安装。
Demo 一 · 手写 SPA hash 路由 + EJS 式模板函数
这个 Demo 在 50 行内复刻了 sme-router 的核心思想(hash 路由)与 ejs-loader 的核心范式(视图即函数):
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>SPA 路由 Demo</title>
<style>
body { margin: 0; font-family: -apple-system, "PingFang SC", sans-serif; }
header { background: #343a40; color: #fff; padding: 12px 24px; }
nav a { color: #ffc107; margin-right: 16px; text-decoration: none; }
nav a.active { color: #fff; border-bottom: 2px solid #fff; padding-bottom: 4px; }
main { padding: 24px; }
.card { border: 1px solid #ddd; padding: 16px; border-radius: 4px; }
</style>
</head>
<body>
<header>
<h2 style="margin:0">广告后台管理系统</h2>
<nav style="margin-top:8px">
<a href="#/index" data-link>控制台</a>
<a href="#/admin" data-link>管理员列表</a>
<a href="#/adv" data-link>广告列表</a>
</nav>
</header>
<main id="app">加载中...</main>
<script>
// ① 模拟 ejs 模板:每个视图是一个「数据 → HTML 字符串」的函数
const views = {
index: (d) => `<div class="card"><h1>${d.title}</h1><p>${d.message}</p></div>`,
admin: () => `<div class="card"><h1>管理员列表</h1><p>张三 / 李四 / 王五</p></div>`,
adv: () => `<div class="card"><h1>广告列表</h1><p>暂无数据</p></div>`
};
// ② 路由表:path → handler(模拟 sme-router 的 route 注册)
const routes = {
'/index': () => views.index({ title: '欢迎', message: '当前路径 #/index' }),
'/admin': () => views.admin(),
'/adv': () => views.adv()
};
// ③ 监听 hashchange 渲染对应视图(hash 模式核心)
function render() {
const hash = location.hash.replace(/^#/, '') || '/index';
const handler = routes[hash] || (() => '<p>404</p>');
document.getElementById('app').innerHTML = handler();
document.querySelectorAll('nav a').forEach(a =>
a.classList.toggle('active', a.getAttribute('href') === '#' + hash));
}
window.addEventListener('hashchange', render);
window.addEventListener('DOMContentLoaded', render);
</script>
</body>
</html>
【代码注释】这段代码浓缩了 SPA 的全部核心:① views 对象里每个视图都是函数,等价于 ejs-loader 编译后的 (data) => HTML,印证了「视图即函数」范式;② routes 表把 path 映射到 handler,等价于 router.route(path, handler);③ 监听 hashchange 切换视图——点击 <a href="#/admin"> 时浏览器只改 hash 不刷新,触发 render 重渲染。保存为 .html 双击即可看到点击导航无刷新切换页面、当前项高亮。市面应用:Vue Router 的 createWebHashHistory 内部与这段代码思路高度一致,只是封装得更完善。
Demo 二 · webpack-merge 合并行为对比
这个 Demo 直观演示「Object.assign 浅合并丢失数组项」与「智能深合并保留数组项」的差异:
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8" /><title>合并行为对比</title>
<style>body{font-family:monospace;padding:24px;line-height:1.8}pre{background:#f5f5f5;padding:12px;border-radius:6px}</style>
</head>
<body>
<h2>Object.assign vs 智能合并(webpack-merge 原理)</h2>
<pre id="out"></pre>
<script>
const base = { mode: 'none', plugins: ['HtmlPlugin'], output: { path: '/dist' } };
const dev = { mode: 'development', plugins: ['HMRPlugin'], output: { clean: true } };
// 浅合并:plugins 被覆盖、output 被覆盖
const shallow = Object.assign({}, base, dev);
// 智能合并:数组追加、对象深合并(模拟 webpack-merge)
const smart = {
...base, ...dev,
plugins: [...base.plugins, ...dev.plugins],
output: { ...base.output, ...dev.output }
};
document.getElementById('out').textContent =
'Object.assign(浅合并):\n' + JSON.stringify(shallow, null, 2) +
'\n\n智能合并(webpack-merge):\n' + JSON.stringify(smart, null, 2);
</script>
</body>
</html>
【代码注释】运行后可清晰看到:Object.assign 的结果里 plugins 只剩 ['HMRPlugin'](base 的 HtmlPlugin 被覆盖丢失)、output 只剩 { clean: true }(path 丢失);而智能合并的结果里 plugins 是 ['HtmlPlugin', 'HMRPlugin']、output 同时保留 path 与 clean。这正是为什么必须用 webpack-merge 而非 Object.assign 合并配置——亲眼看到「插件被吞掉」的现象,比读十遍文档都印象深刻。市面应用:webpack-merge 的真实实现还会按 loader 规则的 test 字段智能合并,比这个 Demo 更精细。
总结
知识体系回顾(思维导图)
【代码注释】这张思维导图把「项目初始化」拆成六大主题——从「为什么这样组织目录」(架构哲学)到「如何配置构建」(多环境、HTML、静态资源),再到「如何组织视图」(SPA 路由、EJS 模板)。建议按此结构建立认知:先理解架构决策的动因,再掌握具体工具。市面应用:面试中「你如何从零搭建一个前端项目」这类题,可按此图分层回答,体现工程化思维而非堆砌工具名。
高频面试题速查
- 为什么区分 public 与 src? 拷贝资源 vs 编译资源,部署稳定性 vs 优化能力。
- webpack-merge 与 Object.assign 区别? 智能合并 vs 浅覆盖。
- 为什么拆 base/dev/prod? 物理隔离环境差异,可读 + 可维护。
- hash 与 history 模式区别? URL 形式、实现机制、SEO、部署要求。
- history 模式刷新 404 怎么解决? 服务端 fallback(DevServer / Nginx)。
- HtmlWebpackPlugin 模板变量怎么注入? options 字段 →
<%= htmlWebpackPlugin.options.xxx %>。 - CopyWebpackPlugin 与 HtmlWebpackPlugin 会冲突吗? 用 ignore 排除 HTML 即可。
- publicPath 是什么? 资源运行时 URL 前缀,影响线上资源加载。
- ejs-loader 编译后 import 是什么? 是函数
(data) => HTML,要调用才得字符串。 - EJS 的
<%= %>与<%- %>区别? 转义防 XSS vs 输出原始 HTML。 - html-loader 有什么用? 让 webpack 追踪 HTML 内部资源引用(
<img src>),配合 asset rule 处理路径与输出。 - 为什么音频与媒体用
asset/resource而非asset? 体积大,Base64 编码会增大 33% 且无法利用浏览器媒体缓存,独立文件更合理。 - 生产环境为什么用 MiniCssExtractPlugin? style-loader 依赖 JS 执行才注入样式,有 FOUC 风险且无法独立缓存;MiniCssExtractPlugin 让 CSS 并行加载、独立 contenthash 缓存。
minimizer里'...'的含义? 展开 webpack 内置的 TerserPlugin,防止自定义 minimizer 覆盖 JS 压缩。- postcss-loader 做了什么? 运行 PostCSS 插件链处理 CSS,最常用的 autoprefixer 自动补全浏览器厂商前缀。
学习建议
- 完整跑通骨架:从
npm init到npm start看到登录页,亲手搭一遍才能理解每个配置的作用。 - 刻意制造错误:把
plugins用Object.assign合并,观察插件被覆盖的现象,理解webpack-merge的价值。 - 手写简易路由:实现一个监听
hashchange/popstate的迷你路由,吃透前端路由原理。 - 对比框架方案:用 Vue 3 + Vue Router 重写同一个后台,体会响应式与组件化带来的开发效率差异。
- 实测部署:在 Nginx 里配
try_files,把 dist 部署到服务器,亲历 history 路由刷新场景。

839

被折叠的 条评论
为什么被折叠?



