前端开发工具链
前端开发工具链
前面几篇里我写的页面都是一个套路: 建个 .html 文件, 双击, 浏览器打开就能跑。
可是随便打开一个真实的前端项目, 里面全是 package.json、node_modules、vite.config.js 这种东西, 还得先敲一堆命令才能启动。
这篇笔记就是解决这个疑问的: 这些工具各自是为了什么痛点才出现的, 装上它们之后开发方式会变成什么样。
读之前需要把 HTML、CSS、JavaScript 三篇过完, 尤其是 JavaScript 那篇的 async/await 和 fetch。
双击能跑, 为什么还要一堆工具
写小 demo 的时候确实什么都不用装。但项目一大, 麻烦就来了:
- 想用别人写好的库 (日期处理、发请求之类), 只能去官网手动下载 js 文件再用
<script>引入, 升级版本也全靠手动 - 代码全挤在一个文件里想拆开, 但多个
<script>标签共享全局变量, 加载顺序错一个就崩 - 想用新的 JS 语法, 又怕用户的旧浏览器不认识
- 多人协作时每个人的代码风格五花八门, 改一行 diff 出来一屏
这些事后端世界其实早就解决过一遍: Java 管依赖有 Maven, 查代码风格有 CheckStyle, 打包发布有各种插件。前端工具链干的是同一类事, 只是名字换了一批。下面按痛点一个个过。
npm: 前端世界的 Maven
先交代一个前提: npm 是随 Node.js 一起装的。Node.js 是一个能让 JavaScript 离开浏览器、直接在电脑上运行的运行时, 之前 JS 只能活在网页里, 有了 Node 它就能像 Java 程序一样在命令行跑, 所以各种前端工具本身都是用 JS 写的、靠 Node 运行的。
Node 的安装和版本管理我单独记在 Node、NVM 与 npm 前置知识, 这里只讲概念。
npm 就是前端的 Maven: 有一个中央仓库 (npm registry), 别人把写好的代码打成”包”发布上去, 我用一条命令装下来。
代码块收起展开
# 在空目录里初始化一个项目, 会生成 package.json
npm init -y # -y 表示所有问题都用默认值, 不用一路回车
# 安装包
npm install axios # 装 axios 这个包, 记入 dependencies
npm install -D vite # -D 表示记入 devDependencies (只在开发时用的工具)
npm install # 什么都不带: 按 package.json 把所有依赖装齐
# 卸载
npm uninstall axios
# 运行 package.json 里定义的脚本 (下面马上讲)
npm run dev装好之后项目里会多出几样东西, 对着 Maven 理解最快。
package.json 就是 pom.xml: 记录项目叫什么、依赖哪些包、有哪些可执行的脚本命令。
代码块收起展开
{
"name": "my-project",
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"dependencies": {
"axios": "^1.6.0"
},
"devDependencies": {
"vite": "^5.0.0"
}
}dependencies 是程序运行本身需要的库, devDependencies 是只在开发和构建阶段用的工具 (构建器、代码检查器), 大致对应 Maven 里的 scope。
scripts 是自定义命令的别名表: 我敲 npm run dev, npm 就去 scripts 里找到 "dev": "vite", 实际执行的是 vite 命令。
所以以后看到任何 npm run xxx 看不懂, 答案都在 package.json 的 scripts 里。
node_modules 是装在项目里的本地仓库: 所有依赖的实际代码都下载到这个目录。
和 Maven 不一样的地方在于, Maven 的依赖统一放在用户目录的 .m2 里全局共享, npm 默认每个项目自己装一份, 所以 node_modules 动辄几百 MB, 也因此绝不提交 Git, 需要时 npm install 随时能重建。
package-lock.json 是版本锁: package.json 里的 ^1.6.0 只是个版本范围, 每次安装解析到的具体版本可能不同; lock 文件把这次实际装到的每个精确版本都记下来, 队友拿着它 npm install, 装出来的依赖树就和我完全一致。
它和 node_modules 待遇相反, 要提交进 Git。
另外还有个 pnpm, 命令和 npm 基本一样 (pnpm install、pnpm add axios), 靠全局缓存加硬链接解决了”每个项目一份”的浪费, 更快更省磁盘。知道有这个东西就行, 学习阶段用 npm 完全没问题。
模块化: import 和 export
这一节是本篇最重要的部分, 后面 Vue 代码里满屏的 import ... from ... 都源自这里。
先回顾一下痛点。到目前为止我写的 JS 都在一个 <script> 里, 浏览器里的”裸 JS”根本没有 import 这个东西。
早年想拆文件, 只能在 HTML 里排一排 <script> 标签, 所有文件共享全局变量, 谁先加载谁后加载全靠标签顺序, 两个文件定义了同名变量还会互相覆盖。项目一大这就是灾难。
ES6 给 JS 带来了官方的模块标准, 叫 ES Modules: 每个文件是一个独立的模块, 内部的变量默认外面看不见, 想给别人用就 export, 想用别人的就 import。
导出:
代码块收起展开
// utils.js
// 命名导出: 导出时带着名字, 一个文件可以导出很多个
export function add(a, b) {
return a + b;
}
export const PI = 3.14159;
// 也可以先定义, 最后集中导出
function subtract(a, b) {
return a - b;
}
export { subtract };
// 默认导出: 一个文件只能有一个, 相当于这个模块的"主打产品"
export default function multiply(a, b) {
return a * b;
}导入:
代码块收起展开
// main.js
// 导入命名导出: 花括号里的名字必须和导出时一致
import { add, PI } from './utils.js';
console.log(add(1, 2)); // 3
// 导入默认导出: 不用花括号, 名字随便起
import multiply from './utils.js';
// 重命名, 避免和本地名字冲突
import { add as sum } from './utils.js';
// 全部打包成一个对象导入
import * as utils from './utils.js';
utils.add(1, 2);from 后面那个字符串有讲究, 这是读别人代码时最容易迷糊的点:
- 以
./或../开头的是相对路径, 指向自己项目里的文件, 比如'./utils.js'就是旁边那个文件 - 不带路径的裸名字, 比如
import axios from 'axios', 意思是去 node_modules 里找叫 axios 的包。这个包哪来的? 就是之前npm install axios装进去的
所以以后看到任何 import, 心里过一遍: 这个名字从哪个来源来, 这个来源是本地文件还是 npm 装的包, 导出来的是函数还是对象。想清楚这三件事, import 就不神秘了。
和 Java 的 import 长得很像, 但含义不同: Java 的 import 只是让我少写全限定名, 类路径上的类本来就都在; JS 的 import 是真的去加载那个文件或包, 不 import 它就不存在。
最后, 想在 HTML 里用模块化的 JS, script 标签要加 type="module":
代码块收起展开
<script type="module" src="main.js"></script>这里有个坑: 加了 type=“module” 之后再双击打开 HTML 会报错, 因为浏览器出于安全限制, 不允许 file:// 协议下的页面去加载模块文件, 必须通过 HTTP 服务器访问。也就是说, 一旦开始用模块化, “双击就能跑”的时代就结束了, 需要一个本地开发服务器。这正好引出下一个主角。
Vite: 开发服务器加打包器
Vite (法语”快”, 读作 veet) 一个人干两件事:
第一件是开发服务器。npm run dev 之后它在本地起一个 HTTP 服务 (默认 localhost:5173), 我在浏览器里访问这个地址开发, 改一行代码浏览器立刻更新, 连手动刷新都不用, 这叫热更新 (HMR)。
体验很像 Spring Boot 的内嵌 Tomcat 加热部署, 但比 devtools 的重启快得多, 因为 Vite 开发时根本不打包, 浏览器要哪个文件它就现场转换哪个文件。
第二件是打包。npm run build 把整个项目压缩、合并、优化成一堆纯静态文件, 交给服务器部署上线。
创建一个 Vite 项目:
代码块收起展开
npm create vite@latest my-app -- --template vanilla这条命令值得逐词拆开:
npm create vite其实是npm exec create-vite的缩写: 下载并运行一个叫 create-vite 的脚手架包, 由它来生成项目骨架@latest指定用这个脚手架的最新版本- 单独的
--是个分隔符, 它后面的参数会越过 npm, 原样传给脚手架 --template vanilla告诉脚手架用原生 JS 模板 (vanilla 是”香草味”即”原味”的意思, 前端圈用它指不带任何框架的原生 JS)
生成的项目长这样:
代码块收起展开
my-app/
├── index.html # 入口 HTML
├── package.json # 依赖和脚本
├── src/
│ ├── main.js # JS 入口
│ └── style.css
└── public/ # 原样复制的静态资源然后三连:
代码块收起展开
cd my-app
npm install # 装依赖 (此时才会出现 node_modules)
npm run dev # 启动开发服务器, dev 定义在 package.json 的 scripts 里需要改 Vite 行为时, 在项目根目录放一个 vite.config.js:
代码块收起展开
// defineConfig 是 vite 包导出的辅助函数, 主要作用是让编辑器有提示
import { defineConfig } from 'vite';
export default defineConfig({
server: {
port: 3000, // 开发服务器端口
open: true // 启动时自动打开浏览器
}
});顺带记一个兼容性的事: Vite 打包默认面向现代浏览器。如果要兼容很老的浏览器, 有 @vitejs/plugin-legacy 这类插件把新语法编译回旧语法 (干这个活的编译器叫 Babel); CSS 的浏览器前缀 (-webkit- 开头那种, 老浏览器认这个) 则由 Vite 内置的 Autoprefixer 自动补。现阶段混个眼熟就够, 不用配置。
Sass: 让 CSS 少写点重复
原生 CSS 的痛点: 同一个主题色在几十个地方复制粘贴, 想改色全文替换; 选择器层级每次都要从头写全。Sass 是 CSS 的增强方言, 加了变量、嵌套、mixin 这些能力, 写的时候用 .scss 文件, 构建时编译回普通 CSS。
代码块收起展开
// styles.scss
// 变量: 改一处, 全局生效
$primary-color: #667eea;
.container {
padding: 20px;
// 嵌套: 直接写在里面, 等于 .container .header
.header {
background: $primary-color;
// & 代表父选择器, 这里等于 .container .header:hover
&:hover {
opacity: 0.8;
}
}
}
// mixin: 一段可复用的样式片段, 类似样式界的工具方法
@mixin flex-center {
display: flex;
justify-content: center;
align-items: center;
}
.box {
@include flex-center; // 把上面那三行"粘贴"进来
}在 Vite 项目里用它只需要装一个开发依赖:
代码块收起展开
npm install -D sass然后在 JS 入口里 import './styles.scss', Vite 看到 .scss 后缀会自动编译。现在原生 CSS 也支持变量了, 但嵌套和 mixin 还是 Sass 更顺手, 很多老项目里也全是它。
TypeScript: 给 JS 加上编译期检查
JS 是动态类型语言, user.nmae 这种拼写错误要等代码真正跑到那一行才会暴露。
写惯 Java 的人对解法太熟了: 静态类型, 让错误在编译期就报出来。TypeScript 之于 JavaScript, 就像静态类型之于动态语言: TS 是 JS 的超集, 在 JS 语法上多了一层类型标注, 编译成 JS 之后类型信息全部擦掉, 浏览器最终跑的还是普通 JS。
代码块收起展开
// 变量类型标注: 冒号后面写类型
let username: string = '张三';
let age: number = 25;
let list: number[] = [1, 2, 3];
// interface 描述一个对象该长什么样
// 和 Java 的 interface 神似, 但只描述形状, 没有方法实现
interface User {
name: string;
age: number;
email?: string; // 问号表示这个属性可有可无
}
let user: User = { name: '张三', age: 25 };
// 函数: 参数和返回值都能标类型
function greet(name: string): string {
return `你好, ${name}`;
}
// 联合类型: 这个值可以是字符串或数字, Java 里没有的写法
type ID = string | number;
let userId: ID = '123';Vue 官方脚手架默认就给 TS 模板, 因为项目一大, 类型提示和重构安全实在太省心。现阶段看不懂 : string 这类标注完全可以先无视, 把它们当注释, 剩下的就是普通 JS; 到 Vue 深入篇会正式用起来。
ESLint 和 Prettier: 一个查错, 一个排版
这俩经常一起出现, 分工其实很清楚。ESLint 做静态检查, 抓的是潜在 bug 和坏习惯: 定义了没用的变量、用了 而没用 =、条件里手滑写了赋值。Prettier 只管排版: 缩进、引号、换行, 保存时自动把代码刷成统一格式, 团队里从此没有格式之争。
代码块收起展开
npm install -D eslint prettier
npx eslint src/ # 检查 src 目录
npx eslint src/ --fix # 能自动修的顺手修掉
npx prettier --write src/ # 全部格式化这里第一次出现 npx: 它是 npm 附带的命令, 用来直接运行 node_modules 里装的命令行工具。eslint 装在项目里而没装到系统全局, 直接敲 eslint 系统找不到, 前面加 npx 就行。
实际开发中很少手敲这些命令, 都是给 VS Code 装上 ESLint 和 Prettier 插件, 写代码时实时标波浪线、保存时自动格式化。Vue 的脚手架也会问要装哪些, 选上即可。
Git 速查
我平时已经在用 Git 了, 这节纯粹是留一份最小命令速查, 给完全没接触过版本控制的读者兜底, 会的直接跳过。
代码块收起展开
git init # 把当前目录变成 Git 仓库
git status # 看哪些文件变了
git add . # 把改动放进暂存区
git commit -m "说明" # 提交
git log --oneline # 精简查看历史
git checkout -b feature # 建分支并切换过去
git merge feature # 合并分支
git clone <url> # 克隆远程仓库
git push / git pull # 推送 / 拉取前端项目有一点必须记住: .gitignore 里一定要有 node_modules/ 和 dist/。前者几百 MB 而且 npm install 随时能重建, 后者是构建产物, 都没有进仓库的理由。
代码块收起展开
# .gitignore
node_modules/
dist/
# .env 是环境变量文件, 常放 API 密钥这类不能公开的配置, 更不能进仓库
.env
*.log发请求: 从 fetch 到 axios
浏览器自带的 fetch 在 JavaScript 篇学过了。
真实项目里更常见的是 axios, 一个 npm 上的第三方请求库, 相比 fetch 的实际好处: 响应自动解析成 JSON 不用多一步 .json()、HTTP 错误状态码会直接抛异常、能配置统一的 baseURL 和超时、还支持拦截器 (在每个请求发出前统一塞 token, 在每个响应回来后统一处理 401 跳登录, 作用很像 Servlet 的 Filter)。
npm install axios// axios 包默认导出一个对象, 方法都挂在它上面
import axios from 'axios';
async function getUsers() {
try {
const response = await axios.get('https://api.example.com/users');
console.log(response.data); // 数据在 response.data 里, 已经是 JS 对象
} catch (error) {
console.error('请求失败', error.message);
}
}真实项目里怎么封装 axios (拦截器、统一错误提示、和后端约定返回格式) 是个不小的话题, 我记在 Vue/05 工程化与请求层里, 这里知道它是干嘛的就够。
动手: 用 Vite 搭一个天气查询
把这篇的东西串起来跑一遍:
代码块收起展开
npm create vite@latest weather-app -- --template vanilla
cd weather-app
npm install
npm install axiossrc/main.js 整个替换成:
代码块收起展开
// 在 JS 里 import 一个 CSS 文件? 这是 Vite 提供的能力, 浏览器本身不认
// Vite 会把它转换成正常的样式加载, 好处是 JS 和它依赖的样式写在一起
import './style.css';
import axios from 'axios'; // 上一步 npm install 装进 node_modules 的
const cityInput = document.querySelector('#cityInput');
const searchBtn = document.querySelector('#searchBtn');
const weatherDiv = document.querySelector('#weather');
searchBtn.addEventListener('click', async () => {
const city = cityInput.value.trim();
if (!city) return;
try {
weatherDiv.innerHTML = '<p>加载中...</p>';
// 示例 API, YOUR_API_KEY 要去 openweathermap 注册后替换
const response = await axios.get(
`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=YOUR_API_KEY&units=metric&lang=zh_cn`
);
const data = response.data;
weatherDiv.innerHTML = `
<h2>${data.name}</h2>
<p>温度: ${data.main.temp}°C</p>
<p>天气: ${data.weather[0].description}</p>
`;
} catch (error) {
weatherDiv.innerHTML = '<p>查询失败, 请检查城市名称</p>';
}
});index.html 的 body 里对应放上三个元素 (加在原有的 script 标签之前):
代码块收起展开
<input id="cityInput" placeholder="输入城市, 如 Guangzhou" />
<button id="searchBtn">查询</button>
<div id="weather"></div>然后 npm run dev。和之前双击 HTML 的开发方式对比一下: 依赖是 npm 装的, 代码是模块化拆分的, 跑在开发服务器上还有热更新, 这就是工具链带来的开发方式。
下一步
到这里, 学习路线是: 基础三件套, 然后本篇的工具链, 接着就是 Vue。进 Vue 之前建议先看两篇: Node、NVM 与 npm 前置知识从装环境的视角把 Node 和 npm 的操作细节过一遍; 前端文件架构讲一个 Vue 项目里每个文件是干嘛的。
然后从 Vue3 快速上手开始进入主线。