你单排吧
工会低代码前端操作手册
  • 🇨🇳【你单排吧】的笔记空间
  • 🌖Vue2全家桶
    • Part1-vue基本语法
    • Part2-常用指令
    • Part3-指令与传值
    • Part4-Webpack与脚手架
    • Part5-《TodoList》项目实战
    • Part6-开发常用知识点
    • Part7-Git与Vuex
    • Part8-路由与请求
    • 《叩丁狼积分商城》PC端项目实战
  • 🌗React17全家桶
    • Part1-React基本语法
    • Part2-传值、路由与请求
    • Part3-Redux与React Redux
    • Part4-React Hooks
    • 《IT猿题库》项目实战
  • 🌘微信小程序入门与项目实战
    • Part1-语法入门
    • Part2-项目实战
    • Part3-UniApp项目实战
  • 🌑前端必学之Linux与Nginx
    • 服务器、Nginx与Linux
  • 🌒Koa2与MySQL
    • Part1 - Koa2
    • Part2 - MySQL
  • 🌓CMS全栈笔记
    • Part1-TypeScript
    • Part2-React+TS
    • Part3-Koa与MySQL
    • Part4-React+Antd+TS开发后台界面
    • Part5-前后端登录注册实现
    • Part6-前后端图片上传
    • Part7-文章模块开发
    • Part8-Nuxt+ElementUI开发官网
    • Part9-服务器选购与基本配置
    • Part10-SSR渲染与线上部署
  • 🌔Gulp前端自动化
    • Gulp入门与项目实战
  • 🌕更多学习资料
由 GitBook 提供支持
在本页
  • 预习资料
  • 实战课学习目标
  • 一、项目参考
  • 项目图片资源
  • 二、技术栈
  • 1、React
  • 2、Material-UI
  • 三、项目创建与安装
  • 1、依赖安装
  • 2、FastClick解决
  • 3、调用
  • 4、清除默认边距与样式
  • 四、蓝湖
  • 五、配置rem
  • 1、安装依赖包
  • 2、解包
  • 3、配置loader
  • 4、flexible引入
  • 5、rem测试
  • 6、兼容ipad
  • 7、修改meta标签
  • 六、配置less
  • 1、安装
  • 2、解包
  • 3、配置loader
  • 4、测试less
  • 5、文字三属性
  • 七、登录判断
  • 八、request封装
  • 1、接口文档
  • 2、封装request
  • 3、解决跨域
  • 4、api导出
  • 5、请求
  • 九、路由配置
  • 1、安装
  • 2、路由配置
  • 3、创建五个页面
  • 十、Tabbar配置
  • 1、Tabbar引用
  • 2、函数式组件路由
  • 3、Tabbar动态化
  • 4、最终Tabbar代码
  • 十一、Toast组件
  • 1、Toast组件封装
  • 2、调用方式
  • 十三、登录注册页套用
  • 1、登录页代码
  • 2、注册页代码
  • 十三、开发小提示
  • 1、JSX条件判断
  • 2、路由跳转问题
  • 3、自定义进度条
  • 4、配置项目@指向src
  • 5、组件中无法使用history?
  • 6、setState是异步的
  • 7、统一使用消息提示
  • 8、ReactRedux包装Alert
  • 9、React插入html
  • 10、反序列化问题
  • 11、React实现移动端滑动
  • 12、答题之多选组件UI部分代码
  • 13、事件参数与事件对象
  • 14、禁用Link
  • 十四、项目打包
  • 1、Hash模式打包
  • 2、History模式打包
  • 3、跨域
  • 十五、作业
  • 作业1(初级)
  • 作业2(中级)
  • 作业3(高级)
  • 作业4(顶级)

这有帮助吗?

  1. React17全家桶

《IT猿题库》项目实战

上一页Part4-React Hooks下一页微信小程序入门与项目实战

最后更新于3年前

这有帮助吗?

预习资料

项目地址:

参考该项目及这份笔记,并尝试搭建项目。

实战课学习目标

结合React、React Redux、React Router、Hooks、Axios与material-ui,完成《IT猿题库》项目。

一、项目参考

项目地址:

界面效果:

项目图片资源

链接: 提取码: 9p5h

二、技术栈

1、React

简介:使用React开发移动端项目

2、Material-UI

简介:一个基于 Preact / React / React Native 的 UI 组件库

三、项目创建与安装

1、依赖安装

使用 npx create-react-app yuantiku 创建完项目后,安装material-ui:

# 用npm安装
$ npm install @material-ui/core

# 用yarn安装
$ yarn add @material-ui/core

# 顺便安装SVG 图标
# 通过 npm
$ npm install @material-ui/icons

# 通过 yarn
$ yarn add @material-ui/icons

2、FastClick解决

在 public/index.html 的 head 标签中插入:

<title>IT猿题库</title>
<script src="https://as.alipayobjects.com/g/component/fastclick/1.0.6/fastclick.js"></script>
<script>
  if ('addEventListener' in document) {
    document.addEventListener('DOMContentLoaded', function() {
      FastClick.attach(document.body);
    }, false);
  }
  if(!window.Promise) {
    document.writeln('<script src="https://as.alipayobjects.com/g/component/es6-promise/3.2.2/es6-promise.min.js"'+'>'+'<'+'/'+'script>');
  }
</script>

这里建议把 https://as.alipayobjects.com/g/component/fastclick/1.0.6/fastclick.js 和 https://as.alipayobjects.com/g/component/es6-promise/3.2.2/es6-promise.min.js 保存到本地

3、调用

在 App.js 中:

import React, { Component } from 'react'
import Button from '@material-ui/core/Button';

export default class App extends Component {
  render() {
    return (
      <Button variant="contained" color="primary">你好,世界</Button>
    )
  }
}

然后就可以看到按钮被正式引入:

4、清除默认边距与样式

安装:

$ yarn add reset-css

使用:

// index.js中
import 'reset-css';

四、蓝湖

将设计图用photoshop打开,并上传至蓝湖。此时的设计图是3x尺寸,因此要勾选对应的1125px尺寸。

五、配置rem

1、安装依赖包

$ yarn add lib-flexible postcss-pxtorem

2、解包

解包需要先做git提交,否则无法解包,因此先执行:

$ git add .
$ git commit -m 'eject之前的提交'

接下来直接解包:

$ yarn eject

3、配置loader

解包后,可以看到项目目录下多了一个 config 文件夹。打开 config/webpack.config.js :

// 引入 postcss-px2rem 
const px2rem = require('postcss-px2rem')

搜索 postcss-loader ,添加:

const loaders = [
 	 ...,
  {
        // Options for PostCSS as we reference these options twice
        // Adds vendor prefixing based on your specified browser support in
        // package.json
        loader: require.resolve('postcss-loader'),
        options: {
          postcssOptions: {
            // Necessary for external CSS imports to work
            // https://github.com/facebook/create-react-app/issues/2677
            ident: 'postcss',
            config: false,
            plugins: !useTailwind
              ? [
                  'postcss-flexbugs-fixes',
                  [
                    'postcss-preset-env',
                    {
                      autoprefixer: {
                        flexbox: 'no-2009',
                      },
                      stage: 3,
                    },
                  ]],
	              /* -------添加下面这一段------- */
                  [
                    'postcss-pxtorem',
                    {
                      rootValue: 112.5,
                      selectorBlackList: [],
                      propList: ['*'],
                      exclude: /node_modules/i
                    }
                  ],
               	  /* -------添加上面这一段------- */
                  // Adds PostCSS Normalize as the reset css with default options,
                  // so that it honors browserslist config in package.json
                  // which in turn let's users customize the target behavior as per their needs.
                  'postcss-normalize',
                ]
              : [
                  'tailwindcss',
                  'postcss-flexbugs-fixes',
                  [
                    'postcss-preset-env',
                    {
                      autoprefixer: {
                        flexbox: 'no-2009',
                      },
                      stage: 3,
                    },
                  ],
	              /* -------添加下面这一段------- */
                  [
                    'postcss-pxtorem',
                    {
                      rootValue: 112.5,
                      selectorBlackList: [],
                      propList: ['*'],
                      exclude: /node_modules/i
                    }
                  ]
               	  /* -------添加上面这一段------- */
                ],
          },
          sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
        },
  ...
]

这里的 rootValue: 112.5 的意思就是1rem = 112.5px 这个是根据1125px设计稿来的。

4、flexible引入

在 入口文件 index.js 里引入 lib-flexible:

import 'lib-flexible'

5、rem测试

在 App.js 中写个类名,创建 App.css ,并写入:

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

export default class App extends Component {
  render() {
    return (
      <div className="box">
        盒子
      </div>
    )
  }
}

// App.css
.box{
    width: 1125px;
    height: 186px;
    background: pink;
}

接下来打开浏览器:

可以看到,iphoneX的尺寸下,html的字体大小为37.5px,此时box的宽度为10rem,再来看看其他尺寸:

当其他尺寸下时,可以发现html字体大小为41.1px,而此时box的宽度仍为10rem,这就代表我们rem配置成功了。

6、兼容ipad

但是,当你点开ipad时,会发现盒子兼容出了问题,这是因为淘宝弹性布局方案lib-flexible不兼容ipad和ipad pro。我们这里给出解决方案:

在public>index.html的head标签中添加:

<script>
        /(iPhone|iPad|iPhone OS|Phone|iPod|iOS)/i.test(navigator.userAgent)&&(head=document.getElementsByTagName('head'),viewport=document.createElement('meta'),viewport.name='viewport',viewport.content='target-densitydpi=device-dpi, width=480px, user-scalable=no',head.length>0&&head[head.length-1].appendChild(viewport));
</script>

这样,我们就解决ipad的兼容问题了。

7、修改meta标签

<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />

六、配置less

1、安装

$ yarn add less less-loader@5.0.0
# 或者
$ npm install less less-loader@5.0.0

2、解包

接下来要解包,如果上一步你已经解包过,就直接跳过。

如果未解包,请以上参考第五步。

3、配置loader

找到 webpack.config.js ,搜索 sassRegex:

const lessModuleRegex = /\.less$/;

搜索 sass-loader 后,在其下方添加:

module: {
  ...,
  // less加载器 
	{
    test: lessModuleRegex,
    use: getStyleLoaders(
        {
            //暂不配置
        },
        'less-loader'
    ),
	},
}

修改了配置文件,记得重新 yarn start 哦!

4、测试less

将 App.css 改为 App.less 进行测试,依然没问题。

5、文字三属性

在 App.less 中添加:

@import url("http://at.alicdn.com/t/font_2390471_h1demfeh4rc.css");

#root {
    font-size: 38px;
    font-family: NotoSansHans;
    color: #333333;
}

这里注意,由于html和body标签已被强行设定了font-size,因此我们设定#root的font-size即可。

七、登录判断

如果我们每个页面都需要在 componentWillMount 判断是否已经登录,那么太麻烦。React中有mixins,但已经被淘汰了。取而代之,我们使用高阶函数来操作。

在src下创建 hoc>index.js :

import {Component} from 'react'

export const ifLoginFn = (Comp) => {
    // 定义一个判断登录的高阶组件,在需要判断登录的页面套上这个组件
    return class extends Component {
        UNSAFE_componentWillMount(){
            let token = localStorage.getItem('token');
            if(!token){
                this.props.history.push("/login")
            }
        }
        render(){
            return <Comp />
        }
    }
}

在任何一个页面都可以如下调用(这里以Home页面举例):

import React, { Component } from 'react'
import "./Home.less"
import {ifLoginFn} from '../../components/hoc'

class Home extends Component {
    render() {
        return (
            <div>
                首页
            </div>
        )
    }
    
}

// 使用高阶函数
export default ifLoginFn(Home)

八、request封装

1、接口文档

2、封装request

react的数据请求我们依然使用axios,我们先封装request:

import axios from 'axios'

const instance = axios.create({
  baseURL: '/api',	// 通过使用配置的proxy来解决跨域
  timeout: 5000
});

// 添加请求拦截器
instance.interceptors.request.use(function (config) {
  let token = localStorage.getItem("x-auth-token");
  if (token) {
    config.headers = {
      "x-auth-token": token
    }
  }
  return config;
}, function (error) {
  // 对请求错误做些什么
  return Promise.reject(error);
});

// 添加响应拦截器
instance.interceptors.response.use(function (response) {
  // 对响应数据做点什么
  return response.data;
}, function (error) {
  // 对响应错误做点什么
  return Promise.reject(error);
});


export default instance;

Token过期:

如果后端判断到token过期,可以在响应拦截器到reject之前:

import {HashRouter} from 'react-router-dom' // 借用HashRouter协助路由操作

if (error.response.status === 500 && error.response.data.errCode === 1002) {
  // 使用HashRouter
  const router = new HashRouter();
  // 如果是token过期,直接跳到登录页
  router.history.push('/login');
}

3、解决跨域

方案一:

react简单解决跨域可以直接在 package.json 中添加 proxy 属性

方案二:

如果你已经进行了 npm run eject ,建议你直接修改 config>webpackDevServer.config.js :

proxy: {
  '/api': {
    target: 'https://www.ahsj.link/rambo', // 后台服务地址以及端口号
    changeOrigin: true, //是否跨域
    pathRewrite: { '^/api': '/' }
  }
}

方案三(推荐):

安装 http-proxy-middleware :

yarn add http-proxy-middleware

这里注意,http-proxy-middleware 模块是有版本区别的,默认安装最新版本,然后在 src 目录下新建 setupProxy.js :

const { createProxyMiddleware } = require("http-proxy-middleware");
module.exports = function (app) {
    app.use(
        "/api",
        createProxyMiddleware({
            target: "https://www.ahsj.link/rambo",
            changeOrigin: true,
            pathRewrite: {
                "/api": "",
            },
        })
    );
};

重新 npm run start 即可解决跨域。

4、api导出

我们做请求时的api需要导出:

import request from './request'

// 首页默认数据
export const HomeDefaultApi = () => request.get('/6666');

// 登录接口
export const LoginApi = (params) => request.post('/1024/login', params)

// 注册接口
export const RegisterApi = (params) => request.post('/1024/register', params)

5、请求

componentDidMount(){
  // 获取token,判断是否有token,有则做请求
  let token = localStorage.getItem("x-auth-token");
  if(token){
    // 首页默认数据
    HomeDefaultApi().then(res=>{
      console.log(res)
    })
  }
}

九、路由配置

1、安装

安装 react-router-dom:

$ yarn add react-router-dom

2、路由配置

src 下创建 router/index.js:

import {BrowserRouter as Router, Route, Switch} from 'react-router-dom'
import App from '../App'

export const BaseRoute = () => {
    return (
        <Router>
            <Switch>
                <Route path="/" component={App}></Route>
            </Switch>
        </Router>
    )
}

App.js 中:

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

class App extends Component {
  render() {
    return (
      <div id="app">
        App
      </div>
    );
  }
}

export default App;

而 src/index.js 下:

import ReactDOM from 'react-dom';
import 'reset-css';
import './base.less'
import 'lib-flexible'
import {BaseRoute} from './router'

ReactDOM.render(
  BaseRoute(),
  document.getElementById('root')
);

可以看到:

3、创建五个页面

src下创建 views/Home.jsx + Fast.jsx + User.jsx + Login.jsx + Register.jsx + Error.jsx,分别代表:首页+快速刷题+我的+登录页+404页面,并在router下做配置:

import { BrowserRouter as Router, Route, Switch, Redirect } from 'react-router-dom'
import App from '../App'
import Home from '../views/Home'
import Fast from '../views/Fast'
import User from '../views/User'
import Login from '../views/Login'
import Register from '../views/Register'
import Error from '../views/Error'

export const BaseRoute = () => {
    return (
        <Router>
            <Route path="/" component={() =>
                <App>
                    <Switch>
                        <Route exact path="/home" component={Home}></Route>
                        <Route exact path="/fast" component={Fast}></Route>
                        <Route exact path="/user" component={User}></Route>
                        <Route exact path="/login" component={Login}></Route>
                        <Route exact path="/register" component={Register}></Route>
                        <Route exact component={Error}></Route>
                    </Switch>
                </App>
            }></Route>
        </Router>
    )
}

修改 App.js :

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

class App extends Component {
  render() {
    return (
      <div id="app">
        {this.props.children}
      </div>
    );
  }
}

export default App;

如此,当我们在浏览器输入 http://localhost:3000 时,就会帮我们自动跳到 http://localhost:3000/home ,如果随意输入 http://localhost:3000/aaa ,就会自动重定向到错误页面。

十、Tabbar配置

1、Tabbar引用

src下创建 components/Tabbar.js :

import React from 'react';
import './less/Tabbar.less'
import BottomNavigation from '@material-ui/core/BottomNavigation';
import BottomNavigationAction from '@material-ui/core/BottomNavigationAction';
import HomeTab from './HomeTab'
import FastTab from './FastTab'
import UserTab from './UserTab'

export default function SimpleBottomNavigation() {
    const [value, setValue] = React.useState(0);

    return (
        <BottomNavigation
            value={value}
            onChange={(event, newValue) => {
                setValue(newValue);
            }}
            showLabels
        >
            <BottomNavigationAction label="首页" icon={<HomeTab num={value} />} />
            <BottomNavigationAction label="快速刷题" icon={<FastTab />} />
            <BottomNavigationAction label="我的" icon={<UserTab num={value} />} />
        </BottomNavigation>
    );
}

src/components/less/Tabbar.less:

.MuiBottomNavigation-root{
    position: fixed;
    width: 100%;
    left: 0;
    bottom: 0;
    .MuiBottomNavigationAction-wrapper {
        .MuiBottomNavigationAction-label {
            font-size: 30px;
        }
        img{
            width: 64px;
        }
    }
    
    .MuiBottomNavigationAction-root.Mui-selected{
        .MuiBottomNavigationAction-label {
            font-size: 30px;
        }
    }
}

src/components/HomeTab.js、src/components/FastTab.js 及 src/components/UserTab.js :

// HomeTab.js
import React, { Component } from 'react';
import home1 from '../images/tabbar/home_1.png'
import home2 from '../images/tabbar/home_2.png'

class HomeTab extends Component {
    render() {
        return (
            <img src={this.props.num===0 ? home1 : home2} width="61" alt="" />
        );
    }
}

export default HomeTab;

// FastTab.js
import React, { Component } from 'react';
import fast from '../images/tabbar/fast.png'

class FastTab extends Component {
    render() {
        return (
            <img src={fast} width="60" alt="" />
        );
    }
}

export default FastTab;

// UserTab.js
import React, { Component } from 'react';
import user1 from '../images/tabbar/my_1.png'
import user2 from '../images/tabbar/my_2.png'

class UserTab extends Component {
    render() {
        return (
            <img src={this.props.num===2 ? user1 : user2} width="64" alt="" />
        );
    }
}

export default UserTab;

2、函数式组件路由

在 Tabbar.js 中,使用hooks:

import {useHistory} from 'react-router-dom'		// 路由中含有 useHistory

export default function SimpleBottomNavigation() {
  let history = useHistory();		//  使用history
  return (
        <BottomNavigation
            value={value}
            onChange={(event, newValue) => {
                setValue(newValue);
                switch (newValue) {
                    case 0:
                        history.push('/home');
                        break;
                    case 1:
                        history.push('/fast');
                        break;
                    case 2:
                        history.push('/user');
                        break;
                    default:
                        break;
                }
            }}
            showLabels
        >
            <BottomNavigationAction label="首页" icon={<HomeTab num={value} />} />
            <BottomNavigationAction label="快速刷题" icon={<FastTab />} />
            <BottomNavigationAction label="我的" icon={<UserTab num={value} />} />
        </BottomNavigation>
    );
}

如此,就实现了路由切换。

3、Tabbar动态化

import React, {useEffect} from 'react';

export default function SimpleBottomNavigation() {
  const [showNav, setShowNav] = React.useState(false);
  
  useEffect(()=>{
    // if可改写为switch,建议写为switch
    if(window.location.pathname==='/home' || window.location.pathname==='/fast' || window.location.pathname==='/user'){
      setShowNav(true)
    }else{
      setShowNav(false)
    }
  }, [window.location.pathname])
  
  return (
  	<BottomNavigation style={{display: showNav ? 'flex' : 'none'}}></BottomNavigation>
  )
}

4、最终Tabbar代码

import React, { useEffect } from 'react';
import './less/Tabbar.less'
import BottomNavigation from '@material-ui/core/BottomNavigation';
import BottomNavigationAction from '@material-ui/core/BottomNavigationAction';
import HomeTab from './HomeTab'
import FastTab from './FastTab'
import UserTab from './UserTab'
import { useHistory } from 'react-router-dom'

export default function SimpleBottomNavigation() {
    const [value, setValue] = React.useState(0);
    const [showNav, setShowNav] = React.useState(false);
    let history = useHistory();

    useEffect(() => {
        switch (history.location.pathname) {
            case '/home':
                setShowNav(true);   // 根据url来决定是否显示tabbar
                setValue(0);    // 根据url来决定现在是哪个tabbar为当前项
                break;
            case '/fast':
                setShowNav(true);
                setValue(1);
                break;
            case '/user':
                setShowNav(true);
                setValue(2);
                break;
            default:
                setShowNav(false);
                break;
        }
        // eslint-disable-next-line
    }, [history.location.pathname])

    return (
        <BottomNavigation
            style={{ display: showNav ? 'flex' : 'none' }}
            value={value}
            onChange={(event, newValue) => {
                setValue(newValue);
                switch (newValue) {
                    case 0:
                        history.push('/home');
                        break;
                    case 1:
                        history.push('/fast');
                        break;
                    case 2:
                        history.push('/user');
                        break;
                    default:
                        break;
                }
            }}
            showLabels
        >
            <BottomNavigationAction label="首页" icon={<HomeTab num={value} />} />
            <BottomNavigationAction label="快速刷题" icon={<FastTab />} />
            <BottomNavigationAction label="我的" icon={<UserTab num={value} />} />
        </BottomNavigation>
    );
}

十一、Toast组件

1、Toast组件封装

这里需要结合react-redux来实现。首先新建 Toast.jsx:

import React from 'react'
import "./less/Toast.less"
import { connect } from 'react-redux'

class Toast extends React.Component {
    render() {
        return (
            <>
                <div className="toast" style={{ display: this.props.showToast ? "block" : "none" }}>
                    <main style={{ display: this.props.icon === "none" ? 'none' : 'block' }}>
                        <i className={
                            this.props.icon === "success" ?
                                "iconfont icon-ceshijieguo-dui" :
                                this.props.icon === "error" ?
                                    "iconfont icon-cuowu" :
                                    "iconfont icon-loading"
                        }></i>
                    </main>
                    <section>{this.props.title}</section>
                </div>
            </>
        )
    }
}

const mapStateToProps = (state) => {
    return {
        showToast: state.showToast,
        title: state.title,
        icon: state.icon
    }
}

const mapDispatchToProps = (dispath) => {
    return {
        showToastFn(icon, title) {
            dispath({ type: "showToastFn", value: { icon, title } })
        }
    }
}

export default connect(mapStateToProps, mapDispatchToProps)(Toast)

Toast.less 中:

.toast{
    position: fixed;
    z-index: 5;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    background: rgba(0,0,0,0.5);
    padding: 50px 40px;
    border-radius: 10px;
    min-width: 3rem;
    max-width: 3.3rem;
    text-align: center;
    color: #fff;
    main{
        text-align: center;
        padding: 20px 0;
        .iconfont{
            display: inline-block;
            margin-bottom: .5rem;
            font-size: 1rem;
        }
        .icon-loading{
            animation: round 1s linear infinite;
        }
    }
    section{
        font-size: 50px;
        width: 100%;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        word-break: break-all;
    }
}

@keyframes round {
    from{transform: rotate(0);transform-origin: center center;}
    to{transform: rotate(360deg);transform-origin: center center;}
}

在 store/reducer.js 中:

const defaultState = {
  /* 
  		提示图标:
      success: (icon-ceshijieguo-dui)
      error: (icon-cuowu1)
      loading: (icon-loading)
      none: (没有图标) 
  */
  icon: "success",
  // toast标题
  title: "暂无内容暂无内容暂无内容暂无内容",
  // 显示Toast
  showToast: false
};

// eslint-disable-next-line
export default (state = defaultState, action) => {
  let newState = JSON.parse(JSON.stringify(state));
  switch (action.type) {
    // 弹出提示
    case "showToastFn":
      newState.showToast = true;
      newState.icon = action.value.icon;
      newState.title = action.value.title;
      break;
    case "hideToastFn":
      newState.showToast = false;
      break;
    default:
      break;
  }
  return newState;
};

使用时机:

比如:每个接口的请求若报500并返回1002,就代表token错误或过期,此时就需要判断token,如果没有token,就直接跳到登录页,跳过去之前,就要先显示loading效果

在 App.jsx 中引入Toast组件:

import React, { Component } from 'react';
import './App.less'
import Tabbar from './components/Tabbar'
import Toast from './components/Toast'

class App extends Component {
  render() {
    return (
      <div id="app">
        <Toast />
        {this.props.children}
        <Tabbar history={this.props.history} />
      </div>
    );
  }
}


// 这里不再是导出App,而是导出连接器
export default App;

2、调用方式

import { connect } from 'react-redux'  //引入连接器

class 组件名称 extends Component {
    handleClick(){
      	// 打开提示
        this.props.showToastFn({
            icon: "none",
            title: "该功能暂未开放"
        })
    }
}

const mapDispatchToProps = (dispatch) => {
    return {
        showToastFn(value) {
            dispatch({ type: "showToastFn", value })
            setTimeout(() => {
                dispatch({ type: "hideToastFn" })
            }, 2000)
        }
    }
}

export default connect(null, dispatchToProps)(组件名称);

十三、登录注册页套用

⚠️ 这里插播一句:登录注册页请直接套用代码就好!

效果如下:

1、登录页代码

将示例代码直接复制粘贴,然后稍做修改:

import React, { useState } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField';
import { LoginApi } from '../request/api'
import { useHistory, Link } from 'react-router-dom'
import { connect } from 'react-redux'
import logo from '../images/logo.png'

const useStyles = makeStyles((theme) => ({
  loginPage: {
    background: '#fff',
    height: '100vh',
    overflow: 'hidden'
  },
  logo: {
    display: 'block',
    margin: '0 auto',
    marginTop: '20vh'
  },
  title: {
    fontSize: '.5rem',
    textAlign: 'right',
    width: '90%',
    margin: '0 auto 20px',
    color: '#02369d'
  },
  btn: {
    color: "#fff",
    fontWeight: 'normal',
    background: "#02369d"
  },
  copyright: {
    width: '90%',
    margin: '20px auto',
    paddingLeft: '8px',
    boxSizing: 'border-box'
  },
  root: {
    width: '90%',
    margin: 'auto',
    '& > *': {
      width: '100%',
      display: 'block',
      fontSize: '.5rem',
    },
    '& .MuiTextField-root': {
      fontSize: '.5rem',
      '& .MuiInputBase-input': {
        fontSize: '.5rem'
      }
    }
  },
}));

function SignIn(props) {
  const classes = useStyles();
  // 用户名
  const [username, setUsername] = useState(localStorage.getItem("phone") || "");
  // 密码
  const [password, setPassword] = useState("wolfcode123");
  // 获取路由
  const history = useHistory()

  // 点击了登录
  function submitFn() {
    LoginApi({
      username,
      password
    }).then(res => {
      if (res.errCode === 0) {
        // 成功
        props.showToastFn({
          icon: "success",
          title: "登录成功"
        });
        let token = res.data;
        // 存入token
        localStorage.setItem('x-auth-token', token);
        setTimeout(() => {
          props.hideToastFn();
          props.showToastFn({
            icon: "loading",
            title: "即将返回首页"
          });
          // 2秒后跳转到首页
          setTimeout(() => {
            // 返回首页
            history.push("/home");
          }, 2000)
        }, 1500)
      } else {
        props.showToastFn({
          icon: "error",
          title: res.message
        });
      }
    }).catch(err => {
      props.showToastFn({
        icon: "error",
        title: err.response.data.message
      });
    })
  }

  return (
    <div className={classes.loginPage}>
      <img src={logo} className={classes.logo} alt="" />
      <h2 className={classes.title}>Login Page</h2>
      <form className={classes.root}>
        <TextField
          variant="outlined"
          margin="normal"
          required
          fullWidth
          id="username"
          placeholder="请输入手机号码"
          name="username"
          autoComplete="username"
          autoFocus
          value={username}
          onChange={(e) => setUsername(e.target.value)}
        />
        <TextField
          variant="outlined"
          margin="normal"
          required
          fullWidth
          name="password"
          placeholder="请输入密码"
          type="password"
          id="password"
          autoComplete="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
        />
        <Button
          className={classes.btn}
          type="button"
          fullWidth
          variant="contained"
          onClick={submitFn}
        >直接登录</Button>
        <div style={{ marginTop: '20px', display: 'flex', justifyContent: 'space-between' }}>
          <Link to="/register" variant="body2" style={{ color: '#02369d' }}>前往注册</Link>
          <Link to="/home" variant="body2" style={{ color: '#999999' }}>返回首页</Link>
        </div>
      </form>
      <section className={classes.copyright}>
        {'Copyright © '}
        <Link color="inherit" to="http://codesohigh.com">
          你单排吧
        </Link>{' '}
        {new Date().getFullYear()}
        {'.'}
      </section>
    </div>
  );
}

const mapDispatchToProps = (dispatch) => {
  return {
    showToastFn(value) {
      dispatch({ type: "showToastFn", value })
      setTimeout(() => {
        dispatch({ type: "hideToastFn" })
      }, 2000)
    },
    hideToastFn() {
      dispatch({ type: "hideToastFn" })
    }
  }
}

export default connect(null, mapDispatchToProps)(SignIn)

然后在 request/api.js 中:

// 登录接口
export const LoginApi = (params) => request.post('/1024/login', params)

如果缺少什么依赖,就按照提示安装即可。这里主要还缺少:

yarn add @material-ui/lab

2、注册页代码

import React, { useState } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField';
import { RegisterApi } from '../request/api'
import { useHistory, Link } from 'react-router-dom'
import { connect } from 'react-redux'
import logo from '../images/logo.png'

const useStyles = makeStyles((theme) => ({
    loginPage: {
        background: '#fff',
        height: '100vh',
        overflow: 'hidden'
    },
    logo: {
        display: 'block',
        margin: '0 auto',
        marginTop: '20vh'
    },
    title: {
        fontSize: '.5rem',
        textAlign: 'right',
        width: '90%',
        margin: '0 auto 20px',
        color: '#02369d'
    },
    btn: {
        color: "#fff",
        fontWeight: 'normal',
        background: "#02369d"
    },
    copyright: {
        width: '90%',
        margin: '20px auto',
        paddingLeft: '8px',
        boxSizing: 'border-box'
    },
    root: {
        width: '90%',
        margin: 'auto',
        '& > *': {
            width: '100%',
            display: 'block',
            fontSize: '.5rem',
        },
        '& .MuiTextField-root': {
            fontSize: '.5rem',
            '& .MuiInputBase-input': {
                fontSize: '.5rem'
            }
        }
    },
}));

function SignIn(props) {
    const classes = useStyles();
    // 用户名
    const [phone, setPhone] = useState("");
    // 密码
    const [password, setPassword] = useState("");
    // 获取路由
    const history = useHistory()

    // 点击了登录
    function submitFn() {
        RegisterApi({
            phone: Number(phone),
            password
        }).then(res => {
            if (res.errCode === 0) {
                // 成功
                props.showToastFn({
                    icon: "success",
                    title: "注册成功"
                });
                // 存储手机号
                localStorage.setItem("phone", phone);
                setTimeout(() => {
                    props.hideToastFn();
                    props.showToastFn({
                        icon: "loading",
                        title: "即将返回登录页"
                    });

                    // 2秒后跳转到首页
                    setTimeout(() => {
                        // 返回登录页
                        history.push('/login');
                    }, 2000)
                }, 1000)
            } else {
                props.showToastFn({
                    icon: "error",
                    title: res.message
                });
            }
        }).catch(err => {
            props.showToastFn({
                icon: "error",
                title: err.response.data.message
            });
        })
    }

    return (
        <div className={classes.loginPage}>
            <img src={logo} className={classes.logo} alt="" />
            <h2 className={classes.title}>Register Page</h2>
            <form className={classes.root}>
                <TextField
                    variant="outlined"
                    margin="normal"
                    required
                    fullWidth
                    id="phone"
                    placeholder="请输入手机号码"
                    name="phone"
                    autoComplete="phone"
                    autoFocus
                    value={phone}
                    onChange={(e) => setPhone(e.target.value)}
                />
                <TextField
                    variant="outlined"
                    margin="normal"
                    required
                    fullWidth
                    name="password"
                    placeholder="请输入密码"
                    type="password"
                    id="password"
                    autoComplete="password"
                    value={password}
                    onChange={(e) => setPassword(e.target.value)}
                />
                <Button
                    className={classes.btn}
                    type="button"
                    fullWidth
                    variant="contained"
                    onClick={submitFn}
                >立即注册</Button>
                <Link to="/login" variant="body2" style={{ color: '#02369d', marginTop: '20px' }}>返回登录</Link>
            </form>
            <section className={classes.copyright}>
                {'Copyright © '}
                <Link color="inherit" to="http://codesohigh.com">
                    你单排吧
                </Link>{' '}
                {new Date().getFullYear()}
                {'.'}
            </section>
        </div>
    );
}

const mapDispatchToProps = (dispatch) => {
    return {
        showToastFn(value) {
            dispatch({ type: "showToastFn", value })
            setTimeout(() => {
                dispatch({ type: "hideToastFn" })
            }, 2000)
        },
        hideToastFn() {
            dispatch({ type: "hideToastFn" })
        }
    }
}

export default connect(null, mapDispatchToProps)(SignIn);

然后在 request/api.js 中:

// 注册接口
export const RegisterApi = (params) => request.post("/1024/register", params)

十三、开发小提示

1、JSX条件判断

在react的class组件中如何使用 switch...case 语法?

<div style={{ backgroundColor: '#F8F8F8', height: '100%' }}>
  {
    (()=>{
      switch(page){
        case 'home':
          return <Home />;
        case 'fast':
          return <Fast />;
        case 'user':
          return <User />;
        default:
          break;
      }
    })()
  }
</div>

使用一个自执行函数,在函数体内使用 switch...case 。

2、路由跳转问题

当我们使用

this.props.history.push()

得到 push of undefined 的报错时,可以这么解决:

import {withRouter} from 'react-router-dom'

class Home extends Component {
  ...
}
  
// 关键一步
export default withRouter(Home)

如果使用的是函数式组件,还可以借助hook:

import {useHistory} from 'react-router-dom'

export default function Home(){
  return (...)
  fn(){
    let history = useHistory();
    history.push("/...")
  }
}

3、自定义进度条

import { withStyles } from '@material-ui/core/styles';
import LinearProgress from '@material-ui/core/LinearProgress';

const BorderLinearProgress = withStyles((theme) => ({
  root: {
    height: 10,
    borderRadius: 5,
  },
  colorPrimary: {
    backgroundColor: theme.palette.grey[theme.palette.type === 'light' ? 200 : 700],
  },
  bar: {
    borderRadius: 5,
    backgroundColor: '#2E57FF',
  },
}))(LinearProgress);

// 使用:
<BorderLinearProgress variant="determinate" value={this.state.progress} />

4、配置项目@指向src

在保证解包 yarn eject 之后,找到 webpack.config.js ,搜索 alias 对象,可以得到3个结果,索引第2个结果,并配置:

alias: {
  '@': path.join(__dirname, '../src'),
   ...
},

重跑项目即可使用@符号指向src。

5、组件中无法使用history?

页面在Router中配置了,但它下面的组件无法使用 this.props.history.push() 进行跳转,并且报了如下错误:

这是因为组件并没有使用 history 的能力,可以使用以上的提示2,也可以将配置过路由的父级的history传递给该组件:

// 父组件中:
<Subject history={this.props.history} />

// 子组件中:
this.props.history.push('/xxx', {title: "xxx"})

// 目标页面接收参数:
this.props.location.state.title;

6、setState是异步的

以下这段代码:

import React, { Component } from 'react';

class Pratice extends Component {
    constructor(props) {
        super(props)
        this.state = {
            num: 1
        }
    }
    render() {
        return (
            <div>
            	<tag onClick={this.handleClick.bind(this)}></tag>
            </div>
        );
    }
    // 每个选项点击后执行的代码
    handleClick(title, index) {
        this.setState({
          num: 2
        });
      	console.log(this.state.num);	// 这里打印出来是1
    }
}

export default Pratice;

为什么以上打印出来是1,而不是2呢?

因为setState是异步的,如果想要正常打印出2,需要:

handleClick(title, index) {
  this.setState({
    num: 2
  }, ()=>{
    console.log(this.state.num);	// 这里打印出来是2
  });
}

也就是,setState有第二个参数,属于回调函数。

7、统一使用消息提示

在使用消息提示的时候,我们每次都要单独写样式比较麻烦,这里提供代码,当你要使用 material-ui 的alert组件时,可以这样引入:

import MuiAlert from '@material-ui/lab/Alert';

<MuiAlert elevation={6} variant="filled" severity="error" className="my-mui-alert">弹框</MuiAlert>

记得先在公用的 base.less 中写入:

// 定义蓝色变量
@blue: #2E57FF;

// 定义灰色变量
@gray: #efefef;

// 消息提示的样式
.my-mui-alert{
    align-items: center;
    position: absolute;
    width: 100%;
    box-sizing: border-box;
    left: 0;
    top: 0;
    .MuiAlert-message{
        font-size: .4rem;
        padding: 0;
    }
}

这里注意:

severity属性规定了消息提示框的类型,分别有:

  • error - 错误提示(红色)

  • warning - 警告提示(橙色)

  • info - 信息提示(蓝色)

  • success - 成功提示(绿色 - 默认值)

至于消息提示框的显示隐藏,可以通过:

<MuiAlert style={{display: xxx ? 'flex' : 'none'}}>弹框</MuiAlert>

来控制。记住,是 display: flex 不是 display: block 。

Alert引用代码演示:

import React, { Component } from 'react';
import Select from '../components/Select'
import { PraticeApi, GetPraticeTestInfoApi } from '../request/api'
import "./less/Pratice.less"
import MuiAlert from '@material-ui/lab/Alert';

class Pratice extends Component {
    constructor(props) {
        super(props)
        this.state = {
            // 弹框提示
            showAlert: false,
            // 弹框类型
            alertType: "success",	// success | info | warning | error
            // 弹框内容
            alertContent: "弹框内容"
        }
    }
    render() {
        return (
            <div>
            		...
                <MuiAlert style={{display: this.state.showAlert ? 'flex' : 'none'}} elevation={6} variant="filled" severity={this.state.alertType} className="my-mui-alert">{this.state.alertContent}</MuiAlert>
            </div>
        );
    }
    // 考试模式
    examModelFn(){
        let _this = this;
        _this.setState({
            showAlert: true,
            alertType: "warning",
            alertContent: "该功能暂未开放"
        }, ()=>{
            setTimeout(()=>{
                _this.setState({
                    showAlert: false
                })
            }, 2000)
        })
    }
}

export default Pratice;

8、ReactRedux包装Alert

安装 redux 与 react-redux:

yarn add redux react-redux

将统一消息提示封装到组件 Alert.js 中:

import React, { Component } from 'react';
import MuiAlert from '@material-ui/lab/Alert';
import {connect} from 'react-redux'  //引入连接器

class Alert extends Component {
    render() {
        return (
            <MuiAlert style={{display: this.props.showAlert ? 'flex' : 'none'}} elevation={6} variant="filled" severity={this.props.alertType} className="my-mui-alert">{this.props.alertContent}</MuiAlert>
        );
    }
}

// stateToProps是一种映射关系,把原来的state映射成组件中的props属性
const stateToProps = (state)=>{
    return {
        showAlert : state.showAlert,
        alertType : state.alertType,
        alertContent : state.alertContent
    }
}

export default connect(stateToProps,null)(Alert);

src目录下创建 store/reducer.js :

const defaultState = {
    // 弹框提示
    showAlert: false,
    // 弹框类型
    alertType: "success",
    // 弹框内容
    alertContent: "弹框内容"
}

// eslint-disable-next-line
export default (state = defaultState, action) => {
    let newState = JSON.parse(JSON.stringify(state));
    switch(action.type){
        case "showAlert":
            newState.showAlert = action.value.showAlert;
            newState.alertType = action.value.alertType;
            newState.alertContent = action.value.alertContent;
            break;
        default:
            break;
    }
    return newState;
}

创建 store/index.js :

import { createStore } from 'redux'
import reducer from './reducer'

const store = createStore(reducer)
export default store

src/App.js 中引入 Alert 组件:

import React, { Component } from 'react';
import './App.less'
import Tabbar from './components/Tabbar'
import Alert from './components/Alert'

class App extends Component {
  render() {
    return (
      <div id="app">
        <Alert />
        {this.props.children}
        <Tabbar />
      </div>
    );
  }
}

// 这里不再是导出App,而是导出连接器
export default App;

最后在 <App /> 外套用提供器Provider:

...
import { Provider } from 'react-redux'	// 提供器
import store from '../store'						// store

export const BaseRoute = () => {
    return (
        <Router>
            <Route path="/" component={() =>
                <Provider store={store}>
                    <App>
                        <Switch>
                            ...
                        </Switch>
                    </App>
                </Provider>
            }></Route>
        </Router>
    )
}

后面某个页面想要触发 Alert ,就可以在该页面使用连接器如下:

...
import { connect } from 'react-redux'  //引入连接器

class Biancheng extends Component {
    render() {
        return (
            <div className="wenda" style={{display: this.props.questionType==="code" ? 'block' : 'none'}}>
                ...
                <div className="btn" onClick={this.sureBtnFn.bind(this)}>确定</div>
            </div>
        );
    }
    // 点击了确定按钮
    sureBtnFn() {
        let val = this.state.value;
        if (val.trim() === "") {
            // 提示不能为空
            this.props.showAlert({
                // 弹框提示
                showAlert: true,
                // 弹框类型
                alertType: "warning",	// success | info | warning | error
                // 弹框内容
                alertContent: "内容不能为空"
            });
            setTimeout(() => {
                this.props.showAlert({
                    // 弹框提示
                    showAlert: false
                });
            }, 2000)
        } else {
            // ...
        }
    }
}

// dispatchToProps也是一种映射,用于传递并修改数据,这里要返回一个对象并包含一个事件
const dispatchToProps = (dispatch) => {
    return {
        showAlert(value) {
            const action = {
                type: "showAlert",
                value
            }
            dispatch(action)
        }
    }
}

export default connect(null, dispatchToProps)(Biancheng);

9、React插入html

使用 dangerouslySetInnerHTML 属性。

class xxxx extends Component {
    state = {
        htmlCode: `
            <ul>
                <li>列表1</li>
                <li>列表2</li>
                <li>列表3</li>
            </ul>
        `
    }
    render() {
        return (
            <div dangerouslySetInnerHTML={{__html: this.state.htmlCode}}></div>
        );
    }
}

这里注意:html前面是两个下划线。

10、反序列化问题

在子组件中获取到的props,想要将props中的字符串转对象或数组,需要使用 JSON.parse(this.props.content || "[]") 。但接收props存在请求异步的延迟,因此会报错:

Unexpected end of JSON input

因此,我们可以改写为:

JSON.parse(this.props.content || "[]");

11、React实现移动端滑动

先给元素加上css定位:

ul{
    padding: 65px 0 0;
    position: relative;
    left: 0;
    top: 0;
    transition: left .5s linear;
    ...
}

js实现:

import React, { Component } from 'react';
...

// 手指触摸点
let startX = -1;
// 手指松开点
let endX = -1;
// 当前li的索引值
let liIndex = 0;

class Test extends Component {
    state = {
      ulLeft: 0
    }
    render() {
        return (
            <div style={{ width: '100%', overflow: 'hidden' }}>
                <Tabs liIndex={Math.abs(liIndex)+1} />
                <ul className="timu"
                    onTouchStart={this.handleTouchStart.bind(this)}
                    onTouchMove={this.handleTouchMove.bind(this)}
                    onTouchEnd={this.handleTouchEnd.bind(this)}
                    style={{ left: this.state.ulLeft }}>
                </ul>
            </div>
        );
    }
    // 手指触摸到屏幕
    handleTouchStart(e){
        startX = e.touches[0].clientX
    }
    // 手指滑动
    handleTouchMove(e){
        endX = e.touches[0].clientX
    }
    // 手指离开屏幕
    handleTouchEnd(e){
      	let _this = this;
        // 获取滑动范围
        if(startX>-1 && endX>-1){
            let distance = Math.abs(startX - endX);
            if (distance > 50) {
                // 两个手指位置距离相差50px,即视为要滑动
                if (startX > endX) {
                    liIndex--;
                  	// index是不能超过数组长度的
                    if (Math.abs(liIndex) >= _this.state.timuArr.length-1) {
                        liIndex = -_this.state.timuArr.length+1;
                    }
                } else {
                    liIndex++;
                    if(liIndex>=0){
                        liIndex=0;
                    }
                }
                this.setState({ulLeft: 100*liIndex+'%'}, ()=>{
                    startX = -1;
                    endX = -1;
                });
            }else{
                return;
            }
        }
    }
}

export default Test;

12、答题之多选组件UI部分代码

多选组件比单选组件复杂,需要给数组项中手动添加ifSelect=false。

/* eslint-disable */
import React, { Component } from 'react';
import "./less/Option.less"

// 多选
class Options extends Component {
    constructor(props){
        super(props);
        // 拿到传过来的content数组
        let arr = JSON.parse(props.content || "[]");
        // 每个数组项都添加一个ifSelect字段
        arr.map((item)=>{
            item.ifSelect = false;
        })
        this.state = {
            arr
        }
    }
    render() {
        return (
            <ul className="option">
                {
                    this.state.arr.map((item, index) => 
                        <li key={item.sort} onClick={this.liClick.bind(this, index)}>
                            <i className={item.ifSelect ? "iconfont icon-danxuanxiangxuanzhong" : "iconfont icon-normal"}></i>
                            <div className="answer">{item.key}. {item.value}</div>
                        </li>
                    )
                }
            </ul>
        );
    }
    // 每一项的点击事件
    liClick(index){
        // 做一次深拷贝
        let newArr = JSON.parse(JSON.stringify(this.state.arr));
        // 修改数组项中都ifSelect
        newArr[index].ifSelect = !newArr[index].ifSelect;
        this.setState({
            arr: newArr
        });
    }
}

export default Options;

13、事件参数与事件对象

<tag onClick={this.handleClick.bind(this, index)}></tag>

当我们这么写事件时,存在一个问题,事件对象的位置不知道是在index之前还是index之后,如:

class xx extends Component {
  // 正确写法
  handleClick(index, e){
    e.stopPropagation();	// 阻止冒泡
  }
  // 错误写法
  handleClick(e, index){
    e.stopPropagation();	// 此时e为索引值index
  }
}

14、禁用Link

想要禁用Link,开发阶段使用 BrowserRouter 可以使用:

<Link to="!#">xxx</Link>

但如果上线使用 HashRouter ,就会跳到404页面,因此需要补充:

<Link to="!#" disabled>xxx</Link>

// App.less中补上:
a[disabled] {
  pointer-events: none;
}

十四、项目打包

1、Hash模式打包

打包需要首先向 package.json 中添加:

{
  ...,
  "homepage": "./"
}

然后找到 router/index.js :

// 将BrowserRouter改成HashRouter
import { HashRouter as Router, Route, Switch, Redirect } from 'react-router-dom'

打包完成后,凡是使用了 this.props.location.state.xxx 传值的路由,都会碰上 Cannot read property 'xxx' of undefined 的报错。

原因是HashRouter模式不支持 this.props.location.state 获取数据,需要使用: this.props.location.params 。因此在路由传值的写法上,也要改为这种形式:

var query = {
  pathname: "/mmm",
  params: {key: value}
}
// 跳转到Toggle页面
this.props.history.push(query);

打包后可能会发现 history.goBack() 会造成上一级路由参数丢失,此时可以尝试 history.go(-1)。

2、History模式打包

在 package.json 中:

{
  "homepage": "http://www.abcd.com/yuantiku/"
}

找到 router/index.js:

import { BrowserRouter as Router, Route, Switch, Redirect } from 'react-router-dom'

<Router basename="/yuantiku/">
	...  
</Router>

但这里必须要有nginx的配置。

首先 ssh root@IP地址 连接上服务器,然后执行 whereis nginx,然后使用vim修改nginx.conf:

location /yuantiku {
  alias /usr/local/nginx/html/yuantiku;
  index index.html; # 默认访问的文件
  try_files $uri $uri/ /yuantiku/index.html;
}

修改完配置后,浏览器打开 http://www.abcd.com/yuantiku 即可。

3、跨域

生产环境下跨域需要nginx配合解决,如果你已在开发环境下使用 /api 代理 https://www.ahsj.link/ramb,那么nginx的配置为:

location /api {
  include  uwsgi_params;
  proxy_pass  https://www.ahsj.link/rambo;
}

十五、作业

作业1(初级)

使用Class Component完成微博发布功能。

作业2(中级)

使用Function Component完成微博发布功能。

作业3(高级)

使用Class Component完成Todolist。

作业4(顶级)

使用Function Component完成Todolist。

官网地址:

官网地址:

npm包路径:

IT猿题库【IT猿题库】 阅读密码:zhaowenxian

这里参照 material-ui 的 Bottom Navigation 底部导航栏 : 。

🌗
https://react.docschina.org/
https://v4.mui.com/zh/
https://www.npmjs.com/package/reset-css
http://www.docway.net/project/1eRv5Lh2UW9/share/1evckeXPiQy
https://material-ui.com/zh/components/bottom-navigation/
http://codesohigh.com/yuantiku
http://codesohigh.com/yuantiku
https://pan.baidu.com/s/1kTOSazkZ0i9MKnjLhQmiKQ
image-20210428111957851
image-20210924091435946
image.png
image-20210425154110249
image-20210425145931070
image-20210428115443204
image-20210514194820474