[学习笔记] Cordova+AmazeUI+React 做个通讯录 系列文章目录
Cordova+AmazeUI+React 做个通讯录 - 准备
Cordova+AmazeUI+React 做个通讯录 - 联系人列表(1)
Cordova+AmazeUI+React 做个通讯录 - 联系人列表(2)
Cordova+AmazeUI+React 做个通讯录 - 联系人详情
Cordova+AmazeUI+React 做个通讯录 - 单页应用 (With Router)
前面实现了联系人列表和详情两个页面,并通过点击事件和返回按钮处理了两个页面之间有切换。但同时引起一个疑问:为什么不是单页程序?
React 的出现不是为了单页应用,但在很多时候用于单页应用。由于其组件化的设计,React 也的确很容易写单页应用。然而说到单页应用,就不得不提到 router,这个曾经只是在服务端使用的名词被单页应用带到了前端。
router,路由器,路由处理器
route,路由
大家都知道,URL 改变会触发浏览器跳转页面——除了一种情况:只改变 #
后面的部分,因为 #
后面的部分是由浏览器为自己设计的跳转标记,连同 #
号一起被称为 hash。它标识了当前页面内部的一个位置,这个位置可能是由 <a name="....">
标记的,也有可能是标签中的 id
属性标记的。
关于 hash,可以参阅 阮一峰 URL的井号
现代浏览器中,hash 变化会增加访问历史,也会触发相应的事件。但无论如何,hash 变化默认情况都不会向服务器请求数据。因此路由的设计就利用 hash 的特点,通过 hash 的变化来改变当前页面的布局,再利用 AJAX 等技术获取新页面布局所需要的后端数据,完成页面的更新。
由此看来,路由处理器的作用其实是在一定程度上代替了浏览器对 URL 的处理,将由 URL 变化产生的整页更新改变为由 hash 改变而触发局部更新。React 的设计在局部更新这个问题进行了非常优秀的处理,尤其是大大增加了其处理效率。因此 React 非常适合用于单页 Web 应用。
还记得早前提到的 Sample Mobile Application with React and Cordova 么,在它的 Iteration 5 就提到了 路由处理(Routing),而在其示例代码中也出现了一个新的脚本: router.js 。
router 处理的入口通常是 window.onhashchange
事件。在 router.js 中,return 之前就有一句
window.onhashchange = start;
所以主要的处理函数是 function start() {...}
。在 start 函数中,最外层循环是在 routes
中循环,而 routes
数组中的内容是由 addRoute()
添加的。所以基本上可以了解这个简易 router 的处理过程:
配置阶段使用 router.addRoute()
添加路由及其对应的处理函数
在 window.onhashchange
的时候从当前 url 中取得 hash 并与配置好的路由进行比较,找到合适的路由,执行其处理函数
仔细分析 start()
中的循环可以发现路由处理的一些细节,不过直接看 app.js 中配置 router 的部分可以更快明白这个简易 router 的用法。
通讯录现在是由两页完成,index.html 和 detail.html,在使用路由就需要将这两页合并在一起。幸好这两个页面只有一句话不同,只需要将 detail.html 中的 <script type="text/jsx" src="js/detail.jsx"></script>
移到 index.html 中就可以完成合并。
<script type="text/jsx" src="js/index.jsx"></script> <script type="text/jsx" src="js/detail.jsx"></script>
之后可以删除 detail.html。但这样的合并只是第一步。这个时候看到的效果已经不是通讯录列表了,而是“查无此人”。Why?因为 index.jsx 和 detai.jsx 都有 React.render()
语句对 document.body
的内容进行重绘,最后执行的一句覆盖了之前的一句。这也是为什么 Sample Mobile Application with React and Cordova 的 app.js 中,路由处理函数可以起作用的原因。
要把两个独立页面合并到一个页面用,并通过路由来控制显示,那就很有必要把原来的页面组件化——哦,原来的页面本来就是以组件方式定义的,只不过是作为根组件渲染的。不过原来并没有考虑到会在同一个运行上下文中使用两个页面,所以它们的名字都叫 Page。是时候改个名字:一个叫 IndexPage,一个叫 DetailPage 就挺好。
每个页面组件都使用了一些其它的自定义组件,而这些组件不会被另一个页面组件用到,所以可以对这些组件进行一个私有化封装。就像这样
var IndexPage = (function(A) { var Person = React.createClass({ ... }); return React.createClass({ ... }); })(AMUIReact);
var DetailPage = (function(A) { var detailBase = { ... }; var DetailItem = React.createClass({ ... }); var DetailLinkItem = React.createClass({ ... }); var Detail = React.createClass({ ... }); return React.createClass({ ... }); })(AMUIReact);
组件化 IndexPage 和 DetailPage 的时候删除了两个 jsx 中的 React.render(...)
,所以还需要一个渲染的入口,不妨加一个 app.jsx:
router.addRoute("", function() { React.render(<IndexPage />, document.body); }); router.addRoute(":id", function() { React.render(<DetailPage />, document.body); }); router.start();
相应的, index.jsx 中跳转到详情的链接也要从 "detail.html#" + this.props.id
改为 "#" + this.props.id
。
由于添加了 router.js
和 app.jsx
,index.html 中引用脚本的部分也需要做一些调整
<script src="js/router.js"></script> <script type="text/jsx" src="js/index.jsx"></script> <script type="text/jsx" src="js/detail.jsx"></script> <script type="text/jsx" src="js/app.jsx"></script>
router.js 的位置只需要在 app.jsx 之前就行。这里把它当作一个库来引用,所以放在最前面。
在抄 router 的时候,我就猜想,如果 router 是一个常用的功能,那就一定已经存在现成的库,即使不是 React 官方的,也会有第 3 方的出现。结果使用“react router”作为关键字一搜,就搜到了 React Router 。然后参考了 再谈 React Router 使用方法 和 React Router 簡介 两篇文章之后,开始着手修改。
在 React Router 的官网及各种文章中都看到这样的示例
var Router = require("react-router");
这很明显是 node.js 的语法。难道 React Router 不是用于前端的?似乎不太可能啊!
终于在 React Router 的 README.md 中发现它提到了 CDN
If you just want to drop a <script> tag in your page and be done with it, you can use the UMD/global build hosted on cdnjs .
既然有 CDN,那应该是可以在前端使用的,但是从源码包没有发现直接可用的 js 文件,只好按照 README.md 的步骤先 npm install react-router
从 NPM 下载一个下来。果然找到了 UMD build 文件:ReactRouter.js 和 ReactRouter.min.js,把这两个文件和 CDN 上的一比较,一模一样。这下放心了。
UMD(Universal Module Definition)是 AMD 和 CommonJS 的糅合。UMD 先判断是否支持 Node.js 模块(即 exports 是否存在),存在则使用 Node.js 模式。再判断是否支持 AMD(define 是否存在),存在则使用 AMD 方式加载。
如果不使用 CommonJS,也不使用 AMD,React Router 会挂在 global 对象上,即 window.ReactRouter。
React Router 是作为 React 组件设计的,所以它的路由配置是以组件的方式进行的。主要使用 ReactRouter.Route 组件进行配置。
React Router 同样也是以组件的方式进行渲染的,配置用于显示的组件,最终会显示在 ReactRouter.RouteHandler 组件中。
需要定义一个 Main 组件来渲染 RouterHandler。而这个 Main 组件将作为 router 配置的根路由处理器(Handler)。
ReactRouter.run 用于启动 router 处理。路由的参数(:id 这种)一般通过 props.params 对象传递。
因为不想多加一个脚本文件,所以准备把定义 Main 组件和处理路由配置都放在 app.jsx 中进行。
首先是定义 Main。因为 IndexPage 和 DetailPage 都是直接在 body 上渲染的,所以这个 Main 也不需要干多余的事情,直接渲染 RouteHandler 就好
var Main = (function(R) { React.createClass({ render: function() { return <R.RouteHandler params={this.props.params} /> } }); })(ReactRouter);
还是按处理 AMUIReact 的办法来处理 ReactRouter,把它简写成 R
。
然后是配置路由
var routes = ( <R.Route path="/" handler={Main}> <R.DefaultRoute handler={IndexPage} /> <R.Route path=":id" handler={DetailPage} /> </R.Route> );
这里使用 Main 作为根路由处理器,默认路由也就是 #/
的时候。渲染 IndexPage,所以把 IndexPage 作为默认路由(DefaultRoute)处理器。下一层路由是详情页面,只需要给个路径参数 :id
,用 DetailPage 作处理器即可。
最后启动路由处理器
R.run(routes, function(Handler, state) { React.render(<Handler params={state.params} />, document.body); });
处理器的回调函数中,第 1 个参数 Handler,就是在配置路由的时候给的根 handler
属性,即对 Main 封装而成的处理函数。而 state 表示了当前路由的状态,包括路径,参数等。其中 state.params
就是路由参数。通过 props.params 传递给 Main,再由 Main 通过 props.params 传递给 RouteHandler……
至于 React Router 是怎么处理各个路由的,这里不深入研究。有兴趣的同学可以去研究 React Router 的源码。
经过上面对 app.jsx 的修改,跑起来已经没有问题了。问题在于详情页面显示的总是“查无此人”。
之前的详情页面在加载数据的时候会根据 hash 来筛选数据,当时的 hash 像这样: #1001
。而现在 React Router 会将 hash 规范化处理成 #/1001
。因此只需要将原来的
"#" + p.id === window.location.hash;
改成
"#/" + p.id === window.location.hash;
就好。
之前自定义的 router 就定义了路由参数,并且可以通过处理参数的形参获取,再通过 props 传递给组件。但是因为偷懒,直接在组件内部通过处理 hash 来获取了。简单的路径这么处理没有问题,但是复杂的路径处理起来就比较复杂了,所以还是应该用现成的。所以现在改用路由参数来筛选数据。
前面提到 React Router 一般是用 props.params 来传递参数,所以在 DetailPage 中可以通过 this.props.params.id
来获取 ID 参数。
componentDidMount: function() { var id = this.props.params.id; // <-- $.getJSON("/js/data.json").then(function(data) { if (this.isMounted()) { this.setState({ person: data.filter(function(p) { return p.id === id; // <-- })[0] }); } }.bind(this)); }
传送门: 本节示例代码(自定义 router 部分)
传送门: 本节示例代码(React Router 部分)