web项目规范说明

154 阅读6分钟

文件夹结构

目录结构

├── config                   # umi 配置,包含路由,构建等配置
├── mock                     # 本地模拟数据
├── public
│   └── favicon.png          # Favicon
├── src
│   ├── assets               # 本地静态资源
│   ├── components           # 业务通用组件
│   ├── e2e                  # 集成测试用例
│   ├── layouts              # 通用布局
│   ├── models               # 全局 dva model
│   ├── pages                # 业务页面入口和常用模板
│   ├── services             # 后台接口服务
│   ├── utils                # 工具库
│   ├── locales              # 国际化资源
│   ├── global.less          # 全局样式
│   └── global.ts            # 全局 JS
├── tests                    # 测试工具
├── README.md
└── package.json

功能组件目录结构

src
├── components
└── pages
├── Welcome        // 路由组件下不应该再包含其他路由组件,基于这个约定就能清楚的区分路由组件和非路由组件了
|   ├── components // 对于复杂的页面可以再自己做更深层次的组织,但建议不要超过三层
|   ├── Form.tsx
|   ├── index.tsx  // 页面组件的代码
|   └── index.less // 页面样式
├── Order          // 路由组件下不应该再包含其他路由组件,基于这个约定就能清楚的区分路由组件和非路由组件了
|   ├── index.tsx
|   └── index.less
├── user           // 一系列页面推荐通过小写的单一字母做 group 目录
|   ├── components // group 下公用的组件集合
|   ├── Login      // group 下的页面 Login
|   ├── Register   // group 下的页面 Register
|   └── util.ts    // 这里可以有一些共用方法之类,不做推荐和约束,看业务场景自行做组织
└── *              // 其它页面组件代码

安装包及构建

yarn

package.jsonscripts脚本中运行相关命令 例如

yarn start:stag

"start:stag": "cross-env REACT_APP_ENV=stag MOCK=none UMI_ENV=stag umi dev",
"start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=test umi dev",
"start:prod": "cross-env REACT_APP_ENV=prod MOCK=none UMI_ENV=prod umi dev",

主题和加载页

config文件夹中defaultSettings.ts中配置主题和Logo

const settings: LayoutSettings &amp {
  pwa?: boolean
  logo?: string
} = {
  // 修改右上角的 logo
  logo: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg',
  // 设置标题的 title
  title: 'Ant Design Pro',
  navTheme: 'light',
  // 拂晓蓝
  primaryColor: '#1890ff',
  layout: 'mix',
  contentWidth: 'Fluid',
  fixSiderbar: true,
}
export default settings

global.less导入自定义antd样式

@import '~antd/es/style/themes/default.less'
@import './antd-reset.less'

app入口

export { initialStateConfig, getInitialState } from './plugins/plugin-initial-state'
export { layout } from './plugins/plugin-layout'

获取用户信息的loading页面,根据缓存中的token的值请求用户信息接口,判断是否显示登录页面。

项目部分代码注释

const signInPath = '/user/sign-in'

/** 获取用户信息比较慢的时候会展示一个 loading */
export const initialStateConfig = {
  loading: <PageLoading />,
}

/**
 * @see  https://umijs.org/zh-CN/plugins/plugin-initial-state
 * */
export const getInitialState = async (): Promise<{
  settings?: Partial<LayoutSettings>
  token?: string
  organizationId?: string
  currentUser?: API.CurrentUser
  fetchUserInfo?: () => Promise<API.CurrentUser | undefined>
  fetchUseOrgans?: () => Promise<API.CurrentUser | undefined>
}> => {
  const token = lockr.get<string>(Lockr.TOKEN)
  const fetchUserInfo = async () => {
    try {
      const data = await getCurrentUser()
      return data
    } catch (error) {
      lockr.remove(Lockr.TOKEN)
      history.push(signInPath)
      return undefined
    }
  }

  const fetchUseOrgans = async () => {
    if (token) {
      const data = await getUserOrganizes()
      let list = []
      //...
      return list
    }
    return undefined
  }

  if (token) {
    let currentUser = await fetchUserInfo()
    const organs = await fetchUseOrgans()
    currentUser = currentUser ? { ...currentUser, organizations: organs } : undefined
    const cacheOrganizationId = lockr.get<string | null>(lockr.CacheKey.organizationId)
    let organizationId = currentUser?.organizations?.[0]?.id

    return {
      fetchUserInfo,
      organizationId,
      token,
      currentUser,
      settings: defaultSettings,
    }
  }

  return {
    fetchUserInfo,
    settings: defaultSettings,
  }
}

web启动后,获取基础的个人信息,并且把数据存入单例中,可以在之后的全局访问。 ···

项目代码规范

Lint

禁止使用嵌套的三元表达式

👍推荐

var thing = foo ? bar : foobar
var thing
if (foo) {
  thing = bar
} else if (baz === qux) {
  thing = quxx
} else {
  thing = foobar
}

不推荐

var thing = foo ? bar : baz === qux ? quxx : foobar
foo ? (baz === qux ? quxx() : foobar()) : bar()

setState的用法

👍推荐

this.setState((prevState) => ({ value: prevState.value + 1 }))

不推荐

setState({ value: this.state.value + 1 })

如果没有使用 state 应该使用 react function

👍推荐

const Foo = (props) => {
  if (!props.foo) {
    return null
  }
  return <div>{props.foo}</div>
}

不推荐

class Foo extends React.Component {
  render() {
    if (!this.props.foo) {
      return null
    }
    return <div>{this.props.foo}</div>
  }
}

使用一致的返回,如果有返回值应该每个 render 都提供返回值

👍推荐

function doSomething(condition) {
  if (condition) {
    return true
  } else {
    return false
  }
}

不推荐

function doSomething(condition) {
  if (condition) {
    return true
  } else {
    return
  }
}

使用解构赋值从 props 中取值

👍推荐

const MyComponent = (props) => {
  const { id } = props;
  return <div id={id} />
}

不推荐

const MyComponent = (props) => {
  return <div id={props.id} />
}

每一行的代码不应超过80个字符

👍推荐

var foo = {
  bar: 'This is a bar.',
  baz: { qux: 'This is a qux' },
  easier: 'to read',
}

不推荐

var foo = { bar: 'This is a bar.', baz: { qux: 'This is a qux' }, difficult: 'to read' }

不要对函数的参数进行直接操作

👍推荐

var foo = {
  bar: 'This is a bar.',
  baz: { qux: 'This is a qux' },
  easier: 'to read',
}

不推荐

function foo(bar) {
  bar++
}

hooks在函数最顶层使用

👍推荐

useEffect(function persistForm() {
  if (name !== '') {
    localStorage.setItem('formData', name)
  }
})

不推荐

if (name !== '') {
  useEffect(function persistForm() {
    localStorage.setItem('formData', name)
  })
}

TypeScript

变量、函数命名驼峰标识,要见文知义,通俗易懂

👍推荐

classCustomer {
  private generationTimestamp: Date
  private modificationTimestamp: Date
  private recordId = '102'
}

不推荐

class DtaRcrd102 {
  private genymdhms: Date // 你能读出这个变量名么? 
  private modymdhms: Date
  private pszqint = '102'
}

参数、函数名更容易理解变量的含义

👍推荐

function between<T>(value: T, left: T, right: T) {
  return left <= value && value <= right
}

不推荐

function between<T>(a1: T, a2: T, a3: T) {
  return a2 <= a1 && a1 <= a3
}

常量的可易读性

👍推荐

const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000
setTimeout(restart, MILLISECONDS_IN_A_DAY)

不推荐

setTimeout(restart, 86400000)

使用自解构的变量名

👍推荐

declare const users:Map<string, User>;
for (const [id, user] of users) {
  // ...
}

不推荐

declare const users:Map<string, User>;
for (const keyValue of users) {
  // ...
}

命名不要重复表达,过于累赘

👍推荐

type Car = {
  make: string;
  model: string;
  color: string;
}
function print(car: Car): void {
  console.log(`${this.make} ${this.model} (${this.color})`);
}

不推荐

type Car = {
  carMake: string;
  carModel: string;
  carColor: string;
}
function print(car: Car): void {
  console.log(`${this.carMake} ${this.carModel} (${this.carColor})`);
}

使用默认参数

👍推荐

function loadPages(count: number = 10) {
  // ...
}

不推荐

function loadPages(count: number) {
  const loadCount = count !== undefined ? count : 10;
  // ...
}

参数越少越好,若有多个参数,请使用对象,采用解构的方式

  1. 当有人查看函数签名时,会立即清楚使用了哪些属性。
  2. 解构对传递给函数的参数对象做深拷贝,这可预防副作用。(注意:不会克隆从参数对象中解构的对象和数组)
  3. TypeScript 会对未使用的属性显示警告。

可以通过TypeScript的类型别名,进一步提高可读性 👍推荐

type MenuOptions = {title: string, body: string, buttonText: string, cancellable: boolean}

function createMenu(options: MenuOptions) {
  // ...
}

createMenu(
  {
    title: 'Foo',
    body: 'Bar',
    buttonText: 'Baz',
    cancellable: true
  }
)

不推荐

function createMenu(title: string, body: string, buttonText: string, cancellable: boolean) {
  // ...
}
createMenu('Foo', 'Bar', 'Baz', true)

使用Object.assign来设置默认对象

👍推荐

type MenuConfig = {title?: string, body?: string, buttonText?: string, cancellable?: boolean}

function createMenu(config: MenuConfig) {
  const menuConfig = Object.assign({
    title: 'Foo',
    body: 'Bar',
    buttonText: 'Baz',
    cancellable: true
  }, config);
}

createMenu({ body: 'MyBar' })

不推荐

type MenuConfig = {title?: string, body?: string, buttonText?: string, cancellable?: boolean}

function createMenu(config: MenuConfig) {
  config.title = config.title || 'Foo'
  config.body = config.body || 'Bar'
  config.buttonText = config.buttonText || 'Baz'
  config.cancellable = config.cancellable !== undefined ? config.cancellable : true
}

const menuConfig = {
  title: null,
  body: 'Bar',
  buttonText: null,
  cancellable: true
}

createMenu(menuConfig)

类型和接口,typeinterface

当需要联合或是多个属性时,可以使用类型;如果需要给某个类或几个类,添加同一个方法,只是实现方法不一样,可以使用接口 👍推荐

type EmailConfig {
  // ...
}
type DbConfig {
  // ...
}
type Config  = EmailConfig | DbConfig

interface Shape {
}
class Circle implements Shape {
  // ...
}
class Square implements Shape {
  // ...
}

不推荐

interface EmailConfig {
  // ...
}
interface DbConfig {
  // ...
}
interface Config {
  // ...
}
//...
type Shape {
  // ...
}

函数的粒子化,一个函数做一件事

👍推荐

function emailClients(clients: Client) {
  clients.filter(isActiveClient).forEach(email)
}

function isActiveClient(client: Client) {
  const clientRecord = database.lookup(client)
  return clientRecord.isActive()
}

不推荐

function emailClients(clients: Client) {
  clients.forEach((client) => {
    const clientRecord = database.lookup(client)
    if (clientRecord.isActive()) {
      email(client)
    }
  })
}

删除重复代码,以代码简洁为大道

👍推荐

class Developer {
  // ...
  getExtraDetails() {
    return {
      githubLink: this.githubLink,
    }
  }
}

class Manager {
  // ...
  getExtraDetails() {
    return {
      portfolio: this.portfolio,
    }
  }
}

function showEmployeeList(employee: Developer | Manager) {
  employee.forEach((employee) => {
    const expectedSalary = developer.calculateExpectedSalary()
    const experience = developer.getExperience()
    const extra = employee.getExtraDetails()
    
    const data = {
      expectedSalary,
      experience,
      extra,
    }
    render(data)
  })
}

不推荐

function showDeveloperList(developers: Developer[]) {
  developers.forEach((developer) => {
    const expectedSalary = developer.calculateExpectedSalary()
    const experience = developer.getExperience()
    const githubLink = developer.getGithubLink()
    const data = {
      expectedSalary,
      experience,
      githubLink
    }
    
    render(data);
  })
}
function showManagerList(managers: Manager[]) {
  managers.forEach((manager) => {
    const expectedSalary = manager.calculateExpectedSalary()
    const experience = manager.getExperience()
    const portfolio = manager.getMBAProjects()
    const data = {
      expectedSalary,
      experience,
      portfolio
    }
    
    render(data)
  })
}

当然,如果是两个不同的业务之间有一些重复的代码功能,为了维护代码块结构的简单性,可以考虑用独立代码的重复,这样在维护某个业务的时候,可以不用考虑另一个因改代码而带来的影响。

HTML

所有具有开始标签和结束标签的元素都要写上起止标签

👍推荐

<img src='' /> <br/> <input />

不推荐

<img src=''> <br> <input>

段落元素与标题元素只能嵌套内联元素

👍推荐

<h1> <span></span> </h1>

不推荐

<p>第一行<br/>第二行</p>

HTML标签名、类名、标签属性和大部分属性值统一用小写

👍推荐

<div class="demo"></div>

不推荐

<div class="DEMO"></div>

不需要为 CSS、JS 指定类型属性,HTML5 中默认已包含

👍推荐

<link rel="stylesheet" href="" >
<script src=""></script>

不推荐

<link rel="stylesheet" type="text/css" href="" >
<script type="text/javascript" src="" ></script>

单行注释

👍推荐

<!-- Comment Text -->
<div>...</div>

不推荐

<div>...</div><!-- Comment Text -->
<div><!-- Comment Text -->
    ...
</div>