《IT猿题库》项目实战
最后更新于
这有帮助吗?
最后更新于
这有帮助吗?
项目地址:http://codesohigh.com/yuantiku
参考该项目及这份笔记,并尝试搭建项目。
结合React、React Redux、React Router、Hooks、Axios与material-ui,完成《IT猿题库》项目。
项目地址:http://codesohigh.com/yuantiku
界面效果:
链接: https://pan.baidu.com/s/1kTOSazkZ0i9MKnjLhQmiKQ 提取码: 9p5h
简介:使用React开发移动端项目
官网地址:https://react.docschina.org/
简介:一个基于 Preact / React / React Native 的 UI 组件库
使用 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
在 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 保存到本地
在 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>
)
}
}
然后就可以看到按钮被正式引入:
npm包路径:https://www.npmjs.com/package/reset-css
安装:
$ yarn add reset-css
使用:
// index.js中
import 'reset-css';
将设计图用photoshop打开,并上传至蓝湖。此时的设计图是3x尺寸,因此要勾选对应的1125px尺寸。
$ yarn add lib-flexible postcss-pxtorem
解包需要先做git提交,否则无法解包,因此先执行:
$ git add .
$ git commit -m 'eject之前的提交'
接下来直接解包:
$ yarn eject
解包后,可以看到项目目录下多了一个 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设计稿来的。
在 入口文件 index.js
里引入 lib-flexible
:
import 'lib-flexible'
在 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配置成功了。
但是,当你点开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的兼容问题了。
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
$ yarn add less less-loader@5.0.0
# 或者
$ npm install less less-loader@5.0.0
接下来要解包,如果上一步你已经解包过,就直接跳过。
如果未解包,请以上参考第五步。
找到 webpack.config.js
,搜索 sassRegex
:
const lessModuleRegex = /\.less$/;
搜索 sass-loader
后,在其下方添加:
module: {
...,
// less加载器
{
test: lessModuleRegex,
use: getStyleLoaders(
{
//暂不配置
},
'less-loader'
),
},
}
修改了配置文件,记得重新 yarn start
哦!
将 App.css
改为 App.less
进行测试,依然没问题。
在 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)
IT猿题库【IT猿题库】 http://www.docway.net/project/1eRv5Lh2UW9/share/1evckeXPiQy 阅读密码:zhaowenxian
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');
}
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
即可解决跨域。
我们做请求时的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)
componentDidMount(){
// 获取token,判断是否有token,有则做请求
let token = localStorage.getItem("x-auth-token");
if(token){
// 首页默认数据
HomeDefaultApi().then(res=>{
console.log(res)
})
}
}
安装 react-router-dom
:
$ yarn add react-router-dom
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')
);
可以看到:
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
,就会自动重定向到错误页面。
这里参照 material-ui
的 Bottom Navigation 底部导航栏
:https://material-ui.com/zh/components/bottom-navigation/ 。
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;
在 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>
);
}
如此,就实现了路由切换。
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>
)
}
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>
);
}
这里需要结合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;
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)(组件名称);
⚠️ 这里插播一句:登录注册页请直接套用代码就好!
效果如下:
将示例代码直接复制粘贴,然后稍做修改:
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
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)
在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
。
当我们使用
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("/...")
}
}
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} />
在保证解包 yarn eject
之后,找到 webpack.config.js
,搜索 alias
对象,可以得到3个结果,索引第2个结果,并配置:
alias: {
'@': path.join(__dirname, '../src'),
...
},
重跑项目即可使用@符号指向src。
页面在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;
以下这段代码:
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有第二个参数,属于回调函数。
在使用消息提示的时候,我们每次都要单独写样式比较麻烦,这里提供代码,当你要使用 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;
安装 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);
使用 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前面是两个下划线。
在子组件中获取到的props,想要将props中的字符串转对象或数组,需要使用 JSON.parse(this.props.content || "[]")
。但接收props存在请求异步的延迟,因此会报错:
Unexpected end of JSON input
因此,我们可以改写为:
JSON.parse(this.props.content || "[]");
先给元素加上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;
多选组件比单选组件复杂,需要给数组项中手动添加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;
<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
}
}
想要禁用Link,开发阶段使用 BrowserRouter
可以使用:
<Link to="!#">xxx</Link>
但如果上线使用 HashRouter
,就会跳到404页面,因此需要补充:
<Link to="!#" disabled>xxx</Link>
// App.less中补上:
a[disabled] {
pointer-events: none;
}
打包需要首先向 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)
。
在 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
即可。
生产环境下跨域需要nginx配合解决,如果你已在开发环境下使用 /api
代理 https://www.ahsj.link/ramb
,那么nginx的配置为:
location /api {
include uwsgi_params;
proxy_pass https://www.ahsj.link/rambo;
}
使用Class Component完成微博发布功能。
使用Function Component完成微博发布功能。
使用Class Component完成Todolist。
使用Function Component完成Todolist。