前端开发工具链

前端开发工具链

前面几篇里我写的页面都是一个套路: 建个 .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), 别人把写好的代码打成”包”发布上去, 我用一条命令装下来。

代码块BASH · 13 行收起展开
# 在空目录里初始化一个项目, 会生成 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: 记录项目叫什么、依赖哪些包、有哪些可执行的脚本命令。

代码块JSON · 14 行收起展开
{
  "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 installpnpm add axios), 靠全局缓存加硬链接解决了”每个项目一份”的浪费, 更快更省磁盘。知道有这个东西就行, 学习阶段用 npm 完全没问题。

模块化: import 和 export

这一节是本篇最重要的部分, 后面 Vue 代码里满屏的 import ... from ... 都源自这里。

先回顾一下痛点。到目前为止我写的 JS 都在一个 <script> 里, 浏览器里的”裸 JS”根本没有 import 这个东西。
早年想拆文件, 只能在 HTML 里排一排 <script> 标签, 所有文件共享全局变量, 谁先加载谁后加载全靠标签顺序, 两个文件定义了同名变量还会互相覆盖。项目一大这就是灾难。

ES6 给 JS 带来了官方的模块标准, 叫 ES Modules: 每个文件是一个独立的模块, 内部的变量默认外面看不见, 想给别人用就 export, 想用别人的就 import。

导出:

代码块JAVASCRIPT · 19 行收起展开
// 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;
}

导入:

代码块JAVASCRIPT · 15 行收起展开
// 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":

代码块HTML · 1 行收起展开
<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 项目:

代码块BASH · 1 行收起展开
npm create vite@latest my-app -- --template vanilla

这条命令值得逐词拆开:

  • npm create vite 其实是 npm exec create-vite 的缩写: 下载并运行一个叫 create-vite 的脚手架包, 由它来生成项目骨架
  • @latest 指定用这个脚手架的最新版本
  • 单独的 -- 是个分隔符, 它后面的参数会越过 npm, 原样传给脚手架
  • --template vanilla 告诉脚手架用原生 JS 模板 (vanilla 是”香草味”即”原味”的意思, 前端圈用它指不带任何框架的原生 JS)

生成的项目长这样:

代码块PLAINTEXT · 7 行收起展开
my-app/
├── index.html          # 入口 HTML
├── package.json        # 依赖和脚本
├── src/
│   ├── main.js         # JS 入口
│   └── style.css
└── public/             # 原样复制的静态资源

然后三连:

代码块BASH · 3 行收起展开
cd my-app
npm install     # 装依赖 (此时才会出现 node_modules)
npm run dev     # 启动开发服务器, dev 定义在 package.json 的 scripts 里

需要改 Vite 行为时, 在项目根目录放一个 vite.config.js:

代码块JAVASCRIPT · 9 行收起展开
// 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。

代码块SCSS · 29 行收起展开
// 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 项目里用它只需要装一个开发依赖:

代码块BASH · 1 行收起展开
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。

代码块TYPESCRIPT · 23 行收起展开
// 变量类型标注: 冒号后面写类型
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 只管排版: 缩进、引号、换行, 保存时自动把代码刷成统一格式, 团队里从此没有格式之争。

代码块BASH · 4 行收起展开
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 了, 这节纯粹是留一份最小命令速查, 给完全没接触过版本控制的读者兜底, 会的直接跳过。

代码块BASH · 9 行收起展开
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 随时能重建, 后者是构建产物, 都没有进仓库的理由。

代码块PLAINTEXT · 6 行收起展开
# .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 搭一个天气查询

把这篇的东西串起来跑一遍:

代码块BASH · 4 行收起展开
npm create vite@latest weather-app -- --template vanilla
cd weather-app
npm install
npm install axios

src/main.js 整个替换成:

代码块JAVASCRIPT · 31 行收起展开
// 在 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 标签之前):

代码块HTML · 3 行收起展开
<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 快速上手开始进入主线。

延伸阅读