6. 番外篇:打包和优化
我们的项目现在面临一个核心问题,那就用来处理JSX的babel是在浏览器中动态执行的。用户每次加载都会执行,完全重复的处理很低效。如果可以提前处理完毕,能大大加快页面的加载。这种代码完成后,用户使用前的中间处理,叫做打包。
想要打包,我们需要一个打包工具。为了简化,我们选择esbuild作为我们的打包工具。使用如下命令下载esbuild到本地。这里我们选择使用pnpm下载。你也可以选择任意你喜欢的方式下载。
pnpm i esbuild
pnpm下载的包默认放置在当前目录./node_modules/下。
我们之前使用react的库文件是直接在代码中应用,esbuild默认不支持这种方式(非ESM格式),所以我们需要把它们下载下来。还是使用pnpm:
pnpm i react react-dom
ES模块(ESM)是ES6标准引入的官方、标准化 JavaScript 模块系统,我们也使用这个方式更新模块的导入和导出。
另外,把App作为单独的组件,从React入口分离出来,更加符合模块化的标准。component.js文件也需要重命名,以保证和导出的模块名一致。
最后,因为我们使用的是JSX,js文件后缀也可以顺道修改为jsx,esbuild可以直接识别它
更新以后的文件是:
<!-- index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>你最喜欢的水果</title>
</head>
<body>
<div id="root"></div>
<script src="dist/bundle.js"></script>
</body>
</html>
// main.jsx
// ⚠️非推荐使用示范,仅仅是为了展示完全手工打包的过程
import ReactDOM from './node_modules/react-dom/client.js';
import App from './App.jsx';
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
// App.jsx
// ⚠️非推荐使用示范,仅仅是为了展示完全手工打包的过程
import { useState } from './node_modules/react/index.js';
import { FruitItem } from './FruitItem.jsx';
export default function App() {
const fruits = ["苹果", "香蕉", "芒果", "草莓"];
const [selectedFruit, setSelectedFruit] = useState("无");
const handleClick = fruit => {
setSelectedFruit(fruit)
};
return (
<div>
<h2>请选择你喜欢的水果:</h2>
<ul>
{fruits.map(fruit => <FruitItem key={fruit} fruit={fruit} onSelect={handleClick} />)}
</ul>
<p>你选择的水果:{selectedFruit}</p>
</div>
);
}
// FruitItem.jsx
export function FruitItem({ fruit, onSelect }) {
return (
<li
onClick={() => onSelect(fruit)}
style={{ cursor: 'pointer' }}
>
{fruit}
</li>
);
}
执行以下命令完成打包:
./node_modules/.bin/esbuild main.jsx --bundle --jsx=automatic --outfile=dist/bundle.js
这个命令行指定要打包的入口文件是main.jsx,具体操作的是打包(bundle),并且指定自动处理jsx导入,输出放到dist目录的bundle.js文件中,这样就不会和我们自己的文件混淆了 esbuild会从main.jsx开始,转换所有的相关文件,处理JSX语法,并且最终生成完全不依赖任何导入包的目标文件。这样浏览器就可以直接访问了。
注意看index.html,这个文件直接导入了打包之后的结果,这正是我们期望的。
你可以用浏览器查看打包之后的效果了,一如既往。
7. 番外:进入web开发的现代生态 —— 欢迎回到2025年
我们从最基本的从DOM API操作HTML开始,进入到多文件项目,并手工完成了打包。目前看起来还不错,但是现在的web开发早就不是这种刀耕火种的时候,信息时代欢迎你!
依赖管理
依赖管理是现代web项目的基石,一个中等规模的项目使用几百上千的包毫不奇怪。我们之前使用pnpm就是一个依赖管理工具。但是它不仅仅是包下载器,它更是有效项目管理的基础。
你可能注意到,安装一个新的工具的时候,当前目录自动创建了两个文件:
- package.json。这个包管理的核心文件,里面列出了你的项目依赖的所有包的名字和版本信息。这个文件名不能变化
- node_modules。当前项目的依赖包的文件。
安装的时候,pnpm会在互联网的包仓库中查找对应的包和版本。没有指定版本的时候,则自动选择最新的稳定版本,等价于@latest。有些人喜欢显式写上这个。
以下是几个常用的pnpm命令清单。你不需要记住,了解pnpm有哪些核心能力就可以了。
| 命令 | 作用及功能 |
|---|---|
| pnpm install | 安装当前项目 package.json 中定义的所有依赖。 |
| pnpm install <pkg> | 安装指定的依赖包。首先插入全局仓库,然后更新本地记录 |
| pnpm remove <pkg> | 删除本地依赖记录,全局仓库不变 |
| pnpm update | (当前项目)更新依赖到最新版本 |
| pnpm <script> | (当前项目)直接执行 package.json 中定义的脚本命令 |
| pnpm exec <cmd> | 在 node_modules/.bin 路径下执行命令。 |
| pnpm list | (当前项目)树的形式列出依赖包的清单。 |
| pnpm outdated | (当前项目)检查哪些依赖有新的版本可用。 |
| pnpm store path | 显示全局仓库的路径。 |
| pnpm store prune | 清理全局仓库中所有未被任何项目引用的包代码。 |
| pnpm config get registry | 列出 pnpm 访问的公共仓库地址 |
| pnpm config set registry <mirror-url> | 更新公共仓库地址,常用来加速访问 |
| pnpm create <initializer> | 从模板创建工程目录。模板有 vite,next-app等 |
pnpm是现在最流行包管理工具,性能卓越,优化磁盘使用。如果你听说过yarn或者npm,可以使用pnpm替代它们。
pnpm的核心优势是使用符号链接代替文件重复,在多个项目同时使用相同包的情况下,可以大大节约本地存储空间,也加速了文件访问。 同时,pnpm 严格执行只允许你使用你在 package.json 中声明的包。这确保了项目环境的整洁和稳定。
打包与构建
当依赖完备后,把开发项目打包成成品发布就成为关键。
为什么需要打包?
-
浏览器缺失模块生态的完整支持 前端依赖大量 npm 第三方库,这些库封装形式不同,需要统一处理为浏览器支持的格式(ESM)
-
性能优化 上千个js模块都使用页面直接加载会卡死浏览器。打包器会把所有的依赖打包成几个包,按需调用,并且只包含你用到的那部分代码,大大减小浏览器需要加载的代码的量
-
压缩和混淆 很多公司为了不泄露商业秘密,会对代码混淆,这样恶意的竞争对手就很难反向工程。压缩和混淆本身也会减少代码的体积
-
支持先进的语言特性 创新的技术,比如我们之前使用的JSX和后面将会提到的TypeScript,浏览器不直接支持。
-
CSS系统 现代CSS系统也需要处理才能使用。TailwindCSS需要扫描代码以生成css文件,新的css语法也需要适配以支持老旧的浏览器
-
静态资源处理 字体图片的处理,资源的自动随机化命名等,确保刷新浏览器缓存,这样开发环境中,你每次看到都是最新的版本
可以说,没有打包工具的支持,我们的工程能力立马回退到10年之前。
不过,可以看出,上面的理由有一些完全是因为现有浏览器能力的不足。如果浏览器努力进步,是不是就不需要打包,或者打包不再是必须的呢?
这正是现在技术发展的趋势。预计未来三到五年内,我们就可能看到:
- 浏览器原生模块生态(ESM 一统天下)。浏览器可以直接执行三方模块
- HTTP/3技术普及。加上CDN网络,小模块加载成本几乎为零
- 浏览器可以直接管理模块依赖,浏览器甚至直接按需在浏览器中安装包
那个时候,打包将不再是关键环节,默认情况下甚至无需打包。我们重新回到原生HTML的简单时代,但是我们拥有最强大的模块系统,表达能力,天下三分综归一统。
请记住,所有的工程努力都是为了让我们开发应用的时候更加容易而不是更加复杂。进化的过程中,我们会引入过程性的构造,可能是临时的,并不断通过大规模的实践来筛选。我们终将到达可以自由创新开发的彼岸。
记住来时的路,看清未来的标,你就不会迷失在工具的花园里。
Vite介绍
Vite是一个现代web开发构建工具,因其速度飞快而流行。其主要特色是:
- 几乎零配置就可以开始
- 对新技术有直接支持,开箱即用
- 开发阶段不打包(直接用浏览器原生 ESM),生产环境超强优化
- 完善脚手架,一键完成初始项目配置
以下代码可以建立一个基于React的项目
pnpm create vite
│
◇ Project name:
│ vite-project
│
◇ Select a framework:
│ React
│
◇ Select a variant:
│ TypeScript
│
◇ Use rolldown-vite (Experimental)?:
│ No
│
◇ Install with pnpm and start now?
│ No
│
◇ Scaffolding project in /home/bingfeng/vite-project...
│
└ Done. Now run:
cd vite-project
pnpm install
pnpm dev
其项目结构如下,每个文件的含义也同步标出
.
├── README.md <= 项目介绍
├── eslint.config.js <= ESLint是用于检查代码缺陷的工具,这是其配置,默认激活
├── index.html <= 项目入口
├── package.json <= 主配置文件
├── public <= 静态公共文件目录
│ └── vite.svg
├── src <= 代码目录
│ ├── App.css <= 组件CSS定义
│ ├── App.tsx <= 应用组件
│ ├── assets <= 项目静态文件
│ │ └── react.svg
│ ├── index.css <= 主CSS定义
│ └── main.tsx <= React入口
├── tsconfig.app.json \
├── tsconfig.json | 这都是TypeScript的配置,因为后端和前端的要求不同,分别配置
├── tsconfig.node.json /
└── vite.config.ts <= vite配置文件,因为我们使用了React,需要包含react插件
因为我们选择使用TypeScript(我们总是应该选择使用),并且使用了JSX语法,所以文件名是后缀.tsx。
按照上面的指令,你就可以直接安装依赖,并运行这个程序了。
不要关闭它,尝试把我们的最爱水果选择合并到这个项目中。如果哪里不对,浏览器页面直接报错;最终调整好,页面直接出来了,完全不需要重新编译运行。Vite提供了完善的热更新机制,监控你的每一点修改,并立即应用,极大提升了开发体验。
以下是需要做的修改,共参考
- index.html 和 src/main.tsx 都不变
- 新加 src/FruitItem.tsx 文件,内容和之前 FruitItem.jsx 完全一致,注意文件后缀的变更
- 使用原先的 App.jsx 文件替代新生成的 App.tsx。因为我们使用pnpm的打包机制,就不需要指定具体的路径了,需要把
import { useState } from './node_modules/react/index.js'简化为import { useState } from 'react。事实上,我们永远应该使用这种方法引用依赖包。同时把导入文件名从FruitItem.jsx更新为FruitItem.tsx
后面,我们都会用这种工具直接建立项目文件,而不是从头开始。你应该完全了解这些文件的作用了。
vite包含了好几个套件,使用非常广泛,但是不是唯一的;bun是另一个相对流行的类似工具,强调的是all-in-one:一个工具解决所有的问题。这些都是通用的工具。在使用某些具体的平台上,如使用NextJS作为开发框架,next(这个工具就叫这个名!)往往是更好的选择。
使用如下命令运行应用
pnpm dev
我们的全新的水果选择程序,背景是白的(也可能是黑的,如果你的系统主题是黑暗主题的话),几行文字居中靠左排列,看起来太朴素了。
后面,我们会使用CSS技巧,让它变得现代时尚起来
我们到web开发生态大花园的观光旅游结束了,下一篇让我们再回到React的世界。