前端开发规范
编写背景:当前项目前端开发过程中,每个人有不同的编码风格,这就导致同一个模块不同人开发时可能产生阅读不方便的情况,这对于项目的长久开发是不利的,所以编写这套前端开发规范。
编写目的:避免开发者因为查阅、修改其他同事编写的模块时会遇到阅读困难和疑问。让开发者专心编写逻辑代码,不需要考虑由于格式、注释、文件命名等可能产生的问题。
导言
本规范参考了公司《前端开发规范-V1.0.0》且按照前端开发流程顺序依次递进说明,规范开发者在开发过程中的行为和习惯,尽量控制前端开发风险。
规范
开发环境
在做同一个项目时,应统一前端的开发工具和开发环境。如VS Code、Node.js、Yarn等。
项目选型
当新项目立项着手开发时,前端开发者应考虑前端项目选型问题,从而确定该项目的技术发展路线。主要从以下几个方面考虑:
兼容性。
前端是一个GUI软件。对于一个Web项目来说,用户直接的体验是来自于前端页面的,所以技术栈的兼容性一定是第一个要考虑的原因。其次,对于由老项目迁移到新项目的重构项目来说,在开发时间紧迫需要复用之前代码的情况下,也不得不考虑老项目中的兼容问题。
要与业务紧密相关。
对于信息管理系统来说,业务是一个项目的主导。在项目选型之前,应明确选型的需求以及目的。需要从技术因素考虑需求原型的落实到生产时会使用到的技术以及技术难点。
框架。
从技术角度出发,一个项目中最基础的就是框架。选择框架时应充分考虑到团队前端开发者的技能掌握程度,做到能上手、易上手、了解掌握该框架。对于目前公司技术现状,没有一套成熟的自有的前端开发框架,且技术栈单一(绝大部分为Vue.js),故在考虑框架选型时可直接选用Vue全家桶。根据团队开发能力选择Vue版本即可。
插件/库。
如果说框架是一座建筑的地基,那么插件/库就是这座建筑的屋架。框架、插件、库这些的存在是为了简化开发者的开发过程,让开发者不需要考虑技术底层架构即可实现业务功能。
选择插件时应考虑采取渐进增强的方式,在项目起步阶段应该只选取必要的插件引入到项目中。
选取的插件要考虑到商业许可、兼容性(包括对框架的兼容和该插件版本之间的兼容)、API丰富、轻量、是否还在维护、插件设计思路等。
对于一些重量级的插件,比如Loadsh.js,我们只需要其中的一个或者两个函数,那么则不引入该插件,只需要将该插件中的函数拿到项目中即可。
模块化。
随着项目的体量越来越大,模块化打包工具和模块化管理已经是一个项目中不可或缺的重要一环。
webpack是当下最热门的前端资源模块化管理和打包工具。vue-cli(webpack)是Vue项目的脚手架。Vite(gulp)是Vue3项目的脚手架。以上三种项目构建和管理工具都是为了模块化而存在的。通过模块化配置打包,可以将松散的模块打包成符合生产环境部署的前端资源。
SASS是一种CSS预处理器。它是对CSS的语法的一种扩充,是CSS模块化的一种方式。
管理。
代码管理是团队开发的重要一环,也是控制开发风险的重要的工具。SVN、Git狗屎优秀的版本控制工具,应切合实际地选择。
性能/构建优化。
选型阶段的性能/构建优化主要取决于模块化管理和框架、插件的选择。应充分考虑到未来的扩展性,可优化性。对于直接拿来使用的前端开源项目,应对其进行简易的处理,包括但不限于loader的使用、打包代码切割、压缩等。
需求入手
当需求入手时,应与产品、UI确认交付标准。必须充分了解该需求的用户故事、需求文档、原型图、UI图等其他可能必须的与需求相关的物料。
前端开发人员在产品设计阶段不应以技术角度的考量来影响产品的判断,应在原型确立后整体考量该需求所产生的技术难点和复杂度,如果产生疑问,应及时与产品沟通并达成统一的结果。
详细设计
与产品确认需求并分析后,应先着手详细设计。
详细设计可以是规定的详细设计文档,也可以是在项目中撰写的README文档。详细设计应该与后端同事密切结合,前后端应该明确工作内容和协调功能设计。
开发过程
开发过程包括代码的编写和版本控制。
Git版本控制
分支管理:参考公司分支管理规范。
补充:
当多个开发者在同一分支开发功能时,应以该分支为起点,创建个人开发分支。分支删除:所有的分支删除操作必须在本地执行,本地删除后推送到 Git,禁止直接在 Git 远程仓库的分支管理操作界面中执行删除操作。
Git常用操作命令
commit内容应附带前缀:
feat:新功能(feature)
fix:修补bug
docs:文档(documentation)
style: 格式(不影响代码运行的变动)
refactor:重构(即不是新增功能,也不是修改bug的代码变动)
test:增加测试
chore:构建过程或辅助工具的变动
合并分支
# 把分支A merge 到分支B
$ git checkout branch_b # 切换到分支B
$ git pull # 更新本地仓库代码
$ git checkout branch_a # 切换到分支A
$ git pull # 更新本地仓库代码
$ git merge branch_b # 执行merge
# 如果有冲突则处理存在的冲突
$ git push # 将本地分支A的代码推送到远程分支A
删除分支
# 切换分支:先切换到其他分支
$ git checkout master # 一般切换到默认分支
# 删除本地分支A
$ git branch -D branch_a
# 删除远程分支A
$ git push origin --delete branch_a
将Gitee仓库中的分支合并到GitLab仓库中的分支
# 首先cd到GitLab仓库代码文件夹
$ cd GitLabCode # GitLabCode指的是文件夹名称
# 给GitLab仓库添加一个远程源,格式为 git remote add [仓库B名称(自定义)] [仓库B所在远程地址]
$ git remote add gitee https://gitee.com/ma5845211314/vue-element-admin.git
# 拉取Gitee仓库dev分支的代码
# 由于是合并过来的 他们之前没有联系 所以使用 `--allow-unrelated-histories` 来强制执行
# 格式:git pull [仓库B名称] [分支] --allow-unrelated-histories
$ git pull gitee dev --allow-unrelated-histories
# 如果有冲突则处理存在的冲突
$ git push # 将Gitee仓库中的dev分支的代码合并后推送到GitLab的当前远程分支
代码规范
命名规范
目录命名
图片文件夹:images字体文件夹:fontsstyle文件夹:style页面文件夹:views组件文件夹:components插件文件夹:plugins接口文件夹:api工具函数文件夹:utils模块文件夹:modules 图片命名
图片名称必须小写,禁止使用特殊字符、中文;使用英文或拼音缩写,禁止特殊字符;名称间隔使用-符号;命名需要能体现图片的大概用途; HTML/CSS命名
jdc.html
jdc_list.html
jdc_detail.html
jdc.scss
jdc_list.scss
jdc_detail.scss
ClassName命名
ClassName的命名应该尽量精短、明确,必须以字母开头命名,且全部字母为小写,单词之间统一使用中划线 -连接;
不允许使用拼音和特殊字符;
涉及数据、交互等需要联调的部分,禁止通过id选择器定义样式,以免开发过程中id名变化,引起页局错乱;
类名命名需要语义化;
推荐示例:
.wrap{} //外层容器
.mod-box{} //模块容器
.btn-start{} //开始
.btn-download-ios{} //ios下载
.btn-download-andriod{} //安卓下载
.btn-head-nav1{} //头部导航链接1
.btn-news-more{} //更多新闻
.btn-play{} //播放视频
.btn-ico{} //按钮ico
.btn-lottery{} //开始抽奖
.side-nav{} //侧栏导航
.side-nav-btn1{} //侧栏导航-链接1
.btn-more{} //更多
样式名不能包含ad、guanggao、ads、gg是广告含义的关键词,避免元素被网页拓展、插件屏蔽,如:
另外,敏感不和谐字眼也不应该出现,如:
...
组件命名
组件命名应建议使用中划线命名法,组件名称必须具有语义化。
推荐:advanced-search.vue(高级搜索组件)
注释规范
可通过编辑器的语法高亮功能,确保HTML、CSS、JS文件中注释语法正确,建议使用vscode注释插件koroFileHeader
页面头部需要添加制作时间等信息。
在head区域中,title标签下方的注释,按照下方格式分别加上页面设计、页面制作的公司名称,创建的年-月-日
页面设计、页面制作填写具体公司名称,创建:填写页面创建的时间。
如果打包后的静态资源无法删除注释时,禁止注释中出现制作者的个人信息,如姓名、QQ号、邮箱等。
合理的注释有助于后期维护。
较长的的HTML文件,请在板块之间加入注释,使得结构更清晰:
...
...
...
如果是需要和后台开发联调的自定义函数。请注明函数的调用方式,包括函数的用途、参数类型等。
// 转盘初始化
// 参数1:是奖品的个数,数字类型
// 参数2:用来旋转的圆盘元素,可为dom元素 或 选择器字符串
var panel=new PanelLotery({
length:8,
el:'#ltpanel'
});
图片规范
图片优化
图片体积不能超过300KJPG图片必须压缩,一般80%品质即可,保证文字清晰JPG图片必须使用渐进式图片:使用Photoshop的“存储为web所用格式”时候,勾选“连续”透明PNG图片必须使用压缩工具压缩后提供 图片标签
PC端img图片必须填写width、height、alt属性移动端必须填写alt属性alt不能为无意义字符,需要能表现出图片的含义,如图片为道具图,则应该为道具的名称 合理切图
需要变动的文字禁止切到图片中,如果不确定是否需要变动,请咨询接口人需要程序后台动态生成的图片,如道具图片、头像、奖品,必须单独切割出来装饰性图片合并成精灵图,减少页面请求
HTML规范
标签
标签必须合法且闭合、嵌套正确,标签名需小写标签语法无错误,需要符合语义化标签的自定义属性以data-开头,如:除非有特定的功能、组件要求等,禁止随意使用id来定义元素样式 链接
给 标签加上title属性标签的href属性必须写上链接地址,暂无的加上javascript:alert('敬请期待!')非本专题的页面间跳转,采用打开新窗口模式:target="_blank" https协议自适应
将调用静态域名 ossweb-img.qq.com 以及 game.gtimg.cn 的外部请求,写法上一律去掉协议http:部分,采用自适应的写法。具体方法如下:
//CSS背景图片
.bg{background: url(//game.gtimg.cn/images/cf/cp/a20161021sqjs/hd.jpg) no-repeat;}
//链接URL
//图片SRC
//JS链接
flash
页面禁止使用flash,动画使用video、CSS3、canvas等方式实现,低版本浏览器使用背景图片降级。
CSS规范
选择器
CSS类名命名参考ClassName命名
禁止使用层级过深的选择器,最多3级。
错误示范:
.without-animation .book-body .body-inner .book-header .dropdown .dropdown-menu .buttons{}
.without-animation .book-body .body-inner .book-header .dropdown .dropdown-right .buttons{}
.without-animation .book-body .body-inner .book-header .pull-left .dropdown-menu .buttons{}
.without-animation .book-body .body-inner .book-header .pull-left .dropdown-right .buttons{}
.without-animation .book-body .body-inner .book-header .font-settings .dropdown-menu .buttons{}
.without-animation .book-body .body-inner .book-header .font-settings .dropdown-right .buttons{}
.without-animation .book-body .body-inner .book-header .js-toolbar-action .dropdown-menu .buttons{}
.without-animation .book-body .body-inner .book-header .js-toolbar-action .dropdown-right .buttons{}
除非有特定的功能、组件要求等,禁止随意使用id来定义元素样式
除非是样式reset需要,禁止对纯元素选择器设置特定样式,避免样式污染
reset
PC端
body,dl,dd,ul,ol,h1,h2,h3,h4,h5,h6,p,form,header,section,article,footer{margin:0;}
body,button,input,select,textarea{font:12px/1.5 tahoma,'\5FAE\8F6F\96C5\9ED1',sans-serif}
h1,h2,h3,h4,h5,h6{font-size:100%}
em,b{font-style:normal}
a{text-decoration:none}
a:hover{text-decoration:underline}
img{border:0}
body{padding-top:42px}
button,input,select,textarea{font-size:100%;outline:none}
table{border-collapse:collapse;border-spacing:0}
td,th,ul,ol{padding:0}
移动端
移动端页面不需要设置微软雅黑、宋体等字体,终端浏览器字体取决于设备上的系统字体。
/* 有较多文字的文章类页面: */
/* 移动端常用reset.css (文字版本) */
/* reset */
html,body,div,p,ul,li,dl,dt,dd,em,i,span,a,img,input,h1,h2,h3,h4,h5 {margin:0;padding:0}
a,img,input {border:none;}
body{font: 14px/1.75 -apple-system, "Helvetica Neue", Helvetica, Arial, sans-serif;}
a {text-decoration:none;}
ul,li{list-style: none}
/* 如果页面无文字,或者不希望文字被长按选中,可使用下面的reset;适合于大多数专题页面 */
/* reset */
html,body,div,p,ul,li,dl,dt,dd,em,i,span,a,img,input,h1,h2,h3,h4,h5 {margin:0;padding:0}
a,img,input {border:none;}
body{font: 14px/1.75 -apple-system, "Helvetica Neue", Helvetica, Arial, sans-serif;-webkit-tap-highlight-color: rgba(0,0,0,0);}
a {text-decoration:none;}
ul,li{list-style: none}
a, img {-webkit-touch-callout: none; /* 禁止长按链接与图片弹出菜单 */}
html, body {
-webkit-user-select: none; /* 禁止选中文本(如无文本选中需求,此为必选项) */
user-select: none;
}
JavaScript规范
JavaScript 是一种客户端脚本语言,这里列出了编写 JavaScript 时需要遵守的规则。
类型
原始类型: 存取原始类型直接作用于值本身
布尔类型Null 类型Undefined 类型数字类型BigInt 类型字符串类型符号类型 Symbol const foo = 1
let bar = foo
bar = 9
console.log(foo, bar) // 1, 9
复杂类型: 访问复杂类型作用于值的引用
objectarrayfunction const foo = [1, 2, 3]
const bar = foo
bar[0] = 9
console.log(foo[0], bar[0]) // 9, 9
引用
请记得 const 和 let 都是块级作用域,var 是函数级作用域
// const and let only exist in the blocks they are defined in.
{
let a = 1
const b = 1
}
console.log(a) // ReferenceError
console.log(b) // ReferenceError
对所有引用都使用 const,不要使用 var,eslint: prefer-const, no-const-assign
原因:这样做可以确保你无法重新分配引用,以避免出现错误和难以理解的代码
// bad
var a = 1
var b = 2
// good
const a = 1
const b = 2
如果引用是可变动的,使用 let 代替 var,eslint: no-var
原因:let 是块级作用域的,而不像 var 属于函数级作用域
// bad
var count = 1
if (count < 10) {
count += 1
}
// good
let count = 1
if (count < 10) {
count += 1
}
对象
请使用字面量值创建对象,eslint: no-new-object
// bad
const a = new Object{}
// good
const a = {}
别使用保留字作为对象的键值,这样在 IE8 下不会运行
// bad
const a = {
default: {}, // default 是保留字
common: {}
}
// good
const a = {
defaults: {},
common: {}
}
当使用动态属性名创建对象时,请使用对象计算属性名来进行创建
原因:因为这样做就可以让你在一个地方定义所有的对象属性
function getKey(k) {
return `a key named ${k}`
}
// bad
const obj = {
id: 5,
name: 'San Francisco'
};
obj[getKey('enabled')] = true
// good
const obj = {
id: 5,
name: 'San Francisco',
[getKey('enabled')]: true
};
请使用对象方法的简写方式,eslint: object-shorthand
// bad
const item = {
value: 1,
addValue: function (val) {
return item.value + val
}
}
// good
const item = {
value: 1,
addValue (val) {
return item.value + val
}
}
请使用对象属性值的简写方式,eslint: object-shorthand
原因:这样更简短且描述更清楚
const job = 'FrontEnd'
// bad
const item = {
job: job
}
// good
const item = {
job
}
将简写的对象属性分组后统一放到对象声明的开头
原因:这样更容易区分哪些属性用了简写的方式
const job = 'FrontEnd'
const department = 'JDC'
// bad
const item = {
sex: 'male',
job,
age: 25,
department
}
// good
const item = {
job,
department,
sex: 'male',
age: 25
}
只对非法标识符的属性使用引号,eslint: quote-props
原因:因为通常来说我们认为这样主观上会更容易阅读,这样会带来代码高亮上的提升,同时也更容易被主流 JS 引擎优化
// bad
const bad = {
'foo': 3,
'bar': 4,
'data-blah': 5
}
// good
const good = {
foo: 3,
bar: 4,
'data-blah': 5
}
不要直接使用Object.prototype的方法, 例如hasOwnProperty,propertyIsEnumerable和isPrototypeOf方法,eslint:no-prototype-builtins
原因:这些方法可能会被对象自身的同名属性覆盖 - 比如 { hasOwnProperty: false } 或者对象可能是一个 null 对象(Object.create(null))
// bad
console.log(object.hasOwnProperty(key))
// good
console.log(Object.prototype.hasOwnProperty.call(object, key))
// best
const has = Object.prototype.hasOwnProperty // cache the lookup once, in module scope.
console.log(has.call(object, key))
/* or */
import has from 'has' // https://www.npmjs.com/package/has
console.log(has(object, key))
优先使用对象展开运算符 ... 来做对象浅拷贝而不是使用 Object.assign,使用对象剩余操作符来获得一个包含确定的剩余属性的新对象
// very bad
const original = { a: 1, b: 2 }
const copy = Object.assign(original, { c: 3 }) // this mutates `original` ಠ_ಠ
delete copy.a // so does this
// bad
const original = { a: 1, b: 2 }
const copy = Object.assign({}, original, { c: 3 }) // copy => { a: 1, b: 2, c: 3 }
// good
const original = { a: 1, b: 2 }
const copy = { ...original, c: 3 } // copy => { a: 1, b: 2, c: 3 }
const { a, ...noA } = copy // noA => { b: 2, c: 3 }
数组
请使用字面量值创建数组,eslint: no-array-constructor
// bad
const items = new Array()
// good
const items = []
向数组中添加元素时,请使用 push 方法
const items = []
// bad
items[items.length] = 'test'
// good
items.push('test')
使用展开运算符 ... 复制数组
// bad
const items = []
const itemsCopy = []
const len = items.length
let i
// bad
for (i = 0; i < len; i++) {
itemsCopy[i] = items[i]
}
// good
itemsCopy = [...items]
把一个可迭代的对象转换为数组时,使用展开运算符 ... 而不是 Array.from
const foo = document.querySelectorAll('.foo')
// good
const nodes = Array.from(foo)
// best
const nodes = [...foo]
使用 Array.from 来将一个类数组对象转换为数组
const arrLike = { 0: 'foo', 1: 'bar', 2: 'baz', length: 3 }
// bad
const arr = Array.prototype.slice.call(arrLike)
// good
const arr = Array.from(arrLike)
遍历迭代器进行映射时使用 Array.from 代替扩展运算符 ..., 因为这可以避免创建中间数组
// bad
const baz = [...foo].map(bar)
// good
const baz = Array.from(foo, bar)
使用数组的 map 等方法时,请使用 return 声明,如果是单一声明语句的情况,可省略 return
// good
[1, 2, 3].map(x => {
const y = x + 1
return x * y
})
// good
[1, 2, 3].map(x => x + 1)
// bad
const flat = {}
[[0, 1], [2, 3], [4, 5]].reduce((memo, item, index) => {
const flatten = memo.concat(item)
flat[index] = flatten
})
// good
const flat = {}
[[0, 1], [2, 3], [4, 5]].reduce((memo, item, index) => {
const flatten = memo.concat(item)
flat[index] = flatten
return flatten
})
// bad
inbox.filter((msg) => {
const { subject, author } = msg
if (subject === 'Mockingbird') {
return author === 'Harper Lee'
} else {
return false
}
})
// good
inbox.filter((msg) => {
const { subject, author } = msg
if (subject === 'Mockingbird') {
return author === 'Harper Lee'
}
return false
})
如果一个数组有多行则要在数组的开括号后和闭括号前使用新行
// bad
const arr = [
[0, 1], [2, 3], [4, 5]
]
const objectInArray = [{
id: 1
}, {
id: 2
}]
const numberInArray = [
1, 2
]
// good
const arr = [[0, 1], [2, 3], [4, 5]]
const objectInArray = [
{
id: 1
},
{
id: 2
}
]
const numberInArray = [
1,
2
]
解构赋值
当需要使用对象的多个属性时,请使用解构赋值,eslint: 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}`
}
// better
function getFullName ({ firstName, lastName }) {
return `${firstName} ${lastName}`
}
当需要使用数组的多个值时,请同样使用解构赋值,eslint: prefer-destructuring
const arr = [1, 2, 3, 4]
// bad
const first = arr[0]
const second = arr[1]
// good
const [first, second] = arr
函数需要回传多个值时,请使用对象的解构,而不是数组的解构
原因:可以非破坏性地随时增加或者改变属性顺序
// bad
function doSomething () {
return [top, right, bottom, left]
}
// 如果是数组解构,那么在调用时就需要考虑数据的顺序
const [top, xx, xxx, left] = doSomething()
// good
function doSomething () {
return { top, right, bottom, left }
}
// 此时不需要考虑数据的顺序
const { top, left } = doSomething()
字符串
字符串统一使用单引号的形式 '',eslint: quotes
// bad
const department = "JDC"
// good
const department = 'JDC'
字符串太长的时候,请不要使用字符串连接符换行 \,而是使用 +
const str = '凹凸实验室 凹凸实验室 凹凸实验室' +
'凹凸实验室 凹凸实验室 凹凸实验室' +
'凹凸实验室 凹凸实验室'
程序化生成字符串时,请使用模板字符串,eslint: prefer-template template-curly-spacing
const test = 'test'
// bad
const str = ['a', 'b', test].join()
// bad
const str = 'a' + 'b' + test
// good
const str = `ab${test}`
不要对字符串使用eval(),会导致太多漏洞, eslint: no-eval
不要在字符串中使用不必要的转义字符, eslint: no-useless-escape
// bad
const foo = '\'this\' \i\s \"quoted\"'
// good
const foo = '\'this\' is "quoted"'
const foo = `my name is '${name}'`
函数
不要使用Function构造函数创建函数, eslint: no-new-func
原因:此方式创建函数和对字符串使用 eval() 一样会产生漏洞
// bad
const add = new Function('a', 'b', 'return a + b')
// still bad
const subtract = Function('a', 'b', 'return a - b')
在函数签名中使用空格,eslint: space-before-function-paren space-before-blocks
const f = function(){}
const g = function (){}
const h = function() {}
// good
const x = function b () {}
const y = function a () {}
使用具名函数表达式而非函数声明,eslint: func-style
原因:这样做会导致函数声明被提升,这意味着很容易在文件中定义此函数之前引用它,不利于可读性和可维护性。如果你发现函数定义既庞大又复杂以至于不能理解文件的其他部分,或许你应该将它拆分成模块!别忘记要显式命名表达式,而不用管名字是否是从包含的变量(通常出现在现代浏览器中或者使用 Babel 编译器的时候)中推断的。这样会消除错误调用堆栈中的任何假设。 (讨论)
// bad
function foo () {
// ...
}
// bad
const foo = function () {
// ...
}
// good
// lexical name distinguished from the variable-referenced invocation(s)
const short = function longUniqueMoreDescriptiveLexicalFoo () {
// ...
}
用圆括号包裹自执行匿名函数,eslint:wrap-iife
原因:一个立即执行匿名函数表达式是一个单一的单元,将其及其调用括号包装在括号中,能够清楚地表达这一点。注意,在到处都是模块的世界中几乎不需要 IIFE。
// immediately-invoked function expression (IIFE)
(function () {
console.log('Welcome to the Internet. Please follow me.')
}())
不要在非函数代码块(if , while 等)中声明函数,eslint:no-loop-func
// bad
if (isUse) {
function test () {
// do something
}
}
// good
let test
if (isUse) {
test = () => {
// do something
}
}
不要将参数命名为 arguments,会导致该参数的优先级高于每个函数作用域内原先存在的 arguments 对象
// bad
function foo (name, options, arguments) {
// ...
}
// good
function foo (name, options, args) {
// ...
}
不要使用 arguments,使用 剩余运算符 ...
arguments 只是一个类数组,而 ... 是一个真正的数组
// bad
function test () {
const args = Array.prototype.slice.call(arguments)
return args.join('')
}
// good
function test (...args) {
return args.join('')
}
使用参数默认值语法而不是修改函数参数
// really bad
function handleThings (opts) {
// No! We shouldn't mutate function arguments.
// Double bad: if opts is falsy it'll be set to an object which may
// be what you want but it can introduce subtle bugs.
opts = opts || {}
// ...
}
// still bad
function handleThings (opts) {
if (opts === void 0) {
opts = {}
}
// ...
}
// good
function handleThings (opts = { }) {
// ...
}
避免参数默认值的副作用
let b = 1
// bad
function count (a = b++) {
console.log(a)
}
count() // 1
count() // 2
count(3) // 3
count() // 3
将参数默认值放在最后
// bad
function handleThings (opts = {}, name) {
// ...
}
// good
function handleThings (name, opts = {}) {
// ...
}
不要更改参数,eslint: 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
}
不要给参数重新赋值,eslint: no-param-reassign
原因:参数重新赋值可能会导致无法预期的行为,尤其是当操作 arguments 对象时,也可能导致优化问题,尤其是在 V8 引擎中
// bad
function f1 (a) {
a = 1
}
function f2 (a) {
if (!a) { a = 1 }
}
// good
function f3 (a) {
const b = a || 1
}
function f4 (a = 1) {
}
调用可变参数函数时建议使用展开运算符 ...., eslint: prefer-spread
原因:显然你无需使用上下文,很难结合 new 和 apply
// bad
const x = [1, 2, 3, 4, 5]
console.log.apply(console, x)
// good
const x = [1, 2, 3, 4, 5]
console.log(...x)
// bad
new (Function.prototype.bind.apply(Date, [null, 2016, 8, 5]))
// good
new Date(...[2016, 8, 5])
箭头函数
当你必须使用函数表达式(传递匿名函数)时,使用箭头函数标记. eslint: prefer-arrow-callback, arrow-spacing
原因:它将创建在 this 上下文中执行的函数版本,通常是您想要的,并且语法更简洁
如果您有一个相当复杂的函数,则可以将该逻辑移到其自己的命名函数表达式中
// bad
[1, 2, 3].map(function (x) {
const y = x + 1
return x * y
})
// good
[1, 2, 3].map((x) => {
const y = x + 1
return x * y
})
如果函数体只包含一条没有副作用的返回表达式的语句,可以省略花括号并使用隐式的 return, 否则保留花括号并使用 return 语句,eslint: arrow-parens, arrow-body-style
// bad
[1, 2, 3].map(number => {
const nextNumber = number + 1
`A string containing the ${nextNumber}.`
})
// good
[1, 2, 3].map(number => `A string containing the ${number}.`)
// good
[1, 2, 3].map((number) => {
const nextNumber = number + 1
return `A string containing the ${nextNumber}.`
})
// good
[1, 2, 3].map((number, index) => ({
index: number
}))
// No implicit return with side effects
function foo(callback) {
const val = callback()
if (val === true) {
// Do something if callback returns true
}
}
let bool = false
// bad
foo(() => bool = true)
// good
foo(() => {
bool = true
})
一旦表达式跨多行,使用圆括号包裹以便更好阅读
// bad
['get', 'post', 'put'].map(httpMethod => Object.prototype.hasOwnProperty.call(
httpMagicObjectWithAVeryLongName,
httpMethod
)
)
// good
['get', 'post', 'put'].map(httpMethod => (
Object.prototype.hasOwnProperty.call(
httpMagicObjectWithAVeryLongName,
httpMethod
)
))
函数如果只接收一个参数并且没使用用花括号,则省略圆括号,否则为了清晰明确则使用圆括号包裹参数,注意:总是使用圆括号也是可以接受的,eslint 中的 “always” 选项,eslint: arrow-parens
// bad
[1, 2, 3].map((x) => x * x)
// good
[1, 2, 3].map(x => x * x)
// good
[1, 2, 3].map(number => (
`A long string with the ${number}. It’s so long that we’ve broken it ` +
'over multiple lines!'
))
// 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
})
类&构造函数
使用 class,避免直接操作 prototype
// bad
function Queue (contents = []) {
this._queue = [..contents]
}
Queue.prototype.pop = function () {
const value = this._queue[0]
this._queue.splice(0, 1)
return value
}
// good
class Queue {
constructor (contents = []) {
this._queue = [...contents]
}
pop () {
const value = this._queue[0]
this._queue.splice(0, 1)
return value
}
}
使用 extends 来实现继承
原因:这是一个不会破坏 instanceof 的内建实现原型式继承的方式
// bad
const inherits = require('inherits')
function PeekableQueue(contents) {
Queue.apply(this, contents)
}
inherits(PeekableQueue, Queue)
PeekableQueue.prototype.peek = function () {
return this.queue[0]
}
// good
class PeekableQueue extends Queue {
peek () {
return this.queue[0]
}
}
如果未声明构造函数,则类会有一个默认的构造函数,没必要用空的构造函数或者将其委托给父类,eslint: no-useless-constructor
// bad
class Jedi {
constructor () {}
getName() {
return this.name
}
}
// bad
class Rey extends Jedi {
constructor (...args) {
super(...args)
}
}
// good
class Rey extends Jedi {
constructor (...args) {
super(...args)
this.name = 'Rey'
}
}
避免类成员重复,eslint: no-dupe-class-members
原因:重复的类成员声明会默认使用最后声明的,通常会导致 bug
// bad
class Foo {
bar () { return 1 }
bar () { return 2 }
}
// good
class Foo {
bar () { return 1 }
}
// good
class Foo {
bar () { return 2 }
}
模块
使用标准的 ES6 模块语法 import 和 export
原因:模块是未来,让我们现在开始使用未来的特性
// bad
const util = require('./util')
module.exports = util
// good
import Util from './util'
export default Util
// better
import { Util } from './util'
export default Util
不要使用 import 的通配符 *,这样可以确保你只有一个默认的 export
// bad
import * as Util from './util'
// good
import Util from './util'
同个文件每个模块只允许 import 一次,有多个 import 请书写在一起,eslint: no-duplicate-imports
原因:这样可以让代码更易于维护
// bad
import foo from 'foo'
// … some other imports … //
import { named1, named2 } from 'foo'
// good
import foo, { named1, named2 } from 'foo'
// good
import foo, {
named1,
named2
} from 'foo'
将所有 import 语句放在文件最前方,eslint: import/imports-first
// bad
import foo from 'foo'
foo.init()
import bar from 'bar'
// good
import foo from 'foo'
import bar from 'bar'
foo.init()
多行导入应该像多行数组和对象文字一样缩进
// bad
import { longNameA, longNameB, longNameC, longNameD, longNameE } from 'path'
// good
import {
longNameA,
longNameB,
longNameC,
longNameD,
longNameE
} from 'path'
在模块 import 声明中禁止使用 Webpack 的 loader 语法,eslint: import/no-webpack-loader-syntax
// bad
import fooSass from 'css!sass!foo.scss'
import barCss from 'style!css!bar.css'
// good
import fooSass from 'foo.scss'
import barCss from 'bar.css'
迭代器
不要使用 iterators,建议使用 JS 更高优先级的函数代替 for-in 或 for-of 循环,除非迫不得已,eslint: no-iterator no-restricted-syntax
const numbers = [1, 2, 3, 4, 5]
// bad
let sum = 0
for (let num of numbers) {
sum += num
}
// good
let sum = 0
numbers.forEach(num => sum += num)
// better
const sum = numbers.reduce((total, num) => total + num, 0)
生成器
现阶段请不要使用生成器 generator
原因:因为不能很好地翻译成 ES5 代码
对象属性
使用 . 来访问对象属性
const joke = {
name: 'haha',
age: 28
}
// bad
const name = joke['name']
// good
const name = joke.name
当访问的属性是变量时使用 []
const luke = {
jedi: true,
age: 28,
}
function getProp (prop) {
return luke[prop]
}
const isJedi = getProp('jedi')
变量声明
声明变量时,请使用 const、let 关键字,如果没有写关键字,变量就会暴露在全局上下文中,这样很可能会和现有变量冲突,另外,也很难明确该变量的作用域是什么。这里推荐使用 const 来声明变量,我们需要避免全局命名空间的污染。eslint: no-undef prefer-const
// bad
demo = new Demo()
// good
const demo = new Demo()
将所有的 const 和 let 分组
// bad
let a
const b
let c
const d
let e
// good
const b
const d
let a
let c
let e
变量不要进行链式赋值
原因:变量链式赋值会创建隐藏的全局变量
// bad
(function example() {
// JavaScript interprets this as
// let a = ( b = ( c = 1 ) );
// The let keyword only applies to variable a; variables b and c become
// global variables.
let a = b = c = 1
}())
console.log(a) // throws ReferenceError
console.log(b) // 1
console.log(c) // 1
// good
(function example() {
let a = 1
let b = a
let c = a
}())
console.log(a) // throws ReferenceError
console.log(b) // throws ReferenceError
console.log(c) // throws ReferenceError
// the same applies for `const`
不允许出现未被使用的变量,eslint: no-unused-vars
原因:声明但未被使用的变量通常是不完全重构犯下的错误.这种变量在代码里浪费空间并会给读者造成困扰
// bad
var some_unused_var = 42
// Write-only variables are not considered as used.
var y = 10
y = 5
// A read for a modification of itself is not considered as used.
var z = 0
z = z + 1
// Unused function arguments.
function getX (x, y) {
return x
}
// good
function getXPlusY (x, y) {
return x + y
}
const x = 1
const y = a + 2
alert(getXPlusY(x, y))
// 'type' is ignored even if unused because it has a rest property sibling.
// This is a form of extracting an object that omits the specified keys.
const { type, ...coords } = data
// 'coords' is now the 'data' object without its 'type' property.
Hoisting
var 存在变量提升的情况,即 var 声明会被提升至该作用域的顶部,但是他们的赋值并不会。而 const 和 let 并不存在这种情况,他们被赋予了 Temporal Dead Zones, TDZ, 了解 typeof 不再安全很重要
function example () {
console.log(notDefined) // => throws a ReferenceError
}
function example () {
console.log(declareButNotAssigned) // => undefined
var declaredButNotAssigned = true
}
function example () {
let declaredButNotAssigned
console.log(declaredButNotAssigned) // => undefined
declaredButNotAssigned = true
}
function example () {
console.log(declaredButNotAssigned) // => throws a ReferenceError
console.log(typeof declaredButNotAssigned) // => throws a ReferenceError
const declaredButNotAssigned = true
}
匿名函数的变量名会提升,但函数内容不会
function example () {
console.log(anonymous) // => undefined
anonymous()
var anonymous = function () {
console.log('test')
}
}
命名的函数表达式的变量名会被提升,但函数名和函数函数内容并不会
function example() {
console.log(named) // => undefined
named() // => TypeError named is not a function
superPower() // => ReferenceError superPower is not defined
var named = function superPower () {
console.log('Flying')
}
}
function example() {
console.log(named) // => undefined
named() // => TypeError named is not a function
var named = function named () {
console.log('named')
}
}
比较运算符&相等
使用 === 和 !== 而非 == 和 !=,eslint: eqeqeq
条件声明例如 if 会用 ToBoolean 这个抽象方法将表达式转成布尔值并遵循如下规则
Objects 等于 true
Undefined 等于 false
Null 等于 false
Booleans 等于 布尔值
Numbers 在 +0, -0, 或者 NaN 的情况下等于 false, 其他情况是 true
Strings为''时等于false,否则是true
if ([0] && []) {
// true
// 数组(即使是空数组)也是对象,对象等于true
}
分号
我们遵循 Standard 的规范,不使用分号。
关于应不应该使用分号的讨论有很多,本规范认为非必要的时候,应该不使用分号,好的 JS 程序员应该清楚场景下是一定要加分号的,相信你也是名好的开发者。
// bad
const test = 'good';
(function () {
const str = 'hahaha';
})()
// good
const test = 'good'
;(() => {
const str = 'hahaha'
})();
标准特性
为了代码的可移植性和兼容性,我们应该最大化的使用标准方法,例如优先使用 string.charAt(3) 而不是 string[3]
eval()
由于 eval 方法比较 evil,所以我们约定禁止使用该方法
with() {}
由于 with 方法会产生神奇的作用域,所以我们也是禁止使用该方法的
修改内置对象的原型
不要修改内置对象,如 Object 和 Array
Vue规范
ESLint格式化风格
自测阶段
根据测试提供的冒烟用例进行测试,并修改存在的BUG。
生产上线
生产上线后应将相关分支删除。