前端学习笔记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,我们是这么写的:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>使用真实 DOM 进行页面展示</title>
    </head>
    <body>
        <ul id="root"></ul>

        <script type="text/javascript">
            // 第一步:拿到 ul 列表
            let root = document.getElementById("root")

            // 准备一些 GitHub 人员数据
            let data = [
                { id: 1, name: "reactjs", project: 'reactjs'},
                { id: 2, name: "angular", project: 'angular'}
            ]

            // 第二步:创建 li 字符串
            let htmlStr = ''
            for (userIndex in data){
                htmlStr += `<li>${data[userIndex].id}: ${data[userIndex].name}-${data[userIndex].project}</li>`
            }

            // 第三步:把这个字符串渲染为 root 的子节点
            root.innerHTML = htmlStr
        </script>
    </body>
</html>

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

0bc93d4da61b41e8a810f3e7e77d56a6.png

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

6cfdc71f047a43e79f30f3ae06ca0844.png

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

1d67c798f35d4f71a9a770e885fe1ef8.png

be70b955b3ee4bdb9cfdd81f0263bd67.png

0c0020737a284eebb005acdc98834cb2.png 80c2dedaea1f4c879666564b213ce22f.png 

e819366a26ea46c99b2378b2942f70e7.png

0a710d95c63842a99ff3f47172440c1b.png

f47b58bde77f4e37be9c0626b020613f.png

b38b07e3f2454313bdfe319f62ec49d3.png

d69ba64e25164dc6a01a19bbb677f2ba.png

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

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

e67315e2ff8f4a2ebef4ebf5768b8ffa.png

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

de2f025b908c4b91a8dd53df31317093.png

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

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

<!-- 这里只是一个示例,真正的 React 组件可不长这样 -->

<!-- 定义一个 Item 组件,里面有相关的组件代码 -->
<div>
    <!-- 里面写 Item 组件的代码 -->
</div>

<!-- 在需要的时候用到它就很方便 -->
<Item logo="xxx"/>
<Item logo="yyy"/>
<!-- ... -->

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

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

2ca287bbc20746cc87bef8546e6d20c9.png

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

a79bafcc25ce4d0b8a2e6d3676a6c546.png

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

590058b32df548a290a779ccd9cda93e.png

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

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

0314830106c94ef3b4236ef7d8fcd219.png

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

3d16b8789c044c5fb3040694f9100136.png

3. Node.js 以及 NPM 

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

下载链接​​​​​​

4. 浏览器扩展:React Developer Tools

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

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

33a01a2c740e42c7af2fd728b0bb7a54.png

点击继续; 

d0ad6cd170154a618e35663433a96794.png

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

24db5d2979aa467da38724b21aa43264.png

 然后就安装成功了~

bb6e106b6a0d428fba89e1a691dba2a0.png

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

1cd62a42ccef4535b4ec5d8811abfff7.png

1.3 JSX 语法规范

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

dom.innerHTML = <div><span>This is JSX</span></div>

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

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>JSX 演示</title>
    </head>
    <body>
        <div id="root"></div>
        <!-- 引入 Babel -->
        <script src="https://cdn.staticfile.org/babel-standalone/7.20.0/babel.min.js" type="text/javascript"></script>
        <!-- Babel 必须与 React 配合使用 -->
        <!-- 所以这里要引入 React 及 ReactDOM -->
        <script src="https://cdn.staticfile.org/react/18.2.0/umd/react.development.min.js" type="text/javascript"></script>
        <script src="https://cdn.staticfile.org/react-dom/18.2.0/umd/react-dom.development.min.js" type="text/javascript"></script>
        <!-- 引入待会要写的 script.js -->
        <!-- 这里注意 type 一定要是 text/babel 才能被 Babel 发现并转化 -->
        <script src="./script.js" type="text/babel"></script>
    </body>
</html>

然后在同一个目录下创建 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> 
        {/* 第二个语法点,最外层必须只能出现一个标签 */}
        {/* 我们一般使用 div 包裹一整个 XML 代码 */}
        {/* 或许大家也注意到了 JSX 的注释是这样写的 */}
        <h1>在里面可以随心所欲地写 HTML</h1>
        {/* 这里穿插一个语法点,JSX 里的标签必须要闭合,所有的都要,即使像 br 标签这些也要自闭合 */}
        a: {a} <br/>
        {/* 如果想在 JSX 里面显示 JS 表达式的值,请使用花括号里面包一个 JS 表达式 */}
        {/* 一个变量,一个数字,一个数组,一个对象,甚至一个函数的返回值都可以作为表达式 */}
        {/* 例如 func() 这个表达式的值就是这个函数的返回值 */}
        {/* 但是如 if 判断就没有返回值 */}
        {/* 要想在里面写 if 判断必须采用三元运算符的形式 */}
        {/* 或者直接在外部定义一个函数然后再内部调用它 */}
        b: {b} <br/>
        {/* 下面来一个复杂的不知道大家看没看懂 */}
        student: <br/> 
        <ul>
            {student().map((element)=>{
                return ( <li>{element}</li> ) // 在 JSX 内嵌的 JS 里面如果还要嵌一层 JSX
                                              // JSX 里面获取 JS 表达式值还是要用花括号
            })}
        </ul>
        {/* 第三个语法点来了,class 属性要写成 className */}
        {/* 因为 html 里原生的那个 class 属性与 JS 里的 class 关键字重名了 */}
        {/* 还有一个,就是 style 属性不能写成字符串形式,要写成双花括号形式,比如下面 */}
        {/* 里面的例如 background-color 这种属性需要转换成小驼峰形式例如 backgroundColor */}
        {/* 最后一个,就是 HTML 原生的 onclick 这些事件属性全部要写成小驼峰形式 */}
        {/* 比如 onclick 要写成 onClick,onmouseup 要写成 onMouseUp */}
        {/* 还有这些属性传函数时直接传 JS 里面真实的函数,即花括号里包函数名,不要写成字符串形式 */}
        <input type="text" className="my-input" style={{backgroundColor: "red"}} onClick={callback}/>
    </div>
))

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

bf45ad573c414c7ba3388117e1b3b935.png

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

8341f0f89fe449af87f41780671bfb58.png

你可能会看到控制台报了一个警告,这个我们先不用管它,后面讲 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 代码如下:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Hello World</title>
    </head>
    <body>
        <!-- 创建一个模板 div,id 为 root -->
        <!-- 我们待会的渲染操作都是在给这个 div 里面加 HTML -->
        <div id="root"></div>
        <!-- 引入 Babel -->
        <script src="https://cdn.staticfile.org/babel-standalone/7.20.0/babel.min.js" type="text/javascript"></script>
        <!-- 引入 React,版本 18.2.0 -->
        <script src="https://cdn.staticfile.org/react/18.2.0/umd/react.development.min.js" type="text/javascript"></script>
        <!-- 引入 React DOM,版本 18.2.0 -->
        <script src="https://cdn.staticfile.org/react-dom/18.2.0/umd/react-dom.development.min.js" type="text/javascript"></script>
        <!-- 引入待会要写的 script.js -->
        <!-- 注意 type~ 上面 JSX 讲过 -->
        <script src="./script.js" type="text/babel"></script>
    </body>
</html>

可能有同学就要问了:你引入一个 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(<div>Hello World!</div>)

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

效果完美:

c5c30eb9b70049a49a59fb1a4814c8fa.png

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

8efa3c9d522645e9951b94adcccda9e6.png

2.2 React 组件与函数式组件

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

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

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

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

function funcComponent(){
    return (
        <div>
            <h1>这是一个函数式组件</h1>
            <input placeholder="函数式组件里的 Input 框"></input>
        </div>
    )
}

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

function FuncComponent(){
    return (
        <div>
            <h1>这是一个函数式组件</h1>
            <input placeholder="函数式组件里的 Input 框"></input>
        </div>
    )
}

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

function FuncComponent(){
    return (
        <div>
            <h1>这是一个函数式组件</h1>
            <input placeholder="函数式组件里的 Input 框"></input>
        </div>
    )
}

let root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<FuncComponent/>)

效果嘎嘎好:

e0a5a047755c406f9edfe77184b56a99.png

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

function FuncComponent(){
    return (
        <div>
            <h1>这是一个函数式组件</h1>
            <input placeholder="函数式组件里的 Input 框"></input>
        </div>
    )
}

let root = ReactDOM.createRoot(document.getElementById("root"))
root.render(
<div>
    <FuncComponent/>
    <FuncComponent/>
</div>
)

056b1e263d384e1082a5ed692abb20d5.png

怎么样,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 (
            <div>
                <h1>这是一个类式组件</h1>
                <input placeholder="类式组件里面的 input 框"></input>
            </div>
        )
    }
}

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

效果满分:

bd97141c208b41e580aedc86963aef61.png

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

class ClassComponent extends React.Component{ // 继承 React.Component
    render(){ // 在这个函数里面返回需要的 JSX
              // React 会帮你调一次这个函数
        return (
            <div>
                <h1>这是一个类式组件</h1>
                <input placeholder="类式组件里面的 input 框"></input>
            </div>
        )
    }
}

// 然后渲染上去
let root = ReactDOM.createRoot(document.getElementById("root"))
root.render(
    <div>
        <ClassComponent/>
        <ClassComponent/>
        <ClassComponent/>
        <ClassComponent/>
    </div>
)

774c56575c1d4411915a85d8360b61b3.png

2.4 类式组件 props

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

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

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

<!-- 这里由于是 JSX,所以传递数字参数时最好使用花括号包裹 -->
<div>
    <ClassComponent componentIndex={1}/>
    <ClassComponent componentIndex={2}/>
    <ClassComponent componentIndex={3}/>
    <ClassComponent componentIndex={4}/>
</div>

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

class ClassComponent extends React.Component{ 
    render(){ 
        return (
            <div>
                <h1>这是类式组件{this.props.componentIndex}</h1>
                {/* JSX 里嵌套 JS 的模板字符串 */}
                <input placeholder={`类式组件${this.props.componentIndex}里面的 input 框`}></input>
            </div>
        )
    }
}

效果完美:

8398a71c7e0d4b87a302fee6e9fb1400.png

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

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

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

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

class ClassComponent extends React.Component{ 
    render(){ 
        console.log(this.props);
        return (
            <div>1</div>
        )
    }
}

let root = ReactDOM.createRoot(document.getElementById("root"))
let StuInfo = {
    id: 3,
    name: "小明明",
    number: 1000
}
root.render(
    <div>
        {/* 简写方式:{...对象名} */}
        <ClassComponent {...StuInfo}/>
    </div>
)

效果杠杠滴~

0e546d55fef04b3b8d73be4f4797cba1.png

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

ceeea5f722c9478791cf864f6ed95646.png

当你正在使用生产版 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~

9ce6714dda2c434ab4b97749dd0b42ff.png

2.5 类式组件 state

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

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

class ClassComponent extends React.Component{ 
    render(){ 
        return (
            <div>
                <h1>这是类式组件</h1>
                <input placeholder="类式组件里面的 input 框"></input>
            </div>
        )
    }
}

let root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<ClassComponent/>)

然后开始讲需求。假如我们想把上面 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 (
            <div>
                <h1>这是{this.state.value}</h1>
                {/* 绑定事件时别忘记了小驼峰,不要写成 onchange 了 */}
                <input placeholder="类式组件里面的 input 框" onChange={this.onchange}></input>
            </div>
        )
    }
}

let root = ReactDOM.createRoot(document.getElementById("root"))
root.render(
    <div>
        <ClassComponent/>
    </div>
)

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

24a76311b9894914ab3530c21a108d1f.png

这个错的大概意思是:在 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 (
            <div>
                <h1>这是{this.state.value}</h1>
                {/* 绑定事件时别忘记了小驼峰,不要写成 onchange 了 */}
                <input placeholder="类式组件里面的 input 框" onChange={this.onchange}></input>
            </div>
        )
    }
}

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

ef0ec0435fc441d3a854899c04d4281e.png

那为啥不显示出来呢?我们好好回忆一下,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 (
            <div>
                <h1>这是{this.state.value}</h1>
                {/* 绑定事件时别忘记了小驼峰,不要写成 onchange 了 */}
                <input placeholder="类式组件里面的 input 框" onChange={this.onchange}></input>
            </div>
        )
    }
}

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

988f8ff266714160ac20c3f9ac1a3218.png

对于 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 (
            <div>
                {/* 对三元运算符进行的一个小复习 */}
                {/* 如果 this.state.value 为空(会转化为 false)那么显示类式组件 */}
                <h1>这是{this.state.value ? this.state.value : "类式组件"}</h1>
                {/* 绑定事件时别忘记了小驼峰,不要写成 onchange 了 */}
                <input placeholder="类式组件里面的 input 框" onChange={this.onchange}></input>
            </div>
        )
    }
}

let root = ReactDOM.createRoot(document.getElementById("root"))
root.render(
    <div>
        <ClassComponent/>
    </div>
)

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

79d7d5a8fef049eab1c444f884137d1e.png

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 框里的内容。就像下面这样:

7eed88abca79486da25a26063bb80e4d.png

 我们还是照样讲一下思路:首先给按钮绑定一个 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 (
            <div>
                {/* 这里优化了一下用户体验 */}
                {/* 当 input 未输入时上面显示“未输入” */}
                <h1>input 框里的内容:{this.state.value ? this.state.value : "未输入"}</h1>
                {/* 把 input 框存进 ref 变量 */}
                <input placeholder="可以随心所欲地输内容" ref={this.inputNode}/>
                <button onClick={this.onclick}>点我获取输入内容</button>
            </div>
        )
    }
}

let root = ReactDOM.createRoot(document.getElementById("root"))
root.render(
    <div>
        <ClassComponent/>
    </div>
)

试验一下,效果满分~

728a5d2d7a3c479490e4a84223541870.png

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

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

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

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

2.7 类式组件生命周期

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

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

6b03887528614ce19d1369b3903be176.png

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

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 <div>Hello!</div>
    }
}

2.8 类式组件生命周期练习

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

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

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

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

1a86e5807e394ffdb6a67c04327daf93.png

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

677cc8aafced44ed8ec4ed7987c4b9c4.png

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

357261f3a2394c8bb647ccfb5cc11c58.png

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

class ClassComponent extends React.Component{ 
    render(){
        return (
            <div>
                <h1>您已打开该网页0秒</h1>
                暂停<input type="checkbox"></input>
                输出<input type="checkbox"></input>
            </div>
        )
    }
}

let root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<ClassComponent/>)

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

66d37a0c62cc4e8d8ba656eefecdd4e5.png

接下来我们要让 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 (
            <div>
                {/* 把秒数绑定到 this.state.sec 上 */}
                <h1>您已打开该网页{this.state.sec}秒</h1>
                暂停<input type="checkbox"></input>
                输出<input type="checkbox"></input>
            </div>
        )
    }
}

let root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<ClassComponent/>)

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

6d4f7a0185494eaebd4c07e382f0f899.png

接下来我们要做的就是如何实现下面“暂停”和“输出”这两个功能。“暂停”的思路其实很简单,就是增加一个 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 (
            <div>
                {/* 把秒数绑定到 this.state.sec 上 */}
                <h1>您已打开该网页{this.state.sec}秒</h1>
                {/* 绑定 ref */}
                暂停<input type="checkbox" ref={this.isStop}></input>
                输出<input type="checkbox"></input>
            </div>
        )
    }
}

let root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<ClassComponent/>)

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

2796a5efd294425bba72a1d2e38359d0.png

 那“输出”怎么实现呢?其实比暂停还更简单,就使用 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 (
            <div>
                {/* 把秒数绑定到 this.state.sec 上 */}
                <h1>您已打开该网页{this.state.sec}秒</h1>
                {/* 绑定 ref */}
                暂停<input type="checkbox" ref={this.isStop}></input>
                {/* 再绑定一个 ref */}
                输出<input type="checkbox" ref={this.isOutput}></input>
            </div>
        )
    }
}

let root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<ClassComponent/>)

效果嘎嘎好~

f4d943bd437f42f0a18d6898d1f5ac30.png

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 (
            <ul>
                {["小明","小红"].map((element, index)=>{
                    return <li key={index}>{element}</li>
                })}
            </ul>
        )
    }
}

let root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<ClassComponent/>)

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

f7bc77b0764b4d0298b88b2500b97b12.png

但是这样的方法也不完美。假如你想在这个列表的第一位加一个“小刚”,那“小刚”的 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 (
            <ul>
                {[{name: "小明", id: 1},{name: "小红", id: 2}].map((element)=>{
                    return <li key={element.id}>{element.name}</li>
                })}
            </ul>
        )
    }
}

let root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<ClassComponent/>)

效果一模一样。那如果后端没有给你提供 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 也跟着改变。就像下面(但是由于我们用的是函数式组件那个标题也相应的改为函数式组件~)

用函数式组件实现的~

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

// 懒得打字了,用 FC 更快就用 FC 把~
function FC(){
    return (
        <div>
            <h1>我是函数式组件</h1>
            <input type="text"></input>
        </div>
    )
}

let root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<FC/>)

很正常~

关键不是这个,关键在下面: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 (
        <div>
            <h1>我是{text ? text : "函数式组件"}</h1>
            <input type="text" onChange={changeText}></input>
        </div>
    )
}

let root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<FC/>)

效果杠杠滴~

关于 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 => <h1>我是{props.name}</h1>

let root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<FC name="佩奇,这是我的弟弟乔治"/>)

 效果:

加更 3:函数式组件 useRef

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

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

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

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

function FC(){
    return (
        <div>
            <h1>input 框里的内容:</h1>
            <input type="text"></input>
            <button>点我获取输入内容</button>
        </div>
    )
}

let root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<FC/>)

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

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

    return (
        <div>
            <h1>input 框里的内容:{text}</h1>
            <input type="text"></input>
            <button>点我获取输入内容</button>
        </div>
    )
}

let root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<FC/>)

最后一步,也是最重要的一步,即设置 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 (
        <div>
            <h1>input 框里的内容:{text}</h1>
            <input type="text" ref={input}></input>
            <button onClick={onclick}>点我获取输入内容</button>
        </div>
    )
}

let root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<FC/>)

效果 very good~

加更 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 (
        <div>
            <h1>您已打开该网页{time}秒</h1>
            输出<input type="Checkbox" ref={checkbox}></input>
        </div>
    )
}

let root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<FC/>)

到这里所有 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 文件夹名

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

1905cc209f1c46a89f9701c3f1675048.png

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

03fab0c7dc134ea8940c68ccfa19408a.png

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

public 文件夹:

27c32bae182d43e28c9bf2d5567ddffa.png

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

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

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

fa03fcc5b9e242088a914093eaf8983f.png

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

<!DOCTYPE html>
<html>
  <head>
    <title>标题</title>
  </head>
  <body>
  </body>
</html>

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

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>标题</title>
  </head>
  <body>
  </body>
</html>

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

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>标题</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

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

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

<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />

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

然后是 src 文件夹:

d4bbdff932a14003abbf9aaf4da4bb7d.png

这个文件夹就是放我们开发需要用到的源码了。这也是我们以后需要打交道最多的文件夹了。里面一样,还是有很多文件是没用的。我们可以放心删除下列文件,因为从头到尾都没用: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 里面写啥呢?简单来说就是把一个类组件 App 当作默认组件暴露就可以,即在最后一行写 export default App。还是把所有东西都干掉,如下:

import React from 'react'

class App extends React.Component{
  render(){
    // 不知道 return 什么,要不就 hello world 把
    return (
      <div>
        Hello World!
      </div>
    )
  }
}

export default App;

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

import React, { Component } from 'react'

class App extends Component{
  render(){
    return (
      <div>
        Hello World!
      </div>
    )
  }
}

export default App;

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

97e2bd7ff30f4d549df3ce4496e60f44.png

ef07e47ffb0443958e39b762cff156d2.png

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

3.2 React 脚手架运行

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

npm start

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

54552aad51a9498f964ff75dff82abec.png

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

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

4. 练习 1:井字棋

4.1 练习简介

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

904ca8c7bf784aac950eaa404dd69d8e.png

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

0c9b70810d964bbc9ba5a2ba2921aa28.png

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

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

11cb6057695146ec9d8b8c6dd45cdee8.png

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

617c1211063d425d95fb5c820f989d3b.png

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

22e8464a41ff4bd4b6b2afe0405adcd5.png

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

8c30b6d08da44a5b863530f1e71aa5b3.png

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

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

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

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

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

4. 使用 PubSubJS 进行组件通信

……

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

4.2 组件拆分

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

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

fca1e604569743b9a6810f1f392e955b.png

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

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

8063635e548c414c9d984cc323d2232a.png

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

 8aba1b2098df47b8ab64fcb39cff1761.png

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

196fd2de4de44d799cb356ebfc5de0cd.png

4.3 实现静态页面

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

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

bc8ac643ea1b4ecfad5fc561fee53d20.png

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

c7c0922d21d645ba81ab8a5525016727.png

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

0db29b4e0f6e4cf8a9a364729bbe3583.png

大家可能也看到了,上面的组件有一些地方与我们平时开发不一样,定义组件时,文件名为什么都使用 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 一下~

d4349b8ce8704351989a514a33db9d9d.png

3f7770663b344d06bd3791199d06d0de.png

6f7b172f617549ccb1f08d337cd1544b.png

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

5f6b056ba1db4651a1ee1b864ac53502.png

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

0d484ad87ae549aaaf3c14a77aef551a.png

 所以我们很自然地就可以把它用一个表格(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 (
      <div>
        <table>
            <tr>
              {/* 这里可以使用 board.slice().map 优化,当然这是后面的事情,这里先这么放着 */}
              <td>{board[0]}</td>
              <td>{board[1]}</td>
              <td>{board[2]}</td>
            </tr>
			<tr>
              <td>{board[3]}</td>
              <td>{board[4]}</td>
              <td>{board[5]}</td>
			</tr>
			<tr>
              <td>{board[6]}</td>
              <td>{board[7]}</td>
              <td>{board[8]}</td>
			</tr>
        </table>
      </div>
    )
  }
}

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

ce5be213f4744b4692d54cad665f08b1.png

很快就会有反应:

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 (
            <div>{this.props.symbol}</div>
        );
    }
}
// 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 (
      <div>
        <table>
          <tr>
            {board.slice(0,3).map((element,index)=>{
		      // 由于这里不需要逆序添加元素,所以 key 使用 index
			  return <td key={index}><Square symbol={element}/></td>
			})}
          </tr>
		  <tr>
			{board.slice(3,6).map((element,index)=>{
			  // 这里为了避免 key 值重复所以将每一个都加上了 3,后面加 6 同理
			  return <td key={index+3}><Square symbol={element}/></td>
			})}
		  </tr>
	      <tr>
		    {board.slice(6,9).map((element,index)=>{
			  return <td key={index+6}><Square symbol={element}/></td>
			})}
		  </tr>
        </table>
      </div>
    )
  }
}

效果与之前一毛一样~

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

ee52bda2d0994986acbb09115ecd9b0d.png

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

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

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

import './index.css';

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

5433fea840ca4a9b90a6224fb36ff13b.png

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

还是老样子加 className;

73d2195bc5a94fd8a3dade155683a828.png

然后写 index.css;

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

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

7a8accd220434a258d2845d9379b9a5d.png

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

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

import React, { Component } from 'react'

export default class Info extends Component {
  state = {gameState: "正在运行中"}

  render() {
    return (
      <div>
        <span>{this.state.gameState}</span>
        {/* 目前不写 onClick 事件 */}
        <button>重置棋盘</button>
      </div>
    )
  }
}

有内味了~

b8fec2defda24e8f9f438cb2bffd47b1.png

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

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

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

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

import React, { Component } from 'react';
import './index.css'

export default class Square extends Component {
    whenClick = ()=>{

    }

    render() {
        return (
            <div className='square' onClick={this.whenClick}>{this.props.symbol}</div>
        );
    }
}

但是,你怎么改你父组件的 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 (
      <div>
        <table className="game-board">
          <tr>
            {board.slice(0, 3).map((element, index) => {
              // 由于这里不需要逆序添加元素,所以 key 使用 index
              return (
                <td key={index}>
                  <Square symbol={element} index={index} changeBoard={this.changeBoard}/>
                </td>
              );
            })}
          </tr>
          <tr>
            {board.slice(3, 6).map((element, index) => {
              // 这里为了避免 key 值重复所以将每一个都加上了 3,后面加 6 同理
              return (
                <td key={index + 3}>
                  <Square symbol={element} index={index + 3} changeBoard={this.changeBoard}/>
                </td>
              );
            })}
          </tr>
          <tr>
            {board.slice(6, 9).map((element, index) => {
              return (
                <td key={index + 6}>
                  <Square symbol={element} index={index + 6} changeBoard={this.changeBoard}/>
                </td>
              );
            })}
          </tr>
        </table>
      </div>
    );
  }
}
import React, { Component } from "react";
import "./index.css";

export default class Square extends Component {
  whenClick = () => {
    this.props.changeBoard(this.props.index)
  };

  render() {
    return (
      <div className="square" onClick={this.whenClick}>
        {this.props.symbol}
      </div>
    );
  }
}

当你点击 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 (
      <div>
        <table className="game-board">
          <tr>
            {board.slice(0, 3).map((element, index) => {
              // 由于这里不需要逆序添加元素,所以 key 使用 index
              return (
                <td key={index}>
                  <Square symbol={element} index={index} changeBoard={this.changeBoard}/>
                </td>
              );
            })}
          </tr>
          <tr>
            {board.slice(3, 6).map((element, index) => {
              // 这里为了避免 key 值重复所以将每一个都加上了 3,后面加 6 同理
              return (
                <td key={index + 3}>
                  <Square symbol={element} index={index + 3} changeBoard={this.changeBoard}/>
                </td>
              );
            })}
          </tr>
          <tr>
            {board.slice(6, 9).map((element, index) => {
              return (
                <td key={index + 6}>
                  <Square symbol={element} index={index + 6} changeBoard={this.changeBoard}/>
                </td>
              );
            })}
          </tr>
        </table>
      </div>
    );
  }
}

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

fb3a1574b439432fae6cf9b4f80e8718.png

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

  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});
  }

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

cb9168a9dd134ad4ad7e1e945af75b12.png

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 (
      <div>
        <span>{this.state.gameState}</span>
        {/* 目前不写 onClick 事件 */}
        <button>重置棋盘</button>
      </div>
    )
  }
}
// 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 已经可以显示了~

8aae529345864c1cb4f8d848f58e6883.png

接下来就是最后一步,也就是第三步。 这一步其实是三步里面最简单的一个。那具体怎么停止呢?我们可以给这个组件设置一个全局变量 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 (
      <div>
        <table className="game-board">
          <tr>
            {board.slice(0, 3).map((element, index) => {
              // 由于这里不需要逆序添加元素,所以 key 使用 index
              return (
                <td key={index}>
                  <Square symbol={element} index={index} changeBoard={this.changeBoard}/>
                </td>
              );
            })}
          </tr>
          <tr>
            {board.slice(3, 6).map((element, index) => {
              // 这里为了避免 key 值重复所以将每一个都加上了 3,后面加 6 同理
              return (
                <td key={index + 3}>
                  <Square symbol={element} index={index + 3} changeBoard={this.changeBoard}/>
                </td>
              );
            })}
          </tr>
          <tr>
            {board.slice(6, 9).map((element, index) => {
              return (
                <td key={index + 6}>
                  <Square symbol={element} index={index + 6} changeBoard={this.changeBoard}/>
                </td>
              );
            })}
          </tr>
        </table>
      </div>
    );
  }
}

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

c8ef59fea11749d9ad6f50a715016e0e.png

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 (
      <div>
        <span>{this.state.gameState}</span>
        <button onClick={this.onResetBoard}>重置棋盘</button>
      </div>
    )
  }
}
// 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 (
      <div>
        <table className="game-board">
          <tr>
            {board.slice(0, 3).map((element, index) => {
              // 由于这里不需要逆序添加元素,所以 key 使用 index
              return (
                <td key={index}>
                  <Square symbol={element} index={index} changeBoard={this.changeBoard}/>
                </td>
              );
            })}
          </tr>
          <tr>
            {board.slice(3, 6).map((element, index) => {
              // 这里为了避免 key 值重复所以将每一个都加上了 3,后面加 6 同理
              return (
                <td key={index + 3}>
                  <Square symbol={element} index={index + 3} changeBoard={this.changeBoard}/>
                </td>
              );
            })}
          </tr>
          <tr>
            {board.slice(6, 9).map((element, index) => {
              return (
                <td key={index + 6}>
                  <Square symbol={element} index={index + 6} changeBoard={this.changeBoard}/>
                </td>
              );
            })}
          </tr>
        </table>
      </div>
    );
  }
}

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

1342c06b96b341e4b9800346c24c79c7.png

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

那这个怎么实现呢?其实和上面的“重置棋盘”大同小异,就是 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 (
      <div>
        <span>{this.state.gameState}</span>
        <button onClick={this.onResetBoard}>重置棋盘</button>
      </div>
    )
  }
}
// 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 (
      <div>
        <table className="game-board">
          <tr>
            {board.slice(0, 3).map((element, index) => {
              // 由于这里不需要逆序添加元素,所以 key 使用 index
              return (
                <td key={index}>
                  <Square symbol={element} index={index} changeBoard={this.changeBoard}/>
                </td>
              );
            })}
          </tr>
          <tr>
            {board.slice(3, 6).map((element, index) => {
              // 这里为了避免 key 值重复所以将每一个都加上了 3,后面加 6 同理
              return (
                <td key={index + 3}>
                  <Square symbol={element} index={index + 3} changeBoard={this.changeBoard}/>
                </td>
              );
            })}
          </tr>
          <tr>
            {board.slice(6, 9).map((element, index) => {
              return (
                <td key={index + 6}>
                  <Square symbol={element} index={index + 6} changeBoard={this.changeBoard}/>
                </td>
              );
            })}
          </tr>
        </table>
      </div>
    );
  }
}

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

86fa8a71180f4d93b17edcd49b554802.png

然后浅浅地搭建几个 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 (
      <div>
        <h1>登录页面</h1>
        用户名:<input type="text"></input>
        <button onClick={this.request}>登录</button>
        <hr/> 
        <h2>您还未提交登录</h2>
      </div>
    );
  }
}

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 (
      <div>
        <h1>登录页面</h1>
        用户名:<input ref={this.username} type="text"></input>
        <button onClick={this.request}>登录</button>
        <hr/> 
        <h2 ref={this.loginText}>您还未提交登录</h2>
      </div>
    );
  }
}

export default App;

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

3e9450697b6a426fa3a9a4155d917e8f.png

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

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

e4095fa3d80b46738cf5c0f1c8d4331a.png
代理工作流程图(绘图:Gitmind)

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

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

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

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

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

 

接下来就是第二种方法: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 (
      <div>
        <h1>登录页面</h1>
        用户名:<input ref={this.username} type="text"></input>
        <button onClick={this.request}>登录</button>
        <hr/> 
        <h2 ref={this.loginText}>您还未提交登录</h2>
      </div>
    );
  }
}
​
export default App;

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

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

6. React UI 组件库

6.1 UI 组件库简介

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

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

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

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

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

还有个 Semantic UI 也挺不错:

官网: 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 (
      <Button>Click me!</Button>
    )
  }
}

export default App;

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

Ant Design 的样式确实不戳~

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

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

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

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

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

如果你决定了以后要用 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 (
      <ConfigProvider theme={{
        token: {
          colorPrimary: '#00b96b',
        },
      }}>
        <Button type='primary'>Green Button</Button>
      </ConfigProvider>
    )
  }
}

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(
    <ConfigProvider theme={{
        token: {
          colorPrimary: '#00b96b',
        },
      }}>
        <App/>
    </ConfigProvider>    
)

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

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

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

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

名称描述类型默认值
borderRadius基础组件的圆角大小,例如按钮、输入框、卡片等number6
colorBgBase用于派生背景色梯度的基础变量,v5 中我们添加了一层背景色的派生算法可以产出梯度明确的背景色的梯度变量。但请不要在代码中直接使用该 Seed Tokenstring#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 Tokenstring#000
colorWarning用于表示操作警告的 Token 序列,如 Notification、 Alert等警告类组件或 Input 输入类等组件会使用该组梯度变量。string#faad14
controlHeightAnt Design 中按钮和输入框等基础控件的高度number32
fontFamilyAnt 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设计系统中使用最广泛的字体大小,文本梯度也将基于该字号进行派生。number14
lineType用于控制组件边框、分割线等的样式,默认是实线stringsolid
lineWidth用于控制组件边框、分割线等的宽度number1
motionUnit用于控制动画时长的变化单位number0.1
sizeStep用于控制组件尺寸的基础步长,尺寸步长结合尺寸变化单位,就可以派生各种尺寸梯度。通过调整步长即可得到不同的布局模式,例如 V5 紧凑模式下的尺寸步长为 2number4
sizeUnit用于控制组件尺寸的变化单位,在 Ant Design 中我们的基础单位为 4 ,便于更加细致地控制尺寸梯度number4
wireframe用于将组件的视觉效果变为线框化,如果需要使用 V4 的效果,需要开启配置项booleanfalse
zIndexBase所有组件的基础 Z 轴值,用于一些悬浮类的组件的可以基于该值 Z 轴控制层级,例如 BackTop、 Affix 等number0
zIndexPopupBase浮层类组件的基础 Z 轴值,用于一些悬浮类的组件的可以基于该值 Z 轴控制层级,例如 FloatButton、 Affix、Modal 等number1000

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

6.4 Ant Design 导航组件专题

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

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 (
      <Breadcrumb>
        <Breadcrumb.Item>美团</Breadcrumb.Item>
        <Breadcrumb.Item>点餐</Breadcrumb.Item>
        <Breadcrumb.Item>中国特色小吃</Breadcrumb.Item>
        <Breadcrumb.Item>油饼</Breadcrumb.Item>
      </Breadcrumb>
    )
  }
}

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 代表纵向
      <Menu mode="horizontal">
        <Menu.Item>新鲜荔枝9.9元</Menu.Item>
        <Menu.Item>美了么搞活动啦</Menu.Item>
        <Menu.Item>油饼10元3张</Menu.Item>
      </Menu>
    );
  }
}

export default App;

效果很补戳~

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

如果每一个选项都使用 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 (
      <Menu mode="horizontal" items={[1,2,3,4,5,6,7,8,9,10].map((element, index) => {
        return this.getItem("Nav " + element, index)
      })}>
      </Menu>
    );
  }
}

export default App;

效果杠杠的:

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

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 的双花括号和后面的那个逗号一定不能漏!
      <Dropdown menu={{items, }}>
        <Button>Hello There!</Button>
      </Dropdown>
    );
  }
}

export default App;

效果依旧不戳~

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 (
      <div>
        记住密码:<Checkbox value="rememberMe"/>
      </div>
    )
  }
}

export default App;

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

import React, { Component } from 'react';
import { Checkbox } from 'antd'

class App extends Component {
  render(){
    return (
      <div>
        <Checkbox.Group>
          <Checkbox value={1}>圣</Checkbox>
          <Checkbox value={12}>圣诞</Checkbox>
          <Checkbox value={123}>圣诞快</Checkbox>
          <Checkbox value={1234}>圣诞快乐</Checkbox>
        </Checkbox.Group>
      </div>
    )
  }
}

export default App;

效果很补戳~

我想要的圣诞树效果怎么没了(TAT)

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

import React, { Component } from 'react';
import { Radio } from 'antd'

class App extends Component {
  render(){
    return (
      <div>你最喜欢哪个:
        <Radio.Group>
          <Radio value="oilcake">油饼</Radio>
          <Radio value="lichee">荔枝</Radio>
        </Radio.Group>
      </div>
    )
  }
}

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 (
      <div>
        {/* defaultValue 指默认值 */}
        请选择:<Select defaultValue={1}>
          {/* 还是一样的 value 必须要写 */}
          <Select.Option value={1}>选项 1</Select.Option>
          <Select.Option value={2}>选项 2</Select.Option>
          <Select.Option value={3}>选项 3</Select.Option>
        </Select>
      </div>
    )
  }
}

export default App;

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

import React, { Component } from 'react';
import { Select } from 'antd'

class App extends Component {
  render(){
    return (
      <div>
        请选择:<Select defaultValue={1} options={[
          {
            value: 1,
            label: "选项 1"
          },
          {
            value: 2,
            label: "选项 2"
          },
          {
            value: 3,
            label: "选项 3"
          }
        ]}/>
      </div>
    )
  }
}

效果一样滴~

下面是 Cascader。它是 Select 的升级版,可以实现层级选择。其实使用方法也非常简单,把 Select 换成 Cascader,options 里面的东西加上 children 属性支持子选项即可。文字说明不清楚,代码最直观~

import React, { Component } from 'react';
import { Cascader } from 'antd'

class App extends Component {
  render(){
    return (
      <div>
        请选择:<Cascader defaultValue={1} options={[
          {
            value: 1,
            label: "选项 1",
            children: [
              {
                value: 2,
                label: "选项 2"
              },
              {
                value: 3,
                label: "选项 3"
              }
            ]
          },
          {
            value: 4,
            label: "选项 4",
            children: [
              {
                value: 5,
                label: "选项 5"
              }
            ]
          }
        ]}/>
      </div>
    )
  }
}

export default App;

上面这些组件,其实官网的描述也还挺好认的,那我们为什么要把它挑出来讲呢?还有一个更重要的原因:ref 引用。

我在写后面第 8 章那个翻译程序的时候,被一个 ref 有关的问题卡了好半天。结果得出的结论是:当 ref 绑定在 Ant Design 这些元素的属性之上的时候,ref.current 根本不能识别到什么有用的数据。

就拿 Select 框来说吧,我们可以输出一下它的 ref:

我当时选择的选项是“选项 1”,但是我翻遍了这个类包括它的原型,愣是没有找到任何跟“选项 1”有关的内容。所以千万不要用 ref 引用 Ant Design 组件,能不能获取到你需要的值,就全看天意了……

那这个问题怎么解决呢?其实真的很简单。也是拿 Select 框来说。使用 onChange 属性,里面能获取到的 event 竟然就是选项本身……我们可以把它存入 state,这样就间接实现了获取值。

其他的你们可以自己摸索摸索。这个坑真的踩得,花了我整整一天(T_T)

6.6 iconFont 图标库使用

这一小节我们来讲讲 UI 中的图标。目前国内比较常用的图标库是 iconFont。它是阿里妈妈(对,你没听错,就是阿里妈妈)的一个图标库,与 Ant Design 一脉相承(不过其他 UI 也可以用~)你想要什么图标,在 iconFont 上面搜,它立马给你显示。

iconFont 的官网在这里,可以看到真的是阿里妈妈~

比如你想搜关于“翻译”的图标,就直接搜“翻译”,它会显示很多图标~

比如你相中了一款,就点进下面那个小下载图标(你可能需要先登录,不过很快):

填写一下颜色与大小(200 那边),点复制 SVG 代码;

把 SVG 代码粘贴到你的 React 组件里面(SVG 代码也是 html 标签的一种),你的网页中就可以看见了~

有点儿大~

你还可以把图标嵌入你的按钮中。对于 Ant Design,使用 Button 组件的 icon 属性来嵌入按钮:

7. React Router

7.1 React Router 简介

(从这里开始,我们开始使用函数式组件的定义方法~)

这一小节是 React 中最后一个芝士点,也是 React 中最后的一个难点:React Router。那 React Router 是个啥?我们为啥要去学它?它有啥用?

简单来说,React Router 是一个路由库,即让网页可以根据链接的切换显示不同组件的一个库。

那 React Router 具体用在什么地方呢?看一下上图,左边是不是有一个导航栏?那有了导航栏,势必就是要使用不同的页面来显示不同的信息了。但是 React 应用有那么多页面吗?不是只有一个 index.html 吗。那这种情况怎么解决呢?我们可能已经束手无策了。

React Router 另辟蹊径,根据网页地址栏链接的切换,把右边的主体部分放上对应的组件。简而言之,你点击一个 Router 链接,它帮你改网页地址栏的链接。然后 React Router 监测到链接的改变,帮你按照你已经预先配置好的路由表(路由表是一张记录链接与组件对应关系的表格,我们等会儿会使用一个新 Hook 来创建,希望你还记得 Hook 是啥~)里的信息渲染对应的组件。是不是很妙~

React Router 工作流程图示(绘图:Gitmind)

React Router 是怎么控制和读取地址栏的呢?那这就不得不提及到浏览器的历史记录 history 了。

浏览器的 history 采用了类似于的结构。栈是一种先进后出(Last In, First Out,LIFO)的数组(你可以把它理解为一个装着一颗一颗糖果的小包装比如曼妥思,最后一颗放进去的糖果我们总是第一个看见并且吃了它)

 

React Router 在操作时也是使用了浏览器的 history。当你的链接切换的时候,Router 会推入一条新的数据进入 history 里面,然后识别最上面的那一条数据即刚刚推入的数据,然后就可以监听到并且发生改变。当你按浏览器这两个键(历史记录的后退前进键),这个栈的栈顶(即最上面那个元素)也会发生改变,Router 就会检测到了。

上述的这种工作方式是浏览器路由器 BrowserRouter 的工作方式,也是我们最经常使用的工作方式。除此之外,还有一种路由器 HashRouter,它不在浏览器地址栏上进行任何实质性的链接跳转,而是使用了锚链接(带 # 的,希望还没忘)的形式。它原本由于兼容性比较好也很常用,但是现在官方已经不建议使用 HashRouter 了。所以我们的教程统一使用 BrowserRouter~

上面说了那么多关于 React Router 的东西,那它到底怎么安装呢?一行代码:npm install react-router-dom。这里为啥要加个 -dom 呢?因为 React Router 有三个库:react-router,React Router 的核心库;react-router-dom,在包含了前面所有东西的情况下又增加了和 DOM 有关的内容,即和网页开发有关的内容。react-router-native,在包含了 react-router 所有东西的情况下有增加了与 React Native 相关的内容,即和移动端开发有关的内容。这里我们明显是要使用 react-router-dom。

7.2 BrowserRouter 与 useRoutes

这一小节我们要学习路由表的配置。但是这里有一个问题:我们路由器都没有,设置什么路由表啊?所以我们第一步是先要设置路由器。我们一般是把 BrowserRouter 给包在 App 标签的外面(即写在 index.js 里面),即 BrowserRouter 标签,以达到全局路由的效果。

(PS. 这里的路由器指的不是家里那个 Wi-Fi 路由器,指的是 React Router 路由器)

把绞首架还原成最初的样子,然后把 index.js 写成这样:

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
// 引入 BrowserRouter
import { BrowserRouter } from 'react-router-dom'

let root = ReactDOM.createRoot(document.getElementById("root"))
root.render(
  <BrowserRouter>
    <App/>
  </BrowserRouter>
)

之前说过 App 写成函数组件的形式,所以这里就写成函数式组件~ 函数式组件的快速初始化使用快捷键 rfc~

这是我们在这一小节要学习的第一个新 Hook:useRoutes(仔细想想,如果你没有用函数式组件,那你只能用老版的路由配置。老版的路由配置可是很麻烦的偶~)它是用来配置路由表的。

路由表的配置简单极了,useRoutes 传一个数组,数组里面是一个一个小的路由配置表。它是一个对象。里面有两个属性:path 属性和 element 属性。它们分别指的是路由链接和该链接所对应要显示的组件。

当然我们需要拿一个变量把 useRoutes 的返回值存起来。因为我们在网页渲染的时候,首先要给 element 选一个好地方来放置。我们只要在这个地方渲染 useRoutes 的返回值就相当于把 element 放在了这个地方。

当然在 useRoutes 之前,我们先要认识一下路由组件和普通组件的一个小区别:存放路由组件的文件夹一般命名为 Pages,而不是命名为 Components。我们先”象征性“地建两个路由组件,很简单,如下:

 

 然后正式开始建路由表。路由表上面已经说过规则了,直接开始把~

import React from 'react'
import Component1 from './pages/Component1'
import Component2 from './pages/Component2'
import { useRoutes } from 'react-router-dom'

export default function App() {
  let route = useRoutes([
    {
      path: "/path1",
      element: <Component1/>
    },
    {
      path: "/path2",
      element: <Component2/>
    }
  ])

  return (
    <div>
      {route}
    </div>
  )
}

可以尝试在地址栏输入 localhost:3000/path1,你应该会看到 Component1 组件已经渲染出来了。这证明路由表已经开始工作了。

路由表还有一种更规范的形式,就是写在 routes 文件夹下。文字可能不太直观,上图片:

里面 routes 文件夹下 index.js 就是存放路由表的文件。里面写啥呢?其实就把一个路由表数组当作默认接口暴露就可以了。如下:

import Component1 from '../pages/Component1'
import Component2 from '../pages/Component2'

let routes = [
  {
    path: "/path1",
    element: <Component1 />,
  },
  {
    path: "/path2",
    element: <Component2 />,
  },
];

export default routes;

简单不?然后在 App.jsx 里面引入就行了:

import React from 'react'
import { useRoutes } from 'react-router-dom'
import routes from './routes'

export default function App() {
  // 直接就把刚刚写的那个路由表传进参数了
  let route = useRoutes(routes);

  return (
    <div>
      {route}
    </div>
  )
}

我们上面的方法是直接更改地址栏,那有没有能够更改地址栏的链接呢(这不废话)没错,就是用Link。Link 其实跟原生的 a 链接一模一样,只是改了个名字,a 的 href 属性变成了 to 属性。不信看下面的示例:

import React from 'react'
import Component1 from './pages/Component1'
import Component2 from './pages/Component2'
import { useRoutes, Link } from 'react-router-dom'

export default function App() {
  let route = useRoutes([
    {
      path: "/path1",
      element: <Component1/>
    },
    {
      path: "/path2",
      element: <Component2/>
    }
  ])

  return (
    <div>
      <Link to="/path1">点我跳转 path1</Link><br/>
      <Link to="/path2">点我跳转 path2</Link><br/>
      {route}
    </div>
  )
}

果然,连样式都和原生 a 一模一样……

我们可以把 Link 和 Ant Design 中的 Menu 配合使用:

import React from 'react'
import { useRoutes, Link } from 'react-router-dom'
import routes from './routes'
import { Menu } from 'antd'

export default function App() {
  let route = useRoutes(routes)

  return (
    <div>
      {/* 懒得用 getItem,直接写把…… */}
      <Menu mode='horizontal' items={
        [
          {
            key: 1,
            label: <Link to="/path1">点我跳转 path1</Link>
          },
          {
            key: 2,
            label: <Link to="/path2">点我跳转 path2</Link>
          }
        ]
      } />
      {route}
    </div>
  )
}

立马脱胎换骨~

那还有一个 NavLink 是啥?它可以让你在点击该链接时,给链接的 className 加上一个 active。这对于一些样式很有用(因为它匹配 active 类名然后给它加样式),比如 Bootstrap。它还可以把上文所述的 active 换成其他的 class,语法如下:

import React from 'react'
import { NavLink, useRoutes } from 'react-router-dom'
import routes from './routes'

export default function App(){
  let route = useRoutes(routes)
  let computedClassName = ({isActive}) => { // isActive 接收到该 NavLink 是否激活了(其所指向的链接是否正在展示)
    return isActive ? "activeNow" : "" // 如果 isActive 为真那么 className 为 activeNow,反之为空
  }

  return (
    <div>
      <NavLink className={computedClassName}>A NavLink</NavLink>
      {route}
    </div>
  )
}

7.4 Navigate 组件与 useNavigate

其实 React Router 最核心的功能已经结束了,但是还有很多高级亿点的芝士点。这里先来 Navigate 组件。

Navigate 组件是用来实现自动化的页面跳转的。只要他被渲染到屏幕上,页面立马跳转到 Navigate 指定的的页面。它也可以实现另外一种功能:默认页面。比如你打开一个网站,它立马给你跳到某个界面,这也可以用 Navigate 实现。

Navigate 的语法非常简单,就是一个 <Navigate/> 指定一个参数 to,和 <Link/> 里面的 to 是一样的。比如我们这里用 Navigate 实现默认页面,在路由表里把 <Navigate/> 组件和 "/" 路径绑定:

import Component1 from '../pages/Component1'
import Component2 from '../pages/Component2'
import { Navigate } from 'react-router-dom'

let routes = [
  {
    path: "/path1",
    element: <Component1 />,
  },
  {
    path: "/path2",
    element: <Component2 />,
  },
  {
    path: "/", // 默认路径为 /
    element: <Navigate to="/path2" />
  }
];

export default routes;

打开 localhost:3000/ 你应该会发现自动跳转到了 /path2,其中你观察不到按钮有任何的点击,有 Ant Design 样式为证~

那我们如果不想把 Navigate 挂载到网页上,想用代码来跳转网页呢?很简单,使用一个新 Hook,useNavigate,传递的参数为 to 的值就可以。但是这里注意 useNavigate() 的返回值是一个函数,它才是真正可以跳转网页的工具。所以我们一般会建个变量保存 useNavigate 的返回值。比如这个:

import React from 'react'
import { useRoutes, Link, useNavigate } from 'react-router-dom'
import routes from './routes'
import { Menu, Button } from 'antd'

export default function App() {
  let route = useRoutes(routes)
  let navigate = useNavigate();

  return (
    <div>
      <Menu mode='horizontal' items={
        [
          {
            key: 1,
            label: <Link to="/path1">点我跳转 path1</Link>
          },
          {
            key: 2,
            label: <Link to="/path2">点我跳转 path2</Link>
          }
        ]
      } />
      {route}
      <Button onClick={() => navigate("/path1")}>点我跳转 path1</Button>
    </div>
  )
}

上效果:

Navigate 其实有一种不同的跳转方式,那就是 replace 模式跳转。上面不是说了 React Router history 其实是一个栈吗?默认的跳转模式(push模式)其实就是把一个新页面压栈(把它“推”进栈的最上方)replace 则不同,它采用了把栈顶的元素替换成新元素的方式。就是把栈顶“替换”成 Navigate 所指向的元素。

那怎么设置 Navigate 跳转呢?对于 Navigate 组件,直接加上一个 replace 属性就行了:

<Navigate replace to="/path2" />

诚不欺我,居然是真的(历史记录往前翻就到新建页面那去了,如果不加 replace 是不会的)

 

那 useNavigate 怎么实现呢?其实也不难。按照下面来:

{/*  多传一个配置对象,里面 replace: true 就行 */}
<Button onClick={() => navigate("/path1", {replace: true})}>点我跳转 path1</Button>

效果一样的~ 

7.5 多级路由

有的时候只是一级的路由没有办法满足我们的需求,这时候就需要用到多级路由了。这里提一个需求,比方说我们想在 component1 的里面显示两个不同的组件(使用路由来切换),那怎么办?

手动忽略样式,谢谢您的配合❤

 要演示这个场景,我们需要两个组件:SubComp1 和 SubComp2。还是象征性建两个:

要实现多级路由,我们需要在路由表和 Component1 组件两个方面下手。路由表简单,使用 children 属性(与 Cascader 有点相似),属性的值是一个数组,数组里面放一些二级路由的路由表。

import Component1 from '../pages/Component1'
import Component2 from '../pages/Component2'
import SubComp1 from '../pages/SubComp1'
import SubComp2 from '../pages/SubComp2'
import { Navigate } from 'react-router-dom'

let routes = [
  {
    path: "/path1",
    element: <Component1 />,
    children: [
        {
            path: "/path1/subpath1",
            element: <SubComp1 />
        },
        {
            path: "/path1/subpath2",
            element: <SubComp2 />
        }
    ]
  },
  {
    path: "/path2",
    element: <Component2 />,
  },
  {
    path: "/", // 默认路径为 /
    element: <Navigate to="/path2" />
  }
];

export default routes;

OK,现在 React Router 把路由和组件对上号了,开始往 Component1 里面加 Link 了。和 App 如出一辙,如下:

import React from 'react'
import { Link } from 'react-router-dom'
import { Menu } from 'antd'

export default function App() {
  return (
    <div>
      <Menu mode='horizontal' items={
        [
          {
            key: 1,
            label: <Link to="/path1/subpath1">点我跳转 subpath1</Link>
          },
          {
            key: 2,
            label: <Link to="/path1/subpath2">点我跳转 subpath2</Link>
          }
        ]
      } />
    </div>
  )
}

接下来,就是最后一个问题:二级路由的组件放在哪里?很明显是放在下面,但是,怎么放啊?一级路由我们有 useRoutes 的返回值,二级路由呢?这里我们就只能用到 React Router 给我们提供的另一个组件:Outlet。我们通过把 Outlet 组件(不需要传任何参数,Router 会自动识别)渲染到一个位置,就相当于间接实现了把二级路由的渲染放到了此处。如下:

import React from 'react'
import { Link, Outlet } from 'react-router-dom'
import { Menu } from 'antd'

export default function App() {
  return (
    <div>
      <Menu mode='horizontal' items={
        [
          {
            key: 1,
            label: <Link to="/path1/subpath1">点我跳转 subpath1</Link>
          },
          {
            key: 2,
            label: <Link to="/path1/subpath2">点我跳转 subpath2</Link>
          }
        ]
      } />
      <Outlet />
    </div>
  )
}

效果杠杠滴~

7.6 路由传参

这一小节我们将学习 React Router 最难的一个点:路由传参。它的意义其实不复杂,就是给路由组件传递参数,就有点儿类似于 props。但怎么做到这一点,就有点儿难度了。一般来说,路由传参有 3 种方法:params,search,state。下面我们分点讲解。

7.6.1 params

params 是路由传参最常用的方法,所以我们先从 params 开始。首先我们把二级路由的信息删掉,让它保持清爽。那个定向的 button 也删掉,干干净净~

然后我们有一个需求:让 App 给 Component1 传递一个参数 data,然后让 Component1 接住它,并显示出来。这咋办呢?毫无疑问,使用上面所说的 params。

params 是链接上的一个附加信息,比如我现在正在写的文章,这些东西就是附加信息,它可以帮助页面获取想要的结果~

使用 params 有三步。第一步,给附加信息留一个位置,让传递的数据可以通过这个位置传进去。这个位置的语法是 :参数名。如下:

import Component1 from '../pages/Component1'
import Component2 from '../pages/Component2'
import { Navigate } from 'react-router-dom'

let routes = [
  {
    // 如果你愿意,还可以留多个位置,就像 "/path1/:data/:title/:content"
    path: "/path1/:data",
    element: <Component1 />,
  },
  {
    path: "/path2",
    element: <Component2 />,
  },
  {
    path: "/", // 默认路径为 /
    element: <Navigate to="/path2" />
  }
];

export default routes;

第二步,把数据传进去。这一步由 App 操作。步骤也很简单,把刚刚的那个 :data 换成真材实料就行了。如下:

import React from 'react'
import { useRoutes, Link } from 'react-router-dom'
import routes from './routes'
import { Menu } from 'antd'

export default function App() {
  let route = useRoutes(routes)

  return (
    <div>
      <Menu mode='horizontal' items={
        [
          {
            key: 1,
            {/* 这里就不用加冒号了 */}
            label: <Link to="/path1/someDataDataDataDataDataData">点我跳转 path1</Link>
          },
          {
            key: 2,
            label: <Link to="/path2">点我跳转 path2</Link>
          }
        ]
      } />
      {route}
    </div>
  )
}

第三步:Component1 接住它。这里我们要使用一个新的 Hook:useParams。它可以接住传递的参数。它的格式是一个对象,对象里面一个一个键值对就如 {data: "someData"} 的形式。所以我们可以通过解构赋值接住它。

import React from 'react'
import { useParams } from 'react-router-dom'

export default function App() {
  let { data } = useParams() // data 接住了传进来的参数

  return (
    <div>
      收到的 data:{data}
    </div>
  )
}

效果显著:

7.6.2 search

这一小节我们要来学习下一个方法:search 传参。这个方法使用了大家都熟悉的 query 形式(即 ?数据名=数据&数据名=数据这样子的):

使用 search 传参就两步:传递参数,接收参数,不需要留位置。所以位置那个部分给它删了~

import Component1 from '../pages/Component1'
import Component2 from '../pages/Component2'
import { Navigate } from 'react-router-dom'

let routes = [
  {
    path: "/path1",
    element: <Component1 />,
  },
  {
    path: "/path2",
    element: <Component2 />,
  },
  {
    path: "/", // 默认路径为 /
    element: <Navigate to="/path2" />
  }
];

export default routes;

第一步:传参。使用 search 传参非常简单,就直接传 ?data=someData 的形式就行了。如下:

import React from 'react'
import { useRoutes, Link } from 'react-router-dom'
import routes from './routes'
import { Menu } from 'antd'

export default function App() {
  let route = useRoutes(routes)

  return (
    <div>
      <Menu mode='horizontal' items={
        [
          {
            key: 1,
            label: <Link to="/path1/?data=someDataDataDataDataDataData">点我跳转 path1</Link>
          },
          {
            key: 2,
            label: <Link to="/path2">点我跳转 path2</Link>
          }
        ]
      } />
      {route}
    </div>
  )
}

第二步:接收参数。search 参数的接收方式就有点儿玄乎,设计的非常不银杏。首先我们还是来一个新 Hook:useSearchParams。然后我们要用接 state 一样的方法接住它:let [search, setSearch],其中后面的 setSearch 几乎不用,就是为了更改传进来的参数(比如这样:setSearch("data=theOtherData"))然后我们千辛万苦获得的 search,还需要通过一个方法:search.get("数据名") 才能获取到真实的数据。如下:

import React from 'react'
import { useSearchParams } from 'react-router-dom'

export default function App() {
  // 这里为了不报警告我就不接收 setSearch 了
  let [search] = useSearchParams()
  let data = search.get("data")

  return (
    <div>
      收到的 data:{data}
    </div>
  )
}

效果一样的~

7.6.3 state

看到这个传参名字,你是不是想到了 useState~ 但其实这俩只是撞名了,没有啥关系。而且 state 传参在链接上非常地干净,没有任何携带~

还是两步走。首先第一步就是传递 state 了。state 传参的过程非常方便,你想不到的方便,直接在 state 属性里边传一个对象,对象里面就写你要传的数据就行了~

import React from 'react'
import { useRoutes, Link } from 'react-router-dom'
import routes from './routes'
import { Menu } from 'antd'

export default function App() {
  let route = useRoutes(routes)

  return (
    <div>
      <Menu mode='horizontal' items={
        [
          {
            key: 1,
            // 因为传的是对象所以要用双层花括号包裹,外边的花括号是 JSX 引入,里边的花括号是对象
            label: <Link to="/path1/" state={{data: "someDataDataDataDataDataData"}}>点我跳转 path1</Link>
          },
          {
            key: 2,
            label: <Link to="/path2">点我跳转 path2</Link>
          }
        ]
      } />
      {route}
    </div>
  )
}

拿到 state 传递的数据也非常方便。我们还是使用一个新 Hook:useLocation。它的返回值是一个对象,里面就有我们想要的 State 值。所以我们可以通过解构赋值拿出它。然后拿到的 state 直接就是我们传入的 state 了。

import React from 'react'
import { useLocation } from 'react-router-dom'

export default function App() {
  let { state } = useLocation()

  return (
    <div>
      收到的 data:{state.data}
    </div>
  )
}

上效果:

7.7 React Router 中的其它 Hooks

到这儿 React Router 最主要的内容就差不多结束了,但是还剩四个零散的 Hooks 需要讲解~

(果然 React Router 的 Hooks 是真的多,useRoutes useNavigate useParams useSearchParams useLocation 完了还有四个)

7.7.1 useInRouterContext

这个 Hook 属实有点儿鸡肋,它是用来获取某一个组件有没有被 Router 包裹的。它返回一个布尔值。但是我们一整个 App 都被 BrowserRouter 包了,还需要判断吗……

useInRouterContext() -> bool

7.7.2 useNavigationType

它用来获取用户来到页面的方法。比如你是 push 来的就输出 push,你是 replace 来的就输出 replace,你是 pop 来的……等等,pop 是啥啊?我们好像没学过这种方法啊。

pop 其实也很好理解,如果你是刷新页面来的(点了浏览器的刷新键),这种情况就是 pop 来的。

我们可以尝试打印一下来的方法:

import React from 'react'
import { useRoutes, Link, useNavigationType } from 'react-router-dom'
import routes from './routes'
import { Menu } from 'antd'

export default function App() {
  let route = useRoutes(routes)
  console.log(useNavigationType())

  return (
    <div>
      <Menu mode='horizontal' items={
        [
          {
            key: 1,
            label: <Link to="/path1/" state={{data: "someDataDataDataDataDataData"}}>点我跳转 path1</Link>
          },
          {
            key: 2,
            label: <Link to="/path2">点我跳转 path2</Link>
          }
        ]
      } />
      {route}
    </div>
  )
}

可以看到第一次是通过 Navigate 组件(即默认 push 模式)来的~

你刷新一下网页,它立马变成了 POP~

7.7.3 useOutlet

这和上面的 useInRouterContext 一样,都是鸡肋 Hook。它可以返回你通过 Outlet 放上去的组件,即子级组件。由于用得太少这里不再叙述。

7.7.4 useResolvedPath

顾名思义,它是一个可以帮助你分析一个链接里面东西的 Hook,比如"somepath/?data=1#sth"这个链接,它可以帮你拆成三个部分:链接部分 somepath,query 部分 ?data=1,锚链接即 Hash 部分 #sth。我们可以试一下:

console.log(useResolvedPath("somepath/?data=1#sth"))

可以看见它非常尽职尽责~

8. 练习 2:TransBox 翻译盒

8.1 练习简介

也不卖关子了,这个练习就是我上次跟你们提到的翻译软件~

可以看看效果~

这是我在 GitHub 上面的一个小小的开源项目(如果你是大佬,欢迎批评指正),这里是网站地址,可以体验一下。源代码 GitHub 上面有~

可以实现 40+ 不同语言的互译

话不多说,我们开始实现~

8.2 组件拆分及准备

这个网页的组件拆分其实比井字棋还更简单,同学们有没有啥想法~

我的思路估计简单到颠覆三观,把这个网页全写在 App 里面,里面的首页和关于分别两个路由组件,就像这样:

 

接下来我们要做一些准备工作。把 src 文件夹全部删了。再重新建一个。然后把 index.js App.jsx 和里面的两个小组件的框架先张罗好~

 

 

然后清理一下 public 文件夹,清到只剩最基本的结构,然后把 favicon.ico 换成 TransBox 的小 Logo~(ico 文件链接:favicon.zip - 蓝奏云

 然后请确保 axios antd react-router-dom 三个库都有正常安装,我们正式开始~

(不知不觉又水了一个小节~)

8.3 网页布局

有没有发现这个网页有的,市面上的其它网页也有,比如页头,主体和页尾~

某团主页

 这就涉及到了一个网站的布局。一般来说,一个网站都有 Header,Content,Footer 三个部分。

那我们怎么去定义这些布局呢?Ant Design 有给我们提供现成的模板:布局 Layout - Ant Design

里面有很多模板供我们选择,比如你选中了一个模板:

你可以把它的源代码(一定要选择 JavaScript,不能选择 TypeScript,因为我们写的是 JavaScript)复制到你的代码中,整个拷贝,不用缴费~ 为了方便大家,我把代码贴下面(对导出方式进行了一些微小的改动,但是大家应该也能看懂)

// App.jsx
import React from 'react';
import { Breadcrumb, Layout, Menu, theme } from 'antd';
const { Header, Content, Footer } = Layout;

export default function App() {
  const {
    token: { colorBgContainer },
  } = theme.useToken();
  return (
    <Layout className="layout">
      <Header>
        <div className="logo" />
        <Menu
          theme="dark"
          mode="horizontal"
          defaultSelectedKeys={['2']}
          items={new Array(15).fill(null).map((_, index) => {
            const key = index + 1;
            return {
              key,
              label: `nav ${key}`,
            };
          })}
        />
      </Header>
      <Content
        style={{
          padding: '0 50px',
        }}
      >
        <Breadcrumb
          style={{
            margin: '16px 0',
          }}
        >
          <Breadcrumb.Item>Home</Breadcrumb.Item>
          <Breadcrumb.Item>List</Breadcrumb.Item>
          <Breadcrumb.Item>App</Breadcrumb.Item>
        </Breadcrumb>
        <div
          className="site-layout-content"
          style={{
            background: colorBgContainer,
          }}
        >
          Content
        </div>
      </Content>
      <Footer
        style={{
          textAlign: 'center',
        }}
      >
        Ant Design ©2018 Created by Ant UED
      </Footer>
    </Layout>
  );
};

然后我们需要对这些代码进行一些删减和改动。首先,那个 useToken 感觉没啥用,也没学过,干脆删了吧~ 下面 Breadcrumb 下面那个 Div 里面引了 useToken 生成的东西,我们把它一并删除~

import React from 'react';
import { Breadcrumb, Layout, Menu } from 'antd';
const { Header, Content, Footer } = Layout;

export default function App() {
  return (
    <Layout className="layout">
      <Header>
        <div className="logo" />
        <Menu
          theme="dark"
          mode="horizontal"
          defaultSelectedKeys={['2']}
          items={new Array(15).fill(null).map((_, index) => {
            const key = index + 1;
            return {
              key,
              label: `nav ${key}`,
            };
          })}
        />
      </Header>
      <Content
        style={{
          padding: '0 50px',
        }}
      >
        <Breadcrumb
          style={{
            margin: '16px 0',
          }}
        >
          <Breadcrumb.Item>Home</Breadcrumb.Item>
          <Breadcrumb.Item>List</Breadcrumb.Item>
          <Breadcrumb.Item>App</Breadcrumb.Item>
        </Breadcrumb>
        <div
          className="site-layout-content"
        >
          Content
        </div>
      </Content>
      <Footer
        style={{
          textAlign: 'center',
        }}
      >
        Ant Design ©2018 Created by Ant UED
      </Footer>
    </Layout>
  );
};

删掉之后,Content 的背景色没了:

我们给它加上去。为了美观,我们顺手给它加上一个毛玻璃效果(就是把透明度调至 0.7):

import React from 'react';
import { Breadcrumb, Layout, Menu } from 'antd';
const { Header, Content, Footer } = Layout;

export default function App() {
  return (
    <Layout className="layout">
      <Header>
        <div className="logo" />
        <Menu
          theme="dark"
          mode="horizontal"
          defaultSelectedKeys={['2']}
          items={new Array(15).fill(null).map((_, index) => {
            const key = index + 1;
            return {
              key,
              label: `nav ${key}`,
            };
          })}
        />
      </Header>
      <Content
        style={{
          padding: '0 50px',
        }}
      >
        <Breadcrumb
          style={{
            margin: '16px 0',
          }}
        >
          <Breadcrumb.Item>Home</Breadcrumb.Item>
          <Breadcrumb.Item>List</Breadcrumb.Item>
          <Breadcrumb.Item>App</Breadcrumb.Item>
        </Breadcrumb>
        <div
          className="site-layout-content"
          style={{
            backgroundColor: "rgba(255,255,255,0.7)"
          }}
        >
          Content
        </div>
      </Content>
      <Footer
        style={{
          textAlign: 'center',
        }}
      >
        Ant Design ©2018 Created by Ant UED
      </Footer>
    </Layout>
  );
};

效果不戳~

然后上面的那个 Header,我感觉换成浅色更好看,废话不多说,改~

import React from 'react';
import { Breadcrumb, Layout, Menu } from 'antd';
const { Header, Content, Footer } = Layout;

export default function App() {
  return (
    <Layout className="layout">
      <Header style={{
        backgroundColor: "white"
      }}>
        <div className="logo" />
        {/* 这里也要改成 light */}
        <Menu
          theme="light"
          mode="horizontal"
          defaultSelectedKeys={['2']}
          items={new Array(15).fill(null).map((_, index) => {
            const key = index + 1;
            return {
              key,
              label: `nav ${key}`,
            };
          })}
        />
      </Header>
      <Content
        style={{
          padding: '0 50px',
        }}
      >
        <Breadcrumb
          style={{
            margin: '16px 0',
          }}
        >
          <Breadcrumb.Item>Home</Breadcrumb.Item>
          <Breadcrumb.Item>List</Breadcrumb.Item>
          <Breadcrumb.Item>App</Breadcrumb.Item>
        </Breadcrumb>
        <div
          className="site-layout-content"
          style={{
            backgroundColor: "rgba(255,255,255,0.7)"
          }}
        >
          Content
        </div>
      </Content>
      <Footer
        style={{
          textAlign: 'center',
        }}
      >
        Ant Design ©2018 Created by Ant UED
      </Footer>
    </Layout>
  );
};

舒服了~

接着整改 Menu。Menu 放一个首页一个关于就行了,放那么多干嘛~ 还有正常的导航栏不是都应该在右边的吗,所以我们给它浮动到右边。

import React from 'react';
import { Breadcrumb, Layout, Menu } from 'antd';
const { Header, Content, Footer } = Layout;

export default function App() {
  return (
    <Layout className="layout">
      <Header style={{
        backgroundColor: "white"
      }}>
        <div className="logo" />
        {/* 这里也要改成 light */}
        <Menu
          theme="light"
          mode="horizontal"
          defaultSelectedKeys="home"
          style={{
            float: "right"
          }}
        >
          <Menu.Item key="home">首页</Menu.Item>
          <Menu.Item key="about">关于</Menu.Item>
        </Menu>
      </Header>
      <Content
        style={{
          padding: '0 50px',
        }}
      >
        <Breadcrumb
          style={{
            margin: '16px 0',
          }}
        >
          <Breadcrumb.Item>Home</Breadcrumb.Item>
          <Breadcrumb.Item>List</Breadcrumb.Item>
          <Breadcrumb.Item>App</Breadcrumb.Item>
        </Breadcrumb>
        <div
          className="site-layout-content"
          style={{
            backgroundColor: "rgba(255,255,255,0.7)"
          }}
        >
          Content
        </div>
      </Content>
      <Footer
        style={{
          textAlign: 'center',
        }}
      >
        Ant Design ©2018 Created by Ant UED
      </Footer>
    </Layout>
  );
};

 感觉还不搓,就是太窄了~

加上 padding~ (新建一个 App.css 在里面配置 padding,然后 App.jsx 引进来)

import React from 'react';
import { Breadcrumb, Layout, Menu } from 'antd';
import './App.css'
const { Header, Content, Footer } = Layout;

export default function App() {
  return (
    <Layout className="layout">
      <Header style={{
        backgroundColor: "white"
      }}>
        <div className="logo" />
        {/* 这里也要改成 light */}
        <Menu
          theme="light"
          mode="horizontal"
          defaultSelectedKeys="home"
          {/* 请注意这里增加了一个 className */}
          className='main-menu'
          style={{
            float: "right"
          }}
        >
          <Menu.Item key="home">首页</Menu.Item>
          <Menu.Item key="about">关于</Menu.Item>
        </Menu>
      </Header>
      <Content
        style={{
          padding: '0 50px',
        }}
      >
        <Breadcrumb
          style={{
            margin: '16px 0',
          }}
        >
          <Breadcrumb.Item>Home</Breadcrumb.Item>
          <Breadcrumb.Item>List</Breadcrumb.Item>
          <Breadcrumb.Item>App</Breadcrumb.Item>
        </Breadcrumb>
        <div
          className="site-layout-content"
          style={{
            backgroundColor: "rgba(255,255,255,0.7)"
          }}
        >
          Content
        </div>
      </Content>
      <Footer
        style={{
          textAlign: 'center',
        }}
      >
        Ant Design ©2018 Created by Ant UED
      </Footer>
    </Layout>
  );
};
.main-menu *{
    padding-left: 10px;
    padding-right: 10px;
}

效果显著~

返回去检查一下代码发现还有一个 div className = logo 没有删,由于这里不需要用到,我们一并删~

接下来往下走。首先,我们就两个组件,要什么面包屑啊?所以删~

import React from 'react';
import { Layout, Menu } from 'antd';
import './App.css'
const { Header, Content, Footer } = Layout;

export default function App() {
  return (
    <Layout className="layout">
      <Header style={{
        backgroundColor: "white"
      }}>
        {/* 这里也要改成 light */}
        <Menu
          theme="light"
          mode="horizontal"
          defaultSelectedKeys="home"
          className='main-menu'
          style={{
            float: "right"
          }}
        >
          <Menu.Item key="home">首页</Menu.Item>
          <Menu.Item key="about">关于</Menu.Item>
        </Menu>
      </Header>
      <Content
        style={{
          padding: '0 50px',
        }}
      >
        <div
          className="site-layout-content"
          style={{
            backgroundColor: "rgba(255,255,255,0.7)"
          }}
        >
          Content
        </div>
      </Content>
      <Footer
        style={{
          textAlign: 'center',
        }}
      >
        Ant Design ©2018 Created by Ant UED
      </Footer>
    </Layout>
  );
};

加上 margin-top 往上留个空位~

import React from 'react';
import { Layout, Menu } from 'antd';
import './App.css'
const { Header, Content, Footer } = Layout;

export default function App() {
  return (
    <Layout className="layout">
      <Header style={{
        backgroundColor: "white"
      }}>
        <div className="logo" />
        {/* 这里也要改成 light */}
        <Menu
          theme="light"
          mode="horizontal"
          defaultSelectedKeys="home"
          className='main-menu'
          style={{
            float: "right"
          }}
        >
          <Menu.Item key="home">首页</Menu.Item>
          <Menu.Item key="about">关于</Menu.Item>
        </Menu>
      </Header>
      <Content
        style={{
          padding: '0 50px',
        }}
      >
        <div
          className="site-layout-content"
          style={{
            backgroundColor: "rgba(255,255,255,0.7)",
            marginTop: "50px"
          }}
        >
          Content
        </div>
      </Content>
      <Footer
        style={{
          textAlign: 'center',
        }}
      >
        Ant Design ©2018 Created by Ant UED
      </Footer>
    </Layout>
  );
};

这下正常了:

最后把 Footer 的文字调一下就行:

import React from 'react';
import { Layout, Menu } from 'antd';
import './App.css'
const { Header, Content, Footer } = Layout;

export default function App() {
  return (
    <Layout className="layout">
      <Header style={{
        backgroundColor: "white"
      }}>
        <div className="logo" />
        {/* 这里也要改成 light */}
        <Menu
          theme="light"
          mode="horizontal"
          defaultSelectedKeys="home"
          className='main-menu'
          style={{
            float: "right"
          }}
        >
          <Menu.Item key="home">首页</Menu.Item>
          <Menu.Item key="about">关于</Menu.Item>
        </Menu>
      </Header>
      <Content
        style={{
          padding: '0 50px',
        }}
      >
        <div
          className="site-layout-content"
          style={{
            backgroundColor: "rgba(255,255,255,0.7)",
            marginTop: "50px"
          }}
        >
          Content
        </div>
      </Content>
      <Footer
        style={{
          textAlign: 'center',
        }}
      >
        {/* 这里可以换成你的名字~ */}
        TransBox v0.1, Designed by Copcin
      </Footer>
    </Layout>
  );
};

大功告成~

网页布局小 tip:如果你自己也要写一个网站,就上 Ant Design 找布局,然后像上面一样对症下药进行微调,最后调出你满意的布局就可以啦~

8.4 路由功能配置

接下来我们要给网页加上一点儿“有用的”,就是让 Content 那个地方能显示出一点东西来。这个地方一眼就可以看出来需要使用 Router 配置路由表实现网页的切换,所以话不多说,开始配置路由表~

老样子,新建 routes/index.js,然后在里面配置:

import About from "../pages/About";
import Main from "../pages/Main";
import { Navigate } from 'react-router-dom'
 
let routes = [
  {
    path: "/home",
    element: <Main />,
  },
  {
    path: "/about",
    element: <About />,
  },
  {
	path: "/",
	element: <Navigate to="/home"/>
  }
];

export default routes;

然后在 App 里面引入:

import React from 'react';
import { Layout, Menu } from 'antd';
import { useRoutes } 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 (
    <Layout className="layout">
      <Header style={{
        backgroundColor: "white"
      }}>
        <div className="logo" />
        {/* 这里也要改成 light */}
        <Menu
          theme="light"
          mode="horizontal"
          defaultSelectedKeys="home"
          className='main-menu'
          style={{
            float: "right"
          }}
        >
          <Menu.Item key="home">首页</Menu.Item>
          <Menu.Item key="about">关于</Menu.Item>
        </Menu>
      </Header>
      <Content
        style={{
          padding: '0 50px',
        }}
      >
        <div
          className="site-layout-content"
          style={{
            backgroundColor: "rgba(255,255,255,0.7)",
            marginTop: "50px"
          }}
        >
          {/* 把 content 替换为货真价实的 route */}
          {route}
        </div>
      </Content>
      <Footer
        style={{
          textAlign: 'center',
        }}
      >
        TransBox v0.1, Designed by Copcin
      </Footer>
    </Layout>
  );
};

然后我们需要让 Menu 的两个按钮实现响应的功能,其实就是把文字换成 Link。如下:

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 (
    <Layout className="layout">
      <Header style={{
        backgroundColor: "white"
      }}>
        <div className="logo" />
        {/* 这里也要改成 light */}
        <Menu
          theme="light"
          mode="horizontal"
          defaultSelectedKeys="home"
          className='main-menu'
          style={{
            float: "right"
          }}
        >
          <Menu.Item key="home"><Link to="/home">首页</Link></Menu.Item>
          <Menu.Item key="about"><Link to="/about">关于</Link></Menu.Item>
        </Menu>
      </Header>
      <Content
        style={{
          padding: '0 50px',
        }}
      >
        <div
          className="site-layout-content"
          style={{
            backgroundColor: "rgba(255,255,255,0.7)",
            marginTop: "50px"
          }}
        >
          {/* 把 content 替换为货真价实的 route */}
          {route}
        </div>
      </Content>
      <Footer
        style={{
          textAlign: 'center',
        }}
      >
        TransBox v0.1, Designed by Copcin
      </Footer>
    </Layout>
  );
};

现在网页就能够正常用路由切换页面了。并且默认会跳转到 /home。

8.5 Main 组件:实现静态页面

很高兴地告诉你,正片终于开始了~

我们真正的翻译功能就是通过这个 Main 组件实现的。Main 组件的静态页面其实真的很简单,两个 Select,两个文本框:

我们也写两个 select 两个文本框试试看(Ant Design 官网都有使用说明)

import React from 'react'
import { Select, Input } from 'antd'

const { TextArea } = Input

export default function Main() {
  return (
    <div>
      源语言:<Select></Select>
      <TextArea placeholder="输入内容,按下 Enter 以翻译"></TextArea>
      目标语言:<Select></Select>
      <TextArea placeholder="翻译结果"></TextArea>
    </div>
  )
}

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

和上面的原型对比一下,没有对比就没有伤害

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

简单来说,就是使用 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 (
    <div>
      <Row>
        {/* 使用 span 属性来说明占的格数 */}
        <Col span={11}>
          源语言:<Select></Select>
          <TextArea placeholder="输入内容,按下 Enter 以翻译"></TextArea>
        </Col>
        {/* 留白 */}
        <Col span={2} />
        <Col span={11}>
          目标语言:<Select></Select>
          <TextArea placeholder="翻译结果"></TextArea>
        </Col>
      </Row>
    </div>
  )
}

立马看起来有模有样了~

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

首先是外边框 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 (
    <Layout className="layout">
      <Header style={{
        backgroundColor: "white"
      }}>
        <div className="logo" />
        {/* 这里也要改成 light */}
        <Menu
          theme="light"
          mode="horizontal"
          defaultSelectedKeys="home"
          className='main-menu'
          style={{
            float: "right"
          }}
        >
          <Menu.Item key="home"><Link to="/home">首页</Link></Menu.Item>
          <Menu.Item key="about"><Link to="/about">关于</Link></Menu.Item>
        </Menu>
      </Header>
      <Content
        style={{
          padding: '0 50px',
        }}
      >
        <div
          className="site-layout-content"
          style={{
            backgroundColor: "rgba(255,255,255,0.7)",
            marginTop: "50px",
            padding: "30px"
          }}
        >
          {/* 把 content 替换为货真价实的 route */}
          {route}
        </div>
      </Content>
      <Footer
        style={{
          textAlign: 'center',
        }}
      >
        TransBox v0.1, Designed by Copcin
      </Footer>
    </Layout>
  );
};

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

import React from 'react'
import { Select, Input, Row, Col } from 'antd'

const { TextArea } = Input

export default function Main() {
  return (
    <div>
      <Row>
        {/* 使用 span 属性来说明占的格数 */}
        <Col span={11}>
          源语言:<Select></Select>
          <TextArea rows={20} placeholder="输入内容,按下 Enter 以翻译"></TextArea>
        </Col>
        {/* 留白 */}
        <Col span={2} />
        <Col span={11}>
          目标语言:<Select></Select>
          <TextArea rows={20} placeholder="翻译结果"></TextArea>
        </Col>
      </Row>
    </div>
  )
}

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

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

import React from 'react'
import { Select, Input, Row, Col } from 'antd'

const { TextArea } = Input

export default function Main() {
  return (
    <div>
      <Row>
        <Col span={11}>
          源语言:<Select style={{
            marginBottom: "10px"
          }}></Select>
          <TextArea rows={20} placeholder="输入内容,按下 Enter 以翻译"></TextArea>
        </Col>
        <Col span={2} />
        <Col span={11}>
          目标语言:<Select style={{
            marginBottom: "10px"
          }}></Select>
          <TextArea rows={20} placeholder="翻译结果"></TextArea>
        </Col>
      </Row>
    </div>
  )
}

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

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,具体见这里,只有两个字符的简写才有效。

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

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

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,然后再引入它。如果一段代码很长,我们也建议单独建一个文件放它~

然后我们在 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 (
    <div>
      <Row>
        <Col span={11}>
          源语言:<Select style={{
            marginBottom: "10px"
          }}></Select>
          <TextArea rows={20} placeholder="输入内容,按下 Enter 以翻译"></TextArea>
        </Col>
        <Col span={2} />
        <Col span={11}>
          目标语言:<Select style={{
            marginBottom: "10px"
          }} options={transtable}></Select>
          <TextArea rows={20} placeholder="翻译结果"></TextArea>
        </Col>
      </Row>
    </div>
  )
}

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

连另一个 Select 一起调宽~

import React from 'react'
import { Select, Input, Row, Col } from 'antd'
import transtable from './transtable'

const { TextArea } = Input

export default function Main() {
  return (
    <div>
      <Row>
        <Col span={11}>
          源语言:<Select style={{
            marginBottom: "10px",
            width: "150px"
          }}></Select>
          <TextArea rows={20} placeholder="输入内容,按下 Enter 以翻译"></TextArea>
        </Col>
        <Col span={2} />
        <Col span={11}>
          目标语言:<Select style={{
            marginBottom: "10px",
            width: "150px"
          }} options={transtable}></Select>
          <TextArea rows={20} placeholder="翻译结果"></TextArea>
        </Col>
      </Row>
    </div>
  )
}

效果显著~

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

import React from 'react'
import { Select, Input, Row, Col } from 'antd'
import transtable from './transtable'

const { TextArea } = Input

export default function Main() {
  return (
    <div>
      <Row>
        <Col span={11}>
          源语言:<Select style={{
            marginBottom: "10px",
            width: "150px"
          }}></Select>
          <TextArea rows={20} placeholder="输入内容,按下 Enter 以翻译"></TextArea>
        </Col>
        <Col span={2} />
        <Col span={11}>
          目标语言:<Select style={{
            marginBottom: "10px",
            width: "150px"
          }} options={transtable} defaultValue="en"></Select>
          <TextArea rows={20} placeholder="翻译结果"></TextArea>
        </Col>
      </Row>
    </div>
  )
}

那我们怎么获取用户选择的值呢? 上面说过了使用 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 (
    <div>
      <Row>
        <Col span={11}>
          源语言:
          <Select
            style={{
              marginBottom: "10px",
              width: "150px",
            }}
          />
          <TextArea rows={20} placeholder="输入内容,按下 Enter 以翻译" />
        </Col>
        <Col span={2} />
        <Col span={11}>
          目标语言:
          <Select
            style={{
              marginBottom: "10px",
              width: "150px",
            }}
            options={transtable}
            defaultValue="en"
            onChange={(event) => setTransTo(event)}
          />
          <TextArea rows={20} placeholder="翻译结果" />
        </Col>
      </Row>
    </div>
  );
}

同理,把源语言 transSrc 也通过这种方法来加上~(这里由于源语言支持自动检测要加上一组键值对~)

import React, { useState } from "react";
import { Select, Input, Row, Col } from "antd";
import transtable from "./transtable";

let transtableWithAuto = transtable.concat([{ value: "", label: "自动检测" }]);

const { TextArea } = Input;

export default function Main() {
  let [transTo, setTransTo] = useState("en");
  let [transSrc, setTransSrc] = useState("");

  return (
    <div>
      <Row>
        <Col span={11}>
          源语言:
          {/* 默认自动检测 */}
          <Select
            style={{
              marginBottom: "10px",
              width: "150px",
            }}
            options={transtableWithAuto}
            defaultValue=""
            onChange={(event) => setTransSrc(event)}
          />
          <TextArea rows={20} placeholder="输入内容,按下 Enter 以翻译" />
        </Col>
        <Col span={2} />
        <Col span={11}>
          目标语言:
          <Select
            style={{
              marginBottom: "10px",
              width: "150px",
            }}
            options={transtable}
            defaultValue="en"
            onChange={(event) => setTransTo(event)}
          />
          <TextArea rows={20} placeholder="翻译结果" />
        </Col>
      </Row>
    </div>
  );
}

都齐了~

接着正式开始写翻译的代码。我的想法是在输入框按下 Enter 的时候触发翻译。所以加一个 onPressEnter 回调(记得清除默认行为)~ 在回调里面通过 event.target.value 就能获取到输入值了。我们就能在回调里面发起 axios 请求,从而获取翻译结果。

import React, { useState } from "react";
import { Select, Input, Row, Col } from "antd";
import axios from "axios";
import transtable from "./transtable";

let transtableWithAuto = [{ value: "", label: "自动检测" }].concat(transtable);

const { TextArea } = Input;

export default function Main() {
  let [transTo, setTransTo] = useState("en");
  let [transSrc, setTransSrc] = useState("");

  let translate = (event) => {
    event.preventDefault();
    let inputText = event.target.value;
    axios
      .get(
        `http://api.microsofttranslator.com/v2/Http.svc/Translate?appId=AFC76A66CF4F434ED080D245C30CF1E71C22959C&from=${transSrc}&to=${transTo}&text=${inputText}`
      )
      .then((response) => {
        // 这里我们尝试打印一下 response.data
        console.log(response.data);
      });
  };

  return (
    <div>
      <Row>
        <Col span={11}>
          源语言:
          {/* 默认自动检测 */}
          <Select
            style={{
              marginBottom: "10px",
              width: "150px",
            }}
            options={transtableWithAuto}
            defaultValue=""
            onChange={(event) => setTransSrc(event)}
          />
          <TextArea
            rows={20}
            placeholder="输入内容,按下 Enter 以翻译"
            onPressEnter={translate}
          />
        </Col>
        <Col span={2} />
        <Col span={11}>
          目标语言:
          <Select
            style={{
              marginBottom: "10px",
              width: "150px",
            }}
            options={transtable}
            defaultValue="en"
            onChange={(event) => setTransTo(event)}
          />
          <TextArea rows={20} placeholder="翻译结果" />
        </Col>
      </Row>
    </div>
  );
}

OHHHHHHHHHHHHHHHHHHHHHHH!

可以看到它返回的是一个 XML 字符串,这里需要甄别,虽然它长得像 XML,但它实际上是一个字符串。那我们怎么获取到里面的文字呢?这就有点儿考验你的 JS 功底了,这里我是用的截取第一个 > 字符和最后一个 < 字符中间的字符实现的~

let result = response.data.substring(
  response.data.indexOf(">") + 1, // 加 1 代表不截取 > 字符
  response.data.lastIndexOf("<")
)

最后一步就是把内容放进右边的框里了。这里还是记得:ref 获取不到。所以我们只能再建一个 state……(没有 ref 的生活真痛苦)

import React, { useState } from "react";
import { Select, Input, Row, Col } from "antd";
import axios from "axios";
import transtable from "./transtable";

let transtableWithAuto = [{ value: "", label: "自动检测" }].concat(transtable);

const { TextArea } = Input;

export default function Main() {
  let [transTo, setTransTo] = useState("en");
  let [transSrc, setTransSrc] = useState("");
  let [result, setResult] = useState();

  let translate = (event) => {
    event.preventDefault()
    let inputText = event.target.value;
    axios
      .get(
        `http://api.microsofttranslator.com/v2/Http.svc/Translate?appId=AFC76A66CF4F434ED080D245C30CF1E71C22959C&from=${transSrc}&to=${transTo}&text=${inputText}`
      )
      .then((response) => {
        setResult(
          response.data.substring(
            response.data.indexOf(">") + 1, // 加 1 代表不截取 > 字符
            response.data.lastIndexOf("<")
          )
        );
      });
  };

  return (
    <div>
      <Row>
        <Col span={11}>
          源语言:
          {/* 默认自动检测 */}
          <Select
            style={{
              marginBottom: "10px",
              width: "150px",
            }}
            options={transtableWithAuto}
            defaultValue=""
            onChange={(event) => setTransSrc(event)}
          />
          <TextArea
            rows={20}
            placeholder="输入内容,按下 Enter 以翻译"
            onPressEnter={translate}
          />
        </Col>
        <Col span={2} />
        <Col span={11}>
          目标语言:
          <Select
            style={{
              marginBottom: "10px",
              width: "150px",
            }}
            options={transtable}
            defaultValue="en"
            onChange={(event) => setTransTo(event)}
          />
          <TextArea rows={20} placeholder="翻译结果" value={result} />
        </Col>
      </Row>
    </div>
  );
}

不过好在能用了,效果:

大功告成~

8.8 About 组件

Main 组件写多了,是不是还忘记了有个 About 组件~ 总之这个小节把 About 组件结束掉~

About 组件是一些说明性文档,随便写写就行了~

import React from 'react'

export default function About() {
  return (
    <div>
        <h1>Intro | 简介</h1> 
        <p>
            这里是 TransBox 的简介界面。TransBox 是一个简洁的多语言翻译工具,使用必应翻译 API。<br/>
            选择目标语言并输入待翻译的文字之后,按下 Enter 即可完成翻译。<br/>
            TransBox 使用 React 实现。它是作者在 React 学习过程中开发的一个小应用。<br/>
            TransBox 目前还在测试版阶段,欢迎你报告 Bug 或是提出建议。<br/>
            项目 GitHub: <a rel="noreferrer" href="https://github.com/copws/transbox" target="_blank">transbox</a><br/>
            作者:<a rel="noreferrer" href="https://blog.csdn.net/raspi_fans" target="_blank">copcin</a>
        </p>
        <h1>Translation API | 翻译 API</h1>
        <p>
            如果你想了解我们使用的 API,可以参阅这一部分。如果它侵犯了您的权利,请联系删除。<br/>
            Transbox 使用了<a rel="noreferrer" href="https://www.cnblogs.com/fanyang1/p/9414088.html" target="_blank">该博客</a>
            提供的必应翻译 API。具体翻译选项如下:<br/>
            <p>http://api.microsofttranslator.com/v2/Http.svc/Translate?appId=AFC76A66CF4F434ED080D245C30CF1E71C22959C&from=transSrc&to=transTo&text=event.target.value</p>
            transSrc 和 transTo 为翻译源语言和目标语言。其中 transSrc 可以为空,表示自动检测语言。text 为待翻译的文本。<br/>
            transSrc 和 transTo 必须要为语言的两个字母的简称。
        </p>
    </div>
  )
}

看着还行~

(又双叒叕水了一小节~)

8.9 应用优化:样式部分

现在开始优化应用,这一小节我们要对一些显示的问题进行优化。

首先,第一点就是 TextArea 可以随意调整大小,看着很不爽:

这一点我们加一个 CSS resize: none 来实现~

import React, { useState } from "react";
import { Select, Input, Row, Col } from "antd";
import axios from "axios";
import transtable from "./transtable";

let transtableWithAuto = [{ value: "", label: "自动检测" }].concat(transtable);

const { TextArea } = Input;

export default function Main() {
  let [transTo, setTransTo] = useState("en");
  let [transSrc, setTransSrc] = useState("");
  let [result, setResult] = useState();

  let translate = (event) => {
    event.preventDefault();
    let inputText = event.target.value;
    axios
      .get(
        `http://api.microsofttranslator.com/v2/Http.svc/Translate?appId=AFC76A66CF4F434ED080D245C30CF1E71C22959C&from=${transSrc}&to=${transTo}&text=${inputText}`
      )
      .then((response) => {
        setResult(
          response.data.substring(
            response.data.indexOf(">") + 1, // 加 1 代表不截取 > 字符
            response.data.lastIndexOf("<")
          )
        );
      });
  };

  return (
    <div>
      <Row>
        <Col span={11}>
          源语言:
          {/* 默认自动检测 */}
          <Select
            style={{
              marginBottom: "10px",
              width: "150px",
            }}
            options={transtableWithAuto}
            defaultValue=""
            onChange={(event) => setTransSrc(event)}
          />
          <TextArea
            rows={20}
            placeholder="输入内容,按下 Enter 以翻译"
            onPressEnter={translate}
            style={{ resize: "none" }}
          />
        </Col>
        <Col span={2} />
        <Col span={11}>
          目标语言:
          <Select
            style={{
              marginBottom: "10px",
              width: "150px",
            }}
            options={transtable}
            defaultValue="en"
            onChange={(event) => setTransTo(event)}
          />
          <TextArea
            rows={20}
            placeholder="翻译结果"
            value={result}
            style={{ resize: "none" }}
          />
        </Col>
      </Row>
    </div>
  );
}

效果明显~ 想拖也拖不动了~

此外还有一个明显的 Bug,当你在翻译结果那栏敲击键盘的时候。你会发现:翻译结果竟然还可以编辑!所以要将翻译结果设置为只读~

import React, { useState } from "react";
import { Select, Input, Row, Col } from "antd";
import axios from "axios";
import transtable from "./transtable";

let transtableWithAuto = [{ value: "", label: "自动检测" }].concat(transtable);

const { TextArea } = Input;

export default function Main() {
  let [transTo, setTransTo] = useState("en");
  let [transSrc, setTransSrc] = useState("");
  let [result, setResult] = useState();

  let translate = (event) => {
    event.preventDefault();
    let inputText = event.target.value;
    axios
      .get(
        `http://api.microsofttranslator.com/v2/Http.svc/Translate?appId=AFC76A66CF4F434ED080D245C30CF1E71C22959C&from=${transSrc}&to=${transTo}&text=${inputText}`
      )
      .then((response) => {
        setResult(
          response.data.substring(
            response.data.indexOf(">") + 1, // 加 1 代表不截取 > 字符
            response.data.lastIndexOf("<")
          )
        );
      });
  };

  return (
    <div>
      <Row>
        <Col span={11}>
          源语言:
          {/* 默认自动检测 */}
          <Select
            style={{
              marginBottom: "10px",
              width: "150px",
            }}
            options={transtableWithAuto}
            defaultValue=""
            onChange={(event) => setTransSrc(event)}
          />
          <TextArea
            rows={20}
            placeholder="输入内容,按下 Enter 以翻译"
            onPressEnter={translate}
            style={{ resize: "none" }}
          />
        </Col>
        <Col span={2} />
        <Col span={11}>
          目标语言:
          <Select
            style={{
              marginBottom: "10px",
              width: "150px",
            }}
            options={transtable}
            defaultValue="en"
            onChange={(event) => setTransTo(event)}
          />
          <TextArea
            rows={20}
            readOnly
            placeholder="翻译结果"
            value={result}
            style={{ resize: "none" }}
          />
        </Col>
      </Row>
    </div>
  );
}

现在就编辑不了了~

还有最后一个问题:字体实在太大啦!尤其是打开关于界面的时候~

所以这里调小,非常直接~

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 (
    <Layout className="layout">
      <Header style={{
        backgroundColor: "white"
      }}>
        <div className="logo" />
        {/* 这里也要改成 light */}
        <Menu
          theme="light"
          mode="horizontal"
          defaultSelectedKeys="home"
          className='main-menu'
          style={{
            float: "right"
          }}
        >
          <Menu.Item key="home"><Link to="/home">首页</Link></Menu.Item>
          <Menu.Item key="about"><Link to="/about">关于</Link></Menu.Item>
        </Menu>
      </Header>
      <Content
        style={{
          padding: '0 50px',
        }}
      >
        <div
          className="site-layout-content"
          style={{
            backgroundColor: "rgba(255,255,255,0.7)",
            marginTop: "50px",
            padding: "30px",
            fontSize: "14px"
          }}
        >
          {/* 把 content 替换为货真价实的 route */}
          {route}
        </div>
      </Content>
      <Footer
        style={{
          textAlign: 'center',
        }}
      >
        TransBox v0.1, Designed by Copcin
      </Footer>
    </Layout>
  );
};

这下就舒服了~

8.10 应用优化:功能部分

这一部分的优化针对三个功能:当翻译结果还没出来时显示“加载中”,当你没有输入字符却按下 Enter 时提示“您好像没有输入字符”,当网络出现问题,请求失败时,显示请求失败。

我们先来第一个功能。其实这些功能有一个共同点,那就是需要改变翻译结果那个 TextArea 的值。这里还有一个可以让用户更好地读懂这些提示表示意义的工具:status 属性。它可以设置文本框边框的颜色,就像这样:

这里我们就设置了 status 属性为 error。如果你设置 status 属性为 warning,它会变成橙色~

我们这里的 Loading 就使用 warning 属性把,剩下的就不难了,代码如下:

import React, { useState } from "react";
import { Select, Input, Row, Col } from "antd";
import axios from "axios";
import transtable from "./transtable";

let transtableWithAuto = [{ value: "", label: "自动检测" }].concat(transtable);

const { TextArea } = Input;

export default function Main() {
  let [transTo, setTransTo] = useState("en");
  let [transSrc, setTransSrc] = useState("");
  let [result, setResult] = useState();
  // 把 status 保存至一个 state,status 为空代表没有属性
  let [status, setStatus] = useState("")

  let translate = (event) => {
    event.preventDefault();
    setStatus("warning")
    setResult("加载中……")
    let inputText = event.target.value;
    axios
      .get(
        `http://api.microsofttranslator.com/v2/Http.svc/Translate?appId=AFC76A66CF4F434ED080D245C30CF1E71C22959C&from=${transSrc}&to=${transTo}&text=${inputText}`
      )
      .then((response) => {
        setStatus("")
        setResult(
          response.data.substring(
            response.data.indexOf(">") + 1, // 加 1 代表不截取 > 字符
            response.data.lastIndexOf("<")
          )
        );
      });
  };

  return (
    <div>
      <Row>
        <Col span={11}>
          源语言:
          {/* 默认自动检测 */}
          <Select
            style={{
              marginBottom: "10px",
              width: "150px",
            }}
            options={transtableWithAuto}
            defaultValue=""
            onChange={(event) => setTransSrc(event)}
          />
          <TextArea
            rows={20}
            placeholder="输入内容,按下 Enter 以翻译"
            onPressEnter={translate}
            style={{ resize: "none" }}
          />
        </Col>
        <Col span={2} />
        <Col span={11}>
          目标语言:
          <Select
            style={{
              marginBottom: "10px",
              width: "150px",
            }}
            options={transtable}
            defaultValue="en"
            onChange={(event) => setTransTo(event)}
          />
          <TextArea
            rows={20}
            status={status}
            readOnly
            placeholder="翻译结果"
            value={result}
            style={{ resize: "none" }}
          />
        </Col>
      </Row>
    </div>
  );
}

重新打开网页,当你按下 Enter 时,你应该会看见先跳了一下加载中~ 如果在 Network 里面把网速调成 3G 会更明显~

 接着是第二个问题。当你什么都没有输入的时候,它默认会返回这个字符串:

如果用户看到这个字符串,估计已经一脸懵 B 了。所以这里加一个判断,如果返回的字符串是这个那么就提示你好像没有输入字符;

import React, { useState } from "react";
import { Select, Input, Row, Col } from "antd";
import axios from "axios";
import transtable from "./transtable";

let transtableWithAuto = [{ value: "", label: "自动检测" }].concat(transtable);

const { TextArea } = Input;

export default function Main() {
  let [transTo, setTransTo] = useState("en");
  let [transSrc, setTransSrc] = useState("");
  let [result, setResult] = useState();
  // 把 status 保存至一个 state,status 为空代表没有属性
  let [status, setStatus] = useState("");

  let translate = (event) => {
    event.preventDefault();
    setStatus("warning");
    setResult("加载中……");
    let inputText = event.target.value;
    axios
      .get(
        `http://api.microsofttranslator.com/v2/Http.svc/Translate?appId=AFC76A66CF4F434ED080D245C30CF1E71C22959C&from=${transSrc}&to=${transTo}&text=${inputText}`
      )
      .then((response) => {
        setStatus("");
        let result = response.data.substring(
          response.data.indexOf(">") + 1, // 加 1 代表不截取 > 字符
          response.data.lastIndexOf("<")
        );
        // 模板字符串的妙用:里面的单双引号不需要转义~
        if (result === `<string xmlns="http://schemas.microsoft.com/2003/10/Serialization/"/>`){
          // 抛错
          throw new Error("您好像没有输入字符")
        } else {
          setResult(result)
        }
      }).catch(() => { // 由于不需要用到原因这里就不接了,但是要知道 Promise.catch 是可以接 reason 的
        setStatus("error")
        setResult("您好像没有输入字符")
      })
  };

  return (
    <div>
      <Row>
        <Col span={11}>
          源语言:
          {/* 默认自动检测 */}
          <Select
            style={{
              marginBottom: "10px",
              width: "150px",
            }}
            options={transtableWithAuto}
            defaultValue=""
            onChange={(event) => setTransSrc(event)}
          />
          <TextArea
            rows={20}
            placeholder="输入内容,按下 Enter 以翻译"
            onPressEnter={translate}
            style={{ resize: "none" }}
          />
        </Col>
        <Col span={2} />
        <Col span={11}>
          目标语言:
          <Select
            style={{
              marginBottom: "10px",
              width: "150px",
            }}
            options={transtable}
            defaultValue="en"
            onChange={(event) => setTransTo(event)}
          />
          <TextArea
            rows={20}
            status={status}
            readOnly
            placeholder="翻译结果"
            value={result}
            style={{ resize: "none" }}
          />
        </Col>
      </Row>
    </div>
  );
}

效果阔以~

最后一个问题,当你的网络不好的时候点击翻译,它会直接报错,用户体验也很不好,所以这里如果 Axios 出错没法接到请求,也需要又相应的回调来告知用户。

import React, { useState } from "react";
import { Select, Input, Row, Col } from "antd";
import axios from "axios";
import transtable from "./transtable";

let transtableWithAuto = [{ value: "", label: "自动检测" }].concat(transtable);

const { TextArea } = Input;

export default function Main() {
  let [transTo, setTransTo] = useState("en");
  let [transSrc, setTransSrc] = useState("");
  let [result, setResult] = useState();
  // 把 status 保存至一个 state,status 为空代表没有属性
  let [status, setStatus] = useState("");

  let translate = (event) => {
    event.preventDefault();
    setStatus("warning");
    setResult("加载中……");
    let inputText = event.target.value;
    axios
      .get(
        `http://api.microsofttranslator.com/v2/Http.svc/Translate?appId=AFC76A66CF4F434ED080D245C30CF1E71C22959C&from=${transSrc}&to=${transTo}&text=${inputText}`
      )
      .then((response) => {
        setStatus("");
        let result = response.data.substring(
          response.data.indexOf(">") + 1, // 加 1 代表不截取 > 字符
          response.data.lastIndexOf("<")
        );
        // 模板字符串的妙用:里面的单双引号不需要转义~
        if (result === `<string xmlns="http://schemas.microsoft.com/2003/10/Serialization/"/>`){
          // 抛错
          throw new Error("您好像没有输入字符")
        } else {
          setResult(result)
        }
      }, 
      () => { // 这里其实原本也可以接 reason
        setStatus("error")
        setResult("网络罢工了……")
      }).catch(() => {
        setStatus("error")
        setResult("您好像没有输入字符")
      })
  };

  return (
    <div>
      <Row>
        <Col span={11}>
          源语言:
          {/* 默认自动检测 */}
          <Select
            style={{
              marginBottom: "10px",
              width: "150px",
            }}
            options={transtableWithAuto}
            defaultValue=""
            onChange={(event) => setTransSrc(event)}
          />
          <TextArea
            rows={20}
            placeholder="输入内容,按下 Enter 以翻译"
            onPressEnter={translate}
            style={{ resize: "none" }}
          />
        </Col>
        <Col span={2} />
        <Col span={11}>
          目标语言:
          <Select
            style={{
              marginBottom: "10px",
              width: "150px",
            }}
            options={transtable}
            defaultValue="en"
            onChange={(event) => setTransTo(event)}
          />
          <TextArea
            rows={20}
            status={status}
            readOnly
            placeholder="翻译结果"
            value={result}
            style={{ resize: "none" }}
          />
        </Col>
      </Row>
    </div>
  );
}

这样如果你在打开网页之后断网的话,它应该就会提示你网络罢工了~

8.11 小结

恭喜!TransBox 已经开发完成了~

虽然这一小节的新芝士点不多,但是也稍微整理一下(大部分都是关于 Ant Design 的)

首先是网页布局,布局一个网页,完全不需要自己写一个,Ant Design 提供了很多成型的模板,我们可以直接用,改亿点细节就行了~

然后是栅格布局 Row Col 的使用。我们可以通过 Row 把页面等分为 24 块,然后用 Col 指定哪几个组件应该显示在哪几块~

然后还有一个小~~~~芝士:网页节流,可以帮助你模拟一些场面,比如网速慢,没网(不适用于 Firefox)的场面~

然后就没了~ React.js 完结撒花~

9. 后记

9.1 把你的项目部署在 GitHub Pages

大家有没有注意到,整篇文章都完了,都没有讲如何生成生产版 React 项目~ 这里连 GitHub Pages 部署项目一起讲~

生成生产版项目非常简单,一行代码:npm run build。然后 NPM 就会把你一整个项目包括你的依赖一起打包成纯 HTML + CSS + JS 项目,就像这样:

当然这个还不能直接用,我们需要把它部署在一台服务器上。对于像我这样的没有服务器的学生党,GitHub Pages 肯定是最佳的选择了~

要部署在 GitHub 上,你需要确定你这个文件夹已经是一个 GitHub 的开源项目。如果还不是,安装一下 Git,VS Code 登录一下 GitHub,然后按下图操作(这是最简单的面向像我这样一点也不会 Git 的人的方法,如果你会 Git 那自己 fly~):

 准备好之后,安装一个扩展 gh-pages,使用 npm install gh-pages~

然后配置发布的信息。打开 package.json,加入一个配置 homepage,值就填你将要部署到的 GitHub Pages 页面(命名规范是你的用户名.github.io/项目名)。比如 copws.github.io/transbox。如果这一步你填的路径 / 号后面还有数据,比如 /transbox,那么你所有 React Router 的配置(路由表 Link useNavigate 等等)就都需要加上一个 /transbox,要不然你可能会看到 React Router 把你的 /transbox 弄丢了,就像 copws.github.io/home。

然后在 scripts 栏加上两行配置(一个字也不能动):

 然后输入 npm run deploy 即可一键完成发布(可能需要能正常访问 GitHub 的科学的网络环境不然你会疯……)

然后输入你在 Homepage 里面填写的链接,大功告成~

9.2 附言

首先,本文章基于全国最最最最最最好的 React 视频教程:链接,一点也不过分~ 不过如果你开着原速听会感觉老师在讲相声,我是全程开着 2.0x 听完的。由于本文乃至本专栏的写作目标就是让像我一样的写啥啥不行折腾第一名的个人开发者能用上 React 写上网页,所以 Redux 这个部分没有讲到,想学视频课里有~

其次,其实作者本人的 React 水平也属于菜鸟级别,最多写写 TransBox 这种级别的应用,又是第一次写这种知识性的专栏,如果文章有大小错误,请各位大佬喷轻一点(T_T)

这三个月基本没有更新,全在折腾这篇 11 万字的文章了,前端这个坑也差不多完结撒花了~

最后,兔🐇年快乐~

(PS. 写文章千万不能用 CSDN 自带的那个富文本编辑器,太卡了,换了三个浏览器,直接吐血……)

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值