React Router 入门实战教学

React Router 资料夹结构

前言

若你是从一开始一路走到这里读者请先给自己一个爱的鼓励吧!在经历了 React 基础的训练后,相信各位读者应该都等不及想大展拳脚了!接下来我们将进行比较复杂的应用程式开发并和读者介绍目前市场上常见的不刷页单页式应用程式(single page application)的设计方式。

单页式应用程式(single page application)

传统的 Web 开发主要是由伺服器管理 URL Routing 和渲染 HTML 页面,过往每次 URL 一换或使用者连结一点,就需要重新从伺服器端重新载入页面。但随着使用者对于使用者体验的要求提升,许多的网页应用程式纷纷设计成不刷页的单页式应用程式(single page application),由前端负责 URL 的 routing 管理,若需要和后端进行 API 资料沟通的话,通常也会使用 Ajax 的技术。在 React 开发世界中主流是使用 react-router 这个 routing 管理用的 library。

React Router 环境设置

先透过以下指令在根目录产生 npm 设定档 package.json

  1. $ npm init

安装相关套件(包含开发环境使用的套件):

  1. $ npm install --save react react-dom react-router
  1. $ npm install --save-dev babel-core babel-eslint babel-loader babel-preset-es2015 babel-preset-react eslint eslint-config-airbnb eslint-loader eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react webpack webpack-dev-server html-webpack-plugin

安装好后我们可以设计一下我们的资料夹结构,首先我们在根目录建立 srcres 资料夹,分别放置 scriptsource 和静态资源(如:全域使用的 .css 和图档)。在 components 资料夹中我们会放置所有 components(个别组件资料夹中会用 index.js 输出组件,让引入组件更简洁),其余设定档则放置于根目录下。

React Router 资料夹结构

接下来我们先设定一下开发文档。

  1. 设定 Babel 的设定档: .babelrc

    1. {
    2. "presets": [
    3. "es2015",
    4. "react",
    5. ],
    6. "plugins": []
    7. }
  2. 设定 ESLint 的设定档和规则: .eslintrc

    1. {
    2. "extends": "airbnb",
    3. "rules": {
    4. "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
    5. },
    6. "env" :{
    7. "browser": true,
    8. }
    9. }
  3. 设定 Webpack 设定档: webpack.config.js

    1. // 让你可以动态插入 bundle 好的 .js 档到 .index.html
    2. const HtmlWebpackPlugin = require('html-webpack-plugin');
    3. const HTMLWebpackPluginConfig = new HtmlWebpackPlugin({
    4. template: `${__dirname}/src/index.html`,
    5. filename: 'index.html',
    6. inject: 'body',
    7. });
    8. // entry 为进入点,output 为进行完 eslint、babel loader 转译后的档案位置
    9. module.exports = {
    10. entry: [
    11. './src/index.js',
    12. ],
    13. output: {
    14. path: `${__dirname}/dist`,
    15. filename: 'index_bundle.js',
    16. },
    17. module: {
    18. preLoaders: [
    19. {
    20. test: /\.jsx$|\.js$/,
    21. loader: 'eslint-loader',
    22. include: `${__dirname}/src`,
    23. exclude: /bundle\.js$/
    24. }
    25. ],
    26. loaders: [{
    27. test: /\.js$/,
    28. exclude: /node_modules/,
    29. loader: 'babel-loader',
    30. query: {
    31. presets: ['es2015', 'react'],
    32. },
    33. }],
    34. },
    35. // 启动开发测试用 server 设定(不能用在 production)
    36. devServer: {
    37. inline: true,
    38. port: 8008,
    39. },
    40. plugins: [HTMLWebpackPluginConfig],
    41. };

太好了!这样我们就完成了开发环境的设定可以开始动手实作 React Router 应用程式了!

开始 React Routing 之旅

HTML Markup:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>ReactRouter</title>
  6. <link rel="stylesheet" type="text/css" href="../res/styles/main.css">
  7. </head>
  8. <body>
  9. <div id="app"></div>
  10. </body>
  11. </html>

以下是 webpack.config.js 的进入点 src/index.js,负责管理 Routerrender 组件。这边我们要先详细讨论的是,为了使用 React Router 功能引入了许多 react-router 内部的组件。

  1. Router
    Router 是放置 Route 的容器,其本身不定义 routing ,真正 routing 规则由 Route 定义。

  2. Route
    Route 负责 URL 和对应的组件关系,可以有多个 Route 规则也可以有嵌套(nested)Routing。像下面的例子就是每个页面都会先载入 App 组件再载入对应 URL 的组件。

  3. history
    Router 中有一个属性 history 的规则,这边使用我们使用 hashHistory,使用 routing 将由 hash(#)变化决定。例如:当使用者拜访 http://www.github.com/,实际看到的会是 http://www.github.com/#/。下列范例若是拜访了 /about 则会看到 http://localhost:8008/#/about 并载入 App 组件再载入 About 组件。

    • hashHistory
      教学范例使用的,会通过 hash 进行对应。好处是简单易用,不用多余设定。

    • browserHistory
      适用于伺服器端渲染,但需要设定伺服器端避免处理错误,这部份我们会在后面的章节详细说明。注意的是若是使用 Webpack 开发用伺服器需加上 --history-api-fallback

      1. $ webpack-dev-server --inline --content-base . --history-api-fallback
    • createMemoryHistory
      主要用于伺服器渲染,使用上会建立一个存在记忆体的 history 物件,不会修改浏览器的网址位置。

      1. const history = createMemoryHistory(location)
  4. path
    path 是对应 URL 的规则。例如:/repos/torvalds 会对应到 /repos/:name 的位置,并将参数传入 Repos 组件中。由 this.props.params.name 取得参数。顺带一提,若为查询参数 /user?q=torvalds 则由 this.props.location.query.q 取得参数。

  5. IndexRoute
    由于 / 情况下 App 组件对应的 this.props.children 会是 undefinded,所以使用 IndexRoute 来解决对应问题。这样当 URL 为 / 时将会对应到 Home 组件。不过要注意的是 IndexRoute 没有 path 属性。

  1. import React from 'react';
  2. import ReactDOM from 'react-dom';
  3. import { Router, Route, hashHistory, IndexRoute } from 'react-router';
  4. import App from './components/App';
  5. import Home from './components/Home';
  6. import Repos from './components/Repos';
  7. import About from './components/About';
  8. import User from './components/User';
  9. import Contacts from './components/Contacts';
  10. ReactDOM.render(
  11. <Router history={hashHistory}>
  12. <Route path="/" component={App}>
  13. <IndexRoute component={Home} />
  14. <Route path="/repos/:name" component={Repos} />
  15. <Route path="/about" component={About} />
  16. <Route path="/user" component={User} />
  17. <Route path="/contacts" component={Contacts} />
  18. </Route>
  19. </Router>,
  20. document.getElementById('app'));
  21. /* 另外一种写法:
  22. const routes = (
  23. <Route path="/" component={App}>
  24. <IndexRoute component={Home} />
  25. <Route path="/repos/:name" component={Repos} />
  26. <Route path="/about" component={About} />
  27. <Route path="/user" component={User} />
  28. <Route path="/contacts" component={Contacts} />
  29. </Route>
  30. );
  31. ReactDOM.render(
  32. <Router routes={routes} history={hashHistory} />,
  33. document.getElementById('app'));
  34. */

由于我们在 index.js 使用嵌套 routing,把 App 组件当做每个组件都会载入的母模版,亦即进入每个对应页面载入对应组件前都会先载入 App 组件。这样就可以让每个页面都有导览列连结可以点选,同时可以透过 props.children 载入对应 URL 的子组件。

  1. Link
    Link 组件主要用于点击后连结转换,可以想成是 <a> 超连结的 React 版本。若是希望当点击时候有对应的 css style,可以使用 activeStyleactiveClassName 去做设定。范例分别使用于 index.html使用传统 CSS 载入、Inline Style、外部引入 Inline Style 写法。

  2. IndexLink
    IndexLink 主要是了处理 index 用途,特别注意当 child route actived 时,parent route 也会 actived。所以我们回首页的连结使用 <IndexLink /> 内部的 onlyActiveOnIndex 属性来解决这个问题。

  3. Redirect、IndexRedirect
    这边虽然没有用到,但若读者有需要使用到连结跳转的话可以参考这两个组件,用法类似于 RouteIndexRedirect

以下是 src/components/App/App.js 完整程式码:

  1. import React from 'react';
  2. import { Link, IndexLink } from 'react-router';
  3. import styles from './appStyles';
  4. import NavLink from '../NavLink';
  5. const App = (props) => (
  6. <div>
  7. <h1>React Router Tutorial</h1>
  8. <ul>
  9. <li><IndexLink to="/" activeClassName="active">Home</IndexLink></li>
  10. <li><Link to="/about" activeStyle={{ color: 'green' }}>About</Link></li>
  11. <li><Link to="/repos/react-router" activeStyle={styles.active}>Repos</Link></li>
  12. <li><Link to="/user" activeClassName="active">User</Link></li>
  13. <li><NavLink to="/contacts">Contacts</NavLink></li>
  14. </ul>
  15. <!-- 我们将 App 组件当做每个组件都会载入的母模版,因此可以透过 children 载入对应 URL 的子组件 -->
  16. {props.children}
  17. </div>
  18. );
  19. App.propTypes = {
  20. children: React.PropTypes.object,
  21. };
  22. export default App;

对应的组件内部使用 Functional Component 进行 UI 渲染:

以下是 src/components/Repos/Repos.js 完整程式码:

  1. import React from 'react';
  2. const Repos = (props) => (
  3. <div>
  4. <h3>Repos</h3>
  5. <h5>{props.params.name}</h5>
  6. </div>
  7. );
  8. Repos.propTypes = {
  9. params: React.PropTypes.object,
  10. };
  11. export default Repos;

详细的程式码读者可以参考范例资料夹,若读者跟着范例完成的话,可以在终端机上执行 npm start,并于浏览器 http://localhost:8008看到以下成果,当你点选连结时会切换对应组件并改变 actived 状态!

范例成果

总结

到这边我们又一起完成了一个重要的一关,学习 routing 对于使用 React 开发复杂应用程式是非常重要的一步,接下来我们将一起学习一个相对独立的单元 ImmutableJS,但学习 ImmutableJS 可以让我们在使用 ReactFlux/Redux 可以有更好的效能和避免一些副作用。

延伸阅读

  1. Leveling Up With React: React Router
  2. Programmatically navigate using react router
  3. React Router 使用教程
  4. React Router 中文文档
  5. React Router Tutorial

(iamge via seanamarasinghe

| 勘误、提问或许愿 |