本文主要分为两部分,一部分是项目文件结构、一部分是编码规范。
项目文件结构
文件和文件夹的命名规则
-
字母全部小写,单词间用小短线
-
连接auto-complete/ date-picker.tsx
-
React UI 组件文件名后缀为
tsx
button.tsx menu.tsx
-
如果一个文件夹下有多个同类型文件,则文件名应该是复数:
components pages
-
index.ts 只能作为导出文件使用
不然搜索文件时需要关注文件夹名,增加了心智负担。
//index.ts export * from "./my-component"; export { default } from "./my-component";
项目结构划分的方式
-
按功能划分
把一个功能相关联的文件都放在一起:
common/ Avatar.js Avatar.css APIUtils.js APIUtils.test.js feed/ index.js Feed.js Feed.css FeedStory.js FeedStory.test.js FeedAPI.js profile/ index.js Profile.js ProfileHeader.js ProfileHeader.css ProfileAPI.js
按功能划分结构很清晰,一个功能的代码都在一起,要删除一个功能也不用到处去找相关的文件。但是,一个功能的定义因人而异,有时一个功能的定义非常模糊,拆分起来非常困难,比如要实现一个搜索订单的功能,是应该属于搜索还是属于订单?
-
按文件类型划分
将同一类型的文件放一起,比如所有的 component,所有的 api:
apis/ APIUtils.js ProfileAPI.js UserAPI.js components/ Avatar.js Feed.js FeedStory.js Profile.js ProfileHeader.js
按类型放一起的好处在于,不用再纠结该放哪的问题,组件都放 components 就好了,api 都放 apis 下好了,当然这失去了按功能组织的优点,因为相关文件都散落在各处。
这两种项目结构并没有绝对的好坏之分。通常,随着项目扩大,一般都是混用这两种类型。之所以有结构,更多的是一种约定,开发者共同遵守这一约定即可。
项目结构划分
项目结构划分个人更倾向于以按功能划分为主,类型划分为辅的混合方式。 目录大概分为以下类型:
project/
├── docs/
├── scripts/
├── .prettierrc
├── tsconfig.json
└── src/
docs
项目相关的文档文件。
docs/
├── dev.md
└── architecture.md
scripts
项目的一些工具脚本文件。
scripts/
├── compile-message.sh
└── generate-pro-env.ts
.prettierrc
prettier 配置文件,如果不配置,可能导致不同开发人员保存时格式化的结果不一致。
//.prettierrc
{
"semi": true,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "all"
}
tsconfig.json
TypeScript 配置文件。
//tsconfig.json
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": "." //支持通过绝对路径(如src/)引用组件
},
"include": ["src", "scripts"] //src、scripts两个文件夹都接受ts检查
}
src
src 文件夹为项目的主要文件夹,目录结构大致如下:
src/
├── index.tsx
├── app/
├── pages/
├── locales/
├── assets/
├── services/
├── shared/
│ ├── constants/
│ ├── components/
│ ├── hooks/
│ ├── utils/
│ ├── helpers/
│ ├── redux/
│ └── types/
└── features/
-
index.tsx
项目入口文件。
import React from "react"; import ReactDOM from "react-dom/client"; import App from "./app"; const root = ReactDOM.createRoot( document.getElementById("root") as HTMLElement ); root.render( <React.StrictMode> <App /> </React.StrictMode> );
-
app
在一个 React 项目中,app 目录主要用于存放全局应用相关的配置和管理逻辑。以下是常见的内容:
app/ ├── app.tsx // 应用的根组件。 ├── store.ts // Redux store 配置文件,包含中间件和 reducers 的结合。 ├── router.tsx // 应用的路由配置文件,定义了所有的路由和对应的组件。 └── index.tsx // 应用的入口文件,渲染根组件并挂载到 DOM 上。
-
app.tsx
app 入口文件。
import React from "react"; import styled from "styled-components"; import { IntlProvider } from "react-intl"; import { Provider } from "react-redux"; import { PersistGate } from "redux-persist/integration/react"; import { persistStore } from "redux-persist"; import { ConfigProvider } from "antd"; import { getLocale } from "./locales"; import { store } from "./store"; import Router from "./router"; const persistor = persistStore(store); const App = () => ( <Provider store={store}> <IntlProvider locale="zh" messages={getLocale("zh")}> <ConfigProvider locale={zhCN}> <PersistGate persistor={persistor}> <Router /> </PersistGate> </ConfigProvider> </IntlProvider> </Provider> ); export default App;
-
store.ts
redux store 相关的文件,如果不需要就不创建。
//store.ts import { configureStore } from "@reduxjs/toolkit"; import { combineReducers } from "redux"; const persistConfig = { key: "root", storage, whitelist: ["user"], }; const rootReducer = combineReducers({}); export const store = configureStore({ reducer: rootReducer, }); export type RootState = ReturnType<typeof store.getState>; export type AppDispatch = typeof store.dispatch;
-
router.tsx
路由文件。 路由文件这里写得比较简单,如果项目比较复杂可以将路由文件放到对应的 feature 文件夹下。
//router.tsx import React from "react"; import { Routes, Route, HashRouter } from "react-router-dom"; import { HomePage } from "./pages/home"; import { LoginPage } from "./pages/login"; const RouteMap = { ROOT: "/", LOGIN: "/login", }; const Router = () => ( <Routes> <Route path={RouteMap.ROOT} element={<HomePage />} /> <Route path={RouteMap.LOGIN} element={<LoginPage />} /> </Routes> ); export default Router;
-
-
pages
页面文件,pages 目录下为各个页面的文件,每个页面都有一个路由。
pages/ ├── login.tsx ├── about.tsx └── home.tsx
页面应由 feature 组合而成,而不应该包含显示的逻辑(逻辑在子组件中实现):
//pages/login.tsx export const LoginPage = () => { return ( <Container> <LogoContainer> <Logo src={logoImg} /> <Slogan> <FormattedMessage defaultMessage="人工智能助力智慧医疗" /> </Slogan> <LoginForm />//LoginForm包含了登录逻辑 </LoginContainer> <Version /> </Container> ); };
-
locales 多语言文件,一般有中英文的 JSON 文件。
locales/ ├── en.json │ zh.json └── index.ts //导出多语言文件 /***************分割线*****************/ // index.ts import zh from './zh.json'; import en from './en.json'; export const getLocale = (type: string) => ({ zh, en }[type]);
-
assets
资源文件,一般包括图片和字体等。
assets/ ├── images └── fonts
-
services
services 目录通常包含数据处理相关的文件和模块,主要用于与后端 API 进行交互、数据获取和处理、身份验证、日志记录等。
services/ ├── axios-config.ts // axios全局配置文件 ├── apis.ts // axios全局配置文件 ├── auth.ts // 身份验证服务 └── logger.ts // 日志服务
-
shared
shared 目录用于存放多个 feature 模块间共享的资源。
shared/ ├── constants/ ├── components/ ├── hooks/ ├── utils/ ├── helpers/ ├── redux/ └── types
-
constants
项目中的 constants 文件通常用于存放项目中使用的常量值,如 API 端点、错误消息、配置参数等。
constants/ ├── api-end-points.ts └── index.ts // 导出常量
api-end-points.ts
//api-end-points.ts export const API_BASE_URL = 'https://api.example.com'; export const USERS_ENDPOINT = '/users'; export const POSTS_ENDPOINT = '/posts';
-
components
组件目录,只有在多个 feature 需要复用组件时才存在,如果没有复用的需求则 components 应该挪到对应的 feature 目录。一个文件夹为一个组件(组件内部可能有多个小组件),如果组件只需要一个文件则添加与组件同名文件(不能只用一个 index.tsx),并用 index.ts 导出。如果组件内部有 css 文件则名字与文件夹同名。 总体来说,"feature"更侧重于业务功能或用户需求,而"component"更侧重于通用的、可复用的功能单元。在实践中,你可能会发现这两个概念之间有一些重叠,具体的组织方式可能会根据项目的需求和团队的偏好而有所不同。重要的是要保持一致性,并确保目录结构能够反映项目的逻辑组织和架构。
components/ ├── button/ │ ├── button.tsx | └── index.ts ├── icon/ │ ├── icon.tsx │ ├── icon.css │ └── index.ts └── index.ts //导出组件
组件的命名采用基于路径的组件命名方式,即根据相对于 components 文件目录的相对路径来命名,名字为大驼峰式,比如
components/order/list.tsx
,被命名为 OrderList。如果文件名和文件目录名相同,则不需要重复这个名字。比如components/order/form/form.tsx
会命名为 OrderForm 而不是 OrderFormForm。components/ ├── order/ │ └── form │ │ ├── form.tsx │ │ └── form.css │ ├── list.tsx │ └── index.ts
-
hooks
自定义的 React Hook 文件,只有在多个功能需要复用 hook 时才存在。一个文件为一个 hook,如果一个 hook 需要多个文件则拆分成更小的 hook,hook 文件以 use 开头。
hooks/ ├── use-mounted └── use-key └── index.ts //导出hook
hook 组件的命名方式为 use 开头的小驼峰式。
import { useState, useEffect } from "react"; function useFriendStatus(friendID) {}
-
utils
一些可以复用的工具函数,和业务无关,可以项目间通用(比如 lodash 的函数)。
utils/ ├── create-random-number.ts └── debounce.ts └── index.ts //导出工具函数
-
helpers
为完成项目内特定的功能而实现的和业务相关的函数,如果不需要则不创建。
helpers/ ├── sort-case.ts └── make-query.ts └── index.ts //导出帮助函数
-
redux
redux 文件夹用来存放 redux 相关的文件(也可以是其他提供数据源的框架),如果用redux-toolkit,一般包括 slice 文件和 selector 文件:
redux/ ├── slice.ts ├── selector.ts └── index.ts //导出 slice 和 selector
-
types
types 文件夹用来定义 feature 间共享的 type。
types/ ├── api.ts └── index.ts //导出定义的type
api.ts
export interface ApiResponse<T> { data: T; status: number; message: string; } export interface User { id: number; name: string; email: string; }
-
-
features
按功能划分的文件,通过该目录就能知道 app 的相关功能,feature 间严格禁止引用。上述 shared 目录下的文件夹如果没有 feature 间复用的需求应放到具体的 feature 目录下。
features/ ├── todos/ | ├── constants/ | ├── components/ | ├── hooks/ | ├── utils/ | ├── redux/ | ├── helpers/ | ├── types/ | └── index.ts ├── users/ └── index.ts //导出 feature
综上,最终的一个前端项目目录结构大致如下:
project/
├── docs/
├── scripts/
├── .prettierrc
├── tsconfig.json
└── src/
├── pages/
├── locales/
├── assets/
├── services/
├── shared/
├── features/
| ├── feature1/
| | ├── constants/
| | ├── components/
| | ├── hooks/
| | ├── utils/
| | ├── apis/
| | ├── redux/
| | ├── helpers/
| | ├── types/
| | ├── feature1.tsx
| | └── index.ts
| ├── feature2/
| ├── ...
| └── index.ts
├── app/
| ├── store.ts
| ├── router.tsx
| └── app.ts
└── index.tsx
编码规范
-
enum 名和枚举名都采用大驼峰
enum 名采用单数形式。
enum Direction { Up, Down, Left, Right, } enum FileAccess { None, Read Write ReadWrite = Read | Write, }
-
类的私有属性命名以下划线开头
class Man { private _name: string; constructor() { } // 公共方法 getName() { return this._name; } // 公共方法 setName(name) { this._name = name; } }
-
什么时候用 is
is 表示是不是,返回一个布尔值,当这个词是形容词时通常不加 is,比如:
enable disable
如果这个词既是形容词又是动词时,应该加 is,比如 empty,它有清空的意思,表达是否为空就应该是 isEmpty。
-
引入模块的顺序
引入模块时先引入 node_modules 的模块,再引入自定义模块, node_modules 模块和自定义模块间用空行隔开,自定义模块先引入绝对路径的模块,再引入相对路径的模块:
import React, { useMemo } from "react"; import { Table } from "antd"; import styled from "styled-components"; import { ColorTag, Row, Pagination } from "src/components"; import { TagList } from "./tag-list";
-
导出 default
为简单起见一律通过 export 直接导出,不用导出 default:
export const sum = () => {}; export const add = () => {};
-
路由命名全小写,单词间用中划线
-
链接const RouteMap = { Login: "/login", UserCenter: "user-center", DicomViewer: "dicom-viewer", };
-
路由命名全小写,单词间用中划线
-
链接
const RouteMap = {
Login: "/login",
UserCenter: "user-center",
DicomViewer: "dicom-viewer",
};
-
请求参数以驼峰示命名
/api/v1/resource?pageNum=1&pageSize=10
-
从远程获取资源用函数命名以 fetch 开头
fetch 常常用于表示从远程资源(通常是服务器)获取数据的操作。 get 通常表示从某个地方获取数据,不一定是通过网络请求,可能还包括从本地存储、缓存或其他地方获取。
//bad const getProjects = () => {}; //good const fetchProjects = () => {};
-
带有缩写词和缩略词的变量名
缩写词,即单个单词的缩写,如 ID(identification)。 缩略词,即多个单词的缩写,如 CSS(Cascading Style Sheets)。
-
缩写词或 2 个字母以上的缩略词只需要首字母大写
//bad var userID = "1"; var HTMLAndCSS = "xx"; var userBG = "red"; //good var userId = "1"; var htmlAndCss = "xx"; //html是缩略词,但是首字母应小写 var userBg = "red";
-
2 个字母缩略词要全大写
//bad var userDbIoPort = "xx"; //good var userDBIOPort = "xx"; //两个大写字母连着也就意味着这两个字母是一组,所以DB一定是一组,IO一定是一组
-
-
事件函数命名区分
组件内部函数按照
handle{Type}{Event}
命名,例如 handleStatusChange。对外暴露的方法按照on{Type}{Event}
命名,例如 onStatusChange。 -
将属性的缩写放在对象声明的开头。
const a = 1; const b = 2; // bad const obj = { c: 3, d: 4, a, b, }; // good const obj = { a, b, c: 3, d: 4, };
-
减少无意义的前缀
// bad const user = { userName: "lily", userAge: 17, }; // good const user = { name: "lily", age: 17, };
-
函数参数最好不要超过三个
// bad const request = (url, method = 'get', data, params) => {}; // good const request = ({ url, method = 'get', data, params }) => {}; // good const request = (url, data, options: { method = 'get', params }) => {};
-
函数应遵循单一职责原则
// bad const notify = (users) => { users.map((user) => { const record = DB.find(user); if (record.isActive()) { sendMessage(user); } }); }; // good const isActive = (user) => { const record = DB.find(user); return record.isActive(); }; const notify = (users) => { users.filter(isActive).forEach(sendMessage); };
-
函数应提前 return
提前 return 代码结构能更加清晰。
// bad const onChange = ({ a, b, c, d, e }) => { if (a) { if (b) { if (c) { if (d) { if (e) { return true; } } } } } return false; }; // good const onChange = ({ a, b, c, d, e }) => { if (!a) return false; if (!b) return false; if (!c) return false; if (!d) return false; if (!e) return false; return true; };
-
if 应首先处理正逻辑
//bad const submit = (dirty) => { if (!dirty) { } else { } }; //good const submit = (dirty) => { if (dirty) { } else { } };
-
null 和 undefined
如果是赋空值只能用 null,只有在某个属性未定义是才是 undefined,因为这才是未定义的意思。
let a = 3; //bad a = undefined; //good a = null;
-
不要为了共享而把变量设置为类的字段
//bad class A { let v; method1() { v = 1; method2(); } method2() { console.log(v) } } //good class A { method1() { let v = 1; method2(v); } method2(v) { console.log(v) } }
-
package.json 中自定义的 script 命名
如果归类,类别与动作间用
:
号分割,动作单词间用驼峰式命名://bad { "scripts": { "dev": "vite", "build": "vite build", "extractCh": "node scripts/extractChinese.js", "extractEn": "node scripts/extractEnglish.js", "generateAuthToken": "node scripts/generateAuthToken.js", "generateApiToken": "node scripts/generateApiToken.js" } } //good { "scripts": { "dev": "vite", "build": "vite build", "i18n:extractCh": "node scripts/extractChinese.js", "i18n:extractCn": "node scripts/extractEnglish.js", "generate:authToken": "node scripts/generateAuthToken.js", "generate:apiToken": "node scripts/generateApiToken.js" } }
-
尽量用语义化的标签
//bad <b>My page title</b> <div class="top-navigation"> <div class="nav-item"><a href="#home">Home</a></div> <div class="nav-item"><a href="#news">News</a></div> <div class="nav-item"><a href="#about">About</a></div> </div> <div class="news-page"> <div class="page-section news"> <div class="title">All news articles</div> <div class="news-article"> <h2>Bad article</h2> <div class="intro">Introduction sub-title</div> <div class="content">This is a very bad example for HTML semantics</div> <div class="article-side-notes"> I think I'm more on the side and should not receive the main credits </div> <div class="article-foot-notes"> This article was created by David <div class="time">2014-01-01 00:00</div> </div> </div> <div class="section-footer"> Related sections: Events, Public holidays </div> </div> </div> <div class="page-footer">Copyright 2014</div> //good <!-- The page header should go into a header element --> <header> <!-- As this title belongs to the page structure it's a heading and h1 should be used --> <h1>My page title</h1> </header> <!-- All navigation should go into a nav element --> <nav class="top-navigation"> <!-- A listing of elements should always go to UL (OL for ordered listings) --> <ul> <li class="nav-item"><a href="#home">Home</a></li> <li class="nav-item"><a href="#news">News</a></li> <li class="nav-item"><a href="#about">About</a></li> </ul> </nav> <!-- The main part of the page should go into a main element (also use role="main" for accessibility) --> <main class="news-page" role="main"> <!-- A section of a page should go into a section element. Divide a page into sections with semantic elements. --> <section class="page-section news"> <!-- A section header should go into a section element --> <header> <!-- As a page section belongs to the page structure heading elements should be used (in this case h2) --> <h2 class="title">All news articles</h2> </header> <!-- If a section / module can be seen as an article (news article, blog entry, products teaser, any other re-usable module / section that can occur multiple times on a page) a article element should be used --> <article class="news-article"> <!-- An article can contain a header that contains the summary / introduction information of the article --> <header> <!-- As a article title does not belong to the overall page structure there should not be any heading tag! --> <div class="article-title">Good article</div> <!-- Small can optionally be used to reduce importance --> <small class="intro">Introduction sub-title</small> </header> <!-- For the main content in a section or article there is no semantic element --> <div class="content"> <p>This is a good example for HTML semantics</p> </div> <!-- For content that is represented as side note or less important information in a given context use aside --> <aside class="article-side-notes"> <p> I think I'm more on the side and should not receive the main credits </p> </aside> <!-- Articles can also contain footers. If you have footnotes for an article place them into a footer element --> <footer class="article-foot-notes"> <!-- The time element can be used to annotate a timestamp. Use the datetime attribute to specify ISO time while the actual text in the time element can also be more human readable / relative --> <p> This article was created by David <time datetime="2014-01-01 00:00" class="time">1 month ago</time> </p> </footer> </article> <!-- In a section, footnotes or similar information can also go into a footer element --> <footer class="section-footer"> <p>Related sections: Events, Public holidays</p> </footer> </section> </main> <!-- Your page footer should go into a global footer element --> <footer class="page-footer">Copyright 2014</footer>
需要说明的是,不管是项目结构还是编码规范,随着认知的变化是会不断调整的。