转转前端开发规范的落地实践
车同轨,书同文,行同伦 - 《礼记·中庸》
转转在早期团队较小的时候,没有太多规范,遇到问题用自己最擅长的方式去解决就行。随着公司发展,团队越来越大,因为没有统一规范而造成的问题也越来越多,例如
- 组件库重复开发,不同小组组件库各有长短,但整体质量有所欠缺
- 不同业务之间,项目无法直接复用,需要大量的开发改造
- 新人上手成本高,每个项目都要深度了解其代码与配置,而不是了解完业务就行
- 一些自动化提效的工具,因为业务项目没有规范,而无法开展
- 等等。。。
基于这些问题,我们在去年上半年,制定了一个转转前端开发规范,经过这大半年的实践,达到了预期的效果。接下来我会具体介绍下转转的前端开发规范,希望能给大家带去一些帮助。之前我们也分享过转转的中后台规范建设,感兴趣的可以 戳这
规范包含哪些内容
我们通过一张规范架构图来直观的了解一下 如上图,我们把规范分为 五大类
- 开发规范
- 监控规范
- 协作规范
- 流程规范
- 开源规范
其中每个大类下,又包含若干个小分类。本文中,我会详细的介绍下一些重点的规范以及其内容,对于非重点的,待后续我们把文档开源到 Github
上,大家可以自行查阅。
开发规范
本地存储规范
主要包括 localStorage
的使用规范,避免使用存储时会遇到的问题
基础库的使用
使用 localforage
来实现本地存储,该库会优先使用 IndexedDB
以扩大本地存储空间,并采用优雅降级方案,处理了异常情况。
存储溢出问题
localStorage
有存储限制,所以在存储空间用完的情况下,进行 setItem
操作就会报 QuotaExceededError
的错。
事实上,在某些浏览器(如 iOS 11
的 Safari
浏览器)的无痕模式下, 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
、自定义的全局对象myStorage
、Vuex
等,但记得在读取时也依次读取。
存储的 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
的兼容处理上,我们使用的是 postcss
的 autoprefixer
// postcss.config.js
module.exports = {
plugins: {
autoprefixer: {},
cssnano: {
autoprefixer: false,
zindex: false,
discardComments: {
removeAll: true
},
reduceIdents: false
}
}
}
复制代码
js 的兼容要求
在处理 js
的兼容性时,我们使用 babel
抹平浏览器差异。
需要注意的是
proxy
是无法被降级处理的。需要慎重使用。
- 业务项目中,通过配置
@babel/preset-env
的useBuiltIns: '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-env
的useBuiltIns: '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
是一个代码格式化工具。在转转的风格统一方案中, Prettier
和 ESlint
配合使用,但是 Prettier
和 ESlint
的规则是有冲突的,我们做了很多的努力,尽量保持配置规则的一致性。但是还是有冲突的地方。
业界有两种方案
- 一种是以
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
,进行封装,一键安装,开箱即用的代码提交规范。
功能
- 自动检测
commit
是否规范,不规范不允许提交 - 自动提示
commit
填写格式。不怕忘记规范怎么写 - 集成
git add && git commit
不需要执行两个命令 - 自动生成
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
复制代码
- 代码提交 npm run cz
-
选择一个类型会自动询问
-
(非必填)本次提交的改变所影响的范围
-
(必填)写一个简短的变化描述
-
(非必填)提供更详细的变更描述
-
(非必填)是否存在不兼容变更?
-
(非必填)此次变更是否影响某些打开的 issue
changelog 演示
规则
规范名 | 描述 |
---|---|
feat | 新增 feature |
fix | 修复 bug |
merge | 合并分支 |
docs | 仅仅修改了文档,比如 README, CHANGELOG, CONTRIBUTE 等等 |
chore | 改变构建流程、或者增加依赖库、工具等 |
perf | 优化相关,比如提升性能、体验 |
refactor | 代码重构,没有加新功能或者修复 bug |
revert | 回滚到上一个版本 |
style | 仅仅修改了空格、格式缩进、逗号等等,不改变代码逻辑 |
test | 测试用例,包括单元测试、集成测试等 |
Vue项目规范
上面说了一些常规的开发规范,对于不同类型的项目 Vue
、 Vue SSR
、 React
、 小程序
等等,都有着不同的规范,接下来我会以 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”,期望与大家多多交流