2019-06-27 17:48:21 +08:00
|
|
|
|
---
|
|
|
|
|
id: "20190626"
|
|
|
|
|
date: 2019/06/26 10:58:00
|
2019-08-12 17:53:21 +08:00
|
|
|
|
title: "从零开始react实战:云书签1- react环境搭建"
|
2019-06-27 17:48:21 +08:00
|
|
|
|
tags: ["react", "antd", "less", "create-react-web"]
|
|
|
|
|
categories:
|
|
|
|
|
- "前端"
|
|
|
|
|
- "react"
|
|
|
|
|
---
|
|
|
|
|
|
2019-08-12 17:53:21 +08:00
|
|
|
|
总览篇:[react 实战之云书签](https://www.tapme.top/blog/detail/20190625)
|
2019-06-27 17:48:21 +08:00
|
|
|
|
|
|
|
|
|
本篇是实战系列的第一篇,主要是搭建 react 开发环境,在`create-react-app`的基础上加上如下功能:
|
|
|
|
|
|
2019-08-12 17:53:21 +08:00
|
|
|
|
- antd 组件库按需引入 ,支持主题定制
|
|
|
|
|
- 支持 less 语法,并使用 css-module
|
|
|
|
|
- 配置路由
|
2019-06-27 17:48:21 +08:00
|
|
|
|
- 支持 http 请求
|
2019-08-12 17:53:21 +08:00
|
|
|
|
- 配置 redux
|
2019-06-27 17:48:21 +08:00
|
|
|
|
|
|
|
|
|
**注意**:需要 node 版本大于 8.0.
|
|
|
|
|
|
|
|
|
|
## 创建 create-react-app
|
|
|
|
|
|
|
|
|
|
1. 安装
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
npm install -g create-react-app
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
2. 创建 react 应用
|
|
|
|
|
|
|
|
|
|
```bash
|
2019-07-11 15:15:37 +08:00
|
|
|
|
create-react-app bookmark-world
|
2019-06-27 17:48:21 +08:00
|
|
|
|
```
|
|
|
|
|
|
2019-06-27 20:29:09 +08:00
|
|
|
|
<!-- more -->
|
|
|
|
|
|
2019-06-27 17:48:21 +08:00
|
|
|
|
生成的目录结构如下图所示:
|
|
|
|
|
|
|
|
|
|
![目录结构](https://raw.githubusercontent.com/FleyX/files/master/blogImg/20190625160702.png)
|
|
|
|
|
|
|
|
|
|
## 配置 antd,less
|
|
|
|
|
|
|
|
|
|
有两种方法能够对其配置进行修改:
|
|
|
|
|
|
2019-07-11 15:15:37 +08:00
|
|
|
|
- 通过`npm run eject`暴露出配置文件,然后 修改这些配置文件,相比于下面的方法不太优雅,因此不考虑.
|
2019-06-27 17:48:21 +08:00
|
|
|
|
- 通过`react-app-rewired`覆盖配置.
|
|
|
|
|
|
2019-07-11 15:15:37 +08:00
|
|
|
|
后续需要修改配置的都用第二种--覆盖配置。
|
|
|
|
|
|
2019-06-27 17:48:21 +08:00
|
|
|
|
### 首先安装依赖
|
|
|
|
|
|
|
|
|
|
在 2.1.x 版本的 react-app-rewired 需要配合`customize-cra`来进行配置覆盖。所以需要安装如下依赖:
|
|
|
|
|
|
|
|
|
|
- react-app-rewired ,配置覆盖
|
|
|
|
|
- customize-cra ,配置覆盖
|
|
|
|
|
- antd ,ui 库
|
|
|
|
|
- babel-plugin-import ,按需引入 antd
|
|
|
|
|
- less ,less 支持
|
|
|
|
|
- less-loader ,less 支持
|
|
|
|
|
|
|
|
|
|
代码如下:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
npm install --save react-app-rewired customize-cra antd babel-plugin-import less less-loader
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 修改 package.json
|
|
|
|
|
|
|
|
|
|
用`react-app-rewired`替换掉原来的`react-scripts`
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
/* package.json */
|
|
|
|
|
"scripts": {
|
|
|
|
|
- "start": "react-scripts start",
|
|
|
|
|
+ "start": "react-app-rewired start",
|
|
|
|
|
- "build": "react-scripts build",
|
|
|
|
|
+ "build": "react-app-rewired build",
|
|
|
|
|
- "test": "react-scripts test",
|
|
|
|
|
+ "test": "react-app-rewired test",
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 创建 config-overrides.js
|
|
|
|
|
|
|
|
|
|
在项目根目录,也就是`package.json`的同级目录创建`config-overrides.js`文件.内容如下:
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
const { override, fixBabelImports, addLessLoader } = require("customize-cra");
|
|
|
|
|
|
|
|
|
|
module.exports = override(
|
|
|
|
|
fixBabelImports("import", {
|
|
|
|
|
libraryName: "antd",
|
|
|
|
|
libraryDirectory: "es",
|
|
|
|
|
style: true
|
|
|
|
|
}),
|
|
|
|
|
addLessLoader({
|
|
|
|
|
localIdentName: "[local]--[hash:base64:5]",
|
|
|
|
|
javascriptEnabled: true,
|
|
|
|
|
modifyVars: { "@primary-color": "#1DA57A" }
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 使用 css-module
|
|
|
|
|
|
2019-07-11 15:15:37 +08:00
|
|
|
|
要使用 css-module 需要将 css 文件命名为`fileName.module.less`,然后就能在组件中引入并正常使用了,如下:
|
|
|
|
|
|
|
|
|
|
**注意默认情况下后缀必须是.module.less 才能用 css-module 的写法**
|
2019-06-27 17:48:21 +08:00
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
import React, { Component } from "react";
|
|
|
|
|
import { Button } from "antd";
|
|
|
|
|
import styles1 from "./index.module.less";
|
|
|
|
|
|
|
|
|
|
class Hello extends Component {
|
|
|
|
|
render() {
|
|
|
|
|
return (
|
|
|
|
|
<div className={styles1.main}>
|
|
|
|
|
hello
|
|
|
|
|
<div className={styles1.text}>world</div>
|
|
|
|
|
<Button type="primary">你好</Button>
|
|
|
|
|
<div className="text1">heihei</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default Hello;
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 配置路由
|
|
|
|
|
|
|
|
|
|
首先修改 src 目录结构。改成如下所示:
|
|
|
|
|
|
|
|
|
|
![目录结构](https://raw.githubusercontent.com/FleyX/files/master/blogImg/20190627115439.png)
|
|
|
|
|
|
|
|
|
|
目录解释:
|
|
|
|
|
|
|
|
|
|
- assets: 存放图标,小图片等资源文件
|
|
|
|
|
- components:存放公共组件
|
|
|
|
|
- layout: 存放样式组件,用于嵌套路由和子路由中复用代码
|
|
|
|
|
- pages: 存放页面组件
|
|
|
|
|
- redux:存放 redux 相关
|
|
|
|
|
- action: 存放 action
|
|
|
|
|
- reducer: 存放 reducer 操作
|
|
|
|
|
- util: 工具类
|
|
|
|
|
|
2019-08-12 17:53:21 +08:00
|
|
|
|
删除`serviceWorker.js`文件,并在`index.js`中删除和它相关的代码。这个是和离线使用相关的。
|
2019-06-27 17:48:21 +08:00
|
|
|
|
|
|
|
|
|
然后安装`react-router`依赖:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
cnpm install --save react-router-dom
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
从路由开始就能体会到 react 一切都是 js 的精髓,react-router-dom 提供了一些路由组件来进行路由操作。本程序使用`history`路由。
|
|
|
|
|
|
|
|
|
|
首先修改`index.js`根组件放到`<BrowserRouter>`下,以开启 history 路由。代码如下:
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
// index.js
|
|
|
|
|
import React from "react";
|
|
|
|
|
import ReactDOM from "react-dom";
|
|
|
|
|
import "./index.css";
|
|
|
|
|
import App from "./App";
|
|
|
|
|
import { BrowserRouter } from "react-router-dom";
|
|
|
|
|
|
|
|
|
|
const s = (
|
|
|
|
|
<BrowserRouter>
|
|
|
|
|
<App />
|
|
|
|
|
</BrowserRouter>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
ReactDOM.render(s, document.getElementById("root"));
|
|
|
|
|
```
|
|
|
|
|
|
2019-07-11 15:15:37 +08:00
|
|
|
|
然后路由的配置方式有很多种,这里采用代码的方式组织路由,并将将 App.jsx 作为路由配置中心。(也可以基于配置文件,然后写一个解析配置文件的代码)
|
2019-06-27 17:48:21 +08:00
|
|
|
|
|
|
|
|
|
先加入登录和主页的路由,主要代码如下:
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
|
|
|
|
|
render() {
|
|
|
|
|
const mainStyle = {
|
|
|
|
|
fontSize: "0.16rem"
|
|
|
|
|
};
|
|
|
|
|
return (
|
|
|
|
|
<Provider store={store}>
|
|
|
|
|
<div className="fullScreen" style={mainStyle}>
|
|
|
|
|
<Switch>
|
|
|
|
|
<Route exact path="/" component={Main} />
|
|
|
|
|
<Route exact path="/public/login" component={Login} />
|
|
|
|
|
<Route exact path="/404" component={NotFound} />
|
|
|
|
|
{/* 当前面的路由都匹配不到时就会重定向到/404 */}
|
|
|
|
|
<Redirect path="/" to="/404" />
|
|
|
|
|
</Switch>
|
|
|
|
|
</div>
|
|
|
|
|
</Provider>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
名词解释:
|
|
|
|
|
|
|
|
|
|
- Switch: 该组件表示只匹配一个,匹配到后不再继续往下匹配
|
|
|
|
|
- Route:路由组件
|
|
|
|
|
- exact:表示完全匹配,如果开启这个,`/`只匹配`/`,否则匹配所有的路径
|
|
|
|
|
- Redirect:重定向组件,当前面的都不匹配就会匹配这个(因为没有开启`exact`且 path 为`/`),然后重定向到`/404`
|
|
|
|
|
|
|
|
|
|
后续用到嵌套路由时会更加深入的讲解路由相关。
|
|
|
|
|
|
|
|
|
|
## 配置 http 请求工具
|
|
|
|
|
|
|
|
|
|
http 请求工具这里选择的是`axios`。
|
|
|
|
|
|
|
|
|
|
首先安装依赖:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
cnpm install --save axios
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
然后编写工具类`util/httpUtil.js`,代码如下:
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
// httpUtil.js
|
|
|
|
|
|
|
|
|
|
import { notification } from "antd";
|
|
|
|
|
import axios from "axios";
|
|
|
|
|
|
|
|
|
|
//定义http实例
|
|
|
|
|
const instance = axios.create({
|
|
|
|
|
// baseURL: "http://ali.tapme.top:8081/mock/16/chat/api/",
|
|
|
|
|
headers: {
|
|
|
|
|
token: window.token
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
//实例添加拦截器
|
|
|
|
|
instance.interceptors.response.use(
|
|
|
|
|
function(res) {
|
|
|
|
|
return res.data;
|
|
|
|
|
},
|
|
|
|
|
function(error) {
|
|
|
|
|
console.log(error);
|
|
|
|
|
let message, description;
|
|
|
|
|
if (error.response === undefined) {
|
|
|
|
|
message = "出问题啦";
|
|
|
|
|
description = "你的网络有问题";
|
|
|
|
|
} else {
|
|
|
|
|
message = "出问题啦:" + error.response.status;
|
|
|
|
|
description = JSON.stringify(error.response.data);
|
|
|
|
|
//401跳转到登录页面
|
|
|
|
|
}
|
|
|
|
|
notification.open({
|
|
|
|
|
message,
|
|
|
|
|
description,
|
|
|
|
|
duration: 2
|
|
|
|
|
});
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
if (error.response && error.response.status === 401) {
|
|
|
|
|
let redirect = encodeURIComponent(window.location.pathname + window.location.search);
|
|
|
|
|
window.location.replace("/public/login?redirect=" + redirect);
|
|
|
|
|
}
|
|
|
|
|
}, 1000);
|
|
|
|
|
return Promise.reject(error);
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
export default instance;
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
主要实现了如下功能:
|
|
|
|
|
|
|
|
|
|
- 自动添加 token,设计前后端通过 jwt 做认证,因此每个请求都要加上 token
|
|
|
|
|
- 响应预处理,如果有错误,自动弹窗提示。如果响应码为 401,重定向到登录页面。
|
|
|
|
|
|
|
|
|
|
## 配置 redux
|
|
|
|
|
|
|
|
|
|
redux 算是 react 的一大难点。这里我们可以把 redux 理解成一个内存数据库,用一个对象来存储所有的数据.
|
|
|
|
|
|
|
|
|
|
对这个数据的修改有着严格的限制,必须通过 reducer 来修改数据,通过 action 定义修改的动作。
|
|
|
|
|
|
|
|
|
|
这里以用户登录数据为例。
|
|
|
|
|
|
|
|
|
|
### 定义
|
|
|
|
|
|
|
|
|
|
1. 首先定义 action,创建文件`redux/action/loginInfoAction.js`,代码如下:
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
// 定义登录信息在store中的名字
|
|
|
|
|
export const DATA_NAME = "loginInfo";
|
|
|
|
|
|
|
|
|
|
//定义修改loginInfo type
|
|
|
|
|
export const CHANGE_LOGIN_INFO = "changeLoginStatus";
|
|
|
|
|
|
|
|
|
|
export const changeLoginInfo = (token, userInfo) => {
|
|
|
|
|
return {
|
|
|
|
|
type: CHANGE_LOGIN_INFO,
|
|
|
|
|
data: {
|
|
|
|
|
token,
|
|
|
|
|
userInfo
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
- CHANGE_LOGIN_INFO :定义操作类别
|
|
|
|
|
- changeLoginInfo: 定义一个 action,在组件中调用,传入要修改的数据,在这里加上 type 上传递到 reducer 中处理.
|
|
|
|
|
|
|
|
|
|
2. 定义 reducer,创建文件`redux/reducer/loginInfo.js`,代码如下:
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
import * as loginAction from "../action/loginInfoAction";
|
|
|
|
|
|
|
|
|
|
function getInitData() {
|
|
|
|
|
let token, userInfo;
|
|
|
|
|
try {
|
|
|
|
|
token = localStorage.getItem("token");
|
|
|
|
|
userInfo = JSON.parse(localStorage.getItem("userInfo"));
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
token = null;
|
|
|
|
|
userInfo = null;
|
|
|
|
|
}
|
|
|
|
|
window.token = token;
|
|
|
|
|
window.userInfo = userInfo;
|
|
|
|
|
return {
|
|
|
|
|
token,
|
|
|
|
|
userInfo
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const LoginStatusReducer = (state = getInitData(), action) => {
|
|
|
|
|
switch (action.type) {
|
|
|
|
|
case loginAction.CHANGE_LOGIN_INFO:
|
|
|
|
|
return { ...action.data };
|
|
|
|
|
default:
|
|
|
|
|
return state;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default LoginStatusReducer;
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
- getInitData 方法用于初始化 userInfo 数据,这里写的比较复杂,会先从 localeStore 中取数据,然后挂载到 window 中,方便`httpUtil`中获取 token。
|
|
|
|
|
- LoginStatusReducer 方法用于处理 action 中的数据,输出处理后的 loginInfo 数据。
|
|
|
|
|
|
|
|
|
|
3. 编写 reducer 汇总类(redux/reducer/index.js),所有 reducer 都要汇总到一个方法中,这样就能生成整个系统的 store 对象。代码如下:
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
import { combineReducers } from "redux";
|
|
|
|
|
import { DATA_NAME } from "../action/loginInfoAction";
|
|
|
|
|
import loginInfo from "./loginInfo";
|
|
|
|
|
|
|
|
|
|
const data = {};
|
|
|
|
|
data[DATA_NAME] = loginInfo;
|
|
|
|
|
|
|
|
|
|
const reducer = combineReducers(data);
|
|
|
|
|
|
|
|
|
|
export default reducer;
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
4. 编写`redux/index.js`,这里生成真正的数据对象,代码如下:
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
import { createStore } from "redux";
|
|
|
|
|
import reducer from "./reducer";
|
|
|
|
|
|
|
|
|
|
const store = createStore(reducer);
|
|
|
|
|
|
|
|
|
|
export default store;
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
5. 最后将 store 绑定到根节点(App.js)中即可,修改部分如下:
|
|
|
|
|
|
|
|
|
|
![](https://raw.githubusercontent.com/FleyX/files/master/blogImg/20190627165936.png)
|
|
|
|
|
|
|
|
|
|
![](https://raw.githubusercontent.com/FleyX/files/master/blogImg/20190627170032.png)
|
|
|
|
|
|
|
|
|
|
### 使用
|
|
|
|
|
|
|
|
|
|
这里以登录页为例,学习如何获取到 loginInfo 和修改 loginInfo.
|
|
|
|
|
|
|
|
|
|
1. 创建登录页组件,`pages/public/Login/index.js`
|
|
|
|
|
登录页代码如下:
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
import React, { Component } from "react";
|
|
|
|
|
import queryString from "query-string";
|
|
|
|
|
import { Button, Input, message } from "antd";
|
|
|
|
|
import IconFont from "../../../components/IconFont";
|
|
|
|
|
import styles from "./index.module.less";
|
|
|
|
|
import { connect } from "react-redux";
|
|
|
|
|
import { changeLoginInfo, DATA_NAME } from "../../../redux/action/loginInfoAction";
|
|
|
|
|
import axios from "../../../util/httpUtil";
|
|
|
|
|
|
|
|
|
|
function mapStateToProps(state) {
|
|
|
|
|
return state[DATA_NAME];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function mapDispatchToProps(dispatch) {
|
|
|
|
|
return {
|
|
|
|
|
updateLoginInfo: (token, userInfo) => dispatch(changeLoginInfo(token, userInfo))
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class Login extends Component {
|
|
|
|
|
constructor(props) {
|
|
|
|
|
super(props);
|
|
|
|
|
this.state = {
|
|
|
|
|
username: "",
|
|
|
|
|
password: ""
|
|
|
|
|
};
|
|
|
|
|
this.query = queryString.parse(window.location.search);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
usernameInput = e => {
|
|
|
|
|
this.setState({ username: e.target.value });
|
|
|
|
|
};
|
|
|
|
|
passwordInput = e => {
|
|
|
|
|
this.setState({ password: e.target.value });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
submit = () => {
|
|
|
|
|
axios.post("/public/login", this.state).then(res => {
|
|
|
|
|
localStorage.setItem("token", res.token);
|
|
|
|
|
localStorage.setItem("userInfo", JSON.stringify(res.userInfo));
|
|
|
|
|
window.token = res.token;
|
|
|
|
|
window.userInfo = res.userInfo;
|
|
|
|
|
message.success("登录成功");
|
|
|
|
|
this.props.updateLoginInfo(res.token, res.userInfo);
|
|
|
|
|
if (this.query.redirect) {
|
|
|
|
|
this.props.history.replace(decodeURIComponent(this.query.redirect));
|
|
|
|
|
} else {
|
|
|
|
|
this.props.history.replace("/");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
render() {
|
|
|
|
|
return (
|
|
|
|
|
<div className="fullScreen flex main-center across-center">
|
|
|
|
|
// 省略其他部分
|
|
|
|
|
<Button type="primary" onClick={this.submit}>
|
|
|
|
|
登录
|
|
|
|
|
</Button>
|
|
|
|
|
...
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default connect(
|
|
|
|
|
mapStateToProps,
|
|
|
|
|
mapDispatchToProps
|
|
|
|
|
)(Login);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
其中最关键的是下面三个部分:
|
|
|
|
|
|
|
|
|
|
- mapStateToProps:本方法从整个 store 中获取需要的数据,传递到 Login 组件的 props 中。
|
|
|
|
|
- mapDispatchToProps:本方法用于修改 store 数据,返回的函数对象也会绑定到 Login 组件的 props 中,其中的 dispath 参数,用于调用 reducer 中的处理函数,根据 changeLoginInfo 返回的 action。
|
|
|
|
|
- connect 方法用于将上面两个函数和 Login 组件绑定起来,这样就能在 props 中获取到了。**如果还有 withRouter**,应将 withRouter 放在最外层。
|
|
|
|
|
|
|
|
|
|
目前登录访问的接口为 yapi 的 mock 数据,真正的后台代码将会在后面编写。
|
|
|
|
|
|
|
|
|
|
## 结尾
|
|
|
|
|
|
2019-07-02 14:46:32 +08:00
|
|
|
|
作为一个刚开始学习 react 的菜鸟,欢迎各位大牛批评指正。
|
2019-06-27 17:48:21 +08:00
|
|
|
|
|
2019-07-02 14:46:32 +08:00
|
|
|
|
源码:[github](https://github.com/FleyX/ChatRoom),切换到 tag:`第一篇:环境搭建`,便可以看到截止到本篇的源码。
|
2019-06-27 17:48:21 +08:00
|
|
|
|
|
2019-07-11 15:15:37 +08:00
|
|
|
|
**本文原创发布于:**[www.tapme.top/blog/detail/20190626](https://www.tapme.top/blog/detail/20190626)
|