react课程14-使用Vite构建高级ReactProjiect
Last updated on September 11, 2024 pm
Jonas老师真的很有鼓舞力😂
之前也使用过Vite来构建项目,但是没仔细分析它和Creact-react-app的区别……本节课居然回到了第一次构建的pizza项目,但是应该会更加modern。
一、Vite 和 create-react-app (CRA)的区别
1. 启动速度
- Vite:Vite 使用现代的浏览器原生 ES 模块(ESM),按需加载项目中的模块。启动速度非常快,尤其在大型项目中优势明显,因为它只会加载应用程序中实际需要的部分。
- CRA:CRA 使用 Webpack 进行打包,启动时需要对整个项目进行预打包。这种方式在项目规模较大时,启动速度较慢,尤其是开发环境中的初次构建。
2. 构建速度
- Vite:Vite 使用
esbuild来进行构建,它是用 Go 语言编写的,构建速度极快。Vite 的热模块替换(HMR)也非常快,几乎是即时更新,提升了开发体验。 - CRA:CRA 依赖 Webpack 进行构建,构建速度相对较慢,特别是在项目规模增大时,打包时间会显著增加。
3. 开发环境下的模块处理
- Vite:Vite 使用浏览器支持的 ES 模块加载,因此在开发模式下,不需要整体打包,可以按需加载模块,这使得项目在开发时能够更快速响应。
- CRA:CRA 需要先对整个项目进行打包,再通过 Webpack Dev Server 提供开发环境。这种方式在开发时需要处理大量文件,性能较差。
4. 依赖预构建
- Vite:在开发模式下,Vite 使用
esbuild预构建依赖,使得开发服务器加载依赖时速度更快,并且能优化依赖模块的重复打包问题。 - CRA:CRA 没有类似的依赖预构建机制,所有依赖在开发环境中会被打包成一个整体,导致开发时的响应较慢。
5. 热模块替换(HMR)
- Vite:Vite 的 HMR 几乎是即时的,因为它只重新加载修改过的模块,而无需重新加载整个应用。这使得开发体验更加流畅。
- CRA:CRA 也支持 HMR,但由于它基于 Webpack,速度相对较慢,尤其是项目体积较大时,更新等待时间较长。
6. 配置和扩展性
- Vite:Vite 提供了轻量化的默认配置,但也允许通过插件进行高度定制。Vite 采用的插件机制类似于 Rollup,支持生态系统中的各种插件,且配置文件相对简洁。
- CRA:CRA 默认的配置相对封闭,开发者需要使用
eject命令来暴露底层 Webpack 配置,这个过程是不可逆的,且配置复杂。对于初学者来说,CRA 更简单易用,但在需要自定义配置时不够灵活。
7. 生态系统支持
- Vite:Vite 的生态系统正在快速发展,特别是在 Vue 和 React 项目中,Vite 正成为主流的选择。Vite 的插件生态也不断丰富,适配多种现代前端框架。
- CRA:CRA 基于 Webpack,Webpack 是非常成熟的打包工具,拥有丰富的插件和工具支持。不过 CRA 的封闭性限制了对这些工具的灵活使用。
8. 生产构建
- Vite:在生产模式下,Vite 使用 Rollup 进行打包,构建出来的代码体积小,性能优化效果好。Rollup 擅长处理 ES 模块,并且支持树摇(tree-shaking)优化未使用的代码。
- CRA:CRA 使用 Webpack 进行生产构建,虽然 Webpack 也支持树摇和代码分割,但与 Vite 的 Rollup 相比,Webpack 的配置相对复杂,打包时间较长。
9. 插件和框架集成
- Vite:Vite 内置支持多种框架(如 Vue、React、Svelte 等),通过插件机制可以快速集成不同的框架和工具。Vite 的插件生态更灵活,使用 Rollup 插件也非常方便。
- CRA:CRA 是专门为 React 项目设计的,虽然可以手动配置支持其他工具,但其灵活性和扩展性不如 Vite。
10. 社区和发展趋势
- Vite:Vite 是一款相对较新的工具,但它的增长速度非常快,已经成为现代前端开发的趋势之一,特别是在追求高性能的开发环境和生产环境中受到越来越多的欢迎。
- CRA:CRA 是 React 社区中最早的官方项目脚手架之一,适合初学者和小型项目,但在大型项目中的性能逐渐显现出局限性。
二、项目构建起步
终端: npm create vite@4
VScode:npm i eslint vite-plugin-eslint eslint-config-react-app --save-dev
vite.config.js文件:(要修改的部分)
1 | |
.eslintrc.cjs文件:(默认配置加上react-app规则)(我把两个默认规则集都注释了,不然总报错🫠)
1 | |
⁉️在视频里构建完Vite项目后需要自己手动创建.eslintrc.json文件,和本文件有何区别?
.eslintrc.cjs:
- 这是一个使用 CommonJS 模块格式的 ESLint 配置文件,文件内容是以
module.exports = {}的方式导出配置对象,允许使用动态的 JavaScript 语法。 - 支持完整的 JavaScript 语法。你可以在其中使用变量、条件语句、函数等动态逻辑。这种格式适合需要根据环境或条件动态生成 ESLint 配置的场景。
- 基于 CommonJS 模块系统。导出配置使用
module.exports = {},适用于 Node.js 环境中。
- 这是一个使用 CommonJS 模块格式的 ESLint 配置文件,文件内容是以
.eslintrc.json:
- 这是一个标准的 JSON 格式的配置文件,不支持 JavaScript 语法,仅能定义静态的键值对配置。
- 只能使用 JSON 语法,不支持动态配置。如果你需要使用动态逻辑,必须转为
.eslintrc.cjs或.eslintrc.js格式。 - 没有模块系统,纯粹是静态配置数据,使用 JSON 格式,适合简单的配置需求。
三、Application Planing
(1)Thinking in React
在 Redux 中,features是一个概念化的术语,用来描述应用中的特定功能模块或子功能。每个 feature通常包含自己的状态和处理它的逻辑。
✴️对小型程序而言:
- 将UI分解成多个组件
- 建立静态网页
- 开始考虑状态管理和数据流
✴️对大型真实程序:
收集应用程序的需求(requirements)和所需的特性(features)(Redux)
将应用程序分成多个界面
- 考虑整体和页面级别的UI(用户界面)
- 🔻将UI分解成多个组件
- 🔻建立静态网页
将应用程序分为不同的特性类别
- 🔻开始考虑状态管理和数据流
决定我们想要用哪些库(technology decisions)
(2)项目分析
1、项目需求
- 用户可以订购一个或多个pizza
- 不需要账户也不需要登陆,只需要输入名字就可以开始使用
- 可以改变pizza 菜单,需要一个虚假API
- 用户需要购物车来放置想要订购的pizza
- 需要用户的名字、电话号码、地址(来联系)
- 最好可以获得用户的GPS
- 用户可以将自己的订单标记为“优先”(需要多付20%的钱),并且在订单发送后也可以标记
- 订单是通过发送带有所有订单数据的POST请求来完成的(含有用户信息和挑选的pizza信息)
- 只有订单送达才会处理付款,在应用中不需要处理支付
- 每个订单都会得到一个专用ID,会显示在界面上,用户可以通过ID查看订单状态
2、分析特性和界面

3、考虑状态和数据流、决定要使用的库

四、建立项目结构
ui:可重用的组件(按钮、输入等)
services:可重用代码,用于和API交互
utils:helper 函数,可重用,不产生任何side effect
features:用来描述应用中的特定功能模块或子功能,每个 feature 通常包含自己的状态和处理它的逻辑。在实际开发中,features 通常会对应到 Redux 的 slice 文件,即每个 feature 可能会有自己的 slice 来处理该功能模块的状态和逻辑。

五、执行Routes的新方法
⭕npm i react-router-dom@6
react router文档链接:https://reactrouter.com/en/main/routers/create-browser-router
App.js示例版:(见下)
之前的方式:BrowserRouter 和 Routes(React Router v6)
这是 React Router v6 的典型使用方式。主要特点如下:
BrowserRouter:- 包裹整个应用,提供路由功能。
- 内部使用 HTML5 的 History API,监听 URL 的变化。
Routes:- 取代了 React Router v5 中的
Switch,是 v6 中用于渲染路由的主要组件。 Route是嵌套在Routes组件中的,每一个Route定义了一个具体路径及其对应的组件。
- 取代了 React Router v5 中的
- 嵌套路由:
- 支持路由的嵌套,方便组织多层路由结构。例如,
path="app"下的子路由 (cities,countries,form) 会渲染在AppLayout内。 - 子路由不需要写完整路径,可以相对父路由来定义路径。
- 支持路由的嵌套,方便组织多层路由结构。例如,
Suspense:- 使用
Suspense进行懒加载时,可以通过fallback属性定义加载时的占位组件,优化用户体验。
- 使用
ProtectedRoute:- 在
path="app"中,使用了ProtectedRoute来实现路由的保护。只有满足特定条件时(比如用户已登录),路由才会渲染对应组件。
- 在
- 404页面处理:
- 通过
path="*"来捕获所有未匹配的路由,显示PageNotFound组件。
- 通过
现在的方式:createBrowserRouter 和 RouterProvider(React Router v6.4+)
React Router v6.4 引入了新的 Data API,这意味着你可以使用 createBrowserRouter 和 RouterProvider 来定义和提供路由。它的特点包括:
createBrowserRouter:- 新的路由创建方式,提供了更直观的配置方式,将路由声明与数据获取逻辑(如
loader、action)结合起来,适合定义较为复杂的应用路由。 - 更加数据驱动,可以在路由定义时设置数据加载逻辑、错误处理等。
- 新的路由创建方式,提供了更直观的配置方式,将路由声明与数据获取逻辑(如
RouterProvider:- 提供了一个
router对象,用于将创建好的路由配置传递给应用程序。这种方式相比BrowserRouter更灵活,可以在创建路由时进行更多的配置操作。
- 提供了一个
- 不再需要
Routes和Route:- 使用
createBrowserRouter后,不需要再单独使用Routes和Route组件,路由结构是直接在router对象内配置的。 - 路由配置的结构较为简洁,适合数据预加载和复杂路由需求。
- 使用
主要区别:
- 路由声明方式:
- 第一种方式使用
Routes和Route组件来声明路由,是 React Router v6 的常见方式。 - 第二种方式使用
createBrowserRouter来声明路由,更加简洁,适用于 v6.4+ 版本,尤其适合数据驱动的路由需求。
- 第一种方式使用
- 数据加载与处理:
- 在 React Router v6.4+ 中,
createBrowserRouter更加支持与数据加载和动作处理的结合,比如可以直接在路由定义时加入loader和action。这可以让你在定义路由的同时处理数据加载逻辑。 - 而在传统的
BrowserRouter方式中,数据加载通常是在组件内部通过useEffect或其他钩子完成的。
- 在 React Router v6.4+ 中,
- 组件组织方式:
- 在
createBrowserRouter的方式下,路由定义集中在一个地方,比较适合大型应用。 BrowserRouter和Routes的方式适合小型和中型应用,使用起来更加直观。
- 在
- 嵌套路由的管理:
- 两种方式都支持嵌套路由,但第一种方式通过
Route的嵌套方式定义嵌套路由,而第二种方式是在createBrowserRouter的配置对象中通过层次化结构来实现。
- 两种方式都支持嵌套路由,但第一种方式通过
总结
BrowserRouter 和 Routes 的方式:更加直接、简单,适用于中小型应用。
createBrowserRouter 和 RouterProvider 的方式:适合更复杂的应用,尤其是在需要数据预加载和处理时,这种方式更加高效。
六、构建layout
(1)什么是layout?
layout(布局)界面通常是指一个应用程序的整体框架或页面结构。它负责将不同的 UI 组件(如导航栏、侧边栏、页脚、内容区等)组织在一起,并确保它们在页面上以一致的方式显示。布局界面主要用于定义页面的骨架和各部分的排列方式,用户可以在这个基础上进行内容的填充和交互设计。
(2)layout的作用
- 保持一致性:每个页面在布局上的一致性有助于用户熟悉界面,减少学习成本。
- 导航性:通过固定的导航栏或侧边栏,用户可以快速找到所需的功能或页面。
- 复用性:通过将布局界面抽象出来,可以在多个页面间复用同一个结构,而只改变主内容区域的内容。
(3)嵌套路由
在 React Router 中,嵌套路由允许你在一个路由组件内部展示子路由的内容。这种方式非常适合实现像布局组件(Layout)这种页面结构,即主界面有一些固定不变的部分(比如导航栏、页眉),而其余部分根据不同路由切换。
<Outlet> 组件是 React Router 用于渲染嵌套路由的关键部分。它起到“占位符”的作用,表示在这个位置会渲染匹配当前路由的子路由内容。
(4)代码示例:
本次项目所有界面使用同种layout布局界面,因此较为简单。
AppLayout.jsx
1 | |
App.jsx
1 | |
七、Loaders
Loaders 通常指的是在数据加载过程中显示的组件或功能,来向用户展示数据正在获取的状态。它有助于提高用户体验,避免在数据还未加载完毕前呈现空白页面。
(1)Loader的介绍和初步使用
本次项目不同于之前使用useEffect获取数据加useState管理isLoading状态来显示加载界面的方法,由于运用了React Router v6.4,它自带了 Loader 功能用于在路由层面进行数据加载。可以结合路由直接展示加载状态。
使用方法如下:(直接给出例子)
Menu.js
1 | |
- 在需要加载数据的文件中使用loader函数获取并返回数据(export导出,需是异步函数)
- 在该界面的Router中加入loader属性,并设置为导入的loader函数(可重命名)
- 在需要加载数据的组件函数中使用useLoaderData()函数获得数据
useLoaderData 是 React Router v6 提供的一个钩子,用于在路由加载时获取预加载的数据。它通过关联的 loader 函数 提供数据,确保在组件渲染时已经获得了所需的数据,避免了组件一开始就加载空白页面再去异步获取数据的情况。
loader 是一个异步函数,专门用于在导航到该页面之前从服务端或其他数据源获取数据。这个 loader 函数会在渲染 Menu 组件之前执行,确保数据已经加载完毕。
(2)navigation
useNavigation 是 React Router 提供的钩子,用于跟踪应用中的导航状态。它可以用来查看当前的导航状态(比如加载中、等待中等),并显示相应的界面状态。(详见React Router专题部分)
因此通过:
const navigation = useNavigation();
// console.log(navigation);
const isLoading = navigation.state === "loading";
得到加载状态后即可条件显示Loader组件
(3)全局Loader
在代码中,我们把Loader组件放在了AppLayout组件中,由于它位于父路由,因此所有使用useLoaderData的子路由组件在加载的时候都会自动获取Loader组件。
八、错误处理
在Router的父路由中加上errorElement属性,指向显示错误信息的组件。
const error = useRouteError(); 得到错误信息
console.log(error);

error.data或者error.message就是错误的具体信息(可以显示出获取信息失败的错误)
因此也可以在需要获取数据的子路由中加上这个错误属性,使得错误界面也拥有layout的布局。
九、提交表单
(1)actions在提交表单中的用法
首先需要将action函数链接到该组件的Router中
1 | |
另外,我们想要在提交表单数据的同时隐形提交cart中的数据,就在
组件中的任意位置放上这一行代码:1 | |
最后是action函数:(action 函数是在表单提交时执行的,它会处理表单提交的数据)
1 | |
重构前和重构后的数据如图(本来priority在不选时不会出现,现在由true和false描述)


(2)隐藏字段解析
1.<input type="hidden">
- 隐藏字段:
<input type="hidden">是一种不会显示在用户界面上的输入字段。它用于在表单中传递数据,但不让用户直接看到或修改这些数据。 - 作用:主要用于传递用户不需要或不应该直接编辑的内容,例如表单的内部数据或与页面状态相关的信息。
2.name="cart"
- 表单字段名:
name="cart"指定了该字段在表单数据中的名称。当表单提交时,这个字段会作为表单数据的一部分发送到服务器,服务器可以通过name="cart"这个名称来获取它对应的值。
value={JSON.stringify(cart)}
- **
JSON.stringify(cart)**:cart是一个 JavaScript 对象或数组,它被转换为 JSON 字符串并作为表单字段的值提交。因为 HTML 表单只能提交字符串类型的值,而 JavaScript 对象无法直接作为表单字段的值,因此需要使用JSON.stringify()将cart对象转换为 JSON 格式的字符串。
(3)代码运行流程
- 用户在
/order/new页面填写订单信息并提交表单。 - 表单提交后,
createOrderAction函数被调用,表单数据通过request.formData()获取。 - 将表单数据转换为 JavaScript 对象,并处理其中的复杂字段(如解析 JSON 和布尔值转换)。
- 调用
createOrder(order)函数来创建新订单。 - 创建订单后,通过
redirect(/order/${newOrder.id})将用户重定向到新创建的订单详情页。
(4)表单数据检测和错误处理
1、为了防止用户提供的数据格式不对,在创建新订单之前,需要做数据监测:
1 | |
- 创建一个空的对象
errors,用来存储所有的错误信息。这个对象会根据验证条件动态填充相应的错误消息。 - 如果电话号码无效,则在
errors对象中添加一个phone错误字段,并设置一个对应的错误消息 Object.keys(errors)返回errors对象中的所有键(即错误字段)的数组。如果errors中有任何错误信息,Object.keys(errors).length的值就会大于 0,于是代码会返回这个errors对象,通常会在前端显示这些错误信息给用户。
2、在组件里需要接受错误信息并显示:
const formErrors = useActionData();
{formErrors?.phone && <p>{formErrors.phone}</p>}