适用读者:会写页面但不清楚"打包到底做了什么"的人;要解释 SPA 与后端分工的人。
前置:S14-J05、B05(工程化入门)、S02-B04(HTTP)。
覆盖旧文:4432(JavaScript 79k)、4433(XML/Tomcat/HTTP 26k)、4434(Servlet 70k)、4435(会话 59k)、4436(前端工程化 154k)、4437(微头条项目 85k)、4404(笔记 21k)→ 7 篇 / 约 52 万字符。
扩展点:E-S14-020(JS 的四类易错语义:提升、this、闭包、异步顺序)、E-S14-021(Servlet 容器与 SPA 的三种部署形态)、E-S14-022(构建产物的四步:解析→转换→打包→压缩,及各自的失效点)。
上一篇:S14-J05 · HTML/CSS 到布局:盒模型、Flex 与移动端适配
下一篇:补编第 3 篇 · Vue 2/3 笔记合并(待写)。
1. 一句话结论
前端的"魔法"只有两处真实机制:JS 的执行上下文与事件循环(决定代码何时跑、this 是谁),构建工具的模块图与产物(决定代码怎么变成文件)。其余都是约定与工具差异。
出处:
A-4432(JS)、A-4434(Servlet)、A-4436(工程化)、A-4437(项目)。
2. JS 的四类易错语义(E-S14-020)
2.1 声明提升与 TDZ
console.log(a); // undefined —— var 提升但未赋值
var a = 1;
console.log(b); // ❌ ReferenceError —— let/const 有暂时性死区(TDZ)
let b = 2;
2.2 this 的四种绑定(按优先级)
// ① new 绑定 > ② 显式绑定(call/apply/bind)> ③ 隐式绑定(obj.fn())> ④ 默认(严格模式 undefined)
const obj = { v: 1, get() { return this.v; } };
const g = obj.get;
g(); // undefined(默认绑定,非严格模式为 window)
obj.get(); // 1
g.call({ v: 2 }); // 2
const arrow = { v: 3, get: () => this.v }; // 箭头函数无自己的 this → 词法作用域
坑 1:箭头函数没有自己的 this,obj.get = () => this.v 取的是外层 this。
2.3 闭包:作用域而不是"函数里的函数"
function counter() { let n = 0; return () => ++n; } // n 被闭包持有
const c = counter(); c(); c(); // 1, 2
| 用途 | 例 |
|---|---|
| 私有状态 | 上例 |
| 循环 + 异步捕获 | for (let i…)(let 每次迭代新绑定)vs var(共享,全都打印末值) |
| 内存泄漏风险 | 闭包持有大对象 + 长生命周期引用 |
2.4 异步顺序:微任务优先
console.log('1');
setTimeout(() => console.log('4'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('2');
// 输出:1 2 3 4(微任务先于宏任务)
判据:事件循环 = 调用栈清空 → 清空微任务队列 → 取一个宏任务;await 之后的代码等价于微任务。
3. Servlet 容器与 SPA 的三种部署形态(E-S14-021)
| 形态 | 结构 | 适用 | 注意 |
|---|---|---|---|
| 同源同容器 | 前端产物打进 webapp/,与 Servlet 同端口 |
小项目、内网工具 | 刷新路由 404(需 history fallback) |
| 同域反代(推荐) | nginx 静态托管 + /api/ 反代后端 |
博客/中台(即本站形态) | try_files $uri /index.html 兜底 |
| 跨域分离 | 独立域名 + CORS | 前后端团队独立部署 | 预检、Cookie SameSite、凭据跨域 |
# 同域反代的正确兜底(SPA history 模式)
location / { try_files $uri $uri/ /index.html; }
location /api/ { proxy_pass http://127.0.0.1:8080/; }
Servlet 侧四条基础(4433/4434/4435 的压缩):
| 点 | 结论 |
|---|---|
| 生命周期 | init → service(多次)→ destroy;默认单实例多线程,不要在 Servlet 里放可变实例字段 |
| 请求/响应 | HttpServletRequest/Response,编码 setCharacterEncoding("UTF-8") 要在读参数前 |
| 会话 | HttpSession 靠 JSESSIONID Cookie;分布式要共享(Redis)或改无状态 JWT |
| 转发 vs 重定向 | forward 服务端一次请求;sendRedirect 两次请求、地址栏变 |
4. 构建产物四步与失效点(E-S14-022)
① 解析(resolve):入口 → 依赖图(node_modules、别名、扩展名)
② 转换(transform):TS/JSX → JS,CSS 预处理 → CSS,语法降级(target)
③ 打包(bundle):拆 chunk、tree-shaking、代码分割
④ 压缩(minify):压缩 + gzip/brotli 预压缩 + 内容哈希命名
| 步骤 | 常见失效 | 症状 |
|---|---|---|
| 解析 | 别名/扩展名未配 | Module not found(本地能跑因为缓存) |
| 转换 | target 过低/过高 |
老浏览器白屏 / 新语法报错 |
| 打包 | 未配 sideEffects |
tree-shaking 失效,包体虚大 |
| 压缩 | 只压 JS 不压 CSS/HTML | 首屏仍慢 |
| 全部 | 未加内容哈希 | 用户拿到旧缓存("改了没生效") |
打包分析的固定动作:
npx vite build --mode production
npx vite-bundle-visualizer # 或 rollup-plugin-visualizer
# 关注三件事:① 最大的 3 个 chunk ② 是否重复打包同一库 ③ 是否把整库当默认导入
判据:"包体大"先查是否整库导入(import _ from 'lodash' vs import debounce from 'lodash/debounce'),再查重复依赖,最后才考虑换工具。
5. 工程化的四条落地约定
| 约定 | 做法 | 收益 |
|---|---|---|
| 目录分层 | src/{pages,components,composables,stores,utils} |
可预测,新人易上手 |
| 路径别名 | @ → src |
消除 ../../.. |
| 环境变量 | import.meta.env.VITE_*(只暴露前缀变量) |
避免密钥进产物 |
| 质量闸门 | ESLint + Prettier + Vitest + CI | 提交前挡低级错误(见 S14-X02 的 Biome) |
6. 反例:五种"本地正常、线上异常"
| 现象 | 根因 |
|---|---|
| 刷新 404 | SPA history 模式缺 fallback |
| 接口 404 | 反代丢 /api 前缀(proxy_pass 末尾斜杠差异) |
| 白屏无报错 | target 过高,老浏览器不支持 |
| 改了没生效 | 产物名无内容哈希 + CDN 缓存 |
| 密钥泄露 | 环境变量未加 VITE_ 前缀却被打进代码 / 或被 console.log |
7. 自测
- TDZ 是什么?
var与let的差别? this四种绑定与优先级?箭头函数为何特殊?- 事件循环里微任务与宏任务的顺序?
- SPA 三种部署形态?nginx 兜底怎么写?
- 构建四步与各自失效点?
答案
let/const在声明前不可访问(报错);var提升为 undefined。- new > 显式 > 隐式 > 默认;箭头函数无自身 this,取词法作用域。
- 栈清空后先清微任务队列,再取一个宏任务。
- 同容器 / 同域反代 / 跨域分离;
try_files $uri $uri/ /index.html。 - 解析/转换/打包/压缩;模块找不到、语法不兼容、包体过大、缓存不更新。
8. 下一篇
补编第 3 篇 · Vue 2/3 笔记合并 —— Options 与 Composition 的真实差异、ref/reactive 的响应式边界、Vuex 与 Pinia 的取舍、路由守卫与项目结构。
本篇属于系列《S14 · 前端与可视化》。旧文来源:A-4432–4437、A-4404;扩展点 E-S14-020/021/022。