给公司整理的前端规范


一、目录结构规范

普遍地,项目需要有以下几项文件夹或文件:

  • src - 源代码存放的文件夹
  • package.json - 包描述文件
  • yarn.lock - 包管理器 yarn 自动生成的版本锁定文件
  • README.md - 新手上手指南,针对项目的启动命令、环境配置,依赖安装难点等进行描述

其它的目录结构,按照生成项目时使用的脚手架自行决定,推荐的脚手架:

框架脚手架创建命令官网
Reactcreate-react-appyarn create react-app my-apphttps://create-react-app.dev/
Vue2vue-cli-servicevue create my-apphttps://cli.vuejs.org/
Vue3viteyarn create vite my-apphttps://vitejs.dev/

src 目录下,如果脚手架自带的子目录不够使用,可以自己开辟,常见的子目录名称及用法:

  • utils - 存放工具类函数集合的文件夹
  • emnus - 存放枚举型变量集合的文件夹
  • assets / static - 存放图片、音视频等静态资源的文件夹
  • components - 存放全局公共组件的文件夹
  • views / pages - 存放页面级别的业务组件的文件夹

注意:脚手架生成项目完毕,不建议使用 eject 等命令将原始构建脚本暴露,因为这样的操作是不可逆的,不方便将来项目对脚手架的升级。如需对 webpack 等底层工具进行自定义配置,应该采用脚手架建议的方式去拓展或重写,参考各个脚手架的官方文档。

环境配置,尽量使用 .env 文件家族来定义因环境不同而产生的变量,比如 API 访问地址等。

不同环境下的文件命名:

  • development - 开发环境,可用 .env.development.env.development.local 等文件配置环境变量,其中后者仅当前开发者保有,不同步到 Git 代码仓库
  • test - 测试环境,可用 .env.test.env.test.local 等文件配置环境变量
  • production - 生产环境,可用 .env.production.env.production.local 等文件配置环境变量

二、文件命名规范

JS:

  • 单个单词小写,多个单词小驼峰命名法(lowerCamelCase)命名,如:routes.jstaskMap.js

React:

  • 扩展名用 .jsx 作为组件扩展名,但框架如 Next.js 不支持除外。
  • 组件命名,采用大驼峰命名法(UpperCamelCase)命名,也可以将文件夹名作为组件名,根组件用 index.jsx 命名,如 TodoList.jsxTodoList/index.jsx
  • 组件名应该倾向于完整单词而不是缩写,这一点跟 Vue 一样。

Vue:

  • 组件名应该始终是多个单词的,这样做可以避免跟现有的以及未来的 HTML 元素相冲突。
  • 单文件组件的命名,应采用大驼峰命名法(UpperCamelCase)命名,如 TodoList.vue
  • 组件名应该倾向于完整单词而不是缩写,比如 UserProfileOptions.vue 不要缩短成 UProfOpts.vue

三、变量命名规范

JS:

  • 变量声明,使用 constlet,不要使用 var
  • 方法名、参数名、成员变量、局部变量命名,采用小写开头的单词或小驼峰命名法(lowerCamelCase)命名,其中 Boolean 类型的变量一般要以 ishas 开头,方法名采用 动词 + 名词 的动宾形式,如 toNumonClickhandleClick,不要采用动词修饰名词的形式,如 clickHandler
  • 类名和构造函数名,采用大写开头的单词或大驼峰命名法(UpperCamelCase)命名
  • 不要使用 that_this 等指代 this 变量,而应该寻求使用箭头函数等避免改变 this 指向的途径解决。

React:

  • 组件 props 名,采用小驼峰命名法(lowerCamelCase)命名,如 maxLinesbeginTime

Vue:

  • 组件 props 名,定义 props 时采用小驼峰命名法(lowerCamelCase),给组件设置 props 时采用串式命名法(kebab-case),如:props: { greetingText: String } 定义,<some-component greating-text="hello"></some-component> 使用。
  • 始终使用 :@# 等指令缩写代替 v-bind:v-on:v-slot:
  • 组件名,引入时保持和文件名一致,如 import MyComponent from './MyComponent.vue',使用时最好采用成对的串式命名法(kebab-case)使用,如 <my-component></my-component>

CSS:

  • 类名如果是多个单词,采用串式命名法(kebab-case),如 .card-body

四、版本命名规范

前端项目如果有版本发布需求,命名应当参照 npm 官方的命名规范——semver,即三位数字加上短横线再加自定义部分:<数字>.<数字>.<数字>-<自定义>.<自定义2>,按照项目的迭代进度,分为开发版、测试版、线上预览版、试验版和正式版,我们分别采用不同的标识去加以区分,其中正式版不需要标识:

版本标识示例说明
开发版dev1.0.0-dev.12该版本用于开发人员打包自测,验证当次的开发迭代是否会对打包流程造成影响
测试版alpha1.0.0-alpha.20该版本用于交付测试人员,每次构建都应该升级最末端的小版本号,如 alpha.20 -> alpha.21
线上预览版beta1.0.0-beta.4该版本使用独立的版本标识,但连接线上的服务器接口,用于测试人员回归测试
试验版canary1.0.0-canary.1该版本只对少数终端用户可用,如果新的功能或新的设计没有被很好地接受,那么很容易回滚。
正式版1.0.0该版本可以交付给最终用户

五、代码格式规范

统一使用 Prettier,这是一个大部分代码编辑器都支持的格式化工具。

  • 在对象,数组括号与文字之间加空格,如 { foo: bar }
  • 箭头函数参数只有一个时是否省略小括号,如 (x) => {} 省略为 x => {},不要省略
  • 超过 120 列换行
  • 行末不留分号
  • 行结尾模式 LF(Linux 模式)
  • 使用单引号
  • Tab 键宽度设为 2
  • 在对象或数组最后一个元素后面加逗号
  • 不读取 .editorConfig 作为格式化依据
  • 使用空格代替制表符缩进
  • JSX 把 > 单独放一行
  • JSX 中使用单引号代替双引号
  • Vue <script><style> 标签不要整体缩进

想要做到以上这些,只需要往项目根目录中加入 .prettierrc 文件,并且确保编辑器读取文件并且生效,如果使用的是 VS Code 编辑器,则会自动生效。文件内容如下:

{
  "printWidth": 120,
  "arrowParens": "always",
  "bracketSpacing": true,
  "semi": false,
  "endOfLine": "lf",
  "jsxBracketSameLine": true,
  "jsxSingleQuote": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5",
  "useEditorConfig": false,
  "useTabs": false,
  "vueIndentScriptAndStyle": false,
  "withNodeModules": false
}

之后就可以使用编辑器的快捷键进行格式化了。


六、编程规范

编程规范一般指的是模式、语法上的规范,目前比较统一可靠的工具是 ESLint

ESLint 不限定于特定的编码规则,用户也可以自由地启用或禁用各个编码规则,它是一个可高度定制工具。用户可以通过定义原始规则来灵活地设置自己的编码标准。

前面提到的脚手架,其实在初始化的时候就可以选择自动安装 eslint 依赖,并且生成配置文件;如果初始化项目时并未选择 eslint,可以使用命令 npx eslint --init 根据提示生成配置文件,并且向 package.json 添加用户命令:

"scripts": {
  "lint": "eslint src"
}

之后每次使用 yarn eslint 进行代码校验。

常见的配置文件格式如下:

// .eslintrc.js

module.exports = {
  env: {
    browser: true,
    es2021: true,
  },
  extends: ['eslint:recommended', 'plugin:vue/essential'],
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module',
  },
  plugins: ['vue'],
  rules: {},
}

其中,extends 字段表示继承自哪一套或哪几套规则合集,我们一般使用脚手架设定好的就可以,后期公司可以出一套自己的规则,届时本文档会做相应的修改。

ESLint 是由一系列的规则组合起来的,有些规则是代码格式规则,这一点和 Prettier 重合,注意不要冲突,其它规则则是模式、语法上的。全部的规则参考其官网

ESLint 推荐的规则中,值得强调的有:

序号规则说明
1object-shorthand用对象方法简写
2prefer-destructuring尽量用对象的解构赋值来获取和使用对象某个或多个属性值
3prefer-template尽量使用模板字符串而不是字符串拼接
4no-loop-func不要在非函数块内声明函数
5prefer-rest-params不要使用 arguments,用收集参数语法 ... 代替
6default-param-last把默认参数赋值放在最后
7no-param-reassign不要修改参数或对参数重新赋值
8arrow-parens在箭头函数参数两头,总是使用小括号包裹住参数
9class-methods-use-this类方法应该使用 this
10import/firstimport 放在其他所有语句之前
11import/extensionsimport 文件不用包含扩展名
12one-var为每个变量声明都用一个 constlet
13eqeqeq===!== 而不是 ==!=
14no-else-return最后的 return 不需要 else 包裹
15radixparseInt 转换 string 应总是带上基数
16no-new-wrappers不要使用 new 操作符进行类型转化

完整的例子:

1. object-shorthand

用对象方法简写。

// bad
const atom = {
  value: 1,

  // 键 + 匿名方法
  addValue: function (value) {
    return atom.value + value
  },
}

// good
const atom = {
  value: 1,

  // 对象的方法
  addValue(value) {
    return atom.value + value
  },
}
2. prefer-destructuring

尽量用对象的解构赋值来获取和使用对象某个或多个属性值。

// bad
function getFullName(user) {
  const firstName = user.firstName
  const lastName = user.lastName

  return `${firstName} ${lastName}`
}

// good
function getFullName(user) {
  const { firstName, lastName } = user
  return `${firstName} ${lastName}`
}

// best
function getFullName({ firstName, lastName }) {
  return `${firstName} ${lastName}`
}
3. prefer-template

尽量使用模板字符串而不是字符串拼接。

// bad
function sayHi(name) {
  return 'How are you, ' + name + '?'
}

// bad
function sayHi(name) {
  return ['How are you, ', name, '?'].join()
}

// bad
function sayHi(name) {
  return `How are you, ${name}?`
}

// good
function sayHi(name) {
  return `How are you, ${name}?`
}
4. no-loop-func

不要在非函数块(ifwhile 等)内声明函数,浏览器会允许你这样做,但不同浏览器的解析方式不同。

// bad
if (currentUser) {
  function test() {
    console.log('Nope.')
  }
}

// good
let test
if (currentUser) {
  test = () => {
    console.log('Yup.')
  }
}
5. prefer-rest-params

不要使用 arguments,用收集参数语法 ... 代替。

// bad
function concatenateAll() {
  const args = Array.prototype.slice.call(arguments)
  return args.join('')
}

// good
function concatenateAll(...args) {
  return args.join('')
}
6. default-param-last

把默认参数赋值放在最后。

// bad
function handleThings(opts = {}, name) {
  // ...
}

// good
function handleThings(name, opts = {}) {
  // ...
}
7. no-param-reassign

不要修改参数或对参数重新赋值。

// bad
function f1(obj) {
  obj.key = 1
}

// good
function f2(obj) {
  const key = Object.prototype.hasOwnProperty.call(obj, 'key') ? obj.key : 1
}
8. arrow-parens

在箭头函数参数两头,总是使用小括号包裹住参数,这样做使代码更清晰且一致。

// bad
;[1, 2, 3]
  .map((x) => {
    const y = x + 1
    return x * y
  })

  [
    // good
    (1, 2, 3)
  ].map((x) => {
    const y = x + 1
    return x * y
  })
9. class-methods-use-this

除非外部库或框架需要使用特定的非静态方法,否则类方法应该使用 this 或被写成静态方法。

// bad
class Foo {
  bar() {
    console.log('bar')
  }
}

// good - this 被使用了
class Foo {
  bar() {
    console.log(this.bar)
  }
}
10. import/first

import 放在其他所有语句之前。

// bad
import foo from 'foo'
foo.init()

import bar from 'bar'

// good
import foo from 'foo'
import bar from 'bar'

foo.init()
11. import/extensions

import 文件不用包含扩展名。

// bad
import foo from './foo.js'
import bar from './bar.jsx'
import baz from './baz/index.jsx'

// good
import foo from './foo'
import bar from './bar'
import baz from './baz'
12. one-var

为每个变量声明都用一个 constlet

// bad
const items = getItems(),
  goSportsTeam = true,
  dragonball = 'z'

// good
const items = getItems()
const goSportsTeam = true
const dragonball = 'z'
13. eqeqeq

===!== 而不是 ==!=

14. no-else-return

如果 if 语句中每个判断分支总是需要用 return 返回,最后的 return 就不需要 else 包裹了。同时,开发者书写代码时,建议先写最后一行的 return,再写 if 部分,这样可以保证函数有保底的返回语句。

// bad
function foo() {
  if (x) {
    return x
  } else {
    return y
  }
}

// good
function foo() {
  if (x) {
    return x
  }
  return y
}
15. radix

Number 做类型转换,parseInt 转换 string 应总是带上基数。

// bad
const val = parseInt(inputValue)

// good
const val = Number(inputValue)

// good
const val = parseInt(inputValue, 10)
16. no-new-wrappers

不要使用 new 操作符进行类型转化。

const reviewScore = 9
const age = 0

// bad
const totalScore = new String(reviewScore) // typeof totalScore is "object" not "string"
const hasAge = new Boolean(age)

// good
const totalScore = String(this.reviewScore)
const hasAge = Boolean(age)