近两天更新完基本内容,后续长期更新,建议关注收藏点赞。
推荐搭配食用:快速学会react全家桶
目录
必备工具
- ESLint: JavaScript 代码检查工具
- Prettier :格式化代码
- TypeScript :类型安全
npm install @types/react @types/react-dom
react中使用它,使用 .tsx 文件扩展名,告诉 TypeScript 该文件包含 JSX。
- React 开发者工具:检查 React components,编辑 props 和 state,并识别性能问题。
过时内容
React 现在已经不推荐使用 class 和 constructor,主流开发全面转向函数组件和 Hooks。
constructor构造函数 最先被执行
- state
//旧版写法:
this.setState({
inputValue:e.target.value
})
- 绑定事件
constructor(props){
super(props);
this.handleClick = this.handleClick.bind(this);//绑定方法二 更好 节约性能
}
render(){
return (//绑定方法一:
<div onClick={this.handleClick.bind(this)}>
//如果只是一个函数,this会指向不明,所以需要绑定
{this.props.content}
</div>)
}
)
- ref
//旧版
handle(e){
const value = e.target.value;
}
/*
ref 是 React 提供的一种方式,
它可以让你直接访问 DOM 元素或组件实例,避免了通过 e.target 来获取元素。
在 React 中,ref 提供了对组件内部 DOM 元素的直接引用,从而使得你能够更高效地进行 DOM 操作。
*/
import React, { Component } from 'react';
class MyComponent extends Component {
constructor(props) {
super(props);
// 创建 ref 对象
this.inputRef = React.createRef();
}
focusInput = () => {
// 直接通过 ref 聚焦到输入框
this.inputRef.current.focus();
};
handleClick = () => {
// 通过 ref 获取输入框的值
const inputValue = this.inputRef.current.value;
console.log(inputValue);
};
render() {
return (
<div>
<input
ref={this.inputRef} // 将 ref 绑定到输入框元素
type="text"
/>
<button onClick={this.focusInput}>Focus the Input</button>
<button onClick={this.handleClick}>Log Input Value</button>
</div>
);
}
}
export default MyComponent;
简介
React.js 不是一个框架,它只是一个库。它只提供 UI (view)层面的解决方案。在实际的项目当中,它并不能解决我们所有的问题,需要结合其它的库,例如 Redux、React-router 等来协助提供完整的解决方法。
用户断网后网页不会消失。
- 项目目录文件
my-react-app/
├── node_modules/
├── public/
├── src/
│ ├── api/
│ │ └── index.js
│ ├── components/
│ │ ├── common/
│ │ └── specific/
│ ├── hooks/
│ │ └── useCustomHook.js
│ ├── pages/
│ │ ├── Home.js
│ │ └── About.js
│ ├── redux/
│ │ ├── actions/
│ │ ├── reducers/
│ │ └── store.js
│ ├── utils/
│ │ └── helpers.js
│ ├── App.js
│ ├── index.js
│ └── ...
├── .gitignore
├── package.json
├── README.md
└── ...
- package.json
关于项目的基本信息,还有相关指令 npm,如npm run start… - .gitignore
不想传到git的文件可以定义在里面
#注释
要忽略的用相对路径
- public
存放静态资源,包含 HTML 文件、图标等。默认的 index.html 文件是 React 应用的入口 HTML 文件。
在 manifest.json 文件中配置 PWA(渐进式Web应用)的快捷方式图标、网址跳转和主题颜色,可以通过以下属性来完成:
{
"name": "My Progressive Web App",
"short_name": "My PWA",
"description": "This is a PWA example.",
"start_url": "/index.html", // 设置启动页的网址
"display": "standalone", // 可选项: fullscreen, standalone, minimal-ui, browser
"background_color": "#ffffff", // 设置背景颜色
"theme_color": "#0000ff", // 设置应用主题颜色
"icons": [
{
"src": "icons/icon-192x192.png", // 图标路径
"sizes": "192x192", // 图标尺寸
"type": "image/png" // 图标类型
},
{
"src": "icons/icon-512x512.png", // 图标路径
"sizes": "512x512", // 图标尺寸
"type": "image/png" // 图标类型
}
],
"scope": "/", // 限定PWA的范围
"orientation": "portrait", // 设置应用的默认方向,可选 'landscape' 或 'portrait'
"lang": "en", // 设置语言
"dir": "ltr" // 文字方向: "ltr" (从左到右), "rtl" (从右到左)
}
- src目录 (项目源代码)
自动化测试:app.test.js
src 目录是我们主要编写代码的地方,包含了所有的 React 组件、样式和其他资源。通常会按照功能或组件类型来组织代码。
components 目录存放项目的所有 React 组件。通常,我们会按照组件的功能或页面进行子目录的划分。
// src/components/Header.js
import React from 'react';
const Header = () => (
<header>
<h1>My React App</h1>
</header>
);
export default Header;
assets 目录存放项目的静态资源,如图片、字体、样式等。
App.js 是 React 应用的根组件,通常用于设置路由和全局状态管理。
// src/App.js
import React from 'react';
import Header from './components/Header';
const App = () => (
<div>
<Header />
<main>
<p>Welcome to my React app!</p>
</main>
</div>
);
export default App;
运行入口:index.js 负责引入和渲染,index.js 是 React 应用的入口文件,负责渲染根组件App到index.html 中的 root 节点。
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
//将app组件挂载到root节点下,则在div为root的模块下会显示app组件
// src/api/index.js
import axios from 'axios';
const apiClient = axios.create({
baseURL: 'https://api.example.com',
headers: {
'Content-Type': 'application/json'
}
});
export const fetchData = async () => {
const response = await apiClient.get('/data');
return response.data;
};
- pages 目录用于存放页面组件,这些组件通常会包含路由配置。
// src/pages/Home.js
import React from 'react';
const Home = () => (
<div>
<h1>Home Page</h1>
</div>
);
export default Home;
- redux 目录用于存放 Redux 的相关文件,包括 actions、reducers 和 store 配置。
// src/redux/store.js
import { createStore } from 'redux';
import rootReducer from './reducers';
const store = createStore(rootReducer);
export default store;
- utils 目录用于存放通用的工具函数。
// src/utils/helpers.js
export const formatDate = (date) => {
return new Date(date).toLocaleDateString();
};
- 视为树
UI设为树、渲染树、模块依赖树
JSX(JavaScript XML)
JSX(JavaScript XML)是 React 提供的一种语法扩展,允许在 JavaScript 中写类似 HTML 的代码。本质上是 JavaScript 的一种语法糖,用来描述用户界面的结构和布局。JSX 让开发者能够在 React 组件中更加直观地定义 UI,而不需要通过传统的 JavaScript API 方式(例如 React.createElement)来创建 DOM 元素。
- 使用jsx语法(带标签)必须要引入react
import react from 'react';
因为 JSX 本质上会被转译成 React.createElement 调用,React 必须在代码中存在,以便这些 JSX 元素能够被正确处理和渲染。在 React 17 之前,通常需要在每个文件顶部引入 React。 - 从 React 17 开始,JSX 的编译方式发生了变化,React 团队引入了一种新的编译器(@babel/preset-react 的更新版本)。通过这一变化,不再强制要求在每个文件中显式引入 React。这是因为,JSX 会在编译阶段自动从 React 中获取相关的功能,而不需要显式调用 React.createElement()。
因此,在 React 17 及之后的版本中,如果你只是使用 JSX,而不直接使用 React 对象中的其他功能(如 useState、useEffect 等),你可以省略引入 React。 - 为什么 JSX 仍然被广泛使用?
语法简洁:JSX 让开发者可以像写 HTML 那样写 React 组件的 UI,而不是写一堆 React.createElement 调用。这种语法更符合开发者的直觉,也更容易理解和修改。
React 官方推荐R:它不仅简化了组件的书写,还能让开发者更高效地进行开发。即便 React 17 以后支持了“自动引入 React”的功能,JSX 依然是默认推荐的方式。 - 使用 JSX 与不使用 JSX 的区别
//使用JSX
const Button = () => {
return <button>Click me</button>;
};
//最终转译成 JavaScript
//JSX 并不能被浏览器直接识别,它需要经过编译,通常通过 Babel 将 JSX 转换成 JavaScript。
//不使用JSX
const Button = () => {
return React.createElement('button', null, 'Click me');
};
//在 JSX 中,你可以直接嵌入 JavaScript 表达式。表达式必须用大括号 {} 包裹
// 在 JSX 中,所有的动态内容都必须用大括号 {} 包裹。
const name = "John";
const element = <h1>Hello, {name}!</h1>;
//JSX 与 HTML 的区别
// 在 HTML 中,你用 class 来定义元素的类名,但在 JSX 中,应该使用 className,因为 class 是 JavaScript 的保留字。
<div className="my-class"></div>
//同样的,标签中的for用htmlFor替换
//在 JSX 中,自闭合标签必须明确写成自闭合格式。
//例如,<img> 标签在 HTML 中可以省略闭合标签,但在 JSX 中必须写成 <img />。
//在 JSX 中,事件名是驼峰命名法,而不是小写的。
//onclick 在 HTML 中是 onclick,但在 JSX 中应该写作 onClick
<button onClick={handleClick}>Click me</button>
//example
const user = {
name: 'Hedy Lamarr',
imageUrl: 'https://i.imgur.com/yXOvdOSs.jpg',
imageSize: 90,
};
export default function Profile() {
return (
<>
<h1>{user.name}</h1>
<img
className="avatar"
src={user.imageUrl}
alt={'Photo of ' + user.name}
style={{
width: user.imageSize,
height: user.imageSize
}}
/>
</>
);
}
- JSX规范
- 组件也不能返回多个 JSX 标签。你必须将它们包裹到一个共享的父级中,比如
<div>...</div>
或使用空的<>...</>
包裹 - JSX 比 HTML 更加严格。你必须闭合标签,如
<br />
- 大写字母开头为组件,组件写完记得导出
export default TodoItem;
在别的组件里引入TodoItem组件import TodoItem from './TodoItem';
组件
组件定义
React 组件是返回标签的 JavaScript 函数。React 组件必须以大写字母开头,而 HTML 标签则必须是小写字母。
整体父子组件的结构就是一层套一层的。
//App.js
function MyButton() {
return (
<button>
我是一个按钮
</button>
);
}
export default function MyApp() {
return (
<div>
<h1>欢迎来到我的应用</h1>
<MyButton />
</div>
);
}
组件通信
- React 使用单向数据流,通过组件层级结构从父组件传递数据至子组件;为了支持通过用户输入来改变 state,需要让数据反向传输:深层结构的表单组件需要更新 state,即添加函数并且运用对应的setState函数。
function FilterableProductTable({ products }) {
const [filterText, setFilterText] = useState('');
const [inStockOnly, setInStockOnly] = useState(false);
return (
<div>
<SearchBar
filterText={filterText}
inStockOnly={inStockOnly}
onFilterTextChange={setFilterText}
onInStockOnlyChange={setInStockOnly} />
...
function SearchBar({
filterText,
inStockOnly,
onFilterTextChange,
onInStockOnlyChange
}) {
return (
<form>
<input
type="text"
value={filterText}
placeholder="搜索"
onChange={(e) => onFilterTextChange(e.target.value)}
/>
<label>
<input
type="checkbox"
checked={inStockOnly}
onChange={(e) => onInStockOnlyChange(e.target.checked)}
- props v.s. state
在 React 中有两种“模型”数据:props 和 state。
不同之处:
props 像是你传递的参数 至函数。它们使父组件可以传递数据给子组件,定制它们的展示。
state 像是组件的内存。它使组件可以对一些信息保持追踪,并根据交互来改变。
props 和 state 是不同的,但它们可以共同工作。父组件将经常在 state 中放置一些信息(以便它可以改变),并且作为子组件的属性 向下 传递至它的子组件。
props
- 作用
父组件向子组件传递props
将父组件方法传递给子组件,子调用父组件方法修改父组件 - 在 React 中,通常使用 onSomething 命名代表事件的 props,使用 handleSomething 命名处理这些事件的函数。
状态提升:将state移动到这些组件的父组件上 - 推荐解构,这样不用加this.props.前缀
//TodoList中
return(
<div>
<ul>
{
this.state.list.map((item,index)=>{
return(
<div>
<TodoItem index={index} content={item} />
</div>
)
}
</ul>
</div>)
//TodoItem中
render(){
return <div>{this.props.content}</div>
}
//子调用父组件方法修改父组件
return(
<div>
<TodoItem handleID={this.handleItemDelete} />
</div>
)
//TodoItem.js中
handleClick(){
this.props.handleID(this.props.index)
}
//但是报错了 原因是这个this指向的不是父组件(方法是父组件的方法)
//修改为
{
this.state.list.map((item,index)=>{
return (
<div>
<TodoItem
content={item}
index={index}
handleID={this.handleItemDelete.bind(this)}
//将 handleItemDelete 绑定到当前实例
//可用箭头函数 避免用this绑定
/>
</div>
)
}
}
//const content ={ this. props}; 以后用content直接代替这一串
onchange={this.handleInputChange.bind(this) //直接写在属性里影响性能
//修改
//绑定的统一都放在constructor下面
this.handleInputChange = this.handleInputChange.bind(this);
//-------------------
import { useState } from 'react';
export default function MyApp() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<div>
<h1>共同更新的计数器</h1>
<MyButton count={count} onClick={handleClick} />
<MyButton count={count} onClick={handleClick} />
</div>
);
}
function MyButton({ count, onClick }) {
return (
<button onClick={onClick}>
点了 {count} 次
</button>
);
}
基础使用
- 条件渲染
JS原生的if 或 三元运算符 或逻辑与
let content;
if (isLoggedIn) {
content = <AdminPanel />;
} else {
content = <LoginForm />;
}
return (
<div>
{content}
</div>
);
<div>
{isLoggedIn ? (
<AdminPanel />
) : (
<LoginForm />
)}
</div>
<div>
{isLoggedIn && <AdminPanel />}
</div>
- 渲染列表
JS原生的for 循环 和 array 的 map() 函数
对于列表中的每一个元素,都应该传递一个字符串或者数字给 key,用于在其兄弟节点中唯一标识该元素。如果你在后续插入、删除或重新排序这些项目,React 将依靠你提供的 key 来思考发生了什么。
key 不需要是全局唯一的;它们只需要在组件及其同级组件之间是唯一的。
const products = [
{ title: '卷心菜', isFruit: false, id: 1 },
{ title: '大蒜', isFruit: false, id: 2 },
{ title: '苹果', isFruit: true, id: 3 },
];
export default function ShoppingList() {
const listItems = products.map(product =>
<li
key={product.id}
style={{
color: product.isFruit ? 'magenta' : 'darkgreen'
}}
>
{product.title}
</li>
);
return (
<ul>{listItems}</ul>
);
}
- 响应事件
在组件中声明 事件处理 函数,绑定到组件上
onClick={handleClick}
的结尾没有小括号!不要 调用 事件处理函数:你只需 把函数传递给事件 即可。当用户点击按钮时 React 会调用你传递的事件处理函数。
React Hooks
以 use 开头的函数被称为 Hook。Hook 比普通函数更为严格。你只能在你的组件(或其他 Hook)的 顶层 调用 Hook。
- 作用
主要是为了让你在函数组件中使用状态(state)、副作用(side effects)和其他 React 特性,而不必使用类组件。它们让代码更加简洁和灵活,也改善了组件的复用性和可维护性。 - 优点
- 简化代码结构
在类组件中,管理状态和生命周期方法通常需要编写冗长的类定义,使用 Hooks 后,你可以在函数组件中直接管理状态和副作用,代码更简洁,逻辑更清晰。 - 组件复用
Hooks 使得组件的逻辑更加可复用。例如,使用 useState 和 useEffect 可以使状态和副作用的处理变得更模块化,可以将逻辑封装成自定义 Hooks,在多个组件中复用,而不需要通过继承或高阶组件来共享逻辑。 - 更灵活的副作用管理
在类组件中,副作用(例如数据获取、订阅事件等)通常需要在生命周期方法中管理,可能比较难以维护。useEffect 能让你明确地控制副作用的执行时机,并且非常灵活地指定依赖项,避免不必要的副作用执行。 - 不再需要类组件
对很多开发者来说,类组件比较难理解和维护。Hooks 帮助你摆脱了类组件的复杂性,能够只通过函数组件来实现所有的功能。 - 性能优化
useMemo 和 useCallback 这两个 Hooks 可以帮助你优化性能,避免重复渲染和不必要的计算,提升应用性能,尤其是在大型应用中。
- 为什么React 要提供 React Hooks 这种组件,普通的Component类不好么?
简化代码结构:类组件需要手动处理生命周期方法(如 componentDidMount、componentDidUpdate 等),并且状态(this.state)和事件处理(this.setState)通常显得比较冗长和难以组织。而通过 Hooks,函数组件可以通过 useState 和 useEffect 等简化这些逻辑,使得组件代码更加简洁和易于理解。
逻辑复用:类组件之间的逻辑复用通常通过高阶组件(HOC)或者 render props 来实现,但这些方式往往会带来“嵌套地狱”或难以追踪的问题。而 Hooks 提供了一种更直观的方式来复用组件逻辑。例如,可以将某些副作用或状态管理逻辑抽离到自定义 Hook 中,使得代码更加模块化、可复用。
更好的组合性:函数组件和 Hooks 使得组合组件变得更加自然。你可以灵活地组合多个 Hooks 来实现复杂的行为,而不需要通过继承或者嵌套的方式来实现功能。
不再依赖 this:类组件中,this 的使用常常让开发者感到困扰,特别是对于初学者来说,理解和管理 this 的绑定可能很麻烦。Hooks 让函数组件脱离了 this 的依赖,使用起来更直观。
性能优化:React 在 Hooks 的实现上进行了优化,避免了类组件的某些性能瓶颈。例如,React 在内部管理 Hooks 时能做更多的优化,从而减少不必要的重新渲染和计算。
//hooks 目录用于存放自定义的 React Hooks。
// src/hooks/useCustomHook.js
import { useState, useEffect } from 'react';
const useCustomHook = () => {
const [data, setData] = useState(null);
useEffect(() => {
// Fetch data or perform other side effects
}, []);
return data;
};
export default useCustomHook;
const [count, setCount] = useState(0);
useEffect(() => {
// 执行副作用代码
}, [count]); // 可选的依赖项数组
const value = useContext(MyContext);
const inputRef = useRef(null);
//useMemo 和 useCallback 用于优化性能,避免不必要的重新计算或函数重建。
useReducer vs useState
特性 | useState | useReducer |
---|---|---|
✅ 适合场景 | 简单状态,1-2 个变量 | 复杂状态(多个字段、状态之间有关联) |
🔁 状态更新方式 | 手动写更新逻辑(自己 set) | 写一个集中管理状态变化的 reducer |
🧩 可组合性 | 不容易复用逻辑 | 可以抽出 reducer,更好封装 |
🔍 可预测性 | 零散 setState 调用,不集中 | 所有状态变化集中处理,更清晰,好调试 |
⚙️ 依赖外部库 | 无需额外结构 | 类似 Redux 的思想,适合中大型组件 |
- useReducer
使用 reducer 管理状态与直接设置状态略有不同。它不是通过设置状态来告诉 React “要做什么”,而是通过事件处理程序 dispatch 一个 “action” 来指明 “用户刚刚做了什么”。(而状态更新逻辑则保存在其他地方!)
function handleDeleteTask(taskId) {
dispatch(// "action" 对象:
{
type: 'deleted',
id: taskId,
}
);
}
//集中管理
const initialState = {
count: 0,
loading: false,
error: null,
};
function reducer(state, action) {//放置状态逻辑 返回下个状态
switch (action.type) {
case "increment":
return { ...state, count: state.count + 1 };
case "startLoading":
return { ...state, loading: true };
case "setError":
return { ...state, error: action.payload };
default:
return state;
}
}
//reducer变更函数, initialState初始状态
const [state, dispatch] = useReducer(reducer, initialState);
//也可以用Immer
import { useImmerReducer } from 'use-immer';
Immer 为你提供了一种特殊的 draft 对象,你可以通过它安全地修改 state。在底层,Immer 会基于当前 state 创建一个副本。这就是通过 useImmerReducer 来管理 reducer 时,可以修改第一个参数,且不需要返回一个新的 state 的原因。
扩展:为啥叫reducer?
和数组上的 reduce()一样,传递给 reduce 的函数被称为 “reducer”。它接受 目前的结果 和 当前的值,然后返回 下一个结果。
useContext
是一种无需通过组件传递 props 而可以直接在组件树中传递数据的技术。它是通过创建 provider 组件使用,通常还会创建一个 Hook 以在子组件中使用该值。从传递给 createContext 调用的值推断 context 提供的值的类型。
useContext 是一个 Hook。和 useState 以及 useReducer一样,你只能在 React 组件中(不是循环或者条件里)立即调用 Hook。useContext 告诉 React Heading 组件想要读取 LevelContext。
//1. 创建context
//LevelContext.js
import { createContext } from 'react';
export const LevelContext = createContext(1);//默认值是1
//2. 使用context 哪个组件用放哪
//Heading.js
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
export default function Heading({ children }) {
const level = useContext(LevelContext);//访问到上层的context值
...
}
//3. 提供 context
import { LevelContext } from './LevelContext.js';
export default function Section({ level, children }) {
return (
<section className="section">
//用 context provider 包裹起来 以提供 LevelContext 给它们
<LevelContext value={level}> //需要通过哪几层 就包哪几层
{children}
</LevelContext>
/*
如果在 <Section> 组件中的任何子组件请求 LevelContext,给他们这个 level。
组件会使用 UI 树中在它上层最近的那个 <LevelContext> 传递过来的值
*/
</section>
);
}
/*
由于 context 让你可以从上层的组件读取信息,每个 Section 都会从上层的 Section 读取 level,并自动向下层传递 level + 1。
作用:似的一层层嵌套的section不用手动每个层级指定level
*/
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
export default function Section({ children }) {
const level = useContext(LevelContext);
return (
<section className="section">
<LevelContext value={level + 1}>
{children}
</LevelContext>
</section>
);
}
Context 会穿过中间层级的组件,可以在提供 context 的组件和使用它的组件之间的层级插入任意数量的组件。在 React 中,覆盖来自上层的某些 context 的唯一方法是将子组件包裹到一个提供不同值的 context provider 中。不同的 React context 不会覆盖彼此。只有使用和提供 那个特定的 context 的组件才会联系在一起。
- 切勿滥用context
先考虑下面两种情况是否合适,如果都不行再用。
传递 props / 抽象组件并 将 JSX 作为 children 传递 给它们。 - context使用场景:树中不同部分的远距离组件
主题: 如果你的应用允许用户更改其外观(例如暗夜模式),你可以在应用顶层放一个 context provider,并在需要调整其外观的组件中使用该 context。
当前账户: 许多组件可能需要知道当前登录的用户信息。将它放到 context 中可以方便地在树中的任何位置读取它。某些应用还允许你同时操作多个账户(例如,以不同用户的身份发表评论)。在这些情况下,将 UI 的一部分包裹到具有不同账户数据的 provider 中会很方便。
路由: 大多数路由解决方案在其内部使用 context 来保存当前路由。这就是每个链接“知道”它是否处于活动状态的方式。如果你创建自己的路由库,你可能也会这么做。
状态管理: 通常 将 reducer 与 context 搭配使用来管理复杂的状态并将其传递给深层的组件来避免过多的麻烦。
Context 不局限于静态值。如果你在下一次渲染时传递不同的值,React 将会更新读取它的所有下层组件!这就是 context 经常和 state 结合使用的原因。 - 结合使用 reducer 和 context
//1. 创建 context
//TasksContext.js
import { createContext } from 'react';
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);
//原const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
//改造成 TasksContext 提供当前的 tasks 列表
//TasksDispatchContext 提供了一个函数可以让组件分发动作
//2. 将 state 和 dispatch 函数放入 context
//将所有的 context当作容器 赋值state,dispatch 导入 TaskApp 组件
import { TasksContext, TasksDispatchContext } from './TasksContext.js';
export default function TaskApp() {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
// ...
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
...
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
//3. 使用它 :每个组件都会读取它需要的 context
//任何需要的组件都可以从 useContext 中读取它
export default function AddTask() {
const [text, setText] = useState('');
const dispatch = useContext(TasksDispatchContext);
// ...
return (
// ...
<button onClick={() => {
setText('');
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}}>Add</button>
// ...
//4. 单独抽象成一个组件
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
通过[快照]记住信息state
当前的 state(count),以及用于更新它的函数(setCount),按照惯例会像 [something, setSomething] 这样为它们命名。每个组件实例互不影响。
state尽可能扁平化,避免深度嵌套
item的state使用item的ID,而不是item本身
import { useState } from 'react';
function MyButton() {
const [count, setCount] = useState(0);//声明
// ...
//example
import { useState } from 'react';
export default function MyApp() {
return (
<div>
<h1>独立更新的计数器</h1>
<MyButton />
<MyButton />
</div>
);
}
function MyButton() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<button onClick={handleClick}>
点了 {count} 次
</button>
);
}
//setState里面尽可能放函数,而不是直接修改值,尤其含异步操作
async function handleClick() {
setPending(pending + 1);
await delay(3000);
setPending(pending - 1);//❌
setCompleted(completed + 1);//❌
}
//会发生诡异的效果
async function handleClick() {
setPending(p => p + 1);
await delay(3000);
setPending(p => p - 1);//✅
setCompleted(c => c + 1);//✅
}
运行时,React 中存储的 state 可能已经发生了更改,但它是使用用户与之交互时状态的快照进行调度的。React 会使 state 的值始终固定在一次渲染的各个事件处理函数内部。
你无需担心代码运行时 state 是否发生了变化。一个 state 变量的值永远不会在一次渲染的内部发生变化, 即使其事件处理函数的代码是异步的。
无论你是将输出放在 setWalk 调用之前还是之后都不会有区别。那次渲染的 walk 的值是固定的。调用setWalk 只会为 下次 渲染对它进行变更,而不会影响来自上次渲染的事件处理函数。
- immutability不可变性 (no mutation)
把所有存放在 state 中的 JavaScript 对象都视为只读的。
基本类型的值是不可变的,更新可以替换他们。但object不同。
更新 state 中的对象时应该重新创建一个对象赋值给它,不要修改原对象;如果直接修改,并没有使用 state 的设置函数,React 并不知道对象已更改。所以 React 没有做出任何响应。为了真正地 触发一次重新渲染,你需要创建一个新对象并把它传递给 state 的设置函数。
借助…对象展开语法(浅拷贝)轻松更新,后覆盖前,如果是嵌套对象则需要嵌套修改。
const [person, setPerson] = useState({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
});
const nextArtwork = { ...person.artwork, city: 'New Delhi' };
const nextPerson = { ...person, artwork: nextArtwork };
setPerson(nextPerson);
setPerson({
...person, // 复制其它字段的数据
artwork: { // 替换 artwork 字段
...person.artwork, // 复制之前 person.artwork 中的数据
city: 'New Delhi' // 但是将 city 的值替换为 New Delhi!
}
});
如果你不想改变 state 的数据结构,你可能更喜欢用一种更便捷的方式来实现嵌套展开的效果。Immer 是一个非常流行的库,它可以让你使用简便但可以直接修改的语法编写代码,并会帮你处理好复制的过程。通过使用 Immer,你写出的代码看起来就像是你“打破了规则”而直接修改了对象
- 更新 state 中的数组
在 JavaScript 中,数组只是另一种对象。数组虽然是可变的,但是却应该被视为不可变。同对象一样,当你想要更新存储于 state 中的数组时,你需要创建一个新的数组(或者创建一份已有数组的拷贝值),并使用新数组设置 state。
即使你拷贝了数组,你还是不能直接修改其内部的元素。因为数组的拷贝是浅拷贝,修改了拷贝数组内部的某个对象,其实你正在直接修改当前的 state。类似于 更新嵌套的 JavaScript 对象 的方式解决这个问题。
setArtists( // 替换 state
[ // 是通过传入一个新数组实现的
...artists, // 新数组包含原数组的所有元素
{ id: nextId++, name: name } // 并在末尾添加了一个新的元素
//放在扩展运算符前面则是在开头添加
]
);
强推Immer
- this.setState更新方式
setstate是异步函数 不能立刻被执行,React 会等到事件处理函数中的所有代码都运行完毕再处理你的 state 更新。
在 React 的 新版写法 中,setState 函数接收一个回调函数,而这个回调函数的作用是返回一个对象。这个回调函数的形式是通过箭头函数来写的,因此可能会造成上下文(this)的问题。下面提到的报错,是因为箭头函数的写法没有正确访问e.target.value
//在之前的位置
<ul>
{this.getTodoItem()}
</ul>
//新版写法:
this.setState (() => ({
inputValue: e.target.value
}))
}
//无奈报错
//如果箭头函数中没有正确引用 e.target.value,就会导致 作用域 问题。
//this.setState 是异步的,它会在当前执行栈清空后更新状态。
//因此,在某些情况下,e.target.value 可能会因为作用域问题无法正确获取。
//解决方法:手动提取 e.target.value
//在箭头函数外面先将 e.target.value 存储到一个常量中,然后在回调中引用它。
handleChange = (e) => {
const value = e.target.value; // 手动提取 e.target.value
this.setState(() => ({
inputValue: value
}));
}
自定义hook
像 useTasks 和 useTasksDispatch 这样的函数被称为 自定义 Hook。 如果你的函数名以 use 开头,它就被认为是一个自定义 Hook。这让你可以使用其他 Hook,比如 useContext。
export function useTasks() {
return useContext(TasksContext);
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}
//万宗归一特效
//usePointerPosition.js
import { useState, useEffect } from 'react';
export function usePointerPosition() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
function handleMove(e) {
setPosition({ x: e.clientX, y: e.clientY });
}
window.addEventListener('pointermove', handleMove);
return () => window.removeEventListener('pointermove', handleMove);
}, []);
return position;
}
//useDelayedValue.js
import { useState, useEffect } from 'react';
export function useDelayedValue(value, delay) {
const [delayedValue, setDelayedValue] = useState(value);
useEffect(() => {
setTimeout(() => {
setDelayedValue(value);
}, delay);
}, [value, delay]);
return delayedValue;
}
//App.js
import { usePointerPosition } from './usePointerPosition.js';
import { useDelayedValue } from './useDelayedValue.js';
export default function Canvas() {
const pos1 = usePointerPosition();
const pos2 = useDelayedValue(pos1, 100);
const pos3 = useDelayedValue(pos2, 200);
const pos4 = useDelayedValue(pos3, 100);
const pos5 = useDelayedValue(pos4, 50);
return (
<>
<Dot position={pos1} opacity={1} />
<Dot position={pos2} opacity={0.8} />
<Dot position={pos3} opacity={0.6} />
<Dot position={pos4} opacity={0.4} />
<Dot position={pos5} opacity={0.2} />
</>
);
}
function Dot({ position, opacity }) {
return (
<div style={{
position: 'absolute',
backgroundColor: 'pink',
borderRadius: '50%',
opacity,
transform: `translate(${position.x}px, ${position.y}px)`,
pointerEvents: 'none',
left: -20,
top: -20,
width: 40,
height: 40,
}} />
);
}
减少手动编写的 Effect,因为你将能够重用已经编写的自定义 Hooks。React 社区也维护了许多优秀的自定义 Hooks。
生命周期函数
react 16.3 之后,React 引入了新的生命周期方法和一些废弃的方法:
componentWillMount、componentWillReceiveProps 和 componentWillUpdate 已经被弃用。
getDerivedStateFromProps 和 getSnapshotBeforeUpdate 是新的替代方法。
React 的生命周期函数可以分为三个主要阶段:挂载 (Mounting)、更新 (Updating) 和 卸载 (Unmounting)。每个阶段都有不同的生命周期方法,用来处理不同的任务。
- 挂载阶段 (Mounting)
当组件被创建并插入到 DOM 中时,以下生命周期函数会被依次调用:
constructor(props)
在组件实例化时调用。通常用于初始化状态和绑定事件处理程序。
static getDerivedStateFromProps(nextProps, nextState)
在每次渲染之前调用,无论是因为父组件的更新,还是由于内部状态变化。返回一个对象来更新组件的状态,或者返回 null 不更新状态。
注意:这是静态方法,不能访问 this。
render()
必须实现的函数,返回 JSX,React 会将其渲染到 DOM 中。
componentDidMount()
组件挂载完成后调用。可以在这里执行网络请求或订阅事件等操作,通常用于需要获取数据或初始化的情况。 - 更新阶段 (Updating)
当组件的状态或 props 改变时,组件会重新渲染。更新阶段包括以下生命周期方法:
static getDerivedStateFromProps(nextProps, nextState)
这个方法也会在更新阶段被调用(与挂载阶段一样)。
shouldComponentUpdate(nextProps, nextState)
在组件重新渲染前调用,用于判断是否需要更新组件。返回 true 或 false。默认是 true,如果想要优化性能,可以通过此方法来避免不必要的渲染。
render()
在更新阶段会再次调用,重新渲染组件。
getSnapshotBeforeUpdate(prevProps, prevState)
在渲染输出 (render) 后,提交到 DOM 前调用。可以用来记录一些信息,或者进行一些操作(例如,获取滚动位置等)。
componentDidUpdate(prevProps, prevState, snapshot)
在组件更新后调用,可以访问更新前的 props 和 state,以及 getSnapshotBeforeUpdate 返回的快照信息。适合在组件更新后进行操作(例如,网络请求、DOM 更新等)。 - 卸载阶段 (Unmounting)
当组件从 DOM 中移除时,会调用以下生命周期方法:
componentWillUnmount()
在组件卸载前调用,用于清理工作(例如,取消网络请求、移除订阅等)。
错误处理阶段 (Error Handling)
React 16 引入了新的错误边界 (Error Boundaries),可以捕获子组件的渲染、生命周期方法、构造函数等错误:
static getDerivedStateFromError(error)
用于渲染备用 UI。接收错误信息并返回更新的 state。
componentDidCatch(error, info)
捕获错误后的回调函数,可以用于日志记录等操作。
脱围机制(Escape Hatches)
有些组件可能需要控制和同步 React 之外的系统。
例如,你可能需要使用浏览器 API 聚焦输入框,或者在没有 React 的情况下实现视频播放器,或者连接并监听远程服务器的消息。
在本章中,你将学习到一些脱围机制,让你可以“走出” React 并连接到外部系统。大多数应用逻辑和数据流不应该依赖这些功能。
ref–useRef
- 使用 ref 引用值
适用于:希望组件“记住”某些信息,但又不想让这些信息 触发新的渲染时。
设置 state 会重新渲染组件,而更改 ref 不会,可以通过 ref.current 属性访问该 ref 的当前值。
ref 就像组件的一个不被 React 追踪的秘密口袋。例如,可以使用 ref 来存储 timeout ID、DOM 元素 和其他不影响组件渲染输出的对象。 - 使用 ref 操作 DOM
由于 React 会自动更新 DOM 以匹配渲染输出,因此组件通常不需要操作 DOM。但是,有时可能需要访问由 React 管理的 DOM 元素——例如聚焦节点、滚动到此节点,以及测量它的尺寸和位置。React 没有内置的方法来执行此类操作,所以需要一个指向 DOM 节点的 ref 来实现。
import { useRef } from 'react';
export default function Form() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<input ref={inputRef} />
<button onClick={handleClick}>
聚焦输入框
</button>
</>
);
}
- 使用 Effect 进行同步
有些组件需要与外部系统同步。例如,可能需要根据 React 状态控制非 React 组件、设置服务器连接或在组件出现在屏幕上时发送分析日志。与处理特定事件的事件处理程序不同,Effect 在渲染后运行一些代码。使用它将组件与 React 之外的系统同步。
许多 Effect 也会自行“清理”。例如,与聊天服务器建立连接的 Effect 应该返回一个 cleanup 函数,告诉 React 如何断开组件与该服务器的连接
如果没有涉及到外部系统(例如,需要根据一些 props 或 state 的变化来更新一个组件的 state),不应该使用 Effect。移除不必要的 Effect 可以让代码更容易理解,运行得更快,并且更少出错。
有两种常见的不必使用 Effect 的情况:不必为了渲染而使用 Effect 来转换数据;不必使用 Effect 来处理用户事件;不需要 Effect 来根据其他状态调整某些状态;
import { useState, useRef, useEffect } from 'react';
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
}, [isPlaying]);
return <video ref={ref} src={src} loop playsInline />;
}
export default function App() {
const [isPlaying, setIsPlaying] = useState(false);
return (
<>
<button onClick={() => setIsPlaying(!isPlaying)}>
{isPlaying ? '暂停' : '播放'}
</button>
<VideoPlayer
isPlaying={isPlaying}
src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
/>
</>
);
}
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';
export default function ChatRoom() {
useEffect(() => {
const connection = createConnection();
connection.connect();
return () => connection.disconnect();//返回给react
//在开发环境中,React 将立即运行并额外清理一次 Effect。
}, []);
return <h1>欢迎前来聊天!</h1>;
}
- 响应式 Effect 的生命周期
Effect 的生命周期不同于组件。组件可以挂载、更新或卸载。Effect 只能做两件事:开始同步某些东西,然后停止同步它。如果 Effect 依赖于随时间变化的 props 和 state,这个循环可能会发生多次。
props 是 响应值,这意味着它们可以在重新渲染时改变。注意,如果 依赖值 更改,Effect 将会 重新同步(并重新连接到服务器)。 - 将事件从 Effect 中分开【官网正在更新中…】
//有了ref 不再需要e.target获取元素
//在函数组件中,ref 的使用也非常简单。使用 useRef 钩子来代替类组件中的 React.createRef()。
import React, { useRef } from 'react';
function FocusComponent() {
const inputRef = useRef(null); // 创建 ref
const focusInput = () => {
// 直接通过 ref 聚焦到输入框
inputRef.current.focus();
};
return (
<div>
<input
ref={inputRef}
type="text"
placeholder="Click the button to focus"
/>
<button onClick={focusInput}>Focus the Input</button>
</div>
);
}
export default FocusComponent;
- 为什么使用 ref 而不是 e.target?
- 简化代码:ref 让你可以直接访问到元素,而不需要通过 e.target 获取目标元素。这减少了代码的复杂度。
- 避免事件对象依赖:有时候事件对象(e.target)可能会带来不必要的复杂性,尤其是当事件处理函数比较复杂时,ref 提供了更清晰、更直接的访问方式。
- 更强大的 DOM 操作能力:ref 不仅仅用于获取值,它还可以用于获取元素的其他属性,如焦点、滚动位置等,甚至可以在需要时直接操作 DOM。
- 常见场景:使用 ref 的优势
访问 DOM 元素的值:例如,获取输入框的值,而不需要通过 e.target.value。
设置焦点:在用户交互后自动将焦点设置到某个输入框。
控制滚动条位置:直接操作某个容器的滚动条位置。
媒体控制:控制视频或音频元素的播放和暂停等。
实战
井字棋游戏
import { useState } from 'react';
function Square({ value, onSquareClick }) {
//解构了所以不再需要this.props.前缀
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
function Board({ xIsNext, squares, onPlay }) {
function handleClick(i) {
if (calculateWinner(squares) || squares[i]) {
return;
}
const nextSquares = squares.slice();//副本
if (xIsNext) {
nextSquares[i] = 'X';
} else {
nextSquares[i] = 'O';
}
onPlay(nextSquares);
}
const winner = calculateWinner(squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (xIsNext ? 'X' : 'O');
}
return (
//onSquareClick不能直接是 handleClick()否则会导致无限循环
<>
<div className="status">{status}</div>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
<Square value={squares[1]} onSquareClick={() => handleClick(1)} />
<Square value={squares[2]} onSquareClick={() => handleClick(2)} />
</div>
<div className="board-row">
<Square value={squares[3]} onSquareClick={() => handleClick(3)} />
<Square value={squares[4]} onSquareClick={() => handleClick(4)} />
<Square value={squares[5]} onSquareClick={() => handleClick(5)} />
</div>
<div className="board-row">
<Square value={squares[6]} onSquareClick={() => handleClick(6)} />
<Square value={squares[7]} onSquareClick={() => handleClick(7)} />
<Square value={squares[8]} onSquareClick={() => handleClick(8)} />
</div>
</>
);
}
export default function Game() {
const [history, setHistory] = useState([Array(9).fill(null)]);
const [currentMove, setCurrentMove] = useState(0);
const xIsNext = currentMove % 2 === 0;
const currentSquares = history[currentMove];
function handlePlay(nextSquares) {
const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
setHistory(nextHistory);
setCurrentMove(nextHistory.length - 1);
}
function jumpTo(nextMove) {
setCurrentMove(nextMove);
}
const moves = history.map((squares, move) => {
let description;
if (move > 0) {
description = 'Go to move #' + move;
} else {
description = 'Go to game start';
}
return (
<li key={move}>
<button onClick={() => jumpTo(move)}>{description}</button>
</li>
);
});
return (
<div className="game">
<div className="game-board">
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
</div>
<div className="game-info">
<ol>{moves}</ol>
</div>
</div>
);
}
function calculateWinner(squares) {
const lines = [//所有能赢得组合 三子连线
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
];
for (let i = 0; i < lines.length; i++) {
const [a, b, c] = lines[i];
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
return squares[a];
}//三个格子都不为空 且值相同 X或O
}
return null;
}
- history实现
把过去的 squares 数组存储在另一个名为 history 的数组中,把它存储为一个新的 state 变量。history 数组表示所有棋盘的 state,从第一步到最后一步
将 state 提升到顶层 Game 组件。这使 Game 组件可以完全控制 Board 的数据,并使它让 Board 渲染来自 history 的之前的回合。
形成一层套一层的结构。 - 跳转到某个历史
需要 Game 组件来跟踪用户当前正在查看的步骤。为此,定义一个名为 currentMove 的新 state 变量,默认为 0。
更新 Game 中的 jumpTo 函数来更新 currentMove。如果你将 currentMove 更改为偶数,你还将设置 xIsNext 为 true。
如果你“回到过去”然后从那一点开始采取新的行动,你只想保持那一点的历史。不是在 history 中的所有项目(… 扩展语法)之后添加 nextSquares,而是在 history.slice(0, currentMove + 1) 中的所有项目之后添加它,这样你就只保留旧历史的那部分。
每次落子时,你都需要更新 currentMove 以指向最新的历史条目。
原理
渲染
- 当父组件的 state 发生变化时,所有子组件都会自动重新渲染。这甚至包括未受变化影响的子组件。出于性能原因,希望跳过重新渲染显然不受其影响的树的一部分,故修改state时是创建副本再调用setState。不变性使得组件比较其数据是否已更改的成本非常低。
- 更新放在下一次渲染,当前值是快照;不会立即处理更新,等到事件处理函数中的 所有 代码都运行完毕再处理你的 state 更新;
- 不会触发太多的重新渲染,而是批处理。多个setstate会加入队列,在下一次渲染期间,React 会遍历队列并给你更新之后的最终 state。
- 只要一个组件还被渲染在 UI 树的相同位置,React 就会保留它的 state。 (即使是同个组件的不同实例)如果它被移除,或者一个不同的组件被渲染在相同的位置,那么 React 就会丢掉它的 state。
如果移除组件,其组件state会被销毁,再添加时重新初始化并添加到DOM中。 - React 不知道你的函数里是如何进行条件判断的,它只会“看到”你返回的树。如果两个同样的组件渲染到相同位置,会被认为是同一个。不同组件相同位置则重置state
- 永远要将组件定义在最上层并且不要把它们的定义嵌套起来。否则每次组件渲染,都会创建一个被嵌套的新组件,状态被重置。
- 有两个方法可以让同一个位置的同一组件的两个不同实例各自维护各自state:
将组件渲染在不同的位置 / 使用 key 赋予每个组件一个明确的身份
//相同位置
{isPlayerA ? (
<Counter person="Taylor" />
) : (
<Counter person="Sarah" />
)}
//不同位置
{isPlayerA &&
<Counter person="Taylor" />
}
{!isPlayerA &&
<Counter person="Sarah" />
}
但还是有个问题,如何为被移除的组件保留 state?
CSS隐藏,但性能差
状态提升
其他数据源 如localsStorage
虚拟DOM
虚拟DOM的概念和性能优化问题,涉及到现代前端框架(尤其是 React)如何高效地更新和渲染用户界面。
- 第一次和第二次渲染:
在 传统的 DOM 操作 中,每次你修改页面内容时,浏览器都会直接更新页面的 DOM 结构。这样做有两个主要问题:
性能开销:每次 DOM 更新都涉及到重排(Reflow)和重绘(Repaint),这些都是昂贵的操作,尤其在复杂页面上表现得尤为明显。
不必要的重新渲染:即使你只修改了页面的一部分,浏览器通常会重绘整个页面,浪费了很多计算资源。
因此,每次更新都需要生成完整的 DOM 片段,然后将这些片段直接应用到真实 DOM 上。这个过程会消耗性能,特别是在有大量节点更新的情况下。 - 升级版 1.0:性能提升不明显
升级版 1.0 可能指的是一些基于传统 DOM 操作的优化方法,比如批量更新,或者减少对 DOM 的访问次数。
但是,虽然这些方法可以减少操作的次数,仍然会有不少性能瓶颈。特别是:
DOM 更新依然是昂贵的。
仍然存在很多不必要的渲染和 DOM 操作。
因此,性能提升可能不如预期,仍然会遇到效率瓶颈。 - 升级版 2.0:虚拟 DOM 的引入
虚拟 DOM(Virtual DOM)是 React 等框架引入的核心优化技术。它的工作原理大致如下:
JSX -> JS对象 -> 真实 DOM
JSX 是一种语法糖,最终会被转译为 JavaScript 对象。这个对象就是虚拟 DOM(Virtual DOM)。它只是一个描述真实 DOM 的数据结构,而不是实际的 DOM 节点。
虚拟 DOM 计算和比较:当你的组件状态或属性发生变化时,React 并不会立刻修改真实的 DOM,而是会先创建一个新的虚拟 DOM 树。然后,React 会通过diff 算法比较新旧虚拟 DOM 树之间的差异。
只更新差异部分:React 通过比较虚拟 DOM 树的差异(差异化算法(Diffing Algorithm)),找出需要更新的部分,然后只将这些差异应用到真实的 DOM 上。这样可以避免每次渲染时完全重建 DOM,从而大大提高性能。
批量更新:React 的虚拟 DOM 使得多个状态更新操作可以被批量处理,而不是一次次地触发实际 DOM 更新,进一步减少了性能开销。
- 虚拟 DOM 的过程
初始渲染:当组件首次渲染时,React 会生成一个虚拟 DOM,并将其与真实 DOM 进行比较(这时虚拟 DOM 和真实 DOM 是一致的),然后将虚拟 DOM 渲染到浏览器中。
状态变化:当组件的状态变化时,React 会生成一个新的虚拟 DOM 树。React 会将新旧虚拟 DOM 树进行比较,找出差异部分。
更新 DOM:React 根据比较出的差异,最小化地更新真实 DOM,避免重新渲染整个页面。
全栈框架
不需要服务器
支持客户端渲染(CSR)、单页应用(SPA)和静态站点生成(SSG)。这些应用可以部署到 CDN 或静态托管服务(无需服务器)。此外,这些框架允许你根据实际需求,针对特定路由单独启用服务端渲染。
这意味着你可以从纯客户端应用开始构建,后续需求变更时,无需重写整个应用,即可在特定路由上启用服务端功能。