一、基本使用

1 Router、Route和Link 概念
1.1 Router

我们可以把它看做是react路由的一个路由外层盒子,它里面的内容就是我们单页面应用的路由以及路由组件

import { BrowserRouter as Router } from "react-router-dom";
class Main extends Component{
    render(){
        return(
            <Router>
                <div>
                    //otherCoding
                </div>
            </Router>
        )
    }
}
1.2 Link(类似Vue中的Router-link)

是react路由中的点击切换到哪一个组件的链接,(这里之所以不说是页面,而是说组件,因为切换到另一个界面只是展示效果,react的本质还是一个单页面应用-single page application)。

import { BrowserRouter as Router, Link} from "react-router-dom";
class Main extends Component{
    render(){
        return(
        <Router>
            <div>
                <ul>
                    <li><Link to='/'>首页</Link></li>
                    <li><Link to='/other'>其他页</Link></li>
                </ul>
            </div>
        </Router>
        )
    }
}

特别说明:

第一、Router下面只能包含一个盒子标签,类似这里的div。

第二、Link代表一个链接,在html界面中会解析成a标签。作为一个链接,必须有一个to属性,代表链接地址。这个链接地址是一个相对路径。

第三、Route,是下面要说的组件,有一个path属性和一个组件属性(可以是component、render等等)。

1.3 Routes(类似Vue中的Router-view)

Routes组件 
Routes功能上类似于Vue中的Router-view。将来匹配到的路由组件将会被加载到Routes所在的位置。
Routes组件内部存放Route组件。

Route组件
代表了你的路由界面,path代表路径,component代表路径所对应的界面。

import React,{ Component } from "react";
import { render } from "react-dom";
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
class Home extends Component{
    render(){
        return (
            <div>this a Home page</div>
        )
    }
}
class Other extends Component{
    render(){
        return (
            <div>this a Other page</div>
        )
    }
}
class Main extends Component{
    render(){
        return (
            <Router>
                <div>
                    <ul>
                        <li><Link to="/home">首页</Link></li>
                        <li><Link to="/other">其他页</Link></li>
                    </ul>
                    <Routes>
                        <Route path="/home" component={Home}/>
                        <Route path="/other" component={Other}/>
                    </Routes>
                </div>
            </Router>
        )
    }
}
render(<Main />,document.getElementById("root"));

路由的基本使用步骤:

1)安装react-router-dom

2)导入路由核心组件(当然我们要按需导入)

  • BrowserRouter as Router(Router就是BrowserRouter的别名)
  • Route
  • Link

3)使用Router组件包裹整个应用(重要)

4)使用Link组件作为导航菜单(路由入口),会被编译成a标签

<Link to="/first">页面1</Link>

5)使用Route组件配置路由规则和要展示的组件(路由出口)

<Route path="/first(和路由入口匹配)" component={要展示的组件的名}></Route>

但是我们的内容展示在哪呢?Route写在哪就把内容渲染在哪

路由执行过程: 

1)点击Link组件,修改浏览器地址栏URL

2)React路由监听到地址栏变化

3)React路由遍历所有Route组件,使用路由规则path与pathname匹配

4)当匹配成功就展示Route组件内容

编程式导航:通过JS代码实现页面的跳转

1.4 默认路由

进入页面就会展示的路由,进入页面就会被匹配到展示 。

<Route path='/'>
1.5 路由匹配模式 

1)模糊匹配模式

a)"/"所有pathname都可以被匹配

b)只要pathname是以path开头的都会匹配成功

c)pathname指得是Link组件中to属性的值

d)path指的是Route组件中path属性的值

2)精确匹配

a)给Route组件添加exact属性,让其变成精确匹配

b)只有pathname和name完全相同才会被展示

心得:React路由的一切都是组件、我们可以像思考组件一样去思考路由

2 首先安装依赖
npm install react-router-dom
npm install react-router-dom@6.22.2
3 引入实现路由所需的组件,以及页面组件

<Route />  路由出口

import { BrowserRouter, Routes, Route } from "react-router-dom";

import Foo from "./Foo";
import Bar from "./Bar";
function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/foo" element={<Foo />} />
        <Route path="/bar" element={<Bar />} />
      </Routes>
    </BrowserRouter>
  );
}
  • path:路径
  • element:要渲染的组件
  • 注意BrowserRouter 组件最好放在最顶层所有组件之外,这样能确保内部组件使用 Link 做路由跳转时不出错

二、路由跳转

在跳转路由时,如果路径是 /开头的则是绝对路由,否则为相对路由,即相对于当前 URL进行改变

1 Link 组件

Link 组件只能在 Router内部使用,因此使用到Link组件的组件一定要放在顶层的 Router 之内

import { Link } from "react-router-dom";
 
<Link to="foo">to foo</Link>;
  • Link:在单页应用程序(SPA)中提供导航,而不会导致页面重新加载。当用户点击链接时,React会阻止浏览器默认的页面刷新行为,并且使用 react-router 提供的导航方式,只更新 URL 并渲染对应的组件,从而实现单页面应用(SPA)的效果。。
  • a:单击时会导致完整页面重新加载,导航到新 URL
2 NavLink 组件
  • NavLink组件Link组件的功能是一致的,区别在于可以判断其to属性是否是当前匹配到的路由
  • NavLink组件styleclassName可以接收一个函数,函数接收一个含有isActive字段的对象为参数,可根据该参数调整样式
import { NavLink } from "react-router-dom";
 
function Foo() {
  return (
    <NavLink to="/foo" style={({ isActive }) => ({ color: isActive ? "red" : "#fff" })}>
      Click here
    </NavLink>
  );
}
3 编程式跳转 useNavigate

使用 useNavigate钩子函数生成navigate函数用于在不同路由之间导航的钩子,可以通过 JS 代码完成路由跳转

useNavigate取代了原先版本中的useHistory

import { useNavigate } from 'react-router-dom';
 
function Foo(){
    const navigate = useNavigate();
    return (
        // 上一个路径:/a;    当前路径: /a/a1
        <div onClick={() => navigate('/b')}>跳转到/b</div>
        <div onClick={() => navigate('a11')}>跳转到/a/a1/a11</div>
        <div onClick={() => navigate('../a2')}>跳转到/a/a2</div>
        <div onClick={() => navigate(-1)}>跳转到/a</div>
    )
}
  • 可以直接传入要跳转的目标路由(可以使用相对路径,语法和 JS 相同)
  • 传入-1表示后退
传参方式一:直接传入路径字符串

传参:

const navigate = useNavigate();
navigate('/destination?name=xxx');

接收:

const [searchParams,setSearchParams] = useSearchParams();
 
// 使用 URLSearchParams 提供的方法来获取参数
const name = searchParams.get('name');
传参方式二:使用 Location State

传参:

const navigate = useNavigate();
navigate('/destination', { state: { a:10 } });

接收:

const {state} = useLocation();
 
state里面就是{ a:10 }
传参方式三:使用查询字符串 

传参:

const navigate = useNavigate();
navigate('/destination/12');
 
需要对路由进行配置
path:'/destination/:id'

接收:

const { id } = useParams();
// 现在 id 变量包含了通过路由参数传递的参数值

三、动态路由参数

1 路径参数
  • Route组件中的path属性中定义路径参数
  • 在组件内通过useParams hook 访问路径参数
<BrowserRouter>
  <Routes>
    <Route path="/foo/:id" element={<Foo />} />
  </Routes>
</BrowserRouter>;

// useParams React Hook,用于获取当前路由的参数。它返回一个对象,包含了当前路由中定义的参数值。
import { useParams } from "react-router-dom";
export default function Foo() {
  const params = useParams();
  return (
    <div>
      <h1>{params.id}</h1>
    </div>
  );
}
路径匹配规则

当URL同时匹配到含有路径参数的路径和无参数路径时,有限匹配没有参数的"具体的"(specific)路径。

<Route path="teams/:teamId" element={<Team />} />
<Route path="teams/new" element={<NewTeamForm />} />

如上的两个路径,将会匹配 teams/new 

路径的正则匹配已被移除。

兼容类组件

在以前版本中,组件的props会包含一个match对象,在其中可以取到路径参数。

但在最新的 6.x 版本中,无法从 props 获取参数。

并且,针对类组件的 withRouter 高阶组件已被移除。因此对于类组件来说,使用参数有两种兼容方法:

  1. 将类组件改写为函数组件
  2. 自己写一个 HOC 来包裹类组件,用 useParams 获取参数后通过 props 传入原本的类组件
2 search 参数,查询参数不需要在路由中定义
  • 查询参数不需要在路由中定义
  • 使用 useSearchParams hook 来访问和修改查询参数。其用法和 useState 类似,会返回当前对象和更改它的方法
  • 使用 setSearchParams 时,必须传入所有的查询参数,否则会覆盖已有参数
import { useSearchParams } from "react-router-dom";
 
// 当前路径为 /foo?id=12
function Foo() {
  const [searchParams, setSearchParams] = useSearchParams();
  console.log(searchParams.get("id")); // 12
  setSearchParams({
    name: "foo",
  }); // /foo?name=foo
  return <div>foo</div>;
}

四、嵌套路由

1 路由定义

通过嵌套的书写Route组件实现对嵌套路由的定义。

path 开头为 / 的为绝对路径,反之为相对路径。

<Routes>
  <Route path="/" element={<Home />}></Route>
  <Route path="/father" element={<Father />}>
    <Route path="child" element={<Child />}></Route>
    <Route path=":name" element={<Another />}></Route>
  </Route>
</Routes>
2 在父组件中展示

在父组件中使用Outlet组件来显示匹配到的子组件

import { Outlet } from "react-router-dom";
function Father() {
  return (
    <div>
      // ... 自己组件的内容 // 留给子组件Child的出口
      <Outlet />
    </div>
  );
}
3 在组件中定义

可以在任何组件中使用 Routes 组件,且组件内的Routes中,路径默认带上当前组件的路径作为前缀。

注意:此时定义父组件的路由时,要在后面加上 /* ,否则父组件将无法渲染。

<Routes>
  <Route path="/" element={<Home />} />
  <Route path="dashboard/*" element={<Dashboard />} />
</Routes>
function Dashboard() {
  return (
    <div>
      <p>Look, more routes!</p>
      <Routes>
        <Route path="/" element={<DashboardGraphs />} />
        <Route path="invoices" element={<InvoiceList />} />
      </Routes>
    </div>
  );
}

五、默认路由

定义:在嵌套路由中,如果 URL 仅匹配了父级 URL,则Outlet中会显示带有index属性的子路由。可以使用在路由的任何层级

<Routes>
  <Route path="/foo" element={Foo}>
    <Route index element={Default}></Route>
    <Route path="bar" element={Bar}></Route>
  </Route>
</Routes>
  • 当 url 为/foo 时:Foo 中的 Outlet 会显示 Default 组件
  • 当 url 为/foo/bar时:Foo 中的 Outlet 会显示为 Bar 组件

六、全匹配路由

定义: path属性取值为*时,可以匹配任何(非空)路径,该匹配拥有最低的优先级。可以用于设置 404 页面。

<Routes>
  <Route path="/foo" element={Foo}>
    <Route path="bar" element={Bar}></Route>
    <Route path="*" element={NotFound}></Route>
  </Route>
</Routes>

七、多组路由

通常,一个应用中只有一个Routes组件。

但根据实际需要也可以定义多个路由出口(如:侧边栏和主页面都要随 URL 而变化)

<Router>
  <SideBar>
    <Routes>
      <Route></Route>
    </Routes>
  </SideBar>
  <Main>
    <Routes>
      <Route></Route>
    </Routes>
  </Main>
</Router>

八、路由重定向 Navigate

当在某个路径/a下,要重定向到路径/b时,可以通过Navigate组件进行重定向到其他路径

等价于以前版本中的 Redirect组件

import { Navigate } from "react-router-dom";
function A() {
  return <Navigate to="/b" />;
}

九、布局路由

当多个路由有共同的父级组件时,可以将父组件提取为一个没有 path 和 index 属性的Route组件(Layout Route)

<Route element={<PageLayout />}>
    <Route path="/privacy" element={<Privacy />} />
    <Route path="/tos" element={<Tos />} />
</Route>

这种写法等价于:

<Route
  path="/privacy"
  element={
    <PageLayout>
      <Privacy />
    </PageLayout>
  }
/>
<Route
  path="/tos"
  element={
    <PageLayout>
      <Tos />
    </PageLayout>
  }
/>

十、订阅和操作 history stack的原理

浏览器会记录导航堆栈,以实现浏览器中的前进后退功能。在传统的前端项目中,URL的改变意味着向服务器重新请求数据。

在现在的客户端路由( client side routing )中,可以做到编程控制URL改变后的反应。如在点击a标签的回调函数中使用 event.preventDefault() 阻止默认事件,此时URL的改变不会带来任何UI上的更新。

<a
  href="/contact"
  onClick={(event) => {
    // stop the browser from changing the URL and requesting the new document
    event.preventDefault();
    // push an entry into the browser history stack and change the URL
    window.history.pushState({}, undefined, "/contact");
  }}
/>
1 History对象

浏览器没有直接提供监听URL改变(push、pop、replace)的接口,因此 react-router 对原生的 history 对线进行了包装,提供了监听URL改变的API。

let history = createBrowserHistory();
history.listen(({ location, action }) => {
  // this is called whenever new locations come in
  // the action is POP, PUSH, or REPLACE
});

使用 react-router 时不需操作History对象(Routes 组件会进行操作)

2 location对象

react-router 对 window.location 进行包装后,提供了一个形式简洁的location对象,形如:

{
  pathname: "/bbq/pig-pickins",     // 主机名之后的URL地址
  search: "?campaign=instagram",    // 查询参数
  hash: "#menu",                    // 哈希值,用于确定页面滚动的具体位置
  state: null,                      // 对于 window.history.state 的包装
  key: "aefz24ie"                   // 
}
state

不显示在页面上,不会引起刷新,只由开发人员操作。

可用于记录用户的跳转详情(从哪跳到当前页面)或在跳转时携带信息。

可以用在 Link 组件或 navigate 方法中

<Link to="/pins/123" state={{ fromDashboard: true }} />
let navigate = useNavigate();
navigate("/users/123", { state: partialUser });

在目标的组件中,可以用 useLocation 方法获取该对象

let location = useLocation();
console.log(location.state);

state中的信息会进行序列化,因此如日期对象等信息会变为string

key

每个Location对象拥有一个唯一的key,可以据此来实现基于Location的滚动管理,或是数据缓存。

如:将 location.key 和 URL 作为键,每次请求数据前,先查找缓存是否存在来判断是否实际发送请求,来实现客户端数据缓存。

十一、 各类Router组件

1 HashRouter和BrowserRouter的区别
  • HashRouter 只会修改URL中的哈希值部分;而 BrowserRouter 修改的是URL本身
  • HashRouter 是纯前端路由,可以通过输入URL直接访问;使用时 BrowserRouter 直接输入URL会显示404,除非配置Nginx将请求指向对应的HTML文件。初次进入 / 路径时或点击 Link 组件跳转时不会发送请求
2 unstable_HistoryRouter

使用 unstable_HistoryRouter 需要传入一个 history 库的实例,这将允许在非react作用于下操作history对象。

由于项目使用的history和react-router中使用的history版本可能不一样,该API目前标为unstable状态

3 MemoryRouter

HashRouter 和 BrowserRouter 都是依据外部对象(history)进行导航,而 MemoryRouter 则是自己存储和管理状态堆栈,多用于测试场景。

4 NativeRouter

推荐的用于 React Native的Router组件

5 StaticRouter

在nodejs端使用,渲染react应用。

import * as React from "react";
import * as ReactDOMServer from "react-dom/server";
import { StaticRouter } from "react-router-dom/server";
import http from "http";
 
function requestHandler(req, res) {
  let html = ReactDOMServer.renderToString(
    <StaticRouter location={req.url}>
      {/* The rest of your app goes here */}
    </StaticRouter>
  );
 
  res.write(html);
  res.end();
}
 
http.createServer(requestHandler).listen(3000);

十二、使用JS对象定义路由:useRoutes 路由表(推荐)

使用 useRoutes hook,可以使用一个JS对象而不是Routes组件与Route组件来定义路由。其功能类似于react-router-config

useRoutes 的返回是 React Element,或是 null。(可以将它理解为Routes组件)

对于传入的配置对象, 其类型定义如下:

interface RouteObject {
    caseSensitive?: boolean;
    children?: RouteObject[];
    element?: React.ReactNode;
    index?: boolean;
    path?: string;
}

React Router 是 React 应用程序中用于导航的重要库之一。在 v6.22 版本中,它引入了 useRoutes 钩子,使得路由配置更加灵活。本文将介绍如何在 React 应用程序中使用 useRoutes 钩子来管理路由。

1 创建路由配置

在使用 useRoutes 之前,需要定义路由配置。通常,我们将路由配置放在单独的文件中,然后通过 useRoutes 来使用它。例如,我们可以创建一个 routes 文件来定义路由:

// routes/index.js
import { Navigate } from "react-router-dom";
import About from "../About";
import Home from "../Home";

const routes = [
  {
    path: '/about',
    element: <About />,
  },
  {
    path: '/home',
    element: <Home />,
  },
  {
    path: '/',
    // Navigate组件  重定向
    element: <Navigate to="/about" />,
  },
];

export default routes;
2 使用 useRoutes

接下来,在组件中使用 useRoutes 钩子来渲染路由配置:

// Demo1.jsx
import React from 'react';
import { NavLink, useRoutes } from 'react-router-dom';
import routes from './routes';

function Demo1() {
  const routeElement = useRoutes(routes);

  return (
    <div>
      <div className="row">
        <ul className="nav nav-pills">
          <li className="nav-item">
            <NavLink className="nav-link" to="/about">About</NavLink>
          </li>
          <li className="nav-item">
            <NavLink className="nav-link" to="/home">Home</NavLink>
          </li>
        </ul>
      </div>
      <div className="panel">
        {routeElement}
      </div>
    </div>
  );
}

export default Demo1;

通过以上步骤,我们可以在 React 应用程序中使用 useRoutes 钩子来管理路由配置。这种方式使得路由的定义和渲染更加简单和灵活,有助于更好地组织和维护项目中的路由结构。

十三、Lazy 和 Suspense(路由懒加载)

 1、React.lazy 定义

 React.lazy() 函数能让你像渲染常规组件一样处理动态引入的组件。

什么意思呢?其实就是懒加载。其原理就是利用es6 import()函数。这个import不是import命令。同样是引入模块,import命令是同步引入模块,而import()函数动态引入

当 Webpack 解析到该语法时,它会自动地开始进行代码分割(Code Splitting),分割成一个文件,当使用到这个文件的时候会这段代码才会被异步加载。

(1) 为什么代码要分割

当你的程序越来越大,代码量越来越多。一个页面上堆积了很多功能,也许有些功能很可能都用不到,但是一样下载加载到页面上,所以这里面肯定有优化空间。就如图片懒加载的理论。

(2) import()函数 -- 异步

import静态export命令只能在模块的顶层不能在代码块之中(比如,在if代码块之中,或在函数之中)。

import()函数与所加载的模块没有静态连接关系,这点也是与import语句不相同。import()类似于 Node.js 的require()方法,区别主要是前者是异步加载,后者是同步加载。

//import命令 -- 同步,编译阶段
import { add } from './math.js';
console.log(add(16, 26));

//import函数 -- 异步,执行阶段
//由于import()返回 Promise 对象,所以需要使用then()方法指定处理函数。
import("./math.js").then(math => {
  console.log(math.add(16, 26));
});
//考虑到代码的清晰,更推荐使用await命令。
async function addFun() {
  const math = await import("./math");
  math.add(16, 26);
}

addFun();

动态 import() 语法目前只是一个 ECMAScript (JavaScript) 提案, 而不是正式的语法标准。预计在不远的将来就会被正式接受。ES6 入门教程

(3) import函数示例

下面是import一个示例:

在test文件夹下新建两个文件

// test.html
<div id="root">
  页面无内容
</div>
<button id="btn">加载js</button>

<script>
  document.getElementById('btn').onclick=function(){
    import('./test.js').then(d=>{
      d.test()
    })
  }
</script>
// test.js
function test(){
  document.getElementById('root')
  root.innerHTML='页面变的有内容了'
}
export {test}

这时候打开web服务让页面以http的方式访问, 我们在chrome的开发者工具下的Network可以看到只请求了一个页面。

但是当我们点击加载js,你会发现test.js会以动态的方式加入到代码中,同时执行了test函数,使页面的内容发生了变化。

React.lazy和常用的三方包react-loadable,都是使用了这个原理,然后配合webpack进行代码打包拆分达到异步加载,这样首屏渲染的速度将大大的提高。

注意:由于React.lazy不支持服务端渲染,所以这时候react-loadable就是不错的选择。

react-loadable 代码分割和懒加载
npm i react-loadable -save

React Loadable是一个高阶组件,它的主要任务是帮助你在运行时动态地加载React组件。通过配合Webpack 2+的动态导入特性,你可以方便地将大块代码进行拆分,只在必要的时候加载,从而显著降低首次加载时的页面大小。

React Loadable的核心在于提供了一个Loadable函数,这个函数接受一个配置对象,其中包含了模块加载器和加载失败时的处理组件。当你调用Loadable时,它会返回一个新的组件,该组件会在实际渲染之前异步加载对应的模块。这意味着你可以把代码分割点设置到任何你想分离的地方,不仅仅是路由层。

const ErrorComponent = () => (
  <div>
    <h1>Error loading component</h1>
    <p>Please try reloading the page.</p>
  </div>
);
export default ErrorComponent 
import Loadable from 'react-loadable';
import Loading from './my-loading-component';
import ErrorComponent from './my-error-component';

const LoadableComponent = Loadable({
  loader: () => import('./my-component'),
  // loading 在加载期间显示的组件
  loading: Loading,
  // delay 在开始显示加载组件前的延迟时间(毫秒)。默认值是 200ms,这里设置为相同的值。
  // delay: 200,
  // timeout 如果加载超过这个时间(毫秒),则会抛出一个错误。这里设置为 10000ms(10 秒)
  // timeout: 10000,
  error: ErrorComponent, // 自定义的错误组件
});

export default class App extends React.Component {
  render() {
    return <LoadableComponent/>;
  }
}
路由表配置项开发路由 
import React from 'react'
import { createBrowserRouter, Navigate } from "react-router-dom";
import Loadable from "react-loadable"
import Loading from './my-loading-component';
 
// 路由配置项
let routes= [
    {
        name: 'home',
        path: '/',
        exact: true,
        component: Loadable({
            loader: () => import('./my-component'),
            // loading: () => <div />,
            loading: () => Loading,
        })
    },
    
]
 
export default createBrowserRouter(routes)  
2、如何使用React.lazy

下面示例代码使用create-react-app脚手架搭建:

//OtherComponent.js 文件内容

import React from 'react'
const OtherComponent = ()=>{
  return (
    <div>
      我已加载
    </div>
  )
}
export default OtherComponent

// App.js 文件内容
import React from 'react';
import './App.css';

//使用React.lazy导入OtherComponent组件
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function App() {
  return (
    <div className="App">
      <OtherComponent/>
    </div>
  );
}
export default App;

这是最简单的React.lazy,但是这样页面会报错。这个报错提示我们,在React使用了lazy之后,会存在一个加载中的空档期,React不知道在这个空档期中该显示什么内容,所以需要我们指定。接下来就要使用到Suspense

(1) Suspense 解决异步空挡显示问题

如果在 App 渲染完成后,包含 OtherComponent 的模块还没有被加载完成,我们可以使用加载指示器为此组件做优雅降级。这里我们使用 Suspense 组件来解决。

这里将App组件改一改

import React, { Suspense, Component } from 'react';
import './App.css';

//使用React.lazy导入OtherComponent组件
const OtherComponent = React.lazy(() => import('./OtherComponent'));

export default class App extends Component {
  state = {
    visible: false
  }
  render() {
    return (
      <div className="App">
        <button onClick={() => {
          this.setState({ visible: true })
        }}>
          加载OtherComponent组件
        </button>
        <Suspense fallback={<div>Loading...</div>}>
          {
            this.state.visible
              ?
              <OtherComponent />
              :
              null
          }
        </Suspense>
      </div>
    )
  }
}

我们指定了空档期使用Loading展示在界面上面,等OtherComponent组件异步加载完毕,把OtherComponent组件的内容替换掉Loading上。

注意Suspense使用的时候,fallback一定是存在且有内容的, 否则会报错。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐