转转前端开发规范的落地实践

avatar
公众号:转转技术

车同轨,书同文,行同伦 - 《礼记·中庸》

转转在早期团队较小的时候,没有太多规范,遇到问题用自己最擅长的方式去解决就行。随着公司发展,团队越来越大,因为没有统一规范而造成的问题也越来越多,例如

  • 组件库重复开发,不同小组组件库各有长短,但整体质量有所欠缺
  • 不同业务之间,项目无法直接复用,需要大量的开发改造
  • 新人上手成本高,每个项目都要深度了解其代码与配置,而不是了解完业务就行
  • 一些自动化提效的工具,因为业务项目没有规范,而无法开展
  • 等等。。。

基于这些问题,我们在去年上半年,制定了一个转转前端开发规范,经过这大半年的实践,达到了预期的效果。接下来我会具体介绍下转转的前端开发规范,希望能给大家带去一些帮助。之前我们也分享过转转的中后台规范建设,感兴趣的可以 戳这

规范包含哪些内容

我们通过一张规范架构图来直观的了解一下 规范架构 如上图,我们把规范分为五大类

  • 开发规范
  • 监控规范
  • 协作规范
  • 流程规范
  • 开源规范

其中每个大类下,又包含若干个小分类。本文中,我会详细的介绍下一些重点的规范以及其内容,对于非重点的,待后续我们把文档开源到 Github 上,大家可以自行查阅。

开发规范

本地存储规范

主要包括 localStorage 的使用规范,避免使用存储时会遇到的问题

基础库的使用

使用 localforage 来实现本地存储,该库会优先使用 IndexedDB 以扩大本地存储空间,并采用优雅降级方案,处理了异常情况。

存储溢出问题

localStorage 有存储限制,所以在存储空间用完的情况下,进行 setItem 操作就会报 QuotaExceededError 的错。

事实上,在某些浏览器(如 iOS 11Safari 浏览器)的无痕模式下,localStorage 也是无法读写的,进行 setItem 操作会报一样的 QuotaExceededError 错。

为尽可能避免上述情况,或者在出现时优雅处理,请遵循以下规范:

  • 设置过期时间。按如下方式对存储的数据进行封装,并在合适的时机清理过期数据。
const data = {
  data: Object,
  expiredTime: Date.now() + 7 * 24 * 60 * 60 * 1000
}
  • try...catch 包裹 setItem 操作
try {
  localStorage.setItem('my_data', JSON.stringify(data))
} catch (e) {
  // 错误处理
}

常见的错误处理方法有:清理过期数据并重试一次;转存至 sessionStorage、自定义的全局对象 myStorageVuex 等,但记得在读取时也依次读取。

存储的 key 覆盖问题

key 的命名需要符合如下规范 业务线名_项目名_自定义名称 如:

const PREFIX = 'platform_book_'

try {
  localStorage.setItem(PREFIX + 'my_data', JSON.stringify(data))
} catch (e) {
  // 错误处理
}

注释规范

合理的注释,可以提高代码的可读性和可维护性。

注释的原则

  • 如无必要,勿增注释,应当追求「代码自注释」
// bad
// 如果已经准备好数据,就渲染表格
if (data.success && data.result.length > 0) {
  renderTable(data);
}

// good
const isTableDataReady = data.success && data.result.length > 0;
if (isTableDataReady) {
  renderTable(data);
}
  • 如有必要,尽量详尽,需要注释的地方应该尽量详尽地去写

注释的规范

遵循统一的风格规范,如一定的空格、空行,以保证注释自身的可读性

  • 单行注释使用 //

注释行的上方需要有一个空行;注释内容和注释符之间需要有一个空格

function getType() {
  console.log('fetching type...');

  // set the default type to 'no type'
  const type = this.type || 'no type';
  return type;
}
  • 多行注释使用 /** ... */
/**
 * make() returns a new element
 * based on the passed-in tag name
 */
function make(tag) {
  // ...

  return element;
}
  • 使用特殊标记注释:如 TODO、FIXME、HACK 等
// TODO: 这里还需要做什么

// FIXME: 这里的实现有问题,以后需要优化

// HACK: 这里的处理是为了兼容低端安卓机的 bug
  • 文档类注释,如函数、类、文件、事件等,使用 jsdoc 规范
/**
 * Book类,代表一个书本.
 * @constructor
 * @param {string} title - 书本的标题.
 * @param {string} author - 书本的作者.
 */
function Book(title, author) {
  this.title = title;
  this.author = author;
}
  • 避免情绪性注释:如抱怨、歧视、搞怪
/**
 * 穷逼VIP(活动送的那种)
 * @param update
 * @return {boolean}
 */

浏览器兼容规范

团队应该根据的用户设备占比情况、应用类型、开发成本、浏览器市场统计数据等因素,来制定自己的浏览器兼容规范,转转的设备兼容规范如下

总体的机型兼用要求

安卓需要兼容安卓 4.0 以上,iOS 需要兼容 8.0 以上

# .browserlistrc
> 1%
last 3 versions
iOS >= 8
Android >= 4
Chrome >= 40

CSS 的兼容要求

CSS的兼容处理上,我们使用的是 postcssautoprefixer

// postcss.config.js
module.exports = {
  plugins: {
    autoprefixer: {},
    cssnano: {
      autoprefixer: false,
      zindex: false,
      discardComments: {
        removeAll: true
      },
      reduceIdents: false
    }
  }
}

js 的兼容要求

在处理 js 的兼容性时,我们使用 babel 抹平浏览器差异。

需要注意的是 proxy 是无法被降级处理的。需要慎重使用。

  • 业务项目中,通过配置 @babel/preset-envuseBuiltIns: 'entry', corejs: 3,来根据 .browserslistrc 引入 polyfill
// babel.config.js
module.exports = {
  presets: [
    ['@vue/app', {
      useBuiltIns: 'entry'
    }]
  ]
}
// main.js
import 'core-js/stable'
import 'regenerator-runtime/runtime'

Vue 项目为例,@vue/babel-preset-app 的配置会透传至 @babel/preset-env,并默认使用 core-js@3

  • 基础库中,通过配置 @babel/preset-envuseBuiltIns: 'usage', corejs: 3 来处理 js 兼容问题,减少项目体积
// babel.config.js
module.exports = {
  presets: [
    ['@babel/env', {
      useBuiltIns: 'usage',
      corejs: 3
    }]
  ],
  plugins: [
    ['@babel/transform-runtime'],
  ]
}

HTTP 缓存规范

转转的绝大多数项目都是单页应用,根据项目特点我们设置了如下的缓存规范

静态资源

静态资源域名 s1.zhuanstatic.com,是通过 cache-control 来做强制缓存,缓存时间是 30

cache-control: max-age=2592000

html文件

域名 m.zhuanzhuan.com 下的 html 文件,是通过 Etag 来做协商缓存

etag: W/"5dfb384c-4cd"

压缩

所有的资源都已开启的 gzip 压缩

通讯协议

默认使用的通信协议是 http2

编辑器规范

使用 Editorconfig 用来帮助你规范编辑器的的配置

Editorconfig 具体配置

配置文件 .editorconfig

root = true

[*]
indent_style = space                    # 输入的 tab 都用空格代替
indent_size = 2                         # 一个 tab 用 2 个空格代替
end_of_line = lf                        # 换行符使用 unix 的换行符 \n
charset = utf-8                         # 字符编码 utf-8
trim_trailing_whitespace = true         # 去掉每行末尾的空格
insert_final_newline = true             # 每个文件末尾都加一个空行

[*.md]
trim_trailing_whitespace = false        # .md 文件不去掉每行末尾的空格

Prettier规范

Prettier 是一个代码格式化工具。在转转的风格统一方案中,PrettierESlint 配合使用,但是 PrettierESlint 的规则是有冲突的,我们做了很多的努力,尽量保持配置规则的一致性。但是还是有冲突的地方。

业界有两种方案

  • 一种是以 Prettier 为主
  • 一种是以 ESlint 为主

转转选择以 ESlint 为主,所以在修复错误的时候,是 Prettier 先格式化一遍,ESlint 再修复一遍,最终检查以 ESlint 为准。所以如果遇到无法解决的错误,直接修改 ESlint 的配置就可以。

Prettier 具体配置文件

// prettier.config.js
module.exports = {
  // 在ES5中有效的结尾逗号(对象,数组等)
  trailingComma: 'none',
  // 不使用缩进符,而使用空格
  useTabs: false,
  // tab 用两个空格代替
  tabWidth: 2,
  // 仅在语法可能出现错误的时候才会添加分号
  semi: false,
  // 使用单引号
  singleQuote: true,
  // 在Vue文件中缩进脚本和样式标签。
  vueIndentScriptAndStyle: true,
  // 一行最多 100 字符
  printWidth: 100,
  // 对象的 key 仅在必要时用引号
  quoteProps: 'as-needed',
  // jsx 不使用单引号,而使用双引号
  jsxSingleQuote: false,
  // 大括号内的首尾需要空格
  bracketSpacing: true,
  // jsx 标签的反尖括号需要换行
  jsxBracketSameLine: false,
  // 箭头函数,只有一个参数的时候,也需要括号
  arrowParens: 'always',
  // 每个文件格式化的范围是文件的全部内容
  rangeStart: 0,
  rangeEnd: Infinity,
  // 不需要写文件开头的 @prettier
  requirePragma: false,
  // 不需要自动在文件开头插入 @prettier
  insertPragma: false,
  // 使用默认的折行标准
  proseWrap: 'preserve',
  // 根据显示样式决定 html 要不要折行
  htmlWhitespaceSensitivity: 'css',
  // 换行符使用 lf
  endOfLine: 'lf'
}

git-commit 规范

在团队中代码提交 git commit 会有各种各样的风格,甚至有些人根本没有 commit 规范的概念,所以在我们回头去查找在哪个版本出现问题的时候,就会非常尴尬,很难快速定位到问题。为了项目的规范化,代码提交规范就显得尤为重要!

我们通过提交规范插件 vue-cli-plugin-commitlint 进行代码的提交规范(该插件基于 conventional-changelog-angular 进行了修改/封装)。

vue-cli-plugin-commitlint 介绍

vue-cli-plugin-commitlint 是以 vue 插件的形式写的,可以执行 vue add commitlint 直接使用,如果不是 vue 的项目也可以根据下面的配置自行配置。

结合 commitizen commitlint conventional-changelog-cli husky conventional-changelog-angular,进行封装,一键安装,开箱即用的代码提交规范。

功能

  1. 自动检测 commit 是否规范,不规范不允许提交
  2. 自动提示 commit 填写格式。不怕忘记规范怎么写
  3. 集成 git add && git commit 不需要执行两个命令
  4. 自动生成 changelog

配置

脚手架生成的项目默认包含,分为默认使用以及个性化配置

如果是 vue-cli3 的项目

直接使用即可

vue add commitlint

如果不是 vue-cli3 的项目

npm i vue-cli-plugin-commitlint commitizen commitlint conventional-changelog-cli husky -D

package.json 中添加

{
  "scripts": {
    "log": "conventional-changelog --config ./node_modules/vue-cli-plugin-commitlint/lib/log -i CHANGELOG.md -s -r 0",
    "cz": "npm run log && git add . && git cz"
  },
  "husky": {
    "hooks": {
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
    }
  },
  "config": {
    "commitizen": {
      "path": "./node_modules/vue-cli-plugin-commitlint/lib/cz"
    }
  }
}

增加 commitlint.config.js 文件

module.exports = {
  extends: ['./node_modules/vue-cli-plugin-commitlint/lib/lint']
};

使用

npm run cz  # npm run log && git add . && git commit -m 'feat:(xxx): xxx'

npm run log # 生成 CHANGELOG
  1. 代码提交 npm run cz

commander

  1. 选择一个类型会自动询问

  2. (非必填)本次提交的改变所影响的范围

  3. (必填)写一个简短的变化描述

  4. (非必填)提供更详细的变更描述

  5. (非必填)是否存在不兼容变更?

  6. (非必填)此次变更是否影响某些打开的 issue

prompt

changelog 演示

changelog

规则

规范名描述
feat新增 feature
fix修复 bug
merge合并分支
docs仅仅修改了文档,比如 README, CHANGELOG, CONTRIBUTE 等等
chore改变构建流程、或者增加依赖库、工具等
perf优化相关,比如提升性能、体验
refactor代码重构,没有加新功能或者修复 bug
revert回滚到上一个版本
style仅仅修改了空格、格式缩进、逗号等等,不改变代码逻辑
test测试用例,包括单元测试、集成测试等

Vue项目规范

上面说了一些常规的开发规范,对于不同类型的项目 VueVue SSRReact小程序等等,都有着不同的规范,接下来我会以 Vue 项目为例来继续说说开发规范

ESlint 规范

ESLint 属于一种 QA 工具,是一个 ECMAScript/JavaScript 语法规则和代码风格的检查工具,可以用来保证写出语法正确、风格统一的代码。ESLint 旨在完全可配置,它的目标是提供一个插件化的 javascript 代码检测工具。转转的 ESlint 配置基于 Standard 规则的基础上做了特定的修改。

ESlint具体配置文件

每个规则对应的 0,1,2 分别表示 off, warning, error 三个错误级别

module.exports = {
  root: true,
  parserOptions: {
    ecmaVersion: 6, // 指定ECMAScript的版本为 6
    parser: "@typescript-eslint/parser", // 解析 ts
    sourceType: "module"
  },
  // 全局变量
  globals: {
    window: true,
    document: true,
    wx: true
  },
  // 兼容环境
  env: {
    browser: true
  },
  // 插件
  extends: ["plugin:vue/essential", "@vue/standard"],
  // 规则
  rules: {
    // 末尾不加分号,只有在有可能语法错误时才会加分号
    semi: 0,
    // 箭头函数需要有括号 (a) => {}
    "arrow-parens": 0,
    // 两个空格缩进, switch 语句中的 case 为 1 个空格
    indent: [
      "error",
      2,
      {
        SwitchCase: 1
      }
    ],
    // 关闭不允许回调未定义的变量
    "standard/no-callback-literal": 0,
    // 关闭副作用的 new
    "no-new": "off",
    // 关闭每行最大长度小于 80
    "max-len": 0,
    // 函数括号前面不加空格
    "space-before-function-paren": ["error", "never"],
    // 关闭要求 require() 出现在顶层模块作用域中
    "global-require": 0,
    // 关闭关闭类方法中必须使用this
    "class-methods-use-this": 0,
    // 关闭禁止对原生对象或只读的全局对象进行赋值
    "no-global-assign": 0,
    // 关闭禁止对关系运算符的左操作数使用否定操作符
    "no-unsafe-negation": 0,
    // 关闭禁止使用 console
    "no-console": 0,
    // 关闭禁止末尾空行
    "eol-last": 0,
    // 关闭强制在注释中 // 或 /* 使用一致的空格
    "spaced-comment": 0,
    // 关闭禁止对 function 的参数进行重新赋值
    "no-param-reassign": 0,
    // 强制使用一致的换行符风格 (linebreak-style)
    "linebreak-style": ["error", "unix"],
    // 关闭全等 === 校验
    "eqeqeq": "off",
    // 禁止使用拖尾逗号
    "comma-dangle": ["error", "never"],
    // 关闭强制使用骆驼拼写法命名约定
    "camelcase": 0
  }
};

ignore 规范

gitignore 规范

同步到远程仓库时予以忽略的文件及文件夹。基础配置如下,各项目也可以根据需求自行调整

.DS_Store
node_modules
/dist

# local env files
.env.local
.env.*.local

# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

注意:package-lock.json 不建议忽略,参见相关讨论package-lock.json 需要写进 .gitignore 吗? www.zhihu.com/question/26…

eslintignore 规范

eslint 校验时予以忽略的文件及文件夹。基础配置如下,各项目也可以根据需求自行调整

node_modules
dist/
test/
lib/
es/

Vue项目目录规范

├── dist                        // [生成] 打包目录
├── src                         // [必选] 开发目录
│    ├── views                  // [必选] 页面组件,不允许有其他类型组件混入
│    ├── components             // [必选] 业务组件必须写在这里
│    ├── libs                   // [可选] 公共库(一般用于对一些库的封装)
│    ├── utils                  // [可选] 工具库(用于一些函数方法之类的库)
│    ├── assets                 // [可选] 公共资源(被项目引用的经过webpack处理的资源)
│    ├── store                  // [可选] 数据存储 vuex
│    ├── route                  // [可选] 路由
│    ├── style                  // [可选] 公共样式
│    ├── App.vue                // [必选] 根组件
│    └── main.(js|ts)           // [必选] 入口文件
├── public                      // [必选] 不会被webpack编译的资源
│    ├── index.html             // [必选] 模板
│    └── logo.png               // [可选] 项目 logo
├── config                      // [可选] 配置目录
├── mock                        // [可选] mock 数据
├── test                        // [可选] 测试代码
├── docs                        // [可选] 文档
│── .gitignore                  // [必选] git 忽略的文件
│── .editorconfig               // [必选] 编译器配置
│── .npmignore                  // [可选] 如果是 npm 包是必选
│── jsconfig.json               // [可选] 用于 vscode 配置
├── README.md                   // [必选] 导读
├── package.json                // [必选] 大家都懂
└── ......                      // [可选] 一些工具的配置,如果 babel.config.js 等

components、views 具体职能划分

components 只写公共组件,页面附带组件写在 views

└── src
     ├── views
     │    └── home
     │         ├── index.vue
     │         ├── Banner.vue
     │         └── Card.vue
     ├── components
     │    ├── Swiper.vue
     │    └── Button.vue
     ├── store                 // 对于较大的项目,建议按业务模块拆分管理
     │    ├── index.js
     │    ├── home.js
     │    └── mine.js
     ├── route                 // 对于较大的项目,建议按业务模块拆分管理
     │    ├── index.js
     │    ├── home.js
     │    └── mine.js
     └── assets                // 重复使用的公共资源放在顶层 assets 文件,避免重复定义

监控规范

监控主要包含 性能监控、异常监控以及业务埋点,这里我主要介绍下性能和异常监控的规范

性能监控规范

转转所有的移动端面向C端用户的项目必须接入性能监控

项目接入

性能监控和埋点统一使用转转性能SDK,使用脚手架初始化的项目,默认会接入

性能查看

可以通过转转前端性能平台,查看你需要的性能数据

性能预警规则

  • 预警的频率是一天一次
  • 预警线
    • 项目首屏时间低于 1.5s或者 1.5s 开率低于 60%
    • 页面首屏时间低于 1.5s,计算的1天的平均值
  • 预警方式
    • 每天上午11点,通过企业微信发送
  • 白名单机制,过滤掉不需要报警的项目

异常监控规范

转转的前端错误监控体系基于 Sentry 建立,所有的项目必须接入异常监控。

上报策略的调整

使用 Sentry 过程中可能会遇到的一些问题,如下:

  • 收集信息混乱(所有错误信息混杂在一起);
    • 定位问题相对较慢;
    • 影响范围评估难;
    • 错误频率无法统计;
  • 部分监控缺失(不能全方位监控);
    • 小程序缺少监控;
    • 接口缺少监控;
    • 404请求缺少监控;
  • 预警邮件过于频繁(容易让开发人员接收疲劳);

主动上报

侵入项目,虽然前端实际工作中一直以对业务无侵入为研究方向。但在实际的业务中偶尔的侵入业务去做一些处理是很有必要的,给业务带来的收益也是可观的,我们能做的就是尽量少的侵入业务代码,导致污染。以下是我们对项目的改造策略:

页面改造

以上方案不止能有效捕获错误,区分错误级别,还能有效防止子组件错误影响整体页面渲染,导致白屏,简直一举两得。

接口监控

为什么要做接口监控?

  • 辅助后端错误监控及日志排查,提供更多有效信息;
  • 监控接口及服务异常状态,根据异常状态发现现有代码,服务器,及产品逻辑的漏洞;
  • 加强前端开发人员对于线上问题的重视,及对接口错误的重视,更好的融入业务;

多维度标签 & 辅助查错信息 & 自定义错误分组规则

优势:

  • 快速定位问题(1分钟内),迅速评估影响范围;
  • 更多分析问题需要的信息,辅助快速解决问题;
  • 规整错误列表,查看错误频率,优化代码,服务,及产品逻辑风险;

流程规范

包管理规范

该部分内容主要包含公共包的开发与发布相关规范

内部私服 cnpm

转转内部的包,使用内部搭建的 cnpm,发布包时,registry 源地址需要切换成转转的源地址

npm 包版本号规则

主版本号.次版本号.修订号

版本号递增规则如下:

  • 主版本号:当做了不兼容的 API 修改,
  • 次版本号:当做了向下兼容的功能性新增,
  • 修订号:当做了向下兼容的问题修正。先行版本号及版本编译信息可以加到“主版本号.次版本号.修订号”的后面,作为延伸。

当主版本号升级后,次版本号和修订号需要重置为 0,次版本号进行升级后,修订版本需要重置为 0

关于测试版本后缀命名:

  • alpha:初期内测版,可能比较不稳定, 如:1.0.0-alpha.1。
  • beta:中期内测,但会持续加入新的功能。
  • rc:Release Candidate,发行候选版本。RC 版不会再加入新的功能了,主要着重于除错,处在 beta 版之后,正式版之前

semver:semantic version 语义化版本

  • 固定版本:例如 1.0.1、1.2.1-beta.0 这样表示包的特定版本的字符串
  • 范围版本:是对满足特定规则的版本的一种表示,例如 1.0.3-2.3.4、1.x、^0.2、>1.4

语义化说明

用到的语义化字符有: ~、>、<、=、>=、<=、-、||、x、X、*

- '^2.1.1' // 2.x最新版本(主版本号锁定)
- '~2.1.1' // 2.1.x最新版本号(主和次版本号锁定)
- '>2.1'   // 高于2.1版本的最新版本
- '1.0.0 - 1.2.0' // 两个版本之间最新的版本(必须要有空格)
- '*'      // 最新的版本号
- '3.x'    // 对应x部分最新的版本号

npm install 默认安装 ^x.x.x 类型的版本,用于兼容大版本下最新的版本

到这里,转转前端开发的核心主要规范就介绍完了,对于不同类型项目会有不用的规范,大家可以根据业务实际情况,制定属于自己的团队的开发规范~

本月文章预告

预告下,接下来我们会陆续发布转转在 性能、多端SDK、移动端等基础架构和中台技术相关的实践与思考,欢迎大家关注公众号 “大转转FE”,期望与大家多多交流