React는 Facebook의 뛰어난 코더 그룹이 만든 멋진 프레임워크입니다. DOM 메서드를 사용하여 몇 초 만에 필수 구성 요소를 추가하고 불필요한 구성 요소를 몇 초 만에 삭제하는 가상 DOM이 구현됩니다. React는 MVC 구조에서 V의 역할을 하는데, Flux와 함께 사용하면 M과 V를 쉽게 동기화할 수 있는 아주 멋진 프레임워크를 갖게 될 것입니다. Flux에 대해서는 나중에 이야기하겠습니다~
구성품
React에서는 이 튜토리얼의 드롭다운 탐색과 같이 HTML 요소에서 찾을 수 없는 특수 기능이 포함된 구성 요소를 생성할 수 있습니다. 각 구성 요소에는 고유한 범위가 있으므로 구성 요소를 정의한 후에는 다른 구성 요소와 전혀 상호 작용하지 않고도 해당 구성 요소를 계속해서 사용할 수 있습니다!
각 구성요소에는 render라는 함수가 있는데, 이 함수는 HTML을 효율적으로 반환하여 브라우저에 내보낼 수 있습니다. 우리는 또한 React의 다른 구성 요소를 호출할 수 있습니다. 이는 React가 바다만큼 깊다는 것을 의미합니다. 아, 우리가 생각할 수 없는 것은 없지만 우리가 할 수 없는 것은 없습니다.
JSX
React에 자주 주목하다 보면 JSX라는 것이 있다는 것을 알게 될 것입니다. JSX를 사용하면 Javascript를 포함하는 HTML이 아닌 Javascript로 HTML을 작성할 수 있습니다. 문자열이나 줄 바꿈 등에 대해 걱정할 필요가 없기 때문에 빠른 개발에 도움이 됩니다. 브라우저에서 JSX를 실행할 수 있지만 페이지 속도가 느려지므로 권장되지 않습니다. gulp와 grunt는 전처리 작업을 위한 JSX 인터프리터를 제공하므로 JSX를 사용하려면 이 기능을 켜는 것이 좋습니다.
JSX 사용
앞서 언급했듯이 각 JSX 구성 요소에는 "ViewModel"을 생성하는 render 메서드가 있습니다. HTML을 구성 요소에 반환하기 전에 이 모델(ViewModel)의 정보를 view에 넣을 수 있습니다. 즉, HTML이 동적으로 변경됩니다. 이 모델(예: 동적 목록)을 기반으로 합니다.
작업이 끝나면 렌더링하려는 항목을 반환할 수 있습니다. JSX를 사용하면 매우 쉽습니다
var ExampleComponent = React.createClass({ render: function () { return ( <div className="navigation"> Hello World! </div> ); } });
브라우저에서 이 코드를 실행하면 Javascript에서 87a288dd01dd4e56dfc6fb1912fca6c6 문자가 따옴표로 묶여 있기 때문에 구문 오류만 발생합니다. 코드에서 JSX 인터프리터를 실행하면 다음과 같이 변환됩니다.
var ExampleComponent = React.createClass({ render: function () { return ( React.createElement('div', {className: 'navigation'}, 'Hello World!') ); } });
예제를 테스트하려면 여기를 클릭하세요. 저는 브라우저 JSX 인터프리터를 사용하고 있습니다(권장하지는 않지만 JSFiddle을 위한 것입니다).
실행 중입니다! JSX는 React.createElement를 사용하여 생성한 모든 DOM 노드를 해석하여 노드 유형, 매개변수 및 콘텐츠를 생성합니다. JSX 없이도 할 수 있지만 이는 React.createElement를 제외한 모든 DOM 노드를 수동으로 작성해야 함을 의미합니다. 수많은 예제에서 JSX를 사용하라고 나와 있습니다.
왜 DOM에서 class 대신 className을 사용하는지 궁금하실 겁니다. 이는 클래스가 Javascript에서 예약어이기 때문입니다. JSX가 코드를 해석할 때 이 노드의 모든 속성을 객체로 변경하지만 객체를 속성으로 처리할 수는 없습니다.
속성에 변수 사용
구성 요소의 스타일 클래스(또는 다른 속성 값)를 동적으로 변경하려면 변수를 사용할 수 있습니다. 하지만 변수 이름만 전달할 수는 없으며 한 쌍의 중괄호로 묶어야 합니다. 이렇게 하면 JSX는 이것이 외부 변수라는 것을 알게 될 것입니다.
var ExampleComponent = React.createClass({ render: function () { var navigationClass = 'navigation'; return ( <div className={ navigationClass }> Hello World! </div> ); } });
이 기능은 여기서 보실 수 있습니다.
초기 렌더러
처음 React 구성 요소를 렌더링할 때 React에 렌더링할 구성 요소를 알려주고 구성 요소를 렌더링할 위치를 나타내기 위해 기존 DOM 노드를 지정해야 합니다.
var ExampleComponent = React.createClass({ render: function () { return ( <div className="navigation"> Hello World! </div> ); } }); React.render(<ExampleContent />, document.body);
본문 노드에서 구성 요소를 렌더링합니다. 그러면 평소처럼 다른 구성 요소를 호출하거나 React를 사용하여 렌더링하지 않으려는 경우 원하는 만큼 렌더링 기능을 사용할 수 있습니다. 전체 페이지이지만 여전히 여러 구성 요소를 사용하고 싶습니다.
컴포넌트의 기본
구성요소는 고유한 "상태"를 가질 수 있습니다. 이를 통해 동일한 구성 요소를 여러 번 재사용할 수 있지만 각 구성 요소 인스턴스의 상태가 고유하기 때문에 완전히 다르게 보이도록 할 수 있습니다.
구성요소에 속성을 전달하는 것을 속성이라고 합니다. HTML 속성에만 국한하지 말고 원하는 것은 무엇이든 전달하고 구성 요소 내부의 this.props를 통해 액세스할 수 있습니다. 이를 통해 동일한 구성 요소를 재사용하지만 구성 요소의 "구성"과 같은 다른 속성 집합을 전달할 수 있습니다.
속성
이전의 "Hello World!" 예제를 기반으로 HTML 노드에 className 속성이 있습니다. 구성 요소 내에서 this.props.classname을 사용하여 이 값에 액세스할 수 있지만 앞서 언급한 것처럼 원하는 대로 전달할 수 있습니다. 드롭다운의 경우 탐색을 개체로 구성해야 하며, 구성 요소는 이를 렌더링할 구성으로 사용합니다. 시작해 보세요—
var navigationConfig = []; var Navigation = React.createClass({ render: function () { return ( <div className="navigation"> </div> ); } });
React.render(75a8dba852246be4150860216e62d5c1, document.body);
如果现在能访问 this.props.config 的话,我们会受到一个空数组(navigationConfig 的值)。在我们进入到真正导航的编码前先让我们说明一下状态。
状态
如之前所讨论的,每一个组件都有其自己的”状态”。当要使用状态时,你要定义初始状态,让后才可以使用 this.setState 来更新状态。无论何时状态得到了更新,组件都会再一次调用 render 函数,拿新的值去替换或者改变之前渲染的值。这就是虚拟 DOM 的奥义 - 计算差异的算法实在 React 内部完成的,因此我们不用去以来 DOM 的更新了(因为 DOM 很慢)。React 会计算出差异并产生一堆指令的集合 (例如,加入向”navigation__link“加入”active“类,或者移除一个节点),并在 DOM 上执行它们。
使用导航,我们将下拉菜单打开保持在状态中。为此,添加一个 getInitialState 函数到类配置对象上,并返回带有我们想要的初始状态的对象。
var navigationConfig = []; var Navigation = React.createClass({ getInitialState: function () { return { openDropdown: -1 }; }, render: function () { return ( <div className="navigation"> </div> ); } }); React.render(<Navigation config={ navigationConfig } />, document.body);
你会发现我们将其设置成了-1。当我们准备去打开一个下拉菜单时,将使用状态里面的配置数组中的导航项的位置,而由于数组索引开始于0,我们就得使用 -1 来表示还没有指向一个导航项。
我们可以使用 this.state 来访问状态,因此如果我们去观察 atthis.state.openDropdown,应该会有 -1 被返回。
组件的生命周期
每个组件都有其“生命周期”—这是一系列你可以在组件配置里定义的函数,它们将在部件生命周期内被调用。我们已经看过了 getinitialstate 一它只被调用一次,在组件被挂载时调用。
componentWillMount
当组件要被挂载时这个函数被调用。这意味着我们可以在此运行组件功能必须的代码。为由于 render 在组件生命周期里被多次调用,我们一般会把只需要执行一次的代码放在这里,比如 XHR 请求。
var ExampleComponent = React.createClass({ componentWillMount: function () { // xhr request here to get data }, render: function () { // this gets called many times in a components life return ( <div> Hello World! </div> ); } });
componentDidMount
一旦你的组件已经运行了 render 函数,并实际将组件渲染到了 DOM 中,componentDidMount 就会被调用。我们可以在这儿做任何需要做的 DOM 操作,已经任何需要依赖于组件已经实际存在于 DOM 之后才能做的事情, 例如渲染一个图表。你可以通过调用 this.getDOMNode 在内部访问到 DOM 节点。
var ExampleComponent = React.createClass({ componentDidMount: function () { var node = this.getDOMNode(); // render a chart on the DOM node }, render: function () { return ( <div> Hello World! </div> ); } });
componentWillUnmount
如果你准备吧组件从 DOM 移除时,这个函数将会被调用。这让我们可以在组件背后进行清理,比如移除任何我们已经绑定的事件监听器。如果我们没有在自身背后做清理,而当其中一个事件被触发时,就会尝试去计算一个没有载入的组件,React 就会抛出一个错误。
var ExampleComponent = React.createClass({ componentWillUnmount: function () { // unbind any event listeners specific to this component }, render: function () { return ( <div> Hello World! </div> ); } });
组件方法
React 也为组件提供了更方便我们工作的方法。它们会在组件的创建过程中被调用到。例如getInitialState,能让我们定义一个默认的状态,这样我们不用担心在代码里面对状态项目是否存在做进一步的检查了。
getDefaultProps
当我们创建组件时,可以按照我们的想法为组件的属性定义默认值。这意味着我们在调用组件时,如果给这些属性设置值,组件会有一个默认的“配置”,而我们就不用操心在下一行代码中检查属性这类事情了。
当你定义了组件时,这些默认的属性会被缓存起来,因而针对每个组件的实例它们都是一样的,并且不能被改变。对于导航组件,我们将配置指定为一个空的数组,因而就算没有传入配置,render 方法内也不会发生错误。
var Navigation = React.createClass({ getInitialState: function () { return { openDropdown: -1 }; }, getDefaultProps: function () { return { config: [] } }, render: function () { return ( <div className="navigation"> </div> ); } }); <p>React.render(<Navigation config={ navigationConfig } />, document.body);</p>
propTypes
我们也可以随意为每一个属性指定类型。这对于我们检查和处理属性的意外赋值非常有用。如下面的dropdown,我们指定只有数组才能放入配置。
var Navigation = React.createClass({ getInitialState: function () { return { openDropdown: -1 }; }, getDefaultProps: function () { return { config: [] } }, propTypes: { config: React.PropTypes.array }, render: function () { return ( <div className="navigation"> </div> ); } }); React.render(<Navigation config={ navigationConfig } />, document.body);
mixins
我们也可以在组件中加入 mixins。这是一个独立于 React 的基本组件(只是一个对象类型的配置)。这意味着,如果我们有两个功能类似的组件,就可以共享一个配置了(如果初始状态相同)。我们可以抽象化地在 mixin 中建立一个方法,这样就不用把相同的代码写上两次了。
var ExampleMixin = { componentDidMount: function () { // bind some event listeners here }, componentWillUnmount: function () { // unbind those events here! } }; var ExampleComponent = React.createClass({ mixins: [ExampleMixin], render: function () { return ( <div> Hello World! </div> ); } }); var AnotherComponent = React.createClass({ mixins: [ExampleMixin], render: function () { return ( <div> Hello World! </div> ); } });
这样全部组件都有一样的 componentDidMount 和 componentWillUnmount 方法了,保存我们重写的代码。无论如何,你不能 override(覆盖)这些属性,如果这个属性是在mixin里设置的,它在这个组件中是不可覆盖的。
遍历循环
当我们有一个包含对象的数组,如何循环这个数组并渲染每一个对象到列表项中?JSX 允许你在任意 Javascript 文件中使用它,你可以映射这个数组并返回 JSX,然后使用 React 去渲染。
var navigationConfig = [ { href: 'http://ryanclark.me', text: 'My Website' } ]; var Navigation = React.createClass({ getInitialState: function () { return { openDropdown: -1 }; }, getDefaultProps: function () { return { config: [] } }, propTypes: { config: React.PropTypes.array }, render: function () { var config = this.props.config; var items = config.map(function (item) { return ( <li className="navigation__item"> <a className="navigation__link" href={ item.href }> { item.text } </a> </li> ); }); return ( <div className="navigation"> { items } </div> ); } }); React.render(<Navigation config={ navigationConfig } />, document.body);
在 JSFilddle 中随意使用 navigationConfigin
导航配置由数组和对象组成,包括一个指向超链接的 href 属性和一个用于显示的 text 属性。当我们映射的时候,它会一个个依次通过这个数组去取得对象。我们可以访问 href 和 text,并在 HTML 中使用。当返回列表时,这个数组里的列表项都将被替换,所以我们把它放入 React 中处理时它将知道怎么去渲染了!
混编
到目前位置,我们已经做到了所有下拉列表的展开。我们需要知道被下来的项目是哪个,我们将使用 .children 属性去遍历我们的 navigationConfig 数组。接下来,我们可以通过循环来操作下拉的子元素条目。
var navigationConfig = [ { href: 'http://ryanclark.me', text: 'My Website', children: [ { href: 'http://ryanclark.me/how-angularjs-implements-dirty-checking/', text: 'Angular Dirty Checking' }, { href: 'http://ryanclark.me/getting-started-with-react/', text: 'React' } ] } ]; var Navigation = React.createClass({ getInitialState: function () { return { openDropdown: -1 }; }, getDefaultProps: function () { return { config: [] } }, propTypes: { config: React.PropTypes.array }, render: function () { var config = this.props.config; var items = config.map(function (item) { var children, dropdown; if (item.children) { children = item.children.map(function (child) { return ( <li className="navigation__dropdown__item"> <a className="navigation__dropdown__link" href={ child.href }> { child.text } </a> </li> ); }); dropdown = ( <ul className="navigation__dropdown"> { children } </ul> ); } return ( <li className="navigation__item"> <a className="navigation__link" href={ item.href }> { item.text } </a> { dropdown } </li> ); }); return ( <div className="navigation"> { items } </div> ); } }); React.render(<Navigation config={ navigationConfig } />, document.body);
实例在这里 - 但是我们还是能看见下来条目,尽管我们将 openDropdown 设置成为了 -1 。
我们可以通过在组件中访问 this.state ,来判断下拉是否被打开了,并且,我们可以为其添加一个新的 css 样式 class 来展现鼠标 hover 的效果。
var navigationConfig = [ { href: 'http://ryanclark.me', text: 'My Website', children: [ { href: 'http://ryanclark.me/how-angularjs-implements-dirty-checking/', text: 'Angular Dirty Checking' }, { href: 'http://ryanclark.me/getting-started-with-react/', text: 'React' } ] } ]; var Navigation = React.createClass({ getInitialState: function () { return { openDropdown: -1 }; }, getDefaultProps: function () { return { config: [] } }, openDropdown: function (id) { this.setState({ openDropdown: id }); }, closeDropdown: function () { this.setState({ openDropdown: -1 }); }, propTypes: { config: React.PropTypes.array }, render: function () { var config = this.props.config; var items = config.map(function (item, index) { var children, dropdown; if (item.children) { children = item.children.map(function (child) { return ( <li className="navigation__dropdown__item"> <a className="navigation__dropdown__link" href={ child.href }> { child.text } </a> </li> ); }); var dropdownClass = 'navigation__dropdown'; if (this.state.openDropdown === index) { dropdownClass += ' navigation__dropdown--open'; } console.log(this.state.openDropdown, index); dropdown = ( <ul className={ dropdownClass }> { children } </ul> ); } return ( <li className="navigation__item" onMouseOut={ this.closeDropdown } onMouseOver={ this.openDropdown.bind(this, index) }> <a className="navigation__link" href={ item.href }> { item.text } </a> { dropdown } </li> ); }, this); return ( <div className="navigation"> { items } </div> ); } }); React.render(<Navigation config={ navigationConfig } />, document.body);
在这里看实例 - 鼠标划过“My Website”,下拉即会展现。
在前面,我已经给每个列表项添加了鼠标事件。如你所见,我用的是 .bind (绑定) 调用,而非其它的方式调用 - 这是因为,当用户的鼠标划出元素区域,我们并不用关注光标在哪里,所有我们需要知晓的是,将下拉关闭掉,所以我们可以将它的值设置成为-1。但是,我们需要知晓的是当用户鼠标划入的时候哪个元素被下拉展开了,所以我们需要知道该参数(元素的索引)。 我们使用绑定的方式去调用而非简单的透过函数(function)去调用是因为我们需要通过 React 去调用。如果我们直接调用,那我们就需要一直调用,而不是在事件中调用他。
现在我们可以添加很多的条目到 navigationConfig 当中,而且我们也可以给他添加样式到下来功能当中。查看实例.