在阅读了大量 React 的资料(主要是官网)之后,发现有两个概念非常重要
React 的基本元素是组件,所有内容都由各种组件或包含,或组合,搭建而成。
React 从数据渲染页面,单向进行。这个在实践的过程中会慢慢的体会到。
Sample Mobile Application with React and Cordova 页面有一张关于 React 组件的图,说明了这个 React Demo 应用中各组件的关系
Amaze UI React 是在 Amaze UI (jQuery版) 的基础上,抛弃 jQuery,使用 React 开发的组件库。Amaze UI React 和 Amaze UI (jQuery版) 共用一套 CSS。
因此,就从 Amaze 官网的菜单组织也能看到二者的区别。jQuery 版的菜单包含三项:CSS、JS插件、Web组件;而 React 版就只有一项:组件。因此 React 版不再需要用户去了解 CSS 类和过多的脚本操作,只需要关注组件,及其数据(属性和状态)即可。
参考文章的通讯录包含列表页和详情页。学习得一步步进行,所以先实现列表页。
学习的目标是做一个通讯录,功能和而已都与上图类似,只是界面改用 Amaze,所以得先去 Amanze UI React 组件文档页面 了解需要使用的组件,并画出自己的草图。
这个草图就是最初需要实现的东西:一个页头、一个列表、列表项分图标、文本、图标按钮三个部分。而图标按钮点击可以转到拨号页面准备拨号。
目标已经清楚了,下面就要开始搭建程序。第一步,先实现在 Nginx 中能正常显示;第二步再实现构建成 Android 程序在手机上使用(包括显示和拨号操作)。
之前已经做了很充分的准备,但那主要是对环境的准备。现在马上要开始写代码,不得不做一些细致的准备,比如,把需要用到的库安放在代码中适当的位置。
之前已经确定了要使用 jQuery,React 和 Amaze UI React。按我个人的习惯,会把它放在 Web 应用的 /libs
目录下。没有必要去筛选哪些文件用得上哪些用不上,都拷贝过来,结果就有了这样的目录结构
首先就是修改 index.html 文件,在 index.html 中引入需要的 AmazeUI 的 CSS,以及各依赖库的脚本文件。直接从默认生成的 index.html 改过来就好。
<!doctype html> <html> <head> <meta name="format-detection" content="telephone=no"> <meta name="msapplication-tap-highlight" content="no"> <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width"> <title>Concats</title> <link rel="stylesheet" type="text/css" href="libs/amazeui/css/amazeui.min.css" /> <link rel="stylesheet" type="text/css" href="css/index.css"> <script src="libs/jquery/jquery-2.1.4.min.js"></script> <script src="libs/react/react.min.js"></script> <script src="libs/react/JSXTransformer.js"></script> <script src="libs/amazeui/js/amazeui.react.min.js"></script> <script type="text/jsx" src="js/index.jsx"></script> </head> <body> </body> </html>
<body>
标签中留空,因为之后 React 会将组件渲染在 <body>
中。
<head>
最后引入的 js/index.jsx
就是页面对应的脚本,使用 JSX 语法以 React 组件的形式实现。一般这个文件是以 .js
作为扩展名,但是我认为用 .jsx
作为扩展名可以清楚表明该文件中使用了 JSX 语法。
这已经是第2次提到渲染了,为什么说 React 渲染,而不说“执行”、“搭建”、“构造”诸如此类的词呢?先来看看 React 官方是怎么描述 React 的:
React is a JavaScript library for creating user interfaces by Facebook and Instagram. Many people choose to think of React as the V in MVC.
We built React to solve one problem: building large applications with data that changes over time.
简单的说,React 是一个构建用户界面的 JavaScript 库,它为构建大型应用而生,这些应用可能随时产生数据变动。很多人把 React 作为 MVC 中的 V 来使用。
而在其它文章资料中也提到,React 的处理过程是从数据到视图的一个单向过程。综合起来就可以这样理解:React 对数据进行渲染,以 UI 的形式呈现。如果数据发生变动,React 会重新进行渲染(实际上 React 会判断数据变动造成的形式,智能选择最小渲染范围以提高效率)。
上面提到将 <body>
标签内容留空,以便 React 在其中进行渲染。也就是下面这句,React 的渲染入口:
React.render(<Page />, document.body);
上述表达式用了 JSX 语法,而 JSX 中的 X 部分,我理解为是以 XML 方式描写的表达式。为什么是表达式,这会在后面的循环中提及。
正如 React 文档所述,JSX 语法只是一个语法糖,它完全可以用纯粹的脚本来写,而且 React 本身也是需要将 JSX 翻译成 JS 来执行的。上面那句话的纯 JS 写法会是这样
React.readner(React.createElement(Page), document.body);
那么 Page
是什么,这里看起来它应该是一个合法的 JS 变量——是的,这就是马上需要定义的 React 组件。
定义组件会使用 React.createClass()
方法。根据对 React 的初步了解,我想当然的写下了一个 错误 的定义
var Header = AMUIReact.Header; var List = AMUIReact.List; // 错误的定义 var Page = React.createClass({ render: function() { return ( <Header title="通讯录" /> <List /> ); } }); React.render(React.createElement(Page), document.body);
我的原意是希望能定义 <Page />
组件,并将其渲染在 <body>
中,只要能显示页头就行,列表部分暂时留空。
然而通过 Nginx 跑出来之后,从浏览器的控制台得到了一个错误消息
Uncaught Error: Parse Error: Line 13: Adjacent JSX elements must be wrapped in an enclosing tag
大概意思是说,JSX 的元素必须是1个封闭的标签。这里提供了两个要素:“ 1个 ”、“ 封闭 ”。所以我根据这个意思,修改了一下,然后运行出了预期的效果。
render: function(){ return ( <div> <Header title="通讯录" /> <List /> </div> ); }
上面的代码中, var Header = AMUIReact.Header
和 var List = AMUIReact.List
是 Amaze UI React 教程示例中演示的用法——为带命名空间的组件创建简短的名称。其实我更倾向于使用带命名空间的名称,就像 <AMUIReact.Header title="通讯录" />
。这可以避免自定义组件和 Amaze UI React 组件的名称冲突。不过 Amaze UI React 定义的这个命名空间太长,可以自己缩短一下,比如
var A = AMUIReact;
<A.Header title="通讯录" />
后面的示例中就采用这种缩短命名空间的写法。
Header 组件 | Amaze UI React
List 组件 | Amaze UI React
ReactClass createClass(object specification)
根据参数提供的规格说明,创建组件类。规则说明是一个 JavaScript 对象,其提供的 ReactElement render()
函数会返回一个 ReactElement
(对象)用于渲染。
render()
中返回的 ReactElement 可以是 JSX 描述,也可以是纯 JS 脚本。上面的例子是用的 JSX 描述,如果改成 JS 脚本,应该像这样
render: function() { return React.createElement("div", {}, [ React.createElement(Header, { title: "通讯录" }), React.createElement(List) ]); }
很明显,JSX 描述更清晰易读也更容易写出来。
return 后面的的内容是用括号 ()
包起来的,其实如果不包起来也不会出错。但很显然,在写 JavaScript 程序的时候,如果 return 的内容是多行,用括号包起来是个好习惯。
现在继续下一步,添加列表项。在没搞清楚如何使用循环之前,还是先用重复的代码把列表项显示出来再说——当然,在目前没有定义数据结构的情况下,也不会用到循环。考虑到对组件还不够熟悉,列表项的内容,暂时仅展示姓名。
// js/index.jsx var A = AMUIReact; var Page = React.createClass({ render: function() { return (<div> <A.Header title="通讯录" /> <A.List> <A.ListItem> 张三 </A.ListItem> <A.ListItem> 李四 </A.ListItem> <A.ListItem> 王麻子 </A.ListItem> </A.List> </div>); } }); React.render(React.createElement(Page), document.body);
这个结果并不好看,但至少已经实现了列表的显示。待功能完善之后还不能达到满意的效果,可以自定义 CSS 来调整,所以不急。
数据当然不会是固定不变的,直接将列表项写死非常不切合实际。在学习初期,我暂时还不想去和数据库打交道,所以暂时用 JSON 来保存数据,而且为了保持获取数据不节外生枝,先把数据定义在 index.jsx 的最前面。
// js/index.jsx var data = [ { "name": "张三", "tel": "13801234567" }, { "name": "李四", "tel": "18018001800" }, { "name": "王麻子", "tel": "17098765432" } ]; var A = AMUIReact; // ... 后面的代码略
然后改 render()
,想当然的又写了个段错误代码
// 错误的代码 render: function() { return (<div> <A.Header title="通讯录" /> <A.List> for (var i = 0; i < data.length; i++) { <A.ListItem>{data[i].name}</A.ListItem> } </A.List> </div>); }
这回从错误消息中可以发现是 for
循环惹的祸。一开始不明白为啥,仔细思考之后明白了—— return
后面的应该是一个表达式,而 for
循环不是表示式。于是尝试把 for
语句改成 Array.prototype.map()
方法
render: function() { return (<div> <A.Header title="通讯录" /> <A.List> {data.map(function(t) { return ( <A.ListItem>{t.name}</A.ListItem> ); })} </A.List> </div>); }
这回是对了,但是这写法看起来不够简洁,容易把人搞晕。参考网上的资料,发现可以将 map()
的结果保存在一个变量中,再在 <A.List>
中引用
render: function() { var items = data.map(function(t) { return <A.ListItem>{t.name}</A.ListItem>; }); return (<div> <A.Header title="通讯录" /> <A.List> {items} </A.List> </div>); }
小结JSX 中如果在代码中嵌入成对的 <XML标签>
,会被翻译成组件代码( React.createElement(...)
等),而在 xml 标签中使用一对大括号 {}
可以嵌入 JS 代码。如果对 ASP.NET MVC 的 Razor 模块有所了解,就会有似曾相识的感觉。
接下来还要为列表项添加图标,以 React 的组件化思维来思考,比较好的作法是定义一个组件,不妨叫 Person
来封装图标、姓名和电话按钮。那么,如何将每个人的数据传入 Person
对象?
另外,把数据放在 index.jsx 中也不是个办法,迟早还是得用异步(比如 Ajax 或从数据库获取)加载的,又该如何将数据更新到页面中?
欲知后事如何,且看下回分解!
传送门:本节示例代码