React项目结构和组件命名最佳实践

image.png

阅读该文章只需要8分钟

React 只是一个库,它并没有制定任何规则指导你该如何组织你的项目结构。这很好,因为它让我们可以自由尝试不同的组织方式,并选用更适合自己项目的方式。另一方面,这可能会给React开发初学者带来一些困惑。

我将在本文中展示一些我已经采用并扩展良好的方法。这些方法并没有重复造轮子,只是把圈内一些优秀实践组合在一起并加以改良。

记住: 这里提及的方法并不是一成不变的!你可以选用一些你觉得合理的方式
直接采用或者有所修改用于适应你自己的项目。

目录结构 (Folder Structure)

我经常碰到的一个问题是关于如何组织文件和文件夹的目录结构,在本文中,我先假定你使用 create-react-app 创建了一个最基本的目录结构,根目录下包含 .gitignore, package.json, README.md, yarn.lock 文件,以及public
src文件夹,项目的源代码将存放于src目录下。

目录结构如下图所示,本文将关注src目录,其余的一切将保持原样。

image.png

容器和组件 (Containers and Components)

您可能已经在某个项目的根目录中尝试过分离容器组件(Containers)演示组件(Presentation Components), 在src中,创建名为componentscontainers的目录。

src
├─ components 
└─ containers

然后,这样的方式会存在一些问题,如下所示:

  • 主观规则 - 你并没有很清晰的规则区分什么是容器组件, 什么是展示组件
    彼此之间的差异是很主观的,当你在一个团队里面,很难让所有的开发者都有相同的判断。

  • 没有考虑组件的动态性 - 即使当你确定某个组件是容器组件,也很容易在项目生命周期中变更成为展示组件,并迫使你将它从容器组件目录移动到展示组件目录,反之亦然。

  • 允许组件重名 - 在一个应用中,组件应该具有声明性和唯一的命名以避免混淆每个组件的职责。然而,上述的方法可产生漏洞让两个组件重名,一个代表容器组件,一个代表展示组件

  • 效率缺失 - 你需要不断的切换containerscomponents目录,甚至你只是在开发同一个功能点,通常一个功能点会需要两种类型的组件。

这种方法还有一个变种,在模块内部保留这种分离。

想象一下在你的应用内部有一个 User 模块,你用两个目录分离你的组件

src
└─ User
  ├─ components
  └─ containers

上述方法最大限度的降低了你在项目中切换目录的麻烦,同时也衍生了一些杂音。项目中的模块越多,将会产生越多的containerscomponenets目录。

基于这些原因,当我们在讨论如何组织文件和目录时,用展示容器的概念来区分组件显得无关紧要,也就是说,我们可以把所有的组件都存放在components目录除了纯UI的组件。

即便展示组件容器在区分文件夹部分是无关紧要,但了解他们之间的差异依然非常重要,如果对于这个概念你有任何疑问,建议你阅读该文Presentational and Container Components.

代码分离与分组

components目录下,我们按照模块/功能方式分组文件。

对于 userCRUD , 我们将会有一个 User 模块, 结构如下所示:

└─ components
  └─ User
    ├─ Form.jsx
    └─ List.jsx

当一个组件由多个文件组成时,我们把组件以及相关文件放在同名文件夹中。
例如,你的 Form.css 包含 Form.jsx 所需要的样式, 你的目录结构如下所示:

src
└─ components
  └─ User
    ├─ Form
    │ ├─ Form.jsx
    │ └─ Form.css
    └─ List.jsx

测试文件和代码文件放在同一个目录,上述例子,Form.jsx 的测试文件可以命名为Form.spec.jsx并存在相同目录。有没有一点眼熟的感觉,假如你了解google的前端框架

UI 组件

除了按模块分离组件之外,我们还在src/components中包含一个UI文件夹用于存放所以的公用组件

UI组件作为公用组件并不属于任何模块, 甚至是可以保存在开源的lib,因为它们并不包含任何应用的任何业务逻辑。例如Buttons, Inputs, Checkboxes, Selects, Modals, Data display elements 等等。

组件命名

我们讨论过了按照模块来组织和分离我们的组件,那么问题来了,如何为他们命名?

当我们讨论如何为组件命名,指的是给一个定义组件的类或者常量命名:
class MyComponent extends Component {
}
const MyComponent () => {};

为了方便查找和避免冲突,组件的命名必须在应用中清晰且唯一。

当我们使用React Dev Tools调试应用时,好的组件命名会变得非常方便,并在一些运行时错误(run-time errors)中清晰得指出错误的组件。

我们遵循path-based-component-naming原则,用基于src或者components目录的相对路径为组件命名,基本上一个组件的路径为 components/User/List.jsx将会被命名为UserList.

当文件名和所在目录重名时,我们不必再次重复,例如components/User/Form/Form.jsx命名为UserForm而不是UserFormForm

上述模式的一些好处如下:

搜索便利

假如你的编辑器支持模糊搜索,只需要搜索UserForm就可以找到你需要的文件:

image.png

假如你通过文件夹目录查找文件,根据组件名称可以很容易的定位:

image.png
避免文件与文件夹名称重复

按照这种模式,我们根据上下文命名文件。上一段提及的Form,我们知道它是一个User Form,既然它已经在User文件夹了,那么不必在文件名中重复出现User,只需命名为Form.jsx

当我最初使用React时,曾经使用全名(组件名)命名文件,这样导致同一名称多次重复出现(/screens/User/UserForm)并且引用路径变得过于冗长。

请看一下两种方式的不同:

import ScreensUserForm from './screens/User/UserForm';
// vs
import ScreensUserForm from './screens/User/Form';

在上面的例子中,你也许看不到第二种方式有什么优势。但随着项目的不断增长,两种命名方式会有很大的不同,如下所示:

import MediaPlanViewChannel from '/MediaPlan/MediaPlanView/MediaPlanViewChannel.jsx';
// vs
import MediaPlanViewChannel from './MediaPlan/View/Channel';

那再想象一下在这个文件中引用5倍甚至更多的依赖,吧

基于这个原因,我们推荐根据文件的上下文来命名文件,component则相对于componentssrc文件夹的位置来命名。

界面(Screens)

顾名思义,Screens就是我们在应用程序中界面。

user 的增删查改中,我们将会有 user 列表界面,新增user界面和修改user界面。

界面是你用组件为应用程序组合成的一个的页面,理想状况下,界面应该不包含任何的逻辑只是功能组件。

我们将界面(Screens)保存在src根目录下单独的文件夹中,它们按路由(route)定义分组而不是模块:

src
├─ components 
└─ screens
  └─ User
    ├─ Form.jsx
    └─ List.jsx

假设项目使用react-router,我们把Root.jsx保存在Screens文件夹中,并在其中定义所有的应用程序路由。

Root. jsx的代码类似于:

import React, { Component } from 'react';
import { Router } from 'react-router';
import { Redirect, Route, Switch } from 'react-router-dom';

import ScreensUserForm from './User/Form';
import ScreensUserList from './User/List';

const ScreensRoot = () => (
  <Router>
    <Switch>
      <Route path="/user/list" component={ScreensUserList} />
      <Route path="/user/create" component={ScreensUserForm} />
      <Route path="/user/:id" component={ScreensUserForm} />
    </Switch>
  </Router>
);

export default ScreensRoot;

请注意,我们将所有screens文件放在一个与路由同名的文件夹中,user/ ->User/。为每个父路由(parent route)创建一个文件夹,并在其中定义子路由(sub-routes)。在上面例子中,我们创建了User文件夹,并将ScreensUserListScreensUserForm保存在其中。在这种模式下,你通过url轻松找到每个路由对应的渲染界面(screen)

一个界面(screen)可以同时为多个路由(route)渲染, 如上述例子中ScreensUserForm同时为创建user和编辑user渲染。

你应该已经注意到所有的视图组件命名中都包含Screen前缀。当组件文件存放于components文件夹之外,我们应该用针对src的相对路径进行命名。在src/screens/User/List.jsx路径的组件应该命名为ScreensUserList.

创建Root.jsx后,我们的结构如下:

src
├─ components 
└─ screens
  ├─ User
  │ ├─ Form.jsx
  │ └─ List.jsx
  └─ Root.jsx

不要忘记在index.js中引用Root.jsx作为应用程序的根组件(root component)

如果你对怎样定义screen存在疑问,请看下面的user formscreen定义:

import React from 'react';
import UserForm from '../../components/User/Form/Form';

const ScreensUserForm = ({ match: { params } }) => (
  <div>
    <h1>
      {`${!params.id ? 'Create' : 'Update'}`} User
    </h1>
    <UserForm id={params.id} />
  </div>
);

export default ScreensUserForm;

最终,我们的应用程序结构如下所示:

src
├─ components 
│  ├─ User
│  │ ├─ Form
│  │ │ ├─ Form.jsx
│  │ │ └─ Form.css
│  │ └─ List.jsx
│  └─ UI 
│
└─ screens
  ├─ User
  │ ├─ Form.jsx
  │ └─ List.jsx
  └─ Root.jsx

重述

  • 展示组件(Presentational component)和容器组件(container component)都应该保存于 src/components目录
  • 模块/功能分类组件,如User/List
  • 将通用组件存于src/components/UI目录
  • 保持界面(screens)简单,结构和代码最小
  • 根据路由定义为screens分组,路由 /user/list对应的界面(screen)位于/src/screens/User/List.jsx
  • 组件依据和componentsrc相对路径来命名,鉴于此,位于src/components/User/List.jsx应该命名为UserList,位于src/screens/User/List应该命名为ScreensUserList
  • 组件文件名和所在文件夹重名时,组件名不需再重复,位于src/components/User/List/List.jsx的组件应该命名为UserList而不是UserListList

最后

本文仅涵盖React项目结构的一些建议,Redux部分将在后续的文章中介绍。

感谢你的阅读,如有你什么想法请留言,我会很乐意阅读和回复。

如果本文对你有所帮助,请点赞或者推荐给你的同事和朋友阅读,不胜感谢。

原文地址: https://hackernoon.com/structuring-projects-and-naming-components-in-react-1261b6e18d76
感谢 Vinicius Dacal

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容