跳到主要内容

第三方库

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,如enterentering/enter-activeenter-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来控制渐变:

  1. in变为true时,元素带上类名prefix-enter prefix-enter-active,并在timeout时间后变成prefix-enter-done

  2. 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-enterprefix-enter-active,所以元素在最初的瞬间opacity为0,之后渐变为1。

When the in prop is set to true, the child component will first receive the class example-enter, then the example-enter-active will be added in the next tick.CSSTransition forces a reflow between before adding the example-enter-active. This is an important trick because it allows us to transition between example-enter and example-enter-active even though they were added immediately one after another.

appear

intruefalse之间的转换,会触发类名的添加与删除。

如果我们进入页面时in就是true,是不会触发类名的添加与删除,也就做不了动画效果。这个使用我们可以加上appear:

 <CSSTransition
in={true}
timeout={2000}
classNames="prefix"
appear={true}
>

这样,当我们刚进去页面,元素会带上prefix-appear prefix-appear-activetimeout时间后,类名会被替换成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来识别元素。

假设原本itemA, B

  1. 如果给item新增元素C,生成新的虚拟DOM,通过key对比新老虚拟DOM,发现C元素是新增的,那么更新真实DOM的时候给C元素加上了*-enter *-enter-active类,从而实现动画效果。

  2. 如果把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用于表示labelwrapperCol用来表示Form.Item的内容(Input/Select/Button)。

而他们的值是个对象,包含spanoffsetspan是值内容的宽度,而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'

Mobx