react开发框架搭建方案一条龙create-react-app2.x + mobx + axios + antd + less3.x + react router4.x + webpack4....

写在前面:

  • 以下creact-react-app以下简称CRA
  • 本文均使用yard进行包安装及项目启动和打包,喜欢使用npm的同学,可自行换成npm命令
  • 本项目实现了基于CRA2.x搭建的基本项目框架,并实现了一个仿腾讯天气的页面
  • 本文的基础框架搭建可用于基于企业级后台管理系统搭建
  • 笔者搭建环境macOS Mojave10.14.1,node版本8.12.0,IDE是VSCode
  • 前期准备需要node环境,node环境准备请自行搜索,这个不在本文说明范围内
  • 如果是CRA或者React初学者,请务必在GitHub上clone源码对照文章进行学习
  • 本项目旨在记录CRA2.x企业级项目框架搭建过程,顺便在闲暇时间写了一个仿腾讯天气的页面,主要为了实际在项目中运用antd,mobx和echarts-for-react这三个很重要的库的使用
  • 本文所有环节都有注明必学还是选学,有基础的读者权当参考,若无基础的读者,请无比在GitHub上clone代码,对照文章进行学习(再次强调!)
  • 本项目GitHub地址
  • 本项目DEMO
    (ps.项目最主要的bug在于服务器,因为天气预报系统,用到了ip定位,我在开发阶段使用了proxy配置的代理,没有问题,但是挂载到nginx服务器上的时候,每次默认通过ip定位,都定位到北京去了,我查阅了很多资料,有解决办法,但是需要后端配合,但是我的后端又是第三方的api,这个问题感觉有点无解...如果有解决办法的同学或者有其他交流想法的同学,欢迎给我提issues,或给我留言,大家一起交流)
  • 因为第三方api有访问流量限制,如果访问过量,可能存在页面无法打开的情况,如果无法打开demo了,请及时与作者联系

1. 开启CRA之路 (必学步骤

意义说明:这是项目搭建准备的基础中的基础
脚手架工具 create-react-app,CRA详细介绍可以查看 Github CRA链接
使用脚手架的步骤:
npx create-react-app my-app
cd my-app
确认项目运行没有问题
yarn start
打开自定义webpack配置
yarn eject or npm run eject
可以看到多了几个文件,并且package.json里面多了很多内容,这些就是CRA内部封装的配置,暴露出来了,这个操作是不可逆的,这个操作的意义就在于我们可以自行修改webpack配置以及全局变量等配置,这对于一个企业级开发框架来说,是必要的

配置暴露截图.jpg

到这里,我们的第一步就完成了,是不是很简单呢?

2. cross-env及全局变量配置 (必学步骤

意义说明:这节内容是为了解决跨平台开发和灵活应对不同上线需求的(测试环境、预发布环境、正式环境等)
(1)cross-env,用于解决统一mac和windows项目运行的问题,大多数情况下,在windows平台下使用类似于: NODE_ENV=production的命令行指令会卡住,使用cross-env可以让不同平台使用唯一指令,无需担心跨平台问题
安装cross-env yarn add cross-env --dev
配置package.json如下

cross-env配置截图.png

说明:安装好cross-env后,只需要在每个命令行前加上cross-env即可,我这里有三个命令,start不用说,就是运行开发环境,"build:test"和"build:production"是干什么的呢,另外cross-env后面跟的一大堆参数是什么意思呢,这个我会在下面进行详细说明
(2)配置全局变量文件
全局变量文件,通常,我们会把api请求的服务器地址,某些固定请求前缀,特定appId,发布地址,api服务器地址,等相关参数写到全局变量中,我们通过全局变量文件,可以很方便的达到在开发和打包各个环境时,区分开不同的全局变量(比如开发环境请求的api前缀是http://xxxx-test.xxx,而正式环境是http://xxxx-pro.xxx),在企业级前端开发项目中,全局变量文件是必须的

我的项目就区分了三个全局变量,包括.env.development .env.test .env.production,分别用于表示开发环境,测试环境打包,正式环境打包

  • 配置
    全局变量文件必须以.env.xxx的格式命名,放置在项目根目录下,常用的就是development(开发环境),test(测试发布环境),production(正式生产环境)


    全局变量配置文件.png
  • 文件格式:".env.xxxx" 这里的xxx可以自定义想要的名字,但是通常都是上面这三个,现在我们回到package.json中看,也就是上面那张“cross-env配置截图”,在scripts中每个命令中有一个参数就是RECORD_ENV=xxxxx,这个xxxxx就是对应的全局变量的后缀文件名,表示在执行start命令的时候RECORD_ENV环境是development,其他同理,接下来就修改根目录下config文件夹里面的env.js文件,这里面就有我们读取全局文件的代码

    这里需要我们将const NODE_ENV = process.env.NODE_ENV; 改为 const NODE_ENV = process.env.XXXX;,这里的XXXX就是我们在package.json的scripts中,每条运行命令的那个区分环境的参数,我用的就是RECORD_ENV

    修改env.js文件配置.png

  • 全局变量文件中变量命名格式:
    必须是“REACT_APP_XXX”这样的格式,对应的各个不同环境下的全局变量文件各自命名各自所需的全局变量,写好每个文件中的所需变量后,我们就完成了全局变量配置


    全局变量命名格式.png

特别说明1:
关于第二步cross-env及全局变量配置,这一步不是运行基础框架所必须的,因为一般来说个人项目不会存在多环境下,环境变量不同的问题,但是对于企业级项目框架搭建来说,这个配置却是必须的,因为后端一定会存在测试环境,开发环境的区别

特别说明2:
关于内置环境变量的说明:我们修改了env.js中的配置,细心的同学肯定会发现到处都出现了这个变量“NODE_ENV”,下面我就简单说明一下CRA的内置的环境变量
create-react-app内置有两个环境变量,PUBLIC_URL和NODE_ENV
PUBLIC_URL是静态资源的发布路径,在public/index.html有使用,这个默认值是'',需要自己进行配置
NODE_ENV有三个值,分别对应如下:
npm start => development
npm test => test
npm run build => production
NODE_ENV的值不能手动进行覆盖,所以需要我们手动设置一个新的环境变量名称,然后在scripts运行命令的时候,通过指定环境参数来指定当前的环境变量文件,这就是我们上面“修改env.js文件配置”截图所做的事情

3. 配置ESLint和prettier (选学步骤

意义说明:这节内容是为了配置团队开发的时候的代码规范,作为一个企业级的前端项目架构,团队协作开发是必不可少的,但如果认为没有必要的同学,也可直接跳过本章节,代码规范检测及配置,并不是项目运行所必须的配置

首先,我们需要安装prettieryarn add prettier --dev

prettier是业内常用的格式化代码的工具库,笔者用的IDE是VSCode,VSCode有默认的格式化代码的规范,但是没有prettier好用,也可以在VSCode的插件库库里面直接安装prettier插件


prettier插件安装.jpg

关于ESLint,我们的CRA框架默认配置,已经预装了ESLint了,我们只需要选择想用的规则即可,这里我选的是这里我们使用airbnb的标准代码格式规范,业内比较常用的就是airbnb和standard这两个规范,具体相关知识,这里不做赘述,读者可以自行搜索比较

安装airbnb规则插件yarn add eslint-config-airbnb --dev
airbnb是我们主要的代码检测规则
安装prettier规则插件yarn add eslint-config-prettier --dev
eslint-config-prettier可以解决eslint和prettier对于同一个错误多次报错的问题
安装prettier在eslint下的插件yarn add eslint-plugin-prettier --dev
eslint-plugin-prettier会让eslint调用prettier对你的代码风格进行检查

配置ESLint规则文件
eslint --init 可以根据向导一步一步建立配置文件,也可以直接在根目录下建立几种配置文件的格式,我常用的就是.eslintrc.json,配置文件详细内容,请读者自行搜索,这里我只说明我配置的

.eslintrc.json 放在根目录下 用于屏蔽不需要的eslint规则
.eslintignore 用于屏蔽不需要eslint检测的文件夹,一般是node_modules,webpack配置文件等,这些文件夹是不需要eslint检测的


eslint配置文件.png

eslintrc.json具体内容配置说明:


eslintrc配置内容.png

.eslintignore具体内容配置说明:
eslintignore配置内容.png

最后,使用各大IDE的读者们,应该现在所有的IDE都有了ESLint和prettier的插件,这些插件都是依赖于node_modules里面的源码库的,插件只是IDE集成了这两个库的一个使用的入口,建议都把这两个库的插件安装好,然后重启IDE


eslint插件安装.png

到这里,我们关于ESLint和prettier插件的配置就完成了

特别说明:
禁用规则:因为airbnb是比较严格的规范,但是还是有不少规则,其实在实际开发中,会存在不少的麻烦的,我屏蔽了以下规则,具体规则意义可以自行查阅相关文档
"prettier/prettier": "error",
"import/no-unresolved": 0,
"react/forbid-prop-types": 0,
"prefer-destructuring": 0

4. 安装lodash (选学步骤

意义说明:如果不熟悉lodash的同学可以跳过本步骤,lodash是业内较为常用的一个JS库,里面包含了很全面的封装方法,但其实大部分人实际开发的时候,用JS的内置方法也基本都能满足需求了,用lodash只是图一个简洁方便,所以就是笔者个人比较喜欢用lodash的清爽直接,所以安装了
安装 yarn add lodash

5. 安装less相关,webpack配置less支持 (选学步骤

意义说明:这节内容是为了给项目添加less支持,我们在实际开发中,通常都是以下图这样来使用less样式的,对于less不了解的读者,可以自行学习一下less,企业级的项目架构中,肯定都是使用less或者sass的,笔者一直接触的都是less,sass的配置还请读者自行研究,这里我们只讲less的配置

less在项目中的使用.png

首先安装less模块支持
yarn add less --dev
yarn add less-loader --dev

然后分别在根目录下的config/webpack.config.dev.js和config/webpack.config.prod.js文件中配置

config/webpack.config.dev.js文件

  • 第一步修改

    webpack配置less-1.png

    注意:很多相关文档没有 javascriptEnabled: true这句代码,是因为less在3.x版本及以上,会需要加上这句话,否则会报错,除非安装less3.x以下的版本

  • 第二步修改

    webpack配置less-2.png

    注意:这一步修改的意义在于处理node_modules不需要开启css模块化,也就是我们上面的贴图“less在项目中的使用”这样的使用,所以有这样一句,include: [/node_modules/],然后处理本地自己开发的代码的时候,需要加上css模块化配置,否则无法使用import styles from 'xxxx'这样的用法,所以下面处理本地less的时候,加上了exclude: [cssModuleRegex, /node_modules/],这个cssModuleRegex是CRA2.x默认存在的配置,不用动就是了

config/webpack.config.prod.js文件
这个文件表示打包时候运行的webpack配置,和config/webpack.config.dev.js略有不同,但是添加对less支持这块的配置,是一模一样的配置,这里就不赘述了

6. 安装antd,配置antd按需加载,安装mobx mobx-react,修改babel配置 (必学步骤

意义说明:这节内容是为了添加antd的框架,mobx mobx-react,并添加按需加载配置,关于这块内容,官网有更为详细的介绍,读者也可以参考官网的说明antd官网 ,另外本步骤会详细说明一下CRA2.x已经预装到了babel7,有很多配置和以前不一样,建议仔细阅读本步骤 必学步骤
安装antdyarn add antd
安装antd按需加载必备组件yarn add babel-plugin-import --dev
安装mobxyarn add mobx
安装mobx-reactyarn add mobx-react

首先展示我的.babelrc文件配置
CRA默认的babel配置是放在了package.json里面,这里我将package.json里面的babel配置删除,单独在根目录下建立了一个.babelrc文件,个人觉得单独配置会比较清晰一些

babel配置文件修改.png

babel-plugin-import实现按需加载
如上图展示,在plugins中添加[ "import", { "libraryName": "antd", "style": true } ],

添加对ES7修饰器语法支持
如上图展示,在plugins中添加["@babel/plugin-proposal-decorators", { "legacy": true }]修饰器用法是mobx的用法中,最简单直观的,推荐使用mobx的框架都应该使用修饰器的用法

多说一句1
如果你的babel版本是7以下,name就需要先安装 yarn add transform-decorators-legacy --dev,然后在plugins中添加配置transform-decorators-legacy即可,但是CRA2.x已经将babel升到了7了,如果你还按照旧的配置方法,会有如下报错
解决babel 7报错问题
CRA2.x安装的babel版本已经达到7以上了,使用transform-decorators-legacy会有以下报错
The ‘decorators’ plugin requires a ‘decoratorsBeforeExport’ option, whose value must be a boolean. If you are migrating from Babylon/Babel 6 or want to use the old decorators proposal, you should use the ‘decorators-legacy’ plugin instead of ‘decorators’
https://github.com/mobxjs/mobx/issues/1352

多说一句2
如上面所说,可能有的文章是CRA1.x的版本,会说需要npm i transform-decorators-legacy来支持对修饰器的用法,但基于CRA2.x搭建的框架其实不需要了,因为CRA2.x已经预装到了babel7,babel7对修饰器的支持包已经换为了“@babel/plugin-proposal-decorators”这个包,配置也改成这样的配置["@babel/plugin-proposal-decorators", { "legacy": true }],这里你可以自己试一下,单独开一个文件,yarn add babel-preset-react-app,对,关键就是这个babel-preset-react-app,这个是CRA预装的“一条龙”包,里面包含了大量可能用的到的插件包,在CRA创建你的项目的时候,就已经给你装好了的,其中就已经包括了我们需要在.babelrc里面添加的@babel/plugin-proposal-decorators 对修饰器支持的包,这也就是babel配置里面的第一句"presets": ["react-app"],这里的意思就预装使用babel-preset-react-app这个包的内容

这个库已经涵盖了很全面的babel配置插件了,这个库把我们常用的比如@babel/preset-env(适配各版本的babel转换)
@babel/plugin-proposal-class-properties(对class语法的转换)
@babel/plugin-transform-runtime(运行时转换)
等等都已经封装进去了,所以我们这里唯一需要添加的支持就是对修饰器的配置

然后有的读者可能就有疑问了,既然babel-preset-react-app预装包已经包含了我们需要用到的@babel/plugin-proposal-decorators,那么为什么我们还要再plugins里面添加这一句话呢,是因为在babel-preset-react-app的源码中,对于@babel/plugin-proposal-decorators的开启有一句代码,是必须要传入"typescript:true"这句配置,这里我理解,可能对于使用typescript的项目,在babel中加入这样的配置,"presets": [["react-app",{typescript:true}]]才会开启修饰器支持的包,但事实上我已经试过了,运行项目还是会报错,这里就要说到正确的配置["@babel/plugin-proposal-decorators", { "legacy": true }]这里来了,我理解的"legacy": true,就是旧版本的修饰器支持,而新版本的修饰器库还不支持mobx的用法,所以如果不添加"legacy": true,则还是会报错。

正确解决修饰器使用问题,及拓展说明
我们的报错内容有一个关键字“decoratorsBeforeExport”,decoratorsBeforeExport这个参数是最新的修饰器插件所必须的一个参数,但官方也还在社区征集意见到底采用decoratorsBeforeExport:true or decoratorsBeforeExport:false所以现阶段各位同学只需要使用legacy:true即可,这个是babel官方说明
其实这个参数的true or false就是两种格式的争论,有兴趣的同学可以围观,关于这个参数的详细讨论链接在这里

7. react-router4.x使用及路由按需加载 (必学步骤

安装 yarn add react-router-dom
react-router-dom是基于react-router的,比react-router多了一些DOM类的组件,比如 <Link> <BrowserRouter>这样的组件,浏览器开发直接安装react-router-dom即可

关于react-router4.x的详细用法,又可以单独写一篇文章了,这里我不做详细的讲述,本文的主旨是项目架构,这里我只贴出自己的写法,写一些简单的用法,看贴图不清楚的同学,可以把项目clone下来仔细查看

index.jsx文件

index.jsx.jpg

app.jsx文件

app.jsx组件.jpg

routers.jsx文件

routers文件.png

按需加载(重点,路由的按需加载是前端优化的必须步骤,按需加载,顾名思义,页面在加载时只请求当前路由所需的资源,而不会全部路由资源都给请求下来,如果不配置按需加载,在后期项目庞大之后,会非常影响用户体验)

按需加载的配置文章,在网上也有很多,这里我只介绍两种,是我个人觉得比较轻量简单的两种方法,方法不止这两种,读者可自行搜索,详细了解原理

  • react-loadable(按需加载的一个支持库)
    安装yarn add react-loadable
    使用
    import Loadable from 'react-loadable';
    const MineItem = Loadable({ loader: () => import('./pages/mine'), loading: MyLoadingComponent });
    截图:
    按需加载配置1.png
  • 自定义Bundle.js组件,路由组件用这个Bundle套在外层
    截图:


    bundle组件.png

    bundle组件使用.png

8. 安装axios (必学步骤

意义说明:axios和fetch是业内常用的两个api请求库,笔者都有用过,总的来说axios更简单轻量,具体的用法和比较,请读者自行思考了
安装yarn add axios
axios的基本使用,请读者自行查看axios中文文档,这里我只提一个我项目中用到的配置,就是axios的拦截配置,axios可以拦截请求,返回的数据,提前做一些处理,比如:

axios拦截配置.png

配置好axios拦截器后,直接在在index.js中导入这个拦截器文件就好了


导入使用axios拦截器.png

9. 图表库 (选学步骤

意义说明:企业级项目中,不可能不用到可视化图标的库的,这里作为个人学习,可以不需要安装,但是笔者的框架内实现了一个仿腾讯天气的页面,用到了echarts-for-react,这个读者根据需要,自行判断是否需要
安装yarn add echarts-for-react

多说一句
关于图表库,不做多的赘述,业界内用到图表库的时候,最常用的两个就是d3和echarts,echarts-for-react只是这个作者基于echarts封装了一个统一的react组件,api的使用,就直接查看echarts官方api文档即可,没有任何的变化,关于d3和echarts的比较使用,可以查看这篇文章,使用者可自行比较
echarts官方文档
echarts-for-react

10. 前端开发环境的跨域代理配置 (必学步骤

意义说明:跨域是每个前端工程师必备的基本功之一,对于跨域笔者不做多的介绍了,企业级项目的跨域多数都是采用的CORS跨域方法,这个主要的配置是在服务端,而笔者这里介绍的是一种在前端进行跨域的方法,像笔者这样的个人项目搭建,又用的是第三方的api,服务端的设置我肯定是改不了了,所以就用的前端代理的方法,配置方法非常简单

只需要在package.json中加上"proxy": "http://xxxxxx"即可,xxxx就是代理服务器的地址

前端配置跨域代理.png

多说一句
笔者使用CRA1.x搭建项目的时候,proxy还可以像下面这样配置

CRA1.x前端跨域配置.png

这样配置的好处是可以多api服务的跨域,比如定位api用的是百度的,其他api又用的是高德的,就可以采用这样的配置,但是CRA2.x搭建后,我这样配置了,运行的时候给我报错,提示我proxy只能是一个字符串,截止目前,我暂时还没有找到解决的办法,不过因为我这个项目只用到了一个地址的第三方api,所以运行我的项目,也不存在这个问题,然后实际开发中,多数都是服务端使用CORS跨域,可能也不太会遇到这个问题,这个我后面如果解决了,会更新到文章中的

11. nginx准备 (选学步骤

意义说明:这节内容是属于折腾服务器的范畴了,我不准备在这篇文章内进行详细的介绍,我的下一篇文章,会介绍我折腾nginx服务器的正确步骤以及踩坑点,下面列出的是一些需要配置的东西,有兴趣的读者可以自行在网上查找资料学习
系统:服务器系统准备 centos7.2
nginx相关安装(很多依赖库)
nginx反向代理设置(用了第三方的api,必须要配置nginx的反向代理,否则api啥都访问不到,如果是自己写的后端,可以配置使用CORS跨域,就不用配置反向代理了)
nginx gzip压缩配置(大幅增加页面加载速度,启用gzip将我的应用从15s减少到4.x秒,这里又提到上文所说的路由按需加载路由按需加载配置gzip压缩,是前端工程师必须知道或者了解的两个优化方法,可以大幅加快页面加载速度)

写在后面

笔者实现的仿腾讯天气的页面目最主要的bug在于服务器,因为天气预报系统,用到了ip定位,我在开发阶段使用了proxy配置的代理,没有问题,但是挂载到nginx服务器上的时候,每次默认通过ip定位,都定位到北京去了,我查阅了很多资料,有解决办法,但是需要后端配合,但是我的后端又是第三方的api,这个问题感觉有点无解...如果有解决办法的同学或者有其他交流想法的同学,欢迎给我提issues,或给我留言,大家一起交流成长

最后,如若文章有任何错误或者误导读者的地方,请立即与作者联系,这是作者第一次发框架搭建性质的指导文档,难免存在遗漏和模糊不清的地方(亦或是写了太多作者自己理解的东西了,让读者看的比较迷茫😓😓😓😓),都请大家多多见谅。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容