YIXUNFE / blog

文章区
151 stars 25 forks source link

从零开始,用 React 写一个组件 #32

Open ajccom opened 8 years ago

ajccom commented 8 years ago

从零开始,用 React 写一个组件

最近使用 React 尝试着做了一些组件,在这里和大家分享一下经验,也希望能对刚刚开始学习 React 的朋友有所帮助。

下文以制作一个组件为例,一步步的讲解组件从无到有的过程。

阅读文章后你可能会了解:


准备工作

新建一份 HTML 文件,引入 react.jsreact-dom.js

<script src="react.js"></script>
<script src="react-dom.js"></script>

由于我选择使用 JSX 方式编写 React 代码,所以需要注意两个地方:

<!-- include browser.min.js -->
<script src="browser.min.js"></script>

<!-- set type="text/babel" -->
<script type="text/babel" src="..."></script>

<!-- or -->

<script type="text/babel">
...
</script>


组件功能描述

组件的功能很简单:

创建一个按钮,点击按钮显示/隐藏一个弹出层,弹出层可拖动。


第一步:制作按钮组件

首先我们来制作一个按钮,按钮很简单,只有一个功能:

直接上代码:

var ClickBtn = React.createClass({
  getInitialState: function() {
    //设置按钮初始状态为 off
    return {status: 'off', klass: 'btn'}
  },
  onClick: function () {
    this.setState({status: this.state.status === 'off' ? 'on' : 'off'})
  },
  render: function () {
    //返回 React Element
    return <div ref="btn" onClick={this.onClick} className={this.state.klass + ' ' + this.state.status} >Click Me</div>
  }
})

React Element 对象的 className 属性决定了最后页面上元素的 class 属性值。这里为了按钮能够切换状态,除了固定的 btn 类之外,还给按钮设置了一个会变化的类。

在代码的 render 方法出现了十分奇怪的语法,方法返回的是一个类似字符串的内容,却没有引号?

return <div ref="btn" onClick={this.onClick} className={this.state.klass + ' ' + this.state.status} >Click Me

这是因为具有 type="text/babel" 属性的 script 标签并不会被浏览器执行。JSX 会帮你编译这些文本为真正的 Javascript 并执行。所以这里奇怪的语法只是为了方便你写代码的模板而已。


直接写 javascript 与使用 JSX 比较

//JSX 
return <div ref="btn"  onClick={this.onClick} className={this.state.klass + ' ' + this.state.status} >Click Me</div>

//javascript
return React.createElement(
  "div",
  {ref: 'btn', onClick: this.onClick, className: this.state.klass + ' ' + this.state.status},
  "Click Me"
);

显然直接使用 createElement 方法创建 React Element 是比较繁琐的,所以我个人更加倾向于省时省力的 JSX 写法 :smile:。另外顺带一提的是,即使是使用 JSX ,你还是可以在模板中使用 React.createElement 方法哦,不过这有点画蛇添足了。


第二步:制作弹出层

弹出层组件相比上面的按钮组件稍微复杂一点,我们需要多绑定一些事件。

简单地将以往的可拖拽弹出层用 React 实现一遍:

var dom = window.document
var Popup = React.createClass({
  getInitialState: function() {
    var self = this
    //为了拖拽过程流畅,所以在 document 上绑定滑动事件哦
    dom.addEventListener('mousemove', function (e) {self.onMouseMove(e)})
    dom.addEventListener('mouseup', function (e) {self.onMouseUp(e)})
    return {status: 'block', left: 0, top: 0, startLeft: 0, startTop: 0, x: 0, y: 0, ready: false}
  },
  onMouseDown: function (e) {
    var x = e.clientX,
      y = e.clientY
    this.setState({ready: true, x: x, y: y, startLeft: this.state.left, startTop: this.state.top})
  },
  onMouseMove: function (e) {
    //鼠标在弹出层上按住时才可以拖动
    if (!this.state.ready) {return}

    var x = e.clientX,
      y = e.clientY
    this.setState(function (state) {
      return {
        left: state.startLeft + x - state.x,
        top: state.startTop + y - state.y
      }
    })
  },
  onMouseUp: function () {
    this.setState({ready: false})
  },
  render: function () {
    //行内样式可以使用变量放在 React Element 的 style 属性中,或者这样:
    // <div style={{left: 0, top: 0, display: 'none'}}></div>
    var style = {
      left: this.state.left,
      top: this.state.top,
      display: this.state.status
    }
    return (
      <div ref="popup" onMouseDown={this.onMouseDown} className="popup" style={style} >Drag Me</div>
    )
  }
})

到这里我们已经成功创建了一个弹出层的组件。由于弹出层会随着鼠标的移动而改变它的样式,所以我在 React Element 中添加了行内样式,取值来自自身的状态。


第三步:整合按钮与弹出层组件

剩下的工作就是如何整合两个组件了。我们可以按照以往的方法,给组件定义一些实用的外部接口。

给组件增加外部接口

组件一般都会提供外部接口以方便被调用,从而掩盖其内部实现机制。

先给按钮组件添加两个方法,分别是开和关:

// ClickBtn
on: function () {
  this.setState({status: 'on'})
},
off: function () {
  this.setState({status: 'off'})
}

再给弹出层组件做一些修改,增加显示和隐藏的方法:

// Popup
show: function () {
  this.setState({status: 'block'})
},
hide: function () {
  this.setState({status: 'none'})
}


如何调用子组件接口

上面的两个组件都可以独立完成工作,而且也已经有了十分方便的外部接口供调用。那么整合在一起后,它们之间该如何通信呢?我们可以简单的在创建 React Element 时增加 ref 属性达到目的:

<ClickBtn ref="btn" />
<Popup ref="pop" />

这样我们就可以通过对象的 refs 属性获取引用的组件实例。

console.log(this.refs.btn) //打印 ClickBtn 组件实例
console.log(this.refs.pop) //打印 Popup 组件实例

虽然 refs 这种方法简洁明地实现了父组件对子组件的调用,但是难以实现子组件对父组件的通知。所幸我们可以利用使用属性值传递进行父组件与子组件的通信,React 网站首页的示例中就是这么做的。

为了说明问题,我们将 popup 多增加一个功能,鼠标点击弹窗层外的区域则关闭弹出层。此时我们的新组件需要将按钮状态重置回 off 状态。简单的做如下修改:

//Popup
//给 document 元素绑定的事件中增加判断是否点击了外部区域
onMouseUp: function (e) {
 var isNotify = false
  if (this.refs.popup !== e.target && this.state.status === 'block') {
    isNotify = true
  }
  isNotify && this.props.change(e)
  this.setState({ready: false})
}
-------------------------------------------
//Component 
change: function (e) {
  //如果点到的是按钮组件,就不执行这个方法了,因为执行了 onClick 了
  if (this.refs.btn.refs.btn !== e.target) {
    this.toggle()
  }
},
render: function () {
  return (
    <div className="component">
      <div onClick={this.onClick} >
        <ClickBtn ref="btn" status={this.state.status} />
      </div>
      <Popup ref="pop" change={this.change} />
    </div>
  )
}

上面的代码中通过将新组件的 change 方法通过属性传递给 Popup 组件,然后 Popup 组件的在确认需要隐藏弹出层后利用 change 方法通知父组件。

接下来看下父组件的代码:

var Component = React.createClass({
  getInitialState: function() {
    return {status: 'off'}
  },
  onClick: function () {
    this.toggle()
  },
  change: function () {
    //如果点到的是按钮组件,就不执行这个方法了,因为执行了 onClick 了
    if (this.refs.btn.refs.btn !== e.target) {
      this.toggle()
    }
  },
  render: function () {
    return (
      <div className="component">
        <div onClick={this.onClick} >
          <ClickBtn ref="btn" />
        </div>
        <Popup ref="pop" change={this.change} />
      </div>
    )
  },
  toggle: function () {
    var refs = this.refs
    this.setState({status: this.state.status === 'off' ? 'on' : 'off'}, function () {
      if (this.state.status === 'on') {
        refs.btn.on()
        refs.pop.show()
      } else {
        refs.btn.off()
        refs.pop.hide()
      }
    })
  }
})

利用两个子组件合成了 Component 组件后,我利用 refs 调用子组件的方法,利用属性传递实现子组件对父组件的通信。

而对于外部的调用,则可以通过 ReactDOM.render 方法的返回值得到组件实例。

以我们的 Component 组件为例:

window.component = ReactDOM.render(<Component />, document.getElementById('...'));

//可以调用 Component 组件的方法啦
component.toggle()

查看 demo


总结

使用 React 的感觉还是比较方便的,组件之间互相联系起来也很容易。最近看到很多使用 React 制作的组件库,相当值得借鉴学习,同时这也确实推动了我学习 React 的热情。

在以前学习 AngularJS 的时候,也有写过点击出现可拖拽弹出层的示例代码,这也是为什么我这次选用这个例子的原因。两种方案相较,感觉 React 写起来更快一点,这倒不是从技术层面的分析,仅仅是我刚刚熟悉了 React,而 AngularJS 已经好久没用了,不看文档是不会写的了 :stuck_out_tongue:。


Thanks


penglongli commented 7 years ago

今天需要用 React + Redux(Redux-form) 做表单提交之类的,同事直接用的 react-bootstrap 的库,太难看。正准备自己实现一个 模态框 就看到这篇文章了,写的挺好的