阅读 394

React+React Router入门 --- 人员管理实例

前言

码云Gitee 源码链接

如果你想学React,但是看了半天没有好一点的上手demo,请看这里 =>

想学个React,满大街的todo-list,让我不免有点心累。



想到我去年同期自学Vue的时候,还有个小增删改查的小例子。于是灵机一动,掏出了去年的那个小教程(Vue + Vue Router)。

为何不复刻一个React版呢?


这里贴一下本文灵感来源,感谢这篇文章把我带上前端的道路 Vue版人员管理系统 链接 。

由于其他原因,Vue版的图片就不贴了。自行点开上面的连接看一看吧


先上我们React版的动图



主要有以下功能:

  • 首页(index) 和 管理界面(mannger) 的简单路由切换
  • 人员列表显示
  • 新增人员
  • 删除指定的人员
  • 空数据判断

OK,那我们从创建项目开始,一步一步来


创建项目

运行环境:

  • Node.js
  • Webpack
  • React
  • React-Router
  • npm (这个会和Node.js捆绑安装)

首先,我们的Node.js环境不可少,没有的请去nodejs官网安装一下。推荐LTS版最好

安装完毕Node.js后,打开cmd命令行窗口。输入 node -v 和 npm -v

如果如下图所示,说明安装成功



接着我们开始配置React开发环境:

在这里,我们使用Facebook 官方推荐的React脚手架 - create-react-app。

这个脚手架已经做好了基础 webpack 配置,带有自动更新,错误提示等等功能,仅仅需要创建,启动就可以快速开发。


控制台输入 npm install -g create-react-app  

如果网络环境不好,请用淘宝的cnpm进行安装


创建一个React项目

create-react-app manager复制代码

其中,create-react-app 是命令名。manager 是项目名

如果控制台提示 Happy hacking  说明环境安装完毕


这时候,我们可以用IDE打开这个项目文件夹啦!


目录结构 & 环境搭建

首先我们先看一下目录:


  • node_modules 这个文件夹是你开发环境中的一些外部依赖,如Webpack React等。
  • public 这个文件夹防止了一些公共的东西。如HTML模板和网页小图标等
  • src 是代码目录。我们主要修改这里


下面我们来看看package.json 里的内容

重点关注 scripts 字段内的内容

"scripts": {
  "start": "react-scripts start",
  "build": "react-scripts build",
  "test": "react-scripts test",
  "eject": "react-scripts eject"
},复制代码

“start” 这个命令,就是在你的本地启动一个dev服务器。然后可以预览代码结果

“build” 这个命令,会把源码文件编译打包后输出,方便发布到服务器。

“test” 这个命令,会对源码进行断言测试。

“eject” 这个命令,有点像“弹出”的意思。你现在看到的配置,都是简化后的配置。如果想自定义配置(如Webpack),请执行这个命令。不过值得注意的是:此命令执行后,操作无法回退


我们现在执行 yarn run eject  这个命令


在这里先普及一下yarn 这个工具 

Yarn与 npm 一样,是一款 NodeJS 包管理工具。 为何要选择使用 yarn 呢?官网的描述是:

Yarn 会缓存它下载的每个包,所以不需要重复下载。它还能并行化操作以最大化资源利用率,所以安装速度之快前所未有。

Yarn 在每个安装包的代码执行前使用校验码验证包的完整性。

Yarn 使用一个格式详尽但简洁的 lockfile 和一个精确的算法来安装,能够保证在一个系统上的运行的安装过程也会以同样的方式运行在其他系统上。

选用 Yarn 的原因也是因为他的速度提升比npm 要快,使用yarn add <package-name>yarn remove <package-name> 增删 node 包(对应npm installnpm uninstall).


开始后他会提示你 “此操作不可逆,请谨慎操作”。想好了请输入Y

如果错误列出了部分文件,请关闭IDE,删除掉目录下的.git 目录和ide创建的文件。用CMD执行一遍


执行完毕后,我们会发现 script 命令变成了:

"scripts": {
  "start": "node scripts/start.js",
  "build": "node scripts/build.js",
  "test": "node scripts/test.js"
},复制代码

这下都是node原生命令了,目录也多了个config文件夹,config文件夹内主要是一些webpack的配置。在这里不多解释


接下来我们尝试跑一下。命令行输入:yarn run start

然后会抛出一个错误:

 Cannot find module '@babel/plugin-transform-react-jsx'

这个问题是因为eject过程中的出错。

解决方案是删除掉node_modules文件夹,用 npm install 重新安装一遍。在这里我用的是cnpm


如果我们看到下图,说明我们的开发环境搭建完毕。



开始上手


改造下原本的脚手架模板

首先,我们项目中不需要单元测试和PWA。可以把 src

 
下面的 App.test.js serviceWorker.js 删掉(自行了解PWA)


index.js 改造后

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));复制代码

我们引入React,ReactDom是React的基础组件,App是代表根组件。

render方法的第一参数接收一个根组件,第二参数接受一个需要挂载的html节点。组件编译后的内容就会加载在html节点内,其他的什么都不需要引入。


我们在src下新建两个文件夹,名称分别是“index”和“manager”。分别代表我们的两个路由

里面也分别新建两个同名的js和css文件。分别代表两个路由组件和CSS。就是这样子的


接着把index.css也删除掉,只留下App.css一个做全局公共样式就好

app.css 内暂时写以下内容:

* {
    margin: 0 auto;
    padding: 0;
}

a {
    text-decoration: none !important;
}复制代码

去除一下网页边框的 margin 和 padding。所有a标签去除下划线


App.js也很简单。只留下一个React组件模板

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

class App extends Component {
  render() {
    return (
      <div className="App">
      </div>
    );
  }
}

export default App;
复制代码



绘制底部导航栏和整体布局

我们在App.js 内,把我们的React logo引入进来。因为React的logo是两个路由公共的东西

import logo from "./logo.svg";复制代码

下面的也改成这个布局:

class App extends Component {
    render() {
        return (
            <div className="App">
                <img src={logo} alt=""/>
                <div className="tab-bar">
                    <a className="tab-bar__active">首页</a>
                    <a>人员管理</a>
                </div>
            </div>
        );
    }
}复制代码

上面一个React的Logo,然后下面是一个底部导航栏

修改一下app.css

* {
    margin: 0 auto;
    padding: 0;
}

a {
    text-decoration: none !important;
}

.App {
    text-align: center;
}

.App img {
    width: 300px;
}

.tab-bar {
    display: flex;
    flex-direction: row;
    justify-content: center;
    align-items: center;
    position: fixed;
    bottom: 0;
    width: 100%;
}

.tab-bar a {
    flex: 1;
    padding: 16px;
    background-color: #F1F1F1;
    color: #000;
    text-decoration: none;
}

.tab-bar__active {
    background-color: #737171 !important;
    color: #61DAFB !important;
}
复制代码


接着可以看到我们修改后的样式布局


但这时候我们还没有用路由。路由打算最后的时候再使用


绘制首页模块

首页组件很简单,其实只有一行字。但我们也要去写,要不然没法展现出路由的用处

index.css里面是空的

index.js

import React, {Component} from 'react';
export default class Index extends Component {
    render() {
        return (
            <div className="index">
                <span>欢迎来到人员管理系统React版</span>
            </div>
        )
    }
}复制代码


接着我们把封装的index组件,引入到App.js内看看

import Index from './index/index.jsx'
复制代码
// App.js
class App extends Component {
    render() {
        return (
            <div className="App">
                <img src={logo} alt=""/>
                <Index/>
                <div className="tab-bar">
                    <a className="tab-bar__active">首页</a>
                    <a>人员管理</a>
                </div>
            </div>
        );
    }
}复制代码


封装的Index组件就会实时更新在页面上:




绘制管理模块

接下来~~~我们的重头戏就来了:管理模块的编写。整个demo的核心都在这里

我们先写查询功能

同样,我们先上来也是搭布局框架

我们把刚才引入在App.js的Index组件撤掉。换成Manage组件

// import Index from './index/index.jsx'
import Mannage from './manager/manager.jsx'复制代码

App.js 内也要修改

// <Index/>
<Mannage/>复制代码


初始化一下manage.jsx和 manage.css

manage.jsx:

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

export default class Manager extends Component {
    render() {
        return (
            <div className="manager">
                <a className="manager__add">
                    <span>新增</span>
                </a>
                <div className="manager__tab">
                    <div className="manager__tab__header">
                        <span>姓名</span>
                        <span>操作</span>
                    </div>
                    <div className="manager__tab__list">
                        <div className="manager__tab__list__item">
                          <span className="manager__tab__list__item__name">
                             Janlay
                          </span>
                          <span className="manager__tab__list__item__tools">
                              <span className="item__tools__delete">删除</span>
                          </span>
                        </div>
                    </div>
                </div>
            </div>
        )
    }
}
复制代码


manage.css:

.manager__add {
    text-decoration: none;
    padding: 9px 100px;
    color: white;
    background-color: #61DAFB;
    border-radius: 6px;
    cursor: pointer;
    transition: all .3s;
}

.manager__add span {
    user-select: none;
}


.manager__add:focus, .manager__add:active {
    background-color: #19c5f4;
}

.manager__input input {
    padding: 5px 0;
}

.manager__input a {
    padding: 5px 10px;
    margin-left: 10px;
    color: white;
    background-color: #61DAFB;
    border-radius: 3px;
}

.manager__tab {
    margin-top: 15px;
    padding: 0 70px;
}

.manager__tab__header,
.manager__tab__list__item {
    display: flex;
}

.manager__tab__header span,
.manager__tab__list__item span {
    margin: 0;
    flex: 1;
}

.manager__tab__header span:first-child,
.manager__tab__list__item span:first-child {
    text-align: left;
}

.manager__tab__header span:last-child,
.manager__tab__list__item span:last-child {
    text-align: right;
}

.manager__tab__list {
    margin-top: 10px;
    overflow: scroll;
    height: 300px;
}

.manager__tab__list__item {
    margin-top: 5px;
}

.item__tools__delete {
    margin-left: 5px !important;
}
复制代码


初始化后预览:



查询功能的实现

在React的内部组件状态中,使用states进行状态管理。他和Vue的data 性质是一样的。但写法略微有点不同:

我们在构造函数 constructor 内初始化 states 变量。构造函数接受组件外部props变量

constructor(props) {
    super(props);
    this.state = {
        list: [],
    };
}复制代码

这时候就有疑问了,这个super方法是干什么的?

是因为当前咱们的这个React组件是继承于官方组件模板。但是继承后的组件其实是没有this对象的。如果想使用this对象,就必须要调用一下super() 方法。不调用的话,就得不到this对象(ES6)

当然,super()不传参数也没问题。但是组件内部就无法使用 this.props 这个对象。所以为了以后不会忘,就写上吧

接着赋值 this.state。初始化一遍我们要用的key值。在这里,人员的列表变量叫list 


这时候,我们就可以开始准备渲染了。

React的条件渲染和循环遍历和Vue的写法大不一样。React的写法比较偏JS原生写法,但是定制化高

我们在render函数内新建一个变量。这个变量存储我们的HTML模板:

let list = [];
复制代码


列表渲染嘛,也要做好非空判断:

if (this.state.list.length > 0) {
    this.state.list.map((data, index) => {
        list.push(
            <div className="manager__tab__list__item" key={index}>
            <span className="manager__tab__list__item__name">
                {data.name}
            </span>
                <span className="manager__tab__list__item__tools">
                <span className="item__tools__delete">删除</span>
            </span>
            </div>
        )
    });
} else {
    list =
        <span>暂无数据</span>
}复制代码

如果 states 内的列表长度大于0,说明列表内就有数据。这时候遍历一下列表数据。

如果小于0,就显示“暂无数据”

如果你连{index} 都看不懂,请回去看一下React基础语法


这时候不出意料,列表渲染的是“暂无数据”。因为咱们初始化就是个空数组


这时候可以加一条数据看看:

constructor(props) {
    super(props);
    this.state = {
        list: [
            {
                name: "Janlay"
            }
        ],
    };
}复制代码

结果列表渲染出来了:



查询功能完成后的Manager.js:

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

export default class Manager extends Component {
    constructor(props) {
        super(props);
        this.state = {
            list: [
                {
                    name: "Janlay"
                }
            ],
        };
    }

    render() {
        let list = [];
        if (this.state.list.length > 0) {
            this.state.list.map((data, index) => {
                list.push(
                    <div className="manager__tab__list__item" key={index}>
                    <span className="manager__tab__list__item__name">
                        {data.name}
                    </span>
                        <span className="manager__tab__list__item__tools">
                        <span className="item__tools__delete">删除</span>
                    </span>
                    </div>
                )
            });
        } else {
            list =
                <span>暂无数据</span>
        }
        return (
            <div className="manager">
                <a className="manager__add">
                    <span>新增</span>
                </a>
                <div className="manager__tab">
                    <div className="manager__tab__header">
                        <span>姓名</span>
                        <span>操作</span>
                    </div>
                    <div className="manager__tab__list">
                        {list}
                    </div>
                </div>
            </div>
        )
    }
}
复制代码



增加功能的实现

我们的添加按钮是与input输入框在同一个位置,所以要用到条件判断:

在states内新增一个变量,用于切换按钮和输入框的显示隐藏。把它设置为true

this.state = {
    list: [
        {
            name: "Janlay"
        }
    ],
    isShowNew: true
};复制代码

在render函数内增加

let isShowNew;
if (this.state.isShowNew) {
    isShowNew =
        <a className="manager__add">
            <span>新增</span>
        </a>
} else {
    isShowNew =
        <div className="manager__input">
            <input type="text" />
            <a>添加</a>
        </div>
}复制代码

然后在return内加入它:

return (
    <div className="manager">
        {isShowNew}
        <div className="manager__tab">
            <div className="manager__tab__header">
                <span>姓名</span>
                <span>操作</span>
            </div>
            <div className="manager__tab__list">
                {list}
            </div>
        </div>
    </div>
)复制代码


这样我们的切换按钮是否显示的功能ok了,接下来就该上点击事件进行切换了

我们在组件内新增一个方法。用户隐藏 “添加” 按钮

showAddInput() {
    this.setState({
        isShowNew: false
    })
}复制代码

React如果要更改states内的变量,需要用 this.setState() 这个方法。然后里面包含要更改的key和对应的value

然后我们在添加按钮上加入这个方法

<a className="manager__add" onClick={() => this.showAddInput()}>
    <span>新增</span>
</a>复制代码

可能学过react的小伙伴就迷惑了,一般我们注册点击事件的操作是在构造函数内bind一遍啊,为何现在这种方法不用bind就可以了?


首先了解下平时我们写bind()方法和箭头函数的几种形式 

// 第一种
xxFunction(){..}
... 
<XXView xxxx={this.xxFunction.bind(this)} />
复制代码

// 第二种
constructor(props) {
 super(props);
 this.xxFunction= this.xxFunction.bind(this); 
}
 ... 
xxFunction(){..}
复制代码

//第三种
xxFunction= ()=>{..};
... 
<XXView xxxx={this.xxFunction} />
复制代码
//第三种
...
xxFunction(){..}
...
<XXView xxxx={()=>this.xxFunction()} />复制代码

bind函数和箭头函数区别: 

在ES5下,React.createClass会把所有的方法都bind一遍,这样可以提交到任意的地方作为回调函数,而this不会变化。 在ES6下,你需要通过bind来绑定this引用,或者使用箭头函数(它会绑定当前scope的this引用)来调用。  

由此能够得出=>>>bind()方法和箭头函数在使用是等价的。且在其形式上2和3是一致的、1和4是一致的。 

通过 bind() 函数会创建一个新函数(称为绑定函数),该新函数是由指定的this值和初始化参数改造的原函数拷贝。 ---> 这里引用来自CSDN的@学术袁 的文章


那我们为何要这样写呢?

因为我们的项目中,多个地方调用了 this.setState()。导致页面渲染时就会执行我们绑定在html上面的方法。但是setState()方法又会触发重新渲染,就造成了死循环的现象。所以我们改为使用箭头函数的方式进行调用,这样可以避免重新渲染,在我们需要用的时候进行渲染。


接下来,我们绑定input标签的onChange事件,监听输入框变化。

同样,先在states内给input一个坑:

this.state = {
    isShowNew: true,
    list: [],
    input: "" //接受input输入值
};复制代码


我们上面讲到,使用bind方法其实和使用箭头函数的方法一致。所以我们input的监听事件这样写:

inputChange = (event) => {
    this.setState(
        {
            input: event.target.value
        }
    )
}复制代码

接收一个event的参数,然后获取里面的value值。input添加监听事件

<input type="text" onChange={this.inputChange}/>复制代码


有了input监听事件,我们也要加上添加事件才算完整。添加一个add方法

add() {
    let list = this.state.list; //获取当前list的值
    list.push({ //添加一条数据
        name: this.state.input
    });
    this.setState({ //重新赋一下值
        list: list
    })

}
复制代码


我们在添加按钮上添加点击事件:

<a onClick={() => this.add()}>添加</a>复制代码

这样我们的添加事件就大功告成了!




删除功能的实现

删除功能相比增加功能要简单的多。但是要重写一下数组的delete方法

增加一个删除事件,按照数组的下标去删除

delete(deleteIndex) {
    Array.prototype.delete = function (deleteIndex = 0) {
        let temArray = [];
        for (let i = 0; i < this.length; i++) {
            if (i !== deleteIndex) {
                temArray.push(this[i]);
            }
        }
        return temArray;
    };
    let list = this.state.list.delete(deleteIndex);
    // delete list[deleteIndex];
    this.setState({
        list: list
    })
}复制代码

首先我们扩展了下Array的delete事件,默认删除下标0的元素。遍历数组,如果不是要删除的元素下标,就把数据push进新数组内。最后返回新数组

接着调用删除方法,然后赋值给state内的list


我们监听一下删除按钮的Click事件:

<span className="item__tools__delete" onClick={() => this.delete(index)}>删除</span>复制代码


最后看一下效果图:



这样我们所有的功能就大功告成啦!


路由

其实这个demo的核心都在路由这里。

首先要使用react-router,需要先安装react-router和react-router-dom才能进行使用


在项目目录下执行:

npm install react-router react-router-dom --save

如果网络环境不好,请使用 cnpm


首先我们先选择路由容器。react有两种:HashRouter 和 BrowserRouter。

如果使用 HashRouter,你的url会有个#,是因为它通过hash值进行路由控制的。但是很多情况我们不用他。我们用更加优雅一点的 BrowserRouter。它通过HTML5 history API 更改url值。

BrowserRouter 可以指定一个根url。例如:

<BrowserRouter basename="/"></BrowserRouter>复制代码

但我们这里根url就是“/”。所以就不再次指定了


接着我们确定了路由容器,我们新增两个路由视图,分别代表两个页面。

<Route path="/" component={Index}/>
<Route path="/manager" component={Manager}/>复制代码

path是代表我要指定的路由地址。component 代表这个路由地址挂在哪个组件上去


最后我们确定路由点击链接,用于路由的切换

在React中,路由链接有两种:NavLink 和 Link。

Link的api比较简单,主要是to(要链接的路由地址)。NavLink的功能更多,不仅可以设置to,还可以设置选中类名和样式。

在这里,我们使用 NavLink。并设置路由选中的样式类名

<div className="tab-bar">
    <NavLink exact to="/" activeClassName="tab-bar__active">
        首页
    </NavLink>
    <NavLink to="/manager" activeClassName="tab-bar__active">
        人员管理
    </NavLink>
</div>复制代码

App.js 修改后:

import React, {Component} from 'react';
import './App.css';
import logo from "./logo.svg";
import {BrowserRouter, NavLink, Route} from 'react-router-dom';
import Index from './index/index.jsx'
import Manager from './manager/manager.jsx'

class App extends Component {
    render() {
        return (
            <BrowserRouter> //路由容器
                <div className="App">
                    <img src={logo} alt=""/>
                    <Route exact path="/" component={Index}/> //路由视图
                    <Route path="/manager" component={Manager}/>
                    <div className="tab-bar">
                        <NavLink to="/" activeClassName="tab-bar__active"> //路由链接
                            首页
                        </NavLink>
                        <NavLink to="/manager" activeClassName="tab-bar__active">
                            人员管理
                        </NavLink>
                    </div>
                </div>
            </BrowserRouter>
        );
    }
}

export default App;
复制代码


我们打开浏览器后,路由可以使用。但是惊奇的事情发生了



我们切换到manage 路由时,发现两个路由链接都选中了。

这是因为我们的首页路由(/)没有开启严格匹配。如果我们要跳转到“/”,React其实会继续向下匹配,所以他也把“/manage”也一起匹配到了。

解决方案是,在Index的路由视图和路由链接上上,增加 exact 关键字。代表这个路由要严格匹配

<Route exact path="/" component={Index}/>复制代码
<NavLink exact to="/" activeClassName="tab-bar__active">
    首页
</NavLink>复制代码


这样我们再次尝试一下,会发现问题解决了:




这样到现在,我们的这个小demo就大功告成了


结束语

作者我作为一个全职写vue的菜鸟,写这个demo从react入门到结束一共用了三天时间。其中边查资料边看文档才写出了这个例子,个人认为涉及点比较全面,可以拿来快速上手。



如果你觉得好,请给一个赞👍。感谢


关注下面的标签,发现更多相似文章
评论