当前位置: 首页 > 科技观察

使用React+Redux+React-router构建可扩展的前端应用

时间:2023-03-13 02:01:44 科技观察

现在是前端开发的时代,有太多好的框架和工具可以帮助你更好的实现复杂的需求;同时也是最艰难的时代,因为需要掌握的框架和工具太多了。如何利用好各种框架来提高前端开发的质量,是每个人都在探索的问题。本文将介绍如何使用React及相关技术开发实际的前端项目。因为主要是介绍如何将技术付诸实践,希望读者已经对相关概念有一定的了解。本文最初起源于作者在StuQ的同名课程直播,现整理成文,希望对更多人有所启发。为了固化这种做法,当时还开发了一个叫Rekit的工具,保证项目能一直遵循这种做法。现在工具已经进一步完善,大家也可以结合Rekit来了解文中提到的实用解决方案。事实上,无论使用什么样的技术,一个理想的web项目都需要考虑以下几个方面:开发的方便性:在功能开发的时候,不需要关注复杂的技术架构,可以编写功能——相关代码直观。易于扩展:添加新功能时,无需调整现有架构。新功能和现有功能隔离得很好,又能很好地衔接起来。添加新功能不会导致严重的性能问题。易于维护:代码直观易读易懂。即使是新开发成员也能快速理解技术架构和代码逻辑。易于测试:代码单元性好,尽量使用纯函数。单元测试可以在很少或没有模拟的情况下完成。易于构建:代码和静态资源结构符合主流模型,可以使用标准构建工具进行构建。无需自己实现复杂的构建逻辑。这些方面不是相互独立的,而是相互依存、相互制约的。某一方面完善了,就会影响到其他方面。比如写一个计数器函数,用jQuery可以在一页纸内完成,但是开发容易,扩展起来却不容易。因此,我们通常需要根据项目的实际情况在这几点之间进行取舍,以达到适合项目的最佳状态。好在现在的前端技术发展很快,不断涌现的新技术帮助我们在各个方面都有了很大的提升。本文将介绍如何使用React+Redux+React-router构建可扩展的前端应用。这里强调可扩展性,因为传统的前端实现方案往往无法应对复杂的应用,代码结构容易混乱,性能问题难以解决。另一方面,可扩展性意味着能够从项目一开始就支持复杂的项目。首先,让我们看看涉及的主要技术。ReactReact相信大家已经非常熟悉了。它的组件化思维和虚拟DOM的实现都是颠覆性的变化,让前端开发可以在新的方向上不断提升。无论是React-hot-loader、Redux还是React-router,正是因为充分利用了React的这些特性,才能够提供如此强大的功能。作者曾经写过一连串关于《深入浅出React》的文章,有需要的可以继续阅读。ReduxRedux是一个JavaScript程序状态管理框架。虽然它是一个通用的框架,但它可以和React一起更好地工作,因为当状态发生变化时,React可以通过虚拟DOM机制完成优化的UI更新逻辑,而无需关心变化的细节。Redux也被认为是整个React生态中最难掌握的技术之一。虽然它的action、reducer和各种中间件将代码逻辑完全隔离,也就是常说的关注点分离,但也在一定程度上给开发带来了不便。这也是上面提到的。在易维护性、易扩展性、易测试性等方面进行了改进,因此开发的难易度受到了影响。React-router即使是一个简单的应用,路由功能也是极其重要的。就像传统的Web程序使用页面组织不同的功能模块,通过不同的URL来区分和导航一样,单页应用使用Router来实现同样的功能,只是只在前端渲染,而不是服务器端。React应用程序的“标准”路由解决方案是使用React-router。路由功能不仅可以方便用户使用(比如刷新页面后维护UI),还可以让我们在开发时思考如何更好的组织功能单元,这也是复杂功能之后的必然需求。所以即使最初的需求很简单,我们也应该引入React-router来帮助我们以页面为单位来组织功能。其他需要的技术前面提到,开发前端应用需要很多外围技术,进一步提高了前端开发的门槛,比如:使用Babel支持ES2016和JSX语法;使用react-redux无缝结合Redux和React;使用webpack进行项目打包;使用webpack-dll-plugin优化打包性能;使用ESLint进行语法检查;使用Mocha、Enzyme、Istanbul进行单元测试;使用Less、Scss或其他工具进行CSS预编译。这些工具提高了前端开发的能力和效率,但理解和配置它们并不容易。事实上,这些工具与需要开发的功能没有直接关系。使用工具来自动化这些配置是必然的发展方向。就像现在开发C++应用程序一样,VisualStudio会帮你完成所有的配置,构建合适的项目结构,让你专注于功能逻辑的开发。无论我们自己实现还是使用第三方,我们都应该为自己的项目创建这样一个工具链。在简单介绍了相关技术之后,让我们看看如何构建一个可伸缩的Web项目。按功能组织文件夹结构无论是Flux还是Redux,官方提供的示例都是按照技术逻辑组织文件夹的。例如下面是Redux的Todo示例应用的文件夹结构:这种模式虽然在技术上很清晰,但是在实际项目中有一个很大的缺点:难以扩展。当应用功能增多,规模变大时,一个components文件夹下可能有几十个甚至上百个文件,组件之间的关系极不直观。开发困难。在开发某个功能时,通常需要同时开发组件、动作、缩减器和样式。分布在不同的文件夹中,严重影响开发效率。尤其是项目复杂之后,在不同的文件之间切换会非常耗费时间。因此,我们采用按功能组织文件夹的方法,即将与功能相关的代码放在一个文件夹中。例如,对于一个简单的论坛程序,它可能包括用户、主题、评论等几个核心功能。每个功能文件夹都包含自己的页面、组件、样式、操作和缩减器。这种文件夹结构在功能上而非技术上区分了代码逻辑,使应用程序更具可扩展性。添加新功能时,只需要添加一个新的文件夹即可;删除函数时也是如此。使用前面提到的页面(Page)的概念,路由是当今前端应用不可或缺的部分之一,那么对应到组件层面,就是页面组件。因此,在开发的过程中,我们需要明确定义页面的概念:页面有自己的URL地址。页面的显示和隐藏完全由React-router控制。创建页面时,通常意味着在路由配置中添加新规则。这与传统的Web应用程序非常相似。页面对应于Redux的容器组件的概念。页面首先是一个标准的React组件,其次通过react-redux封装成一个容器组件,具备与Redux交互的能力。页面是导航的基本模块化单元,同时也是相同功能的相关UI的容器。这种符合传统Web开发方式的理念有助于使项目结构更易于理解。每个动作都是一个独立的文件。使用Redux管理状态需要开发action和reducer。在官方的例子和几乎所有的教程中,所有的动作都放在一个文件中,所有的reducer都放在另一个文件中。这种方式易于理解但扩展性不强,当项目复杂时,action文件和reducer文件会变得非常冗长,难以开发和维护。所以我们使用每个动作一个单独的文件模式:每个Redux动作和相应的reducer都放在同一个文件中。使用这种方法的另一个原因是我们发现每次创建一个动作时,我们都需要创建一个reducer来几乎立即处理它。放在同一个文件中,有利于开发效率和维护。以开发一个计数器组件为例:为了实现点击“+”号加1的功能,我们首先需要创建一个类型为“COUNTER_PLUS_ONE”的action,然后马上需要创建一个对应的Reducer更新存储数据。官方的例子是分别在actions.js和reducer.js中添加相应的逻辑。每个动作使用独立文件的方法是创建一个名为counterPlusOne.js的文件,并添加如下代码:}exportfunctionreducer(state,action){switch(action.type){caseCOUNTER_PLUS_ONE:return{...state,count:state.count+1,};default:returnstate;}}根据我们的经验,大多数reducer会对应到相应的动作,它很少需要跨函数全局使用。因此,将它们放在一个文件中是非常有意义的,有助于提高开发效率。需要注意的是这里定义的reducer并不是标准的Reduxreducer,因为它没有初始状态(initialstate)。它只是由functions文件夹中的根reducer调用。请注意,此减速器固定命名为“reducer”,以便可以自动加载。对于异步动作(通常是远程API请求),需要处理错误信息,所以这个文件中有多个标准动作。例如,以保存一篇文章为例。在动作文件saveArticle.js中,有两个动作:saveArticle和dismissSaveArticleError。如何处理跨职能行动?虽然不是很常见,但某些操作可能由多个reducer处理。例如站内聊天功能,当收到新消息时:如果聊天框是打开的,则直接显示新消息。否则,显示有新消息的通知。可以看出,NEW_MESSAGE这个action类型需要经过不同的reducer处理,才能在不同的UI组件中有不同的展示。为了处理这种动作,每个函数文件夹都有一个reducer.js文件,可以处理跨函数的动作。虽然不同动作的reducer分布在不同的文件中,但它们和功能相关的rootreducer共同操作同一个state,即同一个storebranch。所以feature/reducer.js有如下代码结构:';construducers=[counterPlusOne,counterMinusOne,resetCounter,];exportdefaultfunctionreducer(state=initialState,action){letnewState;switch(action.type){//Putglobalreducersheredefault:newState=state;break;}returnreducers.reduce((s,r)=>r(s,action),newState);}负责引入不同action的reducer。当一个action到来时,它会遍历所有的reducer,并组合所需的全局reducer来更新store。所有功能相关的rootreducer最终组合成全局的Reduxrootreducer,保证全局只有一个store。需要注意的是,无论何时创建一个新的动作,都需要在这个文件中注册。因为它的模式是很固定的,我们可以使用工具自动注册对应的代码。Rekit可以帮助解决这个问题:当创建一个动作时,它会自动在reducer.js中添加相应的代码,从而减少工作量并避免错误。使用单文件动作的好处使用这种方式可以带来很多好处,例如:开发方便:创建动作时,不需要在多个文件中跳转;维护方便:因为每个动作都在一个单独的文件中,每个文件都很短,通过文件名就可以定位到对应的功能逻辑;易于测试:每个动作都可以由一个独立的测试文件覆盖,测试文件还包含动作和减速器的测试;易于使用的工具优化:由于使用Redux的应用程序具有相对复杂的技术结构,我们可以使用工具来自动化一些逻辑。现在我们可以自动生成代码而无需解析。易于静态分析:全局操作和缩减程序通常意味着模块间依赖关系。这时候我们只需要分析function文件夹下的reducer.js,就可以找到所有这些依赖了。React-router的规则定义一般来说,我们会通过一个配置文件来定义所有的路由规则。同样,这种方法也没有可扩展性,当项目变得复杂时,规则定义表也会变得冗长复杂。既然我们已经按照功能组织了文件夹,那么我们也可以将功能相关的路由规则放到相应的文件夹中。因此,我们可以使用React-router的JavaScriptAPI来定义路由规则,而不是使用常见的JSX语法。比如一个简单的论坛程序,主题功能对应的路由定义放在features/topic/route.js中,内容如下:import{EditPage,ListPage,ViewPage,}from'./index';exportdefault{path:'',name:'',childRoutes:[{path:'',component:ListPage,name:'TopicList',isIndex:true},{path:'topic/add',component:EditPage,name:'NewTopic'},{path:'topic/:topicId',component:ViewPage},],};所有与功能相关的路由定义都由全局根路由配置自动加载,因此路由加载器具有以下代码模式:importtopicRoutefrom'。./features/topic/route';importcommentRoutefrom'../features/comment/route';constroutes=[{path:'/rekit-example',component:App,childRoutes:[topicRoute,commentRoute,{path:'*',name:'Pagenotfound',component:PageNotFound},],}];可以看出,这个全局路由加载器负责加载所有特征的路由规则。和rootreducer类似,这里的codepattern也很固定,所以可以使用工具来维护这个文件。使用Rekit创建页面时,会自动在此处添加路由规则。使用工具辅助开发从上面的介绍中我们可以看出,开发一个React程序并不容易。即使是一个简单的功能,也需要大量琐碎但非常重要的代码来保证良好的架构,从而使应用程序易于扩展和维护,尽管这些外围代码与您需要的功能没有直接关系。例如,对于一个论坛程序,需要一个列表界面来显示最近发布的主题。为了制作这样一个页面,我们通常需要完成以下步骤:创建一个名为TopicList的React组件;为TopicList定义路由规则;创建一个名为TopicList.css的创建样式文件,并导入到合适的位置;使用react-redux将TopicList组件封装成一个容器组件,使其可以使用Reduxstore;创建4种不同的操作类型:FETCH_BEGIN,FETCH_PENDING,FETCH_SUCCESS,FETCH_FAILURE,通常在constants.js中定义;创建两个动作:fetchTopicList和dismissFetchTopicListError;在动作文件中引入类型常量;在reducer中创建4个swtichcase来处理不同的action类型;在reducer文件中引入类型常量;创建组件测试文件及其代码结构;创建动作测试文件及其代码结构;创建reducer测试文件及其代码结构。我的上帝!在正式开始编写论坛逻辑的第一行代码之前,还有很多琐碎的事情要做。当这样的事情人工重复很多次的时候,我们觉得应该有工具把这样的事情自动化。为此创建了一个Rekit工具包,它可以帮助自动生成这些文件结构和代码。与其他代码生成器不同,Rekit基于相对固定的文件和代码结构,因此它可以做更多的事情,例如:它知道在哪里以及如何定义路由规则;它知道如何生成动作类型常量;它知道如何根据动作名称生成类型常量;它知道如何根据动作类型创建减速器;它知道如何创建有意义的测试用例。借助维护良好的工具,我们不必关注技术细节,只需关注功能相关的代码,提高了开发效率。不仅如此,工具还可以减少错误,在代码结构、命名、配置等方面保持高度的一致性,让代码更容易理解和维护。Rekit为本文提出的React+Redux开发实践提供了一套工具,它本身是可扩展的。您可以根据需要更改代码模板,也可以提供自己的工具,针对自己的项目特点提供方便的工具,提高开发效率。总结本文主要介绍如何使用React、Redux和React-router开发可扩展的Web应用。核心思想有两个,一个是以功能(feature)为单位的组件文件夹结构;另一种是使用每个动作单独文件的方式。这样可以使代码更加模块化,添加和删除功能不会对其他模块产生太大影响。同时利用React-router帮助实现页面的概念,让单页应用(SPA)也具备传统Web应用的URL导航功能,进一步减少功能模块之间的耦合线,使得应用结构更清晰、更直观。为了支持这样的实践,文章还介绍了Rekit工具集,它不仅可以帮助创建和配置初始项目模板,还提供了大量实用工具来帮助按照文中提到的方式自动生成技术结构。文章,提高开发效率。更多工具介绍请访问其官网:http://rekit.js.org。