React.js生态系统概览 [译]

前端之家收集整理的这篇文章主要介绍了React.js生态系统概览 [译]前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

JavaScript领域发展速度很快,甚至有人认为这已经引起了负效应。一个前端库从早期开发的小玩具,到流行,再到过时,可能也就几个月时间。判断一个工具能否在几年内依然保持活力都快成了一门艺术了。

React.js在两年前发布时,我刚开始学Angular,React在我看来只是又一个模板库而已。这两年间,Angular得到了JavaScript开发者的认同,它几乎成了现代前端开发的代名词。我还看到一些很保守的团队都在用它,这让我觉得Angular好像就是未来。

但突然发生了件奇怪的事,Angular好像成了奥斯本效应的受害者,或者说它被提前宣布了死亡。Angular团队宣布,Angular 2将会完全不同,基本没有从Angular 1升级迁移的东西,而且Angular 2在接下来的一年里还用不了。这告诉了那些想开发新Web项目的人:你想用一个马上要被淘汰了的框架写项目吗?

开发者们的忧虑影响到了正在建立的React社区,但React总标榜它只是MVC中的视图层(V),让一些依赖完整MVC框架做开发的人感觉有点失望。如何补充其他部分的功能?自己写吗?还是用别的三方库?要是的话该选哪一个呢?

果然,Facebook(React.js的创始)出了另一个杀手锏:Flux工作流,它声称要填补模型层(M)和控制层(C )的功能。Facebook还称Flux只是一种“模式”,不是个框架,他们的Flux实现只是这个模式的一个例子。就像他们所说,这个实现过于简单,但还是要写很多代码和一堆重复的模板才跑得起来。

这时开源社区发力了,一年后便有了各种Flux实现库,甚至都出来比较他们的元项目了。Facebook激起了社区的兴趣,不是给出现成的东西,而是鼓励大家提出自己的解决方案,这点很不错。

当你要结合各种库开发一个完整架构时,摆脱了框架的束缚,独立的库还可以在很多地方重用,在自己构建架构的过程中,这个优点很明显。

这便是为什么React相关的东西这么有意思。它们可以很容易地在其他JavaScript环境中实现重用。就算你不打算用React,看看它的生态系统都能受到启发。可以试试强大又容易配置的模块化打包工具Webpack来简化构建系统,或者用Babel转译器马上开始用ECMAScript 6甚至ECMAScript 7来写代码

在这篇文章里我会给你概览一遍这些有意思的库和特性,来探索下React整个生态系统吧。

构建系统

创建一个新的Web项目时,首先要考虑的可能就是构建系统了。它不只是做为个运行脚本的工具,还能优化你的项目结构。一个构建系统必须能包括下面几个最主要功能

@H_404_24@
  • 管理内部与外部依赖
  • 运行编译器和预处理器(例如CoffeeScript与SASS)
  • 为生产环境优化资源(例如Uglify)
  • 运行开发环境的Web Server,文件监控,浏览器自动刷新
  • 最近几年,YeomanBowerGrunt被誉为现代前端开发的三剑客。他们解决生成基础模板,包管理和各种通用任务问题,后面也很多人从Grunt换到了Gulp

    在React的生态系统里,基本上可以丢掉这些东西了,不是说你用不到他们,而是说可以用更先进的WebpackNPM。怎么做到的呢?Webpack是一个模块化打包工具,用它可以在浏览器环境下使用Node.js中常用的CommonJS模块语法。其实它要更简单点,因为你不用为了前端另外学一种包管理方案。只需要用NPM,就可以做到服务端与前端模块的共用。也不用处理JS文件按顺序加载的问题,因为它能够从每个文件的import语法中推测出依赖关系,整个串联成一个可以在浏览器中加载的脚本。

    Webpack

    更强大的是Webpack不只像同类工具Browserify,它还可以处理其他类型的资源。例如用加载器,可以将任何资源文件转换成JavaScript函数,去内联或加载引用到的文件。用不着手工预处理还有从HTML中引用资源了,只要在JavaScript中requireCSS/SASS/LESS文件就好了,Webpack会根据配置文件的描述去搞定一切。它还提供了个开发环境的Web Server,文件监控器,你可以在package.json中使用scripts域去定义一个任务:

    {
      "name@H_403_52@": "react-example-filmdb"@H_403_52@,"version@H_403_52@": "0.0.1"@H_403_52@,"description@H_403_52@": "Isomorphic React + Flux film database example"@H_403_52@,"main@H_403_52@": "server/index.js"@H_403_52@,"scripts@H_403_52@": {
        "build@H_403_52@": "./node_modules/.bin/webpack --progress --stats --config ./webpack/prod.config.js"@H_403_52@,"dev@H_403_52@": "node --harmony ./webpack/dev-server.js"@H_403_52@,"prod@H_403_52@": "NODE_ENV=production node server/index.js"@H_403_52@,"test@H_403_52@": "./node_modules/.bin/karma start --single-run"@H_403_52@,"postinstall@H_403_52@": "npm run build"@H_403_52@
      }
      ...
    }

    这些东西就可以代替Gulp与Bower了。当然,还是可以继续用Yeoman去生成应用基础模板的。要是Yeoman生成不了你需要的东西时(其实大多时候也都需要删掉那些用不到的库),还可以从Github上Clone一个基础模板,然后再改改。

    马上试试新的ECMAScript

    JavaScript在这几年有很大改善,移除糟粕稳定语言后,我们看到了很多新特性,ECMAScript 6(ES6)草案已经定稿。ECMAScript 7也已纳入标准化日程中,它们的特性也都已经被很多库采用了。

    ECMAScript 7

    可能你觉得到IE支持之前都用不上这些JS新特性,但实际我们不需要等浏览器完全支持,ES转译器已经广泛应用了。目前最好的ES转译器是Babel,它能够把ES6+代码转换成ES5,所以你马上就能用上新ES特性了(指已经在Babel中实现的那些,其实一般新出的特性也会很快被支持)。

    Babel

    新的JavaScript特性在所有前端框架里都可以用,更新的React能很好的在ES6与ES7下运行。这些新特性可以解决在用React开发时遇到的一些问题。来看下这些改善吧,它们对React项目很有用。稍后我们再看看怎样利用这些语法,来搭配使用React的工具和库。

    ES6 Classes

    面向对象编程是一种强大又广泛适用的范式,但在JavaScript里感觉有点不一样。Backbone,Ember,Angular,或React等大多数前端框架都有它们自己的定义类和创建对象的方式。在ES6中,有原生的类支持了,它简洁清晰而不用我们自己实现,例如:

    React.createClass({
      displayName: 'HelloMessage',render() {
        return <div>Hello {this.props.name}</div>;
      }
    })

    使用ES6就可以写成:

    class HelloMessage extends React.Component {
      render() {
        return <div>Hello {this.props.name}</div>;
      }
    }

    再看个详细的例子:

    'Counter',getDefaultProps: function(){
        return {initialCount: 0};
      },getInitialState: function() {
        return {count: this.props.initialCount}
      },propTypes: {initialCount: React.PropTypes.number},tick() {
        this.setState({count: this.state.count + 1});
      },render() {
        return (
          <div onClick={this.tick}> Clicks: {this.state.count} </div> );
      }
    });

    ES6可以写成:

    Counter extends Component {
      static propTypes = {initialCount: React.PropTypes.number};
      static defaultProps = {initialCount: 0};
    
      constructor(props) {
        super(props);
        this.state = {count: props.initialCount};
      }
    
      state = {count: this.props.initialCount};
      tick() {
        this.setState({count: this.state.count + 1});
      }
    
      render() {
        return (
          <div onClick={this.tick.bind(this)}> Clicks: {this.state.count} </div> );
      }
    }

    在这里不用再写getDefaultPropsgetInitialState这两个React的生命周期函数了。getDefaultProps改为了类的静态变量defaultProps,初始state也只需要定义在构造函数中。这种方式唯一的缺点是,在JSX中使用的方法的上下文不会再自动绑定到类实例了,必须用bind指定

    装饰器

    装饰器是ES7中的特性。通过一个包装函数,来增强函数或类的行为。例如想为一些组件使用同一个change handler, 而又不想inheritance antipattern,则可以用类的装饰器去实现。定义一个装饰器:

    addChangeHandler: function@H_403_52@(target@H_403_52@) @H_403_52@{
      target.prototype.changeHandler = function@H_403_52@(key,attr,event@H_403_52@) @H_403_52@{
        var@H_403_52@ state = {};
        state[key] = this@H_403_52@.state[key] || {};
        state[key][attr] = event.currentTarget.value;
        this@H_403_52@.setState(state);
      };
      return@H_403_52@ target;
    }

    在这里,函数addChangeHandler添加changeHandler方法到目标类target的实例上。

    要应用装饰器,只需要:

    MyClass = addChangeHandler(MyClass)

    或者用更优雅的ES7写法:

    @addChangeHandler
    class@H_403_52@ MyClass@H_403_52@ @H_403_52@{
      ...
    }

    因为React没有双向数据绑定,在用到input之类的控件时,代码就会比较冗余,changeHandler函数则把它简化了。第一个参数表示在state对象中使用的key,它将存储input的一个数据对象。第二个参数是属性,它表示input的值。这两个参数在JSX中被传入:

    LoginInput@H_403_52@ extends@H_403_52@ Component@H_403_52@ @H_403_52@{
      constructor@H_403_52@(props) {
        super@H_403_52@(props);
        this@H_403_52@.state = {
          login: {}
        };
      }
      render() {
        return@H_403_52@ (
          <input@H_403_52@ type@H_403_52@='text'@H_403_52@ value@H_403_52@={this.state.login.username}@H_403_52@ onChange@H_403_52@={this.changeHandler.bind(this,@H_403_52@ 'login@H_403_52@','username@H_403_52@')} />@H_403_52@ <input@H_403_52@ type@H_403_52@='password'@H_403_52@ value@H_403_52@={this.state.login.password}@H_403_52@ onChange@H_403_52@=password@H_403_52@')} />@H_403_52@ )@H_403_52@
      }
    }

    用户名输入框发生改变时,输入框的值会被直接存到this.state.login.username中,不需要一个个去写handler了。

    箭头函数

    JavaScript的动态上下文this不太直观,成了开发者的常痛了。比如在类里,包装函数中的this都被指向了全局变量。要fix这个问题,通常是把this存到外部作用域下(例如_this),然后在内部函数中用它:

    DirectoRSStore@H_403_52@ @H_403_52@{
      onFetch(directors) {
        var@H_403_52@ _this = this@H_403_52@;
        this@H_403_52@.directorsHash = {};
        directors.forEach(function@H_403_52@(x@H_403_52@)@H_403_52@{
          _this.directorsHash[x._id] = x;
        })
      }
    }

    在ES6中,函数function(x) {可以写成(x) => {。这种箭头方式的函数定义不仅将内部的this绑定到了外部作用域,而且看起来很简洁,在写大量异步代码时很有用:

    onFetch(directors) {
      this@H_403_52@.directorsHash = {};
      directors.forEach((x) => {
        this@H_403_52@.directorsHash[x._id] = x;
      })
    }

    解构赋值

    ES6中的解构赋值允许在赋值表达式左边写个复合对象:

    var@H_403_52@ o = {p: 42@H_403_52@,q: true@H_403_52@};
    var@H_403_52@ {p,q} = o;
    
    console@H_403_52@.log(p); // 42@H_403_52@
    console@H_403_52@.log(q); // true@H_403_52@

    在React中有什么实际用处吗?看下面这个例子:

    function@H_403_52@ makeRequest@H_403_52@(url,method,params@H_403_52@) @H_403_52@{
      var@H_403_52@ config = {
        url: url,method: method,params: params
      };
      ...
    }

    用解构方式,可以一次给几个键赋值。url,params这些值会被自动赋给有着同名键的对象中。这让代码更不易出错:

    403_52@) @H_403_52@{
      var@H_403_52@ config = {url,params};
      ...
    }

    解构赋值也可以用来加载一个模块的子集:

    const@H_403_52@ {clone,assign} = require@H_403_52@('lodash'@H_403_52@);
    function@H_403_52@ output@H_403_52@(data,optional@H_403_52@) @H_403_52@{
      var@H_403_52@ payload = clone(data);
      assign(payload,optional);
    }

    函数的默认,剩余,扩展参数

    在ES6中函数传参更强大了,可以为函数设置默认参数:

    http@H_403_52@(endpoint,method='GET'@H_403_52@) @H_403_52@{
      console@H_403_52@.log(method)
      ...
    }
    
    http('/api'@H_403_52@) // GET@H_403_52@

    觉得arguments用起来很麻烦?可以把剩余参数写成一个数组:

    networkAction@H_403_52@(context,...rest@H_403_52@) @H_403_52@{
      // rest is an array@H_403_52@
      return@H_403_52@ method.apply(context,rest);
    }

    要是不想调用apply()方法,可以把数组扩展成函数的参数:

    myArguments = ['foo'@H_403_52@,'bar'@H_403_52@,123@H_403_52@];
    myFunction(...myArguments);

    Generator与Async函数

    Generator是一种可以暂停执行,保存状态并稍后恢复执行的函数,每次遇到yield关键字时,就会暂停执行。Generator写法:

    function@H_403_52@* sequence@H_403_52@(from,to@H_403_52@) @H_403_52@{
      console@H_403_52@.log('Ready!'@H_403_52@);
      while@H_403_52@(from@H_403_52@ <= to) {
        yield@H_403_52@ from@H_403_52@++;
      }
    }

    调用Generator函数

    > var@H_403_52@ cursor = sequence(1@H_403_52@,0)">3@H_403_52@)
    Ready!
    > cursor.next()
    { value: false@H_403_52@ }
    > cursor.next()
    { value: 2@H_403_52@,0)">3@H_403_52@,0)">false@H_403_52@ }
    > cursor.next()
    { value: undefined@H_403_52@,0)">true@H_403_52@ }

    调用Generator函数时,会立即执行到第一次遇到的yield关键字处暂停。在调用next()函数后,返回yield后的表达式的值(value),然后Generator里再继续执行之后的代码。每次遇到yield都返回一个值,在第三次调用next()后,Generator函数终止,最后调用next()将返回{ value: undefined,done: true }

    当然咯,Generator不只能用来创建数字序列。它能够暂停和恢复函数的执行,这便可以不需要回调函数就完成异步流的控制。

    我们用异步函数来证明这一点。一般我们会做些I/O操作,这里为了简单起见,用setTimeout模拟。这个异步函数立即返回一个promise。(ES6有原生的promise):

    asyncDouble@H_403_52@(x@H_403_52@) @H_403_52@{
      var@H_403_52@ deferred = Promise@H_403_52@.defer();
      setTimeout(function@H_403_52@(@H_403_52@)@H_403_52@{
        deferred.resolve(x*2@H_403_52@);
      },0)">1000@H_403_52@);
      return@H_403_52@ deferred.promise;
    }

    然后写一个消费者函数

    consumer@H_403_52@(generator@H_403_52@)@H_403_52@{
      var@H_403_52@ cursor = generator();
      var@H_403_52@ value;
      function@H_403_52@ loop@H_403_52@(@H_403_52@) @H_403_52@{
        var@H_403_52@ data = cursor.next(value);
        if@H_403_52@ (data.done) {
          return@H_403_52@;
        } else@H_403_52@ {
          data.value.then(x => {
            value = x;
            loop();
          })
        }
      }
      loop();
    }

    这个函数接受Generator函数作为参数,只要yield有值,就会继续调用next()方法。这个例子中,yield的值是promise,所以必须等待promise调用resolve后,再递归调用loop()循环。

    resolve会调用then()方法,把resolve的结果赋值给value,value被定义在函数外部,它会被传给下一个next(value)。这次next的调用yield产生了一个返回值(即value)。这样可以不用回调函数来写异步调用了:

    myGenerator@H_403_52@(@H_403_52@)@H_403_52@{
      const@H_403_52@ data1 = yield@H_403_52@ asyncDouble(1@H_403_52@);
      console@H_403_52@.log(`Double 1 = ${data1}@H_403_52@`@H_403_52@);
      const@H_403_52@ data2 = yield@H_403_52@ asyncDouble(2@H_403_52@);
      console@H_403_52@.log(`Double 2 = ${data2}@H_403_52@`@H_403_52@);
      const@H_403_52@ data3 = yield@H_403_52@ asyncDouble(3@H_403_52@);
      console@H_403_52@.log(`Double 3 = ${data3}@H_403_52@`@H_403_52@);
    }
    
    consumer(myGenerator);

    Generator函数myGenerator将在每次遇到yield时暂停,等待消费者函数的promise去resolve。在控制台每隔1秒的输出

    Double 1@H_403_52@ = 2@H_403_52@
    Double 2@H_403_52@ = 4@H_403_52@
    Double 3@H_403_52@ = 6@H_403_52@

    上面的示例代码不推荐在生产环境中用,可以用更完善的co库,能很简单地用yield处理异步,还包含了错误处理:

    co(function@H_403_52@ *(@H_403_52@)@H_403_52@{
      var@H_403_52@ a = yield@H_403_52@ Promise@H_403_52@.resolve(1@H_403_52@);
      console@H_403_52@.log(a);
      var@H_403_52@ b = yield@H_403_52@ Promise@H_403_52@.resolve(2@H_403_52@);
      console@H_403_52@.log(b);
      var@H_403_52@ c = yield@H_403_52@ Promise@H_403_52@.resolve(3@H_403_52@);
      console@H_403_52@.log(c);
    }).catch(function@H_403_52@(err@H_403_52@)@H_403_52@{
      console@H_403_52@.error(err.stack);  
    });

    在ES7中,将异步处理的改进更近了一步,增加asyncawait关键字,无需使用Generator。上面的例子可以写成:

    async@H_403_52@ function@H_403_52@ (@H_403_52@)@H_403_52@{
      try@H_403_52@ {
        var@H_403_52@ a = await@H_403_52@ Promise@H_403_52@.resolve(1@H_403_52@);
        console@H_403_52@.log(a);
        var@H_403_52@ b = await@H_403_52@ Promise@H_403_52@.resolve(2@H_403_52@);
        console@H_403_52@.log(b);
        var@H_403_52@ c = await@H_403_52@ Promise@H_403_52@.resolve(3@H_403_52@);
        console@H_403_52@.log(c);
      } catch@H_403_52@ (err) {
        console@H_403_52@.error(err.stack);  
      }
    };

    有了这些特性,不会觉得JavaScript写异步代码麻烦了,而且在任何地方都可以用。

    Generator不仅简洁明了,还能做些用回调很难实现的事。比如Node.js中koaWeb框架的中间件。koa的目标是替换掉Express,它有个很不错的特性:中间件的上下游都可以对服务端响应做进一步修改。看看这段koa服务器代码

    // Response time logger middleware@H_403_52@
    app.use(function@H_403_52@ *(next@H_403_52@)@H_403_52@{
      // Downstream@H_403_52@
      var@H_403_52@ start = new@H_403_52@ Date@H_403_52@;
      yield@H_403_52@ next;
      // Upstream@H_403_52@
      this@H_403_52@.body += ' World'@H_403_52@;
      var@H_403_52@ ms = new@H_403_52@ Date@H_403_52@ - start;
      console@H_403_52@.log('%s %s - %s'@H_403_52@,this@H_403_52@.method,this@H_403_52@.url,ms);
    });
    
    // Response handler@H_403_52@
    app.use(function@H_403_52@ *(@H_403_52@)@H_403_52@{
      this@H_403_52@.body = 'Hello'@H_403_52@;
    });
    
    app.listen(3000@H_403_52@);

    Koa

    进入第一个中间件时(上游),遇到yield后,先进入第二个中间件(下游)将响应体设置为Hello,然后再从yield恢复执行,继续执行上游中间件代码,为响应体追加了World,然后打印了时间日志,在这里上下游共享了相同的上下文(this)。这要比Express更强大,如果用Express来写的话可能会是这样:

    var@H_403_52@ start;
    // Downstream middleware@H_403_52@
    app.use(function@H_403_52@(req,res,next@H_403_52@) @H_403_52@{
      start = new@H_403_52@ Date@H_403_52@;
      next();
      // Already returned,cannot continue here@H_403_52@
    });
    
    // Response@H_403_52@
    app.use(function@H_403_52@ (req,next@H_403_52@)@H_403_52@{
      res.send('Hello World'@H_403_52@)
      next();
    });
    
    // Upstream middleware@H_403_52@
    app.use(function@H_403_52@(req,next@H_403_52@) @H_403_52@{
      // res already sent,cannot modify@H_403_52@
      var@H_403_52@ ms = new@H_403_52@ Date@H_403_52@ - start;
      console@H_403_52@.log( 也许你已经发现问题了,使用全局的start变量在出现并发请求时会产生污染。

    用koa时,可以很容易地用Generator做异步操作:

    app.use(function@H_403_52@ *(@H_403_52@)@H_403_52@{
      try@H_403_52@ {
        const@H_403_52@ part1 = yield@H_403_52@ fs.readFile(this@H_403_52@.request.query.file1,0)">'utf8'@H_403_52@);
        const@H_403_52@ part2 = yield@H_403_52@ fs.readFile(this@H_403_52@.request.query.file2,0)">'utf8'@H_403_52@);
        this@H_403_52@.body = part1 + part2;
      } catch@H_403_52@ (err) {
        this@H_403_52@.status = 404@H_403_52@
        this@H_403_52@.body = err;
      }
    });
    
    app.listen( 可以设想下这个例子在Express中用promise和回调会是什么样子。

    上面讨论的Node.js这些跟React有关系吗?当然咯,Node是React的首选后端服务器。因为Node也用JavaScript,这样可以前后端共享代码,用来构建同构的React Web应用,稍后会介绍到。

    Flux库

    React很擅长创建视图组件,但我们还需要一种方式去管理数据和整个应用的状态。Flux应用架构被普遍认为是React最好的补充。要是你对Flux还很陌生,建议看看这篇快速导览

    Flux

    目前还没有出现大多数人都认同的Flux实现,Facebook官方的Flux实现很多人都觉得很冗余。我们主要关心能不能少写点模板代码,配置方便,并且给多层组件提供些好用的功能支持服务端渲染等等这些。可以看看这里给这些实现库列出的一些指标。我关注了Alt,Reflux,Flummox,Fluxxor,和Marty.js。

    我选择库的方式并不能说客观,但很有用。Fluxxor是我发现的第一个库,但现在看起来它有点旧了。Marty.js很有意思,有很多功能,但还是需要写很多模板代码,有些功能看起来比较多余。Reflux看起来蛮有吸引力,但感觉对初学者来说有点难,还缺少合适的文档。FlummoxAlt很相似,但Alt不需要写很多模板代码,开发也非常活跃,文档更新块,而且有个Slack社区。所以我选了Alt。

    Alt Flux

    Alt Flux

    Alt的Flux实现简单而强大。Facebook的Flux文档描述了很多关于dispatcher的东西,但在Alt中这些都可以忽略,因为dispatcher被隐式关联到了action,通常不需要写多余代码。只需要关心store,action,component。这三层对应MVC模型:store为模型层,action为控6制层,component为视图层。差异是Flux模型是单向数据流,意味着控制层不能直接修改视图层,而是触发模型层后,视图层被动修改。这对有些Angular开发者来说已经是最佳实践了。

    1. component初始化action
    2. store监听action然后更新数据
    3. component被绑定到store,当数据更新时就重新渲染

    Alt Flux

    Action

    使用Alt库时,action通常有两种写法:自动与手动。自动action用generateActions函数创建,它们直接发给dispatcher,手动方法则写成action类的方法,它们可以附带一个payload发给dispatcher。常用例子是自动action通知store有关app的一些事件。其余由手动action负责,它是处理服务端交互的首选方式。

    REST API调用这种就属于action,完整流程如下:

    1. Component触发action
    2. action创建者发起一个异步服务器请求,把结果作为一个payload发给dispatcher
    3. store监听action,对应的action处理器接收结果,然后store更新它的相应状态

    对于AJAX请求,我们可以用axios库,无缝地处理JSON数据和头数据。可以用ES7的async/await关键字,免去promise与回调。如果POST响应状态不是2XX,抛出错误,然后发出返回的数据或接收到的错误

    看个简单的Alt登录示例,在这里注销action不需要做任何事情,只需要通知store,所以可以自动生成它。登录action是手动的,会把登录数据作为一个参数发给action的创建者。从服务端获取响应后,发出数据,有错误的话就发出错误

    LoginActions@H_403_52@ @H_403_52@{
      constructor@H_403_52@() {
        // Automatic action@H_403_52@
        this@H_403_52@.generateActions('logout'@H_403_52@);
      }
    
      // Manual action@H_403_52@
      async@H_403_52@ login(data) {
        try@H_403_52@ {
          const@H_403_52@ response = await@H_403_52@ axios.post('/auth/login'@H_403_52@,data);
          this@H_403_52@.dispatch({ok: true@H_403_52@,user: response.data});
        } catch@H_403_52@ (err) {
          console@H_403_52@.error(err);
          this@H_403_52@.dispatch({ok: false@H_403_52@,error: err.data});
        }
      }
    }
    
    module@H_403_52@.exports = (alt.createActions(LoginActions));

    Store

    Flux模型的store有两个任务:作为action的处理器,保存状态。继续看看登录例子是怎么做的吧。

    创建一个LoginStore,有两个状态属性user用于当前登录用户error用于当前登录相关的错误。为了减少模板代码数量,Alt可以用一个bindActions函数绑定所有action到store类。

    LoginStore@H_403_52@ @H_403_52@{
      constructor@H_403_52@() {
        this@H_403_52@.bindActions(LoginActions);
        this@H_403_52@.user = null@H_403_52@;
        this@H_403_52@.error = null@H_403_52@;
      }
      ...

    处理器定义起来很方便,只需要在对应action名字前加on。因此login的action由onLogin处理。注意action名的首字母是陀峰命名法的大写。在LoginStore中,有以下两个处理器,被对应的action调用

    ...
      onLogin(data) {
        if@H_403_52@ (data.ok) {
          this@H_403_52@.user = data.user;
          this@H_403_52@.error = null@H_403_52@;
          router.transitionTo('home'@H_403_52@);
        } else@H_403_52@ {
          this@H_403_52@.user = null@H_403_52@;
          this@H_403_52@.error = data.error
        }
      }
    
      onlogout() {
        this@H_403_52@.user = null@H_403_52@;
      }
    }

    Component

    通常将store绑定到component的方式是用mixin。但mixin快过时了,需要找个其他方式,有个新方法是使用嵌套的component。将我们的component包装到另一个component里面,它会托管store的监听然后重新调用渲染。component会在props中接收到store的状态。这个方法对于smart and dumbcomponent的代码组织很有用,是以后的趋势。对于Alt来说,包装component是由AltContainer实现的:

    export@H_403_52@ default@H_403_52@ class@H_403_52@ Login@H_403_52@ extends@H_403_52@ Component@H_403_52@ @H_403_52@{
      render() {
        return@H_403_52@ (
          <AltContainer@H_403_52@ stores@H_403_52@={{LoginStore:@H_403_52@ LoginStore@H_403_52@}}>@H_403_52@ <LoginPage@H_403_52@/>@H_403_52@ </AltContainer@H_403_52@>@H_403_52@ )@H_403_52@}
    }

    LoginPagecomponent也用了我们之前介绍过的changeHandler装饰器。LoginStore的数据用来显示登陆失败后的错误,重新渲染则由AltContainer负责。点击登录按钮后执行loginaction,完整的Alt工作流:

    @changeHandler
    export@H_403_52@ default@H_403_52@ class@H_403_52@ LoginPage@H_403_52@ extends@H_403_52@ Component@H_403_52@ @H_403_52@{
      constructor@H_403_52@(props) {
        super@H_403_52@(props);
        this@H_403_52@.state = {
          loginForm: {}
        };
      }
      login() {
        LoginActions.login(this@H_403_52@.state.loginForm)
      }
      render() {
        return@H_403_52@ (
          <Alert@H_403_52@>@H_403_52@{{this.props.LoginStore.error}}</Alert@H_403_52@>@H_403_52@ <Input@H_403_52@ label@H_403_52@='Username'@H_403_52@ type@H_403_52@=403_52@ 'loginForm@H_403_52@','username@H_403_52@')} />@H_403_52@ <Input@H_403_52@ label@H_403_52@='Password'@H_403_52@ type@H_403_52@=password@H_403_52@')} />@H_403_52@ <Button@H_403_52@ onClick@H_403_52@={this.login.bind(this)}@H_403_52@>@H_403_52@Login</Button@H_403_52@>@H_403_52@ )@H_403_52@}
    }

    同构渲染

    同构Web应用是近些年来的热点话题,它能解决传统单页面应用最大的问题:浏览器用JavaScript动态创建HTML,如果浏览器禁用了JavaScript,内容就无法显示了,搜索引擎索引不到Web页面内容不会出现在搜索结果中。实际上有方法解决这个问题,但做的并不够好。同构的方式是通过服务端渲染内容解决问题。Node.js是服务端的JavaScript,React当然也能运行在服务端。

    一些使用单例方式的Flux库,很难做服务端渲染,单例的Flux 在遇到并发请求时,store数据就会变得混乱。一些Flux库用实例来解决这个问题,但需要在代码间传递实例。Alt实际也提供了Flux实例,但它用单例解决服务端渲染问题,它会在每次请求后清空store,以便并发请求时每次store都是初始状态。

    React.renderToString函数是服务端渲染的核心,整个React应用运行在服务端。服务端生成HTML后响应给浏览器。浏览器JavaScript运行时,再渲染剩余部分。可以用Iso库实现这点,它同时也被集成到了Alt中。

    首先,我们在服务端用alt.bootstrap初始化Flux,这会初始化Flux store数据。然后区分哪个URL对应的渲染哪个component,哪个由客户端Router渲染。由于使用alt的单例版,在每次渲染完后,需要使用alt.flush()初始化store以便为下次请求做好准备。使用iso插件,可以把Flux的状态数据序列化到HTML字符串中,以便客户端知道去哪找到这份状态数据:

    // We use react-router to run the URL that is provided in routes.jsx@H_403_52@
    var@H_403_52@ getHandler = function@H_403_52@(routes,url@H_403_52@) @H_403_52@{
      var@H_403_52@ deferred = Promise@H_403_52@.defer();
      Router.run(routes,url,function@H_403_52@ (Handler@H_403_52@) @H_403_52@{
        deferred.resolve(Handler);
      });
      return@H_403_52@ deferred.promise;
    };
    
    app.use(function@H_403_52@ *(next@H_403_52@) @H_403_52@{
      yield@H_403_52@ next;
      // We seed our stores with data@H_403_52@
      alt.bootstrap(JSON@H_403_52@.stringify(this@H_403_52@.locals.data || {}));
      var@H_403_52@ iso = new@H_403_52@ Iso();
      const@H_403_52@ handler = yield@H_403_52@ getHandler(reactRoutes,this@H_403_52@.request.url);
      const@H_403_52@ node = React.renderToString(React.createElement(handler));
      iso.add(node,alt.flush());
      this@H_403_52@.render('layout'@H_403_52@,{html: iso.render()});
    });

    在客户端,拿到了服务端的状态数据,用来初始化alt的状态。然后可以运行RouterReact.render去更新服务端生成的HTML:

    Iso.bootstrap(function@H_403_52@ (state,_,container@H_403_52@) @H_403_52@{
      // Bootstrap the state from the server@H_403_52@
      alt.bootstrap(state)
      Router.run(routes,Router.HistoryLocation,function@H_403_52@ (Handler,req@H_403_52@) @H_403_52@{
        let@H_403_52@ node = React.createElement(Handler)
        React.render(node,container)
      })
    })

    一些有用的库

    例如CSS布局容器,样式表单,按钮,验证,日期选择器等等。

    React-Bootstrap

    Twitter的Bootstrap框架应用已经非常普遍,对那些不想花时间写CSS的开发者很有用。特别是在原型开发阶段,Bootstrap不可或缺。要在React中使用Bootatrap,可以试试React-Bootstrap,它使用原生React component重新实现了Bootstrap的jQuery插件。代码简洁易懂:

    <Navbar brand='React-Bootstrap'>
      <Nav> <NavItem eventKey={1} href='#'>Link</NavItem> <NavItem eventKey={2} href='#'>Link</NavItem> <DropdownButton eventKey={3} title='Dropdown'> <MenuItem eventKey='1'>Action</MenuItem> <MenuItem eventKey='2'>Another action</MenuItem> <MenuItem eventKey='3'>Something else here</MenuItem> <MenuItem divider /> <MenuItem eventKey='4'>Separated link</MenuItem> </DropdownButton> </Nav> </Navbar> 

    个人感觉这才是简单易懂的HTML吧。

    也可以在Webpack中使用Bootstrap,看你喜欢哪个CSS预处理器。支持自定义引入的Bootstrap组件,可以在CSS代码中使用LESS或SASS的全局变量。

    React Router

    React Router几乎已经成了React的路由标准了。它支持嵌套路由,重定向,同构渲染。基于JSX的例子:

    <Router history={new@H_403_52@ BrowserHistory}>
      <Route@H_403_52@ path@H_403_52@="/"@H_403_52@ component@H_403_52@={App}@H_403_52@>@H_403_52@ <Route@H_403_52@ path@H_403_52@="about"@H_403_52@ name@H_403_52@="about"@H_403_52@ component@H_403_52@={About}@H_403_52@/>@H_403_52@ <Route@H_403_52@ path@H_403_52@="users"@H_403_52@ name@H_403_52@="users"@H_403_52@ component@H_403_52@={Users}@H_403_52@ indexComponent@H_403_52@={RecentUsers}@H_403_52@>@H_403_52@ <Route@H_403_52@ path@H_403_52@="/user/:userId"@H_403_52@ name@H_403_52@="user"@H_403_52@ component@H_403_52@={User}@H_403_52@/>@H_403_52@ </Route@H_403_52@>@H_403_52@ <Route@H_403_52@ path@H_403_52@="*"@H_403_52@ component@H_403_52@={NoMatch}@H_403_52@/>@H_403_52@ </Route@H_403_52@>@H_403_52@ </Router@H_403_52@>@H_403_52@ @H_403_52@

    React Router可以用Linkcomponent做应用导航,只需要指定路由名称

    <nav>
      <Link@H_403_52@ to@H_403_52@="about"@H_403_52@>@H_403_52@About</Link@H_403_52@>@H_403_52@ <Link@H_403_52@ to@H_403_52@="users"@H_403_52@>@H_403_52@Users</Link@H_403_52@>@H_403_52@ </nav@H_403_52@>@H_403_52@ @H_403_52@

    还有集成了React-Bootstrap的路由库,不想手写Bootstrap的active类,可以用react-router-bootstrap

    <Nav>
      <NavItemLink@H_403_52@ to@H_403_52@="about"@H_403_52@>@H_403_52@About</NavItemLink@H_403_52@>@H_403_52@ <NavItemLink@H_403_52@ to@H_403_52@="users"@H_403_52@>@H_403_52@Users</NavItemLink@H_403_52@>@H_403_52@ </Nav@H_403_52@>@H_403_52@ @H_403_52@

    Formsy-React

    表单开发通常都比较麻烦,用formsy-react来简化一下吧,它可以用来管理验证和数据模型。Formsy-React不包含实际的表单输入,而是推荐用户自己写(这是正确的)。如果只用通用表单,可以用formsy-react-components。它包括了Bootstrap类:

    import@H_403_52@ Formsy from@H_403_52@ 'formsy-react'@H_403_52@;
    import@H_403_52@ {Input} from@H_403_52@ 'formsy-react-components'@H_403_52@;
    export@H_403_52@ default@H_403_52@ class@H_403_52@ FormsyForm@H_403_52@ extends@H_403_52@ Component@H_403_52@ @H_403_52@{
      enableButton() {
        this@H_403_52@.setState({canSubmit: true@H_403_52@});
      }
      disableButton() {
        this@H_403_52@.setState({canSubmit: true@H_403_52@});
      }
      submit(model) {
        FormActions.saveEmail(model.email);
      }
      render() {
        return@H_403_52@ (
          <Formsy.Form@H_403_52@ onValidSubmit@H_403_52@={this.submit}@H_403_52@ onValid@H_403_52@={this.enableButton}@H_403_52@ onInvalid@H_403_52@={this.disableButton}@H_403_52@>@H_403_52@ <Input@H_403_52@ name@H_403_52@="email"@H_403_52@ validations@H_403_52@="isEmail"@H_403_52@ validationError@H_403_52@="This is not a valid email"@H_403_52@ required@H_403_52@/>@H_403_52@ <button@H_403_52@ type@H_403_52@="submit"@H_403_52@ disabled@H_403_52@={!this.state.canSubmit}@H_403_52@>@H_403_52@Submit</button@H_403_52@>@H_403_52@ </Formsy.Form@H_403_52@>@H_403_52@ )@H_403_52@}
    }

    日期与选择器

    日期与选择器组件算是UI库的锦上添花了,遗憾的是这两个组件在Bootstrap 3上被移除了,因为它们对于一个通用CSS框架来说比较特殊了。不过我发现了两个不错的代替:react-pikadayreact-select。我尝试过10多个库,这两个总体来说很不错。非常易用:

    import@H_403_52@ Pikaday from@H_403_52@ 'react-pikaday'@H_403_52@;
    import@H_403_52@ Select from@H_403_52@ 'react-select'@H_403_52@;
    
    export@H_403_52@ default@H_403_52@ class@H_403_52@ CalendarAndTypeahead@H_403_52@ extends@H_403_52@ Component@H_403_52@ @H_403_52@{
      constructor@H_403_52@(props){
        super@H_403_52@(props);
        this@H_403_52@.options = [
          { value: 'one'@H_403_52@,label: 'One'@H_403_52@ },{ value: 'two'@H_403_52@,0)">'Two'@H_403_52@ }
        ];
      }
      dateChange(date) {
        this@H_403_52@.setState({date: date});
      },selectChange(selected) {
        this@H_403_52@.setState({selected: selected});
      },render() {
        return@H_403_52@ (
          <Pikaday@H_403_52@ value@H_403_52@={this.state.date}@H_403_52@ onChange@H_403_52@={this.dateChange}@H_403_52@ />@H_403_52@ <Select@H_403_52@ name@H_403_52@="form-field-name"@H_403_52@ value@H_403_52@={this.state.selected}@H_403_52@ options@H_403_52@={this.options}@H_403_52@ onChange@H_403_52@={selectChange}@H_403_52@ />@H_403_52@ )@H_403_52@}
    }

    总结 - React.js

    这篇文章介绍了目前Web开发的一些技术与框架。有些是React相关的,因为React的开放性,它们也能被用在其他环境。有时候技术进步总会被对新事物的恐惧所阻碍,我希望这篇文章可以打消你对尝试React,Flux和新的ECMAScript的疑虑。

    要是感兴趣,可以看看我用以上技术构建的示例应用。源码在Github上。

    感谢能坚持阅读到这里 :)

    本文译自2015年年中的《Navigating the React.JS Ecosystem》-Tomas Holas,对原文有理解性改动,水平有限,欢迎提出意见。:)

    猜你在找的React相关文章