如果你也刚入门React,来一起学习吧

5,719 阅读16分钟

本文提要

本文主要写一些CRA脚手架的安装,React的语法,组件分类和组件传值等;如果您是已经在React上有丰富经验的开发者,欢迎指出文中有问题和可以改进的地方,对此我将表示感谢!
这是react的官方站点React官网

本文将主要分为:

  • React项目搭建
  • React语法
  • React组件介绍
  • React组件传值
  • 一个简单的组件化小例子
  • 本文暂时不介绍React-Router

阅读全文可能会花费您10-20分钟,如果觉得有兴趣,可以一起敲敲代码
(使用编辑器:VSCODE,插件:VS Code ES7 React/Redux/React-Native/JS snippets,这个插件可以快速构建组件格式,如果想要练练手的同学就不要用快速指令了哦)

下面开始正文

1.通过脚手架创建React项目

First thing first,这里我们利用create-react-app(需要nodejs环境)来创建这个项目,毕竟比较方便嘛,有其他创建项目和服务的方式也可以使用。

找一个工作文件夹,然后打开命令行工具,输入create-react-app mycode就可以创建一个文件夹为mycode的项目文件夹,注意哦,这个项目名称不支持大写字母
ok,几分钟后会提示你脚手架初始化成功了

这里有几个指令:

  • npm start开启开发服务器,一般默认是3000端口,启动后会自动弹出localhost:3000的页面
  • npm run build为生产环境创建打包的静态文件
  • npm test开启测试,这个我没有用过,有用过的同学可以在评论里分享一下使用技术文章
  • npm run ejectEject 将所有的工具(配置文件和 package.json 依赖库)解压到应用所在的路径,这个过程是不可逆的

那我们开始吧,cd mycode & npm start

2.基本语法

项目中两个重要的文件

我们启动后会看到这个界面,这是脚手架自带的。 请移步到编辑器,这里我们暂时只关注两个重要的文件

  • public/index.html 因为React搭建的是SPA,所以index.html是我们的主页,在文件中你也可以看到<div id="root"></div>,root就是根组件渲染的位置。
  • src/index.js 这是我们的主要的js文件,
    其中这一句表达式:render(<App></App>, window.root)表明,我们使用一个渲染方式render,将App渲染到root中去,不论App中有什么,有多少层级,有多少组件,有多少逻辑,最终只有这一个入口。

React中的一切都从这里开始

我们将脚手架src下的所有文件全部删掉,创建一个空白的index.js,开始coding。

index.js中,我们要做的就是,引入React库,引入react-dom,引入根组件,然后执行根组件的渲染方法:

  • import React from 'react' 注意这里的React必须首字母大写
  • import {Component} from 'react',引入组件方法,使用{Component}解构方式引入
  • import App from './App',引入根组件App,我们再下一步将会创建一个src/App.js作为我们的根组件,这里你可以取任何名字作为你的根组件js,我习惯取作App
  • render(<App></App>, window.root)渲染组件到root容器,render是react的核心渲染方法,后面我们会一直用到

语法介绍

① jsx简介

按照上一节的引入各种库,我们可以在index.js中coding来学习React的基础语法。
简要来说,react的核心语言是jsx,按照官方文档的举例:

const name = 'Josh Perez';
const element = <h1>Hello, {name}</h1>;

ReactDOM.render(
  element,
  document.getElementById('root')
);

我们在React下写的<h1><h1>这样的html标签,实际上都会按照上面的代码渲染到页面上,只不过作为一个很sweet的语法糖,我们不需要再写ReactDOM.render(),而是

let el = <h1>hello <span>world</span></h1>
render(el,window.root);

使用javascript + xml语法,定义一个元素,然后再render渲染就可以了;在index.js中,你可以先注释掉原本的render(<App></App>, window.root)这句话,改为上面的代码,在localhost:3000中可以看到

hello world渲染上了页面;(后续如果有时间,会写一篇小文讲解一下react虚拟dom的实现原理)

② <>和{}

用一句话来概括就是: jsx元素/react元素 用<号标识, 看到{ 会认为里面包含的是js代码

1){}中执行js

  • 变量取值
let str = '<h1>world</h1>'
let el = (
  <div>
    <div>{str}</div>
  </div>
)
render(el,window.root);

页面效果如图

str变量在{}中执行,div中间的内容应该是字符串'<h1>world</h1>',而不是标签h1

  • 注销

如果我们想在代码中写备注怎么办?是这样吗//,哦不行,加个{//}呢?哦不行,你在编辑器中可以看到

这后面的}也被注销了,所以在react中,我们使用 {/*hello*/}的方式作为备注

  • 字符串解析为html
    在第一个小例子里面提到 '<h1>world</h1>'会被作为字符串内容渲染,但是如果确实想要作为dom展示h1呢?
    这里我们使用 <div dangerouslySetInnerHTML={{ __html: str }}></div>,这个API很长对吧,我们在容器上标注 dangerouslySetInnerHTML~危险地设置innerHTML

    这么做可能有被插入恶意执行脚本的的风险。

  • 执行一个方法

function a() { 
  return <h3>hello function</h3>
}

let el = (
  <div>
    <div>{a()}</div>
  </div>
)

页面:

  • 循环
    添加一个li key的要时最好不要用数组的索引,为了能标示每个循环元素便于dom-diff, 一般用id,这里由于例子比较简单,我们使用数组的索引填写key ;使用arr.map的方式处理数组返回
let arr = [1,2,3];
// 
let el = (
  arr.map((item, key) => (
    <li key={key}>{item}</li>
  ))
)

  1. jsx中html属性的几个特点
  • classclassName 这个驼峰方式的写法代替了原生html的class,但是class还是可以用的,脚手架会提示你这里应当使用className

  • forhtmlFor 这个for是html的label上的for,用于指向控制的input,在jsx中我们使用htmlFor来替代

<label htmlFor="username">用户名</label>
<input type="text" id="username" />
  • <div style="color:red">hello</div><div style={{ color: 'red' }}>hello</div> 外层的{}表示js语法的标示,{ color: 'red' }是对象

  • React.Fragment
    如果有用过vue的同学应该知道,vue返回的html一定要有一个根节点包裹,即返回的dom一定是一个,不能是平级的多个,即

<div>
    <div></div>
    <p></p>
</div>

在react中同样,如果我们返回平级的多个div的话:

react会提示语法错误:jsx必须被一个闭合标签包裹

但是如果我们在某种情况下,必须使用一些平级元素怎么办呢,比如处于样式的考虑,我们外层没有什么需要div包裹的。这时候我们使用<React.Fragment>来包裹平级的元素,这个<React.Fragment>是没有实际意义的,就充当一个节点闭合标签。

let el1 = (
  <React.Fragment>
    <div>{str1}</div>
    <div>{a()}</div>
    <div>{JSON.stringify(obj)}</div>
    <div>{false?<span>你好</span>:void 0}</div>
  </React.Fragment>
)

这样就不会报错了

总的来说,react的API较少,写jsx是很自由的,js+xml的方式,使js功底很深厚的开发者可以在html中任意的书写js逻辑,所写即所得,可能这就是react的魅力吧。

3.组件

在react项目中,基本上所有的结构功能都可以拆分成很细的一个个组件,比如一个页面上常用的菜单栏,可以拆分成:列表框List,列表项ListItem,列表链接Link等等,这样的好处是:
1.复用 2.方便维护 3.提高工作效率。

react声明组件的方式分为函数声明和类声明

  • 函数式声明组件
    函数式声明组件的方式如下
function Build(props) {
  let {title,content} = props;
  return (
    <div>
      <div>{title}</div>
      <div>{content}</div>
    </div>
  )
}

render(<div>
  <Build title="build1" content="content1"></Build>
  <Build title="build2" content="content2"></Build>
  <Build title="build3" content="content3"></Build>
</div>, window.root);

如果我们仅需要展示一些信息到页面上,不需要去控制变化,则函数组件可以简单实现
组件的定义一定要是首字母大写的,函数式组件传值的方式是按照在组件中定义了属性名,在组件使用时直接写在组件上 <Build title="build3" content="content3"></Build>
函数组件的缺点是 1.没有this 2.没有状态 3.没有声明周期 可以通过定时器可以实现函数式组件中值的定时改变,比如这个例子

function Clock(props) {
  return <div> 时间更新:<span>{props.time}</span></div>
}
setInterval(()=>{
  render(<Clock time={new Date().toLocaleString()} />, window.root);
},1000)
  • 类声明
    使用es6创建类的方式创建组件,类声明的组件拥有了状态,视图通过setState方法进行更新 之后我们做的小例子的组件,都用类声明的方式进行创建。在使用中来学习使用

受控组件 和 非受控组件

非受控组件:表单数据由DOM本身处理。即不受setState()的控制,与传统的HTML表单输入相似,input输入值即显示最新值(使用 ref 从DOM获取表单值)
受控组件:在HTML中,标签<input><textarea><select>的值的改变通常是根据用户输入进行更新。在React中,可变状态通常保存在组件的状态属性中,并且只能使用 setState() 更新,而呈现表单的React组件也控制着在后续用户输入时该表单中发生的情况,以这种由React控制的输入表单元素而改变其值的方式,称为:“受控组件”。

这里我们写一个非受控组件的小例子,我们输入的值通过点击显示出来;非受控组件常用于操作dom,较为方便

import React,{Component} from 'react';
import {render} from 'react-dom';
class UnControl extends Component{
  b=React.createRef();
  handleClick = () =>{
    alert(this.a.value); // 写法1
    alert(this.b.current.value) // 写法2
  }
  render(){
    return (<div>
      <input type="text" id="username" ref={dom=>this.a=dom}/>
      <input type="text" id="password" ref={this.b}/>
      <button onClick={this.handleClick}>点击</button>
    </div>)
  }
}
render(<UnControl></UnControl>, window.root);

接下来我们将实现这样的一个小例子:

实现一个评论组件,类似于掘金下方的评论栏,我们将这个组件大功能拆分为
根组件App,列表组件List,列表项ListItemComment评论组件,在实现的过程中,我们会讨论组件间数据传递的方式。

4.更新视图的方法

首先,我们不拆分组件,将上述的例子简单构建出来,页面结构使用bootstrap UI(npm install boostrap@3) 组件。
在这个例子中,我们采用axios(npm install axios)请求初始列表数据,封装为一个request.js,代码如下:

import axios from 'axios';

axios.interceptors.response.use(function (res) {
  if (res.data.code === 0) {
    return res.data.users
  } else {
    return Promise.reject('错误');
  }
})

export default axios

请求的数据格式自己简单拟定为:

{
    "code":0,
    "users": [
      {
        "id": 1,
        "avatar": "http://05.imgmini.eastday.com/mobile/20171112/20171112104845_40c4a989ba5a02f512b05336bff309f8_1.jpeg",
        "username": "Jim",
        "content": "Hi,你的文章很不错"
      },
      {
        "id": 2,
        "avatar": "http://05.imgmini.eastday.com/mobile/20171112/20171112104845_40c4a989ba5a02f512b05336bff309f8_1.jpeg",
        "username": "Jim",
        "content": "一般般的说"
      }
    ]
  }

然后贴出我们的App.js,我们将全部的内容都放在App.js中,不拆分组件:

import React, { Component } from 'react';
import axios from './request'
import 'bootstrap/dist/css/bootstrap.css'
import './Common/common.css'

class App extends Component {
    state = {
        users: [],
        count: 0,
        id: 3
    }
    // 点赞功能
    increment = () => {
        this.setState({
            count: this.state.count + 1
        })
    }
    // 添加评论
    addComment = (val) => {
        let id = this.state.id;
        let users = [...this.state.users, { avatar:  "http://05.imgmini.eastday.com/mobile/20171112/20171112104845_40c4a989ba5a02f512b05336bff309f8_1.jpeg", content: val, username: 'Jim', id: id}];
        this.setState({
            users
        });
        this.state.id+=1;
    }
    content = React.createRef();
    // 提交数据
    handleSubmit = (e) => {
      e.preventDefault();
      this.addComment(this.content.current.value);
  }
    // 删除一条
    removeById = (id) => {
        let users = this.state.users.filter(user=>user.id!==id); // 排除列表里相同id的,即达到删除的目的
        this.setState({
            users
        })
    }
    // 获取列表数据
    async componentDidMount() {
        let users = await axios.get('/users.json');
        this.setState({
            users
        });
    }
    render() {
        return (
          <div className="container">
          <div className="panel panel-danger">
              <div className="panel-heading">
                  评论
              </div>
              <div className="panel-body">
              {
                this.state.users.map((user, index) => {
                  return (
                    <div className="media">
                    <div className="media-left">
                        <img className="avatar" src={user.avatar} />
                    </div>
                    <div className="media-right">
                        <h3>{user.username} </h3>
                        <div>评论:{user.content}</div>
                        <button className="btn btn-danger" onClick={(e)=>{
                            this.removeById(user.id)
                        }}>删除</button>

                    </div>
                </div>
                  )
                })
              }
              
              </div>
              <div className="panel-bottom">
                <form onSubmit={this.handleSubmit}>
                <textarea className="form-control" required ref={this.content}></textarea>
                <button type="submit" >评论</button>
                </form>
              </div>
          </div>
      </div>
            
        );
    }
}

export default App;

效果:

到这里,我们的代码实现的功能有,加一条评论,也可以删除一条评论。
在React中,视图是受到数据的驱动的,我们最初定义的

 state = {
        users: [],
        count: 0,
        id: 3
    }

state中users的数据,会在componentDidMount生命周期时,获取到users列表,并通过this.setState({ users });方法更新视图。其他的操作,类似于handleSubmitremoveById同样都是通过操作state.users的数据达到增删的目的。

5.拆分组件

考虑到一个项目中的复杂度,我们可以将上述App.js中的相关内容进行拆分为:列表组件List,列表项ListItemComment评论组件,这样,构造其他结构的时候,我们就不用再去重新写一遍相同的代码。我们在src文件夹下新建components文件夹,并且创建List.js ListItem.js Comment.js

  • 列表项组件:
import React, { Component } from 'react'
export default class ListItem extends Component {
    state = {
        users: [],
        id: 100000
    }
    addComment = (val) => {
        let id = this.state.id;
        let users = [...this.state.users, { avatar:  "http://05.imgmini.eastday.com/mobile/20171112/20171112104845_40c4a989ba5a02f512b05336bff309f8_1.jpeg", content: val, username: 'Jim', id: id}];
        this.setState({
            users
        });
        this.state.id+=1;
    }
    handleClick = (id) => {
        this.props.removeById(id);
    }
    removeById = (id) => {
        let users = this.state.users.filter(user=>user.id!==id); // 排除列表里相同id的,即达到删除的目的
        this.setState({
            users
        })
    }
  render() {
    let {id, avatar, content, username} = this.props;
    return (
        <div className="media">
                    <div className="media-left">
                        <img className="avatar" src={avatar} />
                    </div>
                    <div className="media-right">
                        <h3>{username} {id}</h3>
                        <div>评论:{content}</div>
                        <button className="btn btn-danger" onClick={(e)=>{
                            this.handleClick(id)
                        }}>删除</button>

                    </div>
                </div>
       
    )
  }
}

  • 列表组件
import React, { Component } from 'react'
import ListItem from './ListItem'
export default class List extends Component {
  static props = {
    showComment: true
  }
  render() {
    return (
      <div>
        {
            this.props.users.map((user, index) => {
                return (
                    <ListItem showComment={this.props.showComment} {...user} key={index} removeById={this.props.removeById} addComment={this.props.addComment}></ListItem>
                )
            })
        }
      </div>
    )
  }
}

  • 评论框组件
import React, { Component } from 'react'

export default class Comment extends Component {
    content = React.createRef();
    handleSubmit = (e) => {
        e.preventDefault();
        this.props.addComment(this.content.current.value);
    }
    render() {
        return (
            <form onSubmit={this.handleSubmit}>
            <textarea className="form-control" required ref={this.content}></textarea>
            <button type="submit" >评论</button>
            </form>
        )
    }
}

  • App.js变为
import React, { Component } from 'react';
import axios from './request'
import 'bootstrap/dist/css/bootstrap.css'
import './Common/common.css'
import Comment from './components/Comment'
import List from './components/List'
import {Provider} from './context'

class App extends Component {
    state = {
        users: [],
        count: 0,
        id: 3
    }
    increment = () => {
        this.setState({
            count: this.state.count + 1
        })
    }
    addComment = (val) => {
        let id = this.state.id;
        let users = [...this.state.users, { avatar:  "http://05.imgmini.eastday.com/mobile/20171112/20171112104845_40c4a989ba5a02f512b05336bff309f8_1.jpeg", content: val, username: 'Jim', id: id}];
        this.setState({
            users
        });
        this.state.id+=1;
    }
    removeById = (id) => {
        console.log(id)

        let users = this.state.users.filter(user=>user.id!==id); // 排除列表里相同id的,即达到删除的目的
        this.setState({
            users
        })
    }
    async componentDidMount() {
        let users = await axios.get('/users.json');
        this.setState({
            users
        });
    }
    render() {
        return (
        <Provider value={{increment: this.increment}}>
            <div className="container">
                <div className="panel panel-danger">
                    <div className="panel-heading">
                        评论
                    </div>
                    <div className="panel-body">
                        <List users={this.state.users} showComment={true} removeById={this.removeById} addComment={this.addComment}></List>
                    </div>
                    <div className="panel-bottom">
                    
                    <br/>
                    <Comment addComment={this.addComment}></Comment>
                    获得的赞数量{this.state.count}
                    </div>
                </div>
            </div>
        </Provider>
            
        );
    }
}

export default App;

看到这里,一定有疑问,那么我们之前定义的users数据,removeByIdaddComment的方法,怎么用到组件上呢?下面我们进行讲解。

6.组件间属性的传递

  • 组件的数据交互的方式是属性传递,传递属性值或方法
  • 子组件不能直接修改属性值
  • 但是可以通过父组件传递进来的方法调用以改变属性值
  • 数据传递是单向的:父->子,即常说的单项数据流
  • 子组件获取属性的方法:`this.props.fn
  • 可以使用contextApi实现跨组件传递

上一节我们拆分的组件中,在列表组件中原本的循环体数据源,由this.state.users改为了使用this.props.users,而在App.js中传入的方式为

<List users={this.state.users} showComment={true} removeById={this.removeById} addComment={this.addComment}></List>

传入和获取是一一对应的。
同样,由于ListItem组件需要removeById方法,所以我们从App.jsList组件就传入removeById,在List组件中调用ListItem时,再次传入ListItem,是一个父传子,子传孙的过程:

<ListItem showComment={this.props.showComment} {...user} key={index} removeById={this.props.removeById} addComment={this.props.addComment}></ListItem>

ListItem组件中,我们对removeById方法再包装一层

handleClick = (id) => {
        this.props.removeById(id);
}

...

<button className="btn btn-danger" onClick={(e)=>{
    this.removeById(user.id)
}}>删除</button>

这里我们的删除方法来自于根组件传递下来的方法,子组件获取后,对同样是传递进来的users进行修改,以到达改变数据的目的。以上就是简单的组件传值的讲解。

contextApi

如果我们想给这个列表加一个点赞功能,即任何一个列表项组件都可以点赞,而且点赞还可以收集总数,这时候如果再去用父子间组件传值,可能代码实现起来会比较麻烦或者易错,因为涉及的层级很多。所以我们利用contextApi来实现(react16.3)。

引入的方式(在例子中,我抽离了这个引入到context.js,就不用在每个页面写一遍解构了):

import React from 'react'
let {Provider, Consumer} = React.createContext();

export {Provider, Consumer}

在组件中的使用方法是,在父组件引入后,将父组件的返回值使用Provider包裹,并传入value属性:

import React, { Component } from 'react';
import {Provider} from './context'

class App extends Component {
    state = {
        users: [],
        count: 0,
        id: 3
    }
    // 点赞功能
    increment = () => {
        this.setState({
            count: this.state.count + 1
        })
    }
    render() {
        return (
        <Provider value={{increment: this.increment}}>
            <div className="container">
                <div className="panel panel-danger">
                    <div className="panel-heading">
                        评论
                    </div>
                    <div className="panel-body">
                        <List users={this.state.users} showComment={true} removeById={this.removeById} addComment={this.addComment}></List>
                    </div>
                    <div className="panel-bottom">
                    
                    <br/>
                    <Comment addComment={this.addComment}></Comment>
                    获得的赞数量{this.state.count}
                    </div>
                </div>
            </div>
        </Provider>
            
        );
    }
}

export default App;

在子组件中,需要使用(消费)的返回值外层包裹Consumer,使用箭头函数传入value的值,即Provider传入的属性,即可在组件中直接调用父组件或更高阶的组件的传入属性。

import React, { Component } from 'react'
import {Consumer} from '../context'
...
export default class ListItem extends Component {
    ...
  render() {
    let {id, avatar, content, username} = this.props;
    return (
        <Consumer>
            {(value)=>{
                return <div className="media">
                    <div className="media-right">
                        ...
                        <button className="btn btn-primary" onClick={()=>{
                            value.increment()
                        }}>赞</button>
                        ...
                      </div>
                </div>
            }}
            
        </Consumer>
       
    )
  }
}

总结

以上是我学习React入门的一些小总结,写了一个不太成熟的例子来练手,在表述上可能有一些跳跃还请见谅。这里附上这个小例子的Github代码,有需要详细了解的同学可以看看:code
希望我的文章能帮到你。