2015 前端[JS]工程师必知必会 – 前端外刊评论 – 知乎专栏

上次我写《前端工程师必知必会》已经是三年前了,那是我写过最火的文章了。三年了,我仍然会在Twitter上收到关于这篇文章的消息。

从2012年到现在,一篇文章都没发过让我觉得有点羞羞哒。三年是一段很长的时间,很多东西都发生了改变。2012年,我鼓励同学们去学习浏览器开发者工具和模块化;虽然有很多同学会觉得CSS预编译和客户端模板引擎并不靠谱,但我仍然想要说一说它们;还有JSHint,虽然有#getoffmylawn(滚出我的地盘)的警告,但依然无法阻止JSHint成为一个受欢迎的理念(准确的说,JSLint真的(只是)存在过)。

已经是2015年了,我想写一篇新的,但是当我坐下来开始动笔的时候,想到了两个事情。一,这些东西被称作“必知必会”可能有人会觉得不太公平——如果你已经觉得2012年的那篇文章如此,那本文也是一样的了。也许有同学会说,我们应该把 “足够应付业务需求的技能” 作为 “前端必须掌握的知识”,但考虑到前端行业里也有各种各样的工作可供选择,这么做也只能得到一个并不适合所有人的 “前端基础知识”。对于我来说,我需要的不是工作,我想要的是被邀请去做一份牛逼的工作。我想要的不只是去干活而已,而是想和一群牛逼的人一起做牛逼的事。我不想仅仅满足于用已有的知识来完成现在的工作,而是希望掌握更多的知识来解决未来将会面对的问题。

第二,我现在已经完全把Javascript作为我的核心了:CSS知识只有在必须关注性能问题时才会用到,其他场景已经用的越来越少。我知道有很多牛逼的前端同学并不是这样的,但我也意识到,关注JS的同学和关注CSS的同学之间的距离也越来越远。这可能需要在另起一篇文章来讨论,不过我想说的是,这篇文章中不会有介绍CSS技能标准的内容,因为我还远远没有达到能那么做的水平。

总之,就算这个技能列表并不适合你的前端工作,没关系,不要有压力,地球也不会爆炸。

Javascript

回想2009年,那时候当你知道 HTML5 在2014年才能用的时候,你是不是觉得这辈子基本上都用不到它了?如果是,那么你需要准备好接受进展缓慢但是已经趋于稳定的ES6了,它也是下一代的Javascript(现在叫 ES2015 了,嗯,这名字至少表示今年就能用了)。就我而言,ES6,额,ES2015 无疑是我个人现在最关注的 Javascript 内容。在 ES6 中将会出现一些比较大的变化:类,真正的私有,经过改进更易用的函数和参数设定,可导入的模块,等等等等。那些掌握和理解新的语法的同学以后将会在 JS 社区牛逼闪闪。相关阅读:

  • Understanding ES6,Nicholas Zakas 正在写的书。
  • BabelJS,一个可以把你写的 ES6 的代码编译成 ES5 并在现代浏览器中运行的工具。他们也有一个不错的介绍 ES6 的文档
  • ES6 Rocks,里面有大量的文章探索 ES6 的特性,语义和缺陷。

你也许会问:那我需要成为一个 ES6 专家么?也许现在不需要,但至少你得和你的同事懂的一样多吧?或者比他们稍微多一点?当然,如果能在你的下一个新项目中作为一个娱乐性的技术尝试也是不错的,做好准备肯定没错的,因为我们永远不知道下一刻会发生什么。

先不说新的语言特性,使用回调和 promises 管理异步 Javascript 至少得背的滚瓜烂熟吧。浏览器端应用加载,以及应用间通信策略得形成一套自己的观点吧。而且你应该知道哪种框架最适合你,而不是现在还把时间花在理解各种框架的实现原理和该选择哪种框架上。

模块化和构建工具

毫无疑问,模块化是构建 Web 客户端应用的基石。回到2012年,关于使用哪种模块化(AMD/CommonJS)方案构建浏览器端应用还存在很多争论。而最近慢慢火起来的 UMD 则在保证代码可复用的前提下尝试避免这样的问题。 其实也没什么好争得,毕竟这俩玩意儿之间也就差几个字符吧?

我觉得类似这样的争论其实并不都需要有一个答案,这也是我觉得从2012年到现在我们发生的最大的转变,当然,也许只是我自己这么认为。因为我觉得与其说“我再也不用 AMD 了”之类的话,倒不如多去讨论 “在开发和打包过程中使用 CommonJS 和 npm 遇到的各种难题” 来的更有价值。

虽然很感激 RequireJS 曾经对模块化做出的贡献,不过现在我开始有点迷恋webpack 了。 webpack 的构建配置比 RequireJS 更加易于理解,也更具访问性。通过它的热插拔特性和内置的本地静态服务器可以让发布更加便捷。它并不强制要求使用 AMD 或者 CommonJS – 两个它都支持。它还实现了一大堆加载器,用来完成常见的繁琐工作。 Browserify 也值得去了解一下,不过我个人认为它比 Webpack 落后很多。一些靠谱的朋友告诉我说 systemjs 也是这个领域的竞争者,不过我还没有用过,而且它的文档烂的我连看都不想看。不过我觉得它的好基友 jspm (包管理器)比较有趣,jspm 可以让你从各种包管理服务器加载你需要的各种组件,(组件必须是符合 ES6, AMD, CommonJS and globals 规范的),包括 npm, github 等,但是我对于这两个玩意的合体还是有点不太理解。啊,还有,虽然我说了这么多关于模块化之外的内容,但我从来没想过放弃 AMD,我们边走边看吧。

我觉得如果要停止对模块化和构建工具的争论,形成统一的模块化系统,并且在这个系统里面,任何项目的代码都可以共享,而且还不需要 UMD 这样额外的补丁工具,我们还有很长的路要走。理想状况下,ES6 modules 的到来会解决这些问题,不过在这一天到来之前,类似 UMD 之类的转换器会填补这些空缺,不过貌似这样做我们又把事情变得复杂了,好像我们也总喜欢把事情弄得复杂。

与此同时,前端开发人员也需要对构建工具,各种模块化系统有自己的见解和知识储备。不管是好是坏,根据 Javascript 现在的进度,你的模块化策略会对你的项目有比较大的影响。

测试

客户端的代码测试变得越来越普遍,最近也诞生了一些新的测试框架: KarmaIntern 。我发现基于 promise 的 Intern 的异步测试方法相当优雅。不过可能是因为习惯,我大多数情况下还是用 Mocha 写测试用例。

测试的主要障碍其实是前端开发者的代码编写方式。我在2012年发表过一个关于《编写可测试的Javascript》下载地址的演讲,紧接着几个月后又发表了一篇相关的文章

测试的第二大障碍是工具。Webdriver 是一个艰难而巨大的工作。目前在各个浏览器端做持续集成的 UI 自动化测试基本上是不可能的,更不用说移动端了。我们仍然停留在局限于某一小部分浏览器和设备上做轻量级的自动化功能测试,尽我们所能去研究怎样快速,低成本的进行这种测试的阶段。

如果你对如何改进代码的可测试性感兴趣的话,那么唯一一本最值得看的书是Working Effectively with Legacy Code 中译版:《修改代码的艺术》。作者 Michael Feathers 定义了“遗留代码”的概念:任何未经测试的代码都是遗留代码。在测试领域,最基本的要素就是上面这句话,尽管你可能不这么认为。

流程自动化

你首先会想到 Grunt,这也是理所当然的。而 Gulp  Broccoli 的自动化构建方式也别具匠心。我没用过Broccoli,只玩过Gulp,我也开始意识到Grunt对于依赖其他服务的复杂任务的自动化工作存在局限性,尤其是当这种任务每天需要运行上千次的时候。

Yeoman是在我写完2012年的那篇文章仅仅45天之后发布的,我承认当时我并没有及时去尝试一下。不过最近我开始启动一些新项目,这些新项目有两个特点
a) 这些项目都是从零开始
b) 尝试用一些不同的技术方案,试图通过这种方式找到 Bazaarvoice(提供第三方点评服务)上第三方 JS 应用的规范化的开发方式。
Yeoman 在这两方面做的都很好。一个简单的 yo react-webpack 命令就可以为你初始化好你的项目,然后各种你想要的玩具也都应有尽有:生成测试用例,本地静态服务器,hello world 入门程序,等等等等。如果 React 和 webpack 不是你想要的,也许你会在 Yeoman 的 generators(项目生成器)里面找到一个你想要的,当然,自己自定义一个这样的构建包也是比较容易的。

鉴于 Yeoman 只是一个在项目开始时才会用到的构建工具,并且鉴于我们并不是总是做新项目,所以大多情况下了解一下就够了。除非,你也想去规范整个项目开发过程,那么它可能会更有价值一点。

Broccoli 已经得到了 ember-cli 的采纳,我觉得他们的配对可能会有一个新名字,这样在未来才比较方便和 Grunt /Yeoman 对抗。而 Grunt 和 Yeoman 的开发进度也放缓了,所以未来会发生什么,我们还是静观其变吧。

代码质量

如果你像我一样,一看见违反代码规范的代码时就开始抓狂,那么 JSCS 
ESLint 就是老天赐给你的礼物,而2012压根就没这些玩意。他们都提供了自定义代码规范的方式,并且可以在代码提交前对你的代码做自动化校验。这让我想起了…

Git

从2012年到现在,github 的使用流程并没有发生很大的变化,比如在 pull request 页面连个分支名都没有(只是恶搞一下)。

你应该非常清楚和流畅地使用功能分支(feature branches), 使用 rebase 合并别人的代码干活,使用交互式 rebase 命令和 squash 合并提交记录,或者尽可能细颗粒度的划分项目内容,避免引起代码冲突。另一个可用的 Git 工具是钩子,具体而言,就是你可以在 push 前,commit 前,执行你的各种测试用例,检查代码质量。你可以自己写钩子,也可以使用 ghooks ,由于 ghooks 使钩子工作变得非常简单,所以你简直没有理由不用它。

客户端模板

这可能是我在2012年的那篇文章中写的最烂的内容了,某种意义上的“烂”。客户端模板还是很有价值的,而且它已经被内置到 ES2015 里面了,这不仅仅只是一件好事而已。这些年也有一些惨重的教训,不少团队把所有的渲染工作全部丢到浏览器端去做,结果产生了严重的性能问题,所以 “在浏览器端渲染生成所有 HTML” 的做法理所当然的被摒弃了。 而更为聪明的做法则是,把 HTML 生成放在服务器端,或者通过预编译的方式,先将模板做为静态资源储存起来,在需要时快速的编译成 HTML,需要更新时也可以直接在客户端更新模板。

这里会有一些新的展望,不仅是对我自己,也是对所有人,当你在考虑性能问题时,也许没必要把自己完全限定在浏览器范围内。所以,这又让我想起了……

Node

听说你懂 Javascript,那么我觉得你也应该懂 Node,至少在遇到 Node 问题是能帮得上忙的,如果连忙都帮不上,那也至少深入研究一下吧:Node 的文件系统,流,服务器,完全不同于前端的一些开发模式等等。对后端敬而远之只会限制我们前端的发展潜力。

即使你的真实生产环境中后端不用 Node,当你的工作被后端限制或阻碍的时候,Node 也是一个非常有用的工具。最起码,你也应该熟悉怎么去初始化一个 Node 项目,怎么用 Express 搭建服务器设置路由,怎么使用请求模块代理请求。

最后

感谢 Paul, Alex, Adam, Ralph 对本文的 Review,感谢他们毫不吝啬的指出我的不足之处,并给我提了很好的意见。

就这样,祝你好运。也许,三年之后我们会再见。

原文链接: A Baseline for Front-End ‘JS’ Developers: 2015

阅读详情 -> 2015 前端[JS]工程师必知必会 – 前端外刊评论 – 知乎专栏.

Categories: 前端周边, 前端工具, 前端资讯

React 初探 | Web前端 腾讯AlloyTeam Blog | 愿景: 成为地球卓越的Web团队!

React 简单介绍

先说 React 与 React Native

他们是真的亲戚,可不像 Java 和 Javascript 一样。

其实第一次看到 React 的语法我是拒绝的,因为这么丑的写法,你不能让我写我就写。

但当我发现 React Native 横空出世后,它学习一次到处运行的理念非常诱人。React Native 可以写出原生体验的 iOS/Android 应用?那不就多了一门装逼技能?所以我们调研小组试了一下,感觉 “Duang” 一下,很爽很舒服。写 React Native 需要两门基础技能:React 语法 和 iOS 基础知识。

很爽很舒服,索性就研究一下,算是入门。 了解之后发现,React 真是有另一番天地,值得学习。

接下来总结以下我对 React 的理解,分享给大家。

至于 React Native,有机会再好好探究下。

这部分废话太多,喜欢实战的可以直接看代码部分。

React 是 Facebook 出品的一套颠覆式的前端开发类库。

为什么说它是颠覆式的呢?

内存维护虚拟 DOM

对于传统的 DOM 维护,我们的步骤可能是:

1. 初始化 DOM 结构
2. 从服务器获取新数据
3. 使用新数据更新局部 DOM
4. 绑定各种事件

首先,我们操作 DOM 是最昂贵的开销,对于 需要反复更新 DOM 的网页,无疑是噩梦。其次,对 DOM 局部的更新以及事件绑定加大了维护的难度。

而 React 引入了一个全新的概念:虚拟 DOM。

虚拟 DOM 是躺在内存里的一种特殊的结构,我们可以理解为这是真实 DOM 在内存里的映射。

除了结构上的映射外,这个虚拟的 DOM 还包括了渲染 真实所需要的数据以及事件绑定。

全量更新真实 DOM

虚拟 DOM 在创建时,首先是使用 JSX 的语法生成一个真实 DOM 树的映射,其次是从服务器端拉取远程数据,接着注入到这个虚拟 DOM 树中,同时绑定事件。

好了,有了虚拟 DOM、数据、事件,万事俱备。

接下来,调用 render() 方法一次性渲染出真实的 DOM,然后全量插入到网页中。

虚拟 DOM 静静地躺在内存里,等待数据更新。

新数据来临,调用 setState() 方法更新数据到虚拟 DOM 中,然后自动调用 render() 再一次性渲染出真实的 DOM ,然后全量更新到网页中。

一个虚拟 DOM,对应一个真实 DOM
一份数据更新,重新生成虚拟 DOM ,全量更新真实 DOM

就这么简单。 除了带来性能上的提升之外,很显然这种写法简化了我们维护 DOM 的成本 — 我们只需要维护一份数据。

只是 View,可配合其他类库使用

可以看到,React 里最重要的概念有虚拟 DOM、单向数据注入(虚拟 DOM 到真实 DOM)。 这里并没有引入太多其他的东西,所以我对 React 的理解是一个类库,而非框架。 如果要使用 MVC、MVVM 等技术的吧,完全可以把 React 当做其中的 V,即 View, 配合其他类库使用。

组件化

我虽然是个前端菜鸟,但日观天象也是能嗅到下一代 Web 将是组件化、组件复用共享的时代。

React 编写起来,就是编写一个个的组件。

我对一个 React 组件的理解是:

- 模板 HTML (JSX 语法格式)
- 样式 CSS  (还是独立的样式文件)
- 交互 JS   (与HTML一起,揉和到 JSX 语法中)

以上三者可以打包复用,甚至是无缝接入,我脚得它就可能是未来了。

HTML 与 JS 使用 JSX 语法糅合到一起的方式是见仁见智,恐怕会引起战争。

我刚接触到 JSX 的时候,一开口也是『我*,好丑』。

但慢慢地却发现,这种方式一开始写起来别扭,但用得却很爽。

接下来,我通过编写一个简单的应用来入门 React。

看完如果大呼不过瘾,建议直飞 React 官方看文档,那才是宝藏!

React 简单示例

示例代码放置在 demo/目录下,每个文件夹为一个独立的示例。

先看下这个 demo 最终的样子吧:

demo – 速度与激情

每个示例的入口文件 index.html 结构大体相同:

<!-- React 真实 DOM 将会插入到这里 -->
<div id="demo"></div>

<!-- 引入 React -->
<script src="../../bower_components/react/react.js"></script>
<!-- 引入 JSX 语法格式转换器 -->
<script src="../../bower_components/react/JSXTransformer.js"></script>

<!-- 注意:script 需要注明 type 为 text/jsx 以指定这是一个 JSX 语法格式 -->
<script type="text/jsx" src="demo.js"></script>
</body>

渲染一个虚拟 DOM 为真实 DOM

使用 render() 方法生成真实 DOM 并插入到网页中。

// 使用 React.createClass 创建一个组件
var DemoComponent = React.createClass({
    // 使用 render 方法自动渲染 DOM
    render: function () {
        return (
            <div className="component-hello">
                <h1 className="hello-title">Hello React</h1>
                <p  className="hello-desc">React 初探</p>
                <div className="hello-movies">
                    <p2>我喜欢的电影</p2>
                    <ul>
                        <li className="movie-item">
                            <span className="movie-name">速度与激情7</span>
                            -
                            <span className="movie-date">2015</span>
                        </li>
                    </ul>
                </div>
            </div>
        )
    }
});

// 将组件插入到网页中指定的位置
React.render(<DemoComponent />, document.getElementById('demo'));

在线演示 demo/render

示例代码 demo/render

设置初始数据

第一次渲染真实 DOM 时将使用 getInitialState() 返回的数据。

// 使用 React.createClass 创建一个组件
var DemoComponent = React.createClass({
    // getInitialState 中返回的值将会作为数据的默认值
    getInitialState: function () {
        return {
            title: '我喜欢的电影',
            movies: [
                {
                    id: 7,
                    name: '速度与激情7',
                    date: 2015
                },
                {
                    id: 6,
                    name: '速度与激情6',
                    date: 2013
                }
            ]
        }
    },
    // 使用 render 方法自动渲染 DOM
    render: function () {
        // this.state 用于存储数据
        var title  = this.state.title;
        var movies = this.state.movies.map(function (movie) {
            return (
                <li className="movie-item" key={movie.id}>
                    <span className="movie-name">{movie.name}</span>
                    -
                    <span className="movie-date">{movie.date}</span>
                </li>
            )
        });

        return (
            <div className="component-hello">
                <h1 className="hello-title">Hello React</h1>
                <p  className="hello-desc">React 初探</p>

                <div className="hello-movies">
                    <p2>{title}</p2>
                    <ul>{movies}</ul>
                </div>
            </div>
        )
    }
});

// 将组件插入到网页中指定的位置
React.render(<DemoComponent />, document.getElementById('demo'));

在线演示 demo/get-initial-state

示例代码 demo/get-initial-state

动态更新数据

第二次更新渲染真实 DOM 时将使用 setState() 设置的数据。

// 使用 componentDidMount 在组件初始化后执行一些操作
    componentDidMount: function () {
        // 拉取远程数据
        // 开启假数据服务器:
        // cd fake-server && npm install && node index.js
        this.fetchData();
    },

    // 使用自定义的 fetchData 方法从远程服务器获取数据
    fetchData: function () {
        var self = this;
        // 发起 ajax 获取到数据后调用 setState 方法更新组件的数据
        var url = '../../fake-data/movies.json';
        $.getJSON(url, function (movies) {
            // 本地模拟返回太快了,模拟一下网络延迟
            setTimeout(function() {
                self.setState({
                    movies: movies
                });
            }, 2000);
        });
    },

在线演示 demo/set-state

示例代码 demo/set-state

绑定事件

绑定事件时,我们可以使用 ref=”name” 属性对一个 DOM 节点进行标记,同时可以通过 React.findDOMNode(this.refs.name) 获取到这个节点的原生 DOM。

// 使用 render 方法自动渲染 DOM
    render: function () {
        var self = this;
        // this.state 用于存储数据
        var title  = this.state.title;
        var movies = this.state.movies.map(function (movie) {
            return (
                <li className="movie-item" key={movie.id}>
                    <span className="movie-name">{movie.name}</span>
                    -
                    <span className="movie-date">{movie.date}</span>
                    <a href="#" onClick={self.onRemove.bind(null, movie)}>删除</a>
                </li>
            )
        }.bind(this));// 注意这里 bind(this) 修正了上下文

        return (
            <div className="component-hello">
                <h1 className="hello-title">Hello React</h1>
                <p  className="hello-desc">React 初探</p>

                <div className="hello-movies">
                    <p2>{title}</p2>
                    <form onSubmit={this.onAdd}>
                        {/* 注意这里指定 ref 属性,然后我们就可以使用 this.refs.xxx 访问到 */}
                        <input type="text" ref="name" placehlder="输入你喜欢的电影"/>
                        <input type="text" ref="date" placeholder="上映时间"/>
                        <input type="submit" value="提交"/>
                    </form>
                    <ul>{movies}</ul>
                    {this.state.loading ? <div>大家好我是菊花, 我现在在转</div> : null}
                </div>
            </div>
        )
    }
onRemove: function (movie) {
        var id = movie.id;
        console.log(movie)
        // 删除这个 item
        var movies = this.state.movies;
        var len = movies.length;
        var index = -1;
        for(var i = 0; i < len; i++) {
            var _movie = movies[i];
            if (_movie.id === id) {
                index = i;
                break;
            }
        }
        if (index > 0) {
            movies.splice(index, 1);
            this.setState({
                movies: movies
            });
        }
    },

    onAdd: function (e) {
        e.preventDefault();
        var refs = this.refs;
        var refName = React.findDOMNode(refs.name);
        var refDate = React.findDOMNode(refs.date);
        if (refName.value === '') {
            alert('请输入电影名');
            return;
        } else if (refDate === '') {
            alert('请输入上映时间');
            return;
        }
        var movie = {
            // 使用 findDOMNode 获取到原生的 DOM 对象
            name: refName.value,
            date: refDate.value,
            id: Date.now() // 粗暴地以时间数字作为随机 id
        };

        var movies = this.state.movies;
        movies.push(movie);
        this.setState(movies);

        refName.value = '';
        refDate.value = '';
    },

在线演示 demo/events

示例代码 demo/events

多组件与组件嵌套

一个组件就包含了 JSX 模板、数据维护、事件绑定的话,代码量已经够多了,这时候可以采用 AMD/CMD 的方式,将组件进行更细粒度的划分,可以以文件即组件的方式来编写,这里就不上 demo 了。

组件间通信

在 React 中,数据流是单向的,且组件之间可以嵌套,我们可以通过对最顶层组件传递属性方式,向下层组件传送数据。

  • 嵌套组件间,使用 this.props 属性向下传递数据

  • 独立组件之间,自行维护数据则需要自行维护一个全局数据存储,或者使用发布订阅地方式通知数据的更新。

全局数据存储怎么做呢?可以理解为不同的组件获取的数据源一致,在组件的外部维护这个数据集合,或者干脆直接从服务器端获取。

有人会说了,这样很不方便。

但我觉得,既然是一个组件,那就配套有获取组件所需数据的方式,独立组件间有很强的数据依赖时,要么使用上述方式,要么可以简单粗暴,将独立组件用一个顶层组件包裹起来,转化为嵌套组件的关系,即可数据互通。

// 将子组件抽离出来
var LiWrapper = React.createClass({
    render: function () {
        // 使用 this.props 获得传入组件的数据
        var movie = this.props.movie;
        return (
            <li>{/* ... */}</li>
        )
    }
});

// 使用 React.createClass 创建一个组件
var DemoComponent = React.createClass({
    // 使用 getInitialState 的返回值作为数据的默认值
    getInitialState: function () {
      // ...
    },

    // 使用 render 方法自动渲染 DOM
    render: function () {
        // this.state 用于存储数据
        var movies = this.state.movies.map(function (movie) {
            return (
               <LiWrapper movie={movie}/>
            )
        }.bind(this));// 注意这里 bind(this) 修正了上下文

        return (
            <div className="component-hello">
                {/* ... */}
                <div className="hello-movies">
                    <ul>{movies}</ul>
                </div>
            </div>
        )
    }
});

// 将组件插入到网页中指定的位置
// 在使用组件时传入 movies 数据
var movies = [// ...];
React.render(<DemoComponent movies={movies}/>, document.getElementById('demo'));

在线演示 demo/comunications

示例代码 demo/comunications

打造丝滑的构建 使用 ES6 + gulp + webpack

ES6 和 gulp 的话就不多介绍啦。

webpack 是一款新生的前端构建工具,兼容 AMD/CMD 等写法,支持 Browser 和 Node 端共享代码,在浏览器端可以像写 Node 一样方便的进行模块化的划分。

在这里主要用 webpack 的两个插件:

  • 使用 jsx-loader 这个插件支持 jsx 语法解析

  • 使用 esx-loader 这个插件支持 es6 语法解析

来看下简单目录结构:

  • js/main.js 为入口文件,引入了两个组件。
var React = require('react');

var MovieListComponent = require('./components/movie-list');
var HelloMessageComponent = require('./components/hello');

var movies = [
    {
        id: 5,
        name: '速度与激情5',
        date: 2011
    },
    {
        id: 4,
        name: '速度与激情4',
        date: 2009
    }
];

var wording = '保罗';

var MainComponent = React.createClass({
    render: function () {
        return (
            <div className="component-hello">
                <HelloMessageComponent name={wording}/>
                <MovieListComponent movies={movies} />
            </div>
        )
    }
});

React.render(<MainComponent />, document.getElementById('demo'));

  • js/components/movie-list.js 组件为 JSX 语法编写
var React = require('react');

// 引入子组件
var MovieComponent = require('./movie');

// 使用 React.createClass 创建一个组件
var MovieListComponent = React.createClass({
    // 使用 getInitialState 的返回值作为数据的默认值
    getInitialState: function () {
        return {
            loading: true,
            title: '我喜欢的电影',
            // 注意这里将 外部传入的数据赋值给了 this.state
            movies: []
        }
    },

    // 使用 render 方法自动渲染 DOM
    render: function () {
        // this.state 用于存储数据
        var title  = this.state.title;
        // this.props 用于从组件外部传入数据
        var movies = this.props.movies;
        movies = movies.map(function (movie) {
            return (
                <MovieComponent movie={movie}/>
            )
        }.bind(this));// 注意这里 bind(this) 修正了上下文

        return (
            <ul>{movies}</ul>
        )
    }
});

module.exports = MovieListComponent;
  • js/components/hello.js 组件为 ES6 + JSX 语法编写
var React = require('react');

class HelloComponent extends React.Component {
    constructor(props) {
        super(props);
        this.state = {wording: '你好呀, '};
    }
    render() {
        return <div>{this.state.wording} {this.props.name}</div>;
    }
}

module.exports = HelloComponent;

  • webpack.config.js 指定 jsx-loader 和 es6-loader
module.exports = {
    entry: ['./js/main.js'],
    output: {
        path: __dirname,
        filename: 'js/bundle.js'
    },
    module: {
        loaders: [
            { test: /\.js$/, loader: 'es6-loader' },
            { test: /\.js$/, loader: 'jsx-loader' }
        ]
    }
};
  • gulpfile.js 在这里配置 webpack 任务,启动文件监听
var gulp = require('gulp');
var livereload = require('gulp-livereload');
var webpack = require("gulp-webpack");

var webpackConfig = require('./webpack.config');

gulp.task("webpack", function() {
    return gulp.src('./js/main.js')
        .pipe(webpack(webpackConfig))
        .pipe(gulp.dest('.'));
});

gulp.task('watch', function() {
    livereload.listen();
    gulp.watch(['js/**/*.js', '!js/bundle.js'], ['webpack']);
});

gulp.task('default', [
    'webpack',
    'watch'
]);
  • index.html 示例页面,引入 webpack 打包后的 js/bundle.js
<!-- React 真实 DOM 将会插入到这里 -->
<div id="demo"></div>
<script src="./js/bundle.js"></script>

在 js/main.js 中引入两个不同的组件,然后在 webpack.config.js 中指定编译 JSX 和 ES6 的 loader 工具,使用 gulp 监听 js/ 中文件变化,自动编译出的 js/bundle.js 将被 index.html 引用。

嗯,再在 webpack 中加入各种你喜欢的 loader,在 gulp 中加上各种 css、js、img 的处理任务,编写代码,自动重新编译,纵享丝滑。

示例代码

零碎总结

文章到这里应该就算结束了,接下来是一些在学习过程中记下来的几个小点,也分享给大家。

简单理解 JSX 语法

JSX 把 JS 和 HTML 糅合起来了,这么理解是不是感觉比较简单:

遇到 {} 包裹的是 JS,遇到 <> 包裹的是 HTML

render() 中 返回的的 JSX 模板需要一个根元素包裹起来

比如:

// 错误的写法
var MyComponent = React.createClass({
    render: function () {
        return (
            <h1>速度与激情7</h1>
            <p>致敬保罗</p>
        )
    }
});

应该写成:

// 正确的写法
var MyComponent = React.createClass({
    render: function () {
        return (
            <div>
                <h1>速度与激情7</h1>
                <p>致敬保罗</p>
            </div>   
        )
    }
});

几个重要方法

  • render() 返回的是一系列嵌套的组件 this.props 获取父组件传递给子组件的数据 this.setState({data: data}); 用于动态更新状态,设置数据(设置后UI会自动刷新)
  • getInitialState() 在整个组件的生命周期中只会执行一次,用于初始化数据
  • componentDidMount 会在 render 后自动调用,用于异步获取数据,更新数据

操作数据的流程

  1. gitInitialState() 初始化数据
  2. render() 渲染初始化数据
  3. componentDidMount() 异步获取数据
  4. setState() 更新数据

理解一个组件的状态转换

每一个组件都可以理解为有一个简单的状态机。

调用 setState(data, callback) 后,data 将会混入 this.state 中,数据得到了更新,render() 就会被调用,UI 就能被更新。

组件之间如何通信

<Parent><Child /></Parent>

父组件可以获取到子组件:this.props.children

render() 永远不要手动调用

render() 在 React 创建时会调用一次,在数据更新时调用 setState() 方法则会继续调用它来更新网页中的真实 DOM。

使用 getInitialState() 设置默认值

这个方法返回的值会在组件初始化第一次调用 render() 时就被使用

class 是关键字,改用 className

// 错误的写法
var MyComponent = React.createClass({
    render: function () {
        return (
            <div class="movie">
                <h1>速度与激情7</h1>
                <p>致敬保罗</p>
            </div>
        )
    }
});

应该写成:

// 正确的写法
var MyComponent = React.createClass({
    render: function () {
        return (
            <div className="movie">
                <h1>速度与激情7</h1>
                <p>致敬保罗</p>
            </div>   
        )
    }
});

组件名大写,不然不被识别

// 错误的写法
var myComponent = React.createClass({
    render: function () {
        return (
            <div class="movie">
                <h1>速度与激情7</h1>
                <p>致敬保罗</p>
            </div>
        )
    }
});

React.render(<myComponent />, document.getElementById('demo'));

应该写成:

// 正确的写法
var MyComponent = React.createClass({
    render: function () {
        return (
            <div className="movie">
                <h1>速度与激情7</h1>
                <p>致敬保罗</p>
            </div>   
        )
    }
});

React.render(<MyComponent />, document.getElementById('demo'));

怎么隐藏或显示菊花

var MyComponent = React.createClass({
    getInitialState: function () {
        return {loading: true}
    },
    showLoading: function () {
        this.setState({loading: true})
    },
    hideLoading: function () {
        this.setState({loading: false})
    },
    render: function () {
        return (
            { 
                this.state.loading ?
                <div>大家好我是菊花,我在转</div>
                :
                null
            } 
        )
    }
});

插入原生的 HTML 片段的方式

React 会为我们过滤 XSS,要让一段 HTML 片段直接显示出来,需要这样:

<div dangerouslySetInnerHTML={{__html: 'First &middot; Second'}} />

让 React 支持移动触摸实践

React.initializeTouchEvents(true);

处理表单

表单因为会因用户交互而变化,所以有特定的一些属性

  • input 和 textarea 组件具有 value
  • input[type=checkbox] 和 input[type=radio] 具有 checked
  • option 具有 selected,如果要支持多选,可以传入数组:
<select multiple={true} value={['B', 'C']}>

表单项具有 onChange 事件

注意如果这么写:

render: function() {
    return <input type="text" value="Hello!" />;
  }

那每次 render 的时候 input 的 value 都会被重置为 “Hello!”,所以需要这么控制:

getInitialState: function() {
    return {value: 'Hello!'};
  },
  handleChange: function(event) {
    this.setState({value: event.target.value});
  },
  render: function() {
    var value = this.state.value;
    return <input type="text" value={value} onChange={this.handleChange} />;
  }

利用这点,可以无缝地接入一些验证规则,比如限制文字为 140 字:

handleChange: function(event) {
   this.setState({value: event.target.value.substr(0, 140)});
 }

如果不想这么被控制呢?那就在返回 input 的时候,不要设置 value 属性,这样随着用户输入,value 不会被重置:

 render: function() {
    return <input type="text" />;
  }

也可以设置默认值:

render: function() {
    return <input type="text" defaultValue="Hello!" />;
  }

除了 defaultValue 之外,还支持 defaultChecked

理解虚拟 DOM

React 会在内存里维护一个代表 DOM 的结构,调用 render 方法时才生成真正的 DOM 插入到网页中。

理解组件的生命周期

一个组件的声明周期可以理解为三个阶段:

  1. mounting 组件正在插入到 DOM 中
  2. updating 组件正在重新注入新数据后更新到 DOM 中
  3. unmounting 组件从 DOM 中移除

mounting 阶段

  • getInitialState() 被调用,返回原始数据

  • componentWillMount() 在组件 mounting 前调用

  • componentDidMount() 在组件 mounting 完成后调用

updating 阶段

  • componentWillReceiveProps(nextProps) 在接收到新的 props 时调用
  • shouldComponentUpdate(nextProps, nextState) 在组件需要更新 DOM 时调用,若这个函数返回 false 则告诉 React 不要更新
  • componentWillUpdate(nextProps, nextState) 在更新发生时调用,可以在这里调用 this.steState() 刷新数据
  • componentDidUpdate(prevProps, prevState) 在更新完成后调用

unmounting 阶段

  • componentWillUnmount() 在组件移除时被调用,在这里可以对数据进行清理

强制使用数据更新组件

forceUpdate() 强制使用数据更新组件,而不用调用 this.setState()

获取原生 DOM 元素

React.findDOMNode(component) 返回原生的 DOM 元素 注意要获取原生的 DOM 元素,必须在 render 被调用, 真正的 DOM 已经被插入到页面中时。

理解 refs

可以把 refs 理解为我们在 HTML 中的id,用于定位到指定的组件。

<form onSubmit={this.onAdd}>
    {/* 注意这里指定 ref 属性,然后我们就可以使用 this.refs.xxx 访问到 */}
    <input type="text" ref="name" placehlder="输入你喜欢的电影"/>
    <input type="text" ref="date" placeholder="上映时间"/>
    <input type="submit" value="提交"/>
</form>

ref 属性可以是一个回调函数而不是名字,这个回调会在组件 mounted 后被调用。回调函数使用被引用的组件作为参数。

<input ref={ function(component){ React.findDOMNode(component).focus();} } />

注意不要在 render 方法中访问 refs 属性。

阅读详情 -> React 初探 | Web前端 腾讯AlloyTeam Blog | 愿景: 成为地球卓越的Web团队!.

Categories: JavaScript

10款Web程序员必备的CSS工具 – WEB骇客

阅读详情 -> 10款Web程序员必备的CSS工具 – WEB骇客.

对于web开发来说,CSS是最有效的美化页面、设置页面布局的技术。但问题是,CSS是一种标记性语言,语法结构非常的松散、不严谨。WEB程序员会经常发现自己的或别人的CSS文件里有大量的冗余代码或错误或能够大量优化的地方。如果你经常使用静态编程语言(比如,Java,C语言)等,你会发现实用的IDE工具会给编程带来巨大的效率,像Eclipse这样的能够实时自动分析代码问题的集成开发环境就是一个典型的例子。那么,CSS程序员有没有这样的帮助工具呢?。

下面将要介绍的10款工具都是一些在线的应用,你不需要将它们安装到自己的电脑上,只要能联网,你就可以使用它们。大部分的这些工具使用起来都非常的简单,但也有需要技巧的地方。如果你在使用它们的过程中有什么心得体验,请留在评论里分享给大家。

CSS问题检查工具:CSS Lint

CSS Lint是一个开源的校验CSS文件质量的工具,最初是由 Nicholas C. Zakas和 Nicole Sullivan编写的,最初版本在Velocity会议上于2011年6月发布。CSS Lint的检测规则包括错误的和警告,当选择器或属性书写不正确、漏掉了大括号、多写了分号等时,会提示解析错误,解析错误优先提示。

CSS代码分析统计工具:CSS Stats

Css Stats是一款在线CSS代码分析工具,输入网站CSS网址即可进行CSS代码分析。Css Stats是前端网页设计师分析网站CSS代码的利器,可以统计CSS代码里的规则、字体大小、宽度高度、颜色数等等。

对于网页设计师而言分享网页CSS代码是必须要做的事情,统计网站设计里使用了多少种字体、多少种字体大小、多少种颜色、背景颜色有多少种,只有对CSS代码有一个详细的统计数字才能分析出来整个网站设计出来以后的效果。Css Stats还提供热门网站的CSS分析数据,例如谷歌、雅虎、Twitter、FaceBook、Tumblr等网站。

CSS代码优化压缩工具:CSS Shrinks

CSS Shrinks 能够非常明显的压缩你的CSS体积大小。很多Web程序员编写的CSS代码里有大量的冗余语法,空白空间等,这款工具能在不影响你的CSS的正确性的情况下,优化CSS的语法,去除无用的空格和空行,显著的压缩CSS的提交,大量的减少带宽的浪费。

CSS代码整理工具:ProCSSor

ProCSSor 除了提供基本优化CSS代码功能,还提供了大量的自定义选项。比如,让你设定CSS规则,CSS属性,CSS语法的优化选项。它还提供了对新型CSS3属性、规则中各种浏览器里的不兼容替代方案。

Codrops CSS 语法参考

Codrops CSS 参考内容丰富而全面,并且界面清爽直接,你可以使用这个工具掌握CSS里最重要而全面的知识。它的CSS知识库分成了数个类别,包括伪类,属性,函数,数据类型,概念,规则等。

CSS3浏览器兼容支持情况查询工具:Can I Use

“Can I Use”在这里你能找到所有web新特性在各个品牌浏览器以及各品牌浏览器不同版本的兼容性,当你知道你针对的用户都在使用什么浏览器时,这写table将对你建设网站有很大帮助。打开caniuse.com,该网站首页将所有HTML5、CSS3等web新特性罗列出来,如果你想查看某个特性在不同浏览器种的兼容情况,点击一下就可以。比如,看一下@font-face Web fonts在各个浏览器中的兼容性,点击CSS区域中的第一项,会看到一个表格,列出所有浏览器的版本,用不同颜色代表每个浏览器对@font face Web fonts的支持,被标识为红色的代表不支持,浅绿色代表部分支持。图中列出的浏览器还包括一些手机平板设备浏览器,例如Android系统浏览器。如此全面,设计网站时,可以根据网站针对的用户有选择的使用CSS和Javascript的高级特性,提高用户体验。

检查你的代码是否符合CSS标准:W3C CSS Validation Service

这个工具是用来检查你的CSS语法是否正确,是否符合W3C CSS标准。我们知道从最早的IE开始,各种浏览器都实现了一些自己的方言,这些方言中在各种浏览器里互不相通。但我们开发网页时,必须最大限度的考虑各种浏览器的兼容性,最好的方法是遵守W3C的CSS标准规范。W3C CSS Validation Service就是用来校验你的css中的问题,它会提醒你那些语法,哪些属性,那些规则是有问题的。

CodePad

Codepad.org是一个很有意思的网站,它的主页很简单,左边是可以编译并执行的程序语言,右边则是让你输入程序的输入框,输入框的下面是一个“Run Code”的复选钮和一个“Submit”的提交按钮。

其操作起来也非常简单,先选中你要编译并运行的程序语言,然后在输入框中粘贴或输入程序的原代码,然后,点击提交,你就可以看么你程序编译出错的提示,或是执行的结果。

也许,你会觉得很无聊天,但我觉得这在某些时候会非常有用,尤其是你找不到编译器而又想验证一段代码的时候,这种时候还是比较多的。特别是我们很难有一台可以运行所有语言的电脑,如果有的话,那一定是你自己的个人电脑,当你不使用你自己的电脑时,你就会着急了。而且,我觉得这项服务非常地有意思,因为,这样一来,你甚至可以在你的手机上写任何语言的程序了。

目前这个网站支持下面这样语言——C,C++,D,Haskell,Lua,OCaml,PHP,Perl,Plain Text,Python,Ruby,Scheme,Tcl。

CSS动画生成工具:Gradient Animator

这是一款使用CSS Gradient和CSS Animation技术实现的动态背景生成工具。工具非常易用,轻松地点击几下鼠标,一个现代感十足的渐变动态背景代码就生成了。它可以让CSS渐变背景平滑地移动,我们可以设置移动的角度,移动的速度,渐变的角度。支持所有现代浏览器以及 ie 10+。非常适合做网页开屏背景。

Web颜色工具:CSS Colours

故名思议,这个工具是用来方便Web设计者查找颜色的,它提供了各种颜色的视觉效果,对于的颜色的英文名称,RGB值,16进制值,使用起来非常的方便。

Categories: css3

React-Native学习指南

阅读详情 -> ele828/react-native-guide.

本指南汇集React-Native各类学习资源,给大家提供便利。指南正在不断的更新,大家有好的资源欢迎Pull Requests!

同时还有Awesome React-Native系列

https://github.com/jondot/awesome-react-native

教程

开源APP

研究源码也是一个很好的学习方式

组件

由于已经有较好的组件库网站,这里就不做总结。可以直接查看如下网站,过后可能精选一部分优质组件出来 😛

工具

资源网站

业界讨论

Categories: JavaScript

深入浅出React(一):React的设计哲学 – 简单之美

阅读详情 -> 深入浅出React(一):React的设计哲学 – 简单之美.

编者按:自2013年Facebook发布以来,React吸引了越来越多的开发者,基于它的衍生技术,如React Native、React Canvas等也层出不穷。InfoQ精心策划“深入浅出React”系列文章,为读者剖析React开发的技术细节。

React最初来自Facebook内部的广告系统项目,项目实施过程中前端开发遇到了巨大挑战,代码变得越来越臃肿且混乱不堪,难以维护。于是痛定思痛,他们决定抛开很多所谓的“最佳实践”,重新思考前端界面的构建方式,于是就有了React。

React带来了很多开创性的思路来构建前端界面,虽然选择React的最重要原因之一是性能,但是相关技术背后的设计思想更值得我们去思考。之前我也曾写过一篇React的入门文章,并提供了示例代码,大家可以结合参考。

上个月React发布了最新的0.13版,并提供了对ES6的支持。在新版本中,一个小小的改变是React取消了函数的自动绑定,也就是说,以前可以这样去绑定一个事件:

<button onClick={this.handleSubmit}>Submit</button>

而在以ES6语法定义的组件中,必须写为:

<button onClick={this.handleSubmit.bind(this)}>Submit</button>

了解前端开发和JavaScript的同学都知道,做事件绑定时我们需要通过bind(或类似函数)来实现一个闭包以让事件处理函数自带上下文信息,这是由JavaScript语言特性决定的。而在0.13版本之前,React会自动在初始化时对组件的每一个方法做一次这样的绑定,类似于this.func = this.func.bind(this),这样在JSX的事件绑定中就可以直接写为onClick={this.handleSubmit}

表面上看自动绑定给开发带来了便利,而Facebook却认为这破坏了JavaScript的语言习惯,其背后的神奇(Magic)逻辑或许会给初学者带来困惑,甚至开发者如果从React再转到其它库也可能会无所适从。基于同样的理由,React还取消了对mixin的支持,基于ES6的React组件不再能够以mixin的形式进行代码复用或者扩展。尽管这带来了很大不便,但Facebook认为mixin增加了代码的不可预测性,无法直观的去理解。关于mixin的思考,还可以参考这篇文章

以简单直观、符合习惯的(idiomatic)方式去编程,让代码更容易被理解,从而易于维护和不断演进。这正是React的设计哲学。

编写可预测,符合习惯的代码

所谓可预测(predictable),即容易理解的代码。在年初的React开发者大会上,React项目经理Tom Occhino进一步阐述React诞生的初衷,在演讲中提到,React最大的价值究竟是什么?是高性能虚拟DOM、服务器端Render、封装过的事件机制、还是完善的错误提示信息?尽管每一点都足以重要。但他指出,其实React最有价值的是声明式的,直观的编程方式。

软件工程向来不提倡用高深莫测的技巧去编程,相反,如何写出可理解可维护的代码才是质量和效率的关键。试想,一个月之后你回头看你写的代码,是否一眼就明白某个变量,某个if判断的含义;一个新加入的同事想去增加一个小小的新功能或是修复某个Bug,他是否对自己的代码有足够的信心不引入任何副作用?随着功能的增加,代码很容易变得越来越复杂,这些问题也将越来越严重,最终导致一份难以维护的代码。而React号称,新同事甚至在加入的第一天就能开始开发新功能。

那么React是如何做的呢?

使用JSX直观的定义用户界面

JSX是React的核心组成部分,它使用XML标记的方式去直接声明界面,界面组件之间可以互相嵌套。但是JSX给人的第一印象却是相当“丑陋”。当下面这样的例子被第一次展示的时候,甚至很多人称之为“巨大的退步(Huge Step Backwards)”:

var React = require(‘React’);
var message =
  <div class=“hello” onClick={someFunc}>
    <span>Hello World</span>
  </div>;
React.renderComponent(message, document.body);

将HTML直接嵌入到JavaScript代码中看上去确实是一件足够疯狂的事情。人们花了多年时间总结出的界面和业务逻辑相互分离的“最佳实践”就这么被彻底打破。那么React为何要如此另类?

模板出现的初衷是让非开发人员也能对界面做一定的修改。但这个初衷在当前Web程序里已完全不适用,每个模板背后的代码逻辑严重依赖模板中的内容和DOM结构,两者是紧密耦合的。即使做到文件位置的分离,实际上两者还是一体的,并且为了两者之间的协作而不得不引入很多机制和概念。以Angularjs的首页示例代码为例:

<ul class="unstyled">
  <li ng-repeat="todo in todoList.todos">
    <input type="checkbox" ng-model="todo.done">
    <span class="done-{{todo.done}}">{{todo.text}}</span>
  </li>
</ul>

尽管我们很容易看懂这一小段模板的含义,但你却无法开始写这样的代码,因为你需要学习这一整套语法。比如说,你得知道有ng-repeat这样的标记的准确含义,其中的”todo in todoList.todos”看上去是repeat语法的一部分,或许还有其它语法存在;可以看到有{{todo.text}}这样的数据绑定,那么如果要对这段文本格式化(加一个formatter)该怎么做;另外,ng-model背后又需要什么样的数据结构?

现在来看React怎么写这段逻辑:

//...
render: function () {
  var lis = this.todoList.todos.map(function (todo) {
    return  (
      <li>
        <input type="checkbox" checked={todo.done}>
        <span className="done-{todo.done}">{todo.text}</span>
      </li>);
  });
  return (
    <ul class="unstyled">
      {lis}
    </ul>
  );
}
//...

可以看到,JSX中除了另类的HTML标记之外,并没有引入其它任何新的概念(事实上HTML标记也可以完全用JavaScript去写)。Angular中的repeat在这里被一个简单的数组方法map所替代。在这里你可以利用熟悉的JavaScript语法去定义界面,在你的思维过程中其实已经不需要存在模板的概念,需要考虑的仅仅是如何用代码构建整个界面。这种自然而直观的方式直接降低了React的学习门槛并且让代码更容易理解。

简化的组件模型:所谓组件,其实就是状态机器

组件并不是一个新的概念,它意味着某个独立功能或界面的封装,达到复用、或是业务逻辑分离的目的。而React却这样理解界面组件

所谓组件,就是状态机器

React将用户界面看做简单的状态机器。当组件处于某个状态时,那么就输出这个状态对应的界面。通过这种方式,就很容易去保证界面的一致性。

在React中,你简单的去更新某个组件的状态,然后输出基于新状态的整个界面。React负责以最高效的方式去比较两个界面并更新DOM树。

这种组件模型简化了我们思考的方式:对组件的管理就是对状态的管理。不同于其它框架模型,React组件很少需要暴露组件方法和外部交互。例如,某个组件有只读和编辑两个状态。一般的思路可能是提供beginEditing()endEditing()这样的方法来实现切换;而在React中,需要做的是setState({editing: true/false})。在组件的输出逻辑中负责正确展现当前状态。这种方式,你不需要考虑beginEditing和endEditing中应该怎样更新UI,而只需要考虑在某个状态下,UI是怎样的。显然后者更加自然和直观。

组件是React中构建用户界面的基本单位。它们和外界的交互除了状态(state)之外,还有就是属性(props)。事实上,状态更多的是一个组件内部去自己维护,而属性则由外部在初始化这个组件时传递进来(一般是组件需要管理的数据)。React认为属性应该是只读的,一旦赋值过去后就不应该变化。关于状态和属性的使用在后续文章中还会深入探讨。

每一次界面变化都是整体刷新

数据模型驱动UI界面的两层编程模型从概念角度看上去是直观的,而在实际开发中却困难重重。一个数据模型的变化可能导致分散在界面多个角落的UI同时发生变化。界面越复杂,这种数据和界面的一致性越难维护。在Facebook内部他们称之为“Cascading Updates”,即层叠式更新,意味着UI界面之间会有一种互相依赖的关系。开发者为了维护这种依赖更新,有时不得不触发大范围的界面刷新,而其中很多并不真的需要。React的初衷之一就是,既然整体刷新一定能解决层叠更新的问题,那我们为什么不索性就每次都这么做呢?让框架自身去解决哪些局部UI需要更新的问题。这听上去非常有挑战,但React却做到了,实现途径就是通过虚拟DOM(Virtual DOM)。

关于虚拟DOM的原理我在去年底的文章有过比较详细的介绍,这里不再重复。简而言之就是,UI界面是一棵DOM树,对应的我们创建一个全局唯一的数据模型,每次数据模型有任何变化,都将整个数据模型应用到UI DOM树上,由React来负责去更新需要更新的界面部分。事实证明,这种方式不但简化了开发逻辑并且极大的提高了性能。

以这种思路出发,我们在考虑不断变化的UI界面时,仅仅需要整体考虑UI的构成。编程模型的简化带来的是代码的精简和易于理解,也即React不断提到的可预测(Predictable)的代码,代码的功能一目了然易于理解。Tom Occhino在2015 React开发者大会上也分享了React在Facebook内部的应用案例,随着新功能被不断的添加到系统中,开发进度非但没有变慢,甚至越来越快。

单向数据流动:Flux

既然已经有了组件机制去定义界面,那么还需要一定的机制来定义组件之间,以及组件和数据模型之间如何通信。为此,Facebook提出了Flux框架用于管理数据流。Flux是一个相当宽松的概念框架,同样符合React简单直观的原则。不同于其它大多数MVC框架的双向数据绑定,Flux提倡的是单向数据流动,即永远只有从模型到视图的数据流动。

Flux引入了Dispatcher和Action的概念:Dispatcher是一个全局的分发器负责接收Action,而Store可以在Dispatcher上监听到Action并做出相应的操作。简单的理解可以认为类似于全局的消息发布订阅模型。Action可以来自于用户的某个界面操作,比如点击提交按钮;也可以来自服务器端的某个数据更新。当数据模型发生变化时,就触发刷新整个界面。

Flux的定义非常宽松,除了Facebook自己的实现之外,社区中还出现了很多Flux的不同实现,各有特点,比较流行的包括FlexibleRefluxFlummox等等。

让数据模型也变简单:Immutability

Immutability含义是只读数据,React提倡使用只读数据来建立数据模型。这又是一个听上去相当疯狂的机制:所有数据都是只读的,如果需要修改它,那么你只能产生一份包含新的修改的数据。假设有如下数据:

var employee = {
  name: ‘John’,
  age: 28
};

如果要修改年龄,那么你需要产生一份新的数据:

var updated = {
  name: employee.name,
  age: 29
};

这样,原来的employee对象并没有发生任何变化,相反,产生了一个新的updated对象,体现了年龄发生了变化。这时候需要把新的updated对象应用到界面组件上来进行界面的更新。

只读数据并不是Facebook的全新发明,而是起源于Clojure, Scala, Haskell等函数式编程语言。只读的数据可以让代码更加的安全和易于维护,你不再需要担心数据在某个角落被某段神奇的代码所修改;也就不必再为了找到修改的地方而苦苦调试。而结合React,只读数据能够让React的组件仅仅通过比较对象引用是否相等来决定自身是否要重新Render。这在复杂的界面上可以极大的提高性能。

针对只读数据,Facebook开发了一整套框架immutable.js,将只读数据的概念引入JavaScript,并且在github开源。如果不希望一开始就引入这样一个较大的框架,React还提供了一个工具类插件,帮助管理和操作只读数据:React.addons.update

React思想的衍生:React Native, React Canvas等等

在前几天的Facebook F8开发者大会上,React Native终于众望所归的发布,它将React的思想延伸到了原生移动开发。它的口号是“Learn Once, Write Anywhere”,有React开发经验的开发人员将可以无缝的进行React Native开发。无论是组件化的思想,调试工具,动态代码加载等React具有的强大特性都可以应用在React Native。相信这会对以后的移动开发布局产生重要影响。

React对UI层进行了完美的抽象,写Web界面时甚至能够做到完全的去DOM化:开发者可以无需进行任何DOM操作。因此,这也让对UI层进行整体替换成为了可能。React Native正是将浏览器基于DOM的UI层换成了iOS或者Android的原生控件。而Flipboard则将UI层换成了Canvas。

React Canvas是Flipboard出品的一套前端框架,所有的界面元素都通过Canvas来绘制,infoQ之前也有文章对其进行了介绍。Flipboard追求极致的性能和用户体验,因此对浏览器的缓慢DOM操作深恶痛绝,不惜大刀阔斧彻底舍弃了DOM,而完全用Canvas实现了整套UI控件。有兴趣的同学不妨一试。

小结

React并不是突然从哪里蹦出来,而是为了解决前端开发中的痛点而生。以简单为原则设计也决定了React具有极其平缓的学习曲线,开发者可以快速上手并应用到实际项目中。本文总结分析了其相关技术背后的设计思想,希望通过这个角度能让大家对React有一个总体的认识,从而在React的实际项目开发中,遵循简单直观的原则,进行高效率高质量的产品开发。

参考资料

  1. React官方网站:http://facebook.github.io/react/
  2. React博客:http://facebook.github.io/react/blog/
  3. React入门:http://ryanclark.me/getting-started-with-react/
  4. 颠覆式前端UI框架:React:http://www.infoq.com/cn/articles/subversion-front-end-ui-development-framework-react
  5. Immutable.js: http://facebook.github.io/immutable-js/
  6. React Native: http://facebook.github.io/react-native/
  7. Flux: https://facebook.github.io/flux/
  8. Flux框架对比:https://github.com/voronianski/flux-comparison
  9. React开发者大会网站:http://conf.reactjs.com/index.html
  10. React在Slack上的聊天社区:http://reactiflux.com/

 

Categories: JavaScript

touch系事件 – 专栏 [] – 前端乱炖

阅读详情 -> touch系事件 – 专栏 [] – 前端乱炖.

 

touch系事件概述

touch系事件是触屏设备上的一系列事件,当用户触摸屏幕时及进行一系列操作时发生的事件。 包含touchstart, touchmove, touchend事件。

程序思路

我们将焦点集中在事件这个抽象对象上去,例如当手指在触摸屏上移动过程中发生的touchmove事件,我们去查找此事件相关的属性,根据前后事件的发生(例如touchstart和touchend事件),去判断是否符合程序触发任务的条件(例如当用户向上滑动时你要做一个页面动画)。

事件模式

需要指出的是,touch事件同其他dom事件一样(因为本身就属于dom事件,只不过用在触屏设备的新增html5事件而已),用addEventListener绑定,在事件处理时使用e.prevantDefault()来阻止默认事件执行(例如你在滚动div时,使用它来阻止页面滚动),使用e.stopPropagation()来阻止事件向上冒泡,等等。

对象解释

touchList:touch类对象组成的数组 touch对象:touch对象表示一个触点,触点属性包含identifier, target, clientX/clientY, pageX/pageY, screenX/screenY, force(接触面最小椭圆角度)/radiusX(接触面最小椭圆X轴)/radiusY(接触面最小椭圆Y轴)/rotationAngle(chrome上目前还是带有webkit前缀的webkitForce(压力)/webkitRadiusX/webkitRadiusY/webkitRotationAngle), 其中identifier用来标识该触点,因为屏幕上可能同时存在多个触点。

事件解析

touchstart事件
1、发生条件:当用户触摸到屏幕时,发生touchstart事件。

实验:

  • 一根手指触摸屏幕: 此时触发一个touchstart事件
  • 两根手指触摸屏幕: 先后发生两个touchstart事件 summary:touchstart是按触摸点(严格说应该是分离的两个触摸点)来定义的。(关于触发原理,这里是臆想而已,这个问题已在知乎上提问,届时更新)
2、属性解析
  • changedTouches属性: 一个touchList数组,表示该事件发生时发生改变的触点,一般都是该触点本身。
    此时来说说indetifier这个属性吧:
    实验:
    1、一根手指Fa触发touchstart,该对象的identifier为0;
    2、放上手指Fb, 此时触发第二个touchstart事件,identifier为1;
    3、再放上Fc,此时触发第三个touchstart事件,identifier为2;
    4、再放上Fd,此时触发第四个touchstart事件,identifier为3;
    5、此时把Fa抬起,放上Fe, 此时触发第五的touchstart事件,identifier为0;
    6、此时把Fd抬起,放上Ff,此时触发第六个tounchstart事件,identifier为3;
    summary:标识会自动填上从0开始且不存在的identifier,所以以indentifier来标识触摸点时要注意这一点。

  • touches属性:
    一 个 TouchList 对象,包含了所有当前接触触摸平面的触点的 Touch 对象,无论它们的起始于哪个element 上,也无论它们状态是否发生了变化。【摘自https://developer.mozilla.org

  • targetTouches属性:
    一个 TouchList 对象,是包含了如下触点的 Touch 对象:触摸起始于当前事件的目标 element 上,并且仍然没有离开触摸平面的触点.【摘自https://developer.mozilla.org
    targetTouches和touches联合起来做一个实验:
    1、在div#test上绑定touchstart事件;
    2、Fa放置在div#test, 触发一次touchstart;
    3、Fb放置在div#test以外的body上;
    4、Fc触发一次div#test上的触屏并立即抬起;
    5、Fd触发一次div#test上的touchstart;
    现象:此时我们观察两个Fa触发的touchstart事件对象和Fd触发的touchstart事件对象:
    Fa的touchstart中touches和targetTouches对象都仅仅包含一个touch对象,对应Fa的触摸点。
    Fd的touchstart中touches和targetTouches对象是一样的,包含两个touch对象,分别是Fa和Fd对应的触摸点。
touchmove事件

当触摸点在触摸平面上移动时,触发touchmove事件。
实验:
* 再次拿出touch对象,但这时我们实验一下touch对象的target属性:
简单地让Fa从div#test滑出到body区域
过程中我们记录了touchmove事件,为简单起见,我们查看最后一个touchmove的事件对象,我们发现:
现象:此时的changedTouches中的touch对象的target为div#test,所以target属性指的是触发事件时所在的元素;
我们在此又发现一个现象:
从始至终,这一系列的touchmove事件都会触发div#test上的touchmove事件回调;

  • 旨在观察当move过程中删除div#test的情况。代码如下:
var test=document.getElementById('test');

window.addEventListener('touchmove',function(e){
  console.log('move on the window');
  console.log(e);
},false);

test.addEventListener('touchmove',function(e){
  e.preventDefault();
  //e.stopPropagation();
  console.log('move on the test');
  console.log(e);
  if(e.changedTouches[0].clientY>420){
    //因为该测试中div#test的高度为400px且起点为(0,0)
    if(test.parentNode){
      test.parentNode.removeChild(test);
      console.log('remove')
    }
  }
},false);

我们依然简单地让Fa从div#test滑出到body区域。
现象:在控制台上可以看出:

当div#test被删除后,只执行了div#test上的touchmove, 但已经不再冒泡到window。 注意:remove打印出来之前,事件已经冒泡到了window,所以随后有一个window的touchend的回调被执行。

touchend事件

当触摸点离开屏幕时触发touchend事件。 实验: * 在div#test上触屏后离开,触点无移动 现象:触发div#test的touchend事件
* 在div#test上滑动,且过程中没有离开div#test 现象:不会触发touchend事件
* 在div#test上滑动,且最终停止到body上并抬起手指 现象:不会触发touchend事件

Categories: JavaScript

ECMAScript 6 Features 中文版

阅读详情 -> es6features/README.md at master · ES-CN/es6features.

如词不达意,欢迎提 PR & issue

采用中英混排的方式进行译制,如不解请查看对应原文

本文档将与原作者的 文档 保持同步更新,欢迎关注

Contributors 翻译贡献者

Introduction 简介

ECMAScript 6, also known as ECMAScript 2015, is the upcoming version of the ECMAScript standard. This standard is targeting ratification in June 2015. ES6 is a significant update to the language, and the first update to the language since ES5 was standardized in 2009. Implementation of these features in major JavaScript engines is underway now.

ECMAScript 6(标准官方名称是 ECMAScript 2015) 是 ECMAScript 的下一代标准,预计将在 2015年6月 正式发布。ES6 的发布将是这门语言自2009年 ES5 正式发布以来的首次更新,是一次富有意义的更新。主流Javascript引擎中的这些新特性正在开发中。

See the draft ES6 standard for full specification of the ECMAScript 6 language.

若希望阅读 ECMAScript 6 语言的完整规范,请参见ES6标准草案

ES6 includes the following new features:

ES6 包含了以下这些新特性:

ECMAScript 6 Features 特性

Arrows 箭头函数

Arrows are a function shorthand using the => syntax. They are syntactically similar to the related feature in C#, Java 8 and CoffeeScript. They support both expression and statement bodies. Unlike functions, arrows share the same lexical this as their surrounding code.

箭头函数是使用=>语法的函数简写形式。这在语法上与 C#、Java 8 和 CoffeeScript 的相关特性非常相似。它们同时支持表达式体和语句体。与(普通的)函数所不同的是,箭头函数和其上下文中的代码共享同一个具有词法作用域的this

// Expression bodies
// 表达式体
var odds = evens.map(v => v + 1);
var nums = evens.map((v, i) => v + i);
var pairs = evens.map(v => ({even: v, odd: v + 1}));

// Statement bodies
// 语句体
nums.forEach(v => {
  if (v % 5 === 0)
    fives.push(v);
});

// Lexical this
// 具有词法作用域的 this
var bob = {
  _name: "Bob",
  _friends: ["Amy", "Bob", "Cinne", "Dylan", "Ellen"],
  printFriends() {
    this._friends.forEach(f =>
      console.log(this._name + " knows " + f));
  }
}

Classes 类

ES6 classes are a simple sugar over the prototype-based OO pattern. Having a single convenient declarative form makes class patterns easier to use, and encourages interoperability. Classes support prototype-based inheritance, super calls, instance and static methods and constructors.

ES6 的类是在基于原型的面向对象模式之上的简单语法糖,它有唯一的、便捷的声明形式,这使得类模式更容易使用,并且鼓励了互操作性。class定义的类支持基于原型的继承、super 调用、实例和静态方法以及构造函数。

class SkinnedMesh extends THREE.Mesh {
  constructor(geometry, materials) {
    super(geometry, materials);

    this.idMatrix = SkinnedMesh.defaultMatrix();
    this.bones = [];
    this.boneMatrices = [];
    //...
  }
  update(camera) {
    //...
    super.update();
  }
  get boneCount() {
    return this.bones.length;
  }
  set matrixType(matrixType) {
    this.idMatrix = SkinnedMesh[matrixType]();
  }
  static defaultMatrix() {
    return new THREE.Matrix4();
  }
}

Enhanced Object Literals 增强的Object字面量

Object literals are extended to support setting the prototype at construction, shorthand for foo: foo assignments, defining methods, making super calls, and computing property names with expressions. Together, these also bring object literals and class declarations closer together, and let object-based design benefit from some of the same conveniences.

对象字面量被扩展以支持以下特性:在构建的时候设置原型、foo: foo赋值的简写形式、定义方法、进行super 调用以及使用表达式计算属性名称等。这样就使得对象字面量和类的声明的联系更加紧密,使得基于对象的设计更加便利。

var obj = {
    // __proto__
    __proto__: theProtoObj,
    // Shorthand for ‘handler: handler’
    // ‘handler: handler’ 的简写形式
    handler,
    // Methods
    toString() {
      // Super calls
      return "d " + super.toString();
    },
    // Computed (dynamic) property names
    // 计算所得的(动态的)属性名称
    [ 'prop_' + (() => 42)() ]: 42
};

Template Strings 模板字符串

Template strings provide syntactic sugar for constructing strings. This is similar to string interpolation features in Perl, Python and more. Optionally, a tag can be added to allow the string construction to be customized, avoiding injection attacks or constructing higher level data structures from string contents.

模板字符串提供构造字符串的语法糖,这与Perl、Python等许多语言中的字符串插值功能非常相似,你也可以通过添加标签(tag)来自定义构造字符串,避免注入攻击,或者基于字符串构建更高层次的数据结构。

// Basic literal string creation
// 基础字符串字面量的创建
`In JavaScript '\n' is a line-feed.`

// Multiline strings
// 多行字符串
`In JavaScript this is
 not legal.`

 // String interpolation
// 字符串插值
var name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`

// Construct an HTTP request prefix is used to interpret the replacements and construction
// 构造一个HTTP请求前缀用来解释替换和构造,大意就是可以构造一个通用的HTTP prefix并通过赋值生成最终的HTTP请求
GET`http://foo.org/bar?a=${a}&b=${b}
    Content-Type: application/json
    X-Credentials: ${credentials}
    { "foo": ${foo},
      "bar": ${bar}}`(myOnReadyStateChangeHandler);

Destructuring 解构

Destructuring allows binding using pattern matching, with support for matching arrays and objects. Destructuring is fail-soft, similar to standard object lookup foo["bar"], producing undefined values when not found.

解构允许在(变量-值)绑定时使用模式匹配,支持匹配数组和对象,解构支持失效弱化,与标准的对象查询foo["bar"]相似,当查询无结果时生成undefined值。

// list matching
// 列表匹配
var [a, , b] = [1,2,3];

// object matching
// 对象匹配
var { op: a, lhs: { op: b }, rhs: c }
       = getASTNode()

// object matching shorthand
// binds `op`, `lhs` and `rhs` in scope
// 对象匹配简写形式
var {op, lhs, rhs} = getASTNode()

// 上面作者给的示例看得云里雾里的,这里我再给出一个
function today() { return { d: 2, m: 3, y: 2015 }; }
var { m: month, y: year } = today(); // month = 3, year = 2015

// Can be used in parameter position
// 也可以作为参数使用
function g({name: x}) {
  console.log(x);
}
g({name: 5})

// Fail-soft destructuring
// 失效弱化解构,结果查询不到时定义为 undefined
var [a] = [];
a === undefined;

// Fail-soft destructuring with defaults
// 具备默认值的失效弱化解构
var [a = 1] = [];
a === 1;

Default + Rest + Spread 默认值+多余参数组合+参数伸展

Callee-evaluated default parameter values. Turn an array into consecutive arguments in a function call. Bind trailing parameters to an array. Rest replaces the need for arguments and addresses common cases more directly.

支持由被调用函数进行求值的参数默认值。 在函数调用时使用...运算符,可以将作为参数的数组拆解为连续的多个参数。 在函数定义时使用...运算符,则可以将函数尾部的多个参数绑定到一个数组中。 “多余参数组合”取代了arguments,并可更直接地应用于通常的用例中。

function f(x, y=12) {
  // y is 12 if not passed (or passed as undefined)
  return x + y;
}
f(3) == 15
function f(x, ...y) {
  // y is an Array
  return x * y.length;
}
f(3, "hello", true) == 6
function f(x, y, z) {
  return x + y + z;
}
// Pass each elem of array as argument
f(...[1,2,3]) == 6

Let + Const 操作符

Block-scoped binding constructs. let is the new varconst is single-assignment. Static restrictions prevent use before assignment.

let 和 const 是具有块级作用域的绑定用构造,let 是新的 var,只在块级作用域内有效,const 是单赋值,声明的是块级作用域的常量。此两种操作符具有静态限制,可以防止出现“在赋值之前使用”的错误。

function f() {
  {
    let x;
    {
      // okay, block scoped name
      const x = "sneaky";
      // error, const
      x = "foo";
    }
    // error, already declared in block
    let x = "inner";
  }
}

Iterators + For..Of 迭代器 + For..of 循环

Iterator objects enable custom iteration like CLR IEnumerable or Java Iterable. Generalize for..in to custom iterator-based iteration with for..of. Don’t require realizing an array, enabling lazy design patterns like LINQ.

迭代器对象允许像 CLI IEnumerable 或者 Java Iterable 一样自定义迭代器。将for..in转换为自定义的基于迭代器的形如for..of的迭代,不需要实现一个数组,支持像 LINQ 一样的惰性设计模式

let fibonacci = {
  [Symbol.iterator]() {
    let pre = 0, cur = 1;
    return {
      next() {
        [pre, cur] = [cur, pre + cur];
        return { done: false, value: cur }
      }
    }
  }
}

for (var n of fibonacci) {
  // truncate the sequence at 1000
  if (n > 1000)
    break;
  console.log(n);
}

Iteration is based on these duck-typed interfaces (using TypeScript type syntax for exposition only):

迭代器基于这些鸭子类型的接口 (此处使用TypeScript 的类型语法,仅用于阐述问题):

interface IteratorResult {
  done: boolean;
  value: any;
}
interface Iterator {
  next(): IteratorResult;
}
interface Iterable {
  [Symbol.iterator](): Iterator
}

Generators 生成器

Generators simplify iterator-authoring using function* and yield. A function declared as function* returns a Generator instance. Generators are subtypes of iterators which include additional next and throw. These enable values to flow back into the generator, so yield is an expression form which returns a value (or throws).

生成器通过使用function*yield简化迭代器的编写, 形如function*的函数声明返回一个生成器实例,生成器是迭代器的子类型,迭代器包括附加的nextthrow,这使得值可以回流到生成器中,所以,yield是一个返回或抛出值的表达式形式。

Note: Can also be used to enable ‘await’-like async programming, see also ES7 await proposal. 注意:也可以被用作类似‘await’一样的异步编程中,具体细节查看ES7的await提案

var fibonacci = {
  [Symbol.iterator]: function*() {
    var pre = 0, cur = 1;
    for (;;) {
      var temp = pre;
      pre = cur;
      cur += temp;
      yield cur;
    }
  }
}

for (var n of fibonacci) {
  // truncate the sequence at 1000
  if (n > 1000)
    break;
  console.log(n);
}

The generator interface is (using TypeScript type syntax for exposition only): 生成器接口如下(此处使用TypeScript 的类型语法,仅用于阐述问题):

interface Generator extends Iterator {
    next(value?: any): IteratorResult;
    throw(exception: any);
}

Unicode 统一码

Non-breaking additions to support full Unicode, including new Unicode literal form in strings and new RegExp u mode to handle code points, as well as new APIs to process strings at the 21bit code points level. These additions support building global apps in JavaScript.

Non-breaking additions to support full Unicode

这句看了半天不知道作者想要表达什么,我就查了下资料,有一种可能是: 增加不换行空格的特性以全面支持Unicode,还有一种可能是:渐进增强地、非破坏性地全面支持Unicode,也就是说,新加入的特性并不影响老的代码的使用。我个人比较倾向于第二种解读。@sumhat提示说第二种解读是正确的

(续)字符串支持新的Unicode文本形式,也增加了新的正则表达式修饰符u来处理码位,同时,新的API可以在21bit码位级别上处理字符串,增加这些支持后可以使用 Javascript 构建全球化的应用。 注:关于Unicode推荐阅读复杂的Unicode,疑惑的Python

// same as ES5.1
// 与 ES5.1 相同
"

Categories: JavaScript

深入浅出 React Native:使用 JavaScript 构建原生应用 – 前端外刊评论 – 知乎专栏

阅读详情 -> 深入浅出 React Native:使用 JavaScript 构建原生应用 – 前端外刊评论 – 知乎专栏.

数月前,Facebook 对外宣布了正在开发的 React Native 框架,这个框架允许你使用 JavaScript 开发原生的 iOS 应用——就在今天,Beta 版的仓库释出了!

基于 PhoneGap 使用 JavaScript 和 HTML5 开发 iOS 应用已经有好几年了,那 React Native 有什么牛的?

React Native 真的很牛,让大家兴奋异常的主要原因有两点:

  1. 可以基于 React Native使用 JavaScript 编写应用逻辑,UI 则可以保持全是原生的。这样的话就没有必要就 HTML5 的 UI 做出常见的妥协;

  2. React 引入了一种与众不同的、略显激进但具备高可用性的方案来构建用户界面。长话短说,应用的 UI 简单通过一个基于应用目前状态的函数来表达。

React Native 的关键就是,以把 React 编程模式的能力带到移动开发来作为主要目标。它的目标不是跨平台一次编写到处执行,而是一次学习跨平台开发。这个是一个非常大的区别。这篇教程只介绍 iOS 平台,不过你一旦掌握了相关的概念,就可以应用到 Android 平台,快速构建 Android 应用。

如果之前只用过 Objective-C 或者 Swift 写应用的话,你很可能不会对使用 JavaScript 来编写应用的愿景感到兴奋。尽管如此,作为一个 Swift 开发者来说,上面提到的第二点应该可以激起你的兴趣!

你通过 Swift,毫无疑问学习到了新的更多有效的编码方法和技巧,鼓励转换和不变性。然而,构建 UI 的方式还是和使用 Objective-C 的方式一致。仍然以 UIKit 为基础,独断专横。

通过像 virtual DOM 和 reconciliation 这些有趣的概念,React 将函数式编程直接带到了 UI 层。

这篇教程将带着你一路构建一个 UK 房产搜索应用:

如果你之前一点 JavaScript 都没写过,别担心。这篇教程带着你进行一步一步进行编码。React 使用 CSS 属性来定义样式,一般比较容易读也比较容易理解。但是如果你想了解更多的话,可以去看看 Mozilla Developer Network reference,很不错的。

想要学习更多,继续往下读!

准备工作

React Native 框架托管在 GitHub 上。你可以通过两种方式获取到它:使用 git 克隆仓库,或者下载一个 zip 压缩包文件。如果你的机器上已经安装了 React Native,在着手编码前还有其他几个因素需要考虑。

  • React Native 借助 Node.js,即 JavaScript 运行时来创建 JavaScript 代码。如果你已经安装了 Node.js,那就可以上手了。

首先,使用 Homebrew 官网提供的指引安装 Homebrew,然后在终端执行以下命令:

brew install node

接下来,使用 homebrew 安装 watchman,一个来自Facebook 的观察程序:

brew install watchman

通过配置 watchman,React 实现了在代码发生变化时,完成相关的重建的功能。就像在使用 Xcode 时,每次保存文件都会进行一次创建。

React Native 有很多的依赖,需要在运行之前安装好。在 React Native 文件目录下打开一个终端,执行下面代码:

npm install

这里通过 Node 包管理器抓取到项目的所有依赖;功能上和 CocoaPods 或者 Carthage 类似。成功执行该命令后,你会发现一个 node_modules 文件夹被创建,包含了各种外部依赖。

最后,启动开发服务器。在刚才打开的终端中,执行下面命令:

npm start

执行上面命令,你会看到:

$ npm start

> react-native@0.1.0 start /Users/colineberhardt/Projects/react-native
> ./packager/packager.sh

 ===============================================================
 |  Running packager on port 8081.       
 |  Keep this packager running while developing on any JS         
 |  projects. Feel free to close this tab and run your own      
 |  packager instance if you prefer.                              
 |                                                              
 |     https://github.com/facebook/react-native                 
 |                                                              
 ===============================================================


    React packager ready.

就这样简单,准备开始!脚本在终端继续执行,我们继续。

至此,我推荐尝试一个 React Native 示例来测试配置项。在 react-native/Examples/Movies 文件夹下打开项目,然后创建并且运行它,确保你可以正确地发布这个 Movies 应用。

注意:在进入编码工作之前,还有最后一件事 —— 在这个教程中,你需要编写大量的 JavaScript 代码,Xcode 并非是最好的工具!我使用 Sublime Text,一个价格合理且应用广泛的编辑器。不过,atombrackets 或者其他轻量的编辑器都能胜任这份工作。

React Native 你好

在开始“搜房App”之前,先来个简单的 Hello World App 热热身。在这一节里,你将会使用到一些组件。

下载起始项目,解压缩到react-native/Examples目录中。解压完成后,在Xcode中打开 PropertyFinder 项目,不要直接运行这个项目,还需要加上一些JS!

在编辑器中打开 PropertyFinderApp.js,将下面这行代码加到文件的开头位置:

'use strict';

这行代码是用于开启 Strict Mode,Strict mode的错误处理可以有所提高,JavaScript的一些语言缺陷也可以避免。简而言之就是,JavaScript在这种模式下工作地更好!

注意:想要研究一下 Strict Mode 的朋友,我会推荐你阅读 Jon Resig 的文章:“ECMAScript 5 Strict Mode, JSON, and More”

然后,加上这一行:

var React = require('react-native');

这句代码是将 react-native 模块加载进来,并将它赋值给变量 React 的。React Native 使用同 Node.js 相同的模块加载方式:require,这个概念可以等同于 Swift 中的“链接库”或者“导入库”。

注意:想要了解更多关于 JavaScript 模块的知识,我推荐阅读 Addy Osmani 写的这篇文章

在 require 语句的下面,加上这一段:

var styles = React.StyleSheet.create({
  text: {
    color: 'black',
    backgroundColor: 'white',
    fontSize: 30,
    margin: 80
  }
});

以上代码定义了一段应用在 “Hello World” 文本上的样式。如果你曾接触过We开发,那你很可能已经发现了:React Native 使用的是 CSS 来定义应用界面的样式。

现在我们来关注应用本身吧!依然是在相同的文件下,将以下代码添加到样式代码的下面:

class PropertyFinderApp extends React.Component {
  render() {
    return React.createElement(React.Text, {style: styles.text}, "Hello World!");
  }
}

是的,奏是 JavaScript class!

类 (class) 是在ES6中被引入的,纵然JavaScript一直在进步,但Web开发者受困于兼容浏览器的状况中,不能怎么使用JS的新特性。React Native运行在JavaScriptCore中是,也就是说,你可以使用JS的新特性啦,完全不用担心兼容什么的呢。

注意:如果你是一名 Web 开发者,我百分百鼓励你要使用现代的JavaScript,然后使用像 Babel 这样的工具生成兼容性的 JavaScript,用于支持兼容性不好的老浏览器。

PropertyFinderApp 继承了 React.Component(React UI的基础模块)。组件包含着不可变的属性,可变的状态变量以及暴露给渲染用的方法。这会你做的应用比较简单,只用一个渲染方法就可以啦。

React Native 组件并不是 UIKit 类,它们只能说是在某种程度上等同。框架只是将 React 组件树转化成为原生的UI。

最后一步啦,将这一行加在文件末尾:

React.AppRegistry.registerComponent('PropertyFinderApp', function() { return PropertyFinderApp });

AppRegistry 定义了App的入口,并提供了根组件。

保存 PropertyFinderApp.js,回到Xcode中。确保 PropertyFinder 规划(scheme)已经勾选了,并设置了相应的 iPhone 模拟器,然后生成并运行你的项目。几秒之后,你应该就可以看到 “Hello World” 应用正在运行了:

这个JavaScript应用运行在模拟器上,使用的是原生UI,没有任何内嵌的浏览器哦!

还不相信这是真的?:] 那打开你的 Xcode,选择 Debug\View Debugging\Capture View Hierarchy,你看 native view hierarchy 中都没有 UIWebView,就只有一个原生的view!:]

你一定很好奇其中的原理吧,那就在 Xcode 中打开 AppDelegate.m,接着找到 application:didFinishLaunchingWithOptions:这个方法构建了 RCTRootView 用于加载 JavaScript 应用以及渲染最后的视图的。

当应用开始运行的时候,RCTRootView将会从以下的URL中加载应用:

http://localhost:8081/Examples/PropertyFinder/PropertyFinderApp.includeRequire.runModule.bundle

重新调用了你在运行这个App时打开的终端窗口,它开启了一个 packager 和 server 来处理上面的请求。

在 Safari 中打开那个 URL;你将会看到这个 App 的 JavaScript 代码。你也可以在 React Native 框架中找到你的 “Hello World” 代码。

当你的App开始运行了以后,这段代码将会被加载进来,然后 JavaScriptCore 框架将会执行它。在 Hello World 的例子里,它将会加载 PropertyFinderApp 组件,然后构建出原生的 UIKit 视图。关于这部分的内容,后文里会再详细解释的。

你好 JSX 的世界

你当前的应用程序会使用 React.createElement 来构建应用 UI ,React会将其转换到原生环境中。在当前情况下,你的JavaScript代码是完全可读的,但一个更复杂的 UI 与嵌套的元素将迅速使代码变成一大坨。

确保应用程序仍在运行,然后回到你的文本编辑器中,编辑 PropertyFinderApp.js 。修改组件 render 方法的返回语句如下:

return <React.Text style={styles.text}>Hello World (Again)</React.Text>;

这是 JSX ,或 JavaScript 语法扩展,它直接在你的 JavaScript 代码中混合了类似 HTML 的语法;如果你是一个 web 开发人员,应该对此不陌生。在本篇文章中你将一直使用 JSX 。

把你的改动保存到 PropertyFinderApp.js 中,并返回到模拟器。按下 Cmd + R ,你将看到你的应用程序刷新,并显示更新的消息 “Hello World(again)”。

重新运行一个 React Native 应用程序像刷新 web 浏览器一样简单!:]

因为你会使用相同的一系列 JavaScript 文件,您可以让应用程序一直运行,只在更改和保存 PropertyFinderApp.js 后刷新即可

注意:如果你感到好奇,可以看看你的“包”在浏览器中,JSX被转换成什么。

这个 “Hello World” 已经够大家玩耍了,是时候构建实际的应用程序了!

添加导航

我们的房产查找应用使用标准的栈式导航,基于 UIKit 的 navigation controller。现在正是添加的时候。

在 index.ios.js 文件中,把 PropertyFinderApp 重命名为HelloWorld:

class HelloWorld extends React.Component {

“Hello World” 这几个字你还需要让它显示一会儿,但它不再是应用的根组件了。

接下来,在 HelloWorld 这个组件下面添加如下这个类:

class PropertyFinderApp extends React.Component {
  render() {
    return (
      <React.NavigatorIOS
        style={styles.container}
        initialRoute={{
          title: 'Property Finder',
          component: HelloWorld,
        }}/>
    );
  }
}

构造一个 navigation controller,应用一个样式,并把初始路由设为 Hello World 组件。在 Web 开发中,路由就是一种定义应用导航的一种技术,即定义页面——或者说是路由——与 URL 的对应关系。

在同一个文件中,更新样式定义,包含如下 container 的样式:

var styles = React.StyleSheet.create({
  text: {
    color: 'black',
    backgroundColor: 'white',
    fontSize: 30,
    margin: 80
  },
  container: {
    flex: 1
  }
});

在随后的教程中会告诉你 flex: 1 是什么意思。

回到模拟器,Cmd+R,看看新 UI 的样子:

这就是包含了 root view 的 navigation controller,目前 root view 就是 “Hello World”。很棒——应用已经有了基础的导航结构,到添加真实 UI 的时候了。

创建搜索页

在项目中添加一个新文件,命名为 SearchPage.js,然后将其放在PropertyFinderApp.js 所在目录下。在文件中添加下面代码:

'use strict';

var React = require('react-native');
var {
  StyleSheet,
  Text,
  TextInput,
  View,
  TouchableHighlight,
  ActivityIndicatorIOS,
  Image,
  Component
} = React;

你会注意到,位于引入 react-native 所在位置的前面有一个严格模式标识,紧接着的声明语句是新知识。

这是一种解构赋值,准许你获取对象的多个属性并且使用一条语句将它们赋给多个变量。结果是,后面的代码中可以省略掉 React 前缀;比如,你可以直接引用 StyleSheet ,而不再需要 React.StyleSheet。解构同样适用于操作数组,更多细节请戳这里

继续在 SearchPage.js 文件中添加下面的样式:

var styles = StyleSheet.create({
  description: {
    marginBottom: 20,
    fontSize: 18,
    textAlign: 'center',
    color: '#656565'
  },
  container: {
    padding: 30,
    marginTop: 65,
    alignItems: 'center'
  }
});

同样,以上都是标准的 CSS 属性。和 Interface Builder 相比,这样设置样式缺少了可视化,但是比起在 viewDidLoad() 中逐个设置视图属性的做法更友好!

只需要把组件添加到样式声明的前面:

class SearchPage extends Component {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.description}>
          Search for houses to buy!
        </Text>
        <Text style={styles.description}>
          Search by place-name, postcode or search near your location.
        </Text>
      </View>
    );
  }
}

render 很好地展示出 JSX 以及它表示的结构。通过这个样式,你可以轻易地描绘出组件 UI 的结构:一个容器,包含两个 text 标签。

最后,将下面的代码添加到文件末尾:

module.exports = SearchPage;

这可以 export SearchPage 类,方便在其他文件中使用它。

下一步是更新应用的路由,以初始化路由。

打开 PropertyFinderApp.js,在文件顶部紧接着上一个 require 语句的位置添加下面代码:

var SearchPage = require('./SearchPage');

在 PropertyFinderApp 类的 render 函数内部,通过更新 initialRoute 来引用最新添加的页面,如下:

component: SearchPage

此时,如果你愿意则可以移除 HelloWorld 类以及与它相关联的样式。你不在需要那段代码了。

切换到模拟器,按下 Cmd+R 查看新的 UI:

使用 Flexbox 定义外观

现在,你已经看到了用基本的 CSS 属性来控制外间距(margin),内间距(padding)还有颜色(color)。不过,可能你还不太了解要如何使用伸缩盒(flexbox),flexbox 是最近新加入 CSS 规范,用它就能很便利地布局界面。

React Native 用 css-layout(这是一个用 JavaScript 实现flexbox标准然后编译成 C(iOS平台)或者Java(Android平台)的库)。

Facebook把这个项目单独出来实在太正确了,这样可以编译成多种语言,促进更多新颖的应用的发展,比如flexbox layout to SVG

在你的App中,容器(container)默认地是纵向布局,也就是说在它的子元素将会竖直地排列,像这样:

这被称为主轴 (main axis),它的方向可以是竖直的也可以是水平的。

每一个子元素在竖直方向上的位置是由它的margin,height和padding共同决定的。容器的 alignItems 属性也要设置成 center,这个属性可以控制子元素在十字轴上的位置。在这里,它实现了居中对齐的文本。

好啦,现在我们把输入框和按钮加上去吧。打开 SearchPage.js,将下面的代码插入第二个 Text 元素的后面:

<View style={styles.flowRight}>
  <TextInput
    style={styles.searchInput}
    placeholder='Search via name or postcode'/>
  <TouchableHighlight style={styles.button}
      underlayColor='#99d9f4'>
    <Text style={styles.buttonText}>Go</Text>
  </TouchableHighlight>
</View>
<TouchableHighlight style={styles.button}
    underlayColor='#99d9f4'>
  <Text style={styles.buttonText}>Location</Text>
</TouchableHighlight>

现在你已经加上了两个最高等级的视图(top-level view),一个视图包含了文本输入框和一个按钮,还有一个视图内只有一个按钮。在后文中你会看到,它们的样式是什么样的。

接着,添加上对应的样式:

flowRight: {
  flexDirection: 'row',
  alignItems: 'center',
  alignSelf: 'stretch'
},
buttonText: {
  fontSize: 18,
  color: 'white',
  alignSelf: 'center'
},
button: {
  height: 36,
  flex: 1,
  flexDirection: 'row',
  backgroundColor: '#48BBEC',
  borderColor: '#48BBEC',
  borderWidth: 1,
  borderRadius: 8,
  marginBottom: 10,
  alignSelf: 'stretch',
  justifyContent: 'center'
},
searchInput: {
  height: 36,
  padding: 4,
  marginRight: 5,
  flex: 4,
  fontSize: 18,
  borderWidth: 1,
  borderColor: '#48BBEC',
  borderRadius: 8,
  color: '#48BBEC'
}

要注意格式问题:每一个样式都是用逗号分隔开的,所以别忘了在container 选择器后面还要加上一个逗号。

以上的样式将会应用在你刚刚加上的输入框和按钮上。

现在返回到模拟器,然后按下 Cmd+R 刷新界面:

文本区域和 ’Go’ 按钮在同一行,不需要显式地定义两个组件的宽度,你只需要将它们放在同一个容器中,加上 flexDirection:’row’ 样式,再定义好它们的 flex 值。文本区域是 flex:4,按钮则是 flex:1,这说明两者的宽度比是4:1。

大概你也发现了,你的“按钮”其实并不是按钮!:] 使用了 UIKit 后,按钮更倾向于是可以轻碰(tap)的标签(label),所以 React Native 团队决定直接在 JavaScript 中构建按钮了。所以你在 App 中使用的按钮是 TouchableHighlight,这是一个 React Native 组件,当轻碰 TouchableHighlight 时,它会变得透明从而显示出衬底的颜色(也就是按钮下层的组件颜色)。

搜索界面的最后一步就是加上一张图片.你可以从这里下载我们用的图片素材并解压。

在Xcode中打开Images.xcassets文件,点击加号添加一个新的图片集。然后将图片素材拖进正确的“区间”:

你需要重启应用才能让图片生效。

将以下代码添加到 TouchableHighlight 组件后面,它将用于“获取位置”按钮:

<Image source={require('image!house')} style={styles.image}/>

现在再样式表的最后加上图片对应的样式,别忘了给原样式中最后一个加上逗号哦:

image: {
  width: 217,
  height: 138
}

require(‘image!house’) 语句用于确定在你应用的asset目录下的图片资源,在 Xcode 中,如果你的打开了 Images.xcassets,你会看到一个“房屋”的图标,正是上面代码中引用到的。

返回到模拟器,Cmd+R刷新UI:

注意:如果你这会没有看到“房屋”图片,取而代之的是一张“找不到资源”的图片,尝试重启packager(也就是在终端里输入 npm start 命令)。

现在你的应用看起来挺不错的啦,不过它还少了点功能。接下来你的任务就是给它加上点状态,让它执行一些操作。

添加组件状态

每个 React 组件都带有一个key-value存储的状态对象,你必须在组件渲染之前设置其初始状态。

在 SearchPage.js 中,我们对 SearchPage 类中,render方法前添加以下的代码。

constructor(props) {
  super(props);
  this.state = {
    searchString: 'london'
  };
}

现在你的组件拥有一个状态变量:searchString ,且初始值被设置为 london 。

这时候你需要利用起组件中的状态了。在render方法中,用以下的代码替换TextInput元素中的内容:

<TextInput
  style={styles.searchInput}
  value={this.state.searchString}
  placeholder='Search via name or postcode'/>

这一步设置了 TextInput 组件 value 属性的值,这个值用于把状态变量 searchString 的当前值作为展示给用户的文字。我们已经考虑初始值的设定了,但如果用户编辑这里的文字会发生什么呢?

第一步需要建立一个方法来处理事件。在 SearchPage 类中添加以下的代码:

onSearchTextChanged(event) {
  console.log('onSearchTextChanged');
  this.setState({ searchString: event.nativeEvent.text });
  console.log(this.state.searchString);
}

上面的代码从 events 中取出了 text 属性的值,并用于更新组件的状态。这里面也添加了一些有用的调试代码。

当文字改变时,需要让这个方法被调用,调用后的文字会通过 render 函数返回到组件中。因此我们需要在标签上添加一个 onChange 属性,添加后的标签如下所示:

<TextInput
  style={styles.searchInput}
  value={this.state.searchString}
  onChange={this.onSearchTextChanged.bind(this)}
  placeholder='Search via name or postcode'/>

当用户更改文本时,会调用 onChange 上 的函数;在本例中,则是 onSearchTextChanged 。

注意:你估计会对 bind(this) 语句有疑问。在 JavaScript 中,this 这个关键字有点不同于大多数其他语言;在 Swift 表示 “自身”。在这种情况中,bind 可以确保在 onSearchTextChanged 方法中, this 可以作为组件实例的引用。有关更多信息,请参见MDN this页面。

在你再次刷新你的应用程序之前,还有一个步骤:在 return 前添加以下语句,打印一条日志来记录 render() 函数的调用:

 console.log('SearchPage.render');

你会从这些日志语句中学到一些很有趣的东西!:]

回到你的模拟器,然后按Cmd + R。您现在应该看到文本输入的初始值为 “london” ,编辑一下文本,从而在 Xcode 控制台中产生一些日志:

注意看上面的截图,日志打印的顺序看起来有些奇怪:

第一次调用 render() 函数用于设置视图。当文本变化时, onSearchTextChanged 函数被调用。之后,通过更新组件的状态来反映输入了新的文本,这会触发另一次 render 。 onSearchTextChanged() 函数也会被调用,会将改变的字符串打印出来。每当应用程序更新任何 React 组件,将会触发整个UI层的重新绘制,这会调用你所有组件的 render 方法。这是一个好主意,因为这样做把组件的渲染逻辑,从状态变化影响UI这一过程中完全解耦出来。

与其他大多数 UI 框架所不同的是,你既不需要在状态改变的时候去手动更新 UI ,或使用某种类型的绑定框架,来创建某种应用程序状态和它的 UI 表现的关联;例如,我的文章中讲的,通过ReactiveCocoa实现MVVM模式

在 React 中,你不再需要担心 UI 的哪些部分可能受到状态变化的影响;你的整个应用程序的 UI,都可以简单地表示为一个函数的状态。

此时,你可能已经发现了这一概念中一个根本性的缺陷。是的,非常准确——性能!

你肯定不能在 UI 变化时,完全抛弃掉整个 UI 然后重新绘制吧
?这就是 React 高明的地方了。每当 UI 渲染出来后,render 方法会返回一颗视图渲染树,并与当前的 UIKit 视图进行比较。这个称之为 reconciliation 的过程的输出是一个简单的更新列表, React 会将这个列表应用到当前视图。这意味着,只有实际改变了的部分才会重新绘制。

这个令人拍案叫绝的崭新概念让ReactJS变得独特——virtual-DOM(文档对象模型,一个web文档的视图树)和 reconciliation 这些概念——被应用于iOS应用程序。

稍后你可以整理下思路,之后,在刚才的应用中你仍然有一些工作要做。日志代码增加了代码的繁琐性,已经不需要了,所以删除掉日志代码。

初始化搜索功能

为了实现搜索功能,你需要处理 “Go” 按钮的点击事件,调用对应的 API,并提供一个视觉效果,告诉用户正在做查询。

在 SearchPage.js 中,在构造函数中把初始状态更新成:

this.state = {
  searchString: 'london',
  isLoading: false
};

新的 isLoading 属性将会记录是否有请求正在处理的状态。

在 render 开始的添加如下逻辑:

var spinner = this.state.isLoading ?
  ( <ActivityIndicatorIOS
      hidden='true'
      size='large'/> ) :
  ( <View/>);

这是一个三元操作符,与 if 语句类似,即根据组件 isLoading 的状态,要么添加一个 indicator,要么添加一个空的 view。因为整个组件会不停地更新,所以你自由地混合 JSX 和 JavaSript 代码。

回到用 JSX 定义搜索界面的地方,在图片的下面添加:

{spinner}

给渲染“Go”的 TouchableHighlight 标记添加如下的属性:

onPress={this.onSearchPressed.bind(this)}

接下来,添加下面这两个方法到 SearchPage 类中:

_executeQuery(query) {
  console.log(query);
  this.setState({ isLoading: true });
}

onSearchPressed() {
  var query = urlForQueryAndPage('place_name', this.state.searchString, 1);
  this._executeQuery(query);
}

_executeQuery() 之后会进行真实的查询,现在的话就是简单输出一条信息到控制台,并且把 isLoading 设置为对应的值,这样 UI 就可以显示新的状态了。

提示:JavaScript 的类并没有访问修饰符,因此没有 “私有” 的该奶奶。因此常常会发现开发者使用一个下划线作为方法的前缀,来说明这些方法是私有方法。

当 “Go” 按钮被点击时,onSearchPressed() 将会被调用,开始查询。

最后,添加下面这个工具函数在定义 SearchPage 类的上面:

function urlForQueryAndPage(key, value, pageNumber) {
  var data = {
      country: 'uk',
      pretty: '1',
      encoding: 'json',
      listing_type: 'buy',
      action: 'search_listings',
      page: pageNumber
  };
  data[key] = value;

  var querystring = Object.keys(data)
    .map(key => key + '=' + encodeURIComponent(data[key]))
    .join('&');

  return 'http://api.nestoria.co.uk/api?' + querystring;
};
  • 这个函数并不依赖 SearchPage,因此被定义成了一个独立的函数,而不是类方法。他首先通过 data 来定义查询字符串所需要的参数,接着将 data 转换成需要的字符串格式,name=value 对,使用 & 符号分割。语法 => 是一个箭头函数,又一个对 JavaScript 语言的扩展,提供了这个便捷的语法来创建一个匿名函数。

回到模拟器,Cmd+R,重新加载应用,点击 “Go” 按钮。你可以看到 activity indicator 显示出来,再看看 Xcode 的控制台:

activity indicator 渲染了,并且作为请求的 URL 出现在输出中。把 URL 拷贝到浏览器中访问看看得到的结果。你会看到大量的 JSON 对象。别担心——你不需要理解它们,之后会使用代码来解析之。

提示:应用使用了 Nestoria 的 API 来做房产的搜索。API 返回的 JSON 数据非常的直白。但是你也可以看看文档了解更多细节,请求什么 URL 地址,以及返回数据的格式。

下一步就是从应用中发出请求。

执行 API 请求

还是 SearchPage.js 文件中,更新构造器中的初始 state 添加一个message 变量:

this.state = {
  searchString: 'london',
  isLoading: false,
  message: ''
};

在 render 内部,将下面的代码添加到 UI 的底部:

<Text style={styles.description}>{this.state.message}</Text>

你需要使用这个为用户展示多种信息。

在 SearchPage 类内部,将以下代码添加到 _executeQuery() 底部:

fetch(query)
  .then(response => response.json())
  .then(json => this._handleResponse(json.response))
  .catch(error => 
     this.setState({
      isLoading: false,
      message: 'Something bad happened ' + error
   }));

这里使用了 fetch 函数,它是 Web API 的一部分。和 XMLHttpRequest 相比,它提供了更加先进的 API。异步响应会返回一个 promise,成功的话会转化 JSON 并且为它提供了一个你将要添加的方法。

最后一步是将下面的函数添加到 SearchPage:

_handleResponse(response) {
  this.setState({ isLoading: false , message: '' });
  if (response.application_response_code.substr(0, 1) === '1') {
    console.log('Properties found: ' + response.listings.length);
  } else {
    this.setState({ message: 'Location not recognized; please try again.'});
  }
}

如果查询成功,这个方法会清除掉正在加载标识并且记录下查询到属性的个数。

注意:Nestoria 有很多种返回码具备潜在的用途。比如,202 和 200 会返回最佳位置列表。当你创建完一个应用,为什么不处理一下这些,可以为用户呈现一个可选列表。

保存项目,然后在模拟器中按下 Cmd+R,尝试搜索 ‘london’;你会在日志信息中看到 20 properties were found。然后随便尝试搜索一个不存在的位置,比如 ‘narnia’,你会得到下面的问候语。

是时候看一下这20个房屋所对应的真实的地方,比如伦敦!

结果显示

创建一个新的文件,命名为 SearchResults.js,然后加上下面这段代码:

'use strict';

var React = require('react-native');
var {
  StyleSheet,
  Image, 
  View,
  TouchableHighlight,
  ListView,
  Text,
  Component
} = React;

你肯定注意到啦,这里用到了 require 语句将 react-native 模块引入其中,还有一个重构赋值语句。

接着就是加入搜索结果的组件:

class SearchResults extends Component {

  constructor(props) {
    super(props);
    var dataSource = new ListView.DataSource(
      {rowHasChanged: (r1, r2) => r1.guid !== r2.guid});
    this.state = {
      dataSource: dataSource.cloneWithRows(this.props.listings)
    };
  }

  renderRow(rowData, sectionID, rowID) {
    return (
      <TouchableHighlight
          underlayColor='#dddddd'>
        <View>
          <Text>{rowData.title}</Text>
        </View>
      </TouchableHighlight>
    );
  }

  render() {
    return (
      <ListView
        dataSource={this.state.dataSource}
        renderRow={this.renderRow.bind(this)}/>
    );
  }

}

上述的代码里用到了一个特定的组件 – ListView – 它能将数据一行行地呈现出来,并放置在一个可滚动的容器内,和 UITableView 很相似。通过 ListView.DataSource 将 ListView 的数据引入,还有一个函数来显示每一行UI。

在构建数据源的同时,你还需要一个函数用来比较每两行之间是否重复。 为了确认列表数据的变化,在 reconciliation 过程中ListView 就会使用到这个函数。在这个实例中,由 Nestoria API 返回的房屋数据都有一个guid 属性,它就是用来测试数据变化的。

现在将模块导出的代码添加至文件末尾:

module.exports = SearchResults;

将下面这段代码加到 SearchPage.js 较前的位置,不过要在 require 语句的后面哦:

var SearchResults = require('./SearchResults');

这样我们就能在 SearchPage 类中使用刚刚加上的 SearchResults 类。

还要把 _handleResponse 方法中的 console.log 语句改成下面这样:

this.props.navigator.push({
  title: 'Results',
  component: SearchResults,
  passProps: {listings: response.listings}
});

SearchResults 组件通过上面的代码传入列表里。在这里用的是 push方法确保搜索结果全部推进导航栈中,这样你就可以通过 ‘Back’ 按钮返回到根页面。

回到模拟器,按下 Cmd+R 刷新页面,然后试试看我们的搜索。估计你会得到类似下面这样的结果:

耶!你的搜索实现了呢,不过这搜索结果页面的颜值也太低了,不要担心,接下来给它化化妆。

可点击样式

这些 React Native 的原生代码现在应该理解起来轻车熟路了,所以本教程将会加快速度。

在 SearchResults.js 中,destructuring 声明后面添加以下语句来定义样式:

var styles = StyleSheet.create({
  thumb: {
    width: 80,
    height: 80,
    marginRight: 10
  },
  textContainer: {
    flex: 1
  },
  separator: {
    height: 1,
    backgroundColor: '#dddddd'
  },
  price: {
    fontSize: 25,
    fontWeight: 'bold',
    color: '#48BBEC'
  },
  title: {
    fontSize: 20,
    color: '#656565'
  },
  rowContainer: {
    flexDirection: 'row',
    padding: 10
  }
});

这些定义了每一行的样式。

接下来修改 renderRow() 如下:

renderRow(rowData, sectionID, rowID) {
  var price = rowData.price_formatted.split(' ')[0];

  return (
    <TouchableHighlight onPress={() => this.rowPressed(rowData.guid)}
        underlayColor='#dddddd'>
      <View>
        <View style={styles.rowContainer}>
          <Image style={styles.thumb} source={{ uri: rowData.img_url }} />
          <View  style={styles.textContainer}>
            <Text style={styles.price}>£{price}</Text>
            <Text style={styles.title} 
                  numberOfLines={1}>{rowData.title}</Text>
          </View>
        </View>
        <View style={styles.separator}/>
      </View>
    </TouchableHighlight>
  );
}

这个操作修改了返回的价格,将已经格式了化的”300000 GBP”中的GBP后缀删除。然后它通过你已经很熟悉的技术来渲染每一行的 UI 。这一次,通过一个 URL 来提供缩略图的数据, React Native 负责在主线程之外解码这些数据。

同时要注意 TouchableHighlight 组件中 onPress属性后使用的箭头函数;它用于捕获每一行的 guid。

最后一步,给类添加一个方法来处理按下操作:

rowPressed(propertyGuid) {
  var property = this.props.listings.filter(prop => prop.guid === propertyGuid)[0];
}

该方法通过用户触发的属性来定位。目前该方法没有做任何事,你可以稍后处理。现在,是时候欣赏你的大作了。

回到模拟器,按下 Cmd + R 查看结果:

看起来好多了——尽管你会怀疑是否任何人都能承受住在伦敦的代价!

是时候向应用程序添加最后一个视图了。

房产详情视图

添加一个新的文件 PropertyView.js 到项目中,在文件的顶部添加如下代码:

'use strict';

var React = require('react-native');
var {
  StyleSheet,
  Image, 
  View,
  Text,
  Component
} = React;

信手拈来了吧!

接着添加如下样式:

var styles = StyleSheet.create({
  container: {
    marginTop: 65
  },
  heading: {
    backgroundColor: '#F8F8F8',
  },
  separator: {
    height: 1,
    backgroundColor: '#DDDDDD'
  },
  image: {
    width: 400,
    height: 300
  },
  price: {
    fontSize: 25,
    fontWeight: 'bold',
    margin: 5,
    color: '#48BBEC'
  },
  title: {
    fontSize: 20,
    margin: 5,
    color: '#656565'
  },
  description: {
    fontSize: 18,
    margin: 5,
    color: '#656565'
  }
});

然后加上组件本身:

class PropertyView extends Component {

  render() {
    var property = this.props.property;
    var stats = property.bedroom_number + ' bed ' + property.property_type;
    if (property.bathroom_number) {
      stats += ', ' + property.bathroom_number + ' ' + (property.bathroom_number > 1
        ? 'bathrooms' : 'bathroom');
    }

    var price = property.price_formatted.split(' ')[0];

    return (
      <View style={styles.container}>
        <Image style={styles.image} 
            source={{uri: property.img_url}} />
        <View style={styles.heading}>
          <Text style={styles.price}>£{price}</Text>
          <Text style={styles.title}>{property.title}</Text>
          <View style={styles.separator}/>
        </View>
        <Text style={styles.description}>{stats}</Text>
        <Text style={styles.description}>{property.summary}</Text>
      </View>
    );
  }
}

render() 前面部分对数据进行了处理,与通常的情况一样,API 返回的数据良莠不齐,往往有些字段是缺失的。这段代码通过一些简单的逻辑,让数据更加地规整一些。

render 剩余的部分就非常直接了。它就是一个简单的这个状态不可变状态的函数。

最后在文件的末尾加上如下的 export:

module.exports = PropertyView;

返回到 SearchResults.js 文件,在顶部,require React 的下面,添加一个新的 require 语句。

var PropertyView = require('./PropertyView');

接下来更新 rowPassed(),添加跳转到新加入的 PropertyView:

rowPressed(propertyGuid) {
  var property = this.props.listings.filter(prop => prop.guid === propertyGuid)[0];

  this.props.navigator.push({
    title: "Property",
    component: PropertyView,
    passProps: {property: property}
  });
}

你知道的:回到模拟器,Cmd + R,一路通过搜索点击一行到房产详情界面:

物廉价美——看上去很不错哦!

应用即将完成,最后一步是允许用户搜索附近的房产。

地理位置搜索

在 Xcode 中,打开 Info.plist 添加一个新的 key,在编辑器内部单击鼠标右键并且选择 Add Row。使用NSLocationWhenInUseUsageDescription 作为 key 名并且使用下面的值:

PropertyFinder would like to use your location to find nearby properties

下面是当你添加了新的 key 后,所得到的属性列表:

你将把这个关键的细节提示呈现给用户,方便他们请求访问当前位置。

打开 SearchPage.js,找到用于渲染 Location 按钮的TouchableHighlight,然后为其添加下面的属性值:

onPress={this.onLocationPressed.bind(this)}

当你用手指轻点这个按钮,会调用 onLocationPressed —— 接下来会定义这个方法。

将下面的代码添加到 SearchPage 类中:

onLocationPressed() {
  navigator.geolocation.getCurrentPosition(
    location => {
      var search = location.coords.latitude + ',' + location.coords.longitude;
      this.setState({ searchString: search });
      var query = urlForQueryAndPage('centre_point', search, 1);
      this._executeQuery(query);
    },
    error => {
      this.setState({
        message: 'There was a problem with obtaining your location: ' + error
      });
    });
}

通过 navigator.geolocation 检索当前位置;这是一个 Web API 所定义的接口,所以对于每个在浏览器中使用 location 服务的用户来说这个接口都应该是一致的。React Native 框架借助原生的 iOS location 服务提供了自身的 API 实现。

如果当前位置很容易获取到,你将调用第一个箭头函数;这会向Nestoria 发送一个 query。如果出现错误则会得到一个基本的出错信息。

因为你已经改变了属性列表,你需要重新启动这个应用以看到更改。抱歉,这次不可以 Cmd+R。请中断 Xcode 中的应用,然后创建和运行项目。

在使用基于位置的搜索前,你需要指定一个被 Nestoria 数据库覆盖的位置。在模拟器菜单中,选择 Debug\Location\Custom Location … 然后输入 55.02 维度和 -1.42 经度,这个坐标是英格兰北部的一个景色优美的海边小镇,我经常在那给家里打电话。

警示:我们可以正常地使用位置搜索功能,不过可能有部分同学不能使用(在访问时返回 access denied 错误)—— 我们尚不确定其原因,可能是 React Native 的问题?如果谁遇到了同样的问题并且已经结果,烦请告诉我们。这里没有伦敦那样值得炫耀 —— 不过更加经济!:]

下一步行动?

完成了第一个 React Native 应用呢,恭喜你!你可以下载本教程的完整代码,亲自来试试看。

如果已经接触过 Web 开发了,你会发现使用 JavaScript 和 React 来定义与原生 UI 相连接的接口和导航是多么地容易。而如果你曾经开发过原生 App,我相信在使用 React Native 的过程里你会感受到它种种好处:快速的应用迭代,JavaScript 的引入以及清晰地使用 CSS 定义样式。

也许下次做 App 的时候,你可以试试这个框架?或者说,你依然坚持使用 Swift 或者 Objective-C?无论之后你的选择是怎么样的,我都希望读完这篇文章的你有所收获,还能把这些收获融入到你的项目当中是最好的啦。

如果你对这篇教程有任何疑问,请在这篇文章的讨论区留言!

原文:Introducing React Native: Building Apps with JavaScript

Categories: JavaScript, 前端周边

JavaScript之web通信

阅读详情 -> JavaScript之web通信 – Div.IO.

web通信,一个特别大的topic,涉及面也是很广的。因最近学习了 javascript 中一些 web 通信知识,在这里总结下。文中应该会有理解错误或者表述不清晰的地方,还望斧正!

前言

comet技术

浏览器作为 Web 应用的前台,自身的处理功能比较有限。浏览器的发展需要客户端升级软件,同时由于客户端浏览器软件的多样性,在某种意义上,也影响了浏览器新技术的推广。在 Web 应用中,浏览器的主要工作是发送请求、解析服务器返回的信息以不同的风格显示。AJAX 是浏览器技术发展的成果,通过在浏览器端发送异步请求,提高了单用户操作的响应性。但 Web 本质上是一个多用户的系统,对任何用户来说,可以认为服务器是另外一个用户。现有 AJAX 技术的发展并不能解决在一个多用户的 Web 应用中,将更新的信息实时传送给客户端,从而用户可能在“过时”的信息下进行操作。而 AJAX 的应用又使后台数据更新更加频繁成为可能。

随着互联网的发展,web 应用层出不穷,也不乏各种网站监控、即时报价、即时通讯系统,为了让用户得到更好的体验,服务器需要频繁的向客户端推送信息。开发者一般会采用基于 AJAX 的长轮询方式或者基于 iframe 及 htmlfile 的流方式处理。当然有些程序需要在客户端安装各种插件( Java applet 或者 Flash )来支持性能比较良好的“推”信息。

HTTP协议中的长、短连接

短连接的操作步骤是:建立连接——数据传输——关闭连接…建立连接——数据传输——关闭连接
长连接的操作步骤是:建立连接——数据传输…(保持连接)…数据传输——关闭连接

长连接与短连接的不同主要在于client和server采取的关闭策略不同。短连接在建立连接以后只进行一次数据传输就关闭连接,而长连接在建立连接以后会进行多次数据数据传输直至关闭连接(长连接中关闭连接通过Connection:closed头部字段)。

web 通信

首先要搞清楚,xhr 的 readystate 各种状态。

属性 描述
onreadystatechange 存储函数(或函数名),每当 readyState 属性改变时,就会调用该函数。
readyState 存有 XMLHttpRequest 的状态。从 0 到 4 发生变化。

0: 请求未初始化
1: 服务器连接已建立
2: 请求已接收
3: 请求处理中
4: 请求已完成,且响应已就绪

status 200: “OK”
404: 未找到页面

轮询

轮询是一种“拉”取信息的工作模式。设置一个定时器,定时询问服务器是否有信息,每次建立连接传输数据之后,链接会关闭。

前端实现:

var polling = function(url, type, data){
    var xhr = new XMLHttpRequest(), 
        type = type || "GET",
        data = data || null;

    xhr.onreadystatechange = function(){
        if(xhr.readyState == 4) {
            receive(xhr.responseText);
            xhr.onreadystatechange = null;
        }
    };

    xhr.open(type, url, true);
    //IE的ActiveXObject("Microsoft.XMLHTTP")支持GET方法发送数据,
    //其它浏览器不支持,已测试验证
    xhr.send(type == "GET" ? null : data);
};

var timer = setInterval(function(){
    polling();
}, 1000);

在轮询的过程中,如果因为网络原因,导致上一个 xhr 对象还没传输完毕,定时器已经开始了下一个询问,上一次的传输是否还会在队列中,这个问题我没去研究。如果感兴趣可以自己写一个ajax的请求管理队列。

长轮询(long-polling)

长轮询其实也没啥特殊的地方,就是在xhr对象关闭连接的时候马上又给他接上~ 看码:

var longPoll = function(type, url){
    var xhr = new XMLHttpRequest();

    xhr.onreadystatechange = function(){
        // 状态为 4,数据传输完毕,重新连接
        if(xhr.readyState == 4) {
            receive(xhr.responseText);
            xhr.onreadystatechange = null;

            longPoll(type, url);
        }
    };

    xhr.open(type, url, true);
    xhr.send();
}

只要服务器断开连接,客户端马上连接,不让他有一刻的休息时间,这就是长轮询。

数据流

数据流方式,在建立的连接断开之前,也就是 readystate 状态为 3 的时候接受数据,但是麻烦的事情也在这里,因为数据正在传输,你拿到的 xhr.response 可能就是半截数据,所以呢,最好定义一个数据传输的协议,比如前2个字节表示字符串的长度,然后你只获取这个长度的内容,接着改变游标的位置。

假如数据格式为: data splitChar data为数据内容,splitChar为数据结束标志(长度为1)。
那么传输的数据内容为 data splitChar data splitChar data splitChar…

var dataStream = function(type, url){
    var xhr = new XMLHttpRequest();

    xhr.onreadystatechange = function(){

        // 状态为 3,数据接收中
        if(xhr.readyState == 3) {
            var i, l, s;

            s = xhr.response; //读取数据
            l = s.length;     //获取数据长度

            //从游标位置开始获取数据,并用分割数据
            s = s.slice(p, l - 1).split(splitChar);

            //循环并操作数据
            for(i in s) if(s[i])  deal(s[i]);

            p = l;  //更新游标位置

        }

        // 状态为 4,数据传输完毕,重新连接
        if(xhr.readyState == 4) {
            xhr.onreadystatechange = null;

            dataStream(type, url);
        }
    };

    xhr.open(type, url, true);
    xhr.send();
};

这个代码写的是存在问题的,当readystate为3的时候可以获取数据,但是这时获取的数据可能只是整体数据的一部分,那后半截就拿不到了。readystate在数据传输完毕之前是不会改变的,也就是说他并不会继续接受剩下的数据。我们可以定时去监听readystate,这个下面的例子中可以看到。

这样的处理不算复杂,但是存在问题。上面的轮询和长轮询是所有浏览器都支持的,所以我就没有写兼容IE的代码,但是这里,低版本IE不允许在readystate为3的时候读取数据,所以我们必须采用其他的方式来实现。

在ajax还没有进入web专题之前,我们已经拥有了一个法宝,那就是iframe,利用iframe照样可以异步获取数据,对于低版本IE可以使用iframe开接受数据流。

if(isIE){
    var dataStream = function(url){
        var ifr = document.createElement("iframe"), doc, timer;

        ifr.src = url;
        document.body.appendChild(ifr);

        doc = ifr.contentWindow.document;

        timer = setInterval(function(){

            if(ifr.readyState == "interactive"){
                // 处理数据,同上
            }

            // 重新建立链接
            if(ifr.readyState == "complete"){
                clearInterval(timer);

                dataStream(url);
            }
        }, 16);
    };
};

定时去监听iframe的readystate的变化,从而获取数据流,不过,上面的处理方式还是存在问题。数据流实现“服务器推”数据的原理是什么呢,简单点说,就是文档(数据)还没有加载完,这个时候浏览器的工作就是去服务器拿数据完成文档(数据)加载,我们就是利用这点,给浏览器塞点东西过去~ 所以上述利用iframe的方式获取数据,会使浏览器一直处于加载状态,title上的那个圈圈一直在转动,鼠标的状态也是loading,这看着是相当不爽的。幸好,IE提高了HTMLFile对象,这个对象就相当于一个内存中的Document对象,它会解析文档。所以我们创建一个HTMLFile对象,在里面放置一个IFRAME来连接服务器。这样,各种浏览器就都支持了。

if(isIE){
    var dataStream = function(url){
        var doc = new ActiveXObject("HTMLFile"), 
            ifr = doc.createElement("iframe"), 
            timer, d;

        doc.write("<body/>");

        ifr.src = url;
        doc.body.appendChild(ifr);

        d = ifr.contentWindow.document;

        timer = setInterval(function(){

            if(d.readyState == "interactive"){
                // 处理数据,同上
            }

            // 重新建立链接
            if(d.readyState == "complete"){
                clearInterval(timer);

                dataStream(url);
            }
        }, 16);
    };
};

websocket

websocket是前端一个神器,ajax用了这么久了,相关技术也是很成熟,不过要实现个数据的拉取确实十分不易,从上面的代码中也看到了,各种兼容性问题,各种细节处理问题,自从有了websocket,哈哈,一口气上五楼…

var ws = new WebSocket("ws://www.example.com:8888");

ws.onopen = function(evt){};
ws.onmessage = function(evt){
    deal(evt.data);
};
ws.onclose  = function(evt){};

//ws.close();

新建一个WebSocket实例,一切就OK了,ws:// 是websocket的连接协议,8888为端口号码。onmessage中提供了data这个属性,相当方便

EventSource

HTML5中提供的EventSource这玩意儿,这是无比简洁的服务器推送信息的接受函数。

new EventSource("test.php").onmessage=function(evt){
    console.log(evt.data);
};

简洁程度和websocket是一样的啦,只是这里有一个需要注意的地方,test.php输出的数据流应该是特殊的MIME类型,要求是”text/event-stream”,如果不设置的话,你试试~ (直接抛出异常)

.ActionScript

情非得已就别考虑这第六种方式了,虽说兼容性最好,要是不懂as,出了点bug你也不会调试。

具体实现方法:在 HTML 页面中内嵌入一个使用了 XMLSocket 类的 Flash 程序。JavaScript 通过调用此 Flash 程序提供的套接口接口与服务器端的套接口进行通信。JavaScript 在收到服务器端以 XML 格式传送的信息后可以很容易地控制 HTML 页面的内容显示。

Java Applet套接口

这玩意儿原理和Flash类似,不过我不懂,就不细说了。

后端处理方式

本文主要是总结Javascript的各种通讯方式,后端配合node来处理,应该是挺给力的。

var conns = new Array();

var ws = require("websocket-server");
var server = ws.createServer();

server.addListener("connection", function(connection){
  console.log("Connection request on Websocket-Server");
  conns.push(connection);
  connection.addListener('message',function(msg){
        console.log(msg);
        for(var i=0; i<conns.length; i++){
            if(conns[i]!=connection){
                conns[i].send(msg);
            }
        }
    });
});
server.listen(8888);

下面是一个php的测试demo。

header('Content-Type:text/html; charset=utf-8');
while(1){
    echo date('Y-m-d H:i:s');
    flush();
    sleep(1);
};

web 通信方式利弊分析

  • 轮询,这种方式应该是最没技术含量的,操作起来最方便,不过是及时性不强,把定时器的间隔时间设置的短一些可以稍微得到缓和。
  • 长轮询,算是比较不错的一个web通讯方式,不过每次断开连接,比较耗服务器资源,客户端到无所谓。
  • 数据流,他和长轮询不同之处是接受数据的时间不一样,数据流是readystate为3的时候接受,低版本IE不太兼容,处理起来略麻烦,而且还要自己设计数据传输协议。不过他对资源的消耗比上面几种都可观。
  • websocket和EventSource,两个利器,不过,没几个浏览器支持,这是比较让人伤心~
  • ActionScript和Java Applet,两者都是需要在客户端安装插件的,一个是Flash插件,一个是Java插件,而且搞前端的人一般对这东西不太熟悉,如果没有封装比较好的库可以使用,那建议还是别用了。

参考资料

Categories: JavaScript

现在起,可以在 Windows 和 OS X 电脑安装运行 Android 应用了 | 36氪

阅读详情 -> 现在起,可以在 Windows 和 OS X 电脑安装运行 Android 应用了 | 36氪.

作为一个急性子,昨天刚允许所有开发者把 Android 应用迁移到 Chrome 上的 Google,今天就上架了提供相关运行支持的 Chrome 插件。

无论是应用移植还是运行,依靠的都是 Google 在 I/O 2014 上发布的 App Runtime for Chrome。而这次跨平台还要得益于 NaCL 技术,NaCL 是 Native Chrome Client 的简称,是为 Chrome 应用提供本地化运行能力的框架。开发者可以通过 NaCL 在系统上充分利用 CPU 和 GPU 资源,获得接近于原生应用的性能。此前 Slack 的 Windows 版客户端就使用 NaCL。

Image title

今天发布的是允许开发者和用户在多平台 Chrome 浏览器中运行 Android 应用的 ARC Welder 插件,用户需要先下载约 114M 的运行环境,然后安装约 11M 的插件主体。插件会要求用户指定一个本地文件夹存放相关文件,完成后就能直接运行本地的 .apk 应用安装文件。最新的 ARC 运行环境中一经包含对 Google Play Service 的支持,应用可以调用 Google 服务 API,也能直接使用 Google+ 账户登录,此外还有完整的硬件和系统环境支持。

用户在 ARC Welder 中安装运行过的 Android 应用会以 Chrome App 形式出现在 Chrome 浏览器应用列表中,但安装新应用时会强制删除上一个部署的应用。用户也可以在导入 apk 时选择 “Download ZIP” 将应用直接打包为 .crx Chrome 插件,供开发者模式下直接部署使用。

在实际使用过程中,会发现现有的 ARC Welder 插件在不同 PC 上回遇到各种程度的联网、Google Play Service 支持问题,并不非常完善。但这次跨平台插件的上架让 Android 开发者在 Android 系统自有的 Android TV、Android Auto、Android Wear 生态外,具备更强的跨平台能力。

Categories: 前端资讯