如今是前端开发最好的时代,有太多很好的框架和工具帮你更好的实现复杂需求;同时又是最困难的时代,由于须要掌握太多的框架和工具。如何利用好各类框架来提升前端开发质量是你们都在探索的问题。本文就将介绍如何使用 React 及其相关技术,来进行实际前端项目的开发。由于主要介绍如何将技术用于实践,因此但愿读者已经对相关概念已经有必定的了解。javascript
本文最初来源于笔者在 StuQ 的一次同名课程直播,如今加以整理成文,但愿能对更多的人有所启发。为了固化这种实践方式,当时还开发了一个名为 Rekit 的工具,用于确保项目可以始终遵循这种实践方式。如今工具也得到进一步完善,你们也能够结合 Rekit 来理解文中提到的实践方案。css
其实不管使用什么样的技术,一个理想中的 Web 项目大概都须要考虑如下几个方面:html
易于开发:在功能开发时,无需关注复杂的技术架构,可以直观的写功能相关的代码。前端
易于扩展:增长新功能时,无需对已有架构进行调整,新功能和已有功能具备很好的隔离性,并能很好的衔接。新功能的增长并不会带来显著的性能问题。java
易于维护:代码直观易读易理解。即便是新加入的开发成员,也可以很快的理解技术架构和代码逻辑。react
易于测试:代码单元性好,可以尽可能使用纯函数。无需或不多须要 mock 便可完成单元测试。webpack
易于构建:代码和静态资源结构符合主流模式,可以使用标准的构建工具进行构建。无需本身实现复杂的构建逻辑。git
这些方面并非互相独立,而是互相依赖互相制约。当某个方面作到极致,其它点就会受到影响。举例来讲,写一个计数器功能,用jQuery一个页面内便可完成,可是易开发了,却不易扩展。所以咱们一般都须要根据实际项目状况在这些点之间作一个权衡,达到适合项目的最佳状态。庆幸的是,如今的前端技术快速发展,不断出现的新技术帮助咱们在各个方面都得到很大提高。github
本文将要介绍的就是如何利用 React + Redux + React-router 来构建可扩展的前端应用。这里强调可扩展,由于传统前端实现方案一般在面对复杂应用时经常力不从心,代码结构容易混乱,性能问题难以解决。而可扩展则意味着可以从项目的初始阶段就具备了支持复杂项目的能力。首先咱们看下涉及到的主要技术。web
React 相信你们已经很是熟悉,其组件化的思想和虚拟 DOM 的实现都是颠覆性的变革,从而让前端开发能够在新的方向上不断提高。不管是 React-hot-loader,Redux 仍是 React-router,都正是由于充分利用了 React 的这些特性,才可以提供如此强大的功能。笔者曾经写过《深刻浅出React》 的系列文章,有须要的话能够进一步阅读。
Redux 是 JavaScript 程序状态管理框架。尽管是一个通用型的框架,可是和 React 在一块儿可以更好的工做,由于当状态变化时,React 能够不用关心变化的细节,由虚拟 DOM 机制完成优化过的UI更新逻辑。
Redux 也被认为整个 React 生态圈最难掌握的技术之一。其 action,reducer 和各类中间件虽然将代码逻辑充分隔离,即常说的 separation of concerns,但在必定程度上也给开发带来了不便。这也是上面提到的,在易维护、易扩展、易测试上获得了提高,那么易开发则受到了影响。
即便对于一个简单的应用,路由功能也是极其重要的。正如传统 Web 程序用页面来组织不一样的功能模块,由不一样的 URL 来区分和导航,单页应用使用 Router 来实现一样的功能,只是在前端进行渲染而不是服务器端。React 应用的“标准”路由方案就是使用 React-router。
路由功能不只让用户更容易使用(例如刷新页面后维持 UI),也可以在开发时让咱们思考如何更好组织功能单元,这也是功能复杂以后的必然需求。因此即便一开始的需求很简单,咱们也应该引入 React-router 帮助咱们以页面为单元进行功能的组织。
正如前面提到的,开发前端应用须要不少周边技术,这进一步增长了前端开发的门槛,例如:
使用 Babel 支持 ES2016 和 JSX 语法;
使用 react-redux 将 Redux 和 React 无缝结合;
使用 Webpack 进行项目打包;
使用 webpack-dll-plugin 优化打包性能;
使用 ESLint 进行语法检查;
这些工具提升了前端开发的能力和效率,可是了解并配置它们却并不是易事,而事实上这些工具和须要开发的功能并无直接的关系。使用工具来自动化这些配置是必然的发展方向,正如如今开发一个 C++ 应用,Visual Studio 会帮你完成全部的配置并搭建合适的项目结构,让你专一于功能逻辑的开发。不管是本身实现,仍是利用第三方,咱们都应该为本身的项目建立这样的工具链。
简单介绍了相关技术,下面咱们来看如何去构建可扩展的 Web 项目。
不管是 Flux 仍是 Redux,提供的官方示例都是以技术逻辑来组织文件夹的,例如,下面是 Redux 的 Todo 示例应用的文件夹结构:
虽然这种模式在技术上很清晰,在实际项目中却有很大的缺点:
难以扩展。当应用功能增长,规模变大时,一个 components 文件夹下可能会有几十上百个文件,组件间的关系极不直观。
难以开发。在开发某个功能时,一般须要同时开发组件,action,reducer 和样式。把它们分布在不一样文件夹下严重影响开发效率。尤为是项目复杂以后,不一样文件的切换会消耗大量时间。
所以,咱们使用按功能来组织文件夹的方式,即功能相关的代码放到一个文件夹。例如,对于一个简单论坛程序,可能包含 user,topic,comment 这么几个核心功能。
每一个功能文件夹下包含本身的页面,组件,样式,action 和 reducer。
这种文件夹结构在功能上而非技术上对代码逻辑进行区分,使得应用具备更好的扩展性,当增长新的功能时,只需增长一个新的文件夹便可;删除功能时同理。
前面提到了路由是当今前端应用的不可缺乏的部分之一,那么对应到组件级别,就是页面组件。所以咱们在开发的过程当中,须要明肯定义页面的概念:
一个页面拥有本身的 URL 地址。页面的展示和隐藏彻底由 React-router 进行控制。当建立一个页面时,一般意味着在路由配置里增长一条新的规则。这和传统 Web 应用很是相似。
一个页面对应 Redux 的容器组件的概念。页面首先是一个标准的 React 组件,其次它经过 react-redux 封装成容器组件从而具有和 Redux 交互的能力。
页面是导航的基本模块单元,同时也是同一功能相关 UI 的容器,这种符合传统 Web 开发方式的概念有助于让项目结构更容易理解。
使用 Redux 来管理状态,就须要进行 action 和 reducer 的开发。在官方示例以及几乎全部的教程中,全部的 action 都放在一个文件,而全部的 reducer 则放在另外的文件。这种作法易于理解可是不具有很好的可扩展性,并且当项目复杂后,action 文件和 reducer 文件都会变得很冗长,不易开发和维护。
所以咱们使用每一个 action 一个独立文件的模式:每一个 Redux 的 action 和对应的 reducer 放在同一个文件。使用这个作法的另外一个缘由是咱们发现每次建立完 action 几乎都须要马上建立 reducer 对其进行处理。把它们放在同一个文件有利于开发效率和维护。
以开发一个计数器组件为例:
为实现点击“+”号增长1的功能,咱们首先须要建立一个类型为 "COUNTER_PLUS_ONE" 的 action ,以后就马上须要建立对应的 Reducer 来更新 store 的数据。官方示例的作法是分别在 actions.js 和 reducer.js 中分别加入相应的逻辑。而使用每一个 action 独立文件的作法,则是建立一个名为 counterPlusOne.js 的文件,加入以下代码:
import { COUNTER_PLUS_ONE, } from './constants'; export function counterPlusOne() { return { type: COUNTER_PLUS_ONE, }; } export function reducer(state, action) { switch (action.type) { case COUNTER_PLUS_ONE: return { ...state, count: state.count + 1, }; default: return state; } }
按咱们的经验,大部分的 reducer 都会对应到相应的 action,不多须要跨功能全局使用。所以,将它们放入一个文件是彻底合理的,有助于提升开发效率。须要注意的是,这里定义的 reducer 并非标准的 Redux reducer,由于它没有初始状态(initial state)。它仅仅是被功能文件夹下的根 reducer 调用。注意这个 reducer 固定命名为 "reducer",从而方便其被自动加载。
对于异步 action(一般是远程 API 请求),则须要对错误信息进行处理,所以在这个文件中有多个标准 action 存在。例如以保存文章为例,在 saveArticle.js 这个 action 文件中,同时存在 saveArticle 和 dismissSaveArticleError 这两个 action。
尽管不是很常见,可是有些 action 是可能被多个 reducer 处理的。例如,对于站内聊天功能,当收到一条新消息时:
若是聊天框开着,那么直接显示新消息。
不然,显示一条通知提示有新的消息。
可见,NEW_MESSAGE 这个 action 类型须要被不一样的 reducer 处理,从而可以在不一样的 UI 组件作不一样的展示。为了处理这类 action,每一个功能文件夹下都有一个 reducer.js 文件,在里面能够处理跨功能的 action。
虽然不一样 action 的 reducer 分布在不一样的文件中,但它们和功能相关的 root reducer 共同操做同一个状态,即同一个 store 分支。所以 feature/reducer.js 具备以下的代码结构:
import initialState from './initialState'; import { reducer as counterPlusOne } from './counterPlusOne'; import { reducer as counterMinusOne } from './counterMinusOne'; import { reducer as resetCounter } from './resetCounter'; const reducers = [ counterPlusOne, counterMinusOne, resetCounter, ]; export default function reducer(state = initialState, action) { let newState; switch (action.type) { // Put global reducers here default: newState = state; break; } return reducers.reduce((s, r) => r(s, action), newState); }
它负责引入不一样 action 的 reducer,当有 action 过来时,遍历全部的 reducer 并结合须要的全局 reducer 来实现对 store 的更新。全部功能相关的 root reducer 最终被组合到全局的 Redux root reducer 从而保证全局只有一个 store 的存在。
须要注意的是,每当建立一个新的 action 时,都须要在这个文件中注册。由于其模式很是固定,咱们彻底可使用工具来自动注册相应的代码。Rekit 能够帮助作到这一点:当建立 action 时,它会自动在 reducer.js 中加入相应的代码,既减小了工做量,又能够避免出错。
使用这种方式,能够带来不少好处,好比:
易于开发:当建立 action 时,无需在多个文件中跳转;
易于维护:由于每一个 action 在单独的文件,所以每一个文件都很短小,经过文件名就能够定位到相应的功能逻辑;
易于测试:每一个 action 均可以使用一个独立的测试文件进行覆盖,测试文件中也是同时包含对 action 和 reducer 的测试;
易于工具化:由于使用 Redux 的应用具备较为复杂的技术结构,咱们可使用工具来自动化一些逻辑。如今咱们无需进行语法分析就能够自动生成代码。
易于静态分析:全局的 action 和 reducer 一般意味着模块间的依赖。这时咱们只要分析功能文件夹下的 reducer.js,便可以找到全部这些依赖。
一般来讲,咱们会经过一个配置文件定义全部的路由规则。一样的,这种方式不具备扩展性,当项目变复杂以后,规则定义表会变得冗长而复杂。既然咱们已经以功能为单位进行文件夹的组织,咱们一样能够把功能相关的路由规则也放到对应文件夹下。所以,咱们能够利用 React-router 的 JavaScript API 进行路由规则的定义,而不是用常见的 JSX 语法。
例如,对于一个简单论坛程序,主题功能对应的路由定义就放在 features/topic/route.js 中,内容以下:
import { EditPage, ListPage, ViewPage, } from './index'; export default { path: '', name: '', childRoutes: [ { path: '', component: ListPage, name: 'Topic List', isIndex: true }, { path: 'topic/add', component: EditPage, name: 'New Topic' }, { path: 'topic/:topicId', component: ViewPage }, ], };
全部功能相关的路由定义都被全局的根路由配置自动加载,所以,路由加载器具备以下的代码模式:
import topicRoute from '../features/topic/route'; import commentRoute from '../features/comment/route'; const routes = [{ path: '/rekit-example', component: App, childRoutes: [ topicRoute, commentRoute, { path: '*', name: 'Page not found', component: PageNotFound }, ], }];
可见,这个全局路由加载器负责加载全部 feature 的路由规则。相似 root reducer,这里的代码模式也是很是固定的,所以能够借助工具来维护这个文件。当使用 Rekit 建立页面时,就会自动在此加入路由规则。
由上面的介绍能够看到,开发一个 React 程序并不容易,即便一个简单的功能,也须要大量的琐碎的,但却很是重要的代码来确保一个良好的架构,从而让应用易于扩展和维护,虽然这些周边代码和你须要的功能并无直接关系。
例如,对于一个论坛程序,须要一个列表界面展现最近发表的主题,为了作这样一个页面,咱们一般都须要完成如下步骤:
建立一个名为 TopicList 的 React 组件;
为 TopicList 定义一条路由规则;
建立一个名为 TopicList.css 的样式文件,并在合适的位置引入;
使用 react-redux 将 TopicList 组件封装成容器组件,从而使其可使用 Redux store;
建立4种不一样的 action 类型:FETCH_BEGIN, FETCH_PENDING, FETCH_SUCCESS, FETCH_FAILURE,一般定义在 constants.js;
建立两个 action:fetchTopicList 和 dismissFetchTopicListError;
在 action 文件中引入类型常量;
在 reducer 中建立4个 swtich case 来处理不一样的 action 类型;
在 reducer 文件中引入类型常量;
建立组件的测试文件及其代码结构;
建立 action 的测试文件及其代码结构;
建立 reducer 的测试文件及其代码结构。
天!在正式开始写论坛逻辑的第一行代码以前,居然须要作这么多琐碎的事情。当这样的事情手动重复了屡次以后,咱们以为应该有工具来自动化这样的事情。为此建立了 Rekit 工具包,能够帮助自动生成这些文件结构和代码。不一样于其它的代码生成器,Rekit 基于一个相对固定的文件和代码结构,所以能够作更多的事情,例如:
它知道在哪里以及如何定义路由规则;
它知道如何生成 action 类型常量;
它知道如何根据 action 名字来生成类型常量;
它知道如何根据 action 类型来建立 reducer;
它知道如何建立有意义的测试案例。
借助于精心维护的工具,咱们能够没必要关注技术细节,而只需专一于功能相关的代码,提升了开发效率。不只如此,工具也能够减小错误,并在代码结构,命名,配置等方面维持高度一致性,让代码更加容易理解和维护。
Rekit 针对本文提出的 React + Redux 开发实践提供了一套工具集,其自己也是可扩展的。你彻底能够根据须要更改代码模板,或者提供本身的工具,针对本身的项目特性提供便捷的工具来提升开发效率。
本文主要介绍了如何使用 React,Redux 以及 React-router 来开发可扩展的 Web 应用。其核心思路有两个,一是以功能(feature)为单位组件文件夹结构;二是采用每一个 action 单独文件的模式。这样可以让代码更加模块化,增长和删除功能都不会对其它模块产生太大影响。同时使用 React-router 来帮助实现页面的概念,让单页应用(SPA)也拥有传统 Web 应用的 URL 导航功能,进一步下降了功能模块间的耦合行,让应用结构更加清晰直观。
为了支持这样的实践,文中还介绍了 Rekit 工具集,不只能够帮助建立和配置初始的项目模板,并且还提供了大量实用的工具帮助以文中提到的方式自动生成技术结构,提升了开发效率。更多的工具介绍能够访问其官网:http://rekit.js.org。