本文主要分为两部分,一部分是项目文件结构、一部分是编码规范。
项目文件结构
文件和文件夹的命名规则
-
字母全部小写,单词间用小短线
-
连接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/
├── pages/
├── locales/
├── fetcher/ (可能有)
├── components/ (可能有)
├── hooks/ (可能有)
├── utils/ (可能有)
├── features/
├── store.ts (可能有)
├── routes.tsx (可能有)
├── app.tsx
└── index.tsx
-
pages
页面文件,pages 下为各个页面的文件,每个页面都有一个路由,主页命名为 index.tsx。
pages/ ├── login.tsx ├── about.tsx └── index.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]);
-
fetcher
处理 api 请求权限,配置等相关的文件,如果数据请求无需统一配置,也没有权限问题,则不创建该文件夹。
fetcher/ ├── fetcher.ts └── index.tsx /***************分割线*****************/ // fetcher.ts import A, { AxiosInstance, AxiosError } from 'axios'; export const STRAPI_CMS_HOST = process.env.REACT_APP_STRAPI_CMS_URL || ''; class Fetcher { private _axios?: AxiosInstance; initAxios = (token: string, onUnauthorizedError: () => void) => { this._axios = A.create({ baseURL: STRAPI_CMS_HOST, headers: { authorization: `Bearer ${token}`, }, }); this._axios.interceptors.response.use(undefined, (error: AxiosError) => { if (error?.response?.status === 401) { onUnauthorizedError(); } else { return Promise.reject(error); } }); }; get axios(): AxiosInstance { if (!this._axios) { throw new Error('It must be init axios'); } return this._axios!; } } export const fetcher = new Fetcher();
-
components
通用组件文件,与业务无关,只有在多个 feature 需要复用组件时才存在,如果没有复用的需求则 components 应该挪到对应的 feature 目录。一个文件夹为一个组件(组件内部可能有多个小组件),如果组件只需要一个文件则添加与组件同名文件(不能只用一个 index.tsx),并用 index.ts 导出。如果组件内部有 css 文件则名字与文件夹同名。
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 //导出工具函数
-
features
按功能划分的文件,通过该目录就能知道 app 的相关功能,feature 间严格禁止引用。上述的 components、hooks、utils 如果没有 feature 间复用的需求应放到 features 目录下,当 components 放到 feature 目录下时也可以放与业务相关的组件。此外 features 目录下可能还有 models、apis、helpers、assets。
features/ ├── todos/ | ├── components/ | ├── hooks/ | ├── utils/ | ├── apis/ | ├── models/ | ├── helpers/ | ├── assets/ | └── index.ts ├── users/ └── index.ts //导出 feature
-
apis
数据请求的 api 文件,如果有 fetcher 则会引用到 fetcher 文件。
apis/ ├── user.ts ├── log.ts └── index.ts //导出api
-
models
redux 相关的文件(也可以是其他提供数据源的框架),如果用redux-toolkit,一般包括 slice 文件和 selector 文件:
models/ ├── todos/ | ├── slice.ts | ├── selector.ts | └── index.ts //导出 slice 和 selector └── index.ts //导出 model
-
helpers
为完成项目内特定的功能而实现的和业务相关的函数,如果不需要则不创建。
helpers/ ├── sort-case.ts └── make-query.ts └── index.ts //导出帮助函数
-
assets
资源文件,一般包括图片和字体。
assets/ ├── images └── fonts
-
-
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;
-
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;
-
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> );
综上,最终的一个前端项目目录结构如下:
project/
├── docs/
├── scripts/
├── .prettierrc
├── tsconfig.json
└── src/
├── pages/
├── locales/
├── fetcher/ (可能有)
├── components/ (可能有)
├── hooks/ (可能有)
├── utils/ (可能有)
├── features/
| ├── feature1/
| | ├── components/
| | ├── hooks/
| | ├── utils/
| | ├── apis/
| | ├── models/
| | ├── helpers/
| | ├── assets/
| | └── index.ts
| ├── feature2/
| ├── ...
| └── index.ts
├── store.ts (可能有)
├── routes.tsx (可能有)
├── app.tsx
└── index.tsx
编码规范
-
表示某一类别的对象命名用 Map 结尾
对象所代表的内容用单数,结尾加 Map,比如 reducerMap 不应该是 reducers 或 reducersMap。
const reducerMap = { router: connectRouter(history), diagram: diagramReducer, editor: editorReducer, }; const stateMap = { success: 'success', failed: 'failed', pending: 'pending', };
-
不是一个类别的对象命名用复数
如果对象的值不是表示一类参数,且对象键值对不只一个,就用复数形式:
//bad const option = { include: [] target: 'web' }; //good const options = { include: [] target: 'web' };
-
enum 名和枚举名都采用大驼峰
enum 名采用单数形式。
enum Direction { Up, Down, Left, Right, } enum FileAccess { None, Read Write ReadWrite = Read | Write, }
-
常量全部大写,单词间用下划线分隔
const MAX_COUNT = 70; const URL = 'http://www.xinghunm.com';
-
类的私有属性命名以下划线开头
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
当只导出一个变量且可以预见将来不会再导出其他变量时,用 default 导出。当要导出多个变量时,如果没有明确的主体则不导出 default。
//a.js const sum = () => {}; export default sum; /***************分割线*****************/ //b.js //sum、add没有明确的主体,也没有主从关系,不导出default export const sum = () => {}; export const add = () => {};
-
路由命名全小写,单词间用中划线
-
链接const RouteMap = { Login: '/login', UserCenter: 'user-center', DicomViewer: 'dicom-viewer', };
-
缩写大写
缩写一律大写:
//bad caseId htmlAndCss //good caseID HTMLAndCSS
-
事件函数命名区分
组件内部函数按照
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) } }
-
尽量用语义化的标签
//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>
需要说明的是,不管是项目结构还是编码规范,随着认知的变化是会不断调整的。当然,这不意味着反复无常。