前端项目规范

3,377 阅读11分钟

本文主要分为两部分,一部分是项目文件结构、一部分是编码规范。

项目文件结构

文件和文件夹的命名规则

  • 字母全部小写,单词间用小短线-连接

    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>
    

需要说明的是,不管是项目结构还是编码规范,随着认知的变化是会不断调整的。