高标准的开发规范是一个开发者的最低要求,真正的开发规范绝非一篇文章能解决。高标准未必出好产品,但低标准一定会降低团队开发效率。优秀的团队绝不允许特立独行的队员,只有继续完善共同的开发规范才能扬帆起航。

能愿动词

  • 必须(Must) - 只能这样子做,请无条件遵循,没有别的选项;
  • 绝不(Must Not)- 严令禁止,在任何情况下都不能这样做;
  • 应该(Should) - 强烈建议这样做,但是不强求;
  • 不应该(Should Not) - 强烈建议不这样做,但是不强求;
  • 可以(May) - 选择性高一点,必须团队内保持统一;

开发环境

  • 统一使用域名 .vm 作为后缀;
  • 安装开发专用扩展包时 必须 使用 --dev 参数;
  • 开发专用的 provider 绝不 在 config/app.php 里面注册,必须 在 app/Providers/AppServiceProvider.php 文件中判断环境注册:

配置信息

  • 因环境差异的配置 必须 存储在 .env 和 config/app.php 文件中,然后使用 config() 函数来读取;
  • config() 是配置信息,env() 只是用来区分不同环境;

帮助方法

  • 必须 把所有的『自定义辅助函数』存放于 app/Helpers/functions.php 中,或区分文件存储,然后用 composer autoload files 自动加载;

代码风格

  • 代码风格 必须 严格遵循 PSR-2 规范;
<?php
$header = <<<EOF
return PhpCsFixer\Config::create()
    ->setRiskyAllowed(true)
    ->setRules(array(
        '@Symfony' => true,
        'header_comment' => array('header' => $header),
        'array_syntax' => array('syntax' => 'short'),
        'ordered_imports' => true,
        'no_useless_else' => true,
        'no_useless_return' => true,
        'php_unit_construct' => true,
        'php_unit_strict' => true,
        'yoda_style' => false,
    ))
    ->setFinder(
        PhpCsFixer\Finder::create()
            ->exclude('vendor')
            ->in(__DIR__)
    )
;
  • 绝不 在路由配置文件里书写『闭包路由』或者其他业务逻辑代码,因为一旦使用将无法使用路由缓存;
  • 必须 优先使用 Restful 路由,配合资源控制器使用,超出 Restful 路由的,应该 模仿上图的方式来定义路由,参考
  • 使用 resource 方法时,如果仅使用到部分路由,必须 使用 only 列出所有可用路由,绝不 使用 except;
  • 资源路由路由 URI 必须 使用复数形式;
  • 路由 绝不 使用驼峰;
  • 应该 使用全局路由器参数限制,必须 在 RouteServiceProvider 文件的 boot 方法里定义模式;
public function boot(Router $router)
{
    $router->pattern('id', '[0-9]+');

    parent::boot();
}
  • 必须 使用『资源前缀』作为命名规范,如下的 store.card,资源前缀的值 必须 是单数 user.;
$api->get('stores/{sid}/cards', ['uses' => 'StoreCardController@index', 'as' => 'store.card']);
  • 数据模型类名 必须 为 单数, 如:AppModelsUsers;
  • 数据库表名字 必须 为 复数;
  • 应该 利用 Trait 来精简逻辑代码量,提高可读性;
  • 所有的全局作用域都 必须 统一使用 闭包定义全局作用域;
  • 控制器 必须 使用 单数 形式;
  • 不应该 为 方法 过多注释,这要求方法取名要足够合理;
  • 应该 为一些复杂的逻辑代码块书写注释,主要介绍产品逻辑 - 为什么要这么做;
  • 绝不 遗留 死方法,就是没有用到的方法,控制器里的所有方法,都应该被使用到,否则应该删除,而必须不是注释;
  • 所有的自定义命令,都 必须 有项目的命名空间;
  • TODO 必须写 双冒号 加 Git username,如 TODO::bigface;
  • 所有可清理缓存 必须 使用 Cache Facade。不可清理缓存 必须 用 Redis;
  • 必须 根据需求清晰定义每个模型的 $fillable 或 $guarded 属性
  • Try 宁可不用,不应该乱用;

MySQL 规范

  • 库名、表名、字段名 必须 使用小写字母,绝不 超过32个字符,须见名知意
  • 应该 使用InnoDB存储引擎,除非读写比率 < 1%, 才考虑使用 MYISAM 存储引擎;其 他存储引擎请在 DBA 的建议下使用。
  • 必须 使用 utf8mb4 字符集;
  • 必须要有主键,主键尽量用自增字段类型,推荐类型为 INT 或者 BIGINT 类型;
  • 单表 不应该 有太多字段,应该 在 20 以内;
  • 绝不 在数据库中存储图片,文件等大的二进制数据;
  • 应该 选择符合存储需要的最小的数据类型;
  • 非负型的数据 必须 使用无符号整型来存储;
  • 所有需要精确到时间(时分秒)的字段 应该 使用 DATETIME , created_at, updated_at, deleted_at 用 TIMESTAMP;
  • 禁止使用存储过程、视图、触发器、Event;

高并发大数据的互联网业务,架构设计思路是“解放数据库CPU,将计算转移到服务层”,并发量大的情况下,这些功能很可能将数据库拖死,业务逻辑放到服务层具备更好的扩展性,能够轻易实现“增机器就加性能”。数据库擅长存储与索引,CPU计算还是上移。

  • 禁止使用ENUM,可使用TINYINT代替;
  • 所有 是/否 字段 必须 用 TINYINT,值为0/1,禁止字符串,存汉字;
  • 所有超过 2 个值的状态字段 应该 用 SMALLINT,如完成状态值为 100,值以 10 步进;
  • 绝不 使用大事务操作,容易阻塞;
  • 非唯一索引 必须 以 idx_字段1_字段2 命名,唯一索引 必须 以 uniq_字段1_字段2 命名;
  • 前期建表 不应该 建索引;
  • 建立组合索引,必须 把区分度高的字段放在前面;
  • 单表索引 不应该 超过5个;
  • 绝不 使用数据类型的隐式转换,会导致索引失效;
  • 绝不 在更新十分频繁、区分度不高的字段上建立索引;

更新会变更 B+ 树,更新频繁的字段建立索引会大大降低数据库性能;

『性别』这种区分度不大的属性,建立索引是没有什么意义的,不能有效过滤数据,性能与全表扫描类似

API 返回值规范

status codeerror message
2XX请求正常处理并返回
3XX重定向,请求的资源位置发生变化
4XX客户端发送的请求有错误
5XX服务器端错误

附录

  • 我的 vscode 配置,gist e2784c213e0e477ad585b5cd720f9166
  • 我的 eslint 配置,可针对团队内部协议调整,比如单引号、末尾分号
module.exports = {
  root: true,
  parserOptions: {
    parser: 'babel-eslint',
    sourceType: 'module'
  },
  env: {
    browser: true,
    node: true,
    es6: true,
  },
  extends: ['plugin:vue/recommended', 'eslint:recommended'],

  // add your custom rules here
  //it is base on https://github.com/vuejs/eslint-config-vue
  rules: {
    "vue/max-attributes-per-line": [2, {
      "singleline": 10,
      "multiline": {
        "max": 1,
        "allowFirstLine": false
      }
    }],
    "vue/singleline-html-element-content-newline": "off",
    "vue/multiline-html-element-content-newline":"off",
    // vue标签不加空格 eg.<city-address/>
    // "vue/html-closing-bracket-spacing": "off",
    "vue/html-self-closing": "off",
    "vue/name-property-casing": ["error", "PascalCase"],
    "vue/no-v-html": "off",
    'accessor-pairs': 2,
    'arrow-spacing': [2, {
      'before': true,
      'after': true
    }],
    'block-spacing': [2, 'always'],
    'brace-style': [2, '1tbs', {
      'allowSingleLine': true
    }],
    'camelcase': [0, {
      'properties': 'always'
    }],
    'comma-dangle': [2, 'never'],
    'comma-spacing': [2, {
      'before': false,
      'after': true
    }],
    'comma-style': [2, 'last'],
    'constructor-super': 2,
    'curly': [2, 'multi-line'],
    'dot-location': [2, 'property'],
    'eol-last': 2,
    'eqeqeq': ["error", "always", {"null": "ignore"}],
    'generator-star-spacing': [2, {
      'before': true,
      'after': true
    }],
    'handle-callback-err': [2, '^(err|error)$'],
    'indent': [2, 2, {
      'SwitchCase': 1
    }],
    'jsx-quotes': [2, 'prefer-single'],
    'key-spacing': [2, {
      'beforeColon': false,
      'afterColon': true
    }],
    'keyword-spacing': [2, {
      'before': true,
      'after': true
    }],
    'new-cap': [2, {
      'newIsCap': true,
      'capIsNew': false
    }],
    'new-parens': 2,
    'no-array-constructor': 2,
    'no-caller': 2,
    'no-console': 'off',
    'no-class-assign': 2,
    'no-cond-assign': 2,
    'no-const-assign': 2,
    'no-control-regex': 0,
    'no-delete-var': 2,
    'no-dupe-args': 2,
    'no-dupe-class-members': 2,
    'no-dupe-keys': 2,
    'no-duplicate-case': 2,
    'no-empty-character-class': 2,
    'no-empty-pattern': 2,
    'no-eval': 2,
    'no-ex-assign': 2,
    'no-extend-native': 2,
    'no-extra-bind': 2,
    'no-extra-boolean-cast': 2,
    'no-extra-parens': [2, 'functions'],
    'no-fallthrough': 2,
    'no-floating-decimal': 2,
    'no-func-assign': 2,
    'no-implied-eval': 2,
    'no-inner-declarations': [2, 'functions'],
    'no-invalid-regexp': 2,
    'no-irregular-whitespace': 2,
    'no-iterator': 2,
    'no-label-var': 2,
    'no-labels': [2, {
      'allowLoop': false,
      'allowSwitch': false
    }],
    'no-lone-blocks': 2,
    'no-mixed-spaces-and-tabs': 2,
    'no-multi-spaces': 2,
    'no-multi-str': 2,
    'no-multiple-empty-lines': [2, {
      'max': 1
    }],
    'no-native-reassign': 2,
    'no-negated-in-lhs': 2,
    'no-new-object': 2,
    'no-new-require': 2,
    'no-new-symbol': 2,
    'no-new-wrappers': 2,
    'no-obj-calls': 2,
    'no-octal': 2,
    'no-octal-escape': 2,
    'no-path-concat': 2,
    'no-proto': 2,
    'no-redeclare': 2,
    'no-regex-spaces': 2,
    'no-return-assign': [2, 'except-parens'],
    'no-self-assign': 2,
    'no-self-compare': 2,
    'no-sequences': 2,
    'no-shadow-restricted-names': 2,
    'no-spaced-func': 2,
    'no-sparse-arrays': 2,
    'no-this-before-super': 2,
    'no-throw-literal': 2,
    'no-trailing-spaces': 2,
    'no-undef': 2,
    'no-undef-init': 2,
    'no-unexpected-multiline': 2,
    'no-unmodified-loop-condition': 2,
    'no-unneeded-ternary': [2, {
      'defaultAssignment': false
    }],
    'no-unreachable': 2,
    'no-unsafe-finally': 2,
    'no-unused-vars': [2, {
      'vars': 'all',
      'args': 'none'
    }],
    'no-useless-call': 2,
    'no-useless-computed-key': 2,
    'no-useless-constructor': 2,
    'no-useless-escape': 0,
    'no-whitespace-before-property': 2,
    'no-with': 2,
    'one-var': [2, {
      'initialized': 'never'
    }],
    'operator-linebreak': [2, 'after', {
      'overrides': {
        '?': 'before',
        ':': 'before'
      }
    }],
    'padded-blocks': [2, 'never'],
    'quotes': [1, "single"],
    // 强制js末尾加分号,忽略单行加分号
    'semi': ["error", "always", {
      "omitLastInOneLineBlock": true
    }],
    'semi-spacing': [2, {
      'before': false,
      'after': true
    }],
    'space-before-blocks': [2, 'always'],
    'space-before-function-paren': [2, 'never'],
    'space-in-parens': [2, 'never'],
    'space-infix-ops': 2,
    'space-unary-ops': [2, {
      'words': true,
      'nonwords': false
    }],
    'spaced-comment': [2, 'always', {
      'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
    }],
    'template-curly-spacing': [2, 'never'],
    'use-isnan': 2,
    'valid-typeof': 2,
    'wrap-iife': [2, 'any'],
    'yield-star-spacing': [2, 'both'],
    'yoda': [2, 'never'],
    'prefer-const': 2,
    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
    'object-curly-spacing': [2, 'always', {
      objectsInObjects: false
    }],
    'array-bracket-spacing': [2, 'never']
  }
}

参考