react技术栈
React的特点和优势
- 虚拟DOM
之前操作dom的⽅式是通过document.getElementById()的⽅式,这样的过程实际上是先去读取html的dom结构,将结构转换成变量,再进⾏操作
⽽reactjs定义了⼀套变量形式的dom模型,⼀切操作和换算直接在变量中,这样减少了操作真实dom,性能真实相当的⾼,和主流MVC框架有本质的区别,并不和dom打交道 - 组件系统
react最核⼼的思想是将⻚⾯中任何⼀个区域或者元素都可以看做⼀个组件 component
那么什么是组件呢?
组件指的就是同时包含了html、css、js、image元素的聚合体
使⽤react开发的核⼼就是将⻚⾯拆分成若⼲个组件,并且react⼀个组件中同时耦合了css、js、
image,这种模式整个颠覆了过去的传统的⽅式 - 单向数据流
其实reactjs的核⼼内容就是数据绑定,所谓数据绑定指的是只要将⼀些服务端的数据和前端⻚⾯绑定
好,开发者只关注实现业务就⾏了 - JSX 语法
在vue中,我们使⽤render函数来构建组件的dom结构性能较⾼,因为省去了查找和编译模板的过程,
但是在render中利⽤createElement创建结构的时候代码可读性较低,较为复杂,此时可以利⽤jsx语法
来在render中创建dom,解决这个问题,但是前提是需要使⽤⼯具来编译jsx
核心代码
main.js
全局定义将组件App放置在id='App'
的容器里
1 | import React from 'react' |
APP.jsx
1 | // 在 react 中,如要要创建 DOM 元素了,只能使用 React 提供的 JS API 来创建,不能【直接】像 Vue 中那样,手写 HTML 元素 |
1 | // 在React中,构造函数,就是一个最基本的组件 |
组件的生命周期
https://react.docschina.org/docs/react-component.html
https://m.html.cn/qa/react/14367.html
从出生到成长,最后到死亡,这个过程的时间可以理解为生命周期。React的生命周期同理也是这么一个过程。
React的生命周期分为三个阶段:挂载期(也叫实例化期)、更新期(也叫存在期)、卸载期(也叫销毁期)。在每个周期中React都提供了一些钩子函数。
生命周期的描述如下:
挂载期:一个组件实例初次北创建的过程。
更新期:组件在创建后再次渲染的过程。
卸载期:组件在使用完后被销毁的过程。
组件初始化阶段
组件实例创建阶段的生命周期函数,在组件的一辈子中,只执行一次;
constructor(props)
仅用于以下两种情况:
在为 React.Component 子类实现构造函数时,通过 super(props)调用父类React Component的构造函数,⽤来将⽗组件传来的 props 绑定到这个类中。否则,
this.props
在构造函数中可能会出现未定义的 bug。componentWillMount(17后已经过时): 组件将要被挂载,此时还没有开始渲染虚拟DOM,无法获取到页面上的任何元素,因为虚拟DOM和页面都还没有开始渲染呢。
进⾏ajax请求,作者一开始也喜欢在React的willMount函数中进行异步获取数据(认为这可以减少白屏的时间),后来发现其实应该在didMount中进行。
可以修改state
render:
render()
方法是 class 组件中唯一必须实现的方法。 第一次开始渲染,创建虚拟dom,当render执行完,内存中就有了完整的虚拟DOM了。但是,页面上尚未真正显示DOM元素componentDidMount:
componentDidMount()
会在组件挂载后(插入 DOM 树中)立即调用。依赖于 DOM 节点的初始化应该放在这里。- 网络请求获取数据
- 对DOM进行操作
- 这个方法是比较适合添加订阅的地方。如果添加了订阅,请不要忘记在
componentWillUnmount()
里取消订阅
组件更新阶段
更新根据组件的state和props的改变,有选择性的触发0次或多次;
componentWillReceiveProps(17后已经过时):
组件将要接收新属性props,此时,只要这个方法被触发,就证明父组件为当前子组件传递了新的属性值;
如果我们使用 this.props 来获取属性值,这个属性值,不是最新的,是上一次的旧属性值1
2
3componentWillReceiveProps(nextProps){
console.log(this.props.pmsg + ' ---- ' + nextProps.pmsg
);}shouldComponentUpdate: 组件是否需要被更新,此时,组件尚未被更新,但是,state 和 props 肯定是最新的。 首次渲染或使用
forceUpdate()
时不会调用该方法。1
shouldComponentUpdate(nextProps, nextState)
componentWillUpdate(17已经过时): 组件将要被更新,此时,尚未开始更新,内存中的虚拟DOM树还是旧的,页面上的 DOM 元素 也是旧的
render: 根据最新的 state 和 props 重新渲染一棵内存中的 虚拟DOM树,当 render 调用完毕,内存中的旧DOM树,已经被新DOM树替换了!此时页面还是旧的
componentDidUpdate: 此时,页面又被重新渲染了,state 和 虚拟DOM 和 页面已经完全保持同步
组件销毁阶段
也有一个显著的特点,一辈子只执行一次
1 | componentWillUnmount: 组件将要被卸载,此时组件还可以正常使用; |
新增
当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下:
- constructor()
- static getDerivedStateFromProps()
- render()
- componentDidMount()
当组件的 props 或 state 发生变化时会触发更新。组件更新的生命周期调用顺序如下:
- static getDerivedStateFromProps()
- shouldComponentUpdate()
- render()
- getSnapshotBeforeUpdate()
- componentDidUpdate()
错误处理
当渲染过程,生命周期,或子组件的构造函数中抛出错误时,会调用如下方法:
- static getDerivedStateFromError()
- componentDidCatch()
废弃的生命周期
被废弃的三个函数都是在render之前
为什么废弃
由于React
未来会推出新的渲染方式–异步渲染,一种生命周期可被打断的渲染方式(因为fiber的出现,很可能因为高优先级任务的出现而打断现有任务导致它们会被执行多次),具体是在render()
生成虚拟 dom
阶段可以打断重来, 这就会导致在dom挂载之前或是被更新之前的所有任务都会重复操作,所以componentWillMount()
、·componentWillReceiveProps()
componentWIllUpdate()
方法可能会执行多次。(函数内部逻辑多次调用)
componentWillMount
新版本中官方推荐将初始化的操作放在constructor()
中, 将请求异步数据、订阅事件源、监听事件的操作放在componentDidMount()
componentWillReceiveProps
在老版本的React
中,如果组件自身的state与其props密切相关的话,我们就会用到componentWillReceiveProps(nextProps)
。常见的业务场景比如,tabs的激活状态,一般我们会在组件自身内通过state维持,但是当我们从其他页面返回时,想要保持离开之前时的tabs状态,这时我们可以通过props来传递,(破坏了数据源的单一性)
1 | //previous |
该生命周期函数按照上面图谱中应该是在props属性改变之后调用,但其实只要父组件重新渲染,无论子组件的props有没有更新,子组件都会调用componentWillReceiveProps
注意这里可能会造成死循环,即当子组件在该方法中调用了父组件通过props传递过来的函数时, 恰巧该函数中有能让父组件重新渲染的逻辑,就会造成死循环
1 | class Parent extends Component { |
componentWillUpdate
getDerivedStateFromProps
使用getDerivedStateFromProps代替了旧的componentWillReceiveProps及componentWillMount
getDerivedStateFromProps是一个静态方法,在挂载和更新阶段时调用,可以返回一个对象来更新状态或者返回null不更新。
优点
getDSFP是静态方法,在这里不能使用this,也就是一个纯函数,开发者不能写出副作用的代码
开发者只能通过prevState而不是prevProps来做对比,保证了state和props之间的简单关系以及不需要处理第一次渲染时prevProps为空的情况
getSnapshotBeforeUpdate
getSnapshotBeforeUpdate代替了旧的componentWillUpdate。
getSnapshotBeforeUpdate()
在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期的任何返回值将作为参数传递给 componentDidUpdate()
。
1 | class ScrollingList extends React.Component { |
getDerivedStateFromError
1 | static getDerivedStateFromError(error) |
此生命周期会在后代组件抛出错误后被调用。 它将抛出的错误作为参数,并返回一个值以更新 state
1 | class ErrorBoundary extends React.Component { |
forceUpdate
默认情况下,当组件的 state 或 props 发生变化时,组件将重新渲染。如果 render()
方法依赖于其他数据,则可以调用 forceUpdate()
强制让组件重新渲染。
调用 forceUpdate()
将致使组件调用 render()
方法,此操作会跳过该组件的 shouldComponentUpdate()
。但其子组件会触发正常的生命周期方法,包括 shouldComponentUpdate()
方法。如果标记发生变化,React 仍将只更新 DOM。
核心概念
jsx
条件渲染
1 | function Greeting(props) { |
列表 & Key
key为每个元素加上唯一的标识,这样在执行diff的时候会加快位置的确定
1 | function NumberList(props) { |
在 JSX 中嵌入 map()
1 | function NumberList(props) { |
key 帮助 React 识别哪些元素改变了,比如被添加或删除。因此你应当给数组中的每一个元素赋予一个确定的标识。一个元素的 key 最好是这个元素在列表中拥有的一个独一无二的字符串。
如果使用数组索引,那么在对dom进行添加或删除,会出问题:
页面渲染好了之后,3 个 input 输入框依次输入随机内容,当我们用 index 作为 key 的时候,点击删除第一项按钮会发现,左侧文字正确改变,input 输入框最后一项没了,这不是我们希望的样子。 因为当我们使用 index 作为 key 时,此时 key 为 0、1、2,删掉第一项后 key 变为 0、1,此时 react 在执行 diff 算法过程中,任务 key=0 存在,只需要更新子节点的值,所以左侧的 name 成功改变,而 input 的值非受控,不会更新。同时在对比计算中少了 key=2 这项,删除了最后一项。
添加样式的方式
第一种:行内样式
想给虚拟dom添加行内样式,需要使用表达式传入样式对象的方式来实现:
1 | // 注意这里的两个括号,第一个表示我们在要JSX里插入JS了,第二个是对象的括号 |
动态添加样式
1 | <div style={{display: (index===this.state.currentIndex) ? "block" : "none"}}>此标签是否隐藏</div> |
1 | <div className={index===this.state.currentIndex?"active":null}>此标签是否选中</div> |
第二种:内嵌样式
1 | <style>{`.operafor4{margin-top:42px !important}`}</style> |
第三种:css modules
https://www.ruanyifeng.com/blog/2016/06/css_modules.html
CSS的规则都是全局的,任何一个组件的样式规则,都对整个页面有效。例如:父组件和子组件使用相同的class,父组件的class会覆盖子组件的样式
产生局部作用域的唯一方法,就是使用一个独一无二的class
的名字,不会与其他选择器重名。这就是 CSS Modules 的做法。
css Modules 添加多个className
1 | <div className={`${styles.sAll} ${styles.s1}`}>aaaaaa</div> |
CSS Modules 允许使用:global(.className)
的语法,声明一个全局规则。
1 | .title { |
第四种:样式组件(styled-components)
styled-components是针对React写的一套css-in-js框架,简单来讲就是在js中写css。
styled-components是一个第三方包,要安装。Material框架中的样式也是如此
表单和受控组件
在 HTML 中,表单元素(如<input>
、 <textarea>
和 <select>
)之类的表单元素通常自己维护 state,并根据用户输入进行更新。而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState()
来更新。
我们可以把两者结合起来,使 React 的 state 成为“唯一数据源”。渲染表单的 React 组件还控制着用户输入过程中表单发生的操作。被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”。
1 | class NameForm extends React.Component { |
由于在表单元素上设置了 value
属性,因此显示的值将始终为 this.state.value
,这使得 React 的 state 成为唯一数据源。由于 handlechange
在每次按键时都会执行并更新 React 的 state,因此显示的值将随着用户输入而更新。
对于受控组件来说,输入的值始终由 React 的 state 驱动
props
1 | function Welcome(props) { return <h1>Hello, {props.name}</h1>; |
defaultProps
无论是函数组件还是 class 组件,都拥有 defaultProps
属性。可以通过配置特定的 defaultProps
属性来定义 props
的默认值
1 | class Greeting extends React.Component { |
PropTypes类型检查
PropTypes 进行类型检查,可用于确保组件接收到的props数据类型是有效的
导入包
1 | import PropTypes from 'prop-types'; |
编写组件
1 | class Greeting extends React.Component { |
新增类型检查
1 | Greeting.propTypes = { |
this.props.children
将一个组件写在另一个组件的内容中,然后在外层组件中通过 this.props.children来接收内容中的组件
如果当前组件没有子节点,它就是 undefined ;
如果有一个子节点,数据类型是 Object;
如果有多个子节点,数据类型就是 Array。
setState
https://juejin.cn/post/6850418109636050958
https://juejin.cn/post/6959885030063603743#heading-0
1 | state = { |
setState是一个异步方法,如果每次调用setState都会触发更新,那么性能消耗就大,异步操作是为了提高性能,将多个状态更新合并一起进行批量更新,减少re-render调用。 将 setState()
视为请求而不是立即更新组件的命令。
1 | for ( let i = 0; i < 100; i++ ) { |
如果setState是一个同步执行的机制,那么这个状态会被重新渲染100次,这对性能是一个相当大的消耗。
React会将多个setState的调用合并为一个来执行,也就是说,当执行setState的时候,state中的数据并不会马上更新
回调函数
setState提供了一个回调函数供开发者使用,在回调函数中,我们可以实时的获取到更新之后的数据。还是以刚才的例子做示范:
1 | state = { |
总结:
setState本身并不是异步(不会立即更新state的结果),只是因为react的性能优化机制体现为异步。在react的生命周期函数或者作用域下为异步,在原生的环境下为同步。
React18之前,react 无法对 setTimeout 的代码前后加上事务逻辑(除非 react 重写 setTimeout)。
所以当遇到
setTimeout/setInterval/Promise.then(fn)/fetch 回调/xhr 网络回调
时,react 都是无法控制的(可以使用手动批处理)。- 在setTimeout,Promise.then等异步事件中。setState和useState是同步执行的(立即更新state的结果)
ref
react的核心思想是虚拟DOM。react包含了生成虚拟DOM的函数react.createElement,及Component类。而react-dom包的核心功能就是把这些虚拟DOM渲染到文档中变成实际DOM。
原生JS获取Dom
1 | import {Component} from "react"; |
Ref
使用场景
- 对Dom元素的焦点控制、内容选择、控制
- 对Dom元素的内容设置及媒体播放
- 对Dom元素的操作和对组件实例的操作
- 集成第三方 DOM 库
回调 Ref
支持在函数组件和类组件内部使用
使用回调 refs需要将回调函数赋值给 React元素 的 ref 属性。这个函数接受 React 组件 或 HTML 元素作为参数,将其挂载到实例属性上
React 会在组件挂载时,调用 ref
回调函数并传入 DOM元素,当卸载时调用它并传入 null
。在 componentDidMount
或 componentDidUpdate
触发前,React 会保证 Refs 一定是最新的。
1 | //类组件 |
1 | //函数组件 |
createRef
支持在类组件中使用
Refs API使用
1 | import React from 'react'; |
ref 的值根据节点的类型而有所不同:
一、当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性。
二、当 ref 属性用于自定义 class 组件时,ref 对象接收组件的 挂载实例 作为其 current 属性。
三、不能挂载到函数组件上,因为函数组件没有实例(instance)
但是,你可以在函数式组件中使用ref属性,就像你引用DOM元素和类组件一样。
useRef
只能在函数组件中使用
区别:https://zhuanlan.zhihu.com/p/105276393
useRef 用法类似于React.createRef(),区别:
createRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用。
useRef
返回的 ref 对象在组件的整个生命周期内保持不变。useRef 不仅仅是用来管理 DOM ref 的,它还相当于 this , 可以存放任何变量。useRef 可以很好的解决闭包带来的不方便性
1 | import React, { useRef } from "react"; |
useImperativeHandle
1 | useImperativeHandle(ref, createHandle, [deps]) |
- 通过useImperativeHandle可以只暴露特定的操作
- 通过useImperativeHandle的Hook, 将父组件传入的ref和useImperativeHandle第二个参数返回的对象绑定到了一起
- 所以在父组件中, 调用inputRef.current时, 实际上是返回的对象
- useImperativeHandle使用简单总结:
- 作用: 减少暴露给父组件获取的DOM元素属性, 只暴露给父组件需要用到的DOM方法
- 参数1: 父组件传递的ref属性
- 参数2: 返回一个对象, 以供给父组件中通过ref.current调用该对象中的方法
1 | import React, { useRef, forwardRef, useImperativeHandle } from 'react' |
1 | import React, { forwardRef, useImperativeHandle, useEffect, useRef } from 'react' |
forwardRef 转发/传递
React.forwardRef是转发ref 获取组件内的DOM节点 ,
以下两种场景中特别有用:
-
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import React from 'react';
const MyInput = React.forwardRef((props, ref) => {
return (
<input type="text" ref={ref} {...props} />
)
});
function Form() {
const inputRef = React.useRef(null);//class组件用createRef
React.useEffect(() => {
console.log(inputRef.current);//input节点
})
return (
<MyInput ref={inputRef} />
)
}- 调用
React.useRef
创建了一个React ref
并将其赋值给ref
变量。 - 指定
ref
为JSX属性,并向下传递<MyInput ref={inputRef}>
- React 传递
ref
给forwardRef
内函数(props, ref) => ...
作为其第二个参数。 - 向下转发该
ref
参数到<button ref={ref}>
,将其指定为JSX属性 - 当
ref
挂载完成,inputRef.current
指向input
DOM节点
- 调用
findDOMNode()
当组件加载到页面上之后(mounted),你都可以通过 react-dom
提供的 findDOMNode()
方法拿到组件对应的 DOM 元素。
1 | import { findDOMNode } from 'react-dom'; |
findDOMNode()
不能用在无状态组件上。
事件处理
react 事件的命名采用小驼峰式(camelCase),而不是纯小写。
使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串。
不能通过返回
false
的方式阻止默认行为。你必须显式的使用preventDefault
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = {isToggleOn: true};
// 为了在回调中使用 `this`,这个绑定是必不可少的
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(state => ({
isToggleOn: !state.isToggleOn
}));
}
render() {
return (
<button onClick={this.handleClick}>
{this.state.isToggleOn ? 'ON' : 'OFF'}
</button>
);
}
}
ReactDOM.render(
<Toggle />,
document.getElementById('root')
);
在 JavaScript 中,class 的方法默认不会绑定 this
。如果你忘记绑定 this.handleClick
并把它传入了 onClick
,当你调用这个函数的时候 this
的值为 undefined
。
绑定this:
在constructor中用bind绑定
箭头函数
回调中使用箭头函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14class LoggingButton extends React.Component {
handleClick() {
console.log('this is:', this);
}
render() {
// 此语法确保 `handleClick` 内的 `this` 已被绑定。
return (
<button onClick={() => this.handleClick()}>
Click me
</button>
);
}
}
组件
组件渲染机制
Component
在 state
改变,props
改变,调用this.setState({...})
,的时候都会进行渲染
强制React组件重新渲染
使用React的
forceUpdate
函数这是一个最明显的方式。在React类组件中,你可以通过调用这个方法,强制重渲染一个组件:
1
this.forceUpdate();
在React hooks中强制更新组件
在React hooks中,
forceUpdate
函数是无法使用的。你可以使用如下方式强制更新组件,并且不更改组件的state:1
2const [state, updateState] = React.useState();
const forceUpdate = React.useCallback(() => updateState({}), []);shouldComponentUpdate
1
2
3
4
5
6
7shouldComponentUpdate(nextProps,nextState) {
if(this.state.name === nextState.name) {
return false
}else {
return true
}
}通过memo来判断指定的参数变化更新组件
componentWillReciveProps
状态组件
状态组件对比
使用 function 创建的组件,叫做【无状态组件】;使用 class 创建的组件,叫做【有状态组件】
- 使用 function 构造函数创建的组件,内部没有 state 私有数据,只有一个props来接收外界传递过来的数据
- 使用 class创建的组件,内部,除了有 this.props 这个只读属性之外,还有一个 专门用于 存放自己私有数据的this.state 属性,这个 state 是可读可写的!
有状态组件和无状态组件,最本质的区别:
有无 state 属性;
class 创建的组件,有自己的生命周期函数,但是,function 创建的 组件,没有自己的生命周期函数;
问题来了:什么时候使用 有状态组件,什么时候使用无状态组件呢???
- 如果一个组件需要存放自己的私有数据,或者需要在组件的不同阶段执行不同的业务逻辑,此时,非常适合用 class 创建出来的有状态组件;
- 如果一个组件,只需要根据外界传递过来的 props,渲染固定的 页面结构就完事儿了,此时,非常适合使用 function 创建出来的 无状态组件;(使用无状态组件的小小好处: 由于剔除了组件的生命周期,所以,运行速度会相对快一丢丢)
class组件
用class关键字创建出来的组件:“有状态组件”
1 | // 使用 class 创建的类,通过 extends 关键字,继承了 React.Component 之后,这个类,就是一个组件的模板了 |
函数组件
函数/无状态组件是一个纯函数,它可接受接受参数,并返回react元素。这些都是没有任何副作用的纯函数。这些组件没有状态或生命周期方法
1 | // 组件的首字母必须是大写 |
内置组件
PureComponent
shouldComponentUpdate模拟
https://blog.csdn.net/deng1456694385/article/details/88746797
1 | class demo extent Component { |
上面的组件会在this.setState
调用后就会重新传染一次,但是我们可以看出name
状态并没有没被我们用到,也没有改变,这种渲染就是无效渲染,所以为了优化我们通常会使用钩子函数shouldComponentUpdate
来做一些逻辑判断,来确定是否要重新render
一次
1 | class demo extent Component { |
这样就可以避免无效渲染,优化性能,但是如果这种判断逻辑多到一定程度,光判断逻辑就很复杂,而且每次都要判断也会影响性能,所以才有了 PureComponent
,**PureComponent
的区别在于相当于自己写了一个shouldComponentUpdate
钩子函数处理, 对props
和state
进行浅比较,所谓浅比较就是之比较内部第一层的各个属性的值是否相同,像对象和数组这种数据类型,如果只改变内部的元素,就不会造成渲染**
PureComponent的浅比较
浅比较通过一个shallowEqual
函数来完成:
1 | function is(x, y) { |
Component vs PureComponent 总结
PureComponent相较于Component区别就是,对props和state默认进行判断来确定是否渲染,从而减少无效渲染次数. 大部分情况下直接用PureComponent比较好可以提高性能,但是如果遇到需要频繁修改值重新渲染的组件,用Component比较好,因为PureComponent频繁的判断也会影响性能.
memo
针对函数组件的,减少组件的不必要更新。 React.memo
仅检查 props 变更。如果函数组件被 React.memo
包裹,且其实现中拥有 useState
,useReducer
或 useContext
的 Hook,当 state 或 context 发生变化时,它仍会重新渲染。
1 | const TextCell = memo(function(props:any) { |
在这里如果没有用到memo 每次父组件重新setNumber,子组件都会重新渲染一次,加上了后只会在初始化的时候渲染(useMemo会在页面初始化的时候执行一次,并把执行的结果缓存一份),减少了子组件渲染的次数
默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。
1 | function MyComponent(props) { |
Fragment
无论是函数组件还是类组件,return 的 React 元素的语法必须是由一个标签包裹起来的所有虚拟 DOM 内容
一种是使用一个 div 标签将其包裹起来,另外一种方式就是使用 React 提供的 <React.Fragment>
将其包裹起来。但是我们不期望,增加额外的dom
节点,所以react
提供Fragment
碎片概念,能够让一个组件返回多个元素。
1 | render() { |
组件通信
props
适用于父子组件通信
父组件->子组件
父组件将需要传递的参数通过key={xxx}
方式传递至子组件,子组件通过this.props.key
获取参数.
子组件->父组件
利用 props callback 通信,父组件传递一个 callback 到子组件,当事件触发时将参数放置到 callback 带回给父组件.
1 | // 父组件 |
Context
https://zh-hans.reactjs.org/docs/context.html
数据是通过 props 属性自上而下(由父及子)进行传递的 ,需要显式地通过组件树的逐层传递 props。Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据
1 | // context.js |
当 Provider 的 value
值发生变化时,它内部的所有消费组件都会重新渲染。从 Provider 到其内部 consumer 组件(包括 .contextType 和 useContext)的传播不受制于 shouldComponentUpdate
函数,因此当 consumer 组件在其祖先组件跳过更新的情况下也能更新。
高阶函数与组件
高阶组件即高阶函数
,前面我们讲到,React遵循函数式开发,而高阶组件这个概念其实是React社区繁衍出来的概念。
在这里我们要谨记这一句话,组件 = 函数。
高阶函数,通俗的讲,就是把函数当作参数,传入另外一个函数当中,再返回一个函数。
实际应用场景
权限按钮
1 | import React, { FC } from 'react'; |
使用高阶组件
1 | import React from "react"; |
Hooks函数
http://www.ruanyifeng.com/blog/2019/09/react-hooks.html
- 纯函数组件没有状态
- 纯函数组件没有生命周期
- 纯函数组件没有
this
这就注定,我们所推崇的函数组件,只能做UI展示的功能,涉及到状态的管理与切换,我们不得不用类组件或者redux,但我们知道类组件的也是有缺点的,比如,遇到简单的页面,你的代码会显得很重,并且每创建一个类组件,都要去继承一个React实例,至于Redux,更不用多说,很久之前Redux的作者就说过,“能用React解决的问题就不用Redux”,等等一系列的话。关于React类组件redux的作者又有话说
- 大型组件很难拆分和重构,也很难测试。
- 业务逻辑分散在组件的各个方法之中,导致重复逻辑或关联逻辑。
- 组件类引入了复杂的编程模式,比如 render props 和高阶组件。
Hooks 优势
- 能优化类组件的三大问题
- 能在无需修改组件结构的情况下复用状态逻辑(自定义 Hooks )
- 能将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据)
React Hooks 的意思是,组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码”钩”进来。而React Hooks 就是我们所说的“钩子”。
useState():状态钩子
用于为函数组件引入状态(state)。纯函数不能有状态,所以把状态放在钩子里面。
原理
1 | //useState模拟1.0 |
1 | //一个组件用了两个useState怎么办?useState模拟2.0 |
在正常的react的事件流里(如onClick等)
setState和useState是异步执行的(不会立即更新state的结果,所以console数据没有更新)
多次执行setState和useState,只会调用一次重新渲染render
不同的是,setState会进行state的合并,而useState会进行state的覆盖
在setTimeout,Promise.then等异步事件中
setState和useState是同步执行的(立即更新state的结果,react17之后还是会批处理)
多次执行setState和useState,每一次的执行setState和useState,都会调用一次render
批处理
batch批量处理:在每次执行 useState 的时候,组件都要重新 render 一次,会造成无效渲染,浪费时间(因为最后一次渲染会覆盖掉前面所有的渲染效果)。 所以 react 会把一些可以一起更新的 useState/setState 放在一起,只渲染一次。
在React16版本及以前,React 会对所有React内部触发的事件监听函数中的更新(比如onClick函数)做批处理,如果是绕过react组件,如addEventListenr,或者异步调用如异步请求或者setTimeout等,不会进行批处理。在React17版本及之后,React会对所有的更新做批处理。
unstable_batchedUpdates手动批处理
1 | function handleClick3() { |
Tip
react中useState更新了组件,但是页面上的组件没有刷新
原因:useState更新的数据,是一个多层次的数据,react监听的时候,是浅层监听(默认开启 类 Object.is 的浅层比较,所以指向的地址不变),所以不一定及时刷新页面
解决办法:深拷贝,把需要更新的数据深拷贝一份,再使用useState 存储,就能实现每次都及时更新页面
useContext():共享状态钩子
如果需要在层层组件之间共享状态,可以使用useContext()
。
第一步就是使用 React Context API,在组件外部建立一个 Context。
1 const AppContext = React.createContext({});
组件封装代码如下。
1
2
3
4
5
6
7
8
9
10 // 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
// 无论多深,任何组件都能读取这个值。
<AppContext.Provider value={{
username: 'superawesome'
}}>
<div className="App">
<Navbar/>
<Messages/>
</div>
</AppContext.Provider>
上面代码中,AppContext.Provider
提供了一个 Context 对象,这个对象可以被子组件共享。
Navbar 组件的代码如下。
1
2
3
4
5
6
7
8
9 const Navbar = () => {
const { username } = useContext(AppContext);
return (
<div className="navbar">
<p>AwesomeSite</p>
<p>{username}</p>
</div>
);
}
useReducer()钩子
useReducer适用于引用类型,而useState适合值类型
1 | const [state, dispatch] = useReducer(reducer, initialArg, init) |
useReducer 接收三个参数,第一个参数为一个 reducer 函数,第二个参数是reducer的初始值,第三个参数为可选参数,值为一个函数,可以用来惰性提供初始状态。
reducer 接受两个参数一个是 state 另一个是 action ,用法原理和 redux 中的 reducer 一致
useReducer 返回一个数组,数组中包含一个 state 和 dispath,state 是返回状态中的值,而 dispatch 是一个可以发布事件来更新 state 的函数。
原理
useReucer 也是 useState 的内部实现
1 | let memoizedState |
实例
1 | const initialState = {count: 0}; |
useEffect():副作用钩子
纯函数只能进行数据计算,那些不涉及计算的操作(比如ajax 请求、访问原生dom 元素、本地持久化缓存、绑定/解绑事件、添加订阅、设置定时器、记录日志)应该写在哪里呢?
函数式编程将那些跟数据计算无关的操作,都称为 “副效应“ (side effect) 。
useEffect()
用来引入具有副作用的操作,最常见的就是向服务器请求数据。可以把 useEffect
Hook 看做 componentDidMount
,componentDidUpdate
和 componentWillUnmount
这三个函数的组合。
useEffect()
的用法如下:
1
2
3
4
5 useEffect(() => {
// Async Action
//return 则是在页面被卸载时调用.返回一个函数来指定如何“清除”副作用
return fn;
}, [dependencies])
上面用法中,useEffect()
接受两个参数。第一个参数是一个函数,异步操作的代码放在里面。第二个参数是一个数组,用于给出 Effect 的依赖项,只要这个数组发生变化,useEffect()
就会执行。第二个参数可以省略,这时每次组件渲染时,就会执行useEffect()
。
它的常见用途有下面几种:
- 获取数据(data fetching)
- 事件监听或订阅(setting up a subscription)
- 改变 DOM(changing the DOM)
- 输出日志(logging)
tips
它在第一次渲染之后和每次更新之后都会执行
使用
useEffect()
时,有一点需要注意。如果有多个副效应,应该调用多个useEffect()
,而不应该合并写在一起。在useEffect中,不仅会请求后端的数据,还会通过调用setData来更新本地的状态,这样会触发view的更新。
但是,运行这个程序的时候,会出现无限循环的情况。useEffect在组件mount时执行,但也会在组件更新时执行。因为我们在每次请求数据之后都会设置本地的状态,所以组件会更新,因此useEffect会再次执行,因此出现了无限循环的情况。我们只想在组件mount时请求数据。我们可以传递一个空数组作为useEffect的第二个参数,这样就能避免在组件更新执行useEffect,只会在组件mount时执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26import React, { useState, useEffect } from 'react';
import axios from 'axios';
function App() {
const [data, setData] = useState({ hits: [] });
useEffect(async () => {
const result = await axios(
'http://localhost/api/v1/search?query=redux',
);
setData(result.data);
}, []);
return (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
);
}
export default App;升级加载loading
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55import React, { Fragment, useState, useEffect } from 'react';
import axios from 'axios';
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
const [url, setUrl] = useState(
'http://hn.algolia.com/api/v1/search?query=redux',
);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
const result = await axios(url);
setData(result.data);
setIsLoading(false);
};
fetchData();
}, [url]);
return (
<Fragment>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<button
type="button"
onClick={() =>
setUrl(`http://localhost/api/v1/search?query=${query}`)
}
>
Search
</button>
{isLoading ? (
<div>Loading ...</div>
) : (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
)}
</Fragment>
);
}
export default App;
useCallback和useMemo
https://www.jianshu.com/p/014ee0ebe959
useCallback和useMemo都会在组件第一次渲染的时候执行,之后会在其依赖的变量发生改变时再次执行;并且这两个hooks都返回缓存,useMemo返回缓存的变量,useCallback返回缓存的函数。
1 | type DependencyList = ReadonlyArray<any>; |
React 中当组件的 props 或 state 变化时,会重新渲染视图
useCallback
父组件给子组件传递属性(函数),父组件重新渲染,会重新创建函数,对应函数地址改变,即传给子组件的属性发生了变化,导致子组件渲染。
1 | const TextCell = memo(function(props:any) { |
这里如果不使用useCallback,哪怕子组件用memo包裹了 也还是会更新子组件,因为子组件的绑定的函数click在父组件更新的时候也会更新引用地址,导致子组件的更新,但是这个其实是没必要的更新,绑定的函数并不需要子组件更新,useCallback就是阻止这类没必要的更新而存在的
这里需要注意的是 如果是有参数需要传递,则需要这样写
1 | <TextCell click={useCallback(()=>handleClick(‘传递的参数’),[])}/> |
作用
防止死循环
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// 用于记录 getData 调用次数
let count = 0;
function App() {
const [val, setVal] = useState("");
function getData() {
setTimeout(() => {
setVal("new data " + count);
count++;
}, 500);
}
return <Child val={val} getData={getData} />;
}
function Child({val, getData}) {
useEffect(() => {
getData();
}, [getData]);
return <div>{val}</div>;
}执行过程:
App
渲染Child
,将val
和getData
传进去Child
使用useEffect
获取数据。因为对getData
有依赖,于是将其加入依赖列表getData
执行时,调用setVal
,导致App
重新渲染App
重新渲染时生成新的getData
方法,传给Child
Child
发现getData
的引用变了,又会执行getData
- 3 -> 5 是一个死循环
useMemo
调试
useEventListener
如果你发现自己使用useEffect添加了许多事件监听,那你可能需要考虑将这些逻辑封装成一个通用的hook。
useWhyDidYouUpdate
这个hook让你更加容易观察到是哪一个prop的改变导致了一个组件的重新渲染。
useLockBodyScroll
有时候当一些特别的组件在你们的页面中展示时,你想要阻止用户滑动你的页面(想一想modal框或者移动端的全屏菜单)。
路由
路由是一种向用户显示不同页面的能力。 这意味着用户可以通过输入 URL 或单击页面元素在 WEB 应用的不同部分之间切换
这里需要说明一下 React Router 库中几个不同的 npm 依赖包,每个包都有不同的用途
相关组件 | 功能 |
---|---|
react-router | 实现了路由的核心功能,用作下面几个包的运行时依赖项(peer dependency)。 |
react-router-dom | 基于 react-router 添加了浏览器运行环境的一些组件和功能。 |
react-router-native | 适用于 React Native |
react-router-redux | React Router 和 Redux 的集成。 |
eact-router-config | 提供可配置化的路由 |
React Routers三类组件
路由器Router
<BrowserRouter>
和<HashRouter>
,两者之间的主要区别是它们存储URL和与Web服务器通信的方式。
BrowserRouter
<BrowserRouter>
使用常规的URL路径。但它们要求正确配置服务器。具体来说,您的Web服务器需要在所有由React Router客户端管理的URL上提供相同的页面
BrowserRouter提供了如下属性
basename (string)
当前位置的基准 URL。当应用程序放置于服务器上子目录中时,可以设置,比如/public
。forceRefresh (boolean)
,在导航的过程中整个页面是否刷新getUserConfirmation (func)
,当导航需要确认时执行的函数。默认是:window.confirmkeyLength (number)
location.key 的长度。默认是 6children (node)
要渲染的子节点
HashRouter
<HashRouter>
将当前位置存储在URL的hash
一部分中,因此URL看起来像http://example.com/#/your/page
。由于哈希从不发送到服务器,因此这意味着不需要特殊的服务器配置(在任意的路由进行页面的刷新都不会是 404)。
HashRouter提供了如下属性
basename: string
, 同<BrowserRouter>
的basename
。getUserConfirmation: function
, 同<BrowserRouter>
的getUserConfirmation
。hashType: string
, Hash 编码类型,可选值'slash'(默认) | 'noslash' | 'hashbang'
。slash
, 创建像#/
,#/user/1
这样的 hash 地址,默认值。noslash
, 创建像#
,#user/1
这样的 hash 地址hashbang
, 创建像#!/
,#!/user/1
这样的 ajax crawlable(已被 Google 遗弃) 的 hash 地址
children: node
, 同<BrowserRouter>
的children: node
。
原理
HashRouter:使用 URL 的哈希值实现
原理:监听 window 的 hashchange
事件来实现的
BrowserRouter(推荐):使用 H5 的 history.pushState() API 实现
原理:监听 window 的 popstate
事件来实现的
BrowserRouter组件都会创建一个 history
实例对象,它记录了当前的位置,还记录了堆栈中以前的位置。在当前位置发生变化时,页面会被重新渲染,于是你就有一种导航跳转的感觉。
那么如何改变当前的位置呢?也就是说如何做到导航跳转呢?这时候 history
的作用就来了,这个对象暴露了一些方法,比如 history.push
和 history.replace
,它们就可以拿来处理上面的问题。
当你点击一个 <Link>
组件时,history.push
就会被调用,而当你使用一个 <Redirect>
组件时,history.replace
就会被调用。其它的方法比如 history.Back
和 history.Forward
可以用来在历史堆栈中回溯或前进。
参数
basename: string
原因是:ngix服务器上面要放不止一个网站 根目录下面已经有一个网站,这个网站需单独建一个文件夹。
作用:为所有位置添加一个基准URL
使用场景:假如你需要把页面部署到服务器的二级目录,你可以使用basename
设置到此目录。
路线匹配器Route
Route
内联函数
https://www.jianshu.com/p/76ee90125e9f
1 | <span> |
如果我们使用内联函数,则每次调用“render”函数时都会创建一个新的函数实例。
当 React 进行虚拟 DOM diffing 时,它每次都会找到一个新的函数实例;因此在渲染阶段它会会绑定新函数并将旧实例扔给垃圾回收。
因此直接绑定内联函数就需要额外做垃圾回收和绑定到 DOM 的新函数的工作。
三种渲染方式
https://www.cnblogs.com/ypSharing/p/15587340.html
优先级是 children > component > render。
<Route component>
1
2
3<Route exact path="/home" component={Home} /> //推荐
或
<Route exact path="/home" component={()=><Home />} /> // 内联函数参数:对象
<Route path='/home' component={home}/>
- 直接使用组件类–使用最多的方式
- 缺点:不能把父组件中的数据通过props传递给路由组件中
参数:函数
<Route path='/home' component={()=><home/>} />
使用函数,可以写条件判断,根据条件来渲染不同的组件
可以通过props来完成父组件中的数据向路由渲染组件传递
缺点:每次匹配路由成功都会从新创建组件—效率低下,不建议使用
路由会使用
React.createElement
从指定的组件中创建一个新的React元素。这意味着,如果你向组件属性提供内置函数,则将在每个渲染中创建一个新组件。这将导致现有组件的卸载和新组件的安装,而不是仅更新现有组件。使用内置函数进行内联渲染时,应使用render
或children
属性。
<Route render={(props)=>{return <component/>}}>
render方式渲染,使用函数方式
如果匹配相同,则不重新创建,效率高
建议如果组件对象方式渲染(函数方式)推荐使用render
1
2
3
4
5
6
7<Route path='/home' render={(props)=>{
if(this.state.count==1){
return <Home1 count={this.state.count}/>
}else{
retutn <Home2/>
}
}}/>
<Route children>
组件对象方式:必须匹配到path的路由规则才渲染和render与component一样
<Route path="/about" children={<About />} />
函数方式:不管是否和path匹配都渲染
1
2
3
4
5
6
7// 在匹配时,容器的class是light,<Home />会被渲染
// 在不匹配时,容器的class是dark,<About />会被渲染
<Route path='/home' children={({ match }) => (
<div className={match ? 'light' : 'dark'}>
{match ? <Home/>:<About>}
</div>
)}/>一、它同 render 类似,是一个 function。不同的地方在于它会被传入一个 match 参数来告诉你这个 Route 的 path 和 location 匹配上没有。
二、第二个特殊的地方在于,即使 path 没有匹配上,我们也可以将它渲染出来。秘诀就在于前面一点提到的 match 参数。我们可以根据这个参数来决定在匹配的时候渲染什么,不匹配的时候又渲染什么。
参数
exact 是否进行精确匹配,路由
/a
可以和/a/、/a
匹配当exact为false时,根据路由匹配所有组件,例如/a/b/c 能匹配到/、/a、/a/b、/a/b/c 且匹配还是按顺序的
例如路由设置的前后顺序为:
1./ ;
2./a;
3./a/b ;
4./a/b/c
且前3个路径都没有设置 exact,这样前3个组件都会被渲染并且默认将2当作1的子页面,3当作2的子页面strict
是否进行严格匹配,指明路径只匹配以斜线结尾的路径,路由/a
可以和/a
匹配,不能和/a/
匹配,相比exact
会更严格些path (string)
标识路由的路径,path
属性可以使用通配符。1
<Route path="/hello/:name">
通配符的规则如下:
paramName
:paramName
匹配URL的一个部分,直到遇到下一个/
、?
、#
为止。这个路径参数可以通过this.props.params.paramName
取出。()
()
表示URL的这个部分是可选的。*
*
匹配任意字符,直到模式里面的下一个字符为止。匹配方式是非贪婪模式。**
**
匹配任意字符,直到下一个/
、?
、#
为止。匹配方式是贪婪模式。
component
表示路径对应显示的组件location (object)
除了通过 path 传递路由路径,也可以通过传递 location 对象可以匹配sensitive (boolean)
匹配路径时,是否区分大小写
Swtich
Swtich
就近匹配路由,仅渲染一个路由,路由的默认行为是匹配了就直接渲染
1 | /// 假设你访问的URL为 /dog |
链接
Link
<Link>
组件被用来在页面之间进行导航,它其实就是 HTML 中的 <a>
标签的上层封装,不过在其源码中使用 event.preventDefault
禁止了其默认行为,然后使用 history API 自己实现了跳转。我们都知道,如果使用 <a>
标签去进行导航的话,整个页面都会被刷新,这是我们不希望看到的。所以我们使用 <Link>
组件来导航到一个目标 URL,可以在不刷新页面的情况下重新渲染页面。
参数
to(string | object | function)
为 string 时 就是一个明确的路径地址
1
<Link to="/courses?sort=name" />
为 object 时有如下属性(就是一个location对象)
1
2
3
4
5
6
7
8<Link
to={{
pathname: "/courses",
search: "?sort=name",
hash: "#the-hash",
state: { fromDashboard: true }
}}
/>- pathname:URL路径。
- search:URl中查询字符串。
- hash:URL的hash分段,例如#a-hash。
- state:表示location中的状态
为 function 时,就是一个函数接收当前 location 为参数,然后以字符串或对象的形式返回位置形式
replace (boolean)
,当为true
时,单击链接将替换历史堆栈中的当前记录,而不是添加一个新记录。
NavLink
NavLink
功能与 Link
类似不过参数更多,并且可以设置被选中时的样式或者类
exact (boolean)
是否进行精确匹配strict (boolean)
是否进行严格匹配to(string | object)
需要跳转到的路径(pathname)或地址(location)activeClassName (string)
是选中状态的类名,我们可以为其添加样式当激活(
to
属性与当前 URL 匹配)时,会将这个 class 选择器名添加到元素上,默认值为'active'
1
2
3<NavLink to="/faq" activeClassName="selected">
FAQs
</NavLink>activeStyle (Object)
元素处于选中状态时,应用于元素的样式1
2
3
4
5
6
7
8
9<NavLink
to="/faq"
activeStyle={{
fontWeight: "bold",
color: "red"
}}
>
FAQs
</NavLink>isActive(function)
,一个函数,用于添加额外的逻辑,以确定链接是否处于激活状态。如果您想做的不仅仅是验证链接的路径名是否与当前 URL 的路径名匹配,那么应该使用此方法来返回true
或false
。1
2
3
4
5
6
7
8
9
10
11
12
13
14<NavLink
to="/events/123"
isActive={(match, location) => {
if (!match) {
return false;
}
// only consider an event active if its event id is an odd number
const eventID = parseInt(match.params.eventID);
return !isNaN(eventID) && eventID % 2 === 1;
}}
>
Event 123
</NavLink>
Redirect
重定向,新位置将覆盖历史堆栈中的当前位置
from (string)
需要重定向的路径,可以包括动态参数
push (boolean)
为 true 时,重定向会将新条目推入历史记录,而不是替换当前条目
to (string | object)
重定向到的路径
exact (boolean)
是否要对 from 进行精确匹配
strict (boolean)
是否要对 from 进行严格匹配
sensitive (boolean)
匹配 from 时是否区分大小写
IndexRoute和IndexRedirect
Index Routes
通常情况下,我们会建立如下情况的路由:
1 | <Router> |
当用户访问 /
时, App 组件被渲染,但组件内的子元素却没有, App
内部的 this.props.children
为 undefined 。 你可以简单地使用 {this.props.children ||}
来渲染一些默认的 UI 组件。
1 | <Router> |
如此配置后,我们再次访问 /
路由,你会发现页面渲染了 Home 组件的内容。这就是 IndexRoute 的功能,指定一个路由的默认页。
Index Redirects
上面这种情况比较常见,还有一种非常常见的方式就是当我们尝试访问 /
这个路由时,我们想让其直接跳转到 ‘/Accounts’,直接免去了默认页 Home,这样来的更加直接。由此我们就需要 IndexRedirect
功能。考虑如下路由:
1 | <Router> |
这样设计路由后,我们再次访问 /
时,系统默认会跳转到 /accounts
路由。
总结
以上就是 IndexRoute 和 IndexRedirect 的功能介绍,让我们来总结一下他们两个的区别。
- IndexRoute 一般情况下用于设计一个默认页且不改变 URL 地址,而 IndexRedirect 则是跳转默认地址且地址会发生改变。
- IndexRoute 指定一个组件作为默认页,而 IndexRedirect 指定一个路由地址作为跳转地址。
Hooks
属性的隐式传递
this.props.history/match/location
所属 | 属性 | 类型 | 含义 |
---|---|---|---|
history | length | number | 表示history堆栈的数量 |
action | string | 表示当前的动作。比如pop、replace或push | |
location | object | 表示当前的位置 | |
push(path, [state]) | function | 在history堆栈顶加入一个新的条目 | |
replace(path, [state]) | function | 替换在history堆栈中的当前条目 | |
go(n) | function | 将history堆栈中的指针向前移动 | |
goBack() | function | 等同于go(-1) | |
goForward() | function | 等同于go(1) | |
block(promt) | function | 阻止跳转 | |
match | params | object | 表示路径参数,通过解析URL中动态的部分获得的键值对 |
isExact | boolean | 为true时,表示精确匹配 | |
path | string | 用来做匹配的路径格式 | |
url | string | URL匹配的部分 | |
location | pathname | string | URL路径 |
search | string | URl中查询字符串 | |
hash | string | URL的hash分段 | |
state | string | 表示location中的状态 |
useHistory
用以获取history对象,进行编程式的导航
1 | const Husky = props => { |
useLocation
用以获取location对象,可以查看当前路由信息
1 | const Husky = props => { |
useParams
useParams和props.match.params可以获取路由参数
1 | <Route path="/blog/:eat"> |
useRouteMatch
useRouteMatch
,接受一个path字符串作为参数。当参数的path与当前的路径相匹配时,useRouteMatch会返回match对象,否则返回null。
useRouteMatch
在对于一些,不是路由级别的组件。但是组件自身的显隐却和当前路径相关的组件时,非常有用。
比如,你在做一个后台管理系统时,网页的Header只会在登录页显示,登录完成后不需要显示,这种场景下就可以用到useRouteMatch
。
1 | const Home = () => { |
实战
路由嵌套
可以通过嵌套 route
来实现路由嵌套,注意exact
1 | //根路由 |
注意:如果在父路由中开启 exact 匹配,就会导致子组件加载不出来
路由懒加载
https://zh-hans.reactjs.org/docs/code-splitting.html
Suspense和lazy
如果我们项目有三个模块,用户管理(UserManage)、资产管理(AssetManage)、考勤管理(AttendanceManage)。当我们进入首页的时候由于没有进入任何一个模块,为了提高响应效率是不需要进行模块资源加载的,同时当我们进入用户管理的时候只需要加载用户管理路由对应的模块资源,进入其他模块亦然。这时候我们就需要对代码进行拆分,React.lazy可以结合Router来对模块进行懒加载。
1 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; |
withRouter
本质: 高阶组件
作用: 可以在非路由组件中注入路由对象
在没有路由指向(就是没有Route对象)的组件默认this.props当中没有路由所需要的参数,使用withRouter可以添加
1 | import React from 'react'; |
1 | import React from 'react'; |
路由传参
param动态路由传参
1 | <Route path='/path/:name' component={Path}/> |
优点:
1、传参和接收都比较简单
2、刷新页面参数不会丢失
缺点:
1、 当复杂数据对象或数组需要传参时,这样做比较麻烦,需要通过json字符串的方式进行处理
1 | // 定义路由匹配 |
2、多个参数的传递,url 会又长又不美观
3、参数会出现在url上,不够安全
search传参
1 | <Route path='/web/departManange' component={DepartManange}/> |
优点:
1、传参和接收都比较简单
2、刷新页面参数不会丢失
3、可以传递多个参数
缺点:
1、当复杂数据对象或数组需要传参时,这样做比较麻烦,需要通过json字符串的方式进行处理
2、参数会出现在url上,不够安全
query传参
1 | <Route path='/query' component={Query}/> |
优点:
1、传参和接收都比较简单
2、可以传递多个参数
3、传递对象数组等复杂参数方便
4、不会暴露给用户,比较安全
缺点:
1、如果手动刷新当前路由时,数据参数有可能会丢失
state传参
1 | <Link to={{ |
优点:
1、传参和接收都比较简单
2、可以传递多个参数
3、传递对象数组等复杂参数方便
4、不会暴露给用户,比较安全
缺点:
1、如果手动刷新当前路由时,数据参数有可能会丢失
在react中,最外层包裹了BrowserRouter时,不会丢失,但如果使用的时HashRouter,刷新当前页面时,会丢失state中的数据
状态管理器
Redux
Redux是将整个应用状态存储到一个地方,称为store。里面保存一棵状态树(state tree)。组件可以派发(dispatch)行为(action)给store,action发出命令后将state放入reucer加工函数中,返回新的state。其它组件可以通过订阅store中的状态(state)来刷新自己的视图
三大原则
单一数据源
整个应用的state被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个store 中。
State 是只读的
唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
这样确保了视图和网络请求都不能直接修改 state,相反它们只能表达想要修改的意图。action就是改变state的指令,有多少操作state的动作就会有多少action。
1 | //添加todo任务的 action 是这样的: |
使用纯函数来执行修改
reducer 就是一个纯函数,接收旧的 state 和 action,返回新的 state。
1 | (previousState, action) => newState |
之所以将这样的函数称之为reducer,是因为这种函数与被传入 Array.prototype.reduce(reducer, ?initialValue)
里的回调函数属于相同的类型。保持 reducer 纯净非常重要。永远不要在 reducer 里做这些操作:
- 修改传入参数;
- 执行有副作用的操作,如 API 请求和路由跳转;
- 调用非纯函数,如
Date.now()
或Math.random()
。
这是一个redux的经典案例
通过createStore创建store
actions 定义指令
调用store.dispatch()发出修改state的命令
定义reducer函数根据action的类型改变state
1 | import { createStore } from 'redux'; |
store构建
目录结构
action
存放描述行为的数据结构(本质上是 JavaScript 普通对象),一般来说你会通过 store.dispatch() 将 action 传到 store。
我们约定,action 内必须使用一个字符串类型的 type
字段来表示将要执行的动作。多数情况下,type
会被定义成字符串常量。当应用规模越来越大时,建议使用单独的模块或文件来存放 action。
1 | // ./actions/counter.js |
注意:当我们表示用户完成任务的动作序列号时,我们还需要再添加一个 action index 来,所以我们通过下标 index
来引用特定的任务。而实际项目中一般会在新建数据的时候生成唯一的 ID 作为数据的引用标识。
Reducer
Reducers 指定了应用状态的变化如何响应 actions 并发送到 store 的。
1 | // ./reducers/counter.js |
1 | // ./reducers/index.js |
store
注意:Redux 应用只有一个单一的 store
我们学会了使用 action 来描述“发生了什么”,和使用 reducers 来根据 action 更新 state 的用法。
Store 就是把它们联系到一起的对象。Store 有以下职责:
- 维持应用的 state;
- 提供
getState()
方法获取 state; - 提供
dispatch(action)
方法更新 state; - 通过
subscribe(listener)
注册监听器; - 通过
subscribe(listener)
返回的函数注销监听器。
https://zhuanlan.zhihu.com/p/258017257
1 | import { createStore, applyMiddleware, compose } from 'redux' |
redux 异步请求
https://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_two_async_operations.html
Action 发出以后,Reducer 立即算出 State,这叫做同步;Action 发出以后,过一段时间再执行 Reducer,这就是异步。
在实际的开发中,redux中管理的很多数据可能来自服务器,我们需要进行异步的请求,再将数据保存到redux中。就是说在异步的网络请求中通过dispatch action来更新state中的数据。这时候就需要用到Redux中间件**(指这个框架允许我们在某个流程的执行中间插入我们自定义的一段代码)**。
Thunk middleware 并不是 Redux 处理异步 action 的唯一方式:
- 你可以使用 redux-promise 或者 redux-promise-middleware 来 dispatch Promise 来替代函数。
- 你可以使用 redux-observable 来 dispatch Observable。
- 你可以使用 redux-saga 中间件来创建更加复杂的异步 action。
- 你可以使用 redux-pack 中间件 dispatch 基于 Promise 的异步 Action。
API
Provider 组件
<Provider store>
使组件层级中的 connect()
方法都能够获得 Redux store。正常情况下,你的根组件应该嵌套在 <Provider>
中才能使用 connect()
方法。
React-Redux 提供Provider
组件,可以让容器组件拿到state
。
1
2
3
4
5
6
7
8
9
10
11
12
13 import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'
let store = createStore(todoApp);
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
上面代码中,Provider
在根组件外面包了一层,这样一来,App
的所有子组件就默认都可以拿到state
了。
它的原理是React
组件的context
属性,请看源码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14 class Provider extends Component {
getChildContext() {
return {
store: this.props.store
};
}
render() {
return this.props.children;
}
}
Provider.childContextTypes = {
store: React.PropTypes.object
}
上面代码中,store
放在了上下文对象context
上面。然后,子组件就可以从context
拿到store
,代码大致如下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 class VisibleTodoList extends Component {
componentDidMount() {
const { store } = this.context;
this.unsubscribe = store.subscribe(() =>
this.forceUpdate()
);
}
render() {
const props = this.props;
const { store } = this.context;
const state = store.getState();
// ...
}
}
VisibleTodoList.contextTypes = {
store: React.PropTypes.object
}
React-Redux
自动生成的容器组件的代码,就类似上面这样,从而拿到store
。
connect
React-Redux 提供connect
方法,用于从 UI 组件生成容器组件。connect
的意思,就是将这两种组件连起来。
1
2 import { connect } from 'react-redux'
const VisibleTodoList = connect()(TodoList);
上面代码中,TodoList
是 UI 组件,VisibleTodoList
就是由 React-Redux 通过connect
方法自动生成的容器组件。
但是,因为没有定义业务逻辑,上面这个容器组件毫无意义,只是 UI 组件的一个单纯的包装层。为了定义业务逻辑,需要给出下面两方面的信息。
(1)输入逻辑:外部的数据(即
state
对象)如何转换为 UI 组件的参数(2)输出逻辑:用户发出的动作如何变为 Action 对象,从 UI 组件传出去。
因此,connect
方法的完整 API 如下。
1
2
3
4
5
6 import { connect } from 'react-redux'
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
上面代码中,connect
方法接受两个参数:mapStateToProps
和mapDispatchToProps
。它们定义了 UI 组件的业务逻辑。前者负责输入逻辑,即将state
映射到 UI 组件的参数(props
),后者负责输出逻辑,即将用户对 UI 组件的操作映射成 Action。
mapStateToProps()
mapStateToProps
是一个函数。它的作用就是像它的名字那样,建立一个从(外部的)state
对象到(UI 组件的)props
对象的映射关系。也就是说, 把state映射到props中去
作为函数,mapStateToProps
执行后应该返回一个对象,里面的每一个键值对就是一个映射。请看下面的例子。
1
2
3
4
5 const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
上面代码中,mapStateToProps
是一个函数,它接受state
作为参数,返回一个对象。这个对象有一个todos
属性,代表 UI 组件的同名参数,后面的getVisibleTodos
也是一个函数,可以从state
算出 todos
的值。
下面就是getVisibleTodos
的一个例子,用来算出todos
。
1
2
3
4
5
6
7
8
9
10
11
12 const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
default:
throw new Error('Unknown filter: ' + filter)
}
}
mapStateToProps
会订阅 Store,每当state
更新的时候,就会自动执行,重新计算 UI 组件的参数,从而触发 UI 组件的重新渲染。
mapStateToProps
的第一个参数总是state
对象,还可以使用第二个参数,代表容器组件的props
对象。
1
2
3
4
5
6
7
8
9
10 // 容器组件的代码
// <FilterLink filter="SHOW_ALL">
// All
// </FilterLink>
const mapStateToProps = (state, ownProps) => {
return {
active: ownProps.filter === state.visibilityFilter
}
}
使用ownProps
作为参数后,如果容器组件的参数发生变化,也会引发 UI 组件重新渲染。
connect
方法可以省略mapStateToProps
参数,那样的话,UI 组件就不会订阅Store,就是说 Store 的更新不会引起 UI 组件的更新。
mapDispatchToProps()
mapDispatchToProps
是connect
函数的第二个参数,用来建立各种dispatch变成props,让你可以直接使用 UI 组件的参数到store.dispatch
方法的映射。也就是说,把各种dispatch变成了props让你可以直接使用
如果mapDispatchToProps
是一个函数,会得到dispatch
和ownProps
(容器组件的props
对象)两个参数。
1
2
3
4
5
6
7
8
9
10
11
12
13 const mapDispatchToProps = (
dispatch,
ownProps
) => {
return {
onClick: () => {
dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: ownProps.filter
});
}
};
}
从上面代码可以看到,mapDispatchToProps
作为函数,应该返回一个对象,该对象的每个键值对都是一个映射,定义了 UI 组件的参数怎样发出 Action。
如果mapDispatchToProps
是一个对象,它的每个键名也是对应 UI 组件的同名参数,键值应该是一个函数,会被当作 Action creator ,返回的 Action 会由 Redux 自动发出。举例来说,上面的mapDispatchToProps
写成对象就是下面这样。
1
2
3
4
5
6 const mapDispatchToProps = {
onClick: (filter) => {
type: 'SET_VISIBILITY_FILTER',
filter: filter
};
}
实例:计数器
我们来看一个实例。下面是一个计数器组件,它是一个纯的 UI 组件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 import React from "react";
import { connect } from "react-redux";
import { increment, decrement } from "../../store/actions/counter";
const Home = function (props) {
//生成props
const { count, onincrement, ondecrement} = props;
// console.log(props);
return (
<div>
<Button
variant="contained"
color="primary"
onClick={onincrement}
>
increment
</Button>
<Button
variant="contained"
color="primary"
onClick={ondecrement}
style={{marginLeft:'30px'}}
>
decrement
</Button>
<p style={{fontSize:'30px'}}>{count}</p>
</div>
);
};
上面代码中,这个 UI 组件有三个参数:count和 onincrement, ondecrement。前者需要从state
计算得到,后者需要向外发出 Action。
接着,定义count
到state
的映射,以及onincrement, ondecrement
到dispatch
的映射。
1
2
3
4
5
6
7
8
9
10
11
12
13 function mapStateToProps(state) {
console.log(state)
return {
count: state.counter.count,
};
}
function mapDispatchToProps(dispatch) {
return {
onincrement: () => dispatch(increment()),
ondecrement: () => dispatch(decrement())
};
}
然后,使用connect
方法生成容器组件。
1 export default connect(mapStateToProps, mapDispatchToProps)(Home);
然后,定义这个组件的 Reducer。
1
2
3
4
5
6
7
8
9
10
11
12
13 // Reducer
import {INCREMENT, DECREMENT} from "../actions/counter"
export default function(state = { count: 0}, action){
const count = state.count
switch (action.type) {
case INCREMENT:
return {count:count + 1};
case DECREMENT:
return {count:count - 1};
default:
return {count:count};
}
}
最后,生成store
对象,并使用Provider
在根组件外面包一层。
1
2
3
4
5
6
7
8
9
10
11
12
13
14 import React from "react";
import route from "../route/index.js";
import { Provider } from "react-redux";
import store from "../store";
export default function Menu() {
const classes = useStyles();
return (
<div className={classes.root}>
<Provider store={store}>
</Provider>
</div>
);
}
createStore
createStore(reducer, [preloadedState], enhancer)
创建一个 Redux store 来以存放应用中所有的 state。
应用中应有且仅有一个 store。
参数
reducer
(Function): 接收两个参数,分别是当前的 state 树和要处理的 action,返回新的 state 树。- [
preloadedState
] (any): 初始时的 state。 在同构应用中,你可以决定是否把服务端传来的 state 水合(hydrate)后传给它,或者从之前保存的用户会话中恢复一个传给它。如果你使用combineReducers
创建reducer
,它必须是一个普通对象,与传入的 keys 保持同样的结构。否则,你可以自由传入任何reducer
可理解的内容。 enhancer
(Function): Store enhancer 是一个组合 store creator 的高阶函数,返回一个新的强化过的 store creator。这与 middleware 相似,它也允许你通过复合函数改变 store 接口。
返回值
(Store
): 保存了应用所有 state 的对象。改变 state 的惟一方法是 dispatch action。你也可以 subscribe 监听 state 的变化,然后更新 UI。
1 | import { createStore } from 'redux' |
- 应用中不要创建多个 store!相反,使用
combineReducers
来把多个 reducer 创建成一个根 reducer。 - 要使用多个 store 增强器的时候,你可能需要使用 compose
Store 方法
Store 就是用来维持应用所有的 state 树 的一个对象。 改变 store 内 state 的惟一途径是对它 dispatch 一个 action。
- getState()
- dispatch(action)
- subscribe(listener)
- replaceReducer(nextReducer)
combineReducers
随着应用变得越来越复杂,可以考虑将 reducer 函数 拆分成多个单独的函数,拆分后的每个函数负责独立管理 state 的一部分。
1 | import { combineReducers } from 'redux' |
combineReducers把一个由多个不同 reducer 函数作为 value 的 object,合并成一个最终的 reducer 函数,然后就可以对这个 reducer 调用 createStore 方法。
合并后的 reducer 可以调用各个子 reducer,并把它们返回的结果合并成一个 state 对象。
applyMiddleware
https://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_two_async_operations.html
applyMiddleware(…middlewares)
使用包含自定义功能的 middleware 来扩展 Redux 是一种推荐的方式。Middleware 可以让你包装 store 的 dispatch 方法来达到你想要的目的。同时, middleware 还拥有“可组合”这一关键特性。多个 middleware 可以被组合到一起使用,形成 middleware 链。其中,每个 middleware 都不需要关心链中它前后的 middleware 的任何信息。
Middleware 最常见的使用场景是实现异步 actions。这种方式可以让你像 dispatch 一般的 actions 那样 dispatch 异步 actions。
示例: 自定义 Logger Middleware
1 | import { createStore, applyMiddleware } from 'redux' |
compose(...functions)
从右到左来组合多个函数。
这是函数式编程中的方法,为了方便,被放到了 Redux 里。
当需要把多个 store 增强器 依次执行的时候,需要用到它。
参数
- (arguments): 需要合成的多个函数。预计每个函数都接收一个参数。它的返回值将作为一个参数提供给它左边的函数,以此类推。例外是最右边的参数可以接受多个参数,因为它将为由此产生的函数提供签名。(译者注:
compose(funcA, funcB, funcC)
形象为compose(funcA(funcB(funcC())))
)
返回值
(Function): 从右到左把接收到的函数合成后的最终函数。
1 | import { createStore, combineReducers, applyMiddleware, compose } from 'redux' |
Mobx
作为了解的内容,在项⽬中使⽤redux的情况更多。
Mobx是⼀个功能强⼤,上⼿⾮常容易的状态管理⼯具。redux的作者也曾经向⼤家推荐过它,在不少情况下可以使⽤Mobx来替代掉redux。
hox
定义 Model: 用 createModel
包装后,就变成了持久化,且全局共享的数据。
1 | import { createModel } from 'hox'; |
使用 Model:createModel
返回值是个 Hook,你可以按 React Hooks 的用法正常使用它。
1 | import { useCounterModel } from "../models/useCounterModel"; |
TS与React
jsx转ts
https://blog.yangteng.me/articles/2021/migrate-react-project-to-typescript/
配置TS
1 | yarn add typescript |
然后加入 TypeScript 的配置文件:将 tsconfig.json 放到项目的根目录下。
1 | { |
package
1 | "devDependencies": { |
配置 babel 和 webpack
将 babel 的 TypeScript 预设加入项目依赖中,并添加到 babel 的配置文件里。
1 | yarn add @babel/preset-typescript --dev |
1 | // .babelrc |
修改 webpack 的配置,将 TypeScript 文件加入 resolve
和babel-loader
的 match 规则中。
1 | // webpack.config.js |
引入类型定义的 package
上一步完成后,实际上已经可以在代码中使用 TypeScript 了。但这时候如果你去写一个 React 组件,就会发现类似 Cannot find module 'react'.
的报错。这就需要将一些你用到的 library 的类型定义加进来了。
1 | yarn add @types/react @types/react-dom @types/node #@types/<package-used-in-your-project> |
TIP
antd,css
1
2
3
4
5
6
7
8{
loader: 'less-loader', // 编译 Less 为 CSS
options: {
lessOptions: {
javascriptEnabled: true,
},
},
},
React.FC
React.FC
是函数式组件,是在TypeScript使用的一个泛型。FC是FunctionComponent的缩写,React.FC
可以写成React.FunctionComponent
。这个类型定义了默认的 props(如 children)以及一些静态属性(如 defaultProps)
1 | import React, { FC } from 'react'; |
NEXT
背景
要从头开始使用 React 构建一个完整的 Web 应用程序,需要考虑许多重要的细节:
- 必须使用打包程序(例如 webpack)打包代码,并使用 Babel 等编译器进行代码转换。
- 你需要针对生产环境进行优化,例如代码拆分。
- 你可能需要对一些页面进行预先渲染以提高页面性能和 SEO。你可能还希望使用服务器端渲染或客户端渲染。
- 你可能必须编写一些服务器端代码才能将 React 应用程序连接到数据存储。
Next.js:React 开发框架
- 直观的、 基于页面 的路由系统(并支持 动态路由)
- 预渲染。支持在页面级的 静态生成 (SSG) 和 服务器端渲染 (SSR)
- 自动代码拆分,提升页面加载速度
- 具有经过优化的预取功能的 客户端路由
- 内置 CSS 和 Sass 的支持,并支持任何 CSS-in-JS 库
- 开发环境支持 快速刷新
- 利用 Serverless Functions 及 API 路由 构建 API 功能
- 完全可扩展
创建
1 | npx create-next-app nextjs-blog --use-npm --example "https://github.com/vercel/next-learn-starter/tree/master/learn-starter" |
1 | cd nextjs-blog |
1 | npm run dev |
在浏览器中打开 http://localhost:3000 。
页面
客户端导航
在 Next.js 中,页面是从pages
目录中的文件导出的 React 组件。
页面与基于其文件名的路由相关联。例如,在开发中:
pages/index.js
与/
路由相关联。pages/posts/first-post.js
与/posts/first-post
路由相关联。
在页面之间导航
1 | import Link from 'next/link' |
1 | Read <Link href="/posts/first-post"><a>this page!</a></Link> |
该Link
组件支持在同一个 Next.js 应用程序中的两个页面之间进行客户端导航。
客户端导航意味着页面转换使用 JavaScript 进行,这比浏览器执行的默认导航更快。
该Link
组件支持在同一个 Next.js 应用程序中的两个页面之间进行客户端导航。
客户端导航意味着页面转换使用 JavaScript 进行,这比浏览器执行的默认导航更快。
这是您可以验证的简单方法:
- 使用浏览器的开发人员工具将
background
CSS 属性更改<html>
为yellow
。 - 单击链接可在两个页面之间来回切换。
- 您会看到黄色背景在页面转换之间持续存在。
这表明浏览器未加载完整页面并且客户端导航正在工作。
如果您使用了<a href="…">
代替<Link href="…">
并执行了此操作,则链接点击时背景颜色将被清除,因为浏览器会完全刷新。
动态路由
Next.js 支持具有动态路由的 pages(页面)。例如,如果你创建了一个命名为 pages/posts/[id].js
的文件,那么就可以通过 posts/1
、posts/2
等类似的路径进行访问。
pages/blog/[slug].js
→/blog/:slug
(/blog/hello-world
)pages/[username]/settings.js
→/:username/settings
(/foo/settings
)pages/post/[...all].js
→/post/*
(/post/2020/id/title
)
代码拆分和预取
Next.js 会自动进行代码拆分,因此每个页面只加载该页面所需的内容。这意味着在呈现主页时,最初不会提供其他页面的代码。
这可确保即使您添加数百个页面,主页也能快速加载。
仅加载您请求的页面的代码也意味着页面变得孤立。如果某个页面抛出错误,应用程序的其余部分仍然可以工作。
此外,在 Next.js 的生产版本中,每当Link
组件出现在浏览器的视口中时,Next.js 都会在后台自动预取链接页面的代码。当您单击链接时,目标页面的代码已在后台加载,页面转换将近乎即时!
HTML
html
<Head>
使用 代替小写字母<head>
。<Head>
是一个内置于 Next.js 的 React 组件。它允许您修改<head>
页面的名称。
1 | import Head from 'next/head' |
1 | <Head> |
img
1 | img统一放在public中,引用直接引用img,不需要添加图像路径 |
css
1 | <style jsx>{` |
这是使用一个名为styled-jsx的库。它是一个“CSS-in-JS”库——它允许你在 React 组件中编写 CSS,并且 CSS 样式将被限定(其他组件不会受到影响)。
Next.js 内置了对styled-jsx 的支持,但您也可以使用其他流行的 CSS-in-JS 库。我用的是materialUI框架中的css-in-js
全局样式
如果你希望每个页面都加载一些 CSS,添加pages/_app.js文件
1
2
3
4import '../styles/global.css'
export default function App({ Component, pageProps }) {
return <Component {...pageProps} />
}创建一个顶级styles目录并global.css在里面创建。将其导入pages/_app.js
内置API
某些页面需要获取外部数据以进行预渲染。有两种情况,在每种情况下,您都可以使用 Next.js 提供的特殊功能:
- 您的页面 内容 取决于外部数据:使用
getStaticProps
。 - 你的页面 paths(路径) 取决于外部数据:使用
getStaticPaths
(通常还要同时使用getStaticProps
)。
getStaticProps函数在构建时被调用,并允许你在预渲染时将获取的数据作为 props
参数传递给页面。getStaticProps不会在页面组件中生效
Next.js 允许你创建具有 动态路由 的页面。例如,你可以创建一个名为 pages/posts/[id].js
的文件用以展示以 id
标识的单篇博客文章。当你访问 posts/1
路径时将展示 id: 1
的博客文章。但是,在构建 id
所对应的内容时可能需要从外部获取数据。getStaticPaths函数在构建时被调用,并允许你指定要预渲染的路径。
1 | // 此函数在构建时被调用 |
为了让页面使用服务端渲染,你需要导出 getServerSideProps 异步函数。这个函数将在每次请求时在服务端被调用。例如,假设你的页面需要用最新的数据预渲染(通过外部的 api 获取数据)。你应该写下 getServerSideProps 来获取数据传递给 Page。
getServerSideProps 和 getStaticProps 很像,但是区别的是,getServerSideProps 是每个请求都会调用而不是在构建时。
mardown解析
插件
https://dev.to/imranib/build-a-next-js-markdown-blog-5777
react-markdown将帮助我们解析和渲染 Markdown 文件
代码格式化:
react-syntax-highlighter
包gray-matter](https://www.npmjs.com/package/react-markdown) 将解析我们博客的顶部内容。(文件顶部的部分
---
)我们需要这样的元数据
title
,data
并description
和slug
。您可以在此处添加任何您喜欢的内容
参数 | 意义 |
---|---|
slug | 导航的参数 |
title | 文章名称 |
data | 最新时间 |
updated | 文章更新日期 |
tags | 文章標籤 |
category | 文章分類 |
description | 文章描述 |
- raw-loader将帮助我们导入我们的markdown文件。
流程
https://dev.to/imranib/build-a-next-js-markdown-blog-5777
https://thetombomb.com/posts/adding-code-snippets-to-static-markdown-in-Next%20js
Tips
- material,classname报错,每次刷新,material失去效果。添加_app.js和__document.js文件