react课程14-使用Vite构建高级ReactProjiect

Last updated on September 11, 2024 pm

Jonas老师真的很有鼓舞力😂

之前也使用过Vite来构建项目,但是没仔细分析它和Creact-react-app的区别……本节课居然回到了第一次构建的pizza项目,但是应该会更加modern。

一、Vitecreate-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
2
3
4
5
6
7
import eslint from "vite-plugin-eslint";

// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), eslint()], //添加了eslint
});

.eslintrc.cjs文件:(默认配置加上react-app规则)(我把两个默认规则集都注释了,不然总报错🫠)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
🫠module.exports = {🫠
root: true, // 表示这是项目的根目录 ESLint 配置,防止 ESLint 搜索父级目录的配置。
env: {
browser: true, // 设定代码运行的环境为浏览器,ESLint 会针对浏览器的全局变量(如 `window`)进行校验。
es2020: true, // 支持 ES2020 语法特性。
},
extends: [
'react-app', // 加入 react-app 的 ESLint 规则
'eslint:recommended', // 启用 ESLint 推荐的默认规则集。
'plugin:react/recommended', // React 推荐的 ESLint 规则集。
'plugin:react/jsx-runtime', // 支持 React 17+ JSX 转换(即不需要显式导入 `React`)。
'plugin:react-hooks/recommended', // React Hooks 的推荐规则。
],
ignorePatterns: ['dist', '.eslintrc.cjs'], // 忽略 `dist` 目录和当前的 ESLint 配置文件,避免 ESLint 校验它们。
parserOptions: {
ecmaVersion: 'latest', // 使用最新的 ECMAScript 版本。
sourceType: 'module', // 代码是基于 ECMAScript 模块(ESM)的。
},
settings: {
react: {
version: '18.2', // 手动指定 React 版本,确保 ESLint 的 React 插件正确解析 JSX 语法。
},
},
plugins: ['react-refresh'], // 使用 `react-refresh` 插件,用于开发时 React 的热更新。
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
], // 仅允许导出 React 组件(针对 react-refresh 的特定规则),如果违反规则会发出警告。
},
};

⁉️在视频里构建完Vite项目后需要自己手动创建.eslintrc.json文件,和本文件有何区别?

  • .eslintrc.cjs

    • 这是一个使用 CommonJS 模块格式的 ESLint 配置文件,文件内容是以 module.exports = {} 的方式导出配置对象,允许使用动态的 JavaScript 语法。
    • 支持完整的 JavaScript 语法。你可以在其中使用变量、条件语句、函数等动态逻辑。这种格式适合需要根据环境或条件动态生成 ESLint 配置的场景。
    • 基于 CommonJS 模块系统。导出配置使用 module.exports = {},适用于 Node.js 环境中。
  • .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、分析特性和界面

image-20240909195022266

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

image-20240909195056831

四、建立项目结构

ui:可重用的组件(按钮、输入等)

services:可重用代码,用于和API交互

utils:helper 函数,可重用,不产生任何side effect

features:用来描述应用中的特定功能模块或子功能,每个 feature 通常包含自己的状态和处理它的逻辑。在实际开发中,features 通常会对应到 Redux 的 slice 文件,即每个 feature 可能会有自己的 slice 来处理该功能模块的状态和逻辑。

image-20240911161457628

五、执行Routes的新方法

npm i react-router-dom@6


react router文档链接:https://reactrouter.com/en/main/routers/create-browser-router

App.js示例版:(见下)

之前的方式:BrowserRouterRoutes(React Router v6)

这是 React Router v6 的典型使用方式。主要特点如下:

  1. BrowserRouter:
    • 包裹整个应用,提供路由功能。
    • 内部使用 HTML5 的 History API,监听 URL 的变化。
  2. Routes:
    • 取代了 React Router v5 中的 Switch,是 v6 中用于渲染路由的主要组件。
    • Route 是嵌套在 Routes 组件中的,每一个 Route 定义了一个具体路径及其对应的组件。
  3. 嵌套路由
    • 支持路由的嵌套,方便组织多层路由结构。例如,path="app" 下的子路由 (cities, countries, form) 会渲染在 AppLayout 内。
    • 子路由不需要写完整路径,可以相对父路由来定义路径。
  4. Suspense:
    • 使用 Suspense 进行懒加载时,可以通过 fallback 属性定义加载时的占位组件,优化用户体验。
  5. ProtectedRoute:
    • path="app" 中,使用了 ProtectedRoute 来实现路由的保护。只有满足特定条件时(比如用户已登录),路由才会渲染对应组件。
  6. 404页面处理
    • 通过 path="*" 来捕获所有未匹配的路由,显示 PageNotFound 组件。

现在的方式:createBrowserRouterRouterProvider(React Router v6.4+)

React Router v6.4 引入了新的 Data API,这意味着你可以使用 createBrowserRouterRouterProvider 来定义和提供路由。它的特点包括:

  1. createBrowserRouter:
    • 新的路由创建方式,提供了更直观的配置方式,将路由声明与数据获取逻辑(如 loaderaction)结合起来,适合定义较为复杂的应用路由。
    • 更加数据驱动,可以在路由定义时设置数据加载逻辑、错误处理等。
  2. RouterProvider:
    • 提供了一个 router 对象,用于将创建好的路由配置传递给应用程序。这种方式相比 BrowserRouter 更灵活,可以在创建路由时进行更多的配置操作。
  3. 不再需要 RoutesRoute:
    • 使用 createBrowserRouter 后,不需要再单独使用 RoutesRoute 组件,路由结构是直接在 router 对象内配置的。
    • 路由配置的结构较为简洁,适合数据预加载和复杂路由需求。

主要区别:

  1. 路由声明方式
    • 第一种方式使用 RoutesRoute 组件来声明路由,是 React Router v6 的常见方式。
    • 第二种方式使用 createBrowserRouter 来声明路由,更加简洁,适用于 v6.4+ 版本,尤其适合数据驱动的路由需求。
  2. 数据加载与处理
    • 在 React Router v6.4+ 中,createBrowserRouter 更加支持与数据加载和动作处理的结合,比如可以直接在路由定义时加入 loaderaction。这可以让你在定义路由的同时处理数据加载逻辑。
    • 而在传统的 BrowserRouter 方式中,数据加载通常是在组件内部通过 useEffect 或其他钩子完成的。
  3. 组件组织方式
    • createBrowserRouter 的方式下,路由定义集中在一个地方,比较适合大型应用。
    • BrowserRouterRoutes 的方式适合小型和中型应用,使用起来更加直观。
  4. 嵌套路由的管理
    • 两种方式都支持嵌套路由,但第一种方式通过 Route 的嵌套方式定义嵌套路由,而第二种方式是在 createBrowserRouter 的配置对象中通过层次化结构来实现。

总结

BrowserRouterRoutes 的方式:更加直接、简单,适用于中小型应用。

createBrowserRouterRouterProvider 的方式:适合更复杂的应用,尤其是在需要数据预加载和处理时,这种方式更加高效。

六、构建layout

(1)什么是layout?

layout(布局)界面通常是指一个应用程序的整体框架或页面结构。它负责将不同的 UI 组件(如导航栏、侧边栏、页脚、内容区等)组织在一起,并确保它们在页面上以一致的方式显示。布局界面主要用于定义页面的骨架和各部分的排列方式,用户可以在这个基础上进行内容的填充和交互设计。

(2)layout的作用

  • 保持一致性:每个页面在布局上的一致性有助于用户熟悉界面,减少学习成本。
  • 导航性:通过固定的导航栏或侧边栏,用户可以快速找到所需的功能或页面。
  • 复用性:通过将布局界面抽象出来,可以在多个页面间复用同一个结构,而只改变主内容区域的内容。

(3)嵌套路由

在 React Router 中,嵌套路由允许你在一个路由组件内部展示子路由的内容。这种方式非常适合实现像布局组件(Layout)这种页面结构,即主界面有一些固定不变的部分(比如导航栏、页眉),而其余部分根据不同路由切换。

<Outlet> 组件是 React Router 用于渲染嵌套路由的关键部分。它起到“占位符”的作用,表示在这个位置会渲染匹配当前路由的子路由内容。

(4)代码示例:

本次项目所有界面使用同种layout布局界面,因此较为简单。

AppLayout.jsx

1
2
3
4
5
6
7
8
9
10
11
function AppLayout() {
return (
<div>
<Header />
<main>
<Outlet />
</main>
<CartOverview />
</div>
);
}

App.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import Menu, { loader as menuLoader } from "./features/menu/Menu";

const router = createBrowserRouter([
{
element: <AppLayout />,
children: [
{
path: "/",
element: <Home />,
},
{
path: "/menu",
element: <Menu />,
loader: menuLoader,
},
],
},
]);

function App() {
return <RouterProvider router={router} />;
}

export default App;

七、Loaders

Loaders 通常指的是在数据加载过程中显示的组件或功能,来向用户展示数据正在获取的状态。它有助于提高用户体验,避免在数据还未加载完毕前呈现空白页面。

(1)Loader的介绍和初步使用

本次项目不同于之前使用useEffect获取数据加useState管理isLoading状态来显示加载界面的方法,由于运用了React Router v6.4,它自带了 Loader 功能用于在路由层面进行数据加载。可以结合路由直接展示加载状态。

使用方法如下:(直接给出例子)

Menu.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { useLoaderData } from "react-router-dom";
import { getMenu } from "../../services/apiRestaurant";
import MenuItem from "./MenuItem";

function Menu() {
const menu = useLoaderData();
return (
<ul>
{menu.map((pizza) => (
<MenuItem pizza={pizza} key={pizza.id} />
))}
</ul>
);
}

export async function loader() {
const menu = await getMenu();
return menu;
}

export default Menu;

  1. 在需要加载数据的文件中使用loader函数获取并返回数据(export导出,需是异步函数)
  2. 在该界面的Router中加入loader属性,并设置为导入的loader函数(可重命名)
  3. 在需要加载数据的组件函数中使用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);

image-20240911162903277

error.data或者error.message就是错误的具体信息(可以显示出获取信息失败的错误)

因此也可以在需要获取数据的子路由中加上这个错误属性,使得错误界面也拥有layout的布局。

九、提交表单

(1)actions在提交表单中的用法

首先需要将action函数链接到该组件的Router中

1
2
3
4
5
{
path: "/order/new",
element: <CreateOrder />,
action: createOrderAction, //已更名
},

另外,我们想要在提交表单数据的同时隐形提交cart中的数据,就在

组件中的任意位置放上这一行代码:

1
<input type="hidden" name="cart" value={JSON.stringify(cart)} />

最后是action函数:(action 函数是在表单提交时执行的,它会处理表单提交的数据)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export async function action({ request }) {
const formData = await request.formData(); //获得数据
const data = Object.fromEntries(formData); //将 FormData 转换为普通的 JavaScript 对象,方便后续处理表单中的字段和值
//console.log(data);
const order = { //重构data结构
...data,
cart: JSON.parse(data.cart), //从字符串形式转化为javaScript对象
priority: data.priority === "on",
};
// console.log(order);
const newOrder = await createOrder(order);

return redirect(`/order/${newOrder.id}`); //重定向
}

重构前和重构后的数据如图(本来priority在不选时不会出现,现在由true和false描述)

image-20240911194331654

image-20240911194427541

(2)隐藏字段解析

1.<input type="hidden">

  • 隐藏字段<input type="hidden"> 是一种不会显示在用户界面上的输入字段。它用于在表单中传递数据,但不让用户直接看到或修改这些数据。
  • 作用:主要用于传递用户不需要或不应该直接编辑的内容,例如表单的内部数据或与页面状态相关的信息。

2.name="cart"

  • 表单字段名name="cart" 指定了该字段在表单数据中的名称。当表单提交时,这个字段会作为表单数据的一部分发送到服务器,服务器可以通过 name="cart" 这个名称来获取它对应的值。
  1. value={JSON.stringify(cart)}
  • **JSON.stringify(cart)**:cart 是一个 JavaScript 对象或数组,它被转换为 JSON 字符串并作为表单字段的值提交。因为 HTML 表单只能提交字符串类型的值,而 JavaScript 对象无法直接作为表单字段的值,因此需要使用 JSON.stringify()cart 对象转换为 JSON 格式的字符串。

(3)代码运行流程

  1. 用户在 /order/new 页面填写订单信息并提交表单。
  2. 表单提交后,createOrderAction 函数被调用,表单数据通过 request.formData() 获取。
  3. 将表单数据转换为 JavaScript 对象,并处理其中的复杂字段(如解析 JSON 和布尔值转换)。
  4. 调用 createOrder(order) 函数来创建新订单。
  5. 创建订单后,通过 redirect(/order/${newOrder.id}) 将用户重定向到新创建的订单详情页。

(4)表单数据检测和错误处理

1、为了防止用户提供的数据格式不对,在创建新订单之前,需要做数据监测:
1
2
3
4
5
6
const error={};
if (!isValidPhone(order.phone))
errors.phone =
"Please give us your correct phone number.We might need it to contact you.";

if (Object.keys(errors).length > 0) return errors;
  1. 创建一个空的对象 errors,用来存储所有的错误信息。这个对象会根据验证条件动态填充相应的错误消息。
  2. 如果电话号码无效,则在 errors 对象中添加一个 phone 错误字段,并设置一个对应的错误消息
  3. Object.keys(errors) 返回 errors 对象中的所有键(即错误字段)的数组。如果 errors 中有任何错误信息,Object.keys(errors).length 的值就会大于 0,于是代码会返回这个 errors 对象,通常会在前端显示这些错误信息给用户。
2、在组件里需要接受错误信息并显示:

const formErrors = useActionData();

{formErrors?.phone && <p>{formErrors.phone}</p>}


react课程14-使用Vite构建高级ReactProjiect
http://example.com/2024/09/09/react课程14-使用Vite构建高级ReactProjiect/
Author
Yaodeer
Posted on
September 9, 2024
Licensed under