第三方库
redux
基础
import { createStore } from 'redux'
const reducer = (state = 0, action) => {
switch(action.type) {
case 'add':
return state + action.payload
case 'delete':
return state - action.payload
default:
return state
}
}
let store = createStore(reducer)
store.getState()
const action1 = {
type: 'add',
payload: 2,
}
const createAction = (val) => ({
type: 'add',
payload: val
})
store.dispatch(action1)
store.dispatch(createAction(111))
store.subscribe(() => {
console.log('change state')
})
简单实现
const createStore = (reducer, enhancer) => {
if (enhancer) {
return enhancer(createStore)(reducer)
}
let state
const getState = () => state
const dispatch = (action) => {
state = reducer(state, action)
return action
}
dispatch({})
return {
getState,
dispatch,
}
}
const compose = (...funcs) => {
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
const logger = ({
getState,
}) => next => action => {
console.log(getState());
next(action)
console.log(getState());
}
const thunk = ({
dispatch,
getState,
}) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState)
} else {
return next(action)
}
}
const applyMiddleWares = (...middlewares) => {
return (createStore) => (reducer) => {
const store = createStore(reducer)
let dispatch
let middlewareAPI = {
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args),
}
// const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = middlewares[0](middlewareAPI)(store.dispatch)
return {
...store,
dispatch
}
}
}
const reducer = (state = 10, action) => {
switch (action.type) {
case "ADD":
return state + action.payload
case "DELETE":
return state - action.payload
default:
return state
}
}
store = createStore(reducer, applyMiddleWares(thunk))
function fetchData(dispatch, getState) {
dispatch({
type: 'ADD',
payload: 10,
})
setTimeout(() => {
dispatch({
type: 'ADD',
payload: 30,
})
}, 2000)
}
store.dispatch(fetchData)
console.log(store.getState());
react-redux
redux
只是一个独立的状态管理库,为了在React中使用它我们还需要一个库react-redux
。
// action.js
export const CHANGE_CHANNEL = 'CHANGE_CHANNEL'
export const changeChannel = (channel) => ({
type: CHANGE_CHANNEL,
channel
})
// reducers.js
import {
CHANGE_CHANNEL
} from './action.js'
import { combineReducers } from 'redux'
const channel = (state = "nintendo", action) => {
switch(action.type) {
case CHANGE_CHANNEL:
return action.channel
default:
return state
}
}
const name = (state = "test", action) => {
return state
}
const rootReducer = combineReducers({
channel,
name,
})
export default rootReducer
// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import reducer from './reducers.js'
import App from './app.js'
let store = createStore(reducer)
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.querySelector('#root')
)
HOC用法
// app.js
import { connect } from 'react-redux'
import {
changeChannel
} from './action.js'
const App = ({
channel,
handlerClick,
}) => {
return (
<div>
<span>{channel}</span>
<button onClick={handlerClick}>Click me</button>
</div>
)
}
// 本质是运用的高阶组件,根据输入的UI组件APP生成容器组件
// mapStateToProps 把状态树中的状态映射进组件的props
const mapStateToProps = (state) => {
return {
// 组件的props 和 状态树中的state.channel对应
channel: state.channel
}
}
// mapDispatchToProps 把Dispatch方法映射为组件中props的方法
const mapDispatchToProps = (dispatch) => {
return {
handlerClick: (value) => {
dispatch(changeChannel(value))
}
}
}
// or mapDispatchToProps 也可以是个对象
// const mapDispatchToProps = {
// // 这里的函数是个action creator
// handlerClick: () => {
// type: 'add'
// }
// }
export default connect(
mapStateToProps,
mapDispatchToProps,
)(App)
Hook用法
import { useSelector, useDispatch } from 'react-redux'
const App = () => {
const channel = useSelector(state => state.channel)
const postsByChannel = useSelector(state => state.postsByChannel)
return (
<div>
<div>...</div>
</div>
)
}
react-router
import React from 'react'
import {
BrowserRouter as Router, // History mode而不是hash mode
Link,
Switch,
Route
} from 'react-router-dom' // web端用react-router-dom
function App () {
return (
<Router>
<Link to='/'>首页</Link>
<Link to='/blog'>博客</Link>
<Link to='/about'>关于我</Link>
<Switch>
<Route path='/about'>
<About />
</Route>
<Route path='/blog'>
<Blog />
</Route>
<Route path='/'>
<Home />
</Route>
</Switch>
</Router>
)
}
api
useLocation
可用于获取当前全局路由
// /profile/akara?name=aa#code
const location = useLocation()
{
hash: "#code"
pathname: "/profile/akara"
search: "?name=aa"
state: undefined
}
useHistory
可用于编程式导航
const history = useHistory()
history.push('/')
useParams
可用于获取动态路由/profile/:id
的参数id
// 当前路由/profile/akara
<Route path="/profile/:id">
<Test />
</Route>
const params = useParams() // { id: akara }
useRouteMatch
我们知道<Route path="">
的path
会和全局路由匹配,得到匹配的结果。
useRouteMatch
也是一样,用于查看全局路由和某个路径是否匹配
The
useRouteMatch
hook attempts to match the current URL in the same way that a<Route>
would
useRouteMatch
基本上有两种用法
- takes no argument and returns the match object of the current
<Route>
- takes a single argument, which is identical to props argument of matchPath. It can be either a pathname as a string (like the example above) or an object with the matching props that
Route
accepts, like this:
// 第一种用法
// 全局路由为/profile/akara
<Route path="/profile/:id">
<Test />
</Route>
function Test() {
// 不传参数,相当于拿到全局路由和<Route path="/profile/:id">的匹配结果
const match = useRouteMatch()
const {
path, // /profile/:id
url, // /profile/akara
params, // { id: akara }
} = match
return (
<div>
<Link to={`${url}/avatar`}>点击</Link>
<Route path={`${path}/avatar`}>
<div>content...</div>
</Route>
</div>
)
}
// 第二种用法
// 全局路由为/profile时,和/test匹配失败
const match = useRouteMatch({
path: '/test'
})
console.log(match); // null
<Redirect>
<Redirect
to={{
pathname: "/login",
state: { from: location }
}}
/>
嵌套路由
<Route path="">
全局路由一旦改变,Route
组件就会对比全局路由和path
的参数,若匹配就渲染对应的组件。
<Link to="/a/b">按钮1</Link>
<Link to="/b">按钮2</Link>
<Route path="/a">
<div>aaa</div>
<Route path="/b">
<div>bbb</div>
</Route>
<Route path="/a/b">
<div>abab</div>
</Route>
</Route>
如果我们点击按钮1时,全局路由变成/a/b
。
此时全局路由和<Route path="/a">
的path
成功匹配,渲染其对应组件;紧接着,因为全局路由又和Route
的子<Route path="/a/b">
成功匹配,渲染其对应组件。
而如果我们点击按钮2,全局路由变成/b
。
此时全局路由和<Route path="/a">
的path
匹配失败,什么也不渲染。
动态路由匹配
import {
...
useParams,
} from 'react-router-dom'
<Switch>
<Route path='/user/:id'>
<User />
</Route>
</Switch>
function User() {
let { id } = useParams()
return (
<div>
user: { id }
</div>
)
}
渐变动画
我们可以借助react-transition-group
库,来给路由的切换加上动画效果
react-transition-group
这个库本身不是动画库,不自带任何动画效果,它只是用来帮助我们更方便实现各种渐变或动画。
其原理是expose
了几个stages
,如enter
,entering/enter-active
,enter-done
重点:
当我们修改组件的状态/state,组件自然会重新渲染,即A视图 -> B视图
如果我们是增加元素Item(enter),那么在进入B视图的瞬间,渲染好内容,只是Item的透明度为0,逐渐调整为1;
如果我们是删除元素Item(exit),那么我们在A视图先将Item的透明度从1逐渐调整为0,最后再渲染B视图。
这里是用透明度来打比方,实际上这里可以是各种动画效果。
我们可以通过在这几个阶段设置的类名增加样式来实现渐变效果;也可以在这几个阶段的回调函数中搭配一些动画库来实现各种动画。
CSSTransition
import { CSSTransition } from 'react-transition-group'
<CSSTransition
in={show} // 根据该变量来管理stages
timeout={2000} // 动画时间
classNames="prefix" // 如果这里是a,之后会带上a-enter a-enter-active a-enter-done类名
unmountOnExit
>
<div>hello world</div>
</CSSTransition>
<button onClick={() => setShow(true)}>打开</button>
<button onClick={() => setShow(false)}>关闭</button>
in
根据in
来控制渐变:
当
in
变为true
时,元素带上类名prefix-enter prefix-enter-active
,并在timeout
时间后变成prefix-enter-done
当
in
变为false
时,元素带上类名prefix-exit prefix-exit-active
,并在timeout
时间后变成prefix-exit-done
由此我们知道,这个库是用来自动添加/删除类名的,我们可以自己写具体的样式来实现渐变效果。
/* 进入效果 */
.prefix-enter {
opacity: 0;
}
.prefix-enter-active {
opacity: 1;
transition: all 3s;
}
/* 退出效果 */
.prefix-exit-active {
opacity: 0;
transition: all 2s;
}
.prefix-exit {
opacity: 1;
}
当我们的in
变成true
,依次(看起来两个是一起添加,但应该是有顺序,只是太快了)给元素带上prefix-enter
和prefix-enter-active
,所以元素在最初的瞬间opacity
为0,之后渐变为1。
When the
in
prop is set totrue
, the child component will first receive the classexample-enter
, then theexample-enter-active
will be added in the next tick.CSSTransition
forces a reflow between before adding theexample-enter-active
. This is an important trick because it allows us to transition betweenexample-enter
andexample-enter-active
even though they were added immediately one after another.
appear
in
的true
和false
之间的转换,会触发类名的添加与删除。
如果我们进入页面时in
就是true
,是不会触发类名的添加与删除,也就做不了动画效果。这个使用我们可以加上appear
:
<CSSTransition
in={true}
timeout={2000}
classNames="prefix"
appear={true}
>
这样,当我们刚进去页面,元素会带上prefix-appear prefix-appear-active
,timeout
时间后,类名会被替换成prefix-appear-done prefix-enter-done
。
unmountOnExit
如果带上这个prop,那么元素在exit
时期就不会挂载(显示)
onEnter
enter
可替换成exit
,原理一样。回调函数中可以拿到元素node
,因此可以搭配一些动画库来实现酷炫的效果。
onEnter
A callback fired immediately after the 'enter' or 'appear' class is applied.
onEntering
A callback fired immediately after the 'enter-active' or 'appear-active' class is applied.
onEntered
A callback fired immediately after the 'enter' or 'appear' classes are removed and the
done
class is added to the DOM node.
<CSSTransition
// node: 元素
// appear: appear是否为true
onEnter={(node, appear) => {}}
>
TransitionGroup
用于管理多个渐变组件
<TransitionGroup>
{item.map(({ id, text }) => (
<CSSTransition
key={id}
timeout={500}
classNames="prefix"
>
<div>{text}</div>
</CSSTransition>
))}
</TransitionGroup>
当我们的item
变化时,组件重新渲染,即A视图 -> B视图。老的节点列表和新的节点列表(虚拟DOM)需要用key
来识别元素。
假设原本item
为A, B
如果给
item
新增元素C,生成新的虚拟DOM,通过key
对比新老虚拟DOM,发现C元素是新增的,那么更新真实DOM的时候给C元素加上了*-enter *-enter-active
类,从而实现动画效果。如果把
item
的A元素删除,生成新的虚拟DOM,通过key
对比新老虚拟DOM,发现A元素要删除,先给A元素加上*-exit *-exit-active
类,当元素类名变成*-exit-done
,再更新真实DOM。
配合react-router
// const location = useLocation()
<TransitionGroup>
<CSSTransition
key={location.key}
classNames="fade"
timeout={300}
>
<Switch location={location}>
<Route path="/hsl/:h/:s/:l" children={<HSL />} />
<Route path="/rgb/:r/:g/:b" children={<RGB />} />
</Switch>
</CSSTransition>
</TransitionGroup>
antd
虽然说这种组件库完全被必要背,但常见组件还是尽量多熟悉一点,提高开发效率,提前下班时间。
Input
// 1. 组件存在内部值,当输入新值的时候,会触发事件,将e.target.value的值赋给内部值
<Input />
// 2. 数据驱动,通过我们的数据来控制组件的内部值
<Input value={count} onChange={(e) => this.setState({count: e.target.value})}/>
Select
// 1.
<Select value="香蕉" >
<Option value="苹果">苹果</Option>
<Option value="香蕉">香蕉</Option>
<Option value="橘子">橘子</Option>
</Select>
// 2. 数据驱动
<Select value={v} onChange={(value) => this.setState({v: value})}> // 这里的参数不是事件e
<Option value="苹果">苹果</Option>
<Option value="香蕉">香蕉</Option>
<Option value="橘子">橘子</Option>
</Select>
Form
// 3. 依然是数据驱动,不过不需要我们自己维护数据了
<Form
name="data"
onFinish={this.handleFinish}
>
<Form.Item
label="用户名"
name="username"
rules={[{required: true, message: 'Please input your username!'}]}
>
<Input />
</Form.Item>
<Form.Item
label="密码"
name="password"
>
<Input />
</Form.Item>
<Form.Item
label="性别"
name="sex"
>
<Select> {/* defaultValue不行 */}
<Option value="male">male</Option>
<Option value="female">female</Option>
<Option value="other">other</Option>
</Select>
</Form.Item>
<Form.Item>
<Button htmlType="submit">提交</Button>
</Form.Item>
</Form>
我们可以通过getFieldsValue/setFieldsValue/resetFields
来获取/设置/清空表单字段的值。
如果是class
组件,则需要借助ref
;如果是函数组件,则需要使用Form.useForm
。
// class 组件
myRef = React.createRef()
// ...
this.myRef.current.getFieldsValue()
// ...
<Form ref={myRef}>
// 函数组件
const [form] = React.useForm()
// ...
form.getFieldsValue()
// ...
<Form form={form}>
Form布局
首先,Form有三种基本布局方式horizontal(default),vertical,inline
<Form layout="inline"></Form>
其次,无论何种基本布局,都可以使用labelCol/wrapperCol
来实现进一步的布局。这两个props既可以直接放在Form
里应用给所有表单项,也可以放在单个Form.Item
里应用给某一个表单项。
<Form
labelCol={{ span: 8 }}
wrapperCol={{ span: 14 }}
>
<Form.Item></Form.Item>
<Form.Item></Form.Item>
<Form.Item
labelCol={{ offset: 8 }}
></Form.Item>
</Form>
labelCol
用于表示label
,wrapperCol
用来表示Form.Item
的内容(Input/Select/Button
)。
而他们的值是个对象,包含span
和offset
,span
是值内容的宽度,而offset
则表示左右的偏移度。
另外,可以使用labelAlign
来调整label
的对齐方式,默认是right
即右对齐。
嵌套结构
表单的结构和接口的结构相对应的话,就无需自己处理数据的格式了。
<Item name="a"></Item>
<Item name="b"></Item>
<Item name={['c', 'c1']}></Item>
<Item name={['c', 'c2']}></Item>
// 结构
- a: 1
- b: 2
- c
- c1: 3
- c2: 4
数组结构
<Form.List name="userLists">
{(fields, {add}) => (
<>
{
fields.map(field => {
return (
<Space key={field.key}>
<Form.Item {...field} name={[field.name, 'name']}>
<Input />
</Form.Item>
<Form.Item {...field} name={[field.name, 'psw']}>
<Input />
</Form.Item>
</Space>
)
})
}
<Form.Item>
<Button onClick={() => add()}>增加</Button>
</Form.Item>
</>
)}
</Form.List>
// 结构
- userList: [{name: 'aaa', psw: 'bbb'}, {name: 'ccc', psw: 'ddd'}]
react-DnD
提供了拖放功能
import { DndProvider } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
function App() {
return (
<DndProvider backend={HTML5Backend}>
<Example />
</DndProvider>
)
}
被拖放元素,即Drag Source
function S() {
const [{ isDragging }, drag] = useDrag({
item: { type: 'test' },
collect: (monitor) => ({
isDragging: !!monitor.isDragging(), // 元素是否正在被拖动
}),
})
return (
<div ref={drag}></div>
)
}
放置区,即Drop Target
function T() {
const [{ isOver, canDrop }, drop] = useDrop({
accept: 'test', //
canDrop: () => canMoveKnight(x, y), // canDrop取决于函数的返回值
drop: () => moveKnight(x, y), // 当元素被拖进到放置区时调用该函数
collect: (monitor) => ({
isOver: !!monitor.isOver(), // 元素是否处于放置区上方
canDrop: !!monitor.canDrop(),
}),
})
return (
<div ref={drop}></div>
)
}
Item Type
比如有可拖动元素A和B,放置区C。希望只有A能被拖放到C上面,可以这样:
// A
item: { type: 'a' }
// B
item: { type: 'b' }
// C
accept: 'a'