前端学习笔记006:React.js

(前面学了那么多,终于到 React 了~)

React.js 大家应该听说过,是用于构建前端网页的主流框架之一。本文会把 React.js 开发中会用到的东西(包括 React 核心,React 脚手架,React AJAX,React UI 组件库,React Router)都讲一遍,废话不多说,直接开始教程~

目录

1. 开始之前

1.1 React 简介

1.2 React 预备

1.3 JSX 语法规范

2. React 核心

2.1 Hello World

2.2 React 组件与函数式组件

2.3 类式组件入门

2.4 类式组件 props

2.5 类式组件 state

2.6 类式组件 refs 

2.7 类式组件生命周期

2.8 类式组件生命周期练习

2.9 React 标签中的 key

加更 1:函数式组件 Hooks 与 useState

加更 2:函数式组件 props

加更 3:函数式组件 useRef

加更 4:函数式组件 useEffect

加更 5:函数式组件及 Hooks 总结

3. React 脚手架

3.1 React 脚手架搭建

3.2 React 脚手架运行

4. 练习 1:井字棋

4.1 练习简介

4.2 组件拆分

4.3 实现静态页面

4.4 实现动态页面:落子功能

4.5 实现动态页面:胜负判定功能

4.6 实现动态页面:重置棋盘功能

4.7 小结

5. React AJAX

5.1 axios 复习

5.2 React 中的 AJAX 请求

6. React UI 组件库

6.1 UI 组件库简介

6.2 Ant Design 基本使用

6.3 Ant Design 配置主题

6.4 Ant Design 导航组件专题

6.5 Ant Design 数据录入组件专题

6.6 iconFont 图标库使用

7. React Router

7.1 React Router 简介

7.2 BrowserRouter 与 useRoutes

7.4 Navigate 组件与 useNavigate

7.5 多级路由

7.6 路由传参

7.7 React Router 中的其它 Hooks

8. 练习 2:TransBox 翻译盒

8.1 练习简介

8.2 组件拆分及准备

8.3 网页布局

8.4 路由功能配置

8.5 Main 组件:实现静态页面

8.6 Main 组件:实现动态页面:翻译 API

8.7 Main 组件:实现动态页面:翻译功能实现

8.8 About 组件

8.9 应用优化:样式部分

8.10 应用优化:功能部分

8.11 小结

9. 后记

9.1 把你的项目部署在 GitHub Pages

9.2 附言


1. 开始之前

1.1 React 简介

前面我们也说了,React 是用于构建前端网页的主流框架之一。但是,大家有没有想过,React 为什么会成为主流框架之一?我们为什么要去使用 React 去做前端网页的开发?最重要的原因就在于 React 使用了虚拟 DOM 技术,能大大加快网页的运行速度。

那什么是虚拟 DOM 呢?那就要先了解真实 DOM。如果我们使用原生 JS 去开发网页,需要用到 document 身上的 getElementByxxxxx 方法吧?这些都是要与真实 DOM 进行交互的。所以简单来说真实 DOM 就是网页身上真实的看得见摸得着的可以使用 document.getElementByxxxxx 获取到的 DOM。

但是我们一旦使用真实 DOM 的次数多了,就会引发严重的效率问题。这里举个例子吧。比如我们想要使用 JS 去展现 GitHub 的一些用户的数据。使用原生 JS,我们是这么写的:



    
        
        使用真实 DOM 进行页面展示
    
    
        

    这样的代码看起来很正常,渲染起来也很正常:

    前端学习笔记006:React.js_第1张图片

    那有的用户就不满了:Vue 哪里去了?给我把 Vue 加上!那前端页面接到用户请求说要加上 Vue 这个 GitHub 用户,就只能改 data,并且重新渲染一遍 DOM。下图描述了操作真实 DOM 时需要进行的步骤:

    6cfdc71f047a43e79f30f3ae06ca0844.png

    那这不是也很正常吗?同学,你仔细想想,我们只添加了一个新用户 Vue,但是却需要把所有的数据全部都重新渲染一遍。大家知道,渲染真实 DOM 的开销是巨大的。为啥呢?你看看 DOM 里面有多少属性就可以理解了,随便找一个 input 标签把:

    前端学习笔记006:React.js_第2张图片

    前端学习笔记006:React.js_第3张图片

    前端学习笔记006:React.js_第4张图片 前端学习笔记006:React.js_第5张图片 

    前端学习笔记006:React.js_第6张图片

    前端学习笔记006:React.js_第7张图片

    前端学习笔记006:React.js_第8张图片

    前端学习笔记006:React.js_第9张图片

    前端学习笔记006:React.js_第10张图片

    上述所有的这些属性都是我们渲染 DOM 时需要用到的。可能我们没有设置一些不常用的属性,但是浏览器照样要处理。处理什么?处理默认值!一个 DOM 开销就这么大,那数十个 DOM 呢?数百个 DOM 呢?结果可想而知。

    所以,我们为什么不可以让前面两个 react 与 angular 不变,只新增 Vue 的东西呢?于是 React 的虚拟 DOM 就派上用场了。简单来说呢,React 帮你把网页里所有的 DOM 保存为一个虚拟 DOM 表。当你更改了网页中一个元素的数据,React 就会帮你生成一份新的虚拟 DOM 表,然后用新的去比较旧的,发现一样就不重新渲染,发现不一样的就帮你重新渲染。这样的机制大大提升了网页的速度。下图简述了使用 React 更新 DOM 时经过的步骤:

    前端学习笔记006:React.js_第11张图片

    当然,React 能做的远远不止虚拟 DOM 这个部分。其中还有一个很重要的部分,就是组件化编码。对于组件化呢,我们可以先简单了解一下。比如说下面这个导航栏:

    前端学习笔记006:React.js_第12张图片

     想必大家也注意到了我圈出来的那个红框。那是一个导航栏。但是这个网页中出现了几次这种导航栏?是不是下面也有很多一样的?于是,我们就可以把这个导航栏封装成一个组件。这个组件里有 HTML,CSS,JS,甚至还有对应的图片等资源。比如我们把这个组件取名叫做 Item;

    然后在需要用到它的时候,直接渲染这个组件(可能还需要传递 logo 以及文字等信息),而且可以重复多次地渲染,这样就可以大大提高我们的效率。如果还不清晰的可以看看下面的代码:

    
    
    
    

    除此之外,你还可以把上面的导航栏给封装成一个组件嵌入网页中,一个网页中有多个组件,这样既提升了页面布局的条理,也让我们写代码思路更加清晰~

    组件化还有一个好处,就是可以实现 UI 组件库。所谓 UI 组件库呢,就是把一些样式非常精美的 HTML 标签(比如按钮)封装成一个组件,CSS 已经内置在组件里边,然后需要用到它的时候已一导入一使用就可以了。比如下面的 Ant Design:

    前端学习笔记006:React.js_第13张图片

    React 的最后一个优势呢,就在于跨平台。别以为 React 只能用于编写网页,iOS 与 Android 等移动端应用它也样样精通。React 有一个扩展模块 React Native,它可以让 React 应用于移动平台,也就是说你不需要再学什么 Objective-C Swift Kotlin,一个 React 通吃天下。(当然我们这里不讲解 React Native,毕竟我们不弄移动端嘛~)

    1.2 React 预备

    在学习 React 之前,你至少需要掌握以下的预备知识才可以学,这些知识都是 React 开发中会大量用到的。我之前出过这些知识点的文章,感兴趣的小伙伴可以点进去补充补充~

    1. HTML、CSS、JavaScript 基本知识

    前端三件套必须要熟,要不然根本没法学,当然我确信能点进来本文的同学三件套都不会太差~

    001: HTML5

    002: CSS3

    003: JS6

    2. 面向对象、模块化、ES6 新特性

    React 开发中会大量使用上述的三个知识点,不学肯定 BBQ~

    003-1: JS 补充

    3. Node.js 与 NPM

    React 脚手架开发时会大量运用到 Node.js 与 NPM~

    004: Node.js + NPM

    4. axios

    React AJAX 需要用到~

    005: 数据传输 + AJAX + axios

    除此之外,进行 React 开发需要用到很多软件与插件。请确保你安装了如下软件:

    1. 一款熟悉的前端 IDE

    这里推荐 VS Code,不说别的,写代码真的超级舒适~

    2. 亿些 IDE 扩展

    React 开发需要的 IDE 扩展还是比较多的,请保证安装以下扩展。如果你的编辑器没有下列扩展还是得换成 VS Code~ 

    1:Live Server

    前端学习笔记006:React.js_第14张图片

    2:ES7+ React/Redux/React-Native snippets

    前端学习笔记006:React.js_第15张图片

    除此之外,还有一些扩展能让开发体验更佳,建议安装,但是非必选。

    1. Path Intellisense,能让代码中文件路径的输入体验更佳。

    前端学习笔记006:React.js_第16张图片

    2. Prettier,让你的代码更整洁~ 

    前端学习笔记006:React.js_第17张图片

    3. Node.js 以及 NPM 

    可以在刚刚我给出的 Node.js 及 NPM 的学习文章里看见安装方法~

    下载链接​​​​​​

    4. 浏览器扩展:React Developer Tools

    这是一个调试 React 应用的小浏览器扩展。如果你正在使用 Edge,请访问这个链接然后点安装就行~(我这里由于已经安装过了所以显示“删除”)
    6796b468b9c9436c9e100ee9c131e470.png

     如果你使用的是 Chrome 或其他基于 Chromium 的浏览器,请打开这个链接,然后点击“安装到浏览器”;

    前端学习笔记006:React.js_第18张图片

    点击继续; 

    前端学习笔记006:React.js_第19张图片

     然后打开 chrome://extensions,把刚刚下载的那个文件,拖进去,点击安装;

    前端学习笔记006:React.js_第20张图片

     然后就安装成功了~

    前端学习笔记006:React.js_第21张图片

    如果你使用的是 Firefox,请打开这个链接,后点安装到浏览器就可以(我这里由于使用 Edge 打开所以显示要下载 Firefox~)

    前端学习笔记006:React.js_第22张图片

    1.3 JSX 语法规范

    babel 大家都很熟悉了,它被用于把 ES6 代码转换为 ES5 代码。但它不仅能做这个,它还可以支持 JSX 语法。那 JSX 是什么呢?简单来说,JSX 就是在 JS 里面嵌入 HTML(准确来说是 XML),就像下面这样:

    dom.innerHTML = 
    This is JSX

    这样的书写方式就是 JSX。在 React 中每时每刻都需要用到它。由于 JSX 必须和 React 配合使用,所以这里也简单把 React 入个门~

    
    
        
            
            JSX 演示
        
        
            

    然后在同一个目录下创建 script.js 代码如下,注释一定一定一定要看!

    // React 第一步,创建根节点
    // createRoot 方法的参数就是我们通过 document.getElementById 获取到的根节点
    // 它的返回值就可以供我们进行 React 操作了
    let root = ReactDOM.createRoot(document.getElementById("root"));
    
    // 随便定义几个变量
    let a = 3;
    let b = 4;
    // 箭头函数的函数体可以直接是一个值,它是箭头函数的返回值
    // 不知道大家还记不记得
    let student = () => ["小明","小红","小强","甲车","乙车","甲工人","乙工人"] // 逐渐离题...
    let callback = () => alert("Hello~!")
    
    // React 第二步,渲染 JSX
    // 使用 root 身上的 render 方法,参数就是 JSX 了
    root.render(
    ( // JSX 的第一个语法点,尽量把嵌入的 XML 代码用括号包裹住
        
    {/* 第二个语法点,最外层必须只能出现一个标签 */} {/* 我们一般使用 div 包裹一整个 XML 代码 */} {/* 或许大家也注意到了 JSX 的注释是这样写的 */}

    在里面可以随心所欲地写 HTML

    {/* 这里穿插一个语法点,JSX 里的标签必须要闭合,所有的都要,即使像 br 标签这些也要自闭合 */} a: {a}
    {/* 如果想在 JSX 里面显示 JS 表达式的值,请使用花括号里面包一个 JS 表达式 */} {/* 一个变量,一个数字,一个数组,一个对象,甚至一个函数的返回值都可以作为表达式 */} {/* 例如 func() 这个表达式的值就是这个函数的返回值 */} {/* 但是如 if 判断就没有返回值 */} {/* 要想在里面写 if 判断必须采用三元运算符的形式 */} {/* 或者直接在外部定义一个函数然后再内部调用它 */} b: {b}
    {/* 下面来一个复杂的不知道大家看没看懂 */} student:
      {student().map((element)=>{ return (
    • {element}
    • ) // 在 JSX 内嵌的 JS 里面如果还要嵌一层 JSX // JSX 里面获取 JS 表达式值还是要用花括号 })}
    {/* 第三个语法点来了,class 属性要写成 className */} {/* 因为 html 里原生的那个 class 属性与 JS 里的 class 关键字重名了 */} {/* 还有一个,就是 style 属性不能写成字符串形式,要写成双花括号形式,比如下面 */} {/* 里面的例如 background-color 这种属性需要转换成小驼峰形式例如 backgroundColor */} {/* 最后一个,就是 HTML 原生的 onclick 这些事件属性全部要写成小驼峰形式 */} {/* 比如 onclick 要写成 onClick,onmouseup 要写成 onMouseUp */} {/* 还有这些属性传函数时直接传 JS 里面真实的函数,即花括号里包函数名,不要写成字符串形式 */}
    ))

    保存 script.js。右键 index.html 选择 Open with Live Server,以后所说的“打开某个 html 文件”全部都是用的这个方法。

    前端学习笔记006:React.js_第23张图片

    你应该可以在浏览器中看见你刚刚所写的 JSX 代码~ 点击 Input 框还真的会有弹窗效果~

    前端学习笔记006:React.js_第24张图片

    你可能会看到控制台报了一个警告,这个我们先不用管它,后面讲 Diffing 算法的时候会专门挑出来讲解~

    2. React 核心

    2.1 Hello World

    前面讲 JSX 的时候有稍微提了一下 React 的渲染流程,这里先用一个 Hello World 完整的学一下~

    首先第一步,准备一个模板 HTML 文件,名字叫做 index.html。在它里面要做的,就是创建一个模板 div 标签(id 一般为 root),然后引入 React、React DOM 与 Babel,最后引入我们自己的脚本文件,一般命名 script.js。index.html 代码如下:

    
    
        
            
            Hello World
        
        
            
            
            

    可能有同学就要问了:你引入一个 React,为什么还要引入一个 React DOM 啊?React DOM 是 React 的一个扩展库,通常用于操作网页中的 DOM。基本上进行 React 网页开发都需要用到这个 React DOM。什么虚拟 DOM 这些东西也都是在 React DOM 身上的。

    然后开始大菜~ 在同目录下创建 script.js。写入以下内容:

    // 第一步,创建根节点
    // 使用 ReactDOM 身上的 createRoot 方法
    // 参数为我们需要往里面写东西的 div 的 DOM 对象
    // 返回值就是 ReactDOM 创建好的根节点
    // 看不懂文字看代码
    let root = ReactDOM.createRoot(document.getElementById("root"))
    
    // 第二步,渲染 JSX
    // 使用 root 身上的 render 方法
    // 参数为刚刚教过的 JSX
    // 就比如下文在根节点中渲染了一个 Div 里面内容是 Hello World
    // 一个 React 应用只能调用一次 root.render
    root.render(
    Hello World!
    )

    简简单单两行代码,保存,然后使用 Live Server 打开 index.html(以后这个打开方式就是默认了)

    效果完美:

    c5c30eb9b70049a49a59fb1a4814c8fa.png

    你可能会在控制台发现一个报错,说找不到 favicon.ico,不用理会,这是 Babel 在加载时自动找的 favicon.ico。刷新一下网页,报错立马消失~

    前端学习笔记006:React.js_第25张图片

    2.2 React 组件与函数式组件

    前面我们在说 React 的优点的时候不是说了 React 是通过组件化编码的吗?那组件在 React 里面怎么定义的呢?在这之前我们要先了解 React 组件到底是啥。React 中组件最核心的功能,其实就是把一些 JSX 封装成组件,在需要的时候渲染那个组件,React 就可以帮我们把组件里的那个 JSX 渲染至页面上。渲染组件的方式其实与渲染 HTML 标签差不多,比如你想渲染 Hello 组件,就使用 即可。

    组件的功能还有很多:比如可以通过 state 来保存组件状态和更新状态,相当于组件里面可以直接调用与更改的变量;还有 props,可以让外部组件通过 props 来给组件传递参数。等等等等,总而言之 React 组件的功能比一般的 JSX 那可是强上太多了。

    那组件那么好,怎么定义组件呢?React 里定义组件的方式有两种:函数式组件与类式组件。函数式组件顾名思义就是定义一个函数,它的返回值是我们需要的 JSX。函数式组件通常用于定义小组件,即规模比较小的组件,因为它的功能比较单一(想说 Hook 的同学请绕道~),就是纯粹的把 JSX 给封装起来。那类式组件呢,其实就是使用一个类来定义一个组件,功能相对比较强大,所以通常被用于定义大组件,即规模大的组件。

    由于函数式组件比较简单,所以这里先讲函数式组件。上文也说了,函数式组件就是一个函数,返回一堆 JSX,那它岂不是可以这样写:

    function funcComponent(){
        return (
            

    这是一个函数式组件

    ) }

    这样写已经对了一半了。但是由于 React 为了把组件与原生的 HTML 标签区隔开来,所以 HTML 标签首字母统一用小写,组件首字母统一用大写,所以组件名肯定也得改成首字母大写啦~

    function FuncComponent(){
        return (
            

    这是一个函数式组件

    ) }

    那 render 里面肯定也得渲染该组件,完整代码如下:

    function FuncComponent(){
        return (
            

    这是一个函数式组件

    ) } let root = ReactDOM.createRoot(document.getElementById("root")) root.render()

    效果嘎嘎好:

    前端学习笔记006:React.js_第26张图片

    如果愿意的话,你还可以渲染两个:

    function FuncComponent(){
        return (
            

    这是一个函数式组件

    ) } let root = ReactDOM.createRoot(document.getElementById("root")) root.render(
    )

    前端学习笔记006:React.js_第27张图片

    怎么样,Get 到组件的实用了吗~ 下面的类式组件还能让组件的功能更上一层楼~

    (这里科普一下 Hook 是啥:Hook 简单来说就是让函数式组件拥有类式组件的强大功能,是一个比较年轻的功能,发布至今也才两年半。)

    穿越科普:2022-12-25 更新:现在类式组件的江山已经快被函数式组件打下来了,因为 Hooks~  所以大家看完第二章类式组件之后的函数式组件一定要看我加更的函数式组件!!!但是由于写作时间原因,第二章到第六章用的都是类式组件。关于函数式组件的内容只有在第七章和练习二补救一下了(欲哭无泪.jpg)

    2.3 类式组件入门

    上文也说了,类式组件是一种更强大的组件定义方式,它使用类来定义。类式组件和普通的类其实没有什么差别,就是继承了 React.Component 这个类而已~ 而类里面怎么返回需要的 JSX 呢?在类里面定义一个函数 render 然后在函数里返回需要的 JSX~

    那上面的函数组件不是可以通过类来定义了?没错,可以像如下这样的方式定义:

    class ClassComponent extends React.Component{ // 继承 React.Component
        render(){ // 在这个函数里面返回需要的 JSX
                  // React 会帮你调一次这个函数
            return (
                

    这是一个类式组件

    ) } } // 然后渲染上去 let root = ReactDOM.createRoot(document.getElementById("root")) root.render()

    效果满分:

    前端学习笔记006:React.js_第28张图片

    你也可以渲染很多个,每一个渲染出来的组件都是一个独立的对象;

    class ClassComponent extends React.Component{ // 继承 React.Component
        render(){ // 在这个函数里面返回需要的 JSX
                  // React 会帮你调一次这个函数
            return (
                

    这是一个类式组件

    ) } } // 然后渲染上去 let root = ReactDOM.createRoot(document.getElementById("root")) root.render(
    )

    前端学习笔记006:React.js_第29张图片

    2.4 类式组件 props

    接下来我们开始来一些类式组件的高级玩法。类式组件最核心的三个玩法就是 state props 与 refs。我们先从最简单的 props 开始开刀~

    假如我给你提一个需求,就是上面那四个类式组件,可不可以加上一个编号,比如“这是类式组件1”之类的。但是组件它怎么知道自己是第几个类式组件啊?所以我们可以采取这样一种方法:在渲染 ClassComponent 时给每一个组件传递一个值,来让组件明白它是第几个组件然后再让组件渲染。

    那怎么传递呢?就是使用 props。你可以在渲染组件时给组件加上一个属性,名字随便取,比如叫 componentIndex,就像下面这样:

    
    

    这样的属性就叫做 props。那组件怎么接到传递的 props 呢?其实很简单,传的 props 就在组件对象身上的 props 里面。比如想拿到传递的 componentIndex 属性,可以使用 this.props.componentIndex。那 ClassComponent 可以改成如下这样:

    class ClassComponent extends React.Component{ 
        render(){ 
            return (
                

    这是类式组件{this.props.componentIndex}

    {/* JSX 里嵌套 JS 的模板字符串 */}
    ) } }

    效果完美:

    前端学习笔记006:React.js_第30张图片

    这里还要说个 props 的简写方式。在此之前做一些准备工作,ClassComponent 别整那么多,一个就可以~

    然后,比如说我们拿到了一个对象,对象里面有很多属性,id name number 等等。那如果我们要把这些属性全部传进 props 里,该怎么传?一般人应该会这样传:

    let root = ReactDOM.createRoot(document.getElementById("root"))
    let StuInfo = {
        id: 3,
        name: "小明明",
        number: 1000
    }
    root.render(
        
    )

    这样传是肯定没错的。但是 Babel 为我们提供了一种简写方式,如下,效果与上面的代码一模一样(请注意我在 render 里面加入了打印 props 的环节)

    class ClassComponent extends React.Component{ 
        render(){ 
            console.log(this.props);
            return (
                
    1
    ) } } let root = ReactDOM.createRoot(document.getElementById("root")) let StuInfo = { id: 3, name: "小明明", number: 1000 } root.render(
    {/* 简写方式:{...对象名} */}
    )

    效果杠杠滴~

    前端学习笔记006:React.js_第31张图片

    借着这个机会顺便简单讲一下浏览器扩展 React Developer Tools 的使用方法。大家应该都安装好了吧?首先第一个使用方法,当该网页是使用 React 编写时,React Developer Tools 的 React 小 Logo 会亮灯。当你的网页是使用开发版 React 运行时,你会看见红色的灯,就像我们刚刚的那个示例:

    前端学习笔记006:React.js_第32张图片

    当你正在使用生产版 React 运行时,你会看见蓝色的灯:

    dd9e702c8185427da0c75db92339cb7a.png

    当你使用不受支持的 React 版本运行时,你会看见黑色的灯:

    3d59cc731040421ab1cfdd24834ecde2.png

    但不管亮着的是啥灯,都说明这个网页是使用 React 编写的~

    当然 React Developer Tools 可不仅仅只是说明网页是不是使用 React 编写的。它还有一个地方有作用,那就是在浏览器的开发者工具(Ctrl + Shift + I)中增加了两个选项:Components 与 Profiler,其中最有用的是 Components。通过 Components,你可以看见这个网页所使用的所有组件。点击某一个组件还能看见这个组件的 props 与 state(后面会讲,这里由于没设置所有没有),还有它的渲染方法 createRoot() 与 React 版本 18.2.0~

    前端学习笔记006:React.js_第33张图片

    2.5 类式组件 state

    类式组件最难的一部分来了,它就是 state。state,就相当于给组件内部使用的变量,可以供组件增删改查的那种。state 是一个对象,存在于类式组件之中,即 this.state。其实 state 本身并不难,但是 state 的使用需要注意的细节很多,相对还是比较难的~

    这里还是照例提出一个需求,当然在此之前我们需要先将 props 的学习痕迹清除,把 script.js 改成下面这样:

    class ClassComponent extends React.Component{ 
        render(){ 
            return (
                

    这是类式组件

    ) } } let root = ReactDOM.createRoot(document.getElementById("root")) root.render()

    然后开始讲需求。假如我们想把上面 h1 标题的内容改成下面 input 框输入的内容,并且实时改变。就像下面这样:

    fe6f69147ced45c7a3f79ea075b2c4a4.png

    相信看到需求大家思路也清晰了把,其实就是把 input 框绑定一个 onChange 事件每当 value 更改的时候调用,然后把 value 取出来放进 this.state 中,最后重新渲染页面。那不是可以这样写:

    class ClassComponent extends React.Component{ 
        // state 是类中的全局变量
        // 所以在这里定义
        // state 值是一个对象
        state = {value: "类式组件"}
    
        // onchange 的回调
        onchange(event) {
            // event.target 拿到调用这个函数的 DOM 节点
            // 所以 event.target.value 就是拿到 value 值了
            // 不知道大家还记不记得
            this.state = {value: event.target.value}
        }
    
        render(){ 
            return (
                

    这是{this.state.value}

    {/* 绑定事件时别忘记了小驼峰,不要写成 onchange 了 */}
    ) } } let root = ReactDOM.createRoot(document.getElementById("root")) root.render(
    )

    这样写写出了大概,但也只有大概,因为还有两个重要的细节没讲。我们可以尝试打开 index.html,随便输入一些内容,你应该会看见控制台报了个错:

    前端学习笔记006:React.js_第34张图片

    这个错的大概意思是:在 onchange 函数里面我们不能给 undefined 设置属性。在 onchange 里面我们给谁设置属性了?不就是 state 吗?那也就是说,state 是 undefined?事实就是这样的。因为我们没有去亲自调用这个函数,它是被作为回调函数来调用的,是 React 调的不是亲自调的。而在回调函数里面获取 this,那就只能是 undefined 了。具体请看我之前写的一篇博客的 2.3 部分,this 的指向。

    那怎么解决这个问题呢?答案就是使用箭头函数来定义回调函数。箭头函数可以避免 this 指向 undefined。具体原理我也不几道,去问 W3C~ 一般来说,类里面除了 render 函数与后面要介绍的生命周期函数,其他函数都使用箭头函数来定义。因为它们都是作为回调函数来执行的。那我们就把上面这个改成箭头函数:

    class ClassComponent extends React.Component{ 
        // state 是类中的全局变量
        // 所以在这里定义
        // state 值是一个对象
        // 这边默认给它的值为"类式组件"
        state = {value: "类式组件"}
    
        // onchange 的回调
        // 回调函数一定要写成箭头函数形式
        onchange = event => {
            // event.target 拿到调用这个函数的 DOM 节点
            // 所以 event.target.value 就是拿到 value 值了
            // 不知道大家还记不记得
            this.state = {value: event.target.value}
        }
    
        render(){ 
            return (
                

    这是{this.state.value}

    {/* 绑定事件时别忘记了小驼峰,不要写成 onchange 了 */}
    ) } }

    重新打开 index.html,你会发现也可以输入内容了,错也不报了,但是就是不显示内容。那我们刚刚不会改了个寂寞把?我们到底有没有改 state 中的内容呢?事实证明,是有的,不信你在 onchange 函数里输出一下 this.state:

    前端学习笔记006:React.js_第35张图片

    那为啥不显示出来呢?我们好好回忆一下,React 是怎么把你的组件加载到网页上的?是不是先调了一次 render 然后把它的返回值放到网页上的?然后,你更改了 state,React 也没说你一更改 state 就帮你重新调一次 render 啊!所以我们的 state 改是改了,但是没有渲染上去也没用。

    那我们再调一次 render 不就行了?但是这里有一个问题,那就是 render 不能自己调,只能 React 调。因为是 React 帮你把 render 的返回值更新至网页的呀!因此 React 帮我们封装了一个方法:this.setState,里面传你需要更改的 state 值,然后 React 就帮你改 state,顺便更新一下组件。更改后的代码如下所示:

    class ClassComponent extends React.Component{ 
        // state 是类中的全局变量
        // 所以在这里定义
        // state 值是一个对象
        state = {value: "类式组件"}
    
        // onchange 的回调
        onchange = event => {
            // event.target 拿到调用这个函数的 DOM 节点
            // 所以 event.target.value 就是拿到 value 值了
            // 不知道大家还记不记得
            this.setState({value: event.target.value})
        }
    
        render(){ 
            return (
                

    这是{this.state.value}

    {/* 绑定事件时别忘记了小驼峰,不要写成 onchange 了 */}
    ) } }

    打开 html,你应该就可以看见我们需要的效果了:

    前端学习笔记006:React.js_第36张图片

    对于 this.setState 有一个备注:this.setState 它是选择性地更新 state 的。 怎么理解这个“选择性”呢?比如 state 原本是 {a:1, b:2},然后如果我们 this.setState({a: 3}),React 只会帮你更新 a 的内容,b 还是原样,没有被删除。感兴趣的童鞋可以去实验一下~

    这里还有一个细节上的小问题,就是我们什么都不输入时,上面的显示框也什么也不显示,我们想让 input 框在什么也没有输入时,上面的标题可以改成“这是类式组件”,那怎么改呢?其实很简单,如下:

    class ClassComponent extends React.Component{ 
        // state 是类中的全局变量
        // 所以在这里定义
        // state 值是一个对象
        state = {value: "类式组件"}
    
        // onchange 的回调
        onchange = event => {
            this.setState({value: event.target.value})
        }
    
        render(){ 
            return (
                
    {/* 对三元运算符进行的一个小复习 */} {/* 如果 this.state.value 为空(会转化为 false)那么显示类式组件 */}

    这是{this.state.value ? this.state.value : "类式组件"}

    {/* 绑定事件时别忘记了小驼峰,不要写成 onchange 了 */}
    ) } } let root = ReactDOM.createRoot(document.getElementById("root")) root.render(
    )

    尝试一下,当我们输入一些内容再全部删除时,你会发现标题显示“这是类式组件”而不是什么都不显示。实际开发中可以通过这种方式增强用户体验~ 

    前端学习笔记006:React.js_第37张图片

    setState 是一个神奇的东西,上面所述的 setState 仅仅只是它的冰山一角。一般来说,setState 有两种形式:

    1. setState(stateChange, callback),其中 callback 为可选。stateChange 就是我们刚刚一直用的状态更改对象。那 callback 是啥?其实它是一个回调,一个在 state 更改之后执行的回调函数。那为啥会有这个回调函数存在呢?我们写成下面这种形式它不香吗:

    this.setState({a: 3})
    // 后面执行回调的代码
    

    但如果写成这样,看似 setState 和回调代码是同步进行的,但是 React 为了优化效率,把这两个操作同时进行了。所以如果你在下面的回调代码当中获取当前的 state,你获取到的是更新之前的。

    而写在 callback 里面就可以获取到更改后的 state 了。callback 就是一个普通的函数,没有参数~

    2. setState(updater, callback),其中 callback 为可选。callback 刚刚已经讲过了。那 updater 又是个啥呢?updater 它是一个用于更新 state 的函数,它可以接到之前的 state 和 props(或者只接 state),然后通过之前的 state 和 props 得出现在的 state 应该是啥,然后返回一个对象(即上文的 stateChange)

    // 箭头函数极端简写方式: (state, props) => ({a: state.a + 1})
    // 后面的对象包裹括号代表直接返回一个对象
    // 不能省略这个括号,要不然会和函数体的括号发生歧义
    this.setState((state, props) => ({a: state.a + 1})
    // 如果你不需要用到 props,那也可以这样写:
    // this.setState( state => ({a: state.a + 1})

    那这两种方法我们该怎么用呢?这里建议:当新 state 与旧 state 相关联时(比如新 state 是旧 state 的 2 倍),使用第二种,反之使用第一种。当然这不是硬性规定,一切以开发需要为主~

    2.6 类式组件 refs 

    这一小节我们要讲的是类式组件的 refs。refs 也是 React 官方给我们提供的功能。简单来说,我们可以将一个 DOM 节点以 ref 的形式保存在组件对象中,在需要的时候拿到这个 ref,就相当于拿到了这个 DOM 节点。它的作用其实和 HTML 里的 id 差不多,但是由于 ref 是操作虚拟 DOM 的,id 是操作真实 DOM 的,所以 ref 的效率会比 id 快很多。同学们想必也知道了 refs 是个啥,其实就是 ref + s 变成复数,代表有多个 ref。

    这里还是提出一个需求。上面给一个 h1,下面给一个 input 框,input 的右边给一个按钮,当按钮按下时,上面的 h1 就显示 input 框里的内容。就像下面这样:

    前端学习笔记006:React.js_第38张图片

     我们还是照样讲一下思路:首先给按钮绑定一个 onClick 事件,然后把 state 里面的 value 值更改成 input 框里的 value 值,然后 h1 里的内容就自然改成 state.value 了~

    但关键是怎么拿到 input 框中的 value 值呢?那肯定需要用到我们上文所说的 ref 了。简单来说,我们的操作流程是:先在组件全局设置一个变量,变量是用来存放 ref 的。它的初始值为 React.createRef(),说明这是一个存放 ref 的变量。然后在 input 框里面加入一个属性 ref,值就是我们刚刚创建的这个变量。这样我们就把 input 框保存至 ref 里了。然后在需要用到它的时候获取 ref 变量就可以了。请注意 ref 这个变量并不是 Input 框的 DOM 对象,ref.current 才是 DOM 对象。综上所述(气氛突然沉重……),我们可以写出以下代码:

    class ClassComponent extends React.Component{ 
        // state 里面的 value 初始值是空串
        state = {value: ""}
    
        // 设置一个 inputNode 变量用于存放 ref
        inputNode = React.createRef()
    
        // 按钮按下时的回调
        onclick = () => {
            // this.inputNode.current.value 拿出 input 框的 value 值
            this.setState({value: this.inputNode.current.value})
        }
    
        render(){ 
            return (
                
    {/* 这里优化了一下用户体验 */} {/* 当 input 未输入时上面显示“未输入” */}

    input 框里的内容:{this.state.value ? this.state.value : "未输入"}

    {/* 把 input 框存进 ref 变量 */}
    ) } } let root = ReactDOM.createRoot(document.getElementById("root")) root.render(
    )

    试验一下,效果满分~

    前端学习笔记006:React.js_第39张图片

    关于 refs,这里有三条备注:

    1. 不可以在函数组件上使用 ref,即不能写出  这样类似的代码。因为函数组件没有对象体,具体参考官方文档。但函数组件里面的 JSX 可以使用~

    2. refs 有三种形式:字符串形式,回调形式与 createRef 形式。其中 createRef 形式是 refs 的最完美实现,也是 React 官方推荐的形式。所以我们只讲 createRef 形式。至于前面两种,字符串形式已被 React 弃用,因为一些效率问题;而回调形式有点麻烦,想了解的可以看看官方文档;

    3. 请勿过度使用 refs。因为 ref 会造成一定的性能开销。所以能不用尽量不用~

    2.7 类式组件生命周期

    这一小节要讲的是类式组件的生命周期,这也是类式组件里面最后一个难点了。类式组件的生命周期,你可以把它比作人的生命周期,简单来说就是生老并s这些东西。那人有生老并 s,组件有没有呢?其实是有的。

    组件的生命周期的核心其实就是一些生命周期函数,他们在组件的一些特定的时间,比如刚渲染到页面上时,执行一些代码,比如设置定时器什么的。下图列举了所有的生命周期函数。

    前端学习笔记006:React.js_第40张图片

      可以见到,这张图分了三条线列举了生命周期函数。下面我们分三条线来讲解该图:

    1. 挂载时,即挂载的时候执行的生命周期函数。

    constructor(props):即该类的构造函数。它可以接到参数,就是传递进来的 props。它在组件类初始化时使用。这个方法的第一行必须是 super(props),应该不用说什么意思吧,功底了;

    static getDerivedStateFromProps(props, state):在调 render 之前调用,通常没啥用,只有在这个时候才调用:state 的值在任何时候都取决于 props。一般 99.9% 的概率不会用到。具体看这里:链接,后面就不讲解了。

    render():这个都很熟悉了吧,渲染组件用的;

    componentDidMount():当组件第一次被渲染完成时调用的生命周期函数,通常用于进行一些初始化操作;

    2. 更新时,这个小点里面有三条支线,下面我们一条一条讲。

    先来走中间那个最熟悉的 setState(),顾名思义,这条线是调用 setState() 所需执行的生命周期函数。

    static getDerivedStateFromProps(props, state):还是那个没啥用的函数;

    shouldComponentUpdate(nextProps, nextState):这个函数是用于决定是否进行组件显示更新的,当返回值为 true 则继续进行组件更新,反之则不更新。nextProps 与 nextState 是即将更新的 props 与 state,可以通过这个来判断是否组件显示更新。如果这个函数返回值为 false,则该组件的显示不更新,但是 state 与 props 依旧在更新

    render():老熟人了~

    getSnapshotBeforeUpdate(prevProps, prevState):这个函数与上面的 static getDerivedStateFromProps(props, state) 一样都没有什么用,但至少比上面那个有用一点。它在 render 之后,渲染组件之前,是用于在渲染组件之前最后一次获取上一次的 props 与 state 的。比如你即将把 state 由 {a:3} 变为 {a:4},在这个方法里面你可以用 prevState 接到 {a:3};

    componentDidUpdate():当组件完成更新时执行的方法。与 componentDidMount 的意义其实是差不多的,不同点在于调用的时机。

    那剩下的两条线是怎么回事呢?下面来讲讲。

    第一条线名字叫 new props。这条线是父组件给你提供的 props 发生更新时调用的。那什么是父组件给你提供的 props 呢?这里举个例子。比如有一个 Father 组件和一个 Son 组件,Father 组件的 render 方法的返回值里面有 Son 组件。这里的 Father 组件就是 Son 组件的父组件了。

    这还不要紧,关键是 Father 组件,还给 Son 组件传递了 props,而且这个 props 还恰巧在 Father 组件的 state 中。这样在 Father 组件 setState 的时候就可以会重新调用一次 Father 的 render,由于给子组件传递的 props 发生了更新,所以子组件也要更新一次,子组件在更新的时候就要走 new props 这条线了。

    关于这条线里面的生命周期函数函数我们就不再讲解了。和上面 setState 一模一样。再回过头来看一下前面 shouldComponentUpdate 的参数里面为什么会有一个 nextProps,大家应该也理解了吧~

    那还有一条名叫 forceUpdate() 的线是怎么回事呢?难道像 setState 一样,组件自身还有一个方法叫 forceUpdate() ?没错,那它是干嘛的?我们可以看一下它的名字,force 是强制,update 是更新,合起来就是强制更新。它其实就是用于强制更新组件的。它不需要任何条件,也不需要任何 state,只要你调用了它,React 就帮你更新组件。

    这条线其实也没有什么非常不同的地方,与 setState 大体相似。只是你有没有注意到少了一个函数:shouldComponentUpdate。那是为啥呢?因为 forceUpdate 一没有更新 props,二也没有更新 state,那 shouldComponentUpdate 还判断啥?自然就省略了。

    3. 卸载时,顾名思义就是卸载组件的时候需要调用的生命周期函数,不多就一个。

    componentWillUnmount():在组件将要被卸载时执行,一般用于进行收尾操作,比如清除定时器等。

    但是,上面讲了那么多,什么是卸载组件呢?比如我们关闭网页,是不是在卸载一整个网页?或者我们也可以使用 ReactDOM.createRoot() 创建出来的 Root 对象身上的 unmount 方法来卸载(root.unmount)

    上面说的所有函数都是类式组件身上的方法,除了 render 方法必须实现外,其他都是可选的。这些生命周期函数不需要使用箭头函数来定义,使用普通函数就行,也可以正常使用 this。比如我们需要定义 componentDidMount 函数,可以像下面这样:

    class ClassComponent extends React.Component{
        componentDidMount(){
            console.log("Component Did Mount")
        }
    
        render(){
            return 
    Hello!
    } }

    2.8 类式组件生命周期练习

    其实上面说了那么多生命周期函数,真正常用的就三个:componentDidMount,render 与 componentWillUnmount。第一个是用于初始化的,第二个是用于渲染的,第三个是用于收尾的,分工非常明确。此外还有两个 componentDidUpdate 与 shouldComponentUpdate 也有一点用。但是光说不练总是不行的,所以这个小节我们浅浅写一个练习,把上面常用的生命周期函数练练手。

    在练习之前还是要先把 script.js 清空,消除前面的学习痕迹~

    这个要求具体比较长,具体如下:

    这个案例的主题是一个时钟,如下,记录着你打开网页的秒数,并且时钟不断更新;

    前端学习笔记006:React.js_第41张图片

    下面有两个 checkbox,当暂停 checkbox 被选中的的时候,暂停网页显示的更新(但是时钟其实还在运转,取消勾选之后显示真实秒数);

    前端学习笔记006:React.js_第42张图片

    当输出 checkbox 勾选的时候(且该时钟正在运行没有暂停),每过一秒在控制台输出打开的秒数;

    前端学习笔记006:React.js_第43张图片

    由于这个案例比较复杂,所以我们先把静态页面整出来。其实这个网页的静态页面非常简单,就是一个 h1 和两个 checkbox。如下:

    class ClassComponent extends React.Component{ 
        render(){
            return (
                

    您已打开该网页0秒

    暂停 输出
    ) } } let root = ReactDOM.createRoot(document.getElementById("root")) root.render()

    实现之后的页面如下所示:

    前端学习笔记006:React.js_第44张图片

    接下来我们要让 h1 上面的秒数动起来。我们可以设置一个 state 状态名为 sec,存储打开网页的秒数。那怎么让 sec 存储打开网页的秒数呢?我们可以在网页打开时设置一个定时器,每隔一秒将 state.sec 加上 1。这个操作可以在哪里进行呢?当然是 componentDidMount 啊~ 由于定时器需要在网页关之前也关掉,所以我们再来一个 componentWillUnmount 来收尾。最后把 h1 上面的秒数切换成 this.state.sec 即可。实现后的代码如下所示:

    class ClassComponent extends React.Component{ 
        // 设置 sec,初始值为 0
        state = {sec: 0}
    
        // 在网页第一次渲染时开启定时器
        componentDidMount(){
            // 每隔一秒加一次 sec,把返回值保存至 this.timer 供以后关闭定时器使用
            this.timer = setInterval(()=>{
                this.setState({sec: this.state.sec + 1})
            }, 1000);
        }
    
        // 在组件即将卸载时清除定时器
        componentWillUnmount(){
            clearInterval(this.timer)
        }
    
        render(){
            return (
                
    {/* 把秒数绑定到 this.state.sec 上 */}

    您已打开该网页{this.state.sec}秒

    暂停 输出
    ) } } let root = ReactDOM.createRoot(document.getElementById("root")) root.render()

    目前我们网页的主功能已经实现了,如下所示:

    前端学习笔记006:React.js_第45张图片

    接下来我们要做的就是如何实现下面“暂停”和“输出”这两个功能。“暂停”的思路其实很简单,就是增加一个 shouldComponentUpdate,当“暂停”这个 checkbox 选中的时候返回 false,反之返回 true。那怎么获取到“暂停”的选中情况呢?其实使用 refs 就可以实现~ 具体代码如下:

    class ClassComponent extends React.Component{ 
        // 设置 sec,初始值为 0
        state = {sec: 0}
        // 增加一个 ref 存放暂停的 checkbox
        isStop = React.createRef()
    
        // 在网页第一次渲染时开启定时器
        componentDidMount(){
            // 每隔一秒加一次 sec,把返回值保存至 this.timer 供以后关闭定时器使用
            this.timer = setInterval(()=>{
                this.setState({sec: this.state.sec + 1})
            }, 1000);
        }
    
        // 在组件即将卸载时清除定时器
        componentWillUnmount(){
            clearInterval(this.timer)
        }
    
        // 判断是否暂停
        shouldComponentUpdate(){
            // this.isStop.current 拿出 dom,checked 属性代表是否选中
            if (this.isStop.current.checked){
                return false
            } else {
                return true
            }
            // 上面的代码还可以简写为如下形式:
            // return this.isStop.current.checked ? false : true
        }
    
        render(){
            return (
                
    {/* 把秒数绑定到 this.state.sec 上 */}

    您已打开该网页{this.state.sec}秒

    {/* 绑定 ref */} 暂停 输出
    ) } } let root = ReactDOM.createRoot(document.getElementById("root")) root.render()

    暂停的功能就这么实现了~(当你取消暂停的时候你会发现秒数突然正常了,所以 shouldComponentUpdate 处理的只是显示并不是 State)

    前端学习笔记006:React.js_第46张图片

     那“输出”怎么实现呢?其实比暂停还更简单,就使用 componentDidUpdate 然后一顿判断一顿输出就行了,如下:

    class ClassComponent extends React.Component{ 
        // 设置 sec,初始值为 0
        state = {sec: 0}
        // 增加一个 ref 存放暂停的 checkbox
        isStop = React.createRef()
        // 再增加一个 ref 存放输出的 checkbox
        isOutput = React.createRef()
    
        // 在网页第一次渲染时开启定时器
        componentDidMount(){
            // 每隔一秒加一次 sec,把返回值保存至 this.timer 供以后关闭定时器使用
            this.timer = setInterval(()=>{
                this.setState({sec: this.state.sec + 1})
            }, 1000);
        }
    
        // 在组件即将卸载时清除定时器
        componentWillUnmount(){
            clearInterval(this.timer)
        }
    
        // 判断是否暂停
        shouldComponentUpdate(){
            // this.isStop.current 拿出 dom,checked 属性代表是否选中
            if (this.isStop.current.checked){
                return false
            } else {
                return true
            }
            // 上面的代码还可以简写为如下形式:
            // return this.isStop.current.checked ? false : true
        }
    
        // 判断是否输出
        componentDidUpdate(){
            // 除了判断输出是否勾选还要判断组件刷新显示是否停止
            // 要不然也输出不了
            if (this.isOutput.current.checked && !this.isStop.current.checked){
                console.log(`您已打开此网页${this.state.sec}秒`)
            }
        }
    
        render(){
            return (
                
    {/* 把秒数绑定到 this.state.sec 上 */}

    您已打开该网页{this.state.sec}秒

    {/* 绑定 ref */} 暂停 {/* 再绑定一个 ref */} 输出
    ) } } let root = ReactDOM.createRoot(document.getElementById("root")) root.render()

    效果嘎嘎好~

    前端学习笔记006:React.js_第47张图片

    2.9 React 标签中的 key

    这一小节我们主要需要研究的是一个很“鸡肋”的问题,但是以后开发中可能会用到,即 JSX 标签里面的 key 属性。我们一开始学习 JSX 的时候是不是看见过这样一个错误,说没有 key 属性:

    487387fb82e545deb619d670391463a4.png

    那 key 属性是干啥的?我们为什么在列表中一定要定义一个 key 属性(我们当时是因为遍历一个列表报的警告)?

    我们先聊一聊 Diffing 算法是个啥。这个算法其实讲完之后你们肯定不陌生,它就是 React 在实现虚拟 DOM 是所用的算法。当我们在刷新列表的时候,Diffing 算法监测到了 ul 里面的东西发生了改变,就刷新了一整个 ul。那为什么 Diffing 算法不看里面 li 有没有改变呢?因为你重新遍历一整个列表了,它当然以为你遍历的所有东西都是新的呀,难道它会去比较你里面的东西?当然不可能。因为这样太耗时了,不仅发挥不出虚拟 DOM 的速度,反而可能还拖慢速度。

    key 就是用来解决这个问题的。假如你给每一个 li 都加上了唯一的 key,React 就会看一下新旧 li 的 key 是否相等,如果相等就不刷新,如果不相等就刷新。所以 key 增加了网页显示的效率。

    那如何给 li 加上唯一的 key 呢?你还记不记得 map 里面的回调函数可以接到两个参数:element 与 index。那我们就可以把 key 作为 index,这也是最简单的方法,就像下面这样:

    class ClassComponent extends React.Component{ 
        render(){
            return (
                
      {["小明","小红"].map((element, index)=>{ return
    • {element}
    • })}
    ) } } let root = ReactDOM.createRoot(document.getElementById("root")) root.render()

    重新打开网页,你会发现错也不报了,一切都非常正常;

    前端学习笔记006:React.js_第48张图片

    但是这样的方法也不完美。假如你想在这个列表的第一位加一个“小刚”,那“小刚”的 li 的 key 自然就成了 1,那后面的“小明”“小红”自然也就变成了 2 和 3。结果 React 把原来“小明”的 li 和现在“小明”的 li 一对比,哎呦喂,你是 2 我是 1 啊!所以 React 也就云里雾里的把它给更新了。后面的小红也同理。这样就造成了不必要的 DOM 更新。 其实三个 DOM 也还好,那十几个呢?几十个呢?几百个呢?结果可想而知。

    那有没有比 index 更好的 key 解决方案呢?有。那就是使用 id。当然使用 id 的前提是后端给你提供了 id。比如一个学生信息里面有 id,就可以把 key 设置为 id。绝对不会重复,如下:

    class ClassComponent extends React.Component{ 
        render(){
            return (
                
      {[{name: "小明", id: 1},{name: "小红", id: 2}].map((element)=>{ return
    • {element.name}
    • })}
    ) } } let root = ReactDOM.createRoot(document.getElementById("root")) root.render()

    效果一模一样。那如果后端没有给你提供 id 呢?一般不会。作为一个合格的后端,不会连一个 id 也不传给你。所以这方面大家大可放心~

    加更 1:函数式组件 Hooks 与 useState

    前面学了一大堆的类式组件,但是很抱歉,时代变了,类式组件快完了(欲哭无泪.jpg)现在 React 官网和后面要学的 React Router 都 MFGA(Make Function Component Great Again,《重振函数式组件》)了(T_T)原因是什么,还不是因为发布时长短短两年半的 Hooks(T_T)

    这里也不说废话了,开始把~

    Hooks 是个啥东西,上文也提到过了,它可以让函数式组件也用上类式组件的功能,如 state,生命周期函数,ref 等等等等。顾名思义 Hooks 是 Hook 的复数,所以我们后面就都要和 Hook 这玩意儿打交道了~

    Hook 是一个函数,一个可以“增强”函数式组件功能的函数。我们先拿“经典案例”useState 开刀。useState 函数能收到一个数组,数组中的第一个值就相当于 state,第二个值就相当于 setState。简单不~

    把之前类式组件的内容全部清空。然后创建一个函数式组件。这个案例要不就把上面那个“这是类式组件”那个案例用起来把~

    静态页面很简单,就是一个 input 框,一个 h1。当 input 框改变的时候,上面的 h1 也跟着改变。就像下面(但是由于我们用的是函数式组件那个标题也相应的改为函数式组件~)

    前端学习笔记006:React.js_第49张图片 用函数式组件实现的~

    老样子,先写静态页面。刚刚写过静态页面了,很直接了把~

    // 懒得打字了,用 FC 更快就用 FC 把~
    function FC(){
        return (
            

    我是函数式组件

    ) } let root = ReactDOM.createRoot(document.getElementById("root")) root.render()

    很正常~

    前端学习笔记006:React.js_第50张图片

    关键不是这个,关键在下面:useState 咋用。上面也说过了 useState 是一个函数,返回值一个是 state,一个是 setState。但是 Hook 这里就有一个优点了:useState 你可以调无数次,你一个函数里边可以有无数个 state。这样不就省去了对象的麻烦了吗?那由于上面这个显示的是文本(text),那要不就叫 text,setText?(或者你叫 peiqi setPeiqi 也可以),然后就和上面的类式组件一样了,如下:

    // 懒得打字了,用 FC 更快就用 FC 把~
    function FC(){
        // 这个是数组的解构赋值~
        // 如果你想的话,useState 也可以传一个参数,即初始值
        // state 在函数体内部是不能直接用的,但是 setText 是可以正常获取到旧 state 的。
        // 但是在 return 里面可以,因为 babel 帮你改了代码
        let [text, setText] = React.useState()
    
        // 在改变的时候的回调
        // 可以不写成箭头函数,但我觉得这样方便~
        let changeText = event => {
            setText(event.target.value)
        }
    
        return (
            

    我是{text ? text : "函数式组件"}

    ) } let root = ReactDOM.createRoot(document.getElementById("root")) root.render()

    效果杠杠滴~

    前端学习笔记006:React.js_第51张图片

    关于 useState 这里有一个坑:如果新 state 依赖于旧 state,则必须要使用上面类式组件 setState 中的函数更新形式,比如 setCount(count => count+1)。这与上面类式组件有些许不同。如果你不这样写,偏要写 setCount(count+1),这里的 count 值可不是简简单单的旧 state 了,而是你当时设置的初始值(如果你没有设置初始值那应该会直接报错)。但是在 return 里面为啥能够获取到真实的 count?实际是因为 Babel 帮你改了底层代码,刚刚注释里面说过。

    想想上面 this 的痛苦,再看看 useState 的便捷,我突然明白为什么 React 他们都 MFGA 了(T_T)  

    加更 2:函数式组件 props

    这一小节我们要讲的内容和 Hook 没有关系,但是也挺重要的,那就是在函数式组件里使用 props。有的同学问了:函数式组件没有对象体,也就不能用 this.props 的方式获取 props 了,又不使用 Hooks,哪来的 props?同学,我们忽略了一个函数本身的功能,传递参数。我们可以通过参数本身的功能来传递 props,即让函数接到参数 props 然后直接用~

    下面给了一个小案例(注释一定要看),看完之后你应该就懂怎么在函数式组件中使用 props 了~

    // 诶呀妈呀,都简单成这样了……
    let FC = props => 

    我是{props.name}

    let root = ReactDOM.createRoot(document.getElementById("root")) root.render()

     效果:

    前端学习笔记006:React.js_第52张图片

    加更 3:函数式组件 useRef

    useRef 也是我们比较常用的一个 Hook。你只需要把类式组件中的 React.createRef() 换成 React.useRef() 就可以生成一个 ref 变量(这里注意 React.useRef 返回的不是一个数组而是一个 ref 变量~)然后和普通 ref 变量一样把它放进 ref 属性里就行~ 我们也是使用它的 current 值获取 DOM 元素~

    (感觉还省了 3 个字符呢~)

    我们也是把上面的 ref 案例用 useRef 写一遍。需求和上面类式组件的 ref 是一样的。

    前端学习笔记006:React.js_第53张图片

    首先先写静态页面,这个简单,一个 h1,一个 input 框,一个按钮~

    function FC(){
        return (
            

    input 框里的内容:

    ) } let root = ReactDOM.createRoot(document.getElementById("root")) root.render()

    然后 use 一个 state 名叫 text,这里也用到了上面所说的 useState。然后让 h1 显示 text 的内容~

    function FC(){
        // 这里就使用了 useState 的初始值
        let [text, setText] = React.useState("等待用户输入")
    
        return (
            

    input 框里的内容:{text}

    ) } let root = ReactDOM.createRoot(document.getElementById("root")) root.render()

    最后一步,也是最重要的一步,即设置 ref。这里我们使用 useRef 设置一个 ref,然后把它绑定到 input 中。然后给 button 添加一个按下的事件回调,里面把 text 修改为 input 框中的信息~

    function FC(){
        let input = React.useRef()
        // 这里就使用了 useState 的初始值
        let [text, setText] = React.useState("等待用户输入")
    
        let onclick = () => {
            setText(input.current.value)
        }
    
        return (
            

    input 框里的内容:{text}

    ) } let root = ReactDOM.createRoot(document.getElementById("root")) root.render()

    效果 very good~

    前端学习笔记006:React.js_第54张图片

    加更 4:函数式组件 useEffect

    这一小节,我们要学习 React 原生的最后一个 Hook,即 useEffect。其实它是干啥用的也很好理解,类式组件不是有 state,props,refs,生命周期吗?那 state,props,refs 都解决了,剩下的一个生命周期,我们就用 useEffect 来解决了(满级理解~)

    但是这里要做个备注,useEffect 并不能模拟所有的生命周期函数,只能模拟下面这三个:

    componentDidMount()

    componentDidUpdate()(可以分开来监听不同 state 的更新~)

    componentWillUnmount()

    但这一般也够用~

    那怎么模拟呢?很简单,使用 useEffect 函数。这个函数没有任何返回值,所以不需要使用变量来接住它的返回值。

    useEffect 函数其实是一个给操作添加副作用的函数。它的第一个参数就是生命周期函数的函数体,就是第二个参数就有点麻烦。

    当第二个参数为一个空数组的时候,第一个参数相当于 componentDidMount,即挂载时调用的函数。如果这个函数返回了一个函数,那么这个函数相当于 componentWillUnmount。这样说可能有点儿绕,下面这个示例一看你就懂了:

    // 这是一个定时器的例子
    React.useEffect(() => {
        // 这里面写的相当于 componentDidMount
        let timer = setInterval(() => console.log("Hello!"), 1000)
        return () => {
            // 这里面写的相当于 componentWillUnmount
            clearInterval(timer)
        }
    }, []) // 后面是一个空数组

    由于 componentWillUnmount 是 componentDidMount 的收尾操作,所以我们一般把这俩合在一起写。接下来这个是重点了,当第二个参数传的数组里面有内容(通常为 useState 返回数组的第一个值,即我们的 state),比如 [text],那这个函数将充当 componentDidMount 和 componentDidUpdate(只针对当前 state)的结合体。那 componentDidMount 是哪来的呢?你想一下,useState 创建了当前 state,是不是也算是更改 state 的值(从无到有)?所以它也是componentDidMount 时应当执行的函数之一。后面的数组可以不传,代表监听所有 state(和类式组件 componentDidUpdate 一样)。后面数组的值也可以不止一个 state,比如 [text, name],代表监听这些 state。

    React.useEffect(() => {
        // 这里面的内容相当于 componentDidUpdate
        // 但是只针对 text 一个 State
        console.log("Text has changed!")
    }, [text])

    至于练习,大家可以把上面那个练习去掉 shouldComponentUpdate 的内容再来试一试。源代码如下~

    // 这个练习其实挺不错
    // useState,useRef,useEffect 全用上了
    // 大家有兴趣可以研究研究
    function FC(){
        // 这里给它一个初始值 0
        let [time, setTime] = React.useState(0)
        // 给 checkbox 一个 ref 容器
        let checkbox = React.useRef()
    
        // 这是 componentDidMount 与 componentWillUnmount 的内容
        React.useEffect(() => {
            let timer = setInterval(() => {
                // setTime 这个坑一定要牢记
                // 必须要用函数的形式
                setTime(time => time + 1)
            }, 1000)
            return () => clearInterval(timer)
        }, [])
    
        // 这是监听 time 的 componentDidUpdate
        React.useEffect(() => {
            if (checkbox.current.checked) {
                console.log("您已打开该网页" + time + "秒")
            }
        }, [time]) // 监听 time
    
        return (
            

    您已打开该网页{time}秒

    输出
    ) } let root = ReactDOM.createRoot(document.getElementById("root")) root.render()

    到这里所有 React 自带 Hooks 的讲解就完成了,下面进行一下总结~

    加更 5:函数式组件及 Hooks 总结

    这一小节我们把函数式组件“增强”了一下,使用 Hooks 给函数式组件增加了 state refs 和生命周期三个功能。React 的未来是属于函数式组件的(这样会不会太中二……),以后的各种场合我们都会尽量使用函数式组件。

    Hooks 是一个简便的工具。它不仅仅局限于 state refs 和生命周期,以后还会学到各种奇奇怪怪的 Hooks,像是 use这啊,use那啊,反正很多就是了~ 我们以后的代码也尽量使用函数式组件 + Hooks 来写。

    这里关于第 3~6 章为什么都使用类式组件进行一个说明。这篇文章我是从 2022 年 10 月初开始写的,写了三个月才写到第六章。等到第七章的时候,突然看到 React Router 硬性要求使用函数式组件了,然后看了看其他资料,变天了(T_T)

    所以今天(2022-12-26)临时对函数式组件与 Hooks 进行一个加更,以适应第 7~8 章的学习。第 3~6 章使用类式组件,大家就将就写着把,反正类式组件和函数式组件的转化也很好转化~

    PS. 第 7 章会大量运用到其他 Hooks~

    3. React 脚手架

    3.1 React 脚手架搭建

    前面我们运行 React 代码是用的一个 html 与一个 js。但是我们实际开发不可能像上面那样写,因为 React 给我们提供了一个更便捷的开发环境:React 脚手架(不是绞首架……),即 create-react-app。它基于 Webpack,能够帮助我们更好地开发(VS Code 里对 React 脚手架项目有代码提示),调试以及打包发布。下面就开始先来搭建~

    首先 Node.js 与 NPM 肯定是先要有的。然后随便选一个文件夹,win+r 输入 cmd 打开命令提示符,输入以下内容创建一个 React 脚手架。里面的文件夹名换成真实的,但是只能使用小写。

    npx create-react-app 文件夹名

    如果 NPM 速度实在慢到奔溃,也可以使用 CNPM(前提是你得下载它,使用 npm install -g cnpm --registry=https://registry.npmmirror.com/),然后输入以下代码:

    cnpm init react-app 文件夹名

    等待亿会儿,你应该会看见创建完成的提示,就像下面。你还会发现多了一个文件夹名字就是你刚刚取得那个~ 

    前端学习笔记006:React.js_第55张图片

    打开你会发现里面有很多文件:

    前端学习笔记006:React.js_第56张图片

    很明显这就是安装了很多 NPM 包的文件夹。下面我们来看一下这些文件都是干啥子用的:

    public 文件夹:

    前端学习笔记006:React.js_第57张图片

    别看这里面那么多文件,真正有用的就一个:index.html。它的作用就相当于我们用 html js 写 React 时候的模板 HTML 文件。至于剩下的 logo192 logo512 manifest robots 一般非常少用(而且它们也不在本文的讨论范围内),所以可以全删了~

    还有一个 favicon.ico 也是老熟人了,它就是一个 React 网页的图标,你可以换成自己的,但是不能没有要不然报错~

    index.html 它默认给我们的内容太复杂了我们不要,所以,简单粗暴,全部删除~

    前端学习笔记006:React.js_第58张图片

    然后我们再一步一步添加需要的内容。作为一个合格的 HTML 模板,HTML 骨架要的吧?如下:

    
    
      
        标题
      
      
      
    
    

     这已经是我们能做到的最简形式了。由于一些浏览器可能会不支持中文,所以我们把 utf-8 编码的声明也加上:

    
    
      
        
        标题
      
      
      
    

    下一步就是创建一个根节点,一般使用 id 为 root 的 div:

    
    
      
        
        标题
      
      
        

    然后就是引入 React。但是这一步绞首架帮我们配置好了,所以不需要动~ 

    最后一步就是引入 favicon.ico。我们使用这行代码来引入图标。里面的 %PUBLIC_URL% 会被脚手架解析成真正 public 文件夹的路径。

    大功告成~  public 文件夹就可以收起来永远也不用管了~

    然后是 src 文件夹:

    前端学习笔记006:React.js_第59张图片

    这个文件夹就是放我们开发需要用到的源码了。这也是我们以后需要打交道最多的文件夹了。里面一样,还是有很多文件是没用的。我们可以放心删除下列文件,因为从头到尾都没用:App.test.js,logo.svg,reportWebVitals.js 与 setupTests.js。现在是不是干净很多~

     e3e0d961e24e42a0a16c934313a9c75f.png

    然后就是两个 css 文件,我们也可以暂时删除它们。它们是干啥我们也清楚,css 嘛~

    重点来了。index.js 与 App.js 是一整个开发流程中最重要的文件。index.js 是入口文件,每次打开网页是都会调用这个文件里面的内容。所以所有的渲染都要在这个文件里面完成。

    而 App 呢,则是所有组件的“祖宗”,也就是所有的组件都需要构建在 App 里面。这样我们在 index.js 里面渲染时,就只需要渲染一个 App 就可以了~

    index.js 里面原有的内容全部干掉,里面很多我们都用不到~

    首先引入 React 与 ReactDOM。像引入普通模块一样引入就行,只是在引入 react-dom 时后面要加 /client~

    import React from 'react'
    import ReactDOM from 'react-dom/client'
    

    然后就是创建根节点,渲染 App。首先我们得先引入 App,使用如下的语法代表从该目录下的 App.js 文件里引入 App 组件(.js 开发时可以省略)。然后的操作大家都熟悉了~

    import React from 'react'
    import ReactDOM from 'react-dom/client'
    import App from './App'
    
    let root = ReactDOM.createRoot(document.getElementById("root"))
    root.render()

    那 App 里面写啥呢?简单来说就是把一个类组件 App 当作默认组件暴露就可以,即在最后一行写 export default App。还是把所有东西都干掉,如下:

    import React from 'react'
    
    class App extends React.Component{
      render(){
        // 不知道 return 什么,要不就 hello world 把
        return (
          
    Hello World!
    ) } } export default App;

    但是出于便利,我们一般在引入 React 时顺带拿出一个 Component,然后在 extends 后面就直接写 Component 就行。如下:

    import React, { Component } from 'react'
    
    class App extends Component{
      render(){
        return (
          
    Hello World!
    ) } } export default App;

    这样就 perfect 了。还记得之前安装了一个 VS Code 插件 ES7+ React/Redux/React-Native snippets 吗?这个时候就派上用场了。你可以直接键入 rcc 然后回车,它会帮你直接生成一个如上的模板:

    前端学习笔记006:React.js_第60张图片

    前端学习笔记006:React.js_第61张图片

    大功告成!一整个脚手架就搭建完成了~

    3.2 React 脚手架运行

    上面说了那么多,那 React 脚手架怎么开启呢?很简单,就一行代码。打开 cmd 并定位到 React 脚手架所在目录。输入以下内容:

    npm start

    然后 npm 就会开始耕耘(编译),第一次编译需要一些时间。当编译完成你应该会看到如下页面,这就是 App 里面的内容。

    54552aad51a9498f964ff75dff82abec.png

    当我们更改文件内容的时候,我们不需要停止脚手架并重新开启,因为脚手架会自动检测文件内容的更改,当内容更改时自动重新编译并显示新的页面。重新编译速度非常快,比启动脚手架快多了~

    如果你的项目已经开发完成,请使用 npm run build 打包生产版 react 项目,具体详见附录:将 React 项目部署至 GitHub Pages。

    4. 练习 1:井字棋

    4.1 练习简介

    前面说了那么多芝士点,那有没有一个练习能把上面所说的芝士点全都用上呢?去官网找了一找,发现有一个井字棋还不错:

    前端学习笔记006:React.js_第62张图片

     由于官网的那个井字棋打不开,我自己写了一个版本,效果还不错,最终看起来是这样的(请手动忽略样式,如果有 css 大佬的可以自己加~)

    前端学习笔记006:React.js_第63张图片

    由于可能很多小伙伴没玩过井字棋,这里给大家讲一下规则:

    X 和 O 在一个 3x3 的棋盘上交替落子,第一步由 X 先下;

    前端学习笔记006:React.js_第64张图片

     当某行、某列或某条对角线全部都是 X 或 O 时,则该方获胜,并且下面显示某方获胜;

    前端学习笔记006:React.js_第65张图片

    当点击“重置棋盘”时,棋盘被重置,下面的状态也被重置;

    前端学习笔记006:React.js_第66张图片

    如果 9 个格子全部落满但是还没有一方获胜时,显示“棋盘已满”;

    前端学习笔记006:React.js_第67张图片

    怎么样,看起来是不是还挺简单的~ 下面马上开始~

    PS:虽然它看上去挺简单的,但实际上我们要学很多新的芝士点就比如下面这些:

    1. 多组件应用的组件拆分

    2. 多组件应用的组件写在哪以及怎么写

    3. 子组件修改父组件的 state:传递回调函数

    4. 使用 PubSubJS 进行组件通信

    ……

    (如果你需要完整的源代码,可以在这里下载:链接)

    4.2 组件拆分

    前面说了 React 是基于组件的,作为一个完整的应用,我们需要把它拆分成很多组件,每一个组件专门做组件自己的事情,这样就会让一个应用变得有条理。所以这小节我们就来拆分组件。

    ok,回到上面给的那几幅图,你想把它们拆分成哪几个组件~

    前端学习笔记006:React.js_第68张图片

    好了大家都拆完了把,下面说一下我是咋拆的~

    首先一整个游戏界面我们很自然地拆成两个部分:游戏板与下面的信息部分,我们就把它们分成两个组件:Board 组件与 Info 组件。

    前端学习笔记006:React.js_第69张图片

    那 Board 组件里面是不是有九个小格子?我们也可以把里面的小格子封装成一个组件比如叫做 Square,然后渲染九个就行了;

     前端学习笔记006:React.js_第70张图片

     那我们的组件就拆完了。其实像井字棋这种还算比较容易拆,内容一多起来就很费脑,比如下面的 React 官网……

    前端学习笔记006:React.js_第71张图片

    4.3 实现静态页面

     这一小节我们要解决的问题是组件要放在哪里写,怎么写。总不可能一股脑地全部堆在 App.js 里面把?所以这小节我们就是来解决组件的分配问题的。

    那怎么让组件的放置有条理呢?先从一个文件夹开始~ 我们通常会把组件们都写在 components 文件夹下~

    bc8ac643ea1b4ecfad5fc561fee53d20.png

     那建了这个文件夹,不是就可以在里面随便放组件文件啦?答案是还不行。因为什么?因为下图:

    前端学习笔记006:React.js_第72张图片

    大家可以看到,这个文件夹里面啥都有,又有 js,又有 css,又有 jpg,又有其他的一些杂七杂八的文件。这是我们开发中经常会碰到的情况。这样把它们整在了一个文件夹下,也没有啥用。所以,我们通常会把每一个组件需要用到的文件全部归到一个文件夹下,就像下面这样:

    前端学习笔记006:React.js_第73张图片

    大家可能也看到了,上面的组件有一些地方与我们平时开发不一样,定义组件时,文件名为什么都使用 index 呢?还有 .jsx 扩展名是什么?下面一一解答。

    关于为什么文件名都叫 index,其实这里面还涉及到了 js 引用的简写。如果我们把文件名定义成组件名,引用时就得像下面这样:

    import Board from './components/Board/Board.jsx';

     大家可以看到,board 被我们书写了两次。那 index 又是怎么个简写法呢?如果你使用 index 来定义文件名,就可以简写成如下这样:

    import Board from './components/Board'

    后面的 index.jsx 系统帮我们补全了,因为什么?因为它的文件名叫 index~ 所以我们通常使用 index.jsx 来作为文件的名称。

    那 .jsx 扩展名又是什么呢?是这样的,由于我们要把定义 React 的组件文件与普通 JS 区分开,所以就用了 React 使用的 JSX 语法的名称 .jsx 作为扩展名。系统解析时对 jsx 与 js 一视同仁,都是经过 Babel 翻译的文件。这仅仅是为了程序员开发的需要。

    ok,我们把上面提到的所有文件都创建一下,然后给 .jsx 文件 rcc 一下~

    前端学习笔记006:React.js_第74张图片

    前端学习笔记006:React.js_第75张图片

    前端学习笔记006:React.js_第76张图片

    目前,如果你开启脚手架(npm start),井字棋目前还跟你没啥关系,但是先开着后面有用~

    前端学习笔记006:React.js_第77张图片

     接下来我们来设计这个网站的静态页面,首先先看上面的 Board 部分,这是不是一个 3x3 的棋盘~

    前端学习笔记006:React.js_第78张图片

     所以我们很自然地就可以把它用一个表格(table,tr,td 标签)来实现,至于外面的边框怎么办,我们可以使用 CSS 实线边框 border-style: solid 实现~

    那这些棋子的信息怎么存储呢?由于它们需要被渲染到页面上的,所以 state 肯定是最佳的选择~

    修改后的代码如下所示:

    import React, { Component } from 'react'
    
    export default class Board extends Component {
      state = {board: [
    	" "," "," ",
    	" "," "," ",
    	" "," "," "
      ]}
    
      render() {
    	// 这里使用解构赋值拿出 board
    	let { board } = this.state
    
        return (
          
    {/* 这里可以使用 board.slice().map 优化,当然这是后面的事情,这里先这么放着 */}
    {board[0]} {board[1]} {board[2]}
    {board[3]} {board[4]} {board[5]}
    {board[6]} {board[7]} {board[8]}
    ) } }

    现在还是看不到东西,但是假如你给 this.state.board 加点料:

    前端学习笔记006:React.js_第79张图片

    很快就会有反应:

    fa8ed76a0a0b4e5faa521c060f281f7d.png

    我们之前不是定义了一个 Square 吗?正好可以用起来。我们可以把需要显示的符号通过 props 传递给 Square 组件,再由 Square 组件来显示需要的符号。后面我们加样式的时候这样就会很方便。

    (这里给 JS 功底相对不好的童鞋解释一下 board.slice(0,3).map 的含义:board 是一个数组,board.slice 就代表截取该数组从下标 0 到下标 3(包括下标 0,不包括下标 3)的数组。map 就不用说了,开头提到过)

    // Square 组件
    import React, { Component } from 'react';
    
    export default class Square extends Component {
        render() {
            return (
                
    {this.props.symbol}
    ); } }
    // Board 组件
    import React, { Component } from 'react'
    import Square from '../Square'
    
    export default class Board extends Component {
      state = {board: [
    	"X","O","X",
    	"O","X","O",
    	"X","O","X"
      ]}
    
      render() {
    	// 这里使用解构赋值拿出 board
    	let { board } = this.state
    
        return (
          
    {board.slice(0,3).map((element,index)=>{ // 由于这里不需要逆序添加元素,所以 key 使用 index return })} {board.slice(3,6).map((element,index)=>{ // 这里为了避免 key 值重复所以将每一个都加上了 3,后面加 6 同理 return })} {board.slice(6,9).map((element,index)=>{ return })}
    ) } }

    效果与之前一毛一样~

    接下来我们要给这个光秃秃的棋板加一点样式。首先就是边框的实线。我们先给表格加一个 className 属性方便选择,如下:

    前端学习笔记006:React.js_第80张图片

    然后在同目录的 index.css(大家应该已经创建了把)里面写样式,内容如下,就是简简单单的加边框实线。如果有样式大神可以多加一点。

    .game-board{
        border-style: solid;
    }

    最关键的是,我们怎么把这个样式与组件连接起来。其实并不难,简单到你绝对想不到,如下:

    import './index.css';

    再次打开浏览器,你应该可以看见实线的边框了:

    前端学习笔记006:React.js_第81张图片

    然后我们要做的就是让 Square 一整个开阔一点儿,就是把宽高都设为 30px。此外当鼠标悬浮在某一个 div 上时可以加一点灰色,让下棋体验更好~

    还是老样子加 className;

    前端学习笔记006:React.js_第82张图片

    然后写 index.css;

    /* Square 组件的 CSS */
    .square{
        width: 30px;
        height: 30px;
    }
    .square:hover{
        background-color: lightgrey;
    }

    还是老样子把它引入进去,重新打开,有没有瞬间感觉有一点棋盘的样子了~

    前端学习笔记006:React.js_第83张图片

     (这里可能还有一些地方需要微调,比如字符在 div 中的位置,这里由于篇幅,就不展开,能用就行~)

    接下来是 Info 组件。这个组件非常简单,就是几个字符串,一个 button,甚至连 css 都不需要加。字符串里显示的游戏信息我们使用 state 来存储,默认为“正在运行中”。

    import React, { Component } from 'react'
    
    export default class Info extends Component {
      state = {gameState: "正在运行中"}
    
      render() {
        return (
          
    {this.state.gameState} {/* 目前不写 onClick 事件 */}
    ) } }

    有内味了~

    前端学习笔记006:React.js_第84张图片

    静态页面实现完成,开始实现最难的动态页面~

    4.4 实现动态页面:落子功能

    看到这个题目很多人估计就感觉非常简单:这不就绑定一个 onClick 然后改一下 state 吗?其实还真没有这么简单,待会儿做出来就能让你知道什么是暗藏玄机~

    首先我们就按照最简单的方法,在每一个 Square 身上绑定一个 onClick。绑定这事谁都能做,就像下面这样:

    import React, { Component } from 'react';
    import './index.css'
    
    export default class Square extends Component {
        whenClick = ()=>{
    
        }
    
        render() {
            return (
                
    {this.props.symbol}
    ); } }

    但是,你怎么改你父组件的 state 呀?能直接改吗?很明显不能。那咋改?估计很多人就卡在这了。同学,再回想一下,我们在学 state 的时候,是不是说过:在类中,箭头函数在做回调函数的时候 this 始终指向该类的实例。那我们是不是可以这样:Board 中定义一个函数专门用于修改自己的 state,然后把这个函数当作 props 传给 Square,然后当 Square 被点击的时候就调一下这个函数,这不就间接的改了父组件的 state 吗?

    这里详细地说一下解决思路:先在传入 Square props 的时候传入一个符号的下标(即该 Square 在列表中的位置),然后定义一个 changeBoard(index) 函数更改 state 并且传入 Square props,当 Square 被点击的时候调用 changeBoard 传入之前接到的符号下标,完成~

    (由于缩进实在太乱了所以用 Prettier 进行了排版)

    // Board 组件
    import React, { Component } from "react";
    import Square from "../Square";
    import './index.css';
    
    export default class Board extends Component {
      state = { board: ["X", "O", "X", "O", "X", "O", "X", "O", "X"] };
    
      changeBoard = index => {
        // 这里做了一个简单的更改事件
        // 把对应的符号改成 X
        // slice 不传参数代表全文复制
        let boardCopy = this.state.board.slice();
        boardCopy[index] = "X";
        this.setState({board: boardCopy});
      }
    
      render() {
        // 这里使用解构赋值拿出 board
        let { board } = this.state;
    
        return (
          
    {board.slice(0, 3).map((element, index) => { // 由于这里不需要逆序添加元素,所以 key 使用 index return ( ); })} {board.slice(3, 6).map((element, index) => { // 这里为了避免 key 值重复所以将每一个都加上了 3,后面加 6 同理 return ( ); })} {board.slice(6, 9).map((element, index) => { return ( ); })}
    ); } }
    import React, { Component } from "react";
    import "./index.css";
    
    export default class Square extends Component {
      whenClick = () => {
        this.props.changeBoard(this.props.index)
      };
    
      render() {
        return (
          
    {this.props.symbol}
    ); } }

    当你点击 O 的时候你会发现都变成了 X~

    接下来我们要给它加一些正经的功能。由于我们是 O X 交替落子,所以要设置一个全局变量记录现在该谁落子。我们把它设置为 turn。然后把设置 X 那段代码改为设置 turn,最后改变 turn 的值就行了。

    // Board 组件
    import React, { Component } from "react";
    import Square from "../Square";
    import './index.css'
    
    export default class Board extends Component {
      // 可以看到我这里清空了棋盘
      state = { board: [" ", " ", " ", " ", " ", " ", " ", " ", " "] };
    
      // 第一次为 X 落子
      turn = "X";
    
      changeBoard = index => {
        // slice 不传参数代表全文复制
        let boardCopy = this.state.board.slice();
        boardCopy[index] = this.turn;
        // 这里做了判断:当 turn === X 时设置 turn 为 O 否则为 X
        this.turn = ((this.turn === "X") ? "O" : "X");
        this.setState({board: boardCopy});
      }
    
      render() {
        // 这里使用解构赋值拿出 board
        let { board } = this.state;
    
        return (
          
    {board.slice(0, 3).map((element, index) => { // 由于这里不需要逆序添加元素,所以 key 使用 index return ( ); })} {board.slice(3, 6).map((element, index) => { // 这里为了避免 key 值重复所以将每一个都加上了 3,后面加 6 同理 return ( ); })} {board.slice(6, 9).map((element, index) => { return ( ); })}
    ); } }

    井字棋已经差不多能运行了~

    前端学习笔记006:React.js_第85张图片

    这里还要做一个小优化:当这个位置已经有人落子了,那我们就不能重复落子。代码如下:

      changeBoard = index => {
        // slice 不传参数代表全文复制
        let boardCopy = this.state.board.slice();
        if (boardCopy[index] !== " "){
          return;
        }
        boardCopy[index] = this.turn;
        this.turn = ((this.turn === "X") ? "O" : "X");
        this.setState({board: boardCopy});
      }

     现在你再去点那些已经落过子的格子你会发现点不了了~

    前端学习笔记006:React.js_第86张图片

    4.5 实现动态页面:胜负判定功能

    这个功能有点儿复杂,我们可以把它拆成三个小块来完成:

    1. 判定胜负

    2. 把判断结果传给 Info 组件,Info 组件显示胜负结果

    3. Board 棋盘停止运行

    先来完成第一步,也是最重要的一步。判定胜负,那我们什么时候判定呢?毫无疑问肯定是使用 componentDidUpdate。那具体的判定方法就简单了,就是有点麻烦……(如下)

      componentDidUpdate(){
        // 棋盘,为了方便我们简写为 bo
        let bo = this.state.board
        // 判定胜负的符号 sym。
        // 这里问一个问题:我们要拿什么符号来判定胜负?
        // 那肯定是现在落子的是 X 我们就拿 X 来判断
        // 但由于 X 已经落下了,turn 已经切换到 O 了
        // 所以我们不能直接使用 turn 作为判定胜负的符号
        // 要取一个反(就是把 X 改 O,O 改 X)
        let sym = (this.turn === "X") ? "O" : "X"
        // 这可能是世界上最暴力的获胜判定方法了吧……
        let isWin = (
          // 判定横行是否有三子连起来
          (bo[0] === sym && bo[1] === sym && bo[2] === sym) || 
          (bo[3] === sym && bo[4] === sym && bo[5] === sym) ||
          (bo[6] === sym && bo[7] === sym && bo[8] === sym) ||
          // 判定纵列是否有三子连起来
          (bo[0] === sym && bo[3] === sym && bo[6] === sym) ||
          (bo[1] === sym && bo[4] === sym && bo[7] === sym) ||
          (bo[2] === sym && bo[5] === sym && bo[8] === sym) ||
          // 判定两条对角线
          (bo[0] === sym && bo[4] === sym && bo[8] === sym) ||
          (bo[2] === sym && bo[4] === sym && bo[6] === sym)
        )
        // 这里进行一个简单的输出操作
        if (isWin) {
          console.log(`${sym}获胜`)
        }
      }

    重新打开,当你把三子连起来的时候,你会发现控制台输出了一些信息~

    885ed24824384d3e85d01db6c11c1566.png

    接下来我们要解决第二步,即怎么把这个信息传递给 Info 组件。这个问题,很明显是涉及到了组件之间传递信息的方法。如果两个组件是父子组件的关系那很明显我们使用 props 就可以传递,当父组件状态更改传递给子组件的 props 也会更改,自然子组件就收到了信息。但是,这里 Board 组件与 Info 组件是兄弟组件啊!那怎么传递?

    这个问题在原生 React 里实现起来是很麻烦的(就是 Board 组件给它的父组件 App 传数据,使用我们上文提到的回调函数传法,然后 App 再把信息通过 props 传递给 Info),所以我们要使用一个扩展库来解决这个问题:PubSubJS。由于我们要安装一个新的 NPM 库,所以先 Ctrl+C 把脚手架停掉。然后输入下面的代码安装 PubSubJS:

    npm install pubsub-js

    那这个扩展库我们怎么使用呢?其实它也不难,就是一个订阅与发布消息机制的库。一个组件在挂载时(componentDidMount)订阅一个信息(即设置接收所有指定名称的信息),另外一个组件在适当的时候发布一条信息,名称如果与上面订阅的信息一样那上面的组件就会收到。发送信息时可以携带数据,所以我们就可以通过这个方法来传数据了。还有在组件即将取消挂载时(componentWillUnmount)别忘了取消订阅,进行收尾工作。

    下面列举了一些 PubSubJS 常用的 API:

    订阅消息:

    PubSubJS.subscribe(msgname, (msg, data)=>{})

    msgname 参数是需要接收消息的名称(为一个字符串),后一个参数是一个函数,每当收到消息会调用。其中的第一个参数 msg 是消息名。后一个 data 是传进来的数据。

    取消订阅消息:

    PubSubJS.unsubscribe(msgname)

    msgname 参数即为需要取消订阅消息的名称。

    发布消息:

    PubSubJS.publish(msgname, data)

    msgname 为消息名,data 为需要传输的数据。

    话不多说,开始~

    import React, { Component } from 'react'
    // 引入 PubSubJS
    import PubSubJS from 'pubsub-js'
    
    export default class Info extends Component {
      state = {gameState: "正在运行中"}
    
      componentDidMount(){
        // 订阅消息,消息名为 win,data 为获胜的一方
        PubSubJS.subscribe("win",(msg,data)=>{
          this.setState({gameState: `${data} 获胜!`})
        })
      }
    
      componentWillUnmount(){
        PubSubJS.unsubscribe("win")
      }
    
      render() {
        return (
          
    {this.state.gameState} {/* 目前不写 onClick 事件 */}
    ) } }
    // Board 的 componentDidUpdate 函数
      componentDidUpdate(){
        let bo = this.state.board
        let sym = (this.turn === "X") ? "O" : "X"
        let isWin = (
          // 判定横行是否有三子连起来
          (bo[0] === sym && bo[1] === sym && bo[2] === sym) || 
          (bo[3] === sym && bo[4] === sym && bo[5] === sym) ||
          (bo[6] === sym && bo[7] === sym && bo[8] === sym) ||
          // 判定纵列是否有三子连起来
          (bo[0] === sym && bo[3] === sym && bo[6] === sym) ||
          (bo[1] === sym && bo[4] === sym && bo[7] === sym) ||
          (bo[2] === sym && bo[5] === sym && bo[8] === sym) ||
          // 判定两条对角线
          (bo[0] === sym && bo[4] === sym && bo[8] === sym) ||
          (bo[2] === sym && bo[4] === sym && bo[6] === sym)
        )
        if (isWin){
          // 发布消息
          PubSubJS.publish("win",sym)
        }
      }

    重新启动程序,你会发现 Info 已经可以显示了~

    前端学习笔记006:React.js_第87张图片

    接下来就是最后一步,也就是第三步。 这一步其实是三步里面最简单的一个。那具体怎么停止呢?我们可以给这个组件设置一个全局变量 isStop,默认为 false,当有人胜利就为 true。然后在更新棋子的过程中如果 isStop 为 true 的话就不更新。perfect~

    // Board 组件
    import React, { Component } from "react";
    import PubSubJS from 'pubsub-js'
    import Square from "../Square";
    import './index.css'
    
    export default class Board extends Component {
      // 可以看到我这里清空了棋盘
      state = { board: [" ", " ", " ", " ", " ", " ", " ", " ", " "] };
    
      turn = "X";
    
      isStop = false;
    
      changeBoard = index => {
        // slice 不传参数代表全文复制
        let boardCopy = this.state.board.slice();
        // 如果 isStop 为 true 退出函数
        if (this.isStop){
          return;
        }
        if (boardCopy[index] !== " "){
          return;
        }
        boardCopy[index] = this.turn;
        this.turn = ((this.turn === "X") ? "O" : "X");
        this.setState({board: boardCopy});
      }
    
      componentDidUpdate(){
        let bo = this.state.board
        let sym = (this.turn === "X") ? "O" : "X"
        let isWin = (
          // 判定横行是否有三子连起来
          (bo[0] === sym && bo[1] === sym && bo[2] === sym) || 
          (bo[3] === sym && bo[4] === sym && bo[5] === sym) ||
          (bo[6] === sym && bo[7] === sym && bo[8] === sym) ||
          // 判定纵列是否有三子连起来
          (bo[0] === sym && bo[3] === sym && bo[6] === sym) ||
          (bo[1] === sym && bo[4] === sym && bo[7] === sym) ||
          (bo[2] === sym && bo[5] === sym && bo[8] === sym) ||
          // 判定两条对角线
          (bo[0] === sym && bo[4] === sym && bo[8] === sym) ||
          (bo[2] === sym && bo[4] === sym && bo[6] === sym)
        )
        if (isWin){
          PubSubJS.publish("win",sym)
          // 更改 isStop 的值
          this.isStop = !this.isStop;
        }
      }
    
      render() {
        // 这里使用解构赋值拿出 board
        let { board } = this.state;
    
        return (
          
    {board.slice(0, 3).map((element, index) => { // 由于这里不需要逆序添加元素,所以 key 使用 index return ( ); })} {board.slice(3, 6).map((element, index) => { // 这里为了避免 key 值重复所以将每一个都加上了 3,后面加 6 同理 return ( ); })} {board.slice(6, 9).map((element, index) => { return ( ); })}
    ); } }

    现在当有人获胜时,你会发现棋盘点不动了~

    前端学习笔记006:React.js_第88张图片

    4.6 实现动态页面:重置棋盘功能

    接下来就是最后一个功能了。这个井字棋其实已经能玩了。但是还有一个明显的缺陷:那就是只能玩一次。所以最后一个功能——重置棋盘功能,就是让游戏可以多次运行的。话不多说,直接开始分析~

    我们的这个功能是靠一个按钮实现的,所以我们当然是在 onClick 里面写功能。那具体功能怎么实现呢?其实这个功能不难,我们可以在 onClick 里面使用 PubSubJS 发布一条消息给 Board 组件,通知它清空棋盘。然后 Board 组件就 setState 清空棋盘。我们上面不是写了一个 isStop 控制棋盘更新吗?所以我们要把 isStop 变量也重置为 false,即让棋盘更新。最后重置一下 gameState 属性就可以了。话不多说,代码如下:

    import React, { Component } from 'react'
    // 引入 PubSubJS
    import PubSubJS from 'pubsub-js'
    
    export default class Info extends Component {
      state = {gameState: "正在运行中"}
    
      componentDidMount(){
        // 订阅消息,消息名为 win,data 为获胜的一方
        PubSubJS.subscribe("win",(msg,data)=>{
          this.setState({gameState: `${data} 获胜!`})
        })
      }
    
      componentWillUnmount(){
        PubSubJS.unsubscribe("win")
      }
    
      onResetBoard = () => {
        PubSubJS.publish("restart",true)
        this.setState({gameState: "正在运行中"})
      }
    
      render() {
        return (
          
    {this.state.gameState}
    ) } }
    // Board 组件
    import React, { Component } from "react";
    import PubSubJS from 'pubsub-js'
    import Square from "../Square";
    import './index.css'
    
    export default class Board extends Component {
      // 可以看到我这里清空了棋盘
      state = { board: [" ", " ", " ", " ", " ", " ", " ", " ", " "] };
    
      turn = "X";
    
      isStop = false;
    
      changeBoard = index => {
        // slice 不传参数代表全文复制
        let boardCopy = this.state.board.slice();
        // 如果 isStop 为 true 退出函数
        if (this.isStop){
          return;
        }
        if (boardCopy[index] !== " "){
          return;
        }
        boardCopy[index] = this.turn;
        this.turn = ((this.turn === "X") ? "O" : "X");
        this.setState({board: boardCopy});
      }
    
      componentDidMount(){
        PubSubJS.subscribe("restart",(msg,data)=>{
          // 重置棋盘与回合
          this.setState({board: [" ", " ", " ", " ", " ", " ", " ", " ", " "]})
          this.turn = "X"
          // 让游戏开始运行
          this.isStop = false
        })
      }
    
      componentWillUnmount(){
        PubSubJS.unsubscribe("restart")
      }
    
      componentDidUpdate(){
        let bo = this.state.board
        let sym = (this.turn === "X") ? "O" : "X"
        let isWin = (
          // 判定横行是否有三子连起来
          (bo[0] === sym && bo[1] === sym && bo[2] === sym) || 
          (bo[3] === sym && bo[4] === sym && bo[5] === sym) ||
          (bo[6] === sym && bo[7] === sym && bo[8] === sym) ||
          // 判定纵列是否有三子连起来
          (bo[0] === sym && bo[3] === sym && bo[6] === sym) ||
          (bo[1] === sym && bo[4] === sym && bo[7] === sym) ||
          (bo[2] === sym && bo[5] === sym && bo[8] === sym) ||
          // 判定两条对角线
          (bo[0] === sym && bo[4] === sym && bo[8] === sym) ||
          (bo[2] === sym && bo[4] === sym && bo[6] === sym)
        )
        if (isWin){
          PubSubJS.publish("win",sym)
          // 更改 isStop 的值
          this.isStop = !this.isStop;
        }
      }
    
      render() {
        // 这里使用解构赋值拿出 board
        let { board } = this.state;
    
        return (
          
    {board.slice(0, 3).map((element, index) => { // 由于这里不需要逆序添加元素,所以 key 使用 index return ( ); })} {board.slice(3, 6).map((element, index) => { // 这里为了避免 key 值重复所以将每一个都加上了 3,后面加 6 同理 return ( ); })} {board.slice(6, 9).map((element, index) => { return ( ); })}
    ); } }

    你马上就会发现,重置棋盘按钮已经能用了~

    前端学习笔记006:React.js_第89张图片

    现在还有一个小瑕疵,就是当一整个棋盘全部下满的时候,下面的状态栏还是会显示正在运行中,我们只能通过重置棋盘来重新开始。我们可以加上一个“棋盘已满”的提示在下面,以方便玩家。

    那这个怎么实现呢?其实和上面的“重置棋盘”大同小异,就是 Board 给 Info 发一个信息,让 Info 显示就行了。代码如下:

    import React, { Component } from 'react'
    // 引入 PubSubJS
    import PubSubJS from 'pubsub-js'
    
    export default class Info extends Component {
      state = {gameState: "正在运行中"}
    
      componentDidMount(){
        // 订阅消息,消息名为 win,data 为获胜的一方
        PubSubJS.subscribe("win",(msg,data)=>{
          this.setState({gameState: `${data} 获胜!`})
        })
        // 订阅棋盘已满消息
        PubSubJS.subscribe("full",(msg,data)=>{
          this.setState({gameState: "棋盘已满"})
        })
      }
    
      componentWillUnmount(){
        // 如果我不说你们还记不记得 unsubscribe
        PubSubJS.unsubscribe("win")
        PubSubJS.unsubscribe("full")
      }
    
      onResetBoard = () => {
        PubSubJS.publish("restart",true)
        this.setState({gameState: "正在运行中"})
      }
    
      render() {
        return (
          
    {this.state.gameState}
    ) } }
    // Board 组件
    import React, { Component } from "react";
    import PubSubJS from 'pubsub-js'
    import Square from "../Square";
    import './index.css'
    
    export default class Board extends Component {
      // 可以看到我这里清空了棋盘
      state = { board: [" ", " ", " ", " ", " ", " ", " ", " ", " "] };
    
      turn = "X";
    
      isStop = false;
    
      changeBoard = index => {
        // slice 不传参数代表全文复制
        let boardCopy = this.state.board.slice();
        // 如果 isStop 为 true 退出函数
        if (this.isStop){
          return;
        }
        if (boardCopy[index] !== " "){
          return;
        }
        boardCopy[index] = this.turn;
        this.turn = ((this.turn === "X") ? "O" : "X");
        this.setState({board: boardCopy});
      }
    
      componentDidMount(){
        PubSubJS.subscribe("restart",(msg,data)=>{
          // 重置棋盘与回合
          this.setState({board: [" ", " ", " ", " ", " ", " ", " ", " ", " "]})
          this.turn = "X"
          // 让游戏开始运行
          this.isStop = false
        })
      }
    
      componentWillUnmount(){
        PubSubJS.unsubscribe("restart")
      }
    
      componentDidUpdate(){
        let bo = this.state.board
        // 判断棋盘是否已满,如果已满发送消息
        if (bo.indexOf(" ") == -1){ // 如果在棋盘中找不到 " " 元素,即 indexOf 返回值为 -1
          PubSubJS.subscribe("full",true)
          // 别忘了停掉棋盘的运行
          this.isStop = true
        }
        let sym = (this.turn === "X") ? "O" : "X"
        let isWin = (
          // 判定横行是否有三子连起来
          (bo[0] === sym && bo[1] === sym && bo[2] === sym) || 
          (bo[3] === sym && bo[4] === sym && bo[5] === sym) ||
          (bo[6] === sym && bo[7] === sym && bo[8] === sym) ||
          // 判定纵列是否有三子连起来
          (bo[0] === sym && bo[3] === sym && bo[6] === sym) ||
          (bo[1] === sym && bo[4] === sym && bo[7] === sym) ||
          (bo[2] === sym && bo[5] === sym && bo[8] === sym) ||
          // 判定两条对角线
          (bo[0] === sym && bo[4] === sym && bo[8] === sym) ||
          (bo[2] === sym && bo[4] === sym && bo[6] === sym)
        )
        if (isWin){
          PubSubJS.publish("win",sym)
          // 更改 isStop 的值
          this.isStop = !this.isStop;
        }
      }
    
      render() {
        // 这里使用解构赋值拿出 board
        let { board } = this.state;
    
        return (
          
    {board.slice(0, 3).map((element, index) => { // 由于这里不需要逆序添加元素,所以 key 使用 index return ( ); })} {board.slice(3, 6).map((element, index) => { // 这里为了避免 key 值重复所以将每一个都加上了 3,后面加 6 同理 return ( ); })} {board.slice(6, 9).map((element, index) => { return ( ); })}
    ); } }

    Congratulations!恭喜完成你的第一个 React 项目!

    4.7 小结

    由于这个井字棋里面的内容还是稍微有点儿多的,所以在这里简单进行一下小结。

    我们的第一个芝士点就是多组件应用的组件拆分。一个大型的应用,不可能只使用一个组件就可以实现,必须要用很多组件来共同实现。那怎么分配这些组件呢?这时候你就需要把网页拆分成一个个独立的组件,每个组件都有自己独立的样式和功能,这样才能让我们的应用更有条理,自己也方便开发。

    然后是组件放在哪以及怎么写的问题。由于我们有很多组件,所以要对这些组件进行整理。一般来说,我们把组件们放在 components 文件夹下,然后每一个组件再新建自己的文件夹。组件名称一般使用 index.jsx 可以方便我们引入。引入 css 时直接使用 import css 文件就可以了。

    接下来我们聊聊一些技巧。子组件如果想更改父组件的 state,我们可以让父组件编写一个更改自己 state 的函数通过 props 传给子组件,然后子组件需要更改的时候调一下这个函数,ok~

    还有一个通用的方法,那就是 PubSubJS。它适用与任意两个组件之间的信息传递。PubSubJS 通过三个方法:subscribe,unsubscribe 和 publish,实现消息的订阅和发布,从而实现信息的传递。PubSubJS 不仅可以进行信息传递,还可以单纯地发送消息,其实就是不带 data 的消息传递。比如上面的“重置棋盘”与棋盘已满。如果是这种情况,data 可以随便填,比如填个 true 就挺好的。

    这个·井字棋就差不多完成了。马上开始 React AJAX 的学习~

    5. React AJAX

    5.1 axios 复习

    接下来我们要进行两个比较轻松章节的学习:React AJAX 与 React UI 组件库。先来 React AJAX。这个小节可以说是非常非常轻松,因为你真的不需要掌握什么新的知识:这一小节要讲的就是怎么在 React 项目里使用 axios 进行 AJAX 请求。但是如果你没有 axios 基础的话……就可能没有那么轻松了。如果你想快速上手 axios 可以看看我之前写的:005:数据传输 + AJAX + axios

    当然如果你已经会 axios,估计现在也忘了很多,所以这里简单对 axios 进行一下复习。

    首先 axios 要先装上:npm install axios

    前端学习笔记006:React.js_第90张图片

    然后浅浅地搭建几个 Express 服务器。首先你需要安装 express(npm install express),两个服务器(get 与 post)源代码如下。然后使用 node.js 先把 get 跑起来(应该还没忘怎么开把?使用 node 文件名)。因为等会我们要请求它。

    // getServer.js
    let express = require('express');
    let app = express();
    
    app.get('/getServer', (request, response) => { 
      response.send('Hello World!');
    });
     
    app.listen(6888, () => console.log("服务器已开始监听 6888 端口") );
    // postServer.js
    let express = require('express');
    let app = express();
    
    app.post('/postServer', (request, response) => {
      let responseText = request.query // 接收传递过来的信息
      response.send(`${responseText.username},欢迎您!`);
    });
     
    app.listen(7888, () => console.log("服务器已开始监听 7888 端口") );

    粽锁粥汁,axios 是一个 AJAX 请求库,其核心非常简单就是一个 Promise 函数。我们直接就可以使用 axios() 来进行请求。参数是一个对象,对象有三个属性:method 是请求的方法, url 是请求的链接,param 可选,是请求的参数。param 的格式是一个对象,里面传需要传递的数据。然后在这个函数的 then 当中就可以接到响应回来的东西,response.data 就是传递回来的数据了。如果请求时报了错请下载并打开跨域插件 Allow CORS。

    axios({
        method: "get",
        url: "http://127.0.0.1:6888/server"
        // get 不需要 params
    }).then( response => {
        console.log("response 收到的数据是:"+response.data); 
    })

    axios 还提供了一些简写方式,可以帮我们省略 method,就如 axios.get axios.post。这些方法的参数直接传一个字符串 url 就行。如果你需要传 params 可以通过 querystring 的方式即 ?username=xxx&password=xxxx 这样的方式传递。

    axios.get("http://localhost:6888/getServer").then( response => {
        console.log("response 收到的数据是:"+response.data);
    })

    如果你想同时进行多个 AJAX 请求操作,可以使用 axios.all。axios.all 的 then 收到的是一个列表,里面有很多 response。它们分别是每一个请求的 response。由于用的比较少所以这里不再展开说明。

    5.2 React 中的 AJAX 请求

    接下来我们会通过一个登录的小案例来实操一下 React 中的 AJAX 请求,即怎么在 React 中使用 axios 进行 AJAX 请求。同时我们在这里会学习一个新的芝士点:使用代理来解决跨域问题。

    当然在这之前我们要先把 get 服务器停掉。把 post 开起来,因为等会我们要请求 post 服务器。

    这个案例大概长这样,用户填写完用户名然后点登录,然后 React 提交 post 请求,最后把请求回来的数据放到下面来。

    c1a1b11a1ed54a39af85f36cf6817f63.png

    由于这个案例并不大,所以我们直接写在 App 里面。把刚刚的那个井字棋先全部干掉(记得先停掉服务器),然后把 App.jsx 清理成最开始的样子。首先我们先写静态页面。这个网页的静态页面真的挺简单,就是一个 h1,一个 input,一个 button,一条分割线和一个 h2。如下:

    import React, { Component } from 'react';
    
    class App extends Component {
    
      render() {
        return (
          

    登录页面

    用户名:

    您还未提交登录

    ); } } export default App;

    网页的样子已经出来了~

    现在我们开始动态页面。这个案例的核心是一个按钮,我们所有的内容都是要写在这个按钮的 onClick 里面的。我们的具体实现思路也并不难,就是先获取输入的用户名,然后给服务器发送一个 AJAX 请求,最后把这个请求的返回结果放在下面的 h2 上。有的同学可能已经忘记了怎么把内容插入一个标签里,使用 innerHTML~

    import React, { Component } from 'react';
    // axios 再好用也得先引入
    import axios from 'axios'
    
    class App extends Component {
      // 之前学的 refs,希望还记得~
      username = React.createRef()
      loginText = React.createRef()
    
      request = () => {
        let name = this.username.current.value
        // 记得端口号是 7888
        // 这里就用到了 querystring 传递 params
        axios.post(`http://localhost:7888/postServer?username=${name}`).then( response => {
          // 把 response 写入下面的 h2
          this.loginText.current.innerHTML = response.data
        })  
      }
    
      render() {
        return (
          

    登录页面

    用户名:

    您还未提交登录

    ); } } export default App;

    可以看到这个页面已经开始工作了,但是当你按下登录之后,一个熟悉的错误又报了:跨域。 

    前端学习笔记006:React.js_第91张图片

    一些熟悉跨域插件的人可能会说:这不是小事情吗?把跨域插件打开不就得了?但是你想想,这个网页是给谁看的?肯定是给用户啊!但是每一个用户都会装跨域插件吗?肯定是不可能的。所以我们只能使用另一种方法,就是代理。代理也是 React 里面的一个重难点。

    下面先来说说代理是怎么工作的。我们为什么会产生这个跨域错误呢?答案很明显,就是 localhost:3000 请求了 localhost:7888,端口号对不上。那代理是怎么解决这个问题的呢?代理,是存在于 React 内部的一个中间件,顾名思义,它是一个“中间人”,我们把要发给 7888 的请求发给代理,然后代理把自己“伪装”成 7888 的请求,把这个请求转发给 7888。等到 7888 发回来的时候,看了一下端口号,没错,7888。于是代理收到响应之后就把响应发回 3000,一整个请求流程也就结束了。由于代理是 React 里面的,它也是在 React 服务器的范畴之内,所以代理的真实端口号也是 3000。所以我们请求的时候就不能写成 7888,要写成 3000。

    前端学习笔记006:React.js_第92张图片 代理工作流程图(绘图:Gitmind)

    那有些同学可能就说了:我们自己的服务器端口号不也是 3000 吗?那如果我们想请求一个本服务器的内容但是却被代理转发走了那怎么办呢?别担心,这种情况不存在,因为 React 在匹配路径的时候会先匹配本服务器的内容,如果匹配不到再去 7888 寻找。 

    那上面说了那么多,怎么设置代理呢?有两种方法。

    第一种:直接在 package.json 里面配置。这是最简单的方法,直接在 package.json 里面加入一个配置:"proxy": "需要代理的路径"。比如下面我们要代理 7888:

    然后把 App 中的请求端口号从 7888 改成 3000。这里不再贴代码。

    启动服务器,你应该会看见请求已经成功了~

     前端学习笔记006:React.js_第93张图片

    接下来就是第二种方法:setupProxy.js。上面这种方法虽然简单,但是有一定的局限性。比如我们又想请求 localhost:5555,又想请求 localhost:7777 那怎么办?很明显使用上面的方法是不可能解决这个问题的。那 setupProxy.js 又是怎么解决的呢?它使用很多不同的 api。比如你的请求中出现了 /api1,那它就把请求转发给 5555,如果你的请求中出现了 /api2,那他就把请求转发给 7777,以此类推。

    那 setupProxy.js 写在哪里呢?写在 src 目录,即与 App.jsx 同目录就可以了。React 会自动识别这个文件;

    那 setupProxy.js 又怎么写呢? 先别急,有一个库是必须要安装的。先把脚手架停掉,然后输入npm install http-proxy-middleware 安装这个库。

    然后开始写,代码如下,注释不看等于白干:

    // 第一步:引入设置代理工具 createProxyMiddleware
    // 这里要使用 CJS 的方式引入,不能使用 ES6
    // 要不然 localhost 打不开
    const { createProxyMiddleware } = require("http-proxy-middleware")
    
    // 第二步:正式配置
    module.exports = function (app) {
        // 匹配地址中含有 /api1 的链接
        // 即 http://localhost:3000/api1 后面加点东西这种链接
        app.use(createProxyMiddleware('/api1', {
            // 代理需要转发到的目标地址
            target: 'http://localhost:7888',
            // 保持默认 true
            changeOrigin: true,
            // 这个是重点
            // 这行代码的意思是把链接中所有的 /api1 字符替换成空字符串(即删除 /api1)
            // 如果不写这行转发的地址就会变成:localhost:7888/api1/xxxx
            // 所以一定不能忘了它
            // 还有前面的 ^ 不能漏敲
            pathRewrite: {'^/api1': ''}
        }));
        // 如果你愿意,这个函数还可以多传几个参数,即多代理几个地址
        // 多加的参数也是要 createProxyMiddleware 函数,并且逗号不能漏
    };

    然后把 App 里的请求链接加上 /api1 即可:

    import React, { Component } from 'react';
    // axios 再好用也得先引入
    import axios from 'axios'
    ​
    class App extends Component {
      // 之前学的 refs,希望还记得~
      username = React.createRef()
      loginText = React.createRef()
    ​
      request = () => {
        let name = this.username.current.value
        // 记得端口号是 7888
        axios.post(`http://localhost:3000/api1/postServer?username=${name}`).then( response => {
          // 把 response 写入下面的 h2
          this.loginText.current.innerHTML = response.data
        })  
      }
    ​
      render() {
        return (
          
           

    登录页面

           用户名:                
           

    您还未提交登录

         
      ); } } ​ export default App;

    重启绞首架,你也可以看见请求成功了~ React AJAX 部分大功告成~

    (这个小节不单独写练习,与后面的 React UI 组件库,React Router 合起来做一个翻译软件)

    6. React UI 组件库

    6.1 UI 组件库简介

    接下来我们开始一个新的章节:React UI 组件库。UI 组件库,顾名思义就是一个个已经预封装好 UI 的组件,我们可以用这些现成的组件,去搭建一个漂亮的网页界面。

    使用 React 的 UI 组件库有很多,比如阿里的 Ant Design:

    前端学习笔记006:React.js_第94张图片

    官网地址:Ant Design - 一套企业级 UI 设计语言和 React 组件库

    还有一些国外的,比如 Material UI(简称 MUI):

    前端学习笔记006:React.js_第95张图片

    官网:MUI: The React component library you always wanted

    还有个 Semantic UI 也挺不错:

    前端学习笔记006:React.js_第96张图片

    官网: Semantic UI

    这里把国内开发者用的最多的 Ant Design 挑出来讲讲,其他大同小异

    6.2 Ant Design 基本使用

    Ant Design 是阿里巴巴推出的一个 UI 组件库,也是目前国内开发者使用最多的 UI 组件库,不用多说了把~

    首先我们要使用 Ant Design,肯定要在 NPM 里面安装它。还是把上面 AJAX 的内容干掉,然后安装 Ant Design:

    npm install antd

    这里我们使用 Ant Design 的 v5 版本,真的比 v4 改进了很多,组件更美观,更智能了。

    然后就可以开始使用了。Ant Design 不是一个 UI 组件库吗?那我们是不是可以直接引用 Ant Design 中的组件呢?事实证明,是可以的。这里做一个简单的示例(App.jsx),让我们在网页中显示一个 Ant Design 中的按钮。源代码如下:

    import React, { Component } from 'react';
    // 第一步:引入 Ant Design 中的按钮组件 Button
    // 你想用什么组件就 import { 组件名 } from 'antd'; 
    import { Button } from 'antd';
    
    class App extends Component {
      render(){
        // 第二步:渲染这个 Button,和使用原生态 button 的方法一毛一样
        return (
          
        )
      }
    }
    
    export default App;

    把绞首架开起来,可以看到小按钮已经出来了~

    Ant Design 的样式确实不戳~

    前端学习笔记006:React.js_第97张图片

    Ant Design 组件真的很多,那我们怎么使用它们呢?总不可能把它们都背下来把?这里我建议,想用哪个组件的时候直接在 Ant Design 组件总览上查找。

    前端学习笔记006:React.js_第98张图片

    那具体怎么查呢?比如你想查找一个按钮,就点进去按钮的界面(如上图)

    然后在下面的代码演示中找到你喜欢的按钮样式,比如这个蓝色的 Primary Button:

    前端学习笔记006:React.js_第99张图片

     前面那些都不用管,重点是最后一个:显示代码。找到你想要放置的按钮的源代码,比如 Primary Button,复制就可以了~

    前端学习笔记006:React.js_第100张图片

    放到你的代码中,就可以正常显示了。但别忘了把这个组件引进来~

    前端学习笔记006:React.js_第101张图片

    如果你决定了以后要用 Ant Design 写代码,那上面那个 Ant Design 组件总览一定要收藏,以后天天都要用到~

    6.3 Ant Design 配置主题

    这一小节我们将要学习 Ant Design 的配置主题。有些童鞋可能会问:网上一些 Ant Design 的教程不是都有按需引入吗?怎么这里没有?其实还是因为 Ant Design v5 优化了。Ant Design v5 弃用了之前的 less,使用 CSS in JS,实现了原生态的按需引入~

    Ant Design 主题的配置,我们使用一个特殊的 Ant Design 组件:ConfigProvider。这个组件的属性里面可以配置主题。在这个组件里面的所有 Ant Design 子组件就都会应用上这个主题了。

    比如下面我们把一个 Button 按钮的主颜色换成绿色,就可以这样写:

    import React, { Component } from 'react';
    // 千万别忘了引入 ConfigProvider
    import { Button, ConfigProvider } from 'antd';
    
    class App extends Component {
      render(){
        return (
          
            
          
        )
      }
    }
    
    export default App;
    

    重新打开网页你会发现按钮变成了绿色~

    但是每一个组件都得这么去配置主题不是很麻烦?有没有可以配置全局主题的工具?有,而且这个方法配置的主题真的绝,我们可以在 index.js 渲染主题的时候,加上一个 ConfigProvider:

    import React from 'react'
    import ReactDOM from 'react-dom/client'
    import App from './App'
    import { ConfigProvider } from 'antd'
    
    let root = ReactDOM.createRoot(document.getElementById("root"))
    root.render(
        
            
            
    )

    (相应的,App.jsx 里面的 ConfigProvider 就得删掉了)

    这样无论你怎么写,主题的颜色都是绿的~

    如果你想给一些代码配置自己独有的主题,也可以在这段代码的外边套上一个 ConfigProvider,这样就实现了主题的局部配置。

    那我们配置主题的时候,总不可能只配置主题色把?下面列举了一些常用的配置,可以参考(官网上找的)(我觉得比较常用的加上了粗体):

    名称 描述 类型 默认值
    borderRadius 基础组件的圆角大小,例如按钮、输入框、卡片等 number 6
    colorBgBase 用于派生背景色梯度的基础变量,v5 中我们添加了一层背景色的派生算法可以产出梯度明确的背景色的梯度变量。但请不要在代码中直接使用该 Seed Token string #fff
    colorError 用于表示操作失败的 Token 序列,如失败按钮、错误状态提示(Result)组件等。 string #ff4d4f
    colorInfo 用于表示操作信息的 Token 序列,如 Alert 、Tag、 Progress 等组件都有用到该组梯度变量。 string #1677ff
    colorPrimary 品牌色是体现产品特性和传播理念最直观的视觉元素之一。在你完成品牌主色的选取之后,我们会自动帮你生成一套完整的色板,并赋予它们有效的设计语义 string #1677ff
    colorSuccess 用于表示操作成功的 Token 序列,如 Result、Progress 等组件会使用该组梯度变量。 string #52c41a
    colorTextBase 用于派生文本色梯度的基础变量,v5 中我们添加了一层文本色的派生算法可以产出梯度明确的文本色的梯度变量。但请不要在代码中直接使用该 Seed Token string #000
    colorWarning 用于表示操作警告的 Token 序列,如 Notification、 Alert等警告类组件或 Input 输入类等组件会使用该组梯度变量。 string #faad14
    controlHeight Ant Design 中按钮和输入框等基础控件的高度 number 32
    fontFamily Ant Design 的字体家族中优先使用系统默认的界面字体,同时提供了一套利于屏显的备用字体库,来维护在不同平台以及浏览器的显示下,字体始终保持良好的易读性和可读性,体现了友好、稳定和专业的特性。 string -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'
    fontSize 设计系统中使用最广泛的字体大小,文本梯度也将基于该字号进行派生。 number 14
    lineType 用于控制组件边框、分割线等的样式,默认是实线 string solid
    lineWidth 用于控制组件边框、分割线等的宽度 number 1
    motionUnit 用于控制动画时长的变化单位 number 0.1
    sizeStep 用于控制组件尺寸的基础步长,尺寸步长结合尺寸变化单位,就可以派生各种尺寸梯度。通过调整步长即可得到不同的布局模式,例如 V5 紧凑模式下的尺寸步长为 2 number 4
    sizeUnit 用于控制组件尺寸的变化单位,在 Ant Design 中我们的基础单位为 4 ,便于更加细致地控制尺寸梯度 number 4
    wireframe 用于将组件的视觉效果变为线框化,如果需要使用 V4 的效果,需要开启配置项 boolean false
    zIndexBase 所有组件的基础 Z 轴值,用于一些悬浮类的组件的可以基于该值 Z 轴控制层级,例如 BackTop、 Affix 等 number 0
    zIndexPopupBase 浮层类组件的基础 Z 轴值,用于一些悬浮类的组件的可以基于该值 Z 轴控制层级,例如 FloatButton、 Affix、Modal 等 number 1000

    (可以把上面表格的一些词语理解成这样:Token / Seed Token 代表组件,Design Token 代表 Ant Design 组件。它们真实的意思有些许不同,但都差不多)

    6.4 Ant Design 导航组件专题

    接下来的两个小节我们要把 Ant Design 最常用,官网的代码又最令人迷惑的几个组件拿出来单独讲,方便我们的开发。这些组件有一个共同的特点,使用频率很高,但是官网给出的参考代码画风是这样的……

    前端学习笔记006:React.js_第102张图片 Menu 组件的代码,都这种画疯了吗

    这一小节我们要来专门来讲讲 Ant Design 的导航有关的组件:Breadcrumb,Menu,Dropdown。

    6.4.1 Breadcrumb 面包屑

    我们先从最简单的 Breadcrumb 开始。Breadcrumb 面包屑,咱们先来看看它的样子:

    它的官网代码还是能看得懂的,不就外面一个 Breadcrumb 组件,里面几个 Breadcrumb.Item 组件吗。我们可以随便写一下:

    import React, { Component } from 'react'
    import { Breadcrumb } from 'antd'
    
    class App extends Component {
      render() {
        return (
          
            美团
            点餐
            中国特色小吃
            油饼
          
        )
      }
    }
    
    export default App;

    6.4.2 Menu 菜单

    然后是重中之重 Menu。上面的那张图片,就出自 Ant Design 官方对于 Menu 组件的示例代码。我们不可能像官网一样一步登天,所以要一步一步来~

    Menu 组件的基本形式和上面的面包屑几乎一模一样,就是把 Breadcrumb 换成 Menu,然后选择一下横纵向:

    import React, { Component } from 'react';
    import { Menu } from 'antd'
    
    class App extends Component {
      render() {
        return (
          // mode 代表菜单的朝向。horizontal 代表横向,inline 代表纵向
          
            新鲜荔枝9.9元
            美了么搞活动啦
            油饼10元3张
          
        );
      }
    }
    
    export default App;

    效果很补戳~

    前端学习笔记006:React.js_第103张图片

    但是 Menu 这玩意儿,选项一多起来,它可能就会变成这样:

    前端学习笔记006:React.js_第104张图片

    如果每一个选项都使用 Menu.Item 来写,手不得废? 所以官方提供了一种简写方式,可以把这些选项写成一个数组,数组里面的元素统一是 {key, icon, children, label, type} 的对象,其中 key label 必选,它们分别是 Menu.Item 的 key 和显示出来的文字。然后把这个数组传入 Menu 组件的 items 属性里面就可以。出于方便,我们通常会把获取对象的过程封装成一个函数:getItem。比如下面的示例:

    import React, { Component } from 'react';
    import { Menu } from 'antd'
    
    class App extends Component {
      getItem = (label, key, icon, children, type) => ({key, icon, children, label, type})
    
      render() {
        return (
           {
            return this.getItem("Nav " + element, index)
          })}>
          
        );
      }
    }
    
    export default App;

    效果杠杠的:

    前端学习笔记006:React.js_第105张图片

    把它转换为函数式组件,就成了官网上的样子。不信你们试一试~

    6.4.3 Dropdown 下拉框

    最后一个 Dropdown,它是下拉菜单。它和 Menu 几乎也是一摸一样的,甚至连 getItem 函数都不需要变化。只是 items 属性变成了 menu 属性,mode 没了而已。还有 menu 属性有了新语法:双花括号和逗号必须要加,比如 {{items, }}。如下:

    import React, { Component } from 'react';
    import { Dropdown, Button } from 'antd'
    
    class App extends Component {
      getItem = (label, key, icon, children, type) => ({key, icon, children, label, type})
    
      render() {
        // 可以看到我把 items 写在了这里
        let items = [1,2,3,4,5,6,7,8,9,10].map((element, index) => {
          return this.getItem("Nav " + element, index)
        })
    
        return (
          // menu 的双花括号和后面的那个逗号一定不能漏!
          
            
          
        );
      }
    }
    
    export default App;

    效果依旧不戳~
    前端学习笔记006:React.js_第106张图片

    6.5 Ant Design 数据录入组件专题

    这一小节我们会挑一些数据录入的组件出来讲:Checkbox、Radio、Select 与 Cascader。废话不多说,开始~

    6.5.1 Checkbox 与 Radio

    先从 Checkbox 多选框开始。其实这个多选框本身是真的没有啥好讲的(Radio 单选框也一样),不就一个 Checkbox 组件吗(只是要记得 value 值一定要填),如下:

    import React, { Component } from 'react';
    import { Checkbox } from 'antd'
    
    class App extends Component {
      render(){
        return (
          
    记住密码:
    ) } } export default App;

    前端学习笔记006:React.js_第107张图片

    重点是多选框(单选框)的使用。一般一些选择的项都会都会有好几个吧,像考试选择题一样。这就需要用到组了。组的语法其实也不难,就是使用 Checkbox(Radio).Group 包裹就可以了。

    import React, { Component } from 'react';
    import { Checkbox } from 'antd'
    
    class App extends Component {
      render(){
        return (
          
    圣诞 圣诞快 圣诞快乐
    ) } } export default App;

    效果很补戳~

    前端学习笔记006:React.js_第108张图片 我想要的圣诞树效果怎么没了(TAT)

    Radio 其实和 Checkbox 差不多,就是多选变成了单选。举个例子:

    import React, { Component } from 'react';
    import { Radio } from 'antd'
    
    class App extends Component {
      render(){
        return (
          
    你最喜欢哪个: 油饼 荔枝
    ) } } export default App;

    6.5.2 Select 与 Cascader

    Select 与上面的 Menu 一样都有两种形式,一种是淳朴的 Select.Option 形式:

    import React, { Component } from 'react';
    import { Select } from 'antd'
    
    class App extends Component {
      render(){
        return (
          
    {/* defaultValue 指默认值 */} 请选择:
    ) } } export default App;

    前端学习笔记006:React.js_第109张图片

    当然肯定有进阶版的。Select 的进阶版会简单一点,对象里面只有两个值:value 和 label。

    import React, { Component } from 'react';
    import { Select } from 'antd'
    
    class App extends Component {
      render(){
        return (
          
    请选择: 目标语言:
    ) }

    把它渲染上去。为什么都是 Select 和 TextArea,差距咋这么大呢……

    前端学习笔记006:React.js_第168张图片 和上面的原型对比一下,没有对比就没有伤害

    那我们要借助什么工具把它们”整“成这样呢?这里介绍一个 Ant Design 提供的工具:栅格组件。

    前端学习笔记006:React.js_第169张图片

    简单来说,就是使用 Row 组件来把你的组件划分为 24 格(只是一个抽象的划分,不具体显示),然后再使用里面的 Col 组件来决定哪些组件要放在哪几格里面。比如上面最后一行的 Col-6 就指明了在这个 Col 里面的组件必须要写在这行的 1-6 格。

    我们可以把一整个页面也变成一行,让前面的 select textarea 写在前面 11 格,中间两格留白,右边 11 格写后面的 select textarea,不是看起来就会和谐多了~

    import React from 'react'
    import { Select, Input, Row, Col } from 'antd'
    
    const { TextArea } = Input
    
    export default function Main() {
      return (
        
    {/* 使用 span 属性来说明占的格数 */} 源语言: {/* 留白 */} 目标语言:
    ) }

    立马看起来有模有样了~

    前端学习笔记006:React.js_第170张图片

    不过差距还是挺大的。这里进行一下调整。

    首先是外边框 Padding 的问题。留一点 padding 显得宽敞一些~(这是 App 组件)

    import React from 'react';
    import { Layout, Menu } from 'antd';
    import { useRoutes, Link } from 'react-router-dom'
    import routes from './routes'
    import './App.css'
    const { Header, Content, Footer } = Layout;
    
    export default function App() {
      let route = useRoutes(routes)
    
      return (
        
          
    {/* 这里也要改成 light */} 首页 关于
    {/* 把 content 替换为货真价实的 route */} {route}
    TransBox v0.1, Designed by Copcin
    ); };

    前端学习笔记006:React.js_第171张图片

    然后就是最显著的一个问题,高度不够。这也是最好解决的一个问题,直接加入 rows 属性说明高度就行~

    import React from 'react'
    import { Select, Input, Row, Col } from 'antd'
    
    const { TextArea } = Input
    
    export default function Main() {
      return (
        
    {/* 使用 span 属性来说明占的格数 */} 源语言: {/* 留白 */} 目标语言:
    ) }

    翻译软件那感觉立马来了~

    前端学习笔记006:React.js_第172张图片

    最后一个问题,就是 Select 和 Textarea 之间需要留白,我们给 Select 加上 marginBottom 就行~

    import React from 'react'
    import { Select, Input, Row, Col } from 'antd'
    
    const { TextArea } = Input
    
    export default function Main() {
      return (
        
    源语言: 目标语言:
    ) }

    前端学习笔记006:React.js_第173张图片

    静态页面已经做好啦,现在做动态~

    8.6 Main 组件:实现动态页面:翻译 API

    既然我们要翻译,肯定需要有一个翻译的 API,要不然咋翻~ 所以这里简述一下我们所使用的 API。

    免费的翻译 API 真的难找,免费又好用的翻译 API 那更是万里挑一。搜了很多很多,终于搜到了一个能用的必应翻译 API(提供该 API 的博客地址,感谢!):

    http://api.microsofttranslator.com/v2/Http.svc/Translate?appId=AFC76A66CF4F434ED080D245C30CF1E71C22959C&from=transSrc&to=transTo&text=inputText

    其中的 transSrc 代表源文字的语言类型(如果为空的话代表自动检测),transTo 代表翻译后文字的语言类型,inputText 指待翻译文字。其中 transSrc transTo 的值需要语言的两个字母简称,比如中文是 zh,英语是 en,具体见这里,只有两个字符的简写才有效。

    可以在浏览器的地址栏里面试一试,可以看见翻译回来了这些东西:

    前端学习笔记006:React.js_第174张图片

     这个链接有一个好处,那就是不用配置代理。因为后端已经帮你解决好了跨域~

    8.7 Main 组件:实现动态页面:翻译功能实现

    它来了,它来了,最核心的功能它来了~

    这一小节我们就要实现 TransBox 最核心的功能:翻译了。其实翻译的核心就是一个 axios 函数,并不难,关键是怎么获取请求的三个参数:transSrc,transTo 和 inputText。

    先来 TransTo。它其实就右边”目标语言“那个 Select 框的值。我们要先把 Select 框的 item 写好。每一个选项逗号和其对应的两个字母简称挂上钩才方便使用~

    当然为了节省敲 item 的手部医疗费用,这里把我敲的 item 贴在下面:

    let transtable = [
      {
        value: "zh",
        label: "中文",
      },
      {
        value: "en",
        label: "英语",
      },
      {
        value: "ru",
        label: "俄语",
      },
      {
        value: "fr",
        label: "法语",
      },
      {
        value: "ar",
        label: "阿拉伯语",
      },
      {
        value: "es",
        label: "西班牙语",
      },
      {
        value: "bg",
        label: "保加利亚文",
      },
      {
        value: "ca",
        label: "加泰罗尼亚文",
      },
      {
        value: "cs",
        label: "捷克文",
      },
      {
        value: "da",
        label: "丹麦文",
      },
      {
        value: "de",
        label: "德语",
      },
      {
        value: "el",
        label: "希腊文",
      },
      {
        value: "et",
        label: "爱沙尼亚文",
      },
      {
        value: "fi",
        label: "芬兰文",
      },
      {
        value: "ga",
        label: "爱尔兰盖尔文",
      },
      {
        value: "hr",
        label: "克罗地亚文",
      },
      {
        value: "hu",
        label: "匈牙利文",
      },
      {
        value: "is",
        label: "冰岛文",
      },
      {
        value: "it",
        label: "意大利文",
      },
      {
        value: "iw",
        label: "希伯来文",
      },
      {
        value: "ja",
        label: "日语",
      },
      {
        value: "kk",
        label: "哈萨克文",
      },
      {
        value: "ko",
        label: "韩语",
      },
      {
        value: "lt",
        label: "立陶宛文",
      },
      {
        value: "lv",
        label: "拉脱维亚文",
      },
      {
        value: "mk",
        label: "马其顿文",
      },
      {
        value: "nb",
        label: "挪威语(伯克梅尔)",
      },
      {
        value: "nl",
        label: "荷兰文",
      },
      {
        value: "no",
        label: "挪威语",
      },
      {
        value: "pl",
        label: "波兰文",
      },
      {
        value: "pt",
        label: "葡萄牙文",
      },
      {
        value: "ro",
        label: "罗马尼亚文",
      },
      {
        value: "sk",
        label: "斯洛伐克文",
      },
      {
        value: "sl",
        label: "斯洛文尼亚文",
      },
      {
        value: "sq",
        label: "阿尔巴尼亚文",
      },
      {
        value: "sr",
        label: "塞尔维亚文",
      },
      {
        value: "sv",
        label: "瑞典文",
      },
      {
        value: "th",
        label: "泰语",
      },
      {
        value: "tr",
        label: "土耳其文",
      },
      {
        value: "uk",
        label: "乌克兰文",
      },
    ];

    这些语言都是我亲测可以翻的~ 好多语言我自己都没听过~

    为了减小 Main 组件的代码量,所以我们把这些代码写进 transtable.js,然后再引入它。如果一段代码很长,我们也建议单独建一个文件放它~

    前端学习笔记006:React.js_第175张图片

    然后我们在 Main 里面引入它,然后直接把它作为“目标语言”这个 Select 的 options~

    import React from 'react'
    import { Select, Input, Row, Col } from 'antd'
    import transtable from './transtable'
    
    const { TextArea } = Input
    
    export default function Main() {
      return (
        
    源语言: 目标语言:
    ) }

    现在是可以显示了,但是,这么窄,谁看得见啊……

    前端学习笔记006:React.js_第176张图片

    连另一个 Select 一起调宽~

    import React from 'react'
    import { Select, Input, Row, Col } from 'antd'
    import transtable from './transtable'
    
    const { TextArea } = Input
    
    export default function Main() {
      return (
        
    源语言: 目标语言:
    ) }

    效果显著~

    前端学习笔记006:React.js_第177张图片

    可以给它加上一个默认选项,比如英语:

    import React from 'react'
    import { Select, Input, Row, Col } from 'antd'
    import transtable from './transtable'
    
    const { TextArea } = Input
    
    export default function Main() {
      return (
        
    源语言: 目标语言:
    ) }

    前端学习笔记006:React.js_第178张图片

    那我们怎么获取用户选择的值呢? 上面说过了使用 ref 是不能获取到的,那我们只能退而求其次:使用 state。新建一个 state 名叫 transTo~ 然后每当 Select 更改语言就把新语言写入 transTo;

    // 用 prettier 美化了一下
    import React, { useState } from "react";
    import { Select, Input, Row, Col } from "antd";
    import transtable from "./transtable";
    
    const { TextArea } = Input;
    
    export default function Main() {
      // 默认值为 en
      let [transTo, setTransTo] = useState("en");
    
      return (
        
    源语言: