前端代码整洁与开发技巧
为保证前端人员在团队项目开发过程中的规范化、统一化,特建立《前端代码整洁与开发技巧》文档,通过代码简洁推荐、开发技巧推荐等章节来帮助我们统一代码规范和编码风格,从而提升项目的可读性和可维护性。
目录
文章目录
- 前端代码整洁与开发技巧
- 目录
- 一、代码整洁推荐
- 1.1 三元(三目)运算符
- 1.2 短路判断简写
- 1.3 变量声明简写
- 1.4 if真值判断简写
- 1.5 For循环简写
- 1.6 对象属性简写
- 1.7 箭头函数简写
- 1.8 隐式返回简写
- 1.9 模板字符串
- 1.10 默认参数值
- 1.11 解构赋值简写
- 1.12 多条件判断简写
- 1.13 多变量赋值简写
- 1.14 解构时重命名
- 1.15 对象替代switch
- 1.16 链判断运算符
- 1.17 Null 判断运算符
- 1.18 逻辑运算符
- 1.19 扩展运算符
- 1.20 +运算符隐式转换
- 1.21 v-for中使用解构
- 1.22 使用Set给数组去重
- 1.23 Object[key] 重用代码块
- 1.24 禁用不必要的嵌套块
- 1.25 具有默认值的函数参数应该放到最后
- 1.26 **组件模板应该只包含简单的表达式,复杂的表达式则应该重构为计算属性或方法**
- 1.27 计算属性使用解构赋值减少this滥用,提升性能
- 二、开发技巧推荐
- 2.1 vue开发技巧
- 2.1.1 使用v-bind="obj"将所有属性传给子组件
- 2.1.2 使用$on(‘hook:’)
- 2.1.3 如何为Vue组件添加非响应式数据
- 2.1.5 适时使用$options
- 2.1.6 如何解决Vue在main.js全局引入scss文件,组件里使用scss变量报错问题
- 2.2 javascript开发技巧
- 2.2.1 数组操作
- 1、使用 includes 简化 if 判断
- 2、使用every判断是否所有项都满足条件
- 3、使用some判断是否有一项满足条件
- 4、使用reduce遍历数组处理求和等复杂逻辑
- 2.2.2 对象操作
- 1、向对象有条件的添加属性
- 2、检查属性是否存在对象中
- 3、使用Object.entries() 返回一个给定对象自身可枚举属性的键值对数组
- 4 、使用Object.fromEntries( ) 方法把键值对列表转换为一个对象
- 5、使用Object.freeze冻结对象(可在vue data定义非响应式变量使用)
- 2.2.3 字符串操作
- 1、字符串不满两位补零
- 2、判断字符串前缀、后缀
- 3、使用replaceAll 方法替换所有匹配到的字符
- 2.3 css开发技巧
- 2.3.1 巧妙使用伪类生成表单必填标识
- 2.3.2 增强用户体验,使用伪元素实现增大点击热区
- 2.3.3 利用伪类实现鼠标移入时下划线向两边展开的效果
- 三、其它技巧
- 3.1.1 适时使用provide和inject
- 3.1.2 利用key值解决vue就地复用策略的问题
一、代码整洁推荐
1.1 三元(三目)运算符
如果只想在一行中编写if…else语句时,这是一个很好的节省代码的方式。
常规:
const x = 20
let answer
if(x > 10) {answer = '大于10'
}else {answer = '小于等于10'
}
简写:
const x = 20
const answer = x > 10? '大于10' : '小于等于10'
嵌套版三元运算:
const x = 20
const answer = x > 10? '大于10' : x < 5? '小于5' : '在5和10之间'// 第二层加上()增强可读性
const answer = x > 10? '大于10' : (x < 5? '小于5' : '在5和10之间')
注意:三元运算不要超过2层嵌套,否则可读性不强,如果是嵌套两层,要求第二层加上()帮助提升可读性。
1.2 短路判断简写
将变量值分配给另一个变量时,可能希望确保源变量不为null,undefined或为空。这时候可以编写带有多个条件的长 if 语句,也可以使用短路判断。
常规:
if(type !== null || type !== undefined || type !== '') {let wtType = type
} else {let wtType = '01'
}
简写:
let wtType = type || '01'
注意:如果type值为false或者数字0将取值为字符串’01’。
1.3 变量声明简写
在进行连续性的变量声明操作时,应尽可能的使用 一次声明语句来提升程序运行效率。
常规:
let x
let y
let z = 1
简写:
let x, y, z = 1
1.4 if真值判断简写
这可能是微不足道的,但值得一提。在执行“if 检查”时,有时可以省略全等运算符。
常规:
if(likeJavascript === true) {...
}
if(likeNode !== true) {...
}
简写:
if(likeJavascript) {...
}
if(!likeNode) {...
}
1.5 For循环简写
常规:
const arr = [1, 2, 3]
for(let i = 0; i < arr.length; i++) {console.log(i, arr[i])if(arr[i] > 1) {break}
}
// 0, 1
// 1, 2
如果不需要访问索引,请执行以下操作:
for(let item of arr) {console.log('item', item)if(item > 1) {break}
}
// 1
// 2
如果只想访问索引,请执行以下操作:
for(let index in arr) {console.log(index)
}
// 1
// 2
// 3
如果要访问对象中的键,请执行以下操作
const obj = {name: 'zhangsan', age: 18}
for(let key in obj) {console.log(key,obj[key])
}
// name
// age
注意:不能使用for…of 来遍历对象。
1.6 对象属性简写
ES6提供了一种更简单的方法来为对象分配属性。如果变量名称与对象键相同,则可以使用简写表示法。
常规:
let signType = '1'
let params = {signType: signType
}
简写:
let signType = '1'
let params = {signType
}
1.7 箭头函数简写
经典函数以简单的形式易于读写,但是一旦你开始将它们嵌套在其他函数调用中,它们往往会变得有点冗长和混乱。
常规:
function sayHello(name) {console.log('Hello', name)
}
setTimeout(function() {console.log('Loaded')
}, 2000)
list.forEach(function(item) {console.log(item)
})
简写:
const sayHello = name => console.log('Hello', name)
setTimeout(() => console.log('Loaded'), 2000)
list.forEach(item => console.log(item))
1.8 隐式返回简写
Return 是我们经常使用的关键字,用于返回函数的最终结果。具有单个语句的箭头函数将隐式返回其执行结果(函数可以省略大括号{}用()省略return关键字,具体可参考下例)。
要返回多行语句(例如对象),必须使用 () 而不是 {} 来包装函数体。这可确保将代码执行为单个语句。
常规:
function sum(x, y) {return x + y
}
function makeInfo(name, age) {return {name, age}
}
简写:
const sum = (x, y) => x + y
const makeInfo = (name, age) => ({name, age})
1.9 模板字符串
您是否厌倦了使用 ‘+’ 将多个变量连接成一个字符串?有没有更简单的方法?如果你能够使用ES6,那么你很幸运。您需要做的就是使用反引号,并使用 ${} 来包含变量。
常规:
const name = 'zhangsan'
const age = 18
let des = name + '今年' + age + '岁'
简写:
const name = 'zhangsan'
const age = 18
let des = `${name}今年${age}岁`
1.10 默认参数值
您可以使用if语句定义函数参数的默认值。在ES6中,您可以在函数声明本身中定义默认值。
常规:
function volume(l, w, h) {if(w === undefined) {w = 3}if(h === undefined) {h = 4}return l * w * h
}
简写:
const volume = (l, w = 3, h = 4) => l * w * h
volume(2)
// 24
注意:只有w,h为undefined时默认值才会生效。
结构赋值中的默认值也是如此,如果值为null默认值不会生效,如:
const res = {list: null,code: undefined
}
const { list = [], code = 1 } = res
console.log(list) // null
console.log(code) // 1let num = list.length // Uncaught TypeError: Cannot read properties of null (reading 'length')
注意:如果直接取list数组的长度或者利用[index]取里面的值在res.list为null或者不为数组的时候可能会发生报错,针对该问题,大家可以在解构的时候不赋默认值,在解构的下方利用null判断运算符来处理默认值,如:
const res = {list: null
}
let { list , code = 1 } = res
list = list ?? []
console.log(list) // []let num = list.length // 0
1.11 解构赋值简写
对数组和变量进行解构可以减少变量滥用,提升程序的运行效率。
常规:
let params = {wtId: this.transferDetail.wtId,newPic: type === '01' ? this.selectPersonId : '', // type是方法入参oldPic: this.transferDetail.picOld,recordId: this.applyId, // 转派记录replacementPicStatus: '02', // 转派状态 01-转派中 02-转派结束replacementReason: this.replacementReason
}
简写:
const { selectPersonId, applyId, replacementReason, transferDetail } = this
let params = {wtId: transferDetail.wtId,newPic: type === '01' ? selectPersonId : '', // type是方法入参oldPic: transferDetail.picOld,recordId: applyId, // 转派记录replacementPicStatus: '02', // 转派状态 01-转派中 02-转派结束replacementReason
}
解构知识点链接地址:https://es6.ruanyifeng.com/#docs/destructuring
1.12 多条件判断简写
常规:
function typeHandle(type) {if (type === 'test1') { test1(); } else if (type === 'test2') { test2(); } else if (type === 'test3') { test3(); } else if (type === 'test4') { test4(); } else { throw new Error('Invalid value ' + type); }
}
typeHandle('test3')
简写:
function typeHandle(type) { const types = { test1: test1, test2: test2, test3: test3, test4: test4 }; let func = types[type]; (!func) && throw new Error('Invalid value ' + type);func();
}
typeHandle('test3')
1.13 多变量赋值简写
常规:
let test1, test2, test3; test1 = 1;
test2 = 2;
test3 = 3; let name, age, sex;name = 'zhangsan';
age = '18';
sex = '男'
简写:(利用es6数组、对象的结构赋值,实现多变量赋值简写)
let [test1, test2, test3] = [1, 2, 3]; let {name, age, sex} = { name: 'zhangsan', age: '18', sex: '男'};
1.14 解构时重命名
常规:
const { ticketTypeName } = this.tciketDetail
let params = {wtName: ticketTypeName,...
}
简写:
const { ticketTypeName: wtName } = this.tciketDetail
let params = {wtName,...
}
对象解构赋值的简写形式:
let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };
也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
foo // error: foo is not defined
1.15 对象替代switch
常规:
// 获取工作票名称
workTicketName(type) {switch (type) {case '01':return '变一'breakcase '02':return '变二'breakcase '03':return '配一'breakcase '04':return '配二'breakcase '05':return '低压'breakcase '06':return '作业卡'breakcase '07':return '派工单'breakdefault:break}
}
workTicketName('01') // 变一
简写:
// 获取工作票名称
workTicketName(type) {const typeObj = {'01': '变一','02': '变二','03': '配一','04': '配二','05': '低压','06': '作业卡','07': '派工单'}return typeObj[type]
}
workTicketName('01') // 变一
1.16 链判断运算符
编程实务中,如果读取对象内部的某个属性,往往需要判断一下,属性的上层对象是否存在。比如,读取message.body.user.firstName
这个属性,安全的写法是写成下面这样。
// 错误的写法
const firstName = message.body.user.firstName || 'default';// 正确的写法
const firstName = (message&& message.body&& message.body.user&& message.body.user.firstName) || 'default';// 但是这种写法如果层级过长会显得冗长,可读性也不强,这是我们可以利用链判断运算符来解决
//链判断运算符写法
const firstName = message?.body?.user?.firstName || 'default';
链判断运算符?.
有三种写法。
obj?.prop
// 对象属性是否存在obj?.[expr]
// 同上 也可以理解为 arr?.[index]func?.(...args)
// 函数或对象方法是否存在
下面是?.
运算符常见形式,以及不使用该运算符时的等价形式。
a?.b
// 等同于
a == null ? undefined : a.ba?.[x]
// 等同于
a == null ? undefined : a[x]a?.b()
// 等同于
a == null ? undefined : a.b()a?.()
// 等同于
a == null ? undefined : a()
常规:
// 计划 id
let planId = this.ticketDetail && this.ticketDetail.planDetailVoList && this.ticketDetail.planDetailVoList[0] && this.ticketDetail.planDetailVoList[0].planId
简写:
// 计划 id
let planId = this.ticketDetail?.planDetailVoList?.[0]?.planId
// 如果任意一个问号前面的值为null或者undefined都将直接返回undefined,不在向下取值
注意:链判断运算符只能在js代码块中使用,不能在template里面的标签上使用,否则会解析报错,如果想要在template里达到上述简写效果可以在计算属性中使用。
// 错误的写法
<template> <div class="wrap"> <span>计划编号:</span>{{ticketDetail && ticketDetail.planDetailVoList && ticketDetail.planDetailVoList[0] && ticketDetail.planDetailVoList[0].planNo || ''}} </div>
</template>
<script> export default { data() { return {ticketDetail: {planDetailVoList: [{planNo: 'T202304200940' // 计划编号}]} } }}
</script>//正确的写法
<template> <div class="wrap"> <span>计划编号:</span>{{ planNo }} </div>
</template>
<script> export default { data() { return {ticketDetail: {planDetailVoList: [{planNo: 'T202304200940' // 计划编号}]} } },// 计算属性computed: {// 计划编号planNo({ticketDetail}) {return ticketDetail?.planDetailVoList?.[0]?.planNo || ''}}}
</script>
链判断运算符知识点链接:https://es6.ruanyifeng.com/#docs/operator
1.17 Null 判断运算符
读取对象属性的时候,如果某个属性的值是null
或undefined
,有时候需要为它们指定默认值。常见做法是通过||
运算符指定默认值。
const headerText = response.settings.headerText || 'Hello, world!';
const animationDuration = response.settings.animationDuration || 300;
const showSplashScreen = response.settings.showSplashScreen || true;
上面的三行代码都通过||
运算符指定默认值,但是这样写是错的。开发者的原意是,只要属性的值为null
或undefined
,默认值就会生效,但是属性的值如果为空字符串或false
或0
,默认值也会生效。
为了避免这种情况,ES2020 引入了一个新的 Null 判断运算符??
。它的行为类似||
,但是只有运算符左侧的值为null
或undefined
时,才会返回右侧的值。
const headerText = response.settings.headerText ?? 'Hello, world!';
const animationDuration = response.settings.animationDuration ?? 300;
const showSplashScreen = response.settings.showSplashScreen ?? true;
上面代码中,默认值只有在左侧属性值为null
或undefined
时,才会生效。
1.18 逻辑运算符
使用逻辑运算符!! 快速进行布尔转换。
常规:
// 判断是否是ios环境
const isIOS = Boolean(window.navigator.userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/))
简写:
// 判断是否是ios环境
const isIOS = !!window.navigator.userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/)
1.19 扩展运算符
使用扩展运算符进行数组、对象合并。
常规:
let obj = { name: 'zhangsan', sex: '男' }
obj = Object.assign(obj, {age: 18})let arr = ['zhangsan', 'lisi']
arr = arr.concat(['wangwu', 'zhaoliu'])
简写:
let obj = { name: 'zhangsan', sex: '男' }
obj = {...obj, ...{age: 18}}
obj = {...obj, age: 18}let arr = ['zhangsan', 'lisi']
arr = [...arr, ...['wangwu', 'zhaoliu']]
1.20 +运算符隐式转换
使用+运算符不仅可以将字符串转为数字,还可以将时间转为时间戳。
常规:
let type = '1'
type = Number(type)let sjc = new Date().getTime()
简写:
let type = '1'
type = +typelet sjc = +new Date()
1.21 v-for中使用解构
常规:
<li v-for="item in users" :key="item.id" > {{ item.name }} </li>
简写:
<li v-for="{ name, id } in users" :key="id" > {{ name }} </li>
1.22 使用Set给数组去重
常规:
Array.prototype.distinct = function(){let arr = this,result = [],i,j,len = arr.length;for(i = 0; i < len; i++){for(j = i + 1; j < len; j++){if(arr[i] === arr[j]){j = ++i;}}result.push(arr[i]);}return result;
}
const arra = [1,2,3,4,4,1,1,2,1,1,1];
arra.distinct(); // [3,4,2,1]
简写:
const arr = [1, 1, 2, 3, 4, 4];
const uniqueArr = [...new Set(arr)];
const uniqueArr1 = Array.from(new Set(arr)); // [1,2,34]
1.23 Object[key] 重用代码块
常规:
function validate(values) {if(!values.first) {uni.showToast({title: '第一项不可为空',icon: 'none'})return false}if(!values.last) {uni.showToast({title: '最后一项不可为空',icon: 'none'})return false}return true
}
let isCheckPass = validate({first:'', last: ''})
console.log('是否检验通过', isCheckPass)
简写:
// 对象校验规则
const schema = {first: {required: true,failedTip: '第一项不可为空'},last: {required: true,failedTip: '最后一项不可为空'}
}
// 通用校验函数
const validate = (schema, values) => {for(field in schema) {if(schema[field].required) {if(!values[field]) {uni.showToast({title: schema[field].failedTip,icon: 'none'})return false}}}return true
}
let isCheckPass = validate(schema, {first: '1', last: '3'})
console.log('是否检验通过', isCheckPass)
1.24 禁用不必要的嵌套块
常规:
function submitTicket(){const { ticketCode, isDoubleSign } = thisif(ticketCode === '05') {if(isDoubleSign) {this.signType = '02'}}
}
简写:
function submitTicket(){const { ticketCode, isDoubleSign } = thisif(ticketCode === '05' && isDoubleSign) {this.signType = '02'}
}
1.25 具有默认值的函数参数应该放到最后
默认值放到最后可以让函数少传实参还能正常执行,收获预期结果。
常规:
求和函数,把具有默认值的参数放在参数列表「左边」
function sum(a = 10, b) {return a + b
}/*第1个实参 总是对应 第1个形参所以,3 赋值给 a, 替换掉默认值 10参数b没有传值,最终函数返回NaN
*/
sum(3) // returns NaN as b is undefined
简写:
求和函数,把具有默认值的参数放在参数列表「右边」
function sum(b,a = 10) {return a + b
}/*3 赋值给 ba 没有传值,使用默认值 10
*/
sum(3) // 13
1.26 组件模板应该只包含简单的表达式,复杂的表达式则应该重构为计算属性或方法
Bad Code
<template> <div class="wrap"> <span>工作负责人:</span>{{planDetail.personInfo.name + (planDetail.personInfo.phone? '-' + planDetail.personInfo.phone : 'planDetail.personInfo.phone')}} </div>
</template><script> export default { data() { return {planDetail: {personInfo: {name: 'zhangsan',phone: '15236616216'} } } }}
</script>
Good Code
<template> <div class="wrap"> <span>工作负责人:</span>{{WorkLeader}} </div>
</template><script> export default { data() { return {planDetail: {personInfo: {name: 'zhangsan',phone: '15236616216'} },} },conputed: {// 复杂的表达式则应该重构为计算属性WorkLeader({planDetail}) {return planDetail.personInfo.name + (planDetail.personInfo.phone? '-' + planDetail.personInfo.phone : planDetail.personInfo.phone)}}}
</script>
注意:计算属性中必须包含return字段,且不能产生副作用(如修改外部变量,调用外部无关方法操作了dom等等)
1.27 计算属性使用解构赋值减少this滥用,提升性能
Bad Code
<script>
export default { data() { return { ticketDetail: {wtType: '03'},sendFlag: false,provPermitType: ''} }, computed: { // 票类型wtType() {return this.ticketDetail.wtType},// 应采取安全措施是否可编辑measureEdit() { // 配一工作票 非远程许可 非发送 可编辑if (this.ticketDetail.wtType === "03" && !this.sendFlag && !['01', '02'].includes(this.provPermitType)) {return true}return false}}
}
</script>
上述计算属性中存在滥用this去读取data数据的问题 , 使用this去读取data中数据时会去收集依赖,如果滥用this去读取data中数据,会多次重复地收集依赖,从而产生性能问题。
解决办法:
计算属性的值是一个函数,其参数是Vue的实例化this对象,在上述计算属性中滥用this的例子中可以这样优化。
Good Code
<script>
export default { data() { return { ticketDetail: {wtType: '03'},sendFlag: false,provPermitType: ''} }, computed: { // 票类型wtType({ticketDetail}) {return ticketDetail.wtType},// 应采取安全措施是否可编辑measureEdit({wtType, sendFlag, provPermitType}) { // 配一工作票 非远程许可 非发送 可编辑if (wtType === "03" && !sendFlag && !['01','02'].includes(provPermitType)) {return true}return false}}
}
</script>
二、开发技巧推荐
2.1 vue开发技巧
2.1.1 使用v-bind="obj"将所有属性传给子组件
应用场景1:
将自身接收到的所有props传递给它的子组件,子组件需要在其props:{} 中定义要接受的参数名,常用于对组件的二次封装中,如:对el-table、el-form利用json配置项来控制渲染加载等。
1、v-bind=“$props”
p r o p s :当前组件接收到的 p r o p s 对象。可以通过 v − b i n d = " props:当前组件接收到的 props 对象。可以通过 v-bind=" props:当前组件接收到的props对象。可以通过v−bind="props" 传 入 内 部 组 件 , 也 可 以 通 过 v m . props" 传入内部组件,还可以通过vm.props[name]的形式去获取。
应用场景2:
针对组件有过多的属性传参时,可以不用再一个一个的写,可以把较多的参数传递简化,减少组件本身的参数定义。
2、v-bind=“obj”
如:
<Children :name="name" :age="age" :sex="sex" :job="job"></Children>
改写:
<Children v-bind="{name,age,sex,job}"></Children>
2.1.2 使用$on(‘hook:’)
o n ( ‘ h o o k : ’ ) 能够注册 / 监听任意一个钩子函数。使用 on(‘hook:’)能够注册/监听任意一个钩子函数。使用 on(‘hook:’)能够注册/监听任意一个钩子函数。使用on(‘hook:’)方法,可以仅使用一种生命周期方法(而不是两种)来定义/删除事件 。
实现一个定时器的调用与销毁大家很可能会用以下方式实现:
export default{data(){timer:null // 需要创建实例},mounted(){this.timer = setInterval(()=>{//具体执行内容console.log('1');},1000);},beforeDestory(){clearInterval(this.timer);this.timer = null;}
}
这种方法存在的问题是:
1、vue实例中需要有这个定时器的实例,感觉有点多余。
2、创建的定时器代码和销毁定时器的代码没有放在一起,不容易维护,通常很容易忘记去清理这个定时器。
使用 o n ( ‘ h o o k : ’ ) 监听 b e f o r e D e s t o r y 生命周期可以避免该问题,并且因为只需要监听一次,所以可以使用 on(‘hook:’)监听beforeDestory生命周期可以避免该问题,并且因为只需要监听一次,所以可以使用 on(‘hook:’)监听beforeDestory生命周期可以避免该问题,并且因为只需要监听一次,所以可以使用once进行注册监听。
export default{mounted(){const timer = setInterval(()=>{console.log('1');},1000);this.$once('hook:beforeDestory',()=>{ // 监听一次即可clearInterval(timer);timer = null;})}
}
2.1.3 如何为Vue组件添加非响应式数据
在vue组件中内data内函数返回的对象默认是响应式的,vue的observe函数会遍历此对象所有的属性和子孙属性并转化为getter/setter, 使Vue能够追踪依赖,在属性被访问和修改时通知变更。这种响应式被用在模板更新、watch变更、computed依赖中非常有用。
为什么要设置非响应式数据?
如果我们的数据不会改变,或者只会整体改变,或者本身就不需要响应式,那么为深度响应式做的转化、依赖以及产生的闭包、watcher空间其实是多余的,白白浪费了时间和性能。
平时我们自己写的对象不会太复杂这种性能消耗并不明显,但当在引用第三方工具库,比如图表、地图、模型等,如果把多个不需要深度响应式的第三方实例或数据直接挂载到data属性上,又或者遇到大数据量列表,性能的影响就会比较明显。
利用Vue无法检测到对象属性的添加来实现
官方文档中有介绍
受现代 JavaScript 的限制 (而且 Object.observe 也已经被废弃),Vue 无法检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。例如:
let vm = new Vue({data:{a:1}
})
// `vm.a` 是响应式的
vm.b = 2
// `vm.b` 是非响应式的
因此,我们可以待实例完成初始化后,即created中加入
export default {data() {this.version = '' // 这样定义初始值也行return {}},created() {// 直接在created及后续的生命周期函数里定义赋值this.bigData = {···}}···
}
2.1.5 适时使用$options
vue
实例属性$options
是一个对象,可以调用vue的各个组件下的方法和数据 。
应用场景:
1、获取、调用data外定义的属性
<script>
export default {data() {return {};},//在data外面定义的属性和方法通过$options可以获取和调用name: "options_test",age: 18,testOptionsMethod() {console.log("hello options");},created() { console.log(this.$options.name); // options_testconsole.log(this.$options.age); // 18this.$options.testOptionsMethod(); // hello options},
</script>
2、复用过滤器filters中的方法
假设在时间选择控件里可以获取到一个返回的时间戳,需要转换为日期时间来显示,这个时候就可以用到一个filterTime函数来格式化处理时间戳返回想要显示的时间格式。如果这个时候后台要求我们在离线场景提交时增加一个同样时间格式的提交时间字段,这个时候我们就可以利用this.$options.filters来拿到这个函数进行转换,而不是重新在定义一个方法来处理。
<template> <div>{{ planStartSjc | filterText }}</div>
</template>
export default {data() {return {planStartSjc: 1681960059227 // 计划开始时间的时间戳} },filters: {// 时间戳转换filterTime: function (sjc) {let date = new Date(sjc)const year = date.getFullYear()let month = date.getMonth() + 1let day = date.getDate()let hour = date.getHours()let minute = date.getMinutes()let second = date.getSeconds()month = String(month).padStart(2, '0')day = String(day).padStart(2, '0')hour = String(hour).padStart(2, '0')minute = String(minute).padStart(2, '0')second = String(second).padStart(2, '0')let time = `${year}-${month}-${day} ${hour}:${minute}:${second}`return time}},methods:{// 计划提交submitPlan(){let filterTime = this.$options.filters.filterTimelet nowSjc = + new Date() // 此时的时间戳let planCreateTime = filterTime(nowSjc) // 转换后的时间let planStartTime = filterTime(this.planStartSjc) // 转换后的时间let params = {planCreateTime, // 计划创建时间planStartTime, // 计划开始时间}}, },
}
3、一键搞定之重置data中的某个数据
<script>export default {data() {return {// 表单searchForm: {input: '',name: '',isSelected: false}}},methods: {retset() {//重置某一个表单数据this.searchForm = this.$options.data().searchForm;}}}
</script>
4、重置整个data的数据
<script>export default {data() {return {// 表单searchForm: {input: '',name: '',isSelected: false},dataList: [],planId: ''}},methods: {// 更新测试testUpdata() {this.searchForm = {input: '输入',name: '张三',isSelected: true}this.dataList = [1,2]this.planId = '123456'},retset() {// 重置前先更新一些数据this.testUpdata()console.log('重置前的数据', this.$data)// 直接赋值重置即会导致报错产生(最新的Vue已经不允许这样直接对根实例$data进行赋值)// this.$data = this.$options.data(); // 改用以下写法重置Object.assign(this.$data, this.$options.data()); console.log('重置后的数据', this.$data)}}}
</script>
2.1.6 如何解决Vue在main.js全局引入scss文件,组件里使用scss变量报错问题
报错: Syntax Error: SassError: Undefined variable.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TSFtggSk-1682384948777)(前端代码整洁与开发技巧.assets/20210909143917135-1681975189904.png)]
我一开始的引入方式是这样的:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c5PhE89y-1682384948778)(前端代码整洁与开发技巧.assets/20210909145443693-1681975204155.png)]
以为在main.js全局引入了就一劳永逸,写着常规css样式时还好好的,用到scss变量时就报错了。
- 解决方法一
在需要用到的 .vue 文件里单独引用 variable.scss 变量文件,但是达不到我们想要的“一劳永逸“效果,可能还会有奇奇怪怪的潜在问题,建议使用方法二。
- 解决方法二
通过配置,使得全局都能使用scss变量,而不用每个文件单独引入 。
1、vue-cli2创建的项目:
修改build中的utils.js文件,将 scss: generateLoaders(‘sass’),修改为如下:
scss: generateLoaders('sass').concat({loader: 'sass-resources-loader',options: {//你自己的scss全局文件的路径resources: path.resolve(__dirname, '../src/common/index.scss')}
}),
2.vue-cli3创建的项目:
module.exports = {css: {loaderOptions: {// 不同 sass-loader 版本对应关键字, // v8-: data v8: prependData v10+: additionalDatasass: {additionalData: `@import "@/assets/scss/index.scss";`},scss: {additionalData: `@import "@/assets/scss/index.scss";`},}}
}
注意:scss 配置后面需要加 分号 ‘;’,否则会报错 Syntax Error: SassError: media query expression must begin with ‘(’
参考文档:https://cli.vuejs.org/zh/guide/css.html#css-modules
2.2 javascript开发技巧
2.2.1 数组操作
1、使用 includes 简化 if 判断
Bad Code
if(ticketCode === '01' || ticketCode === '02' || ticketCode === '03') { }
Good Code
if(['01', '02', '03'].includes(ticketCode)) { }
// 下面这种也可以
if('01,02,03'.includes(ticketCode)) { }
2、使用every判断是否所有项都满足条件
Bad Code
let arr = [{checked: true}, {checked: false}, {checked: true}, {checked: false}]
let isAllchecked = true // 是否全部选中
arr.forEach(item => {if(!item.checked) {isAllchecked = false}
})
Good Code
数组的every方法只要有一项不满足条件就会返回false,不会再继续遍历
let arr = [{checked: true}, {checked: false}, {checked: true}, {checked: false}]
let isAllchecked = arr.every(item => {console.log('item', item)return !!item.checked
})
3、使用some判断是否有一项满足条件
Bad Code
let arr = [{checked: false}, {checked: true}, {checked: false}, {checked: true}]
let isHaveChecked = false // 是否有选中的
arr.forEach(item => {if(item.checked) {isHaveChecked = true}
})
Good Code
数组的some方法只要有一项满足条件就会返回true,不会再继续遍历
let arr = [{checked: false}, {checked: true}, {checked: false}, {checked: true}]
let isHaveChecked = arr.some(item => {console.log('item', item)return !!item.checked
})
4、使用reduce遍历数组处理求和等复杂逻辑
reduce()
方法对数组中的每个元素按序执行一个由您提供的 reducer 函数,每一次运行 reducer 会将先前元素的计算结果作为参数传入,最后将其结果汇总为单个返回值 。
第一次执行回调函数时,不存在“上一次的计算结果”。如果需要回调函数从数组索引为 0 的元素开始执行,则需要传递初始值。否则,数组索引为 0 的元素将被作为初始值 initialValue,迭代器将从第二个元素开始执行(索引为 1 而不是 0)。
语法
reduce(callbackFn)
reduce(callbackFn, initialValue)
参数
-
callbackFn
一个“reducer”函数,包含四个参数:
previousValue
:上一次调用callbackFn
时的返回值。在第一次调用时,若指定了初始值 initialValue,其值则为 initialValue,否则为数组索引为 0 的元素array[0]
。currentValue
:数组中正在处理的元素。在第一次调用时,若指定了初始值initialValue
,其值则为数组索引为 0 的元素array[0]
,否则为array[1]
。currentIndex
:数组中正在处理的元素的索引。若指定了初始值initialValue
,则起始索引号为 0,否则从索引 1 起始。array
:用于遍历的数组。 -
initialValue
可选作为第一次调用
callback
函数时参数 previousValue 的值。若指定了初始值initialValue
,则currentValue
则将使用数组第一个元素;否则previousValue
将使用数组第一个元素,而currentValue
将使用数组第二个元素。
返回值
使用“reducer”回调函数遍历整个数组后的结果。
示例
(1)计算数组中每个元素出现的次数
let names = ['Alice', 'Bob', 'Tiff', 'Bruce', 'Alice']let nameNum = names.reduce((pre,cur)=>{if(cur in pre) {pre[cur]++} else {pre[cur] = 1 }return pre
}, {})
console.log(nameNum); //{Alice: 2, Bob: 1, Tiff: 1, Bruce: 1}
(2)数组去重
let arr = [1,2,3,4,4,1]
let newArr = arr.reduce((pre,cur)=>{if(!pre.includes(cur)){return pre.concat(cur)}else{return pre}
},[])
console.log(newArr);// [1, 2, 3, 4]// 简便写法
let newArr = [...new Set(arr)]
let newArr = Array.from(new Set(arr))
(3)将二维数组转化为一维
let arr = [[0, 1], [2, 3], [4, 5]]
let newArr = arr.reduce((pre,cur) => {return pre.concat(cur)
}, [])
console.log(newArr); // [0, 1, 2, 3, 4, 5]// 简便写法
let newArr = arr.flat() // 拉平二维数组
(4)将多维数组转化为一维
let arr = [[0, 1], [2, 3], [4,[5,6,7]]]
const newArr = function(arr){return arr.reduce((pre,cur)=>pre.concat(Array.isArray(cur)?newArr(cur):cur),[])
}
console.log(newArr(arr)); //[0, 1, 2, 3, 4, 5, 6, 7]
2.2.2 对象操作
1、向对象有条件的添加属性
使用场景:
表单提交时针对个性化开关控制的必填参数可以通过该方式来添加到固定参数集合中。
const condition = true;
const person = {id: 1,name: 'John Doe',...(condition && { age: 16 }),
};// 如果 condition 为 false,JavaScript 会做这样的事情:
const person = {id: 1,name: '前端小智',...(false),
};
// 展开 `false` 对对象没有影响
console.log(person); // { id: 1, name: 'John Doe' }
2、检查属性是否存在对象中
const example = {};
example.prop = 'exists';// hasOwn 只会对直接属性返回 true::
Object.hasOwn(example, 'prop'); // returns true
Object.hasOwn(example, 'toString'); // returns false
Object.hasOwn(example, 'hasOwnProperty'); // returns false// in 会对直接或继承的属性返回 true:
'prop' in example; // returns true
'toString' in example; // returns true
'hasOwnProperty' in example; // returns true
3、使用Object.entries() 返回一个给定对象自身可枚举属性的键值对数组
const object1 = {a: 'somestring',b: 42
};const arr = Object.entries(object1) //arr:[['a','somestring'],['b','42']]for (const [key, value] of arr) {console.log(`${key}: ${value}`);
}// Expected output:
// "a: somestring"
// "b: 42"
4 、使用Object.fromEntries( ) 方法把键值对列表转换为一个对象
const entries = new Map([['foo', 'bar'],['baz', 42]
]);const obj = Object.fromEntries(entries);console.log(obj);
// Expected output: Object { foo: "bar", baz: 42 }
5、使用Object.freeze冻结对象(可在vue data定义非响应式变量使用)
const obj = {prop: 42
};Object.freeze(obj);obj.prop = 33;
// Throws an error in strict modeconsole.log(obj.prop);
// Expected output: 42
2.2.3 字符串操作
1、字符串不满两位补零
ES2017 引入了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。padStart()
用于头部补全,padEnd()
用于尾部补全。
padStart()
和padEnd()
一共接受两个参数,第一个参数是字符串补全生效的最大长度,第二个参数是用来补全的字符串。
'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax''x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'
如果原字符串的长度,等于或大于最大长度,则字符串补全不生效,返回原字符串。
'xxx'.padStart(2, 'ab') // 'xxx'
'xxx'.padEnd(2, 'ab') // 'xxx'
如果用来补全的字符串与原字符串,两者的长度之和超过了最大长度,则会截去超出位数的补全字符串。
'abc'.padStart(10, '0123456789')
// '0123456abc'
如果省略第二个参数,默认使用空格补全长度。
'x'.padStart(4) // ' x'
'x'.padEnd(4) // 'x '
常用场景:
// 获取当前月份小于10前补0
let month = new Date().getMonth() +1
// 常规写法利用三木运算符处理
month = month < 10 ? '0' + month : month// 也可以利用pdaStart填充实现
month = String(month).padStart(2, '0')
// 还可以按照以下方式实现
month = ('0' + month).slice(-2)console.log(month);
// expected output: "01"// 银行卡号只显示后4位
const fullNumber = '2034399002125581';
const last4Digits = fullNumber.slice(-4);
const maskedNumber = last4Digits.padStart(fullNumber.length, '*');console.log(maskedNumber);
// expected output: "************5581"
2、判断字符串前缀、后缀
判断字符串前缀、后缀不要一言不合就使用正则表达式:
const url = "https://bili98.cn";
const isHTTPS = /^https:\/\//.test(url); // trueconst fileName = "main.py";
const isPythonCode = /\.py$/.test(fileName); // true
推荐使用 String.prototype.startsWith 和 String.prototype.endsWith,语义性更好:
const url = "https://bili98.cn";
const isHTTPS = url.startsWith("https://") // trueconst fileName = "main.py";
const isPythonCode = fileName.endsWith(".py"); // true
3、使用replaceAll 方法替换所有匹配到的字符
ES2021新特性-替换一个字符串中的所有指定字符 replaceAll()方法的使用
在 ES2021 之前,要替换掉一个字符串中的所有指定字符,我们可以这么做:
const str = '2-4-6-8-10'
const newStr = str.replace(/\-/g, '+')
console.log(newStr) // 2+4+6+8+10
ES2021 则提出了 replaceAll 方法,并将其挂载在 String 的原型上,可以这么用:现在可以用String.prototype.replaceAll()替换全部字符串而不需要使用正则。
const str = '2-4-6-8-10'
const newStr = str.replaceAll('-', '+')
console.log(newStr) // 2+4+6+8+10
2.3 css开发技巧
2.3.1 巧妙使用伪类生成表单必填标识
::befor {content: "*";color: red;margin-right: 4px;
}
示例:(element ui里面的表单项必填标识)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dGYZMG1q-1682384948779)(前端代码整洁与开发技巧.assets/1673255318557.png)]
2.3.2 增强用户体验,使用伪元素实现增大点击热区
在移动端,按钮通常都很小,但是有时由于设计稿限制,我们不能直接去改变按钮元素的高宽。那么这个时候有什么办法在不改变按钮原本大小的情况下去增加他的点击热区呢?
这里,伪元素也是可以代表其宿主元素来响应的鼠标交互事件的。借助伪元素可以轻松帮我们实现,如:
.btn::befoer{ content: ""; position: absolute; top: -10px; right: -10px; bottom: -10px; left: -10px;
}
2.3.3 利用伪类实现鼠标移入时下划线向两边展开的效果
<html lang="en">
<head><meta charset="UTF-8"><title>鼠标移入下划线展开</title><style type="text/css">#underline{width: 200px;height: 50px;background: #ddd;margin: 20px;position: relative;}#underline:after{content: "";width: 0;height: 5px;background: blue;position: absolute;top: 100%;left: 50%;transition: all .8s;}#underline:hover:after{left: 0%;width: 100%;}</style>
</head>
<body><div id="underline"></div>
</body>
</html>
效果展示:
1、在浏览器上随便找个网页打开f12;
2、点击元素栏选中html跟标签右键以html格式修改;
3、复制上述代码覆盖掉网页上的html代码完成修改;
4、鼠标移入显示元素即可看到效果。
三、其它技巧
3.1.1 适时使用provide和inject
provide 和 inject 主要为高阶插件/组件库提供用例,并不推荐直接用于应用程序代码中;并且这对选项需要一起使用以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,在上下游关系成立的时间里始终生效。
- provide:
Object | () => Object
- inject:
Array | { [key: string]: string | Symbol | Object }
//父组件:
provide: { // provide 提供一个属性和方法foo: '这是 foo',fooMethod: () => {console.log('父组件 fooMethod 被调用')}
},// 子或者孙子组件
inject: ['foo', 'fooMethod'], //数组或者对象,注入到子组件
mounted() {this.fooMethod()console.log(this.foo)
}
//在父组件下面所有的子组件都可以利用inject
provide 和 inject 绑定并不是可响应的。这是官方刻意为之的。
然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的,因为对象是引用类型
//父组件:
provide: { foo: '这是 foo'
},
mounted(){this.foo = '这是新的 foo'
}// 子或者孙子组件
inject: ['foo'],
mounted() {console.log(this.foo) //子组件打印的还是'这是 foo'
}//父组件:
provide() { return {foo: this.foo,nameList: this.nameList,personalInfo: this.personalInfo}
},
data() {return {foo: '这是foo',nameList: ['张三', '李四'],personalInfo: { age: 18 }}
},
mounted(){this.foo = '这是新的 foo'this.nameList.push('王五') // 改变堆内存中的值// this.nameList = ['王五', '赵六'] // 放开后子组件仍打印 ['张三', '李四', '王五'],this.personalInfo.age = 16 // 改变堆内存中的值this.personalInfo = {age: 12} // 将当前对象的指针指向新的堆内存地址
}// 子或者孙子组件
<template><div>{{foo}}</div><div>{{nameList}}</div><div>{{personalInfo && personalInfo.age}}</div>
</template>
<script>export default{inject: ['foo', 'nameList', 'personalInfo'], mounted() {setTimeout(() => {console.log(this.foo) // 这是foo -视图不会更新console.log(this.nameList) // ['张三', '李四', '王五'] -视图会更新console.log(this.personalInfo.age) // 16 foo -视图会更新}, 1000)}}
</script>
3.1.2 利用key值解决vue就地复用策略的问题
已知:throttle是封装过的节流指令,在规定时间仅第一次执行,后续点击不再执行,目前设置的时间是2秒,即每2秒内的重复点击只执行第一次。
// 节流指令
Vue.directive('throttle', {inserted: (el, binding) => {const throttleTime = binding.value || 2000 // 节流时间el.addEventListener('click', event => {if (el.nodeName === 'BUTTON' || el.type === 'button') {if (!el.disabled) {el.disabled = truesetTimeout(() => {el.disabled = false}, throttleTime)}} else {// 合理使用样式穿透使元素本身及其子元素鼠标事件失效 el.style.pointerEvents = 'none'el.style.color = 'red'el.setAttribute('data-flag', '0') // 只是为了打印setTimeout(() => {if (el && el.style) {el.style.pointerEvents = 'auto'el.style.color = '#000'el.setAttribute('data-flag', '1') // 只是为了打印}}, throttleTime)}}, false)}
})
通过以下代码可以发现点击完接受工作票按钮后不到2秒的时间内由于vue的就地复用策略导致工作许可按钮复用了原来的元素,继承了之前的样式和自定义属性,从而导致工作许可按钮在上述2s内的点击不会生效,且颜色也发生了短暂的改变。(利用项目加浏览器调试来演示说明)
<template><view><!-- 待接收 --><button v-if="stateNo == '10'" class="btn-test" @click="ticketReceive" v-throttle>接收工作票</button><!-- 待许可 --><button v-if="stateNo == '12'" class="btn-test" @click="permitTicket" v-throttle>工作许可</button> </view>
</template>
<script>
export default{data() {return {stateNo: '10'}},methods: {// 接收工作票ticketReceive($event) {console.log('$event', $event)this.stateNo = '12'this.$nextTick(() => {console.log('视图已完成更新', '工作许可按钮已显示')setTimeout(() => {let dom = document.querySelector('.btn-test')console.log('data-flag', dom.getAttribute('data-flag')) // 0}, 1000)setTimeout(() => {let dom = document.querySelector('.btn-test')console.log('data-flag', dom.getAttribute('data-flag')) // 1}, 2600)})},permitTicket() {console.log('点击了工作许可')},}
}
</script>
解决办法:给两个使用v-if的标签添加不同的key值,如:
<template><view><!-- 待接受 --><button v-if="stateNo == '10'" class="btn-test" @click="ticketReceive" v-throttle key="1">接受工作票</button><!-- 待许可 --><button v-if="stateNo == '12'" class="btn-test" @click="permitTicket" v-throttle key="2">工作许可</button> </view>
</template>
‘0’) // 只是为了打印
setTimeout(() => {
if (el && el.style) {
el.style.pointerEvents = ‘auto’
el.style.color = ‘#000’
el.setAttribute(‘data-flag’, ‘1’) // 只是为了打印
}
}, throttleTime)
}
}, false)
}
})
通过以下代码可以发现点击完接受工作票按钮后不到2秒的时间内由于vue的就地复用策略导致工作许可按钮复用了原来的元素,继承了之前的样式和自定义属性,从而导致工作许可按钮在上述2s内的点击不会生效,且颜色也发生了短暂的改变。(利用项目加浏览器调试来演示说明)```jsx
<template><view><!-- 待接收 --><button v-if="stateNo == '10'" class="btn-test" @click="ticketReceive" v-throttle>接收工作票</button><!-- 待许可 --><button v-if="stateNo == '12'" class="btn-test" @click="permitTicket" v-throttle>工作许可</button> </view>
</template>
<script>
export default{data() {return {stateNo: '10'}},methods: {// 接收工作票ticketReceive($event) {console.log('$event', $event)this.stateNo = '12'this.$nextTick(() => {console.log('视图已完成更新', '工作许可按钮已显示')setTimeout(() => {let dom = document.querySelector('.btn-test')console.log('data-flag', dom.getAttribute('data-flag')) // 0}, 1000)setTimeout(() => {let dom = document.querySelector('.btn-test')console.log('data-flag', dom.getAttribute('data-flag')) // 1}, 2600)})},permitTicket() {console.log('点击了工作许可')},}
}
</script>
解决办法:给两个使用v-if的标签添加不同的key值,如:
<template><view><!-- 待接受 --><button v-if="stateNo == '10'" class="btn-test" @click="ticketReceive" v-throttle key="1">接受工作票</button><!-- 待许可 --><button v-if="stateNo == '12'" class="btn-test" @click="permitTicket" v-throttle key="2">工作许可</button> </view>
</template>