Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
ajccom opened this issue Nov 10, 2015 · 1 comment
Open

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

ajccom opened this issue Nov 10, 2015 · 1 comment

Comments

@ajccom
Copy link
Contributor

ajccom commented Nov 10, 2015

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

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

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

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

  • React Element 中如何处理多个 CSS Class 的变换;
  • React Element 行内样式设置;
  • React 组件之间如何通信;
  • 使用 JSX 的一些注意点等。

准备工作

新建一份 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 写法 😄。另外顺带一提的是,即使是使用 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 已经好久没用了,不看文档是不会写的了 😛。


Thanks


@penglongli
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants