学会写代码只是第一步,学会「工程化地写代码」才是职业分水岭。本章覆盖 Node 基础、包管理、Git、构建原理、Vite/Webpack、代码规范、TypeScript 与 CI/CD,让你从「能写」升级到「能产出可维护的工程」。
Node 让 JS 跳出浏览器,也带来了完整的前端工具链。
// 最简单的 Node 脚本:node hello.js 运行
const fs = require("fs"); // 文件系统模块
const path = require("path"); // 路径模块
console.log("你好,Node!");
console.log(__dirname); // 当前文件所在目录
| 模块 | 作用 | 常用 API |
|---|---|---|
| fs | 文件读写 | readFile / writeFile / mkdir(都支持 Promise 版本) |
| path | 路径处理 | join / resolve / basename / extname |
| http / https | 发起 HTTP 请求 | 前端一般用 fetch 或 axios 封装 |
| os | 系统信息 | platform / cpus / totalmem |
| process | 进程信息 | env(环境变量)/ argv(命令行参数) |
// 读取文件(现代写法:Promise)
const fs = require("fs/promises");
async function readConfig() {
const data = await fs.readFile("./config.json", "utf-8");
return JSON.parse(data);
}
依赖管理是工程的命脉,版本策略与锁文件必须理解。
// 版本号格式:主版本.次版本.补丁版本(如 4.17.21)
// 主版本:不兼容的破坏性变更(升级要谨慎)
// 次版本:向后兼容的新功能
// 补丁版本:向后兼容的 bug 修复
// package.json 里的版本范围写法:
"react": "^18.2.0" // ^:允许 18.x.x(次版本内的更新),不允许 19.x
"react": "~18.2.0" // ~:只允许 18.2.x(补丁更新)
"react": "18.2.0" // 精确锁定
"react": "*" // 任意版本(千万别用)
npm init -y // 初始化项目(生成 package.json)
npm install // npm i // 安装 package.json 里的所有依赖
npm i react // 安装并写入 dependencies(生产依赖)
npm i -D typescript // -D:写入 devDependencies(开发依赖,构建产物不包含)
npm run dev // 运行 scripts 里的 dev 脚本
npm outdated // 查看依赖是否有新版本
npm audit // 检查依赖安全漏洞
pnpm:① 硬链接共享依赖,省磁盘;② 幽灵依赖隔离,杜绝「能用但没声明」的隐患;③ 安装快。团队统一用一个,别混用(混用会导致 node_modules 结构混乱)。
Git 是协作的基石,下面这些命令是生存必备。
// 拉取最新 → 建分支 → 改代码 → 提交 → 推送 → 提 PR
git pull // 拉取远程更新
git checkout -b feature/login // 新建并切换分支(命名:feature/ 前缀)
git add . // 暂存改动
git commit -m "feat: 新增登录功能" // 提交(遵循提交规范)
git push -u origin feature/login // 推送并建立关联
// 然后在远程(GitHub/GitLab)创建 Pull Request,走代码评审
| 命令 | 效果 | 适用 |
|---|---|---|
| git merge | 把分支合并进来,保留合并记录 | 默认、团队协作主流 |
| git rebase | 把提交「搬到」目标分支顶部,历史线性整洁 | 个人分支整理、保持历史清晰 |
| git cherry-pick | 挑单个提交应用到当前分支 | 紧急修复提版本 |
// 冲突解决:合并时双方改了同一处 → 打开文件找 <<<<<<< 标记,手工保留需要的部分
<<<<<<< HEAD
当前分支的内容
=======
被合并分支的内容
>>>>>>> feature/login
// 解决后:git add . && git commit(不要慌,冲突是常态)
feat: 新功能、fix: 修复、docs: 文档、refactor: 重构、perf: 性能、test: 测试、chore: 杂务。格式:类型(范围): 描述,如 fix(login): 修复密码框回车不提交。配合 husky 可以在提交前自动校验。
理解「为什么需要打包」,才能理解 Vite 和 Webpack 的设计。
| 时代 | 方案 | 问题 |
|---|---|---|
| 远古 | 多个 script 标签,全局变量 | 变量污染、依赖顺序靠人肉 |
| 第一代 | IIFE(立即执行函数)+ 全局挂载 | 手动管理依赖,还是乱 |
| 第二代 | CommonJS(Node)/ AMD(浏览器,require.js) | 浏览器无法直接用 CommonJS |
| 现代 | ES Modules(ESM) | 浏览器原生支持 + 静态分析 → 打包优化 |
require 是运行时同步加载,浏览器加载本地文件是异步的,无法同步阻塞等待——所以浏览器端模块标准必须走 ESM(import 声明是静态的,浏览器可以预先解析)。打包器的职责之一就是把 ESM/CommonJS 统一处理成浏览器能跑的形式。
| 步骤 | 做什么 | 产出 |
|---|---|---|
| ① 入口分析 | 从入口文件开始 | — |
| ② 依赖解析 | 递归解析 import/require,生成依赖图 | 依赖关系图 |
| ③ 转换编译 | loader 处理:TS→JS、JSX→JS、CSS、图片 | 转换后的模块代码 |
| ④ 合并打包 | 按规则合并成 chunk | bundle 文件 |
| ⑤ 代码分割 | 动态 import 拆成独立 chunk,按需加载 | 多个小文件 |
| ⑥ Tree Shaking | 删掉「未被使用的导出」 | 更小的体积 |
Vite 为什么快?理解它的两大核心机制。
Webpack 开发时要把所有模块打包成一个 bundle 才能启动——项目越大越慢(「启动 30 秒」梗的来源)。Vite 的思路完全不同:
存量项目大量使用 Webpack,理解它的核心概念才能维护改造。
| 概念 | 作用 | 例子 |
|---|---|---|
| Entry | 入口,打包起点 | entry: './src/main.js' |
| Output | 产物输出配置 | output: { filename: '[name].[hash].js' } |
| Loader | 转换模块内容(文件级) | babel-loader(JS)、ts-loader、css-loader、style-loader |
| Plugin | 打包全流程的扩展点(构建级) | HtmlWebpackPlugin、MiniCssExtractPlugin |
// webpack.config.js 最小示例
module.exports = {
mode: "development", // development / production
entry: "./src/index.js",
output: { path: __dirname + "/dist", filename: "bundle.js" },
module: {
rules: [
{ test: /\.js$/, use: "babel-loader", exclude: /node_modules/ },
{ test: /\.css$/, use: ["style-loader", "css-loader"] },
],
},
plugins: [new HtmlWebpackPlugin({ template: "./index.html" })],
devServer: { port: 3000, proxy: { "/api": "http://localhost:8080" } },
};
规范不是束缚,是让团队协作零摩擦的基础设施。
| 工具 | 职责 | 典型规则 |
|---|---|---|
| ESLint | 代码质量与规范(找错误与坏味道) | 未使用变量、禁止 console、强制分号 |
| Prettier | 代码格式化(统一风格,不判断对错) | 缩进 2 空格、单引号、行宽 100 |
| EditorConfig | 编辑器级别的基础配置 | 文件编码、缩进方式(跨 IDE 统一) |
| husky + lint-staged | Git 钩子:提交前自动检查 | 提交时只检查暂存的文件,又快又准 |
// package.json 里的钩子配置(husky + lint-staged)
{
"lint-staged": {
"*.{js,ts,jsx,tsx}": ["eslint --fix", "prettier --write"],
"*.{css,html,md}": ["prettier --write"]
}
}
// 效果:每次 git commit 前自动修复格式,坏代码进不了仓库
TS 是当前大厂前端的标配,从入门到熟练是这一节的目标。
// 基础类型标注
let age: number = 25;
let name: string = "张三";
let isDone: boolean = true;
let list: number[] = [1, 2, 3];
// 接口:定义对象结构
interface User {
id: number;
name: string;
email?: string; // ?: 可选属性
readonly createdAt: Date; // readonly:只读
}
function greet(user: User): string { // 参数类型 + 返回值类型
return `你好,${user.name}`;
}
// 泛型:类型像参数一样传进去,让函数复用且保持类型安全
function identity<T>(value: T): T {
return value;
}
const num = identity<number>(42); // T = number
const str = identity("hello"); // 自动推断 T = string
// 经典应用:API 响应包装
interface ApiResponse<T> {
code: number;
data: T;
message: string;
}
// 用一次,处处类型安全:
const res = await get<User[]>("/api/users"); // res.data 是 User[]
把「检查、构建、测试、部署」变成自动的流水线,质量与效率兼得。
// GitHub Actions 示例:.github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci // 按锁文件精确安装
- run: npm run lint
- run: npm run typecheck
- run: npm test
- run: npm run build
// 全部通过 → 绿色 ✅;任一失败 → 红色 ❌ 拦住合并
本章动手为主,两个项目做完即通关。
要求:① npm create vite@latest 创建项目并理解每个文件;② 接入 ESLint + Prettier + husky + lint-staged,提交不规范代码会被拦;③ 配置路径别名(@/ 指向 src);④ 配置代理解决开发期跨域;⑤ 加 TypeScript 严格模式;⑥ 配好提交规范(commitlint)。验收:随便写点代码,观察「提交时自动修复、commit 信息被校验」的效果。
用 Node 写一个 100 行左右的打包器:① 读取入口文件;② 用正则或简单的解析找到 import;③ 递归生成依赖图;④ 把模块包装成函数,生成一个可运行的 bundle.js。做完你会彻底理解「打包」到底在做什么。
工程化知识以理解 + 实操为准。
答案:Node 是单线程 + 事件驱动 + 非阻塞 I/O 模型,I/O 操作(读写文件、网络请求)交给底层 libuv 线程池处理,主线程不被阻塞,因此高并发 I/O 性能极佳。但 JS 运行在主线程上,CPU 密集计算会长时间占用主线程,导致事件循环被阻塞,所有后续回调排队等待,吞吐量急剧下降。
答案:^18.2.0 允许 18.x.x(次版本内更新,不跨主版本);~18.2.0 只允许 18.2.x(仅补丁更新);18.2.0 精确锁定。锁文件记录了依赖树的每一个精确版本号,提交锁文件可保证团队成员、CI 服务器、生产环境安装出完全一致的依赖树,避免「我机器上没问题」的版本漂移事故。
^ 在版本号第一位非零时不升级到下一主版本(^0.2.3 只允许 0.2.x);~ 在有次版本时锁定到该次版本,无次版本时允许补丁更新(~1 等同 1.x)。锁文件是工程可复现性的基础,删锁文件等于把「确定性」扔进了随机数里。答案:git merge 保留两个分支的历史,产生一个合并提交,历史呈分叉状;git rebase 把当前分支的提交「重放」到目标分支顶端,历史呈线性。个人/未推送的分支用 rebase 保持历史整洁;已推送的公共分支、团队协作的主干用 merge,避免覆盖他人的提交。
答案:常用 Conventional Commits 规范。类型包括:feat(新功能)、fix(修复 bug)、docs(文档变更)、refactor(重构)、perf(性能优化)、test(测试相关)、chore(杂务:构建/工具配置等)、style(代码格式不影响逻辑)、ci(持续集成配置)。格式:类型(可选范围): 描述,如 feat(login): 新增验证码登录。
答案:演进路径:① 远古多 script 标签(全局变量污染)→ ② IFE 封装(手动管依赖)→ ③ CommonJS(Node 同步 require)/ AMD(浏览器异步 define)→ ④ ES Modules(浏览器原生支持)。浏览器端采用 ESM 因为:import/export 声明是静态结构,可在编译阶段确定依赖关系,支持 Tree Shaking 优化;浏览器原生加载无需打包;type="module" 即可使用,标准统一后 Node 也原生支持。
require() 是同步调用,假设模块已在内存中返回;浏览器加载文件是 Ajax 异步请求(受同源策略、网络延迟等限制),根本无法同步阻塞等待。ESM 的 import 是声明式、静态的,浏览器可以在解析 HTML 阶段就建立依赖图,按需异步加载,这是它被浏览器采纳的根本原因。答案:打包流程:入口分析 → 依赖解析(递归 import/require 生成依赖图)→ 转换编译(Loader:TS→JS、CSS、图片)→ 合并打包(生成 chunk/bundle)→ 代码分割(动态 import 拆独立 chunk 按需加载)→ Tree Shaking(删除未使用代码)。Tree Shaking 三前提:① 必须使用 ESM(静态可分析);② 生产模式(开启压缩 + 副作用标记);③ 代码无副作用(或 package.json 声明 "sideEffects": false)。
require 是动态的(可以写 require(variableName)),打包器在编译阶段无法确定哪个模块被使用,因此无法安全删除,Tree Shaking 对 CJS 无效。sideEffects 字段告诉 Webpack「这个包的哪些文件没有副作用,可以放心删」。答案:开发时快的原因:不打包——利用浏览器原生 ESM,按需编译(浏览器请求才编译),启动无任何等待;依赖预构建用 esbuild(Go 编写,比 JS 快 10-100 倍)统一第三方库为 ESM。生产构建用 Rollup 的原因:esbuild 聚焦速度,Tree Shaking 精细度和代码分割算法不如 Rollup 成熟;Rollup 输出更优化的产物(更好的 Scope Hoisting、更精准的死代码删除)。开发用 esbuild 求快,生产用 Rollup 求优,双引擎组合拳。
答案:HMR(Hot Module Replacement)原理:文件变化时 → 编译该模块 → 通过 WebSocket 推送给浏览器 → 浏览器端 HMR Runtime 接收更新的模块 → 通过 ESM 的模块依赖关系找到「谁依赖了它」→ 精准替换变化模块,不刷新整页,保留代码状态。传统开发「改一行 → 整页 reload → 丢失状态」,HMR 实现了秒级反馈 + 状态保留。
答案:Loader 是文件级转换器——在模块被加载时对单个文件内容做转换(如 babel-loader 把 ES6 转 ES5,ts-loader 处理 TS,css-loader 解析 CSS 依赖)。Plugin 是构建级扩展点——通过钩子介入整个打包流程生命周期(如 HtmlWebpackPlugin 在打包结束后生成 HTML,MiniCssExtractPlugin 抽取 CSS 为独立文件)。Loader 执行顺序:从数组末尾向前(从右到左 / 从下到上),例如 ["style-loader", "css-loader"] 先执行 css-loader 再执行 style-loader。
答案:ESLint = 代码质量检查——找「错误与坏味道」(未使用变量、可能的 null 引用、catch 里什么都不写等),部分可 --fix。Prettier = 代码格式化——纯「风格统一」(缩进、引号、行宽、trailing comma),不判断对错,只做无歧义的排版。husky = Git 钩子管理——在 commit 等 Git 事件触发时执行脚本(如提交前自动跑 lint),约束开发者行为,不依赖「人肉记住规范」。
eslint-config-prettier 关掉 ESLint 中与 Prettier 冲突的风格规则,用 eslint-plugin-prettier 或直接分开跑都行。husky + lint-staged 只对 git暂存文件跑检查,避免每次提交全量扫描。CI 服务器还需全量跑一遍作为兜底——本地钩子可以被 git commit --no-verify 绕过。答案:interface 与 type:interface 用于声明对象结构,支持 extends 继承和声明合并(declare merging);type 是类型别名,支持联合类型、交叉类型、映射类型、条件类型等更丰富的玩法,但不能被 implements。实践中:定义对象/类契约用 interface,定义联合、工具类型用 type。泛型解决了类型复用的同时保留类型安全的问题——把类型参数化(像函数参数一样),同一逻辑处理多种类型无需写多个版本(如 identity<T>(x: T): T,ApiResponse<T>)。
type IsString<T> = T extends string ? true : false)和 infer 推理,是类型体操的基础。泛型约束 <T extends Lengthwise> 让泛型不是 any 而是有形状的类型,兼顾灵活性与安全。答案:CI(持续集成) = 每次合并代码时自动跑检查——类型检查、Lint、单测、构建,问题在合入主干前被拦住。CD(持续交付/部署) = 通过 CI 检查后自动将产物部署到测试/预发/生产环境。区别在于:CI 关注「代码能不能合进去」,CD 关注「代码能不能跑起来上线」。质量门禁三层:① 本地层(husky 钩子,最快最省资源);② CI 层(合并前的全量检查,兜底把关);③ 线上层(监控报警,最后的防线)。层层设防让坏代码越来越难上线。