Fullstack React 学习笔记(一)

Your first React Web Application

前期准备

  • Node.js and npm
  • Git
  • Google Chrome
  • JavaScript ES6/ES7

安装依赖

npm install  

运行

npm start  

什么是组件

一个独立的 React 组件可以被看做是一个 UI 组件

class ProductList extends React.Component {  
  render() {
    return (
      <div className='ui unstackable items'>
        Hello, friend! I am a basic React component.
      </div>
    ); 
  }
}

这里我们在 html 中载入了 react <script src="vendor/react.js"></script>

ES 6 为 javascript 增加了 classes 的概念,因此 ES 6 风格的声明如下:

class HelloWorld extends React.Component {  
    render() { return(<p>Hello, world!</p>) }
}

ES 5 风格的声明:

const HelloWorld = React.createClass({  
    render() { return(<p>Hello, world!</p>) }
})

JSX

每个组件都在它的 render() 方法中描述了 view 是如何渲染成 HTML 的,而其中具体的语法就是 JSX。他是 Facebook 创建,可以看做是 JavaScript 的扩展,用来更方便写页面代码。

React 构建了一个虚拟 DOM,它的核心思想是:对复杂的文档DOM结构,提供一种方便的工具,进行最小化地 DOM 操作。

Babel

Bable 可以看做是 JavaScript 的翻译器,它可以将 ES6 代码转义为 ES5,因为目前我们大部分浏览器只支持 ES5。

在 Html 中引入

<head>  
<script src="vendor/babel-standalone.js"></script>  
</head>  

设置 app.js 的 type 为 text/babel

<script src="./js/seed.js"></script>  
<script  
  type="text/babel"
  data-plugins="transform-class-properties"
  src="./js/app.js"
></script>

Babel 还负责将 JSX 代码转义为 JavaScript。

ReactDOM.render()

我们需要通知 React 来渲染指定的 DOM 节点,即 ProductList

class ProductList extends React.Component {  
  render() {
    return (
      <div className='ui unstackable items'>
        Hello, friend! I am a basic React component.
      </div>
    ); 
  }
}

ReactDOM.render(  
  <ProductList />,
  document.getElementById('content')
);

我们为 ReactDOM.render() 传入两个参数:

ReactDOM.render([what], [where]);  

在 React 中,原生的 HTML 元素总是以小写字母开头,而 React 组件总以大写字母开头。

我们现在用 ES6 的类和 JSX 写好了列表组件,接着指定 Babel 转义成 ES5 代码,最后通过 ReactDOM.render() 将该组件写入 DOM 中。

组件元素是可以嵌套渲染的,即父组件渲染自身的子组件。

构建 Product

现在我们来构建子组件 Product,对于每个 product 我们都会添加 image,标题,描述和用户头像

class Product extends React.Component {  
  render() {
    return (
      <div className='item'>
        <div className='image'>
          <img src='images/products/image-aqua.png' />
        </div>
        <div className='middle aligned content'>
          <div className='description'>
            <a>Fort Knight</a>
            <p>Authentic renaissance actors, delivered in just two weeks.</p>
          </div>
          <div className='extra'>
            <span>Submitted by:</span>
            <img
              className='ui avatar image'
              src='images/avatars/daniel.jpg'
            />
          </div>
        </div>
      </div>
    ); 
  }
}

ReactDOM.render(  
  <ProductList />,
  document.getElementById('content')
);

我们使用了 SemanticUI,另外在 JSX 中使用

而不是
,也是为了和 HTML 做区分。

为了使用 Product 组件,我们需要把该组件添加到父组件的 render 方法中:

class ProductList extends React.Component {  
  render() {
    return (
      <div className='ui unstackable items'>
        <Product />
      </div> 
    );
  } 
}

数据驱动的 Product

数据驱动的 Product 组件允许我们基于数据动态渲染 UI,先来熟悉一下 product 的数据模型:Seed.products 是一个 JavaScript 数组对象,每个元素都表示一个 product 对象

const products = [  
  {
    id: 1,
    title: 'Yellow Pail',
    description: 'On-demand sand castle construction expertise.',
    url: '#',
    votes: generateVoteCount(),
    submitterAvatarUrl: 'images/avatars/daniel.jpg',
    productImageUrl: 'images/products/image-aqua.png',
  },

使用 props

我们想要修改 Product 组件,让其不再使用硬编码的数据,相反从其父类组件接收数据。数据流向如下:

在 React 中数据由父组件流向子组件主要通过 props,即当父组件渲染子组件时,向下发送的数据取决于子组件依赖的 props。

我们首先来修改 ProductList 将 props 向下传递给 Product,seed.js(数据源)已经提供了 products 数组,我们可以从中创建一个 product:

class ProductList extends React.Component {  
  render() {
    const product = Seed.products[0];
    return (
      <div className='ui unstackable items'>
        <Product
          id={product.id}
          title={product.title}
          description={product.description}
          url={product.url}
          votes={product.votes}
          submitterAvatarUrl={product.submitterAvatarUrl}
          productImageUrl={product.productImageUrl}
        />
      </div>
    );
  }
}

从 model 里创建一个 Product

在 JSX 中,大括号是一个分隔符,标志中间是一个 JavaScript 表达式;而其他的分隔符比如单引号通常在字符串上使用。id='1'

JSX 属性指必须用大括号或引号分隔

ES6 中更习惯于使用 const 和 let 而不是 var

  • const 表明变量不会再被重新赋值
  • let 变量用于会被重新赋值的情形

这两种都在非函数作用域下具有块级作用域,比起 var 更加安全

在 ProductList 组件将 props 传递给 Product 之前,我们先要完成 Product 的改造,在 React 中,一个组件能够通过 this.props 访问它的所有 props,在 Product 中,this.props 对象类似于:

{
  "id": 1,
  "title": "Yellow Pail",
  "description": "On-demand sand castle construction expertise.",
  "url": "#",
  "votes": 41,
  "submitterAvatarURL": "images/avatars/daniel.jpg",
  "productImageUrl": "images/products/image-aqua.png"
}

让我们把 Product 中硬编码的部分替换为 props

class Product extends React.Component {  
  render() {
    return (
      <div className='item'>
        <div className='image'>
          <img src={this.props.productImageUrl} />
        </div>
        <div className='middle aligned content'>
          <div className='header'>
            <a>
              <i className='large caret up icon' />
            </a>
            {this.props.votes}
          </div>
          <div className='description'>
            <a href={this.props.url}>
              {this.props.title}
            </a>
            <p>
              {this.props.description}
            </p>
          </div>
          <div className='extra'>
            <span>Submitted by:</span>
            <img
              className='ui avatar image'
              src={this.props.submitterAvatarUrl}
            />
          </div>
        </div>
      </div>
    );
  }
}

注意这里我们的属性值使用了 JSX 的 {} 作为分隔符,这种 props 穿插在 HTML 元素中间的方式让我们可以动态地创建数据驱动的 React 组件。

渲染多个 products

我们这次通过 map 函数将数据源数组 Seed.products 初始化为一个 productComponents 数组

class ProductList extends React.Component {  
  render() {
    const productComponents = Seed.products.map((product) => (
      <Product
        key={'product-' + product.id}
        id={product.id}
        title={product.title}
        description={product.description}
        url={product.url}
        votes={product.votes}
        submitterAvatarUrl={product.submitterAvatarUrl}
        productImageUrl={product.productImageUrl}
    /> 
));

productComponents 数组看上去是这样,一个 JSX 实例的数组:

[
  <Product id={1} ... />,
  <Product id={2} ... />,
  <Product id={3} ... />,
  <Product id={4} ... />
]

数组的 map 方法最终返回一个新数组

这里多了一个 key={'product-' + product.id} prop。React 使用这个特定的属性来为每个 Product 组件创建唯一的绑定;主要是 React 框架会用到这个 prop。

接着修改 render 方法的 retrun 返回值,替换为上面的 productComponents 数组

return (  
  <div className='ui unstackable items'>
    {productComponents}
  </div> 
);

现在我们有 5 个 product

先来排个序

class ProductList extends React.Component {  
  render() {
    const products = Seed.products.sort((a, b) => (
      b.votes - a.votes
    ));
    const productComponents = products.map((product) => (
      <Product

React the vote 添加交互性

我们期望点击投票按钮 Product 组件的投票数字会发生变化,但是 Product 组件自身不能修改它的投票数,this.props 是不可变的。

虽然子(组件)可以读取 props,但它不能修改它们。一个孩子并不拥有它们的 props。在我们的应用中,父组件 ProductList 拥有 props 并提供给子组件,即数据的单向流动

A child component does not own its props. Parent components own the props of child components.

我们需要点击子组件之后,通知父组件来更新投票数,更新完数据源后,新数据又再次向下流入子组件。

传播事件

我们都知道父组件通过 props 传递数据给子组件,因为 props 是不可变的,当事件发生时,子组件需要一种方式来通知父组件,然后父组件才能做出必要的修改。

为了解决这一问题,我们可以将一个函数作为 props 向下传递。也就是说 ProductList 组件可以给每一个 Product 组件传递一个函数,而该函数当投票按钮点击时会被调用。这是一种典型的子组件向上传递事件的方式。

来练习一发,在 ProductList 定义一个函数 handleProductUpVote,接收一个参数 productId,在函数内打印 product 的 id:

class ProductList extends React.Component {  
  handleProductUpVote(productId) {
    console.log(productId + ' was upvoted.');
}
render() {  

然后作为 prop 将该函数向下传递给每一个 Product 组件,这里命名为 onVote:

const productComponents = products.map((product) => (  
  <Product
    key={'product-' + product.id}
    id={product.id}
    title={product.title} 
    description={product.description} 
    url={product.url}
    votes={product.votes} 
    submitterAvatarUrl={product.submitterAvatarUrl}
    productImageUrl={product.productImageUrl} 
    onVote={this.handleProductUpVote}
  /> 
));

这样我们在子组件 Product 中就能通过 this.props.onVote 来访问这个函数了,在子组件中定义一个函数来调用父组件的方法

// Inside `Product`
  handleUpVote() {
    this.props.onVote(this.props.id);
}
render() {  

这里我们传递了子组件 Product 的 id,下面我们通过 HTML 的 onClick 属性标记来触发投票按钮

{/* Inside `render` for Product` */}
<div className='middle aligned content'>  
  <div className='header'>
    <a onClick={this.handleUpVote}>
      <i className='large caret up icon' />
    </a>
    {this.props.votes}
  </div>

目前看似一切就绪,但就是不工作,原因出在 this.props.id

handleUpVote() {  
    this.props.onVote(this.props.id);
}

在 render() 方法内部中,this 总是和当前组件绑定在一起的,但是在自定义组件的方法 handleUpVote() 中,this 其实为 null。

绑定自定义的组件方法

在 JavaScript 中,this 的绑定取决于当前的 context,在 render() 内部我们说 this 绑定在当前组件上,这是因为 React 替我们做了绑定 this 的操作,而且对于所有的 React 方法,React 都会自动绑定 this 到组件上。

但是针对我们自定义的组件方法,需要自己手动来绑定 this,关于绑定比较好的位置可以放在构造函数 constructor() 中去完成:

class Product extends React.Component {  
  constructor(props) {
    super(props); // always call this first

    // custom method bindings here
    this.handleUpVote = this.handleUpVote.bind(this);
  }

现在点击投票按钮,观察 console 就有事件日志产生了。

不过怎么执行更新操作呢,我们这里并没有相关的 datastore,而且事实上如果去尝试更新 Seed.products 的话

// Would this work?              
Seed.products.forEach((product) => {  
  if (product.id === productId) {
    product.votes = product.votes + 1;
  }
});

可惜这样不能工作,因为当我们更新 Seed 时,React app 并不会通知数据源变化,界面元素也并不会发生改变

ES6 箭头函数和 this

传统的 JavaScript 函数声明语法 function () {} 会将 this 绑定再一个匿名的全局对象上,这会导致一些问题。

function printSong() {  
console.log("Oops - The Global Object");  
}
const jukebox = { songs: [  
    {
      title: "Wanna Be Startin' Somethin'",
      artist: "Michael Jackson",
    }, 
    {
      title: "Superstar",
      artist: "Madonna",
    },
  ],
  printSong: function (song) {
    console.log(song.title + " - " + song.artist);
  },
  printSongs: function () {
    // 'this' bound to the object (OK)
    this.songs.forEach(function (song) {
      // 'this' bound to global object (bad)                                        
      this.printSong(song); 
    });
  }, 
}

jukebox.printSongs();  
// > "Oops" - The Global Context"
// > "Oops" - The Global Context"

而箭头函数的优势在于替我们自动绑定了 this 到当前环境

// ...
  printSongs: function () { 
    this.songs.forEach((song) => {
      // 'this' bound to same 'this' as 'printSongs()'('jukebox')                                                                  
      this.printSong(song); 
    });
  }, 
}

jukebox.printSongs();  
// > "Wanna Be Startin' Somethin' - Michael Jackson"
// > "Superstar - Madonna"

使用 state

不同于 props 是不可变的且被父组件拥有,state 被组件自身所拥有。this.state 对于组件来说是私有的并且可以通过 this.setState() 方法来更新

严格来讲,当组件的 state 或 props 更新时,component 会重新渲染,所有的 React Component 都只是一个函数,通过其 this.props 和 this.state 进行渲染的。而且这种渲染结果是固定的,即给定一组 props 和 state,渲染出来的 React 组件是唯一的。

我们认为投票数据是状态变化的,父组件作为 state 的拥有者,它作为 props 传递给子组件 Product。

当为组件添加 state 时,需要做的第一件事就是定义初始值,而构造函数是个绝佳的地方。在 React 组件中,state 可以看做是一个对象。ProductList 的 state 对象如下:

{
  products: <Array>,
}

在构造函数中初始化一个空的 products 数组

class ProductList extends React.Component {  
  constructor(props) {
    super(props);
    this.state = {
      products: [],
  }; 
}

componentDidMount() {  
  this.setState({ products: Seed.products });
}

接着修改数据源,这一次从 state 中而不是 Seed 里获取数据:

render() {  
  const products = this.state.products.sort((a, b) => (
    b.votes - a.votes
  ));

通过 this.setState() 来设置 state

一开始设置一个空 state 是个好习惯,稍后 Components & Servers 的章节再说明为什么

React 定义了一组生命周期方法,当组件被安装到页面上之后,会调用 componentDidMount() 方法,下面我们尝试在其中设置 ProductList 的 state

class ProductList extends React.Component {  
// 很不幸,这样是无效的?                    
  componentDidMount() { 
    this.state = Seed.products;
  }
// ...      
}

state 只能在构造函数中修改,即在初始化之后想再次修改 state,必须通过 React 提供的 this.setState() 来搞定。此外该方法也会触发 React 组件的重新渲染。

永远不要在 this.setState 之外设置 state

再次更新正确的写法:

class ProductList extends React.Component {  
  constructor(props) {
    super(props);
    this.state = {
      products: [],
    }; 
  }

  componentDidMount() {
    this.setState({ products: Seed.products });
  }

更新 state 以及探讨不可变性

因为只能使用 this.setState() 来修改 state,所以当 component 可以更新它的 state 时,要把 this.state 对象看做不可变。

但某些行为还是会无意间修改 this.state 的值,比如我们有一组数字数组存储在 state 中

this.setState({nums:[1,2,3]});  

如果想要 nums 数组包括 4,尝试使用 push() 来实现

const nextNums = this.state.nums.push(4);  
this.setState({ nums: nextNums });  

表面上看 this.state 是不可变的,但这里存在一个问题,push() 方法会修改原始数组,验证一下:

console.log(this.state.nums);  
// [1, 2, 3]    
const nextNums = this.state.nums.push(4);  
console.log(nextNums);  
// [1, 2, 3, 4]
console.log(this.state.nums);  
// [1, 2, 3, 4] <-- Uh-oh!                      

如果我们先赋值了一个新值,然后再去 push(),此时依然不会成功

const nextNums = this.state.nums;  
nextNums.push(4);  
console.log(nextNums);  
// [1, 2, 3, 4]
console.log(this.state.nums);  
// [1, 2, 3, 4] <-- Nope!                     

因为二者指向同一块内存区域

我们可以使用数组的 concat() 方法来创建一个新的数组,包含所有传递进来的参数

console.log(this.state.nums);  
// [1, 2, 3]              
const nextNums = this.state.nums.concat(4);  
console.log(nextNums);  
// [1, 2, 3, 4]
console.log(this.state.nums);  
// [1, 2, 3] <-- Unmodified!                  

了解到这一点,下面的方法就会有问题:

// Inside 'ProductList'
// Invalid
handleProductUpVote(productId) {  
  const products = this.state.products; 
  products.forEach((product) => {
    if (product.id === productId) { 
      product.votes = product.votes + 1;
    } 
  });
  this.setState({ 
    products: products,
  }); 
}

当我们使用 this.state.products 来初始化 products 时,引用的是内存中同一个数组:

所以当我们在 forEach() 中修改 product 对象的投票数时,会同时修改原始的 product 对象。

为了解决这一问题,我们使用对象的 assign() 方法:

handleProductUpVote(productId) {  
  const nextProducts = this.state.products.map((product) => {
    if (product.id === productId) {
      return Object.assign({}, product, {
        votes: product.votes + 1,
      });
    } else {
      return product;
    }
  });

  this.setState({
    products: nextProducts,
  });
}

修改 state 会触发 UI 页面刷新

ES 6 中的 assign() 方法

主要用做复制,该方法会创建一个新对象,而不是修改原对象。

const coffee = { };  
const noCream = { cream: false };  
const noMilk = { milk: false };  
Object.assign(coffee, noCream);  
// coffee is now: '{cream: false}'
Object.assign(coffee, noMilk);  
// coffee is now: '{cream: false, milk: false}'                

接收三个参数,第一个是新对象,第二个是要复制的范本,第三个是要在第二个的基础上要做的修改,最终复制到第一个参数上返回

const coffeeWithMilk = Object.assign({}, coffee, { milk: true });  
// coffeeWithMilk is: '{ cream: false, milk: true }'
// coffee was not modified: '{ cream: false, milk: false }'

最后别忘了在 ProductList 的构造函数中为我们自定义的组件方法绑定 this

class ProductList extends React.Component {  
  constructor(props) {
    super(props);
    this.state = {
      products: [],
    };

    this.handleProductUpVote = this.handleProductUpVote.bind(this);
  }

使用 Babel plugin 重构

我们已经在工程中使用了 babel-standalone,它默认使用两个配置(preset),preset 其实是一组插件用来支持特定的语言特性

  • es201520: Adds support for ES2015 (or ES6) JavaScript
  • react21: Adds support for JSX

下面我们要介绍一个新的特性:属性初始化

属性初始化

属性初始化其实还是一个试验阶段的提议,它提供了编译语法简化了 React class components,该特性已经集成在 transform-class-properties 插件中了,还记得如何引用它:

<script  
  type="text/babel" 
  data-plugins="transform-class-properties" 
  src="./js/app.js"
></script>

重构 Product

在 Product 组件中,我们自定义了 handleUpVote 方法,因为它不是 React 组件 API 的一部分,React 并不会绑定 this 到组件上,所以我们才会在构造方法中手动绑定:

class Product extends React.Component {  
  constructor(props) {
    super(props);
    this.handleUpVote = this.handleUpVote.bind(this);
  }
  handleUpVote() {
    this.props.onVote(this.props.id);
  } 
  render() {

不过有了 transform-class-properties 插件后,我们可以用箭头函数来省略掉手动 this 绑定这一过程。

class Product extends React.Component {  
  handleUpVote = () => (
    this.props.onVote(this.props.id)
  );
  render() {

重构 ProductList

有了属性初始化这一插件,我们就不用再到构造函数中去定义 state 啦

class ProductList extends React.Component {  
  state = {
    products: [],
};

同理 handleProductUpVote 函数也能改成箭头函数的写法

handleProductUpVote = (productId) => {  
    ...
}

总结一下,通过属性初始化 Property initializers 特性

  1. 我们可以使用箭头函数来避免手动绑定 this
  2. 我们可以在构造函数之外定义初始 state

chengway

认清生活真相之后依然热爱它!

Subscribe to Talk is cheap, Show me the world!

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!