前端规范主要是为了让代码能有更好的可读性以及优雅性。不要为了规范而去规范,使用某些花里胡哨的写法,本质上是为了代码的维护性更强一些,所以才会制定规范来约束。主要针对vue2.0~ 其实我觉得vue2.0的options的写法是比较清晰的,在vue3.0去除了这些写法就需要一定的规范去让各个模块划分清晰一些,往react靠了,未来的趋势可能就是函数化编程,所以对于把控可维护性这块也需要去跟进,也可以配合ts以及eslint和各种插件去配合使用。
主要针对以下方面推荐一些个人觉得比较好的写法以及规范等,现在放在这里一方面是方便自己阅读,还是就是分享给需要的人去参考一下,有一些是网上搜集的但是忘记出处了我会尽量标记出处,若觉得有问题或可改进的欢迎评论留言哦~
推荐-Vue-Router写法
使用路由懒加载,实现方式是结合Vue异步组件和Webpack代码分割功能。
优点:
减小包体积,提高加载速度
当页面>20个时,组件定义需要拉到编辑器顶部才知道具体路径
bad
import IntentionList from '@/pages/intention/list'
import Variable from '@/pages/variable'
{
path: '/intention/list',
name: 'ilist',
component: IntentionList
},
{
path: '/variable',
name: 'variable',
component: Variable
}
good
{
path: '/intention/list',
name: 'ilist',
component: () => import('@/pages/intention/list')
},
{
path: '/variable',
name: 'variable',
component: () => import('@/pages/variable')
}
import语法需要Babel添加syntax-dynamic-import插件。最新当vue-cli 3.0中默认添加该特性,不需要额外引用。另外,合理控制异步模块的数量。
推荐-Vue项目目录结构
目录结构保持一致,使得多人合作容易理解与管理,提高工作效率
简要说明
main.js主入口,router.js路由划分
plugins 自己或第三方插件,包括但不限于components、directives、filters、third lib
pages 所有路由页面。原则:轻page,重component
components 所有组件。包括原子组件、业务公用组件、页面独有组件
server api引入入口
assets sass、图片资源入口,不常修改数据
utils 工具文件夹
store 标准vuex格式,非必须
详细说明
project
└───src
│ │ app.vue // 主页面
│ │ main.js // 主入口
| | router.js // 所有路由
│ │
│ |____assets // css、image、svg等资源
│ | |____css // 所有sass资源
| | | | reset.scss // 兼容各浏览器
| | | | global.scss // 全局css
| | | | variable.scss // sass变量和function等
│ | |____img // image图标库
| | |____svg // svg图标库
| |
| |____components // 组件
│ | |____common // common自注册组件
│ | |____base // 原子组件(如果是引入第三方,该文件夹可省略)
│ | | ... // 业务公用组件
│ | |____entity // entity页面组件
│ | |____about // about页面组件
| |
| |____pages // UI层(原则:轻page,重component)
| | |____entity
| | | | list.vue // 列表页
| | | | create.vue // 新增页
| | | | edit.vue // 修改页
| | | main.vue
| |
| |____plugins // 自己或第三方插件
| | | index.js // 插件入口文件
| | | directives.js // 所有Vue指令
| | | filters.js // 所有Vue过滤
| |
| |____server // 接口层
| | | index.js // 所有接口
| | | http.js // axios二次封装
| |
| |____store // vuex数据
| | | index.js
| |
| |____utils // 工具层
| | | config.js// 配置文件,包括常量配置
|
└───public // 公用文件,不经过webpack处理
│ │ favicon.ico
│ │ index.html
│ vue.config.js // vue-cli3主配置
│ babel.config.js// babel配置
│ .eslintrc.js // eslint配置
│ .prettierrc.js // perttier配置
│ package.json // npm配置
│ README.md // 项目说明
推荐-Vue实例选项顺序
在Vue中,export default对象中有很多约定的API Key。每个人的顺序排放都可能不一致,但保持统一的代码风格有助于团队成员多人协作。
Vue官网文档中也有推荐顺序,文档中对选项顺序做了许多分类。但从工程项目角度思考,需要更加精简以及合理的排序。推荐如下规则进行排序:
Vue扩展: extends, mixins, components
Vue数据: props, model, data, computed, watch
Vue资源: filters, directives
Vue生命周期: created, mounted, destroy...
Vue方法: methods
以下推荐顺序:
export default {
name: '',
/*1. Vue扩展 */
extends: '', // extends和mixins都扩展逻辑,需要重点放前面
mixins: [],
components: {},
/* 2. Vue数据 */
props: {},
model: { prop: '', event: '' }, // model 会使用到 props
data () {
return {}
},
computed: {},
watch:{}, // watch 监控的是 props 和 data,有必要时监控computed
/* 3. Vue资源 */
filters: {},
directives: {},
/* 4. Vue生命周期 */
created () {},
mounted () {},
destroy () {},
/* 5. Vue方法 */
methods: {}, // all the methods should be put here in the last
}
推荐-优雅引用字体
编程式字体方法的好处
学习视觉同学对于具体字体的考量,也许还能发现视觉同学的bug
全局控制,避免样式散乱
书写字体样式成为一门艺术
bad
.form-title {
font: 'PingFang-SC-medium';
font-size: 18px;
font-color: #22222;
}
.form-text {
font: 'PingFang-SC-regular';
font-size: 14px;
font-color: #333333;
}
good
variables.scss文件
$font-normal-color = #222222; // 字体颜色
$font-light-color = #333333;
@mixin font-class($fontFamily, $fontSize, $fontColor) {
font-family: $fontFamily;
font-size: $fontSize;
color: $fontColor;
}
@mixin font-large($size: 14px, $color: $font-normal-color) {
@include font-class($font-family-medium, $size, $color);
}
@mixin font-normal($size: 14px, $color: $font-light-color) {
@include font-class($font-family-regular, $size, $color);
}
应用:
.form-title {
@include font-large(18px, #22222);
}
.form-text {
@include font-large(14px, #333333);
}
font-large/font-normal等公用mixins建议放在统一的variables.scss文件中,再通过sass-resource自动引入到所有组件中
最佳字体顺序参考
PC端
$font-family-medium = 'PingFang-SC-medium', Helveti ca, Tahoma, Arial, 'Microsoft YaHei', 'Hiragino Sans GB', 'WenQuanYi Micro Hei', sans-serif;
$font-family-regular = 'PingFang-SC-regular', Helvetica, Tahoma, Arial, 'Microsoft YaHei', 'Hiragino Sans GB', 'WenQuanYi Micro Hei', sans-serif;
移动端
$font-family-ultralight = 'PingFangSC-Ultralight', 'Source Han Sans CN', "Helvetica Neue";
$font-family-regular = 'PingFangSC-Regular', 'Source Han Sans CN', "Helvetica Neue";
$font-family-medium = 'PingFangSC-Medium', 'Source Han Sans CN Medium', "Helvetica Neue";
$font-family-thin = 'PingFangSC-Thin', 'Source Han Sans CN Thin', "Helvetica Neue";
$font-family-light = 'PingFangSC-Light', 'Source Han Sans CN Light', "Helvetica Neue";
$font-family-semibold = 'PingFangSC-Semibold', 'Source Han Sans CN Light', "Helvetica Neue";
推荐-200错误统一处理
对于接口层来说,后端经常定义的结构如下:
{
code: [Number], // 状态码
desc: [String], // 详细描述
detail: [Array, Object] // 前端需要的数据
}
bad
batchAddVariable({ globalParamList: validList })
.then(res => {
if (res === SERVER_ERROR_CODE.SUCCESS) { // !!!Bad: how many interface, how many judge 200
this.close(true)
this.$toast.show(res.detail.colletion) // !!!Bad: always get detail data
} else { // !!!Bad: too much nest,reading difficulty
this.$toast.show(res.desc)
if (res === SERVER_ERROR_CODE.REPEAT) {
...
}
}
})
good
batchAddVariable({ globalParamList: validList })
.then(data => {
this.close(true)
this.$toast.show(data.colletion)
})
.catch(res => {
if (res === SERVER_ERROR_CODE.REPEAT) {
...
}
})
解决方案
http层axios拿到数据后进行统一处理
import Vue from 'vue'
import axios from 'axios'
const service = axios.create({
baseURL: rootURL, // api的base_url
timeout: 15000, // 请求超时时间
})
// request拦截器
service.interceptors.request.use(
config => {
if (config.method === 'post' || config.method === 'put' || config.method === 'delete') {
config.headers['Content-Type'] = 'application/json'
if (config.type === 'form') {
config.headers['Content-Type'] = 'multipart/form-data'
} else {
// 序列化
config.data = JSON.stringify(config.data)
}
}
return config
},
error => {
Promise.reject(error)
}
)
// respone拦截器
service.interceptors.response.use(
response => {
const res = response.data
if (res.code === SERVER_ERROR_CODE.SUCCESS) { // 统一处理
return res.detail // 直接返回数据
} else {
Vue.prototype.$toast.show(res.desc) // 错误统一报出
return Promise.reject(res)
}
},
error => {
return Promise.reject(error)
}
)
export default service
到此基本就可以很优雅的写逻辑代码了。不过还有个点可以继续优化。通常情况,后台返回非200错误,只需要$toast提示结果就行,catch代码部分可以省略。类似如下:
batchAddVariable({ globalParamList: validList })
.then(data => this.close(true))
// .catch(() => {}) // 业务通常这里不需要写
多么简洁的代码,但Promise执行reject代码,浏览器会报Uncaught (in promise)错误。这是因为中断了Promise操作但又没有对其进行处理,故由此错误。只需要使用unhandledrejection全局处理即可。
// Promise Catch不报错
window.addEventListener('unhandledrejection', event => event.preventDefault())
API版本控制 (建议)
API的版本号统一放入URL。
https://api.example.com/v{n}/
v{n}n代表版本号,分为整形和浮点型
整形版本号:大功能版本发布形式;具有当前版本状态下的所有API接口,例如:v1,v2
浮点型:为小版本号,只具备补充api的功能,其他api都默认调用大版本号的API,例如v1.1,v1.2
开发流程 (建议)
后端编写和维护接口文档,在 API 变化时更新接口文档
后端根据接口文档进行接口开发
前端根据接口文档进行开发 + Mock平台
开发完成后联调和提交测试
Mock 服务器根据接口文档自动生成 Mock 数据,实现接口文档即API
响应格式
{
"code": 0,
"msg": "操作成功",
"data": {
"name": "XXX",
"code": "XXX"
}
}
下面这块其实跟前端关系不大,但还是说一下吧~ 其实后端返回的数据格式统一,对前端封装公共组件是有很大帮助的,例如表格、分页等大家也都不陌生了,等下会提到。
code值 | 返回描述 |
---|---|
200 | 视业务情况而定,默认为:操作成功 |
400 | 请求错误 |
401 | 无权访问该资源 |
403 | 拒绝访问 |
404 | 请求资源不存在 |
408 | 请求超时 |
500 | 服务器错误 |
501 | 服务未实现 |
502 | 网络错误 |
503 | 服务不可用 |
504 | 网络超时 |
505 | HTTP版本不受支持 |
code值 | 返回描述 |
---|---|
0 | 操作成功 |
-1 | 操作失败 |
500 | 内部异常 |
-2 | 操作失败,传入非法参数 |
1000 | 请求参数错误 |
1001 | 请求不支持GET,请使用POST |
1002 | 请求不支持POST,请使用GET |
列表
// data.list: 响应返回的列表数据
{
"code": 0,
"msg": "操作成功",
"data": {
list:[]
}
}
举例(感觉在写接口文档~):
分页
请求参数
参数名称 | 参数说明 | 类型 | 默认值 | 备注 |
---|---|---|---|---|
pageNo | 当前页数 | Number | 1 | 统一 |
pageSize | 每页显示的条数 | Number | 10 | 统一 |
… | 其他 | - | - | - |
响应参数
参数名称 | 参数说明 | 类型 |
---|---|---|
total | 当前页数 | Number |
totalPage | 总页数 | Number |
currentPage | 当前页数 | Number |
pageSize | 每页显示的条数 | Number |
tableHeaders | 自定义表头信息 | Array |
records | 数据列表 | Number |
tableHeaders(表头)
参数名称 | 参数说明 | 类型 |
---|---|---|
field | 字段名 | String |
title | 标题名称 | String |
返回示例
// 部分接口 存在包裹 pageResult的情况,建议去除,统一采用以下格式:
{
"code": 0,
"msg": "操作成功",
"data": {
"currentPage": 2,
"totalPage": 3,
"total": 22
"pageNo": 1,
"pageSize": 10,
"tableHeaders": [
{
"field": "code",
"title": "部门编码"
}
],
"records": [
{
"id": 1,
"name": "XXX",
"code": "H001"
},
{
"id": 2,
"name": "XXX",
"code": "H001"
}
]
}
}
返回文件Url
返回二制制文件流
关于Boolean类型,JSON数据传输中一律使用1/0来标示,1为是/True,0为否/False
视业务情况而定(yyyy-mm-dd、时间戳)
关于货币格式化,JSON数据传输中一律使用浮点数类型(后端不需要做格式转换)
前端统一处理,保留位数根据业务而定,如下所示:
{
"price": 10.320
}
对于状态字段, 以status结尾,统一返回 label/value 字段, 如下所示
{
"payStatus": {
"label":" 支付成功",
"value": 200
}
}
当某字段无状态,应明确默认值, 如下所示
{
"list": [] // 列表无数据,默认返回空数组
}
当前不同业务线存在时间参数 不统一的情况
对于时间范围参数, key 名称可自定义, 但必须统一为2个参数,分别对应 开始和结束时间,。如下所示:
{
"startTime": "",
"endTime": ""
}
为了提高整体开发效率,需要定制一些vue/electron/官网等脚手架工具。首先要将一些代码规范考虑在内,需要保持
git仓库的代码就像是一个人写出来的。根据团队习惯,考虑后使用组合工具:eslint + stylelint + prettier + husky。
eslint: 对js做规则约束。强制校验。
stylelint: 对css做规则约束。
prettier: 代码格式化。强制格式化。
husky:本地的git钩子工具。
另外敏捷开发过程中,代码复查是至关重要的一环,团队需要使用工具辅助代码分析。经比较和实践后,使用工具:
jsinspect + jscpd
jsinspect: 对js或jsx代码做重复检测。强制校验
jscpd: 对代码重复率进行报告总结,辅助代码复查
以上工具根据需要使用,不要盲目跟风。
npm install --save-dev eslint eslint-plugin-vue babel-eslint
module.exports = {
root: true,
// 指定代码的运行环境。不同的运行环境,全局变量不一样
env: {
browser: true,
node: true
},
parserOptions: {
// ESLint 默认使用Espree作为其解析器,安装了 babel-eslint 用来代替默认的解析器
parser: 'babel-eslint'
},
// 使得不需要自行定义大量的规则
extends: [
// consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
'plugin:vue/essential'
],
// 插件
plugins: [
'vue'
],
// add your custom rules here
rules: {
// allow async-await
'generator-star-spacing': 'off',
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'indent': [2, 4, { 'SwitchCase': 1 }],
...
}
}
将约束命令放置在提交代码前检查,这就要使用husky这个工具,该工具能在提交代码precommit时调用钩子命令。
"scripts": {
"lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
"precommit": "npm run lint"
}
npm install --save-dev prettier
module.exports = {
printWidth: 100, // 设置prettier单行输出(不折行)的(最大)长度
tabWidth: 4, // 设置工具每一个水平缩进的空格数
useTabs: false, // 使用tab(制表位)缩进而非空格
semi: false, // 在语句末尾添加分号
singleQuote: true, // 使用单引号而非双引号
trailingComma: 'none', // 在任何可能的多行中输入尾逗号
bracketSpacing: true, // 在对象字面量声明所使用的的花括号后({)和前(})输出空格
arrowParens: 'avoid', // 为单行箭头函数的参数添加圆括号,参数个数为1时可以省略圆括号
parser: 'babylon', // 指定使用哪一种解析器
jsxBracketSameLine: true, // 在多行JSX元素最后一行的末尾添加 > 而使 > 单独一行(不适用于自闭和元素)
rangeStart: 0, // 只格式化某个文件的一部分
rangeEnd: Infinity, // 只格式化某个文件的一部分
filepath: 'none', // 指定文件的输入路径,这将被用于解析器参照
requirePragma: false, // (v1.7.0+) Prettier可以严格按照按照文件顶部的一些特殊的注释格式化代码,这些注释称为“require pragma”(必须杂注)
insertPragma: false, // (v1.8.0+) Prettier可以在文件的顶部插入一个 @format的特殊注释,以表明改文件已经被Prettier格式化过了。
proseWrap: 'preserve' // (v1.8.2+)
}
在提交git时需要对整个项目执行format格式化,使得代码强制统一。格式化之后再用eslint检查语法错误,无误后把格式化后的代码用git add .添加进入。如果有错误直接中断提交。
"scripts": {
"format": "prettier --write './**/*.{js,ts,vue,json}'",
"lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
"precommit": "npm run format && npm run lint && git add ."
}
可以参考这篇,不再赘述
npm install jsinspect --save-dev
"scripts": {
"format": "prettier --write './**/*.{js,ts,vue,json}'",
"lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
"inspect": "jsinspect -t 50 ./src",
"precommit": "npm run format && npm run lint && npm run inspect && git add ."
}
安装
npm install jscpd --save-dev
代码复查辅助命令
"scripts": {
"codereview": "jscpd ./src"
}
Git分支命名
变量
命名方式:小驼峰
命名规范:前缀名词
// bad
let setCount = 10
// good
let maxCount = 10
常量
命名方式:全部大写
命名规范:多个单词时使用分隔符_
// bad
const serverErrorCode = {
success: 200,
repeat: 444
}
// good
const SERVER_ERROR_CODE = {
SUCCESS: 200,
REPEAT: 444
}
函数
命名方式:小驼峰
命名规范:前缀动词
// bad
function wordClass() {}
// good
function saveWordClass() {}
常用动词:can、has、is、load、get、set
类
命名方式:大驼峰
命名规范:前缀名词
// bad
class person {}
// good
class Person {}
注释
单行
// 单行注释,注意前面的空格
let maxCount = 123
多行
/**
* 多行注释
* /
减少嵌套
确定条件不允许时,尽早返回。经典使用场景:校验数据
// bad
if (condition1) {
if (condition2) {
...
}
}
// good
if (!condition1) return
if (!condition2) return
...
减少特定标记值
使用常量进行自解释
// bad
type: 1 // 1代表新增 2代表修改
// good
const MODIFY_TYPE = {
ADD: 1,
EDIT: 2
}
type: MODIFY_TYPE.ADD
表达式
尽可能简洁表达式
// bad
if (name === ''){}
if (collection.length > 0){}
if (notTrue === false){}
// good
if (!name) {}
if (collection.length){}
if (notTrue){}
分支较多处理
对于相同变量或表达式的多值条件,用switch代替if。
// bad
let type = typeof variable
if (type === 'object') {
// ......
}
else if (type === 'number' || type === 'boolean' || type === 'string') {
// ......
}
// good
switch (typeof variable) {
case 'object':
// ......
break
case 'number':
case 'boolean':
case 'string':
// ......
break
}
使用变量名自解释 V1.1
逻辑复杂时,建议使用变量名自解释,而不是晦涩难懂的简写。
// bad
function(value) {
return !helpers.req(value) || this.entity.entVocabularyEntries.filter(item => item.vocabularyEntryName === value).length < 2
}
// good
function(value) {
let entVocabularyList = this.entity.entVocabularyEntries
let repeatCount = entVocabularyList.filter(item => item.vocabularyEntryName === value).length
return !helpers.req(value) || repeatCount < 2
}
使用函数名自解释 V1.1
遵循单一职责的基础上,可以把逻辑隐藏在函数中,同时使用准确的函数名自解释。
// bad
if (modifyType === MODIFY_TYPE.ADD) {
batchVariableAPI(data).then(() => {
this.closeModal()
this.$toast.show('添加变量成功')
})
} else {
updateVariableAPI(data).then(() => {
this.closeModal()
this.$toast.show('修改变量成功')
})
}
// good
modifyType === MODIFY_TYPE.ADD ? this._insertVariable(data) : this._updateVariable(data)
_insertVariable() {
batchVariableAPI(data).then(() => this._successOperation('添加变量成功'))
}
_updateVariable() {
updateVariableAPI(data).then(() => this._successOperation('修改变量成功'))
}
_successOperation(toastMsg) {
this.closeModal()
this.$toast.show(toastMsg)
}
module.exports = {
printWidth: 100, // 设置prettier单行输出(不折行)的(最大)长度
tabWidth: 4, // 设置工具每一个水平缩进的空格数
useTabs: false, // 使用tab(制表位)缩进而非空格
semi: false, // 在语句末尾添加分号
singleQuote: true, // 使用单引号而非双引号
trailingComma: 'none', // 在任何可能的多行中输入尾逗号
bracketSpacing: true, // 在对象字面量声明所使用的的花括号后({)和前(})输出空格
arrowParens: 'avoid', // 为单行箭头函数的参数添加圆括号,参数个数为1时可以省略圆括号
// parser: 'babylon', // 指定使用哪一种解析器
jsxBracketSameLine: true, // 在多行JSX元素最后一行的末尾添加 > 而使 > 单独一行(不适用于自闭和元素)
rangeStart: 0, // 只格式化某个文件的一部分
rangeEnd: Infinity, // 只格式化某个文件的一部分
filepath: 'none', // 指定文件的输入路径,这将被用于解析器参照
requirePragma: false, // (v1.7.0+) Prettier可以严格按照按照文件顶部的一些特殊的注释格式化代码,这些注释称为“require pragma”(必须杂注)
insertPragma: false, // (v1.8.0+) Prettier可以在文件的顶部插入一个 @format的特殊注释,以表明改文件已经被Prettier格式化过了。
proseWrap: 'preserve' // (v1.8.2+)
}
module.exports = {
root: true,
env: {
browser: true,
node: true
},
extends: ['plugin:vue/essential'],
parserOptions: {
parser: 'babel-eslint'
},
plugins: ['vue'],
// add your custom rules here
rules: {
'arrow-parens': 0, // allow paren-less arrow functions
'generator-star-spacing': 0, // allow async-await
'no-unused-vars': 'error', // disabled no ununsed var `V1.1`
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', // no use debugger in production
'indent': [2, 4, { SwitchCase: 1 }], // 4 space for tab for perttier
'space-before-function-paren': ['error', 'never'], // no space in function name for perttier
}
}
如果是vue-cli3项目,以上配置的eslint插件默认已安装;
如果不是vue-cli3项目,需要npm安装对应包:npm install --save-dev babel-eslint eslint-plugin-vue
参考链接
百度JS规范
.element {
display: block;
float: left;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 100;
margin: 0 100px;
padding: 50px; // padding习惯写到margin后面
width: 100px;
height: 100px;
border: 1px solid #e5e5e5; border-radius: 3px;
font: normal 13px "Helvetica Neue", sans-serif;
color: #333;
text-align: center;
line-height: 1.5;
background-color: #f5f5f5;
opacity: 1;
}
参考连接
百度CSS规范指南
腾讯CSS规范指南
Google CSS规范指南