# react hooks
# 创建项目
使用 react 脚手架 create-react-app 来创建,我们使用 npx,npx 能够下载全局命令行,然后执行完后移除,不会占有硬盘空间,使用命令 npx create-react-app 项目名 来创建项目。并且 npx 可以调用项目中安装的模块,比如我们项目装了 mocha,可以使用 npx mocha --version;
# react-scripts 工作原理
可以从 node_modules 中的 react-script 中看到 bin 里主要是执行的一些命令匹配。然后 script 中主要是一些通过开发环境的 webpack 的配置;通过 npm run eject 来释放这些配置文件,可以进行其他配置。
# 多页面开发配置
首先需要 npm run eject 来释放配置文件,注意这个命令是不可逆的。
# Context
实际开发项目的时候我们可能会经常碰到多层组件传值的情况,就是父组件的值传递给子组件。子组件再传递给下面的子组件,再传递给下面的子组件;可能会遇到这个情况。Context 提供了一种方式,能够让数据在组件树种传递而不必一级一级手动传递。
使用:
// createContext.jsx 文件
// 创建一个上下文的容器(组件), defaultValue 可以设置共享的默认数据
export const ButtonContext = React.createContext(defaultValue)
// onlineContext.jsx 文件
// 创建一个上下文的容器(组件), defaultValue 可以设置共享的默认数据
export const OnlineContext = React.createContext(defaultValue)
// 父级组件中-传递数据
import { ButtonContext } from './createContext'; //引入
import React, { Component } from 'react';
class Father extends Component {
state:{
choseType:'我是要传的值',
online: false
}
const { choseType, online } = this.state
render() {
return (
<div>
<ButtonContext.Provider value={choseType}>
{
// 有多个 Context 直接进行嵌套
}
<OnlineContext.Provider value={online}>
</OnlineContext.Provider>
</ButtonContext.Provider>
</div>
)
}
}
export default Father
// 子组件中接收
import { ButtonContext } from './createContext'
import React, { Component } from 'react'
class Children extends Component {
render() {
return (
<div>
<ButtonContext.Consumer>
{
// 这里必须要一个函数来获取值
}
{value => <h1>{value}</h1>)}
</ButtonContext.Consumer>
{
// 如果需要获取多个数值
<ButtonContext.Consumer>
{value => (
<OnlineContext.Consumer>
{
online => <h1>{value}={online}</h1>
}
</OnlineContext.Consumer>
)}
</ButtonContext.Consumer>
}
</div>
)
}
}
export default Children
# 类组件不足
# 状态逻辑复用难
- 缺少复用机制
- 渲染属性和高阶组件导致层级冗余
# 状态逻辑复用难
- 缺少复用机制
- 渲染属性和高阶组件导致层级冗余
# 趋向复杂难以维护
- 生命周期函数混杂不相干逻辑
- 相干逻辑分散在不同生命周期
# this指向
- 内联函数过度创建新句柄
- 类成员函数不能保证this
# Hooks优势
# 优化类组件的三大问题
- 函数组件无 this 问题(不牵扯实例化)
- 自定义 Hooks 方便复用状态逻辑(自定义hooks可以使用hooks相关api)
- 副作用的关注点分离
# hooks 常见的问题
# 对传统react编程的影响
- 声明周期函数如何映射到hooks?
function App() {
useEffect(() => {
//componentDidMount
return () => {
// componentWillMount
}
}, [])
let renderCounter = useRef(0)
renderCounter.current++
useEffect(() => {
if (renderCounter > 1) {
// componentDidUpdate
}
})
}
- 类实例成员变量如何映射到hooks上?
class App() {
it = 0
}
function App() {
// useRef也可以传入一个初始值参数
const it = useRef(0)
}
- Hooks中如何获取历史props和state
function Counter() {
const [count, setCount] = useState(0)
// 通过ref来保存上一次的count的值
const prevCountRef = useRef()
useEffect(() => {
prevCountRef.current = count
})
const prevCount = prevCountRef.current
return <h1></h1>
}
- 如何强制更新一个hooks组件
function Counter() {
const [updater, setUpdater] = useState(0)
// 组件内部更新数值,会间接更新组件
function forceUpdate() {
setUpdater(updater => updater + 1)
}
return <h1></h1>
}
# useState
他接收的可以是一个设置的初始值,也可以是一个回调函数,回调函数会延迟赋值:
const [count, setCount] = useState(() => {
return 0
})
然后setCount也可以接收一个函数,不过一般建议写函数:
setCount(x => x + 1)
useState是根据程序第一次运行的顺序返回对应的state。如果每次顺序不一致返回的结果也是混乱的。useState在初始化的时候就确定了返回值; 可以使用eslint-plugin-react-hooks插件进行帮助我们对useState的使用规范。 在package.json中进行配置:
"eslintConfig": {
"extends": "react-app",
"plugins": [
"react-hooks"
],
"rules": {
"react-hooks/rules-of-hooks": "error"
}
},
# useEffect
这个api相当于react几乎所有的的声明周期函数。根据[]里面的参数,如果里面的参数改变了,useEffect就会执行。 如果没有第二个参数,加入useEffect里面的数值发送改变就会执行
useEffect 的清除操作是在组件卸载的时候清除;在每次渲染更新执行当前组件的时候会清除上一次 effect 的状态。 useEffect 的第二个参数是一个数组,[] 代表只执行一次,如果里面包含多个值,意思是只要其中一个值变化,就执行 useEffect 里面的内容。
useEffect(() => {
// 如果没有第二个参数,这里的代码只执行一次,相当于 mouted vue 的。
return () => {
// 这里面只有组件注销的时候才会执行。
}
}, [])
useEffect(() => {
document.querySelector('#size').addEventListener('click', onClick, false)
return () => {
// 如果我们获取的dom元素每次会发生改变,会在不同的dom元素上绑定事件,需要在这里进行解绑,这样保证每次绑定的值是正确的。
document.querySelector('#size').removeEventListener('click', onClick, false)
}
})
# useRef
- 获取子组件或者 DOM 节点的句柄
- 渲染周期之间共享数据的存储
ref 在所有渲染的时候都保持着唯一的引用;在所有赋值取值都是拿到的最终的状态。
用法:
const ref = props => {
const ref = useRef()
useEffect(() => {
ref.current.value = props.value
})
return <input ref ={ ref } />
}
其他应用场景:调用子组件的方法
import React, {useState, useMemo, useRef, useCallback} from 'react'
import './App.css'
class Counter extends React.Component {
speak() {
console.log(`now counter is: ${this.props.count}`)
}
render() {
return <h1 onClick={this.props.onClick}>{this.props.count}</h1>
}
}
function App() {
const counterRef = useRef()
const [count, setCount] = useState(0)
const double = useMemo(() => {
return count * 2
}, [count === 3])
const onClick = useCallback(() => {
console.log('click')
// 这里注意我们需要使用current属性来获取到值
// 注意这里如果需要获取子组件的dom,子组件需要是类组件的声明格式
console.log(counterRef.current)
// 调用子组件的方法、
counterRef.current.speak()
}, [counterRef])
return (
<div>
{count}---double: {double}
<button onClick={() => {setCount(count + 1)}}>+</button>
<Counter ref={counterRef} count={count} onClick={onClick}></Counter>
</div>
)
}
export default App
另一个应用场景:如果在组件中需要访问上一次渲染的数据甚至是 state。可以使用 useref 进行保存
import React, {useState, useMemo, useRef, useCallback, useEffect} from 'react'
import './App.css'
class Counter extends React.Component {
speak() {
console.log(`now counter is: ${this.props.count}`)
}
render() {
return <h1 onClick={this.props.onClick}>{this.props.count}</h1>
}
}
function App() {
const counterRef = useRef()
const [count, setCount] = useState(0)
let it
const double = useMemo(() => {
return count * 2
}, [count === 3])
const onClick = useCallback(() => {
console.log('click')
// 这里注意我们需要使用current属性来获取到值
// 注意这里如果需要获取子组件的dom,子组件需要是类组件的声明格式
console.log(counterRef.current)
// 调用子组件的方法、
counterRef.current.speak()
}, [counterRef])
// 只执行一次
useEffect(() => {
setInterval(() => {
it = setCount(count => count + 1)
}, 1000)
}, [])
// 每次都执行
useEffect(() => {
if (count >= 10) {
// 这里不能直接清除,因为每次函数进行渲染,it都是不一样的
clearInterval(it)
}
})
return (
<div>
{count}---double: {double}
<button onClick={() => {setCount(count + 1)}}>+</button>
<Counter ref={counterRef} count={count} onClick={onClick}></Counter>
</div>
)
}
export default App
修改后:
import React, {useState, useMemo, useRef, useCallback, useEffect} from 'react'
import './App.css'
class Counter extends React.Component {
speak() {
console.log(`now counter is: ${this.props.count}`)
}
render() {
return <h1 onClick={this.props.onClick}>{this.props.count}</h1>
}
}
function App() {
const counterRef = useRef()
const [count, setCount] = useState(0)
let it = useRef()
const double = useMemo(() => {
return count * 2
}, [count === 3])
const onClick = useCallback(() => {
console.log('click')
// 这里注意我们需要使用current属性来获取到值
// 注意这里如果需要获取子组件的dom,子组件需要是类组件的声明格式
console.log(counterRef.current)
// 调用子组件的方法、
counterRef.current.speak()
}, [counterRef])
// 只执行一次
useEffect(() => {
setInterval(() => {
it.current = setCount(count => count + 1)
}, 1000)
}, [])
// 每次都执行
useEffect(() => {
if (count >= 10) {
// 这里不能直接清除,因为每次函数进行渲染,it都是不一样的
clearInterval(it.current)
}
})
return (
<div>
{count}---double: {double}
<button onClick={() => {setCount(count + 1)}}>+</button>
<Counter ref={counterRef} count={count} onClick={onClick}></Counter>
</div>
)
}
export default App
# useContext
Context 是用来解决共享数据的 API。
跟之前 Context 的对比:
import React, {useState, createContext, useContext } from 'react'
import './App.css'
// 创建 context
const CountContext = createContext()
function Counter () {
// 子组件接收 context 中的值
const count = useContext(CountContext)
return <h1>{count}</h1>
}
function App() {
const [count, setCount] = useState(0)
return (
<div>
{count}
<button onClick={() => {setCount(count + 1)}}>+</button>
{
// 使用 context 传递数据
}
<CountContext.Provider value={count}>
<Counter></Counter>
</CountContext.Provider>
</div>
)
}
export default App
# useMemo、useCallback
这两个API经常用来优化组件渲染,监听历来的值的变化,来决定是否重新对子组件渲染:
import React, {useState, useMemo, useCallback} from 'react'
import './App.css'
function Counter (props) {
return <h1 onClick={props.onClick}>{props.count}</h1>
}
function App() {
const [count, setCount] = useState(0)
// 第二个参数是依赖的变量组成的数组,如果第二个参数不传入,useMemo的逻辑每次都会运行
// 如果传入空数组就只会运行一次, 他与useEffect区别就是,在useEffect之前运行;并且他可以返回jsx进行渲染页面
// 只有count变化,double才会重新计算---类似vue的watch函数
const double = useMemo(() => {
return count * 2
}, [count === 3])
// useMemo可以依赖另一个memo
const half = useMemo(() => {
return double / 4
}, [double])
// const onClick = () => {
// console.log('Click')
// }
// 保证句柄一致,子元素点击时候,函数没有发生变化,子元素被渲染
// const onClick = useMemo(() => {
// return () => {
// console.log('Click')
// }
// }, [])
// 上面的写法可以写成下面的:如果useMemo(() => fn) <===> useCallback(fn)
const onClick = useCallback(() => {
console.log('Click')
// 第二个参数是useCallback依赖的两个变量
}, [])
return (
<div>
{count}---double: {double}
half: {half}
<button onClick={() => {setCount(count + 1)}}>+</button>
<Counter count={count} onClick={onClick}></Counter>
</div>
)
}
export default App
# 自定义hooks
hooks可以返回jsx参与渲染:
import React, {useState, useRef, useEffect} from 'react'
import './App.css'
function useCounter(count) {
return <h1>{count}</h1>
}
function useCount(defaultCount) {
const [count, setCount] = useState(defaultCount)
let it = useRef()
// 只执行一次
useEffect(() => {
setInterval(() => {
it.current = setCount(count => count + 1)
}, 1000)
}, [])
// 每次都执行
useEffect(() => {
if (count >= 10) {
// 这里不能直接清除,因为每次函数进行渲染,it都是不一样的
clearInterval(it.current)
}
})
return [count, setCount]
}
function App() {
const [count, setCount] = useCount(0)
const Counter = useCounter(count)
return (
<div>
<button onClick={() => {setCount(count + 1)}}>+</button>
{Counter}
</div>
)
}
export default App
另一个例子,加入项目中有很多地方需要获取窗口的大小;通过自定义hooks获取窗口尺寸:
两个组件分享了useSize的逻辑
import React, {useState, useRef, useEffect, useCallback} from 'react'
import './App.css'
function useCounter(count) {
const size = useSize()
return <h1>{size.width} -- {size.height}--{count}</h1>
}
function useSize() {
const [size, setSize] = useState({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
})
const onResize = useCallback(() => {
setSize({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
})
}, [])
useEffect(() => {
window.addEventListener('resize', onResize, false)
return () => [
window.removeEventListener('resize', onResize, false)
]
}, [])
return size
}
function App() {
const Counter = useCounter(count)
const size = useSize()
return (
<div>
{size.width} -- {size.height}
{Counter}
</div>
)
}
export default App
# Context
- Context
- ContextType
- lazy
- Superse
- memo 优化性能
Context 提供了一种方式,能够让数据在组件树中传递而不必一级一级手动传递。使用: 使用createContext这个API去创建一个context,他会给后代组件提供一个Provider进行管理,但是后代组件不能直接获取到值,需要使用Consumer进行获取。
如果context的值发送改变,他会重新渲染Provider下面的所有的元素。
import React, { createContext } from 'react'
import './App.css'
const BatterContext = createContext()
function Leaf() {
return (
<BatterContext.Consumer>
{
// 后代组件中听过Consumer获取共享的值,在Consumer中需要写一个函数。函数的唯一参数就是这个context的值。
battery => <h1>Battery: {battery}</h1>
}
</BatterContext.Consumer>
)
}
function Middle() {
return <Leaf />
}
function App() {
return (
<BatterContext.Provider value={60}>
<Middle/>
</BatterContext.Provider>
)
}
export default App
组件中使用多个context:
import React, { createContext, useState } from 'react'
import './App.css'
const BatterContext = createContext()
const OnlineContext = createContext()
function Leaf() {
return (
<BatterContext.Consumer>
{
battery => (
<OnlineContext.Consumer>
{
online => <h1>Battery: {battery}, Online: {String(online)}</h1>
}
</OnlineContext.Consumer>
)
}
</BatterContext.Consumer>
)
}
function Middle() {
return <Leaf />
}
function App() {
const [battery, setBattery] = useState(60)
const [online, setOnline] = useState(false)
return (
<BatterContext.Provider value = {battery}>
<OnlineContext.Provider value = {online}>
<button onClick={() => { setBattery(battery + 1)} }>+</button>
<button onClick={() => { setOnline(!online)} }>switch</button>
<Middle/>
</OnlineContext.Provider>
</BatterContext.Provider>
)
}
export default App
createContext 的参数是当Context.Consumer找不到Context.Provider的时候,进行获取到的值。
# contextType 的用法
代码:替换了Context.Consumer,
import React, { createContext, useState, Component } from 'react'
import './App.css'
const BatterContext = createContext()
const OnlineContext = createContext()
class Leaf extends Component {
static contextType = BatterContext
render() {
const battery = this.context
return (
<h1>Battery: {battery}</h1>
)
}
}
function Middle() {
return <Leaf />
}
function App() {
const [battery, setBattery] = useState(60)
const [online, setOnline] = useState(false)
return (
<BatterContext.Provider value = {battery}>
<button onClick={() => { setBattery(battery + 1)} }>+</button>
<Middle/>
</BatterContext.Provider>
)
}
export default App
# lazy
使用:About是另一个组件
import React, { lazy, Suspense } from 'react'
import './App.css'
const About = lazy(() => import(/*webpackChunkName: "about"*/'./About'))
class App extends React.Component {
render() {
// fallback传入一个jsx的组件
return (
<div>
<Suspense fallback = { <div>加载中</div> }>
<About/>
</Suspense>
</div>
)
}
}
export default App
使用componentDidCatch声明周期函数捕获渲染失败的问题:
import React, { lazy, Suspense } from 'react'
import './App.css'
const About = lazy(() => import(/*webpackChunkName: "about"*/'./About'))
class App extends React.Component {
state = {
hasError: false
}
componentDidCatch() {
this.setState({
hasError: true
})
}
render() {
if (this.state.hasError) {
return <div>Error</div>
}
// fallback传入一个jsx的组件
return (
<div>
<Suspense fallback = { <div>加载中</div> }>
<About/>
</Suspense>
</div>
)
}
}
export default App
也可以使用静态方法去捕获:
import React, { lazy, Suspense } from 'react'
import './App.css'
const About = lazy(() => import(/*webpackChunkName: "about"*/'./About'))
class App extends React.Component {
state = {
hasError: false
}
// componentDidCatch() {
// this.setState({
// hasError: true
// })
// }
static getDerivedStateFromError() {
return {
hasError: true
}
}
render() {
if (this.state.hasError) {
return <div>Error</div>
}
// fallback传入一个jsx的组件
return (
<div>
<Suspense fallback = { <div>加载中</div> }>
<About/>
</Suspense>
</div>
)
}
}
export default App
# 项目优化
父组件状态值改变会重新渲染子组件,可以使用shouldComponentUpdate进行优化: 在子组件中添加判断。
shouldComponentUpdate(nextProps, nextState) {
// console.log(nextProps, nextState)
// 如果即将渲染的的name跟当前的一直不进行渲染
if (nextProps.name === this.props.name) {
return false
}
return true
}
可以使用PureComponent组件代替上面的效果,不过存在局限性,传入的props如果是一个复杂的对象,不会检测是否发生变化:
import React, { Component, PureComponent } from 'react'
import './App.css'
class Foo extends PureComponent {
render() {
console.log('Foo render')
return null
}
}
class App extends PureComponent {
state = {
count: 0
}
render() {
return (
<div>
<button onClick={() => {this.setState({count: this.state.count + 1})}}>Press</button>
<Foo name="jiegiser"></Foo>
</div>
)
}
}
export default App
对于无状态组件,也就是函数式声明的组件,可以使用memo函数实现性能的优化:
import React, {PureComponent, memo } from 'react'
import './App.css'
const Foo = memo(function Foo() {
console.log('Foo render')
return null
})
class App extends PureComponent {
state = {
count: 0
}
render() {
return (
<div>
<button onClick={() => {this.setState({count: this.state.count + 1})}}>Press</button>
<Foo name="jiegiser"></Foo>
</div>
)
}
}
export default App
useMemo以及useCallback是经常用来优化项目的,监听依赖的变量,阻止在子元素中每次都进行渲染。
使用dispatch以及action以及hooks实现一个简单的todilist:
import React, {useState, useRef, useEffect, useCallback} from 'react'
import './App.css'
let idSeq = Date.now()
function Control(props) {
const { dispatch } = props
const inputRef = useRef()
const onSubmit = (e) => {
e.preventDefault()
const newText = inputRef.current.value.trim()
if (newText.length === 0) {
return
}
dispatch({
type: 'add',
payload: {
id: ++idSeq,
text: newText,
complete: false
}
})
inputRef.current.value = ''
}
return (
<div className="control">
<h1>
todos
</h1>
<form onSubmit={onSubmit}>
<input
type="text"
ref={inputRef}
className="new-todo"
placeholder="what needs to be done?"
/>
</form>
</div>
)
}
function TodoItem(props) {
const {todo: {
id,
text,
complete
}, dispatch} = props
const onChange = () => {
dispatch({
type: 'toggle',
payload: id
})
}
const onRemove = () => {
dispatch({
type: 'remove',
payload: id
})
}
return (
<li className="todo-item">
<input
type="checkbox"
onChange={onChange}
checked={complete}
/>
<label className={complete ? 'complete' : ''}>{text}</label>
<button onClick={onRemove}>×</button>
</li>
)
}
function Todos(props) {
const {todos, dispatch} = props
return (
<ul>
{
todos.map(todo => {
return (
<TodoItem
key={todo.id}
todo={todo}
dispatch={dispatch}
/>
)
})
}
</ul>
)
}
const LS_KEY = 'todo'
function TodoList() {
const [todos, setTodos] = useState([])
// const addTodo = (todo) => {
// setTodos(todos => [...todos, todo])
// }
// const removeTodos = (id) => {
// setTodos(todos => todos.filter(todo => {
// return todo.id !== id
// }))
// }
// const toggleTodo = (id) => {
// setTodos(todos => todos.map(todo => {
// return todo.id === id ? {
// ...todo,
// complete: !todo.complete
// }:todo
// }))
// }
// 使用dispatch以及action管理数据
const dispatch = useCallback((action) => {
const {type, payload} = action
switch(type) {
case 'set':
setTodos(payload)
break
case 'add':
setTodos(todos => [...todos, payload])
break
case 'remove':
setTodos(todos => todos.filter(todo => {
return todo.id !== payload
}))
break
case 'toggle':
setTodos(todos => todos.map(todo => {
return todo.id === payload ? {
...todo,
complete: !todo.complete
}:todo
}))
break
default:
}
}, [])
// 注意hooks函数是有顺序的
// 程序初始化调用,只执行一次
useEffect(() => {
const todos = JSON.parse(localStorage.getItem(LS_KEY) || '[]')
console.log(todos, 'todoGet')
// setTodos(todos)
dispatch({
type: 'set',
payload: todos
})
}, [])
// 只要todos变化就执行,监听todos
useEffect(() => {
localStorage.setItem(LS_KEY, JSON.stringify(todos))
console.log(todos, 'todoSet')
}, [todos])
return (
<div className="todo-list">
<Control dispatch={dispatch}></Control>
<Todos dispatch={dispatch} todos={todos}></Todos>
</div>
)
}
export default TodoList
样式部分:
.todo-list {
width: 550px;
margin: 300px auto;
background: #fff;
box-shadow: 0 2px 4px 0 rgb(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.control h1 {
width: 100%;
font-size: 100px;
text-align: center;
margin: 0;
color: rgba(175, 47, 47, 0.15);
}
.control .new-todo {
padding: 16px 16px 16px 60px;
border: 0;
outline: none;
font-size: 24px;
box-sizing: border-box;
width: 100%;
box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.3);
}
.todos {
margin: 0;
padding: 0;
list-style: none;
}
.todo-item {
margin: 0;
padding: 0;
list-style: none;
font-size: 24px;
display: flex;
align-items: center;
}
.todo-item label {
flex: 1;
padding: 15px 15px 15px 0;
line-height: 1.2;
display: block;
}
.todo-item label.complete {
text-decoration: line-through;
}
.todo-item button {
border: 0;
outline: 0;
display: block;
width: 40px;
text-align: center;
font-size: 30px;
color: #cc9a9a;
}
对于action每次都需要进行构建,可以新建一个actionCreators.js文件,进行构建action,actionCreators。
import React, {useState, useRef, useEffect} from 'react'
import {
createAdd,
createRemove,
createSet,
createToggle
} from './actionCreators'
import './App.css'
let idSeq = Date.now()
// 封装一个函数用来分发action。
function bindActionCreators(actionCreators, dispatch) {
const ret = {}
for(let key in actionCreators) {
ret[key] = function(...args) {
const actionCreator = actionCreators[key]
const action = actionCreator(...args)
dispatch(action)
}
}
return ret
}
function Control(props) {
const { addTodo } = props
const inputRef = useRef()
const onSubmit = (e) => {
e.preventDefault()
const newText = inputRef.current.value.trim()
if (newText.length === 0) {
return
}
addTodo({
id: ++idSeq,
text: newText,
complete: false
})
inputRef.current.value = ''
}
return (
<div className="control">
<h1>
todos
</h1>
<form onSubmit={onSubmit}>
<input
type="text"
ref={inputRef}
className="new-todo"
placeholder="what needs to be done?"
/>
</form>
</div>
)
}
function TodoItem(props) {
const {todo: {
id,
text,
complete
}, dispatch} = props
const onChange = () => {
dispatch(createToggle(id))
}
const onRemove = () => {
dispatch(createRemove(id))
}
return (
<li className="todo-item">
<input
type="checkbox"
onChange={onChange}
checked={complete}
/>
<label className={complete ? 'complete' : ''}>{text}</label>
<button onClick={onRemove}>×</button>
</li>
)
}
function Todos(props) {
const {todos, dispatch} = props
return (
<ul>
{
todos.map(todo => {
return (
<TodoItem
key={todo.id}
todo={todo}
dispatch={dispatch}
/>
)
})
}
</ul>
)
}
const LS_KEY = 'todo'
function TodoList() {
const [todos, setTodos] = useState([])
// 使用dispatch以及action管理数据
const dispatch = (action) => {
const {type, payload} = action
switch(type) {
case 'set':
setTodos(payload)
break
case 'add':
setTodos(todos => [...todos, payload])
break
case 'remove':
setTodos(todos => todos.filter(todo => {
return todo.id !== payload
}))
break
case 'toggle':
setTodos(todos => todos.map(todo => {
return todo.id === payload ? {
...todo,
complete: !todo.complete
}:todo
}))
break
default:
}
}
// 注意hooks函数是有顺序的
// 程序初始化调用,只执行一次
useEffect(() => {
const todos = JSON.parse(localStorage.getItem(LS_KEY) || '[]')
console.log(todos, 'todoGet')
// setTodos(todos)
dispatch(createSet(todos))
}, [])
// 只要todos变化就执行,监听todos
useEffect(() => {
localStorage.setItem(LS_KEY, JSON.stringify(todos))
console.log(todos, 'todoSet')
}, [todos])
return (
<div className="todo-list">
<Control
{
...bindActionCreators({
addTodo: createAdd
}, dispatch)
}
></Control>
<Todos
// {
// ...bindActionCreators({
// removeTodo: createRemove,
// toggleTodo: createToggle
// }, dispatch)
// }
dispatch={dispatch} todos={todos}></Todos>
</div>
)
}
export default TodoList
// actionCreators.js
export function createSet(payload) {
return {
type: 'set',
payload
}
}
export function createAdd(payload) {
return {
type: 'add',
payload
}
}
export function createRemove(payload) {
return {
type: 'remove',
payload
}
}
export function createToggle(payload) {
return {
type: 'toggle',
payload
}
}
使用reducer来管理数据,以及使用combineReducers合并多个reducer:
import React, {useState, useRef, useEffect} from 'react'
import {
createAdd,
createRemove,
createSet,
createToggle
} from './actionCreators'
import './App.css'
let idSeq = Date.now()
// 将多个reducer合并为一个总的reducers
function combineReducers(reducers) {
return function reducer(state, action) {
const change = {}
for (let key in reducers) {
change[key] = reducers[key](state[key], action)
}
return {
...state,
...change
}
}
}
// 封装一个函数用来分发action。
function bindActionCreators(actionCreators, dispatch) {
const ret = {}
for(let key in actionCreators) {
ret[key] = function(...args) {
const actionCreator = actionCreators[key]
const action = actionCreator(...args)
dispatch(action)
}
}
return ret
}
function Control(props) {
const { addTodo } = props
const inputRef = useRef()
const onSubmit = (e) => {
e.preventDefault()
const newText = inputRef.current.value.trim()
if (newText.length === 0) {
return
}
addTodo({
id: ++idSeq,
text: newText,
complete: false
})
inputRef.current.value = ''
}
return (
<div className="control">
<h1>
todos
</h1>
<form onSubmit={onSubmit}>
<input
type="text"
ref={inputRef}
className="new-todo"
placeholder="what needs to be done?"
/>
</form>
</div>
)
}
function TodoItem(props) {
const {todo: {
id,
text,
complete
}, dispatch} = props
const onChange = () => {
dispatch(createToggle(id))
}
const onRemove = () => {
dispatch(createRemove(id))
}
return (
<li className="todo-item">
<input
type="checkbox"
onChange={onChange}
checked={complete}
/>
<label className={complete ? 'complete' : ''}>{text}</label>
<button onClick={onRemove}>×</button>
</li>
)
}
function Todos(props) {
const {todos, dispatch} = props
return (
<ul>
{
todos.map(todo => {
return (
<TodoItem
key={todo.id}
todo={todo}
dispatch={dispatch}
/>
)
})
}
</ul>
)
}
const LS_KEY = 'todo'
function TodoList() {
const [todos, setTodos] = useState([])
const [incrementCount, setIncrementCount] = useState(0)
// 对reducer进行拆分:
const reducers = {
todos(state, action) {
const {type, payload} = action
switch(type) {
case 'set':
return payload
case 'add':
return [...state, payload]
case 'remove':
return state.filter(todo => {
return todo.id !== payload
})
case 'toggle':
return state.map(todo => {
return todo.id === payload ? {
...todo,
complete: !todo.complete
}:todo
})
}
return state
},
incrementCount(state, action) {
const {type} = action
switch(type) {
case 'add':
case 'set':
return state + 1
}
return state
}
}
const reducer = combineReducers(reducers)
// function reducer(state, action) {
// const {type, payload} = action
// const { todos, incrementCount } = state
// switch(type) {
// case 'set':
// return {
// ...state,
// todos: payload,
// incrementCount: incrementCount + 1
// }
// case 'add':
// return {
// ...state,
// todos: [...todos, payload],
// incrementCount: incrementCount + 1
// }
// case 'remove':
// return {
// ...state,
// todos: todos.filter(todo => {
// return todo.id !== payload
// })
// }
// case 'toggle':
// return {
// ...state,
// todos: todos.map(todo => {
// return todo.id === payload ? {
// ...todo,
// complete: !todo.complete
// }:todo
// })
// }
// }
// return state
// }
// 使用dispatch以及action管理数据
const dispatch = (action) => {
const state = {
todos,
incrementCount
}
const setters = {
todos: setTodos,
incrementCount: setIncrementCount
}
const newState = reducer(state, action)
for(let key in newState) {
setters[key](newState[key])
}
}
// 注意hooks函数是有顺序的
// 程序初始化调用,只执行一次
useEffect(() => {
const todos = JSON.parse(localStorage.getItem(LS_KEY) || '[]')
console.log(todos, 'todoGet')
// setTodos(todos)
dispatch(createSet(todos))
}, [])
// 只要todos变化就执行,监听todos
useEffect(() => {
localStorage.setItem(LS_KEY, JSON.stringify(todos))
console.log(todos, 'todoSet')
}, [todos])
return (
<div className="todo-list">
<Control
{
...bindActionCreators({
addTodo: createAdd
}, dispatch)
}
></Control>
<Todos
// {
// ...bindActionCreators({
// removeTodo: createRemove,
// toggleTodo: createToggle
// }, dispatch)
// }
dispatch={dispatch} todos={todos}></Todos>
</div>
)
}
export default TodoList
如果我们处理的action是异步操作,可以这样:
import React, {useState, useRef, useEffect} from 'react'
import {
createAdd,
createRemove,
createSet,
createToggle
} from './actionCreators'
import reducer from './reducer'
import './App.css'
// 封装一个函数用来分发action。
function bindActionCreators(actionCreators, dispatch) {
const ret = {}
for(let key in actionCreators) {
ret[key] = function(...args) {
const actionCreator = actionCreators[key]
const action = actionCreator(...args)
dispatch(action)
}
}
return ret
}
function Control(props) {
const { addTodo } = props
const inputRef = useRef()
const onSubmit = (e) => {
e.preventDefault()
const newText = inputRef.current.value.trim()
if (newText.length === 0) {
return
}
// addTodo({
// id: ++idSeq,
// text: newText,
// complete: false
// })
addTodo(newText)
inputRef.current.value = ''
}
return (
<div className="control">
<h1>
todos
</h1>
<form onSubmit={onSubmit}>
<input
type="text"
ref={inputRef}
className="new-todo"
placeholder="what needs to be done?"
/>
</form>
</div>
)
}
function TodoItem(props) {
const {todo: {
id,
text,
complete
}, dispatch} = props
const onChange = () => {
dispatch(createToggle(id))
}
const onRemove = () => {
dispatch(createRemove(id))
}
return (
<li className="todo-item">
<input
type="checkbox"
onChange={onChange}
checked={complete}
/>
<label className={complete ? 'complete' : ''}>{text}</label>
<button onClick={onRemove}>×</button>
</li>
)
}
function Todos(props) {
const {todos, dispatch} = props
return (
<ul>
{
todos.map(todo => {
return (
<TodoItem
key={todo.id}
todo={todo}
dispatch={dispatch}
/>
)
})
}
</ul>
)
}
const LS_KEY = 'todo'
let store = {
todos: [],
incrementCount: 0
}
function TodoList() {
const [todos, setTodos] = useState([])
const [incrementCount, setIncrementCount] = useState(0)
useEffect(() => {
Object.assign(store, {
todos,
incrementCount
})
}, [todos, incrementCount])
// 使用dispatch以及action管理数据
const dispatch = (action) => {
const setters = {
todos: setTodos,
incrementCount: setIncrementCount
}
if('function' === typeof action) {
action(dispatch, () => store)
return
}
const newState = reducer(store, action)
for(let key in newState) {
setters[key](newState[key])
}
}
// 注意hooks函数是有顺序的
// 程序初始化调用,只执行一次
useEffect(() => {
const todos = JSON.parse(localStorage.getItem(LS_KEY) || '[]')
console.log(todos, 'todoGet')
// setTodos(todos)
dispatch(createSet(todos))
}, [])
// 只要todos变化就执行,监听todos
useEffect(() => {
localStorage.setItem(LS_KEY, JSON.stringify(todos))
console.log(todos, 'todoSet')
}, [todos])
return (
<div className="todo-list">
<Control
{
...bindActionCreators({
addTodo: createAdd
}, dispatch)
}
></Control>
<Todos
// {
// ...bindActionCreators({
// removeTodo: createRemove,
// toggleTodo: createToggle
// }, dispatch)
// }
dispatch={dispatch} todos={todos}></Todos>
</div>
)
}
export default TodoList
// reducer.js
const reducers = {
todos(state, action) {
const {type, payload} = action
switch(type) {
case 'set':
return payload
case 'add':
return [...state, payload]
case 'remove':
return state.filter(todo => {
return todo.id !== payload
})
case 'toggle':
return state.map(todo => {
return todo.id === payload ? {
...todo,
complete: !todo.complete
}:todo
})
}
return state
},
incrementCount(state, action) {
const {type} = action
switch(type) {
case 'add':
case 'set':
return state + 1
}
return state
}
}
// 将多个reducer合并为一个总的reducers
function combineReducers(reducers) {
return function reducer(state, action) {
const change = {}
for (let key in reducers) {
change[key] = reducers[key](state[key], action)
}
return {
...state,
...change
}
}
}
export default combineReducers(reducers)
// actionCreators.js
export function createSet(payload) {
return {
type: 'set',
payload
}
}
let idSeq = Date.now()
export function createAdd(text) {
return (dispatch, getSate) => {
const { todos } = getSate()
if(!todos.find(todo => todo.text === text)) {
dispatch({
type: 'add',
payload: {
id: ++idSeq,
text,
complete: false
}
})
}
}
}
export function createRemove(payload) {
return {
type: 'remove',
payload
}
}
export function createToggle(payload) {
return {
type: 'toggle',
payload
}
}
# pwa
- Service Worker
- Promise
- fetch
- cache API
- Notification API
# Service Worker
服务工作线程
- 常驻内存运行
- 代理网络请求
- 依赖HTTPS
他是pwa最基础的部分。
# Promise
承诺控制流
- 优化回调地狱
- async/await语法同步化
- service worker的API风格
# Fetch
网络请求
- 比XMLHttpRequest更简洁
- Promise风格
- 依旧存在不足
# cache API
支持资源的缓存系统
- 缓存资源(css/script/image)
- 依赖service worker代理网络请求
- 支持离线程序运行
# Notification API
消息推送
- 依赖用户授权
- 适合在service worker中推送
# mpa 配置
首先修改config中的path文件:
module.exports = {
appHtml: resolveApp('public/index.html'),
// 新增四个
appQueryHtml: resolveApp('public/query.html'),
appTicketHtml: resolveApp('public/ticket.html'),
appOrderHtml: resolveApp('public/order.html'),
// 入口文件
appIndexJs: resolveModule(resolveApp, 'src/index/index'),
// 新增四个
appQueryJs: resolveModule(resolveApp, 'src/query/index'),
appTicketJs: resolveModule(resolveApp, 'src/ticket/index'),
appOrderJs: resolveModule(resolveApp, 'src/order/index'),
};
然后在wenpack.config.js中修改entry的配置:
entry: {
index: [paths.appIndexJs, isEnvDevelopment && require.resolve('react-dev-utils/webpackHotDevClient')].filter(Boolean),
query: [paths.appQueryJs, isEnvDevelopment && require.resolve('react-dev-utils/webpackHotDevClient')].filter(Boolean),
ticket: [paths.appTicketJs, isEnvDevelopment && require.resolve('react-dev-utils/webpackHotDevClient')].filter(Boolean),
order: [paths.appOrderJs, isEnvDevelopment && require.resolve('react-dev-utils/webpackHotDevClient')].filter(Boolean),
},
然后修改HtmlWebpackPlugin的模板打包html路径:
plugins: [
// Generates an `index.html` file with the <script> injected.
new HtmlWebpackPlugin(
Object.assign(
{},
{
inject: true,
template: paths.appHtml,
filename: 'index.html',
chunks: ['index']
},
isEnvProduction
? {
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}
: undefined
)
),
new HtmlWebpackPlugin(
Object.assign(
{},
{
inject: true,
template: paths.appQueryHtml,
filename: 'query.html',
chunks: ['query']
},
isEnvProduction
? {
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}
: undefined
)
),
new HtmlWebpackPlugin(
Object.assign(
{},
{
inject: true,
template: paths.appTicketHtml,
filename: 'ticket.html',
chunks: ['ticket']
},
isEnvProduction
? {
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}
: undefined
)
),
new HtmlWebpackPlugin(
Object.assign(
{},
{
inject: true,
template: paths.appOrderHtml,
filename: 'order.html',
chunks: ['order']
},
isEnvProduction
? {
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}
: undefined
)
)]
然后打包会报错,filter的问题,需要注释:new ManifestPlugin的generate属性
new ManifestPlugin({
fileName: 'asset-manifest.json',
publicPath: paths.publicUrlOrPath,
// generate: (seed, files, entrypoints) => {
// const manifestFiles = files.reduce((manifest, file) => {
// manifest[file.name] = file.path;
// return manifest;
// }, seed);
// const entrypointFiles = entrypoints.main.filter(
// fileName => !fileName.endsWith('.map')
// );
// return {
// files: manifestFiles,
// entrypoints: entrypointFiles,
// };
// },
}),
项目中使用prop-types来校验组件传值的格式是否正确,安装:npm i prop-types --save, 使用:
import PropTypres from 'prop-types'
Header.propTypes = {
onBack: PropTypres.func.isRequired,
title: PropTypres.string.isRequired
}
在项目中会经常使用动态加载类样式的情况,(跟vue写多个类一样)如果直接写在标签上,会很不方便, 可以使用第三方的库,安装:npm i classnames --save,使用:
import classnames from 'classnames'
classnames('city-selector', {
hidden: !show
})
// 之前的使用方法
<div className={['city-selector', (!show && 'hidden')].filter(Boolean).join(' ')}>
</div>
// 现在
<div className={ classnames('city-selector', { hidden: !show})}>
</div>
项目中使用dayjs来方便数据时间数据,安装:npm install dayjs --save
如果多页面项目编译不成功,需要修改webpackconfig.js:
// 修改输出文件的文件名:
filename: isEnvProduction
? 'static/js/[name].[contenthash:8].js'
: isEnvDevelopment && 'static/js/[name].js',
项目中用到解析url中的参数的库是urijs,首先进行安装:npm i urijs --save 使用:
useEffect(() => {
const queries = URI.parseQuery(window.location.search)
const {
from,
to,
date,
hightSpeed
} = queries
}, [])
// 设置请求参数
const url = new URI('/rest/query')
.setSearch('from', from)
.setSearch('to', to)
.setSearch('date', dayjs(departDate).format('YYYY-MM-DD'))
.setSearch('highSpeed', highSpeed)
.setSearch('orderType', orderType)
.setSearch('onlyTickets', onlyTickets)
.setSearch('checkedTicketTypes', Object.keys(checkedTicketTypes).join())
.setSearch('checkedTrainTypes', Object.keys(checkedTrainTypes).join())
.setSearch('checkedDepartStations', Object.keys(checkedDepartStations).join())
.setSearch('checkedArriveStations', Object.keys(checkedArriveStations).join())
.setSearch('departTimeStart', departTimeStart)
.setSearch('departTimeEnd', departTimeEnd)
.setSearch('arriveTimeStart', arriveTimeStart)
.setSearch('arriveTimeEnd', arriveTimeEnd)
.toString()
异步加载组件:
// 异步加载组件
const Schedule = lazy(() => import('./Schedule'))
<Suspense fallback={<div>loading</div>}>
<Schedule
date={departDate}
trainNumber={trainNumber}
departStation={departStation}
arriveStation={arriveStation}
/>
</Suspense>
工具库leftPad,left-pad可以在数字或者字符串前面格式化,比如下面在每个数字前端补一个零:
leftPad(index, 2, 0)
useContext的使用场景,比如我们在一个层级很深的子组件中需要获取store中的数据,那样一步步传值下去很是麻烦,可以使用context一步进行获取,
import { TrainContext } from './context'
<TrainContext.Provider value={
{
trainNumber,
departStation,
arriveStation,
departDate
}
}>
<Candidate
tickets={tickets}
/>
</TrainContext.Provider>
// './context'
import { createContext } from 'react'
export const TrainContext = createContext()
// 在子组件中进行获取
import { TrainContext } from './context'
const Channel = memo(function Channel(props) {
const {
trainNumber,
departStation,
arriveStation,
departDate,
} = useContext(TrainContext)
return (
<div className="channel">
</div>
)
})
const Seat = memo(function Seat(props) {
return (
<li>
<div
className="channels"
style={{ height: expanded ? channels.length * 55 + 'px' : 0 }}>
{
channels.map(channel => {
return (
<Channel key={channel.name} {...channel} type={type} />
)
})
}
</div>
</li>
)
})
const Candidate = memo(function Candidate(props) {
return (
<div className="candidate">
<ul>
{
tickets.map((ticket, idx) => {
return <Seat idx={idx} onToggle={onToggle} expanded={expandedIndex === idx} {...ticket} key={ticket.type}/>
})
}
</ul>
</div>
)
})
export default Candidate
react中类似vue的组件插槽的用法:
<Detail
departDate={departDate}
arriveDate={arriveDate}
departTimeStr={departTimeStr}
arriveTimeStr={arriveTimeStr}
trainNumber={trainNumber}
departStation={departStation}
arriveStation={arriveStation}
durationStr={durationStr}
>
<span
style={{ display: 'block' }}
className="train-icon"
></span>
</Detail>
// 在detail子组件中:
<p className="train-mid">
{/* <span className="left"></span>
<span className="schedule" onClick={() => {toggleIsScheduleVisible()}}>时刻表</span>
<span className="right"></span> */}
{
props.children
}
</p>
使用husky、lint-staged工具进行git提交格式化代码,安装:npm i husky lint-staged,进行配置:
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{js,jsx}": [
"eslint --fix",
"git add"
],
"*.{css,md}": [
"git add"
]
},
使用webpack-bundle-analyzer插件来进行打包分析,首先进行安装:npm i webpack-bundle-analyzer 使用,在webpack.config.js中进行配置:在进行打包的时候不会产生打包分析,
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
plugins: [
process.env.GENERATE_BUNDLE_ANALYZER === 'true' && new BundleAnalyzerPlugin({
openAnalyzer: false, // 是否打开8888服务器
analyzerMode: 'static' // 只生成静态html文件
}),
]
配置静态资源文件的引入地址,需要修改webpack.config.js中的publicPath,如下:
// output选项中的
publicPath:
'production' !== process.env.NODE_ENV ||
'true' === process.env.USE_LOCAL_FILES
? '/'
: 'https://www.cdn.com/',
配置service worker,首先在index.js文件中如引入:
import * as serviceWorker from '../serviceWorker'
if ('production' === process.env.NODE_ENV) {
serviceWorker.register()
} else {
serviceWorker.unregister()
}
评 论:
← react-redux 项目笔记 →