Reactk开发 | 核心概念

欢迎访问:【React Native】从React开始——核心概念

1. 前言

    在上一篇【React Native】回归跨平台开发——细细碎碎念念中大致介绍了如何搭建环境的问题。在接下来的日子里将来学习下React这个框架。为什么呢?因为在React Native的官网的第一句话就是:

使用 React 来创建 AndroidiOS 的原生应用

不妨看看昨天的项目目录:

![在这里插入图片描述](https://img-blog.csdnimg.cn/7237e14c8e6b4319908b8c7f76199c1c.png#pic_center =700x)
可以发现这里有androidios两个目录,这两个目录均是RN自动生成的,代码目录见下图:

![在这里插入图片描述](https://img-blog.csdnimg.cn/27f14fb344e84ecbbdc07cafadeff430.png#pic_center =600x)
可以发现其实就是原生开发的目录结构,打开MainActivity.java文件:

package com.awesomeproject;

import com.facebook.react.ReactActivity;

public class MainActivity extends ReactActivity {

  /**
   * Returns the name of the main component registered from JavaScript. This is used to schedule
   * rendering of the component.
   */
  @Override
  protected String getMainComponentName() {
    return "AwesomeProject";
  }
}

其实发现这里的写法就和原生的略有不同,猜测应该事FaceBookReactActivity进行了再次封装,以更方便的支持他提供的各种组件。

究竟事如何生成的这个原生代码部分,确实比较有意思。因为不光有Android的,还生成了ios的代码。当然更加有意思的是:

![在这里插入图片描述](https://img-blog.csdnimg.cn/bc03d28bb3384a2889043f628df52068.png#pic_center =500x)
也就是说其实可以做到混合开发,无缝集成。

所以还是有必要好好学习RN,因为之前没有学过React,所以接下来就好好学习一下这个框架以及复习下JavaScript。无意间看到了这句话,感觉比较搞笑,但也说明了React其重要的地位:

![在这里插入图片描述](https://img-blog.csdnimg.cn/602b8f829f0448cab1221a8b9e57ef8b.png#pic_center =500x)

2. React

首先找到其官方文档,我们知道React它是一个用于构建用户界面的 JavaScript 库,由Facebook20135月开源。React主要用于构建UI,和Vue有些类似。下面正式进入正题。

开始一个 React 应用,可选择两种方式:

  • 通过 HTMLscript 标签引入 React ,具体可参考:在网站中添加 React
  • 搭建本地开发环境;

因为第一种方式比较简单,直接参考官方文档即可,这里记录下第二种方式。首先需要确保安装了Node.js,需要确保 Node >= 14.0.0npm >= 5.6。这里还是检查下:

在这里插入图片描述

这里我需要更新一下node的版本。下载最新版安装即可。

然后执行:

npx create-react-app my-app

![在这里插入图片描述](https://img-blog.csdnimg.cn/50a2861d16d04f63a012cc32ecd9c1b9.png#pic_center =600x)

按照创建完毕后的提示:

yarn start # 启动服务
yarn build # 编译为静态文件
yarn test # 运行测试

那么这里直接运行下,即:

cd my-app
yarn start

![在这里插入图片描述](https://img-blog.csdnimg.cn/d6a340f16ea945ae80bf2be664c2c230.png#pic_center =500x)
按照提示浏览器访问:http://localhost:3000/,即可看见一个React的图标效果。

2.1 编辑器配置语法高亮

推荐参照这篇教程来给你的编辑器配置语法高亮。

因为Webstorm只提供30天白嫖,所以以后还是使用Visual Studio来进行代码编写。所以按照文档说明,这里安装下 vscode-language-babel 这个插件。即:

![在这里插入图片描述](https://img-blog.csdnimg.cn/86b9a5144f7547f3aa111eef9c76b792.png#pic_center =400x)

然后搜索 vscode-language-babel 这个插件安装即可。

![在这里插入图片描述](https://img-blog.csdnimg.cn/cd5420f950964b84b9cf28f416fdd0ea.png#pic_center =700x)

2.2 Hello World!

不妨看下这个项目的文件结构:

![在这里插入图片描述](https://img-blog.csdnimg.cn/8ab587c0c26c43efa5e0036dbbad00e1.png#pic_center =700x)

其实和之前的hexo有些类似,public为生成的html页面,node_modulesnodejs的一些依赖文件,而真正的写代码的地方在src目录中。

不妨删除src目录下的其余文件,仅留下index.jsindex.css文件,然后将index.js文件内容修改为一个正经的Hello World!,如下:

import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'

ReactDOM.render(
  <h1>Hello, world!</h1>, 
  document.getElementById('root')
)

![在这里插入图片描述](https://img-blog.csdnimg.cn/9376fdfb70ad4ba095770addd1cf4126.png#pic_center =400x)

2.3 取消VSCode的自动代码折叠

但是VSCodectrl+s后自动代码折叠可是够麻烦的,所以这里取消一下:
在这里插入图片描述

2.4 语法和使用

2.4.1 元素渲染

在之前的案例中代码如下:

ReactDOM.render(
  <h1>Hello, world!</h1>, 
  document.getElementById('root')
)

我们看到了ReactDOM.render()函数进行渲染,其中传入了两个参数,分别是待渲染元素和DOM根。在React中,更新 UI 唯一的方式是创建一个全新的元素,并将其传入 ReactDOM.render()进行渲染。所以在需要更新的地方都需要使用这个函数,比如下面的案例:

let number = 1;

setInterval(function(){
  number += 1;
  ReactDOM.render(
    <h1>Number is {number}</h1>, 
    document.getElementById('root')
  )
}, 1000);

在这里插入图片描述
并会每一秒钟进行自加1。当然也可以参考官方提供的这个例子:一个计时器的例子

2.4.2 组件

在开发微信小程序的时候,为了少写一些代码通常都需要封装一些组件,以提高代码的复用。在React中也提供 组件的封装,首先从函数组件开始。

2.4.2.1 函数组件

即编写JavaScript 函数,有两种形式,分别是:

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

上述两个组件在 React 里是等效的。

2.4.2.2 渲染组件

在前面的案例中,其实也写过这个部分,其实也就是使用 ReactDOM.render()进行渲染。比如:

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

const element = <Welcome name="Sara" />;
ReactDOM.render(
  element,
  document.getElementById('root')
);

效果为:
在这里插入图片描述
因为首先定义了一个函数组件<Welcome>,而其属性都会传入props这个对象中,所以最终显示结果为上图所示。

注意: 组件名称必须以大写字母开头。
React 会将以小写字母开头的组件视为原生 DOM 标签。

2.4.3 生命周期

为了使用生命周期方法,我们需要使用函数组件的第二种形式,即创建一个同名的 ES6 class,并且继承于 React.Component,函数主体写在render()方法中。和Java等面向对象语言类似,也有构造方法constructor,可以用来接收参数props

为了使在元素渲染部分的案例的计数器更加像一个正规的组件,我们假定最终的调用为:

ReactDOM.render(
  <CountNumber />,
  document.getElementById('root')
);

那么我们就需要对之前的代码进行组件化。在这里引入两个生命周期方法,分别是挂载(mount)和卸载(unmount)。分别在组件第一次被渲染到 DOM 中的时候和被删除的时候执行。

class CountNumber extends React.Component{
  constructor(props){
    super(props)
    this.state = {data: props.number}
  }

  componentDidMount(){
    this.timerId = setInterval(() => this.update(), 1000);
  }

  componentWillUnmount(){
    clearInterval(this.timerId);
  }

  update(){
    var _number = this.state.data;
    this.setState({
      data: _number + 1
    })
  }

  render(){
    return (
      <div>
        <h1>Number is {this.state.data}</h1>
      </div>
    )
  }
}


ReactDOM.render(
  <CountNumber number={1} />,
  document.getElementById('root')
);

效果和前面的一样,也就是自动累加更新。

注意到上面更新数据的时候使用了this.setState(obj)方法,有点类似于微信小程序,提供的用来更新数据的方法。因为this.props是只读的,不支持修改。state 是默认的私有属性,并且完全受控于当前组件。所以如果数据需要更新(即修改),就需要将数据从props中拷贝到state中,进而使用 this.setState() 来时刻更新组件 state

需要注意的是构造函数是唯一可以给 this.state 赋值的地方。且不能直接修改state,即下面的写法是错误的:

this.state.comment = 'Hello';

应该是:

this.setState({comment: 'Hello'});

这让我想起了之前刚开发微信小程序的时候,也有这个问题,当时就错误的直接赋值修改了,导致其修改并没有生效。

当然,在进行更新的时候,也可以传入一个函数,比如:

this.setState(function(state, props) {
  return {
    counter: state.counter + props.increment
  };
});

只要最终返回的值是一个键值对形式的即可。

2.4.3 事件处理

HTML中类似,也支持onClick事件,只是写法上略有不同。同时在 React 中另一个不同点是你不能通过返回 false 的方式阻止默认行为。你必须显式的使用 preventDefault 。比如在下面的案例中,使用a标签,默认链接到百度,但是这里使用preventDefault来阻止默认行为:

class LinkButton extends React.Component{
  constructor(props){
    super(props)
    this.state = {data:props.text, _default:"https://www.baidu.com"}

    // 为了在回调中使用 `this`,这个绑定是必不可少的
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick(e){
    console.log(e);
    // 阻止默认行为。
    e.preventDefault();
  }

  render(){
    return (
      <a href={this.state._default} className="button" onClick={(e) => this.handleClick(e)}>{this.state.data}</a>
    )
  }
}


ReactDOM.render(
  <LinkButton text={"百度搜索"} />,
  document.getElementById('root')
);

至于类样式文件定义在index.css中,和html中使用的一样,这里就不再给出。实际测试发现确实可以阻止了默认的跳转行为。

2.4.4 列表

function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    <li key={number.toString()}>
      {number}
    </li>
  );
  return (
    <ul>{listItems}</ul>
  );
}

const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
  <NumberList numbers={numbers} />,
  document.getElementById('root')
);

其结果也就是:
在这里插入图片描述
其中, map() 函数是JavaScript中提供的用来进行列表处理的一个函数,比如下面我们让数组中的每一项变双倍,从而得到一个新的列表,代码为:

numbers.map((number) => number * 2);

结果为:[2, 4, 6, 8, 10]

需要注意的是,对于创建的列表元素,对于每一项应该有个key字段进行标识。

2.4.5 表单

对于表单控件,其使用还是和HTML中的保持一致。比如下面封装一个登录页面的控件:

在这里插入图片描述

class LoginForm extends React.Component{
  constructor(props){
    super(props)
    this.state = {
          username: props.username === undefined ? "" : props.username,
          passwd: props.passwd === undefined ? "" : props.passwd
    }
    this.handleSubmit = this.handleSubmit.bind(this)
    this.handleChange = this.handleChange.bind(this)

  }

  handleSubmit(event){
    console.log(this.state.username +"\t" + this.state.passwd);
    // 阻止默认行为。
    event.preventDefault();
  }

  handleChange(event){
    var _value = event.target.value;
    var _name = event.target.name;
    if(_name === "passwd"){
      this.setState({
        passwd: _value
      });
    }else{
      this.setState({
        username: _value
      });
    }
  }

  render(){
    return (
      <form onSubmit={this.handleSubmit}>
        <div className="flexontainer">
          <div className="center">简易版用户登录</div>
        </div>
        <div className="flexontainer">
          <div>用户名:</div>
          <div>
            <input type="text" name="username" value={this.state.username} onChange={this.handleChange}/>
          </div>
        </div>
        <div className="flexontainer">
          <div>密码:</div>
          <div>
            <input type="password" name="passwd" value={this.state.passwd} onChange={this.handleChange}/>
          </div>
        </div>
        <input type="submit" value="登录" className="submitButton"/>
      </form>
    )
  }
}

当然,对应的css的样式也很简单:

form{
  width: 500px;
  display: block;
  margin: 20px auto;
  background-color: rgb(201, 198, 198);
  padding: 8px;
}

.flexontainer{
  display: flex;
  flex-direction: row;
  height: 2rem;
  height: 2rem;
  line-height: 2rem;
  text-align: center;
  margin-bottom: 8px;
}
.flexontainer div:first-child{
  text-align: right;
}
.flexontainer div:first-child{
  flex: 1;
}
.flexontainer div:last-child{
  flex: 2;
}
.flexontainer div{
  text-align: left;
}
.flexontainer div>input{
  height: 1.2rem;
  min-width: 200px;
}
.submitButton{
  display: block;
  min-width: 300px;
  height: 1.8rem;
  line-height: 1.8em;
  cursor: pointer;
  margin: 0 auto;
}
.flexontainer div.center{
  text-align: center;
}

使用比较简单,直接调用这个组件即可:

ReactDOM.render(
  <LoginForm username={'李四'} passwd={123}/>,
  document.getElementById("root")
);

这里为表单传入了默认值,所以我们需要在自定义的组件中进行值非空的判断。因为每次input输入框都会触发handleChange方法,所以这里可以自动更新。

至于其余的表单也和HTML中的一样,就不再继续。

2.4.6 状态提升

官网原话是这么说的:

通常,多个组件需要反映相同的变化数据,这时我们建议将共享状态提升到最近的共同父组件中去。

看到这句话我想到了JavaScript中的父子元素之间的冒泡事件。看了一遍官网的状态提升说明,感觉主要思想在于如何将子控件的值及时反馈到父控件,以及父控件如何将值传递到子控件中。接着上个案例修改,比如做一个两个同步的提交表单:

在这里插入图片描述
LoginForm中不再使用state这个状态,而是直接使用props这个只读变量来设置一开始的值,然后直接回传用户输入的结果到父控件即可。

class LoginForm extends React.Component{
  constructor(props){
    super(props)
    this.handleSubmit = this.handleSubmit.bind(this)
    this.handleChange = this.handleChange.bind(this)

  }

  handleSubmit(event){
    console.log(this.props.username +"\t" + this.props.passwd);
    // 阻止默认行为。
    event.preventDefault();
  }

  handleChange(event){
    var _value = event.target.value;
    var _name = event.target.name;
    if(_name === "passwd"){
      // 新增回调函数
      this.props.onPasswordChange(_value);
    }else{
       // 新增回调函数
       this.props.onUserNameChange(_value);
    }
  }

  render(){
    return (
      <form onSubmit={this.handleSubmit}>
        <div className="flexontainer">
          <div className="center">简易版用户登录</div>
        </div>
        <div className="flexontainer">
          <div>用户名:</div>
          <div>
            <input type="text" name="username" value={this.props.username} onChange={this.handleChange}/>
          </div>
        </div>
        <div className="flexontainer">
          <div>密码:</div>
          <div>
            <input type="password" name="passwd" value={this.props.passwd} onChange={this.handleChange}/>
          </div>
        </div>
        <input type="submit" value="登录" className="submitButton"/>
      </form>
    )
  }
}

定义父控件为ParentContainer

class ParentContainer extends React.Component{
  constructor(props){
    super(props)
    this.state = {
        username : '张三',
        passwd : 123
    };
    this.handlePasswordChange = this.handlePasswordChange.bind(this)
    this.handleUserNameChange = this.handleUserNameChange.bind(this)
  }

  handleUserNameChange(value){
    console.log("handleUserNameChange " + value);
    this.setState({
      username : value
    });
    console.log("handleUserNameChange " + this.state.username);
  }

  handlePasswordChange(value){
    console.log("handlePasswordChange " + value);
    this.setState({
      passwd : value
    });
    console.log("handlePasswordChange " + this.state.passwd);
  }

  render(){
    return(
      <div>
        <LoginForm 
          username={this.state.username} 
          passwd={this.state.passwd} 
          onUserNameChange={this.handleUserNameChange} 
          onPasswordChange={this.handlePasswordChange} 
          />

        <LoginForm 
          username={this.state.username} 
          passwd={this.state.passwd} 
          onUserNameChange={this.handleUserNameChange} 
          onPasswordChange={this.handlePasswordChange} 
          />
      </div>
    )
  }
}

使用自定义回调接口,感觉其写法有点类似于Java语言中的接口的思想。需要注意的是这里为了实现状态提升,在子控件中就不再是直接设置,而是将这个权限交给了父控件,由父控件来完成。而如果是子控件设置的话,就有点类似于自己临时拷贝一块内存区域,所以不会影响到原本的结果。将导致两个部分就不会同步。最终的调用如下:

ReactDOM.render(
  <ParentContainer/>,
  document.getElementById("root")
);

2.4.7 组合

React中推荐使用组合而非继承来实现组件间的代码重用。比如上面的那个案例就是一个组合的例子,当然还有一种组合方式,感觉比较巧妙,那就是可以组合在自定义控件的内部,比如下面的案例:

![在这里插入图片描述](https://img-blog.csdnimg.cn/e736a6a39c314e36b9cab9b0bd376396.png#pic_center =600x)

注意下面的写法,因为需要在自定义组件内部添加子元素,所以这里的写法为:{props.children}

function FancyBorder(props){
  return (
    <div className={'FancyBorder color-' + props.color}>
      {props.children}
    </div>
  );
}

function WelcomeDialog(){
  return (
    <FancyBorder color="red">
      <h1 className="Dialog-title">
        Welcome
      </h1>
      <p className="Dialog-message">
        Thank you for visiting our spacecraft!
      </p>
    </FancyBorder>
  );
}

对应的样式为:

.FancyBorder {
  max-width: 500px;
  padding: 10px 10px;
  border: 10px solid;
  margin: 10px auto;
}

.color-red {
  border-color: red;
}

.Dialog-title {
  margin: 0;
  font-family: sans-serif;
}

.Dialog-message {
  font-size: larger;
}

调用:

ReactDOM.render(
  <WelcomeDialog />,
  document.getElementById('root')
);

2.4.8 React 哲学

React 哲学

3. 后记

大致过了一遍React的基础语法,总体来和H5的开发,乃至之前的微信小程序开发还是很相似的。在接下来的日子将继续看React的高级指引部分。


Thanks


   Reprint policy


《Reactk开发 | 核心概念》 by 梦否 is licensed under a Creative Commons Attribution 4.0 International License
 Previous
Android插件化开发指南 | 2.15 实现一个音乐播放器APP Android插件化开发指南 | 2.15 实现一个音乐播放器APP
欢迎访问:Android插件化开发指南——2.15 实现一个音乐播放器APP 1. 前言最近对Android插件化开发比较感兴趣,也读了部分《Android插件化开发指南》这本书。在该书中1.4部分介绍了这么一句话: 我们曾天真地认为,A
Next 
Reactk开发 | React Router 基础 Reactk开发 | React Router 基础
欢迎访问:【React Native】从React开始——React Router 基础 1. 前言在上篇博客【React Native】从React开始——核心概念中了解了React的一些基础用法,并了解到其实和之前学习的微信小程序开发十
2021-11-07
  TOC