Loading...

React 通关指南

最近一段时间特别喜欢前端,这些天在 B 站上找到一个感觉特别良好的视频教程,自我感觉还算是比较用心地跟着学习了一下,但在学习过程中不太方便做笔记,为了避免即学即忘,所以在学完之后,赶紧详细整理一下 React 相关笔记。

在开篇之前有一些题外话需要说明,当然,既然是题外话,那就与学习 React 本身无关,是本篇文章维护相关但内容,以及一些闲话,您也完全可以先行忽略

  1. 本文可选访问地址

  2. 本文略有些“标题党”成分,但文本内容在努力向该标题靠拢。

  3. 本博客暂不愿接受任何形式的转载,允许引用或扩散本文链接(博客版链接),个人觉得“转载”机制的泛滥污染了互联网的学习氛围(仅个人观点,不参与争论)。

  4. 作为 Java 后端开发,在前端知识上的见解,定然远不如术业专攻的前端工程师深刻和独到。

  5. 本人或多或少有些语言恐慌症,希望各位大人在发表观点时尽量抹去锋芒(不情之请)。

  6. 在学习 React 和编写本文的过程中,听到一些人说我“卷”,其实我对编程并没有热爱,但我的性格就是但凡决定要做的事情,就一定会认认真真,没想到被人理解成“卷”,还是略微有些不高兴。

  7. 另外,本文相关外链均存在较大价值,特别是项目案例部分,我也算是为此煞费苦心了。

闲话打住,这就开始吧!!!

React 入门

尊敬的旅者,欢迎来到 React 的世界,我是您此段行程的向导(也被称为“稀里糊涂的 NPC”),刚刚度过了煎熬的五大天,终于从 NPC 培训中心(指 B 站)顺利毕业,为了不辜负师长的期望(假设有),我决定努力做一个优秀的 NPC,因此有了这篇文章。

这篇文章严重参考了该培训中心的相关资料,也有少部分掺杂了微不足道的我的小小见解,希望旅者大人们不要见怪。为了改善大人们的旅途体验,微不足道的我使用了 CodePenCodesandbox 为文中的部分代码编写了在线运行实例,如果一不小心弄巧成拙,也请大人们原谅微不足道的我的过失。

当然了,旅途总是漫长的,希望旅者大人们合理安排时间。

简介

一提到 Web 前端开发,我们很容易就能想到前端的三大主流框架:Vue、React、Angular。个人觉得 Web 前端开发首选推荐肯定是 Vue,React 可以作为在掌握 Vue 知识基础上的扩展,当然,Vue 语言在设计之初就对 React 进行了借鉴,所以二者之间存在诸多联系和相似点,至于 Angular 暂时还不在笔者对学习考虑范围内,不作相关扩展。

另外,从 Vue 和 React 两者官网的描述也可看出两者本质上的差异。

React 是一个用于构建用户界面的 JavaScript 库。

Vue 是一套用于构建用户界面的渐进式框架。

因此,对于学习而言,Vue 封装了更多的 API,适合作为前端及时就业框架的学习首选,而 React 则更加适合在掌握 Vue 的基础上对前端知识进行深化,它更容易帮助学习者从简单的框架使用之中脱离出来,逐渐理解框架运行的底层实现。

React 官网

  1. 英文官网:https://www.reactjs.org

  2. 中文官网:https://react.docschina.org

介绍描述

特点

  1. 声明式编码。

  2. 组件化编码。

  3. React Native 可编写原生应用。

  4. 高效(使用优秀的 Diffing 算法)。

🍎 React 高效的原因?

  1. 使用虚拟 DOM,并非总是直接操作页面的真实 DOM。

  2. DOM 采用 Diffing 算法,使页面最小程度地进行重新渲染。

基本使用

为了方便在学习过程中能够更加直观地展示页面效果,您可以通过 Codepen 等工具进行在线编程。

Hello World

以下是一个最基础的 React 示例,您可以借此对 React 拥有一个基本的认知:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Hello React</title>
</head>
<body>
    <!--准备好用于渲染DOM的容器-->
    <div id="app"></div>

    <script type="text/javascript" src="https://unpkg.com/react@16/umd/react.development.js"></script>
    <script type="text/javascript" src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
    <script type="text/javascript" src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>

    <script type="text/babel">
        // 创建虚拟DOM
        const VDOM = <h2>Hello, React!</h2>
        // 将虚拟DOM渲染到页面的指定节点中
        ReactDOM.render(VDOM, document.getElementById("app"))
    </script>
</body>
</html>

为了方便查看运行效果,您可以点击此处进行在线预览,当然,您也可以亲自尝试一下。

如代码所示,通过 CDN 方式引入 React,你需要引入如下三个文件:

  1. react.development.js - React 核心库。

  2. react-dom.development.js - 提供操作 DOM 的 React 扩展库。

  3. babel.min.js - 用于解析 JSX 的库。

并且,与普通的 JavaScript 代码不同的是,React 脚本的类型需要声明为 text/babel,其作用在于解析 JSX 语法代码并将其转换为浏览器识别的 JS 代码。

在学习过 Web 基础之后,我们知道可以通过 document.createElement() 来创建 DOM 元素,但在 React 中采用了 JSX 语法来替代原生 JavaScript 创建 DOM 的方式。

虚拟 DOM 与真实 DOM

虚拟 DOM 是 React 的亮点之一,所谓虚拟 DOM,即非真实的 DOM。其实虚拟 DOM 本质上是一个一般对象,它相较于真实 DOM 而言拥有更少的属性,且无法在浏览器中直接显示。虚拟 DOM 去除了很多真实 DOM 中存在但程序编码无需关注的属性。

虚拟 DOM 的引入使页面更新效率得到了极大的提升,这是因为真实 DOM 直接作用于页面,对于任何一次更新,真实 DOM 都会重新进行渲染,即便该更新内容只适应于极小的范围,这就造成了浏览器性能的极大浪费。而虚拟 DOM 则充当了用户与真实 DOM 之间的过滤助手,它通过 Diffing 算法,识别出需要更新的数据节点,并将其转换为真实 DOM,而对于未做变更的数据节点,则无需进行 DOM 转换,这样就极大程度地将数据变更控制在了最小的元素单元内。

基于这一设计方式,React 提供了用于创建虚拟 DOM 的 API,其声明示例如下:

// ================ 使用 javascript 创建虚拟 DOM
// React.createElement(node, attributes, content)
const VDOM = React.createElement('h1', {id: 'title'}, React.createElement('span', {}, 'Hello, React~'))

// ================ 使用 jsx 创建虚拟 DOM // 注意:使用 jsx 需要指定 javascript 类型为 text/babel
VDOM = (
    <h1 id="title">
        <span>Hello, React~</span>
    </h1>
)
ReactDOM.render(VDOM, document.getElementById('app'))

仔细观察这个示例,相信您对 JSX 与 JavaScript 之间的差异已经有了基本的了解。

React JSX

通过前文的了解,我们可以看出 JSX 的基本语法与 JavaScript 语法十分相似,而其虚拟 DOM 创建的语法又与 html 语法如出一辙。但是,JSX 与这二者之间仍存在诸多不同之处,观察下面这段代码清单,您或许会有更加深刻的感受:

const VDOM = (
    <div>
        <h2 className="title" id={myId.toLowerCase()}>
            <span style={{color:'white', fontSize:'29px'}}>{myData.toLowerCase()}</span>
        </h2>
        <h2 className="title" id={myId.toUpperCase()}>
            <span style={{color:'white', fontSize:'29px'}}>{myData.toLowerCase()}</span>
        </h2>
        <input type="text"/>
    </div>
)
ReactDOM.render(VDOM,document.getElementById('app'))

从这段代码清单中可以看出,相比于 JavaScript 和 html,JSX 还需要遵循一些额外的规则,这里为您总结 JSX 的语法规则如下:

  1. JSX 在定义虚拟 DOM 时,不应当加引号,否则虚拟 DOM 内容会被识别为普通字符串。

  2. JSX 标签中混入 JavaScript 语法时,应当使用符号 {} 包裹。

  3. 虚拟 DOM 节点的类属性应当使用 className 而非 class

  4. 虚拟 DOM 中的内联样式需要使用 style={{key: value}} 进行书写,且其内的 CSS 属性名也需要使用驼峰命名方式。

    可以理解为 style 中传入的是一个表示样式内容的 JS 对象。

  5. 虚拟 DOM 只能有一个根标签。

  6. 所有虚拟 DOM 的标签都必须闭合。

  7. 虚拟 DOM 标签首字母小写时,转换真实 DOM 则会匹配同名的 html 元素,首写字母大写时,React 则会渲染对应的组件,若对应组件不存在,则会抛出异常。

JS 表达式 / JS 语句

值得注意的是,在 JSX 中允许书写 JS 表达式,但不允许书写 JS 语句。

表达式和语句的区别在于,前者可以产生一个值,可以放在任何需要值的地方,且能够被其他变量接收,它们通常会占用一定的内存空间,而后者则更多地偏向于进行代码的控制。

JS 表达式示例:

  1. a

  2. a + b

  3. handler(20)

  4. arr.map()

  5. function handler(params) {}

JS 代码示例:

  1. if(condition) {}

  2. for(var item in arr) {}

  3. switch(condition) { case: xxx }

虚拟 DOM 渲染

语法:ReactDOM.render(virtualDOM, containerDOM)

模块、组件、模块化、组件化

  1. 模块

    所谓模块,通常是一个向外提供特定功能的 JavaScript 程序,一般就是一个 JS 文件。

    模块的拆分,有利于代码解耦,使代码逻辑更清晰,更容易维护。此外,拆分模块还可以方便 JS 复用,简化 JS 编写,提高 JS 运行效率。

  2. 组件

    组件通常是用来实现局部功能效果的代码和资源的集合,该集合中包含所有实现该组件的相关资源。

    随着页面功能的增加,使用组件对不同功能进行代码聚合,能够很方便地做到即取即用。当不再需要该组件时,能够轻松进行卸载,而不会引起代码的异常连锁。

    总之,组件化是前端发展的趋势,甚至,相同的思想在其他各种语言、各种类型的程序中都已进行了广泛的应用。其典型特点就是:可插拔,易扩展,低耦合,高性能。当然,与之相应的,也会存在一定的学习成本。

  3. 模块化

  4. 组件化

编程过程中难免会碰到很多 ~ ~ 化 的概念,其实无非是加上了 -ization 的区别。如果你能够正确地区分【商业】和【商业化】、【工业】和【工业化】,理解这些概念就不是什么难事了。其实这些只是语文上的区别,所谓的“某某化”,其实就是将这个“某某”进行大范围、大规模地应用,并形成一定的规范而已。

React 面向组件编程

基本理解和使用

React 开发者工具

如您习惯使用 Chrome 浏览器进行开发(推荐),可在 Chrome 商店安装 React Developer Tools 来帮助开发和调试。

chrome 应用商店:https://chrome.google.com/webstore/category/extensions

ChromeReactdevelopertools

将其添加至 Chrome,当访问 React 开发的网页时,即可打开控制台进行调试。

您也可以在添加该插件后,开启该插件并借助美团官网先行体验一下该插件的相关功能,再决定是否使用。

React 组件声明方式

React 中组件有两种声明方式:

  1. 函数式组件

    声明方式:

    <script type="text/babel">
    function MyComponent() {
       console.log(this) // 由于 babel 编译之后开启了严格模式,因此 this 将指向 undefined
       return <h2>函数式组件</h2>
    }
    ReactDOM.render(<MyComponent/>, document.getElementById('app'))
    </script>

    执行流程说明:

    babel 在解析 JSX 的过程中,读取到 ReactDOM.render(),发现其组件标签为 <MyComponent/> 是通过函数进行定义的,因此就会执行该函数并得到其返回值,然后将返回值作为虚拟 DOM,最终转换为真实 DOM 并渲染到页面。

  2. 类式组件

    声明方式:

    <script type="text/babel">
    class MyComponent extends React.Component {
    // 类式组件可声明构造器
    constructor(props) {
        super(props)
    }
    render() {
        // render 存在于当前组件的原型对象上,供该组件的实例使用
        console.log(this) // this 指向当前组件的实例对象
        return <h2>类式组件</h2>
    }
    }
    ReactDOM.render(<MyComponent/>, document.getElementById('app'))
    </script>

    执行流程说明:

    babel 在解析 JSX 的过程中,读取到 ReactDOM.render(),发现其组件标签为 <MyComponent/> 是通过类进行定义的,于是其内部就会通过关键字 new 实例化出该类的实例,并通过该实例调用到原型上的 render 方法,然后将 render 的返回值作为虚拟 DOM,最终转换为真实 DOM 并渲染到页面。

    构造器:

    构造器也被称为构造函数,它存在于 C、Java、Python 等诸多语言中,其作用主要是在创建对象时进行初始化,其执行时机通常是在使用 new 关键字(显式或隐式)创建对象时执行,并且在该对象的生命周期中只执行一次。构造函数通常是默认存在的,并且允许被覆盖重写。

在新版本的 React 中已经推荐使用函数式组件,但由于类式组件更易于知识理解层次的递进,因此,本文将先着重从类式组件入手来解述组件相关知识,最后在归纳在函数式组件中的用法差异。

React 组件实例三大属性

React 组件实例拥有三大属性,它们共同丰富了组件的功能。

  1. state

  2. props

  3. refs

在接下来的叙述中,将对 React 组件三大属性进行逐一说明。

三大核心属性:state

state 是 React 组件最重要的属性,其对应的值被声明为对象类型。从字面解读,该属性被称为“状态机”,(个人认为)也可以理解为一个数据集,其作用是用来存储核心数据,相当于 Vue 中的 data。

state 的基本使用

class LoginStatus extends React.Component{
    constructor(props) {
        console.log('constructor');
        super(props)
        this.state = {isLogin: false, username: '路易斯'}
        // 通过 .bind(this) 可以将this对象传递到 switchLogin() 函数中(强制绑定,不推荐)
        this.switchLogin = this.switchLogin.bind(this)
    }

    // state = {isLogin: false, username: '路易斯'} // 构造器中的初始化内容可以声明在外侧,但需要注意this指向

    render() {
        console.log('render');
        const {isLogin, username} = this.state
        return (
            <div>
                <h3>早上好!<span style={{color: "red"}}>{username}</span>{isLogin ? '欢迎访问XXX系统!' : '请点击下方按钮进行登录~'}</h3>
                <button onClick={this.switchLogin}>{isLogin ? '注销': '登录'}</button>
            </div>
        )
    }

    switchLogin() {
        console.log('switchLogin');
        const isLogin = this.state.isLogin
        this.setState({isLogin: !isLogin})
        console.log(this);
    }
}
ReactDOM.render(<LoginStatus/>, document.getElementById('app'))

您可以点击此处进行在线预览执行效果,浏览时推荐打开浏览器控制台查看相关的输出内容。

在这个示例中,请着重注意以下几个问题:

🍎 state 内的数据是通过什么方式进行变更的?

state 维护的数据只能通过 React 组件对象示例的 setState() 方法进行调用。

扩展资料:知乎:React 为什么不能直接修改 state?

🍎 构造器和 render 函数分别在什么时候执行,以及它们各自会被调用多少次?

构造器在实例声明的时候调用,且只调用一次。render 函数则在除了在 state 渲染到页面时调用外,还会在每次 setState 函数执行时调用,即 render 被调用次数为 setState 函数调用次数加一。

🍎 可以不声明构造器吗?构造器中的 super() 的位置有什么要求?以及它的作用是什么?

React 类式组件可以不显示声明构造器,但 React.Component 中是默认存在的。如显式声明构造器,则必须指定 super() 函数,且应当保证其在构造器内所有的有效代码中第一位执行,这是因为 super 函数会继承父类的 this 对象并对其进行加工。

🍎 构造器及其 super 函数的 props 参数是否必须?

不是必须,视需要而定,如需要在构造器中使用 props 对象,则必须传递。

代码改造

事实上,在 React 类式组件中,通过 function 关键字声明的函数,在函数体中是无法直接获取到 this 对象的,但通过箭头函数声明却可以轻易地获取到 this 对象,基于上面的示例进行改造如下:

class LoginStatus extends React.Component {
    state = { isLogin: false, username: '路易斯' }

    render() {
        const { isLogin, username } = this.state
        return (
            <div>
                <h3>早上好!<span style={{ color: "red" }}>{username}</span>{isLogin ? '欢迎访问XXX系统!' : '请点击下方按钮进行登录~'}</h3>
                <button onClick={this.switchLogin}>{isLogin ? '注销' : '登录'}</button>
            </div>
        )
    }

    switchLogin = () => {
        const isLogin = this.state.isLogin
        this.setState({ isLogin: !isLogin })
    }
}
ReactDOM.render(<LoginStatus />, document.getElementById('app'))

在这个示例中,箭头函数中的 this 指向不会丢失,这是因为箭头函数不会创建其自身的执行上下文,因此箭头函数中的 this 会向其作用域外层逐层查找,直到找到 this 的定义。

函数式组件中的 state

state 在函数式组件中的使用方式略有不同,以下是 state 在函数式组件中的使用:

function LoginState() {
    const [isLogin, setIsLogin] = React.useState(false)
    const [username, setUsername]= React.useState(false)

    const switchLogin = () => setIsLogin(!isLogin)

    return (
        <div>
            <h3>早上好!<span style={{ color: "red" }}>{username}</span>{isLogin ? '欢迎访问XXX系统!' : '请点击下方按钮进行登录~'}</h3>
            <button onClick={switchLogin}>{isLogin ? '注销' : '登录'}</button>
        </div>
    )
}

三大核心属性:props

React 中的每个组件都会有其自身的 props 属性,该属性值来源于组件标签中传递的属性,其作用即是通过标签属性从组件外部向组件内部传递变化的数据。

props 的基本使用

<div id="node01"></div>
<div id="node02"></div>
<div id="node03"></div>

<script type="text/babel">
class Person extends React.Component {
    render() {
        console.log(this);
        const { name, age, gender } = this.props
        return (
            <ul>
                <li>姓名:{name}</li>
                <li>性别:{gender}</li>
                <li>年龄:{age}</li>
            </ul>
        )
    }
}

ReactDOM.render(<Person name="Tom" age={19} gender="男" />, document.getElementById('node01'))
ReactDOM.render(<Person name="Jerry" age={18} gender="女" />, document.getElementById('node02'))

const p = { name: 'Spike', age: 22, gender: '男' }
// 使用扩展运算符(展开运算符)
ReactDOM.render(<Person {...p} />, document.getElementById('node03'))
</script>

您可以点击此处在线预览该代码片段的执行效果,浏览时推荐打开浏览器控制台查看相关的输出内容。

JavaScript 扩展运算符是 ES6 中的语法糖,如不熟悉的,可自行百度了解。

props 参数限制与默认值

此外,props 在接收参数时,也可以对参数类型等进行一系列限制以及为其指定默认值,使其符合组件预期的规范,例如:

class Person extends React.Component {
    static defaultProps = {// 指定默认值
        gender: '男',
        age: 18
    }
    // 类型限制及默认类型,既可以通过static关键字在类内部声明,也可以在类外部声明
    render() {
        console.log(this);
        const { name, age, gender } = this.props
        return (
            <ul>
                <li>姓名:{name}</li>
                <li>性别:{gender}</li>
                <li>年龄:{age}</li>
            </ul>
        )
    }
}

Person.propTypes = {
    name: PropTypes.string.isRequired, // 必须传递限制
    gender: PropTypes.string,// 字符串类型限制
    age: PropTypes.number,// 数值类型限制
    speak: PropTypes.func,// 函数类型限制
}

ReactDOM.render(<Person name="Tom" age={19} speak={speak} />, document.getElementById('node01'))
ReactDOM.render(<Person name="Jerry" gender="女" />, document.getElementById('node02'))

const p = { name: 'Spike', age: 22, gender: '男' }
ReactDOM.render(<Person {...p} />, document.getElementById('node03'))

function speak() {
    console.log("speak()")
}

但值得注意的是,类型限制需要引入一个 prop-types.js 作为支撑,你可以参考 npmjs.com 选择合适的 CDN 库,也可以参考我为您编写的在线代码片段,并尝试触发类型校验。

props 在函数式组件中的使用

与 state 不同,props 参数在 React 函数式组件中是可以直接获取到的,它主要是通过函数的参数进行传递的:

function Person(props) {
    const { name, age, gender } = props
    return (
        <ul>
            <li>姓名:{name}</li>
            <li>性别:{gender}</li>
            <li>年龄:{age}</li>
        </ul>
    )

}
Person.defaultProps = {// 指定默认值
    gender: '男',
    age: 18
}
Person.propTypes = {
    name: PropTypes.string.isRequired, // 必须传递限制
    gender: PropTypes.string,// 字符串类型限制
    age: PropTypes.number,// 数值类型限制
}

ReactDOM.render(<Person name="Tom" age={19} />, document.getElementById('node01'))

三大核心属性:ref(s)

组件内的标签可以通过 ref 属性来标识自己,通过该属性,可以很方便地获取到当前的节点信息。

ref 的基本使用

class Form extends React.Component {
    search = () => {
        const { query } = this.refs
        console.log('捕获的节点信息:', query)
        console.log('搜索关键字:', query.value)
    }
    showName = () => {
        const { username } = this
        console.log('输入的用户名为', username.value)
    }
    myRef = React.createRef()
    showAddress = () => {
        console.log('输入的地址为', this.myRef.current.value)
    }
    render() {
        return (
            <div>
                <p>普通形式的 ref</p>
                <input ref="query" type="text" placeholder="请输入搜索内容" />
                <button onClick={this.search}>搜索</button>
                <hr />
                <p>回调函数形式的 ref</p>
                <input ref={c => this.username = c} type="text" placeholder="请输入用户姓名" onBlur={this.showName} />
                <span>左侧输入框离焦取值</span>
                <hr />
                <p>createRef 的使用</p>
                <input ref={this.myRef} type="text" placeholder="请输入用户地址" onBlur={this.showAddress} />
                <span>左侧输入框离焦取值</span>
            </div>
        )
    }
}
ReactDOM.render(<Form />, document.getElementById('app'))

您可以点击此处在线预览该代码片段的执行效果,浏览时推荐打开浏览器控制台查看相关的输出内容。

事件处理

React 组件中的事件是通过类似 onClick 这种驼峰命名的属性来指定事件处理函数的,在 React 中使用事件处理时,您应当注意如下几点:

  1. React 使用的是自定义事件,而不是使用的原生 DOM 事件,其目的是为了更好的兼容性。

  2. React 中的事件是通过事件委托方式处理的(委托给组件最外层的元素),其目的是为了的高效。

  3. 官方明确提出,不应当过渡使用 ref 属性。在许多常规情形中,我们可以通过 event.target 来获取当前的 DOM 对象,用于替代 ref。

注:React 组件的 ref 属性在函数式组件中的使用方式与类式组件中的使用方式一致。

表单处理

React 中有两种形式来处理表单输入:

  1. 受控组件

    简单理解,如果一个表单元素的值是交由 React 组件进行维护(控制),那么我们就称其为受控组件,例如:

    class Login extends React.Component {
    state = {
    username: '',
    password: ''
    }
    saveUsername = (event) => {
    this.setState({ username: event.target.value })
    }
    savePassword = (event) => {
    this.setState({ password: event.target.value })
    }
    handleSubmit = (event) => {
    event.preventDefault() // 阻止表单提交
    const { username, password } = this.state
    alert(`你输入的用户名是:${username},你输入的密码是:${password}`)
    }
    
    render() {
    return (
        <form onSubmit={this.handleSubmit}>
            用户名:<input onChange={this.saveUsername} type="text" name="username" />
            密码:<input onChange={this.savePassword} type="password" name="password" />
            <button>登录</button>
        </form>
    )
    }
    }
    ReactDOM.render(<Login />, document.getElementById('app'))
  2. 非受控组件

    与受控组件相反,如果一个表单元素的值不是交由 React 组件进行维护,那么我们就称其为非受控组件,例如:

    class Login extends React.Component {
       handleSubmit = (event) => {
           event.preventDefault() //阻止表单提交
           const { username, password } = this
           alert(`你输入的用户名是:${username.value},你输入的密码是:${password.value}`)
       }
       render() {
           return (
               <form onSubmit={this.handleSubmit}>
               用户名:<input ref={c => this.username = c} type="text" name="username" />
               密码:<input ref={c => this.password = c} type="password" name="password" />
                   <button>登录</button>
               </form>
           )
       }
    }
    ReactDOM.render(<Login />, document.getElementById('app'))

组件的生命周期

React组件的生命周期

所谓的生命周期,其实就是对象从创建到销毁的整个过程。React 组件中包含了一系列钩子函数(生命周期函数),能够在其生命周期的不同阶段调用执行。通过这些钩子函数,使我们可以很方便地在 React 组件生命的各个阶段监控其状态或执行一些特定的代码。

总体而言,React 组件的生命周期可分为三个阶段:

下面的徽标表示对应的重要程度,后文会进行详细说明。

  1. 挂载阶段(Mounting)

    当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下:

    • constructor - [普通]{.label .primary}

    • static getDerivedStateFromProps - [忽略]{.label}

    • render - [重要]{.label .danger}

    • componentDidMount - [重要]{.label .danger}

  2. 更新阶段(Updating)

    当组件的 props 或 state 发生变化时会触发更新。组件更新的生命周期调用顺序如下:

    • static getDerivedStateFromProps - [忽略]{.label}

    • shouldComponentUpdate - [忽略]{.label}

    • render - [重要]{.label .danger}

    • getSnapshotBeforeUpdate - [忽略]{.label}

    • componentDidUpdate - [普通]{.label .primary}

  3. 卸载阶段(Unmounting)

    当组件从 DOM 中移除时会调用如下方法:

    • componentWillUnmount - [重要]{.label .danger}

如上,在 React 生命周期中,共计有 8 个生命周期函数,这里分别使用了不同的徽标来表示它们的重要性。其中真正重要的只有 3 个(必须熟知),标记普通的有 2 个(了解或掌握),标记忽略的有 3 个(不常用可忽略)。但这些重要性标签只是出于常规角度考虑,如果您出于面试等知识点考察的需要,推荐详细阅读官方文档:React 组件的生命周期

另外,您也可以借助 React 组件生命周期图谱来获得更加直观的认知,该图谱对于理解 React 生命周期很有益处,请务必看一看。

三个重要的生命周期函数:

  1. render

    render 是类式组件中唯一必须实现的方法,它在初始化渲染或更新渲染时调用。当 render 被调用时,它会检查 this.propsthis.state 的变化并返回以下类型之一:

    • React 元素

    • 数组或 fragments

    • Portals

    • 字符串或数值类型

    • 布尔或 null

  2. componentDidMount

    componentDidMount 会在组件挂载后立即调用,它适用于一些依赖于 DOM 节点的初始化操作。

  3. componentWillUnmount

    componentWillUnmount 会在组件卸载及销毁之前调用,它适用于进行一些必要的收尾操作,例如断开通讯连接、清除定时器等。

两个普通程度的生命周期函数

  1. constructor

    从前文中,我们已经获知 constructor 的作用在于对组件实例对象进行初始化。但在实际应用中,对于实现同样的功能,constructor 通常可以被其他方式等效替代,因此其重要程度相对较低。

  2. componentDidUpdate

    componentDidUpdate 会在 state 或 props 更新后立即调用,但当组件首次渲染,则不会执行该方法。此外,它还可以记录前一次变更的参数信息,基于这个特性,我们可以有效地监控数据的变更,对于一些特定的需求有较大帮助。

    componentDidUpdate (preProps, preState, snapshotValue) {
    console.log(preProps, preState, snapshotValue)
    }

    需要注意的是,由于该函数本身是监控数据状态变更的,因此,当需要在该函数中修改数据时,请务必注意避免导致死循环。

三个标记忽略的生命周期函数

剩下三个标记忽略的生命周期函数,在官方文档中已明确为不常用的生命周期方法,此处不做扩展,如需要了解请直接阅读官方文档。

关于组件的生命周期,您可以尝试执行一下我为您编写的在线小案例

注:React 旧版本中的生命周期略有不同,但个人觉得没必要了解,毕竟技术这东西总是学新不学旧。如感兴趣,可以自行百度。

虚拟 DOM 与 Diffing 算法

Diffing算法

上图源于网络,有助于理解 Diffing 算法,您可以带着对该图的印象继续阅读下文。

扩展资料:https://blog.51cto.com/u_15127606/4026821

虚拟 DOM 中 key 的作用

key 是虚拟 DOM 对象的标识,用于帮助 React 识别哪些元素被改变了。

当状态中的数据发生变化时,React 会根据新数据生成新的虚拟 DOM,然后将新虚拟 DOM 和旧虚拟 DOM 进行 diff 比较,其比较规则如下:

  1. 如果旧的虚拟 DOM 中存在与新的虚拟 DOM 相同的 key:

    则判断虚拟 DOM 中的内容是否变更,如果没变更,则直接使用真实 DOM,如果有变更,则生成新的真实 DOM 并替换掉页面上旧的真实 DOM。

  2. 反之,如果旧的虚拟 DOM 中不存在与新的虚拟 DOM 相同的 key:

    则直接根据数据创建新的真实 DOM 并渲染到页面。

🍎 可以使用 index 作为虚拟 DOM 的 key 吗?

可以,但极不推荐。当使用索引作为虚拟 DOM 的 key 时:

  • 若对数据进行:逆序添加、逆序删除等破坏顺序的操作,会产生没必要的真实 DOM 更新,相当于任何一次修改都会对列表中的数据进行全量更新,效率低。
  • 如果列表结构中包含有输入类的 DOM,则会出现错误的 DOM 更新。

因此,如果不存在对数据的逆序添加、逆序删除等破坏顺序的操作,仅用于列表渲染展示,那么使用索引作为 key 是不会引发异常的。

关于这一部分,您也可以通过点击此处查看为您编写的示例,以便获得更加直观的认知。

扩展资料:深度解析使用索引作为 key 的负面影响

呼~,到此为止,恭喜旅者大人终于具备了开启 React 之旅的基本素质和能力,接下来我将指导您注册身份、打造装备,然后开启您专属的新手村。

React 脚手架

所谓的某某脚手架,其实本质上也是一个项目,只是这个项目的作用在于帮助我们初始化一个新的项目,并创建一个简单的模板,即所谓的抛砖引玉。

脚手架安装与项目初始化

React 为我们提供了一个名为 create-react-app 的脚手架,如您已经安装有 npm 环境,只需执行如下命令,即可完成 React 脚手架的安装:

npm i create-react-app -g

如果您的 npm 在安装脚手架时存在网络问题,可以选择前往源码地址进行下载安装。

安装完成后,即可通过简单的命令初始化一个 React 项目了:

create-react-app myproject

项目初始化完成后,您可以通过如下命令尝试启动它:

cd myproject
npm start

启动成功后,访问 http://localhost:3000,您将看到如下界面:

Reactapp预览

接下来请了解一下我们初始化的项目目录结构,以及不同文件、文件夹的作用:

myproject
├── README.md    ====> 项目导览说明
├── node_modules    ====> 项目依赖包
├── package.json    ====> 项目依赖包及脚本配置
├── .gitignore    ====> GIT代码忽略配置文件
├── public    ====> 静态资源文件夹
│   ├── favicon.ico    ====> 网站页签图标
│   ├── index.html    ====> 【主页面】
│   ├── manifest.json    ====> 应用加壳配置文件
│   └── robots.txt    ====> 爬虫协议文件
└── src    ====> 源码文件夹
    ├── App.css    ====> App组件样式文件
    ├── App.js    ====> App组件
    ├── App.test.js    ====> 测试文件
    ├── index.css    ====> 主页样式文件
    ├── index.js    ====> 主页脚本文件
    ├── logo.svg    ====> LOGO图片
    ├── reportWebVitals.js    ====> 页面性能分析文件
    └── setupTests.js    ====> 组件单元测试支持文件

阅读到这里,如果您还未进行任何编码,那么我在这里郑重地提醒您,请务必亲自尝试一下,并梳理初始项目的运行逻辑,后续的学习路线将会逐渐陡峭,不好好实践是走不出新手村的哦(当然啦,也不必太过担心,微不足道的我还是会很尽心地为各位旅者大人考虑的,但无论怎么看实践都是必须的吧,请大人们不要怠惰)。

🍎 在脚手架创建的项目中,各种文件之间是如何进行相互引用的?

基于模块化思想,模块之间的引用需要使用 exportimport 关键字进行的,被引入的模块需要使用 export 进行暴露,引用方则通过 import Xxx from 'xxx' 的方式进行引入。如果被引用内容是静态资源,则不需要声明 export。

项目案例

各位大人见谅,由于 codepen 只适用于单文件在线编辑,后续示例将转到 codesandbox 进行。

项目标题:我的任务清单

功能简述

本项目用于管理个人任务,支持任务列表显示、任务动态插入、单条任务移除、任务全选、已完成任务清空、任务完成度显示,以及移除按钮悬停展示等功能。

项目预览

  1. 在线预览:https://qqsjx0.csb.app(加载可能会有点慢)

  2. 截图预览:

    我的任务清单演示GIF

项目地址

提示:代码是这一小结的经验点数最多的内容,烦请各位大人移驾,下方两个传送通道可择一而入。

  1. 代码沙箱:CodeSandbox - 我的任务清单

  2. 代码仓库:https://github.com/xfc-exclave/react-learning - myproject

重要知识点

  1. 关于 UI

    本项目中使用了第三方 UI 框架——Material UI,目前值得推荐的 React UI 框架主推如下几个:

  2. 组件的拆分方式

    组件拆分方式并没有一定的规定,不同项目,不同业务逻辑,不同开发者,都会有不一样的组件拆分方式。通常情况下较为合理的是以组件功能作为单元进行拆分,尽量避免大量相似代码重复编写,那样会相当冗余且难以维护。

    总之,组件拆分方式人各不同,您完全可以借鉴一些讨论贴(知乎),来广泛了解,以便形成您自己的组件拆分风格,但只有一条不变的准则是:代码的设计应当尽量保持优雅、高效、易维护

  3. 如何确定 state 声明的位置

    要确定 state 声明的位置,需要先行明确 state 数据被使用的位置,通常来说,state 最佳的声明位置是所有使用该 state 数据的子组件的共有父组件中,因为这样可以使组件之间传递数据的代价最小化。但这也不是绝对的,对于一些复杂的场景,往往需要还更加细致、具体的考虑。

  4. 组件之间的通信方式

    在组件化的项目中,经常需要跨越不同组件使用或处理数据,因此,不同组件之间的数据传递方式则尤为重要。我们可以形象地将组件之间的关系分为兄弟、父子、祖孙、宗亲几类。

    父子组件通信:

    父子组件之间进行传递数据,最直接的方式是通过 props 属性实现。注意,这里所说的数据既可以是普通类型的数据,也可以是一个预定义的函数。

    父组件向子组件传递数据直接使用 props 即可完成,在前文 React 组件的三大属性部分已有充分讲解,这里不再赘述。

    子组件向父组件传递数据,则可以通过 props 属性迂回实现,一种最常用的实现方式如下:

    function Child(props) {
       const changeName = () => props.updateName("xxx")
       return <button onClick={changeName}>Send</button>
    }
    export default function Parent() {
       const [name, setName] = React.useState('')
       const updateHandler = value => setName(value)
       return <Child updateName={updateHandler } />
    }

    兄弟组件通信:

    兄弟组件之间是无法轻易进行直接通信的,它们需要借助共同的父组件实现通信。简单来说,父组件将函数 f 传递给子组件 A,子组件 A 通过函数 f 改变父组件状态,同级子组件 B 可以通过 props 接收到父组件变更后的状态。

    以上方式适用于“血缘”相近的组件关系,“祖孙”组件或“宗亲”组件之间的通信虽然也可通过这种方式进行,但代价相对更大。对于“血缘”关系较远的组件之间进行通信,可以使用如发布订阅、Redux 等方式实现,此处不做扩展,在后文会有专门的章节详细阐述。

React Ajax

AJAX 是一种在无需重新加载整个网页的情况下,就能够更新部分网页的技术。

实在惭愧,在此之前本人一直觉得 ajax 就是指 $.ajax({}),在学习 Vue 的时候也没有仔细思考 ajax 和 axois 的关系,这里再次强调一下,axios 是通过 Promise 对 ajax 的一种封装(大人们不要笑话我)。

其实,React 本身只关注于界面,它并不包含 ajax 网络请求部分,因此,要在 React 中实现网络交互,就需要集成第三方 ajax 库(或者自行封装实现)。

你当然可以选择将 jQuery 集成到 React 项目中,但这就违背了 React 尽量减少真实 DOM 操作的初衷。因此,使用 axios 进行网络通信是 React 的更优选择。或许您在学习 Vue 的过程中已经对 axios 有了相当程度的了解,那么这一小节对您而言将会相当轻松。

axios

开源地址:https://github.com/axios/axios

中文网:http://www.axios-js.com

axios 使用示例:

axios.get('https://chinmoku.usemock.com/user/1').then(
    (response) => this.setState({ data: JSON.stringify(response.data) }),
    (error) => this.setState({ data: "获取数据失败" })
)

您可以点击这里运行在线示例并查看效果。

前端代理

基于上面的 axios 示例,相信您已经能够成功发送网络请求并得到返回的数据。但在实际开发过程中,网络环境相对更加复杂,因此也会引发一些问题,跨域问题就是最常见的问题之一。

🍎 什么是跨域?为什么会有跨域?

当一个请求 url 的协议、域名、端口三者之间任意一个与当前页面 url 不同,即为跨域。其出现是出于浏览器同源策略的限制,而同源策略的目的又是为了保障浏览器的基本安全。在开发过程中,我们也有诸多方式解决来解决浏览器同源策略引发的跨域问题,其中,使用代理就是一种最为常用的方式。

扩展资料:同源策略_百度百科

🍎 什么是代理?为什么要用代理?

代理,即替代他人负责某一件事,在网络通信中,所谓代理即指代替其他网络端负责处理网络通信。例如通过 VPN 连接一些特定的网络就是一种常见的代理。而在 React 中,使用代理的直接目的,就是为了解决网络通信的跨域问题。

React 中配置代理的方式

在 React 中有两种配置代理的方式:

  1. package.json 文件中进行配置

    直接在 package.json 文件中追加内容即可,示例如下:

    {
     "scripts": {}
     "browserslist": {}
     "proxy": "https://chinmoku.usemock.com"
    }

    这种方式的优点在于配置简单方便,使用时直接指定接口地址即可,无需添加额外前缀:

    import axios from "axios";
    // ...省略部分内容
    axios.get('/user/1').then(
       response => console.log(response.data)
    )
  2. 使用代理配置文件

    使用代理配置文件则更加灵活,其配置方式如下:

    执行命令 npm i http-proxy-middleware 安装依赖包,然后在 src 目录下创建文件 setupProxy.js 文件

    const { createProxyMiddleware: proxy } = require('http-proxy-middleware')
    
    module.exports = function (app) {
     app.use(
         proxy('/dev', {
             target: 'http://localhost:8080',
             changeOrigin: true,
             pathRewrite: { '^/dev': '' } // 重写请求路径(必须)
         }),
         proxy('/prod', {
             target: 'https://chinmoku.usemock.com',
             changeOrigin: true,
             pathRewrite: { '^/prod': '' }
         })
     )
    }

    这样就允许同时配置多个代理地址,使用时只需要加上对应的前缀即可:

    import axios from "axios";
       // ...省略部分内容
       axios.get('/prod/user/1').then(
       response => console.log(response.data)
    )

    这种方式在实际开发中更为常用,因为它方便开发者进行网络环境切换。

项目案例

项目标题:Github 用户检索

功能简述

本项目用于对 Github 用户进行检索,并将检索结果动态显示到列表中,同时,为提升用户体验,对加载中及无数据的状态进行友好展示。

项目预览

  1. 在线预览:https://quksqg.csb.app(加载可能会有点慢)

  2. 截图预览:

    githubusersearch

项目地址

  1. 代码沙箱:CodeSandbox - Github 用户检索

  2. 代码仓库:https://github.com/xfc-exclave/react-learning - react-github-search

重要知识点

  1. 关于 UI:此案例中使用了 Ant Design。

  2. 消息发布与订阅机制

    在前文中我们曾使用借助组件的 props 来实现父子组件之间的通信,但其实除了借助 props 之外,我们还可以使用消息发布和订阅机制来实现通信。与 props 相比,消息发布订阅机制并不注重组件之间的关系,即消息发布订阅机制的效率与组件之间的层级关系无关,它可以用于所有的组件关系之间。

    首先,使用消息发布订阅之前,你需要先执行命令 npm i pubsub-js 安装依赖,并在数据的发送方使用如下方式发送消息:

    import PubSub from "pubsub-js";
    // ...省略无关代码
    PubSub.publish('channelNameHere', data)

    消息接收方则通过如下方式接收数据并处理后续逻辑:

    PubSub.subscribe('channelNameHere', (_, data) => {
       // ...自定义data方式
    })

    但需要保证的是,消息发送方与消息接收方的消息通道名称必须保持一致。

  3. React.useEffect

    通常,消息发布方发布消息时通常会有一定的触发时机,而消息接收方为了及时接收到消息,通常会在组件挂载时即开启订阅,在组件卸载时取消订阅,这就依赖于 React 组件的生命周期。

    在类式组件中,由于其父组件 Component 为其暴露了一系列生命周期函数,因此它的生命周期是明确的,能够轻易地在不同的触发时机执行不同的逻辑。而函数式组件由于没有继承自 Component,因此也就没有生命周期函数,它只能通过 Hook 函数 React.useEffect() 来获取监控组件的生命状态,但该函数却是不可靠的(如有必须通过生命周期函数实现的逻辑,个人觉得还是使用类式组件更好)。

    对于 useEffect 这个钩子函数,官方的说法是“在函数组件主体内改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性”,官方似乎并没有给出明确的原因,也没有给出明确的方案(也或许是我目前没了解到,如果哪位大人知道,烦请在文末留言告知)。

  4. 使用 fetch 发送网络请求

    fetch 也是一种发送网络请求的方式,其基本使用方式如下:

    // fetch 发送 GET 请求
    fetch('https://chinmoku.usemock.com/user/1').then(res => {
       return res.data.json();
    }).then(data => {
       handleData(data)
    })
    // fetch 发送 POST 请求
    fetch(url, {
       method: 'POST',// 或其他
       body: JSON.stringify(data)
    }).then(res => {
       return res.data.json();
    }).then(data => {
       handleData(data)
    }).catch(err => {})

React 路由

路由的概念来源于服务端,在服务端,路由描述的是 URL 与处理函数之间的映射关系,例如 Java 中的 Controller,Python 中的 urls.py

在 Web 前端的单页面应用中,路由描述的这是 URL 与 UI 之间的映射关系,这种映射关系是单向的,即 URL 的变化可以引起 UI 的更新,并且这种更新不会引起页面的整体刷新。

SPA

单页 Web 应用(single page web application,SPA),就是只有一张 Web 页面的应用。单页应用程序(SPA)是加载单个 HTML 页面并在用户与应用程序交互时动态更新该页面的 Web 应用程序。浏览器一开始会加载必需的 HTML、CSS 和 JavaScript,所有的操作都在这张页面上完成,都由 JavaScript 来控制。因此,对单页应用来说模块化的开发和设计显得相当重要。

quote from SPA(单页应用程序)_百度百科

在 React 或 Vue 中,项目通常会被设计为这种单页面应用程序,这种设计的好处在于用户体验较好,网页内容的改变不需要整个页面进行刷新,避免了不必要的页面跳转和重新渲染。

总体来说,SPA 具有以下特点:

  1. 整个应用只需要一个完整的页面。

  2. 页面中链接的跳转只是对页面的局部内容进行更新,而不会引起网页的整体刷新。

  3. 页面数据需要通过 ajax 获取,并且在页面异步进行展现。

React 中的路由

react-router-dom

react-router-dom 是 React 中的一个路由插件库,它专门用来实现一个 SPA 应用中的路由跳转功能。react-router-dom 基于 react-router,并且加入了一些浏览器环境下的功能,它封装了更多的 API,更适合在 Web 开发中进行应用。

要使用 react-router-dom,您需要执行如下命令安装依赖:

npm i react-router-dom

项目案例

项目标题:React 路由案例

功能简述

本项目用于集中演示 React 路由相关功能。

项目预览

  1. 在线预览:https://qz4d44.csb.app(加载可能会有点慢)

  2. 截图预览:

项目地址

  1. 代码沙箱:CodeSandbox - React 路由案例

  2. 代码仓库:https://github.com/xfc-exclave/react-learning - react-router-demo

重要知识点

  1. 关于 UI:此案例中使用了 Ant Design。

  2. 关于 react-router-dom 的版本差异。

    注:本人的学习一向注重新版本,对于旧版本通常是简单带过,本文示例使用的 react-router-dom 为 v6+ 版本,如果您在学习时或工作中使用的是旧版本,那么我在此处列出版本之间的差异,以便您能够快速掌握。

    在 V5 中,路由的常规使用方式大体如下:

    import { BrowserRouter, Switch, Route, Redirect } from 'react-router-dom'
    
    function Product(props) {
    return (
     <div>
       <ul className="nav nav-tabs">
         <li>
           <Link to="/product/list">Product List</Link>
         </li>
         <li>
           <NavLink to="/product/detail" activeClassName="my-active">Product Detail</NavLink>
         </li>
       </ul>
       <Switch>
         <Route path="/product/list" component={ProductList} />
         <Route path="/product/detail" component={ProductDetail} />
         <Redirect to="/product/list" />
       </Switch>
     </div>
    )
    }
    function App() {
    return (
     <BrowserRouter>
       <div className="menu-list">
         <Link className="list-group-item" to="/">Home</Link>
         <NavLink activeClassName="my-active" to="/products">Products</NavLink>
       </div>
       <div className="panel-body">
         {/* 注册路由 */}
         <Switch>
           <Route key="keyxxx1" exact path="/" component={Home} />
           <Route key="keyxxx2" exact path="/home" component={Home} />
           <Route key="keyxxx3" path="/product" component={Product} />
           <Redirect to="/about" />{/* 当路由匹配不到时,执行重定向 */}
         </Switch>
       </div >
     </BrowserRouter>
    )
    }
    ReactDOM.render(<App />, document.getElementById("app"));

    在 V6 版本中,对路由组件名称及用法略有改变,请参考示例:

    import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
    
    function App() {
    return (
     <BrowserRouter>
       <div className="menu-list">
         <Link className="list-group-item" to="/">Home</Link>
         <NavLink to="/products">Products</NavLink>
       </div>
       <div className="panel-body">
         {/* 注册路由 */}
         <Routes>
           <Route exact path="/" element={<Home />} />
           <Route exact path="/home" component={<Home />} />
           <Route path="/product" component={<Product />} />
           <Route path='*' element={<Navigate to="/" />} />{/* 当路由匹配不到时,执行重定向 */}
         </Routes>
       </div >
     </BrowserRouter>
    )
    }
    ReactDOM.render(<App />, document.getElementById("app"));

    此外,在编程式路由中,V6 还使用 useNavigate() 替代了 V5 中的 props.history.xxx()。总体来说变化不大,就 API 层面来看,只是对名称和使用方式进行了优化。当然,上文使用示例中的这种硬式编码(将路由写死在组件中的方式)并不值得推荐,为了项目的可维护性,您可以尝试将路由相关信息提取出来,作为路由表进行使用,这样的路由才会更加灵活(此处未编写提取路由表的示例,请大人们亲自动手试试)。

  3. HashRouter 与 BrowserRouter

    HashRouter

    HashRouter 是一种基于 window.location.hash 进行路由的方式,当 HashRouter 工作时,它会获取到自身的 pathname 属性,并将该属性与其内部的诸多 Routepath 属性进行匹配,如匹配成功,则渲染对应的组件。

    而 Link 和 NavLink 等组件的内部,其实是通过 this.props.history.push(path) 改变 HashRouter 中的 pathname 属性来驱动 Route 进行不同组件渲染的,从而实现路由切换效果。

    HashRouter 进行路由的地址通常会携带一个锚点符号 #,例如:http://localhost:3000/#/home

    此外,HashRouter 本身是不支持 location.stateloation.key 的。

    扩展资料:https://segmentfault.com/a/1190000014313428

    BrowserRouter

    相较于 HashRouter,BrowserRouter 在 Web 中的应用更为广泛。与 HashRouter 不同的是,BrowserRouter 是基于 THML5 History API 来实现路由的。它可以通过 pushStatereplaceState 来修改浏览器历史记录,也可以在路由跳转时传递任意参数实现组件之间的通信(HashRouter 则只能通过拼接 URL 字符串实现),因此,BrowserRouter 也常常与 Redux 配合使用来实现组件之间的数据通信。

    BrowserRouter 的表现形式与常规的网页地址一致,例如:http://location:3000/home。

    补充

    前面其实已经大体上说明了 HashRouter 与 BrowserRouter 的区别,相较于 HashRouter,BrowserRouter 似乎更有优势,同时也被官方推荐使用。但在使用 BrowserRouter 时也有一点需要特别注意:

    由于 BrowserRouter 模式下的请求形式表现为 http://host:port/path,这就相当于每一个前端路由变更都会向服务端 API 发出请求。如果该请求不处于服务端 API 覆盖范围内,则会返回 404 异常。此外,前端路由与服务端 API 在实际业务中往往是完全不同的概念,因此,在实际开发中应当进行区分,注意避免前端路由与服务端 API 之间的冲突。

  4. 路由参数的传递方式

    在 react-router-dom 中,有三种不同的方式实现路由参数的传递:

    (a) params 参数的传递

    // 1. 路由链接声明
    <Link to{`/product/{categoryId}/{itemId}`}>Click Here</Link>
    // 2. 注册路由
    <Route path="/product/:cateId/:itemId" component={<Detail />} />
    // 3. 参数接收
    const { cateId, itemId } = this.props.match.params

    (b) search 参数的传递

    // 1. 路由链接声明
    <Link to{`/product/detail/?cateId={categoryId}&itemId={itemId}`}>Click Here</Link>
    // 2. 注册路由
    <Route path="/product/detail" component={<Detail />} />
    // 3. 参数接收
    import queryString from 'query-string'
    const { search } = props.location
    const { cateId, itemId } = queryString.parse(search)

    © state 参数的传递

    // 1. 路由链接声明
    <Link to{`/product/detail`, state: { cateId: categoryId, itemId}>Click Here</Link>
    // 2. 注册路由
    <Route path="/product/detail" component={<Detail />} />
    // 3. 参数接收
    const { cateId, itemId } = props.location.state
    // state接收的参数不会受到浏览器页面刷新的影响

    相信示例中的路由参数的传递方式已经足够明确,就不再做多余的解释了。

  5. 编程式路由导航

    在前端开发中,需要进行路由跳转的场景多种多样,在部分场景下(例如定时跳转),声明式路由方式存在一定的局限性,这就需要通过编程式路由来实现。编程式路是借助于 props.history 对象上的 API 来对路由进行操作的。主要 API 如下:

    (a) props.history.push() 跳转,并向历史记录中推入一条新的记录

    (b) props.history.replace() 跳转,并替换历史记录列表中的最新记录

    © props.history.goBack() 后退一条记录

    (d) props.history.goForward() 前进一条记录

    (e) props.history.go(n) 前进或后退指定条记录

    相信各位大人在学习 JavaScript 基础时已充分了解过 HTML5 History API,这里列出的目的在于帮助您快速回顾知识点,相关知识点如有疑问,请务必点击快速通道进行 JavaScript 基础补习。

    当然,在 react-router-dom V6 中已提供了 useNavigate 函数,可以更方便地进行函数式路由导航。

  6. useNavigate 基本使用

    import { useNavigate } from 'react-router-dom'
    
    const navigate = useNavigate()
    
    const detailHandler = params => navigate('/detail', { state: params })
  7. V6 新增的钩子函数

    在 react-router-dom V5 中,我们可以使用 withRouter() 函数将普通组件转换为路由组件,使其具有路由组件相关属性,但在 V6 版本中,该方法已被移除,作为替代,V6 版本中新增了新的钩子函数,可以更加方便地获取路由参数:

    • useParams

    • useLocation

    • useSearchParams

    这几个钩子函数的使用也十分简单,本章节的案例中已有相关示例,自行动手即可得出结论,此处不再额外补充。

反省:在路由这一小节中,原本想把案例尽量做得好些,但最终还没涵盖到所有知识点,就开始觉得文档编写工作很烦躁(也或许终究还是会累的吧,感觉有些莫名的沮丧,接触编程的这几年,渐渐地有些认不清自己了)。

(⊙o⊙)…呃,上面的话是昨天晚上写下的,感觉好丧气的样子,但还是不删掉算了,就当是本文的一点小痕迹,本人既然决心做一个成熟的 NPC,不成熟的想法还是让它漏出来比较好。

Redux

Redux 简介

Redux 是 JavaScript 应用的状态容器,提供可预测的状态管理,它除了与 React 一起使用外,其实也还支持一些其他的界面库。

基于上面这段话,我们可以进行一个简单的梳理和补充:

  1. Redux 是一个专门用于做状态管理的 JavaScript 库(非 React 插件)。

  2. 它常常会与 React 配合使用,但也可以集成在 Vue, Angular 等项目中。

  3. Redux 的作用在于集中式管理 React 应用中多个组件之间共享等状态。

要在 React 中使用 Redux,您首先需要使用如下命令安装相关依赖:

npm i redux

依赖安装完成后,您可以通过官网给出的基础示例来对 Redux 有一个简单印象:

import { createStore } from 'redux'

const initialState = { value: 0 }
function counterReducer(state = initialState, action) {
    switch (action.type) {
        case 'calculator/increment':
            return { value: state.value + action.data }
        case 'calculator/decrement':
            return { value: state.value - action.data }
        default:
            return state
    }
}

const store = createStore(counterReducer)

// 监听store中状态的变更
store.subscribe(() => console.log(store.getState()))
// 使用
store.dispatch({ type: 'calculator/increment', data: 5 })

在这个示例中,我们可以通过 store.dispatch() 的方式实现一个简单的对数据的自增和自减计算。

Redux 核心概念及工作原理

参考上文中的简单示例,再来理解下面这张 Redux 原理图:

Redux 核心架构图

在这个核心架构图中重点声明了四个主体对象,其中 Views 可以代表 React 中的视图组件,除 Views 外,其他三个组件都是 Redux 的核心概念,下面将对这些概念进行逐一叙述:

  1. Action

    action 是一个具有 type 字段的普通 JavaScript 对象。你可以将 action 视为描述应用程序中发生了什么的事件。

    其中,type 字段是一个用字符串表示,可以理解为当前 action 的名称,其规范化的声明方式应当是 域/事件名称,例如:calculator/increment

    action 中除了 type 外,还可以声明其他该事件执行的相关参数,简单的 action 声明如下:

    const incrementAction = {
       type: 'calculation/increment',
       data: 2
    }
  2. Store

    在同一个 Redux 应用中,有且只有一个 store,它充当着 action 与 reducers 之间的协调者,负责将 action 中的事件及参数交给 reducers 执行,获取到执行结果后,再交由 Views 视图组件进行应用或渲染。

  3. Reducers

    Reducers 是 Redux 中事件的的核心处理者,它被声明为函数类型,接收 state 和一个 action 对象。

    Reducer 必须遵循如下规则:

    • 仅使用 state 和 action 参数计算新的状态值。

    • 禁止直接修改 state,必须通过复制现有的 state 并对复制的值进行更改的方式来做不可变更新。

    • 禁止任何异步逻辑、依赖随机值或导致其他“副作用”的代码。

    简单来说,在使用 Reducer 时,必须保证它是一个纯函数。

扩展资料:

  1. 什么是纯函数

  2. Why Redux need reducers to be “pure functions”

Redux 核心 API

  1. createStore()

    在上文中,我们已经使用过该函数,其使用方式如下:

    import { createStore } from 'redux'
    
    const store = createStore(counterReducer)
  2. store 对象相关函数

    • store.getState()

      获取 reducer 执行后返回的 state。

    • store.dispatch(action)

      它是更新 redux state 的唯一方法,并且需要传递一个 action 对象。

    • store.subscribe(listener)

      向 store 中添加一个监听器,该监听器会在每次 dispatch action 时执行。

  3. applyMiddleware()

    其作用在于允许使用基于 redux 的中间件(插件库),例如后文中将要讲解的 redux-thunk,下面列出它的基本使用方式,您可以先忽略它,或者做一个前瞻性的了解:

    import { createStore, applyMiddleware } from 'redux'
    import countReducer from './count_reducer'
    import thunk from 'redux-thunk'
    
    export default createStore(countReducer, applyMiddleware(thunk))
  4. combineReducers()

    该函数用于将多个 Reducer 函数进行合并,例如:

    import { combineReducers } from 'redux'
    
    const reducers = combineReducers({
     myCounter: counterReducer,
     myFormater: formaterReducer
    })

Redux 异步编程

Redux 本身是不支持进行异步编程的,如果需要在 Redux 中进行异步编程,则需要执行如下命令安装插件库:

npm i redux-thunk

其简易的使用示例如下:

  1. 创建 store

    import { createStore, applyMiddleware } from 'redux'
    import countReducer from './count_reducer'
    import thunk from 'redux-thunk'
    
    export default createStore(countReducer, applyMiddleware(thunk))
  2. 异步使用

    const createIncrementAsyncAction = data => {
       return dispatch => {
           setTimeout(() => dispatch({ type: 'increment', data }), 2000)
       }
    }

react-redux

与 redux 不同,react-redux 是一个基于 Redux 的 React 插件库。使用 react-redux,可以更加方便地读取 store 中的数据,以及向 store 中分发 action 来更新数据。

使用 react-redux 需要执行如下命令安装必要的依赖:

npm i redux react-redux
  1. UI 组件与容器组件

    在 React-Redux 中,所有组件被拆分为 UI 组件和容器组件两大类。

    UI 组件,用于组织页面框架及以及负责 UI 的呈现,它通过 props 来接收数据并进行渲染,其内部通常不会执行任何 Redux API 相关操作,UI 组件一般保存在 components 文件夹下。

    容器组件,主要负责数据管理和业务逻辑的处理,通常保存在 containers 文件夹下。

    看网上有把前者称为傻瓜组件,把后者称为聪明组件,那这么说来前端就是傻瓜开发,后端就是聪明开发咯(虽然没有较真的必要,但还是感觉没啥道理)。

  2. 相关 API 的使用

    • connect

      React-Redux 提供 connect() 方法,用于从 UI 组件生成容器组件。connect 即将这两种组件连起来。

      import { connect } from 'react-redux'
      
      const CountContainer =  connect(
         mapStateToProps,
         mapDispatchToProps
      )(CountUI)

      其中,CountUI 即为 UI 组件,CountContainer 即为容器组件,并且,connect 函数接收两个参数,相关定义请继续阅读后续小点。

    • mapStateToProps

      mapStateToProps() 函数负责输入逻辑,将外部数据(即 state 对象)映射为 UI 组件的标签属性。

      该函数返回一个对象,其声明示例如下:

      const mapStateToProps = state => { count: state }

      此外,该函数还会订阅 Store,每当 state 更新时,该函数就会自动执行,并重新计算 UI 组件的参数,从而触发 UI 组件的重新渲染。

    • mapDispatchToProps

      mapDispatchToProps 负责输出逻辑,即分发 action 的函数转换为 UI 组件的标签属性,该属性既可以是一个对象,也可以是一个函数。

      mapDispatchToProps 作为函数声明方式:

      const mapDispatchToProps = dispatch => {
         return {
             increment: number => dispatch(createIncrementAction(number))
         }
      }

      mapDispatchToProps 作为对象声明方式:

      const mapDispatchToProps = {
         onClick: number => createIncrementAction(number)
      }

      但值得注意的是,当 mapDispatchToProps 使用对象声明时,该对象的键名必须是 UI 组件的同名参数,对应的键值则应当是一个可执行的函数。

    • Provider 组件

      在使用 connect 函数生成容器组件后,容器组件需要获取到 state 参数,才能将其传递到对应的 UI 组件中。您当然可以选择将 state 对象作为桉树传入到容器组件中,但当容器组件层级较深时,这种传递方式将会相当麻烦。

      为此,React-Redux 提供一个 Provider 组件,可以让容器组件获取到 state,使用该组件也相当简单,只需要用该组件作为外层组件,将声明和使用 React-Redux 相关属性的组件包括在内即可,通常我们会直接作用在根组件上:

      import React from 'react'
      import ReactDOM from 'react-dom'
      import App from './App'
      import store from './redux/store'
      import {Provider} from 'react-redux'
      
      ReactDOM.render(
         <Provider store={store}>
             <App/>
         </Provider>,
         document.getElementById('root')
      )
  3. 浏览器调试工具

    个人觉得是一个既鸡肋,又麻烦的插件,但聊胜于无,这里简单带过,各位大人如有兴趣可自行探索。

    步骤一:chrome 商店搜索 Redux DevTools 并添加到浏览器。

    chrome 应用商店:https://chrome.google.com/webstore/category/extensions)

    chrome Redux DevTools

    步骤二:在项目中安装依赖包。

    npm i redux-devtools-extension

项目案例

此小结对于如何组织 redux,相关的使用规范,以及如何进行合理的结构划分并没有做相关叙述,这些非硬性知识点的内容在代码中将会有更加直观的表现。

项目标题:Redux 实践案例

功能简述

本项目用于集中演示 React 路由相关功能,集中展示商品和收获人列表,商品发生变化时,收货人列表可以即时获得反馈,收货人列表发生变化时,商品列表也能即时获得反馈,商品添加使用异步方式执行。

项目预览

  1. 在线预览:https://8jngqw.csb.app(加载可能会有点慢)

  2. 截图预览:

    redux 实践案例

项目地址

  1. 代码沙箱:CodeSandbox - Redux 实践案例

  2. 代码仓库:https://github.com/xfc-exclave/react-learning - react-redux-demo

重要知识点

  1. 关于 UI:此案例中使用了 Ant Design。

  2. 关于 React-Redux

    不知道各位大人对 Redux 的核心概念及工作原理中流程图是否还有印象,React-Redux 在 Redux 流程的基础上,将 Views 拆分为容器组件和 UI 组件。实际上是对 Redux 进行了更加细粒度的拆分和控制。这种将视图和逻辑进行分治管理的思想并非某一种语言特有,相信你一定听说过 MVC、MVP、MVVM 等概念,其实 React-Redux 的拆分方式就是这类思想的一种应用,特意为大人们再次绘图如下:

    React-Redux 原理图

    温馨提示:结合原理图和案例代码进行思考,思路将会更加清晰。

  3. 此案例模块拆分得比较细,各位大人在梳理关系时,可以选择从 App 组件开始,也可以从 UI 组件开始。另外,运行案例时,可以打开浏览器控制台,观察输出顺序。

:::success
到此为止,恭喜您坚持不懈走完了这条新手村的慢慢长路,也很荣幸能够作为您在新手阶段的 NPC,说起来想要努力做一个优秀、成熟的 NPC 还真不容易呢,或许本文部分观点略有偏差,案例也不一定完整契合,知识点也没能面面俱到,行文方式也不一定十分合理,甚至可能还会有一些弄巧成拙的小心思,还希望各位心地善良的大人们念我数日苦战之功,原谅我的诸多过失,另外,如果有幸能够得到大人们宝贵的意见和建议,那将是本 NPC 的莫大荣幸。

至此,江湖再见了。
:::

React 扩展

本文正式内容在 Redux 之后就已经告一段落了,这一小节仅仅是额外进行一些小扩展(或者对遗漏进行补充),其重要级别相对较低,各位大人们可以选择性阅读。

注:本部分内容属于持续更新内容。

  1. lazyload

    // 1.通过React的lazy函数配合import()函数动态加载路由组件 ===> 路由组件代码会被分开打包
    const Login = lazy(()=>import('@/pages/Login'))
    
    // 2.通过<Suspense>指定在加载得到路由打包文件前显示一个自定义loading界面
    <Suspense fallback={<h1>loading.....</h1>}>
       <Switch>
           <Route path="/xxx" component={Xxxx}/>
           <Redirect to="/login"/>
       </Switch>
    </Suspense>

    注:从网络资料中罗移过来,暂未验证和实验。

  2. 错误边界

    每一个成熟的应用程序,都必然会有比较完善的异常应对方案。错误边界(Error Boundary)即是 React 提供的异常应对方案,使用错误边界,可以捕获后代组件的错误,并执行我们预期的错误处理方案。

    相信您在此前的实践中,经常会因为某一个子组件的错误而导致整个应用无法渲染。这在实际环境中产生的影响会相当大,其原因在于:自 React 16 起,任何未被错误边界捕获的错误将会导致整个 React 组件树被卸载,使用错误边界,可以有效地帮助您规避这个问题。

    官方示例:https://codepen.io/gaearon/pen/wqvxGa

    官方文档:错误边界 - React

    注意,错误边界也有其局限性,即只能捕获后代组件生命周期中产生的错误,对于当前组件,以及其他组件在合成事件、定时器中产生的错误则无能为力。

项目实战

学习了前面诸多章节的知识点,不知道大人们的感受如何,接下来就到了本 NPC 的工作尾声了,所谓的“项目实战”其实还处于 will have done 状态,这将是本 NPC 的个人进修课程,其目的是对已掌握知识点进行巩固,以及将这些知识点与实际项目进行衔接。以下仅仅是本 NPC 的个人计划,各位大人可以参考,可以笑笑,也可以自行离场。

本文附属项目实战计划细则:

  1. 选择对公司内 Vue 开发的项目使用 React 进行重新实现,或者对一些开源框架进行 React 前端实现(例如做一个 RuoYi-React 之类的),二选一。

  2. 项目的编写和设计需要尽量规范化,并且必须保持代码规范。

  3. UI 可以优先考虑 antd(antd 对管理系统类应该更为实用)。

  4. 不一定要求很完善,但一定要看起来像那么回事,尽可能广泛地涵盖本文相关知识点。

  5. 后端接口没必要亲自再写,使用 Mock 就好,或者使用其他项目的接口。

  6. 项目大概成型后,补充在文末,并记录时间以便衡量成本(当前时间:2022-05-26)。

  7. 如果有余力,以及条件允许,则部署该项目,并提供演示域名,其意义在于公开成果,有助于接受旁人的学习监督。

  8. 如在实战过程中,发现本文知识点有遗漏、含糊不清,甚至偏差甚大的地方,务必即时补充和更正。

  9. 考虑到还有工作、其他学习计划、其他个人项目,以及一些生活琐事,本项目预期时间为三个月(感觉有些太短了,六个月又太长了……,算了,就先这样计划吧)。

接下来大概又有的忙了,希望我千万不要打自己的脸啊。

参考

版权声明

本文链接:https://www.chinmoku.cc/web/react-tutorial/

本博客中的所有内容,包括但不限于文字、图片、音频、视频、图表和其他可视化材料,均受版权法保护。未经本博客所有者书面授权许可,禁止在任何媒体、网站、社交平台或其他渠道上复制、传播、修改、发布、展示或以任何其他方式使用此博客中的任何内容。

Press ESC to close