前端工程化项目初始化实战:从零搭建可扩展架构

前端工程化项目初始化实战:从零搭建可扩展架构

导读:项目能否长期维护,七成取决于初始化阶段的架构决策。本文以一个真实「广告后台管理系统」为载体,系统讲解前端工程化项目从无到有的搭建过程——多环境 Webpack 配置的分层哲学、webpack-merge 的智能合并机制、HtmlWebpackPlugin 的模板引擎与资源注入、CopyWebpackPlugin 的静态资源策略、单页应用(SPA)前端路由的底层原理(hash 与 history 两种模式)、以及 EJS 模板引擎「视图即函数」的编译范式。每一项技术都从「为什么需要、底层如何运作、何时使用」三个维度展开,结合 MDN、Webpack 官方文档与工程实践经验,适合希望建立「项目架构思维」的中级前端工程师。

目录


一、工程化项目的目录架构哲学

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 的中大型前端项目;多人协作场景下结构约定尤其重要。
  • 常见坑:把第三方库 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-mergeObject.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.jsnpm start 是 npm 内置的免 run 快捷脚本之一。市面应用npm start 启动开发、npm run build 打包发布,是整个前端社区的命令约定。

【实战要点】

  • 经典应用场景:所有中大型项目;多环境(dev/test/staging/prod)部署。
  • 常见坑:用 Object.assign 合并配置导致插件/loader 数组被覆盖丢失。
  • 性能与最佳实践:进一步可用 mergeWithRules 精细控制单条 loader 规则的合并策略(如替换 base 中某条 rule 的 loader)。

【本章小结】

文件职责
baseentry / output / loader / 通用 plugin
devmode:development + devServer
prodmode:production + 抽离 + 压缩
webpack-merge智能合并三者

记忆口诀:「Base 共用,Dev 调试,Prod 上线,Merge 巧合并」。

【面试考点】

Q1:webpack-mergeObject.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/CSSimport 到 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 的 createWebHashHistorycreateWebHistory、React Router 的 HashRouterBrowserRouter,正是这两种模式的对应实现。

权威参考: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/adminpushState + popstatefallback 到 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.titledata.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 中的 srchref 等静态路径当作 JS import 语句处理,触发对应的 asset rule 重命名、压缩、输出。转换后 imgsrc 属性值变为经过 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$/ii 标志让匹配大小写不敏感——工程里存在 .HTML(Windows 常见)也能匹配。html-loader 默认处理 <img src><source src><link href> 等属性;可通过 sources 选项精细控制哪些属性要追踪。市面应用:在 HTML 模板里直接用相对路径引用图片是最自然的写法,html-loader 让这种写法在 webpack 工程里可行;Vue 单文件组件的 template 处理中,vue-loader 内部也有类似的资源路径转换逻辑。

HTML 文件
img 标签 src 本地路径

html-loader
解析资源引用

依赖图
module: banner.png

asset rule
输出文件或内联 Base64

最终 HTML
img src 替换为 hash 路径

【代码注释】这张流程图展示了 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/resourcebanner、背景图
字体asset/resourcewoff2 / ttf / eot
音频与媒体asset/resourcemp3 / 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-mapsource-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)」在开发期可以忍,生产环境不可接受。

生产环境的正确做法:

  1. MiniCssExtractPlugin 把 CSS 从 JS 里分离为独立文件(main.[hash].css
  2. 浏览器可以并行请求 JS 和 CSS,互不阻塞
  3. CSS 文件拥有独立的 contenthash,业务逻辑改变不影响 CSS 缓存(反之亦然)

生产环境

CSS → MiniCssExtractPlugin

独立 .css 文件
标签并行加载

开发环境

CSS → style-loader

注入

【代码注释】这张对比图清楚呈现了两种环境对 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: flexuser-selectappearance)在旧版浏览器中需要厂商前缀(-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 的底层)不只是删空格——它还会合并重复规则、简化 #ff0000red、去掉冗余选择器。在大型项目中,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-loaderMiniCssExtractPlugin.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 更精细。

总结

知识体系回顾(思维导图)

项目初始化

架构哲学

config/src/public 三分

关注点分离

结构自我解释

多环境配置

base/dev/prod 分层

webpack-merge 智能合并

publicPath 资源前缀

HTML 处理

模板变量注入

资源自动注入

minify 压缩

静态资源

编译资源走 src

拷贝资源走 public

CopyWebpackPlugin

AdminLTE 接入

SPA 与路由

首屏一次加载

hash 模式

history 模式

服务端 fallback

EJS 模板

视图即函数

ejs-loader 预编译

data 前缀

XSS 转义

媒体资源处理

html-loader 发现引用

asset/resource 媒体文件

图片阈值内联策略

生产 CSS 优化

MiniCssExtractPlugin

CssMinimizerPlugin

postcss-loader + autoprefixer

style-loader vs Extract

【代码注释】这张思维导图把「项目初始化」拆成六大主题——从「为什么这样组织目录」(架构哲学)到「如何配置构建」(多环境、HTML、静态资源),再到「如何组织视图」(SPA 路由、EJS 模板)。建议按此结构建立认知:先理解架构决策的动因,再掌握具体工具。市面应用:面试中「你如何从零搭建一个前端项目」这类题,可按此图分层回答,体现工程化思维而非堆砌工具名。

高频面试题速查

  1. 为什么区分 public 与 src? 拷贝资源 vs 编译资源,部署稳定性 vs 优化能力。
  2. webpack-merge 与 Object.assign 区别? 智能合并 vs 浅覆盖。
  3. 为什么拆 base/dev/prod? 物理隔离环境差异,可读 + 可维护。
  4. hash 与 history 模式区别? URL 形式、实现机制、SEO、部署要求。
  5. history 模式刷新 404 怎么解决? 服务端 fallback(DevServer / Nginx)。
  6. HtmlWebpackPlugin 模板变量怎么注入? options 字段 → <%= htmlWebpackPlugin.options.xxx %>
  7. CopyWebpackPlugin 与 HtmlWebpackPlugin 会冲突吗? 用 ignore 排除 HTML 即可。
  8. publicPath 是什么? 资源运行时 URL 前缀,影响线上资源加载。
  9. ejs-loader 编译后 import 是什么? 是函数 (data) => HTML,要调用才得字符串。
  10. EJS 的 <%= %><%- %> 区别? 转义防 XSS vs 输出原始 HTML。
  11. html-loader 有什么用? 让 webpack 追踪 HTML 内部资源引用(<img src>),配合 asset rule 处理路径与输出。
  12. 为什么音频与媒体用 asset/resource 而非 asset 体积大,Base64 编码会增大 33% 且无法利用浏览器媒体缓存,独立文件更合理。
  13. 生产环境为什么用 MiniCssExtractPlugin? style-loader 依赖 JS 执行才注入样式,有 FOUC 风险且无法独立缓存;MiniCssExtractPlugin 让 CSS 并行加载、独立 contenthash 缓存。
  14. minimizer'...' 的含义? 展开 webpack 内置的 TerserPlugin,防止自定义 minimizer 覆盖 JS 压缩。
  15. postcss-loader 做了什么? 运行 PostCSS 插件链处理 CSS,最常用的 autoprefixer 自动补全浏览器厂商前缀。

学习建议

  1. 完整跑通骨架:从 npm initnpm start 看到登录页,亲手搭一遍才能理解每个配置的作用。
  2. 刻意制造错误:把 pluginsObject.assign 合并,观察插件被覆盖的现象,理解 webpack-merge 的价值。
  3. 手写简易路由:实现一个监听 hashchange / popstate 的迷你路由,吃透前端路由原理。
  4. 对比框架方案:用 Vue 3 + Vue Router 重写同一个后台,体会响应式与组件化带来的开发效率差异。
  5. 实测部署:在 Nginx 里配 try_files,把 dist 部署到服务器,亲历 history 路由刷新场景。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值