react比较重要的一些
React 列表 & Keys
我们可以使用 JavaScript 的 map() 方法来创建列表。
React 实例
使用 map() 方法遍历数组生成了一个 1 到 5 的数字列表:
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((numbers) =>
<li>{numbers}</li>
);
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<ul>{listItems}</ul>
);
我们可以将以上实例重构成一个组件,组件接收数组参数,每个列表元素分配一个 key,不然会出现警告 a key should be provided for list items,意思就是需要包含 key:
React 实例
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<li key={number.toString()}>
{number}
</li>
);
return (
<ul>{listItems}</ul>
);
}
const numbers = [1, 2, 3, 4, 5];
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<NumberList numbers={numbers} />
);
Keys
Keys 可以在 DOM 中的某些元素被增加或删除的时候帮助 React 识别哪些元素发生了变化。因此你应当给数组中的每一个元素赋予一个确定的标识。
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
<li key={number.toString()}>
{number}
</li>
);
一个元素的 key 最好是这个元素在列表中拥有的一个独一无二的字符串。通常,我们使用来自数据的 id 作为元素的 key:
const todoItems = todos.map((todo) =>
<li key={todo.id}>
{todo.text}
</li>
);
当元素没有确定的 id 时,你可以使用他的序列号索引 index 作为 key:
const todoItems = todos.map((todo, index) =>
// 只有在没有确定的 id 时使用
<li key={index}>
{todo.text}
</li>
);
如果列表可以重新排序,我们不建议使用索引来进行排序,因为这会导致渲染变得很慢。
用keys提取组件
元素的 key 只有在它和它的兄弟节点对比时才有意义。
比方说,如果你提取出一个 ListItem 组件,你应该把 key 保存在数组中的这个 <ListItem />
元素上,而不是放在 ListItem 组件中的 <li>
元素上。
错误的示范
function ListItem(props) {
const value = props.value;
return (
// 错啦!你不需要在这里指定key:
<li key={value.toString()}>
{value}
</li>
);
}
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
//错啦!元素的key应该在这里指定:
<ListItem value={number} />
);
return (
<ul>
{listItems}
</ul>
);
}
const numbers = [1, 2, 3, 4, 5];
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<NumberList numbers={numbers} />
);
key的正确使用方式
React 实例
function ListItem(props) {
// 对啦!这里不需要指定key:
return <li>{props.value}</li>;
}
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
// 又对啦!key应该在数组的上下文中被指定
<ListItem key={number.toString()}
value={number} />
);
return (
<ul>
{listItems}
</ul>
);
}
const numbers = [1, 2, 3, 4, 5];
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<NumberList numbers={numbers} />
);
元素的 key 在他的兄弟元素之间应该唯一
数组元素中使用的 key 在其兄弟之间应该是独一无二的。然而,它们不需要是全局唯一的。当我们生成两个不同的数组时,我们可以使用相同的键。
React 实例
function Blog(props) {
const sidebar = (
<ul>
{props.posts.map((post) =>
<li key={post.id}>
{post.title}
</li>
)}
</ul>
);
const content = props.posts.map((post) =>
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.content}</p>
</div>
);
return (
<div>
{sidebar}
<hr />
{content}
</div>
);
}
const posts = [
{id: 1, title: 'Hello World', content: 'Welcome to learning React!'},
{id: 2, title: 'Installation', content: 'You can install React from npm.'}
];
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<Blog posts={posts} />
);
key 会作为给 React 的提示,但不会传递给你的组件。如果您的组件中需要使用和 key 相同的值,请将其作为属性传递:
const content = posts.map((post) =>
<Post
key={post.id}
id={post.id}
title={post.title} />
);
上面例子中,Post 组件可以读出 props.id,但是不能读出 props.key。
在 jsx 中嵌入 map()
在上面的例子中,我们声明了一个单独的 listItems 变量并将其包含在 JSX 中:
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<ListItem key={number.toString()}
value={number} />
);
return (
<ul>
{listItems}
</ul>
);
}
JSX 允许在大括号中嵌入任何表达式,所以我们可以在 map() 中这样使用:
React 实例
function NumberList(props) {
const numbers = props.numbers;
return (
<ul>
{numbers.map((number) =>
<ListItem key={number.toString()}
value={number} />
)}
</ul>
);
}
这么做有时可以使你的代码更清晰,但有时这种风格也会被滥用。就像在 JavaScript 中一样,何时需要为了可读性提取出一个变量,这完全取决于你。但请记住,如果一个 map() 嵌套了太多层级,那你就可以提取出组件。
React 组件 API
React 组件 API 涉及多个重要的方面,包括生命周期方法、状态管理、属性传递和事件处理。
以下是 React 组件 API 的详细说明:
生命周期方法
React 组件的生命周期方法分为三个主要阶段:挂载、更新和卸载,详细说明参见:React 组件的生命周期 。
挂载阶段
- constructor(props): 构造函数,用于初始化状态或绑定方法。
- static getDerivedStateFromProps(props, state): 每次在调用 render 方法之前调用,用于更新状态。
- componentDidMount(): 组件挂载后调用,此时可以进行 DOM 操作或数据请求。
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.reset) {
return { count: 0 };
}
return null;
}
componentDidMount() {
console.log('Component mounted');
}
render() {
return <div>{this.state.count}</div>;
}
}
更新阶段
- static getDerivedStateFromProps(props, state): 与挂载阶段相同,用于更新状态。
- shouldComponentUpdate(nextProps, nextState): 返回布尔值,决定组件是否重新渲染。
- render(): 渲染组件的 UI。
- getSnapshotBeforeUpdate(prevProps, prevState): 在 DOM 更新之前调用,用于捕获一些信息(如滚动位置)。
- componentDidUpdate(prevProps, prevState, snapshot): 在组件更新后调用。
实例
class MyComponent extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
return nextState.count !== this.state.count;
}
getSnapshotBeforeUpdate(prevProps, prevState) {
return { scrollPosition: window.scrollY };
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (snapshot) {
window.scrollTo(0, snapshot.scrollPosition);
}
console.log('Component updated');
}
render() {
return <div>{this.state.count}</div>;
}
}
卸载阶段
componentWillUnmount(): 组件即将卸载时调用,用于清理资源(如定时器、事件监听等)。
实例
class MyComponent extends React.Component {
componentWillUnmount() {
console.log('Component will unmount');
}
render() {
return <div>{this.state.count}</div>;
}
}
状态管理
状态是一个组件内部的数据,使用 this.state 来定义和管理。通过 setState 方法更新状态。
实例
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
increment = () => {
this.setState((prevState) => ({ count: prevState.count + 1 }));
};
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}
属性传递
通过 this.props 访问传递给组件的属性,可以使用 PropTypes 进行类型检查。
实例
import PropTypes from 'prop-types';
class MyComponent extends React.Component {
render() {
return <div>{this.props.title}</div>;
}
}
MyComponent.propTypes = {
title: PropTypes.string.isRequired,
};
// 使用组件并传递属性
<MyComponent title="Hello, World!" />
事件处理
通过事件处理函数处理用户交互。需要使用 .bind(this) 或箭头函数来确保 this 指向正确。
实例
class MyComponent extends React.Component {
handleClick = () => {
console.log('Button clicked');
};
render() {
return <button onClick={this.handleClick}>Click me</button>;
}
}
条件渲染
通过条件语句控制组件的渲染。
实例
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { isVisible: true };
}
toggleVisibility = () => {
this.setState((prevState) => ({ isVisible: !prevState.isVisible }));
};
render() {
return (
<div>
{this.state.isVisible && <p>This is visible</p>}
<button onClick={this.toggleVisibility}>Toggle</button>
</div>
);
}
}
列表渲染
通过数组的 map 方法渲染列表。
实例
class MyComponent extends React.Component {
render() {
const items = ['Item 1', 'Item 2', 'Item 3'];
return (
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
);
}
}
受控组件
通过状态控制表单元素的值。
实例
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { value: '' };
}
handleChange = (event) => {
this.setState({ value: event.target.value });
};
handleSubmit = (event) => {
event.preventDefault();
console.log('Submitted value:', this.state.value);
};
render() {
return (
<form onSubmit={this.handleSubmit}>
<input type="text" value={this.state.value} onChange={this.handleChange} />
<button type="submit">Submit</button>
</form>
);
}
}
设置状态:setState
setState 是 React 中用于更新组件状态的方法。
语法格式如下:
setState(object nextState[, function callback])
参数说明
- object nextState: 一个对象,包含要更新的状态键值对。React 会将这个对象合并到当前状态中。
- function callback: 一个可选的回调函数,会在状态更新并重新渲染完成后执行。
合并 nextState 和当前 state,并重新渲染组件。
setState 是 React 事件处理函数中和请求回调函数中触发 UI 更新的主要方法。
关于setState
不能在组件内部通过 this.state 修改状态,因为该状态会在调用 setState() 后被替换。
setState() 并不会立即改变 this.state,而是创建一个即将处理的 state。setState() 并不一定是同步的,为了提升性能 React 会批量执行 state 和 DOM 渲染。
setState()总是会触发一次组件重绘,除非在 shouldComponentUpdate() 中实现了一些条件渲染逻辑。
通过合理使用 setState,可以有效地管理组件状态,并确保在状态更新后执行必要的操作,从而提高应用的响应性和可靠性。
React 实例
class Counter extends React.Component{
constructor(props) {
super(props);
this.state = {clickCount: 0};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(function(state) {
return {clickCount: state.clickCount + 1};
});
}
render () {
return (<h2 onClick={this.handleClick}>点我!点击次数为: {this.state.clickCount}</h2>);
}
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<Counter />
);
React AJAX
在 React 应用中实现 AJAX 请求,通常可以使用 fetch API 或者第三方库如 axios、jquery 等库来进行网络请求。
以下是使用这两种方法的基本说明:
使用 fetch API 进行 AJAX 请求
fetch 是一个在浏览器中内置的 API,用于发起网络请求。以下是使用 fetch 在 React 组件中发起 AJAX 请求的基本步骤:
- 引入 fetch - 如果你使用的是现代浏览器,通常不需要引入 fetch。但是,如果你需要支持旧浏览器,可以使用 unfetch 这样的 polyfill。
- 使用 useEffect 钩子 - 利用 useEffect 钩子来处理副作用,比如发起网络请求。
- 发起请求 - 使用 fetch 发起 GET 或 POST 请求。
- 处理响应 - 使用 .then() 来处理响应数据。
- 错误处理 - 使用 .catch() 来捕获并处理错误。
- 更新状态 - 使用 useState 钩子来存储请求的数据,并在请求成功后更新状态。
import React, { useState, useEffect } from 'react';
const MyComponent = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/data');
const result = await response.json();
setData(result);
setLoading(false);
} catch (error) {
console.error('Error fetching data:', error);
}
};
fetchData();
}, []);
if (loading) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Data from API:</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
};
export default MyComponent;
说明:
-
useState 和 useEffect:使用 useState 来存储 AJAX 请求返回的数据 (data) 和加载状态 (loading),并使用 useEffect 在组件加载后执行 AJAX 请求。
-
fetch:使用 fetch 发起 GET 请求,并使用 response.json() 将返回的 JSON 数据解析为 JavaScript 对象。
-
异步处理:使用 async/await 来处理异步操作,确保在请求完成后更新状态。
-
错误处理:在 fetch 请求中使用 try/catch 来捕获和处理可能发生的错误。
使用 axios
axios 是一个基于 Promise 的 HTTP 客户端,适用于浏览器和 Node.js。
以下是使用 axios 的基本步骤:
- 安装 axios - 通过 npm 或 yarn 安装 axios。
npm install axios
- 引入 axios - 在组件中引入 axios。
- 发起请求 - 使用 axios 发起 GET 或 POST 请求。
- 处理响应和错误 - 使用 .then() 和 .catch() 来处理响应和捕获错误。
- 更新状态 - 与使用 fetch 类似,使用 useState 更新状态。
实例
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const MyComponent = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
const response = await axios.get('https://api.example.com/data');
setData(response.data);
setLoading(false);
} catch (error) {
console.error('Error fetching data:', error);
}
};
fetchData();
}, []);
if (loading) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Data from API:</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
};
export default MyComponent;
使用 jQuery 在线测试
以下实例演示了获取 Github 用户最新 gist 共享描述:
React 组件的数据可以通过 componentDidMount 方法中的 Ajax 来获取,当从服务端获取数据时可以将数据存储在 state 中,再用 this.setState 方法重新渲染 UI。
当使用异步加载数据时,在组件卸载前使用 componentWillUnmount 来取消未完成的请求。
React 实例
class UserGist extends React.Component {
constructor(props) {
super(props);
this.state = {username: '', lastGistUrl: ''};
}
componentDidMount() {
this.serverRequest = $.get(this.props.source, function (result) {
var lastGist = result[0];
this.setState({
username: lastGist.owner.login,
lastGistUrl: lastGist.html_url
});
}.bind(this));
}
componentWillUnmount() {
this.serverRequest.abort();
}
render() {
return (
<div>
{this.state.username} 用户最新的 Gist 共享地址:
<a href={this.state.lastGistUrl}>{this.state.lastGistUrl}</a>
</div>
);
}
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<UserGist source="https://api.github.com/users/octocat/gists" />
);
React 表单与事件
本章节我们将讨论如何在 React 中使用表单。
HTML 表单元素与 React 中的其他 DOM 元素有所不同,因为表单元素生来就保留一些内部状态。
在 HTML 当中,像 <input>
, <textarea>
, 和 <select>
这类表单元素会维持自身状态,并根据用户输入进行更新。但在React中,可变的状态通常保存在组件的状态属性中,并且只能用 setState() 方法进行更新。
一个简单的实例
在实例中我们设置了输入框 input 值 value = {this.state.data}。在输入框值发生变化时我们可以更新 state。我们可以使用 onChange 事件来监听 input 的变化,并修改 state。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>菜鸟教程 React 实例</title>
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/react/18.2.0/umd/react.production.min.js" ></script>
<script src="https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/babel-standalone/6.26.0/babel.min.js" ></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
class Content extends React.Component {
render() {
return <div>
<input type="text" value={this.props.myDataProp} onChange={this.props.updateStateProp} />
<h4>{this.props.myDataProp}</h4>
</div>;
}
}
class HelloMessage extends React.Component {
constructor(props) {
super(props);
this.state = {value: 'Hello Runoob!'};
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
render() {
var value = this.state.value;
return <div>
<Content myDataProp = {value}
updateStateProp = {this.handleChange}></Content>
</div>;
}
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<HelloMessage />
);
</script>
</body>
</html>
上面的代码将渲染出一个值为 Hello Runoob! 的 input 元素,并通过 onChange 事件响应更新用户输入的值。
实例 2
在以下实例中我们将为大家演示如何在子组件上使用表单。 onChange 方法将触发 state 的更新并将更新的值传递到子组件的输入框的 value 上来重新渲染界面。
你需要在父组件通过创建事件句柄 (handleChange) ,并作为 prop (updateStateProp) 传递到你的子组件上
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>菜鸟教程 React 实例</title>
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/react/18.2.0/umd/react.production.min.js" ></script>
<script src="https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/babel-standalone/6.26.0/babel.min.js" ></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
class Content extends React.Component {
render() {
return (
<div>
<input type="text" value={this.props.myDataProp} onChange={this.props.updateStateProp} />
<h4>{this.props.myDataProp}</h4>
</div>
);
}
}
class HelloMessage extends React.Component {
constructor(props) {
super(props);
this.state = { value: 'Hello Runoob!' };
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
this.setState({ value: event.target.value });
}
render() {
var value = this.state.value;
return (
<div>
<Content myDataProp={value} updateStateProp={this.handleChange} />
</div>
);
}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<HelloMessage />);
</script>
</body>
</html>
Select 下拉菜单
在 React 中,不使用 selected 属性,而在根 select 标签上用 value 属性来表示选中项。
React 实例
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>菜鸟教程 React 实例</title>
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/react/18.2.0/umd/react.production.min.js" ></script>
<script src="https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/babel-standalone/6.26.0/babel.min.js" ></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
class FlavorForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: 'coconut'};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
alert('Your favorite flavor is: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
选择您最喜欢的网站
<select value={this.state.value} onChange={this.handleChange}>
<option value="gg">Google</option>
<option value="rn">Runoob</option>
<option value="tb">Taobao</option>
<option value="fb">Facebook</option>
</select>
</label>
<input type="submit" value="提交" />
</form>
);
}
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<FlavorForm />
);
</script>
</body>
</html>
多个表单
当你有处理多个 input 元素时,你可以通过给每个元素添加一个 name 属性,来让处理函数根据 event.target.name 的值来选择做什么。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>菜鸟教程 React 实例</title>
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/react/18.2.0/umd/react.production.min.js" ></script>
<script src="https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/babel-standalone/6.26.0/babel.min.js" ></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
class Reservation extends React.Component {
constructor(props) {
super(props);
this.state = {
isGoing: true,
numberOfGuests: 2
};
this.handleInputChange = this.handleInputChange.bind(this);
}
handleInputChange(event) {
const target = event.target;
const value = target.type === 'checkbox' ? target.checked : target.value;
const name = target.name;
this.setState({
[name]: value
});
}
render() {
return (
<form>
<label>
是否离开:
<input
name="isGoing"
type="checkbox"
checked={this.state.isGoing}
onChange={this.handleInputChange} />
</label>
<br />
<label>
访客数:
<input
name="numberOfGuests"
type="number"
value={this.state.numberOfGuests}
onChange={this.handleInputChange} />
</label>
</form>
);
}
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<Reservation />
);
</script>
</body>
</html>
React 事件
以下实例演示通过 onClick 事件来修改数据:
React 实例
class HelloMessage extends React.Component {
constructor(props) {
super(props);
this.state = {value: 'Hello Runoob!'};
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
this.setState({value: '菜鸟教程'})
}
render() {
var value = this.state.value;
return <div>
<button onClick={this.handleChange}>点我</button>
<h4>{value}</h4>
</div>;
}
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<HelloMessage />
);
当你需要从子组件中更新父组件的 state 时,你需要在父组件通过创建事件句柄 (handleChange) ,并作为 prop (updateStateProp) 传递到你的子组件上。实例如下:
React 实例
class Content extends React.Component {
render() {
return <div>
<button onClick = {this.props.updateStateProp}>点我</button>
<h4>{this.props.myDataProp}</h4>
</div>
}
}
class HelloMessage extends React.Component {
constructor(props) {
super(props);
this.state = {value: 'Hello Runoob!'};
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
this.setState({value: '菜鸟教程'})
}
render() {
var value = this.state.value;
return <div>
<Content myDataProp = {value}
updateStateProp = {this.handleChange}></Content>
</div>;
}
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<HelloMessage />
);
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现
2020-11-26 奶牛(树状数组)