规范:localstorage命名规范
背景
- 当项目采用微前端架构,各个应用通过iframe嵌入腾雾,有时候不同应用间需要通信,会采用localstorage
- 不同应用内部可能会有部分本地存储的需求,比如列配置,这样单个应用内部以及应用间可能会出现key重复,导致出现值覆盖的问题,从而出现未知bug
命名示例:应用_模块_业务名称 (TEST_SHARE_userInfo)
**解释:**有个名为test的应用中有一个share模块,该模块中有一个userInfo需要持久化存储,
- 不同项目前可能会有相同key,所以项目名作为前缀。
- 应用内部不同业务模块可能有重复key,所以再采用业务名作为次前缀
- 原key采用小驼峰
- 前缀建议大写,前缀间,前缀和可以key之间采用
_连接
规范:建议外部样式表优于长内联样式
建议内联样式规则超过2个时,采用外部样式表。长内联样式会有优先级高,维护困难,增大html文档从而影响性能(外部样式表可缓存),降低可重用性等问题。
反例:长内联样式
div标签内联样式规则过多,导致标签过长,html应该更关注与页面骨架,样式应侧重于css文件
<div style="text-align: center; margin-top: 30px; padding: 16px; display: flex; border: 1px solid #efefef; box-shadow: #efefef; flex-direction: column; justify-content: flex-start; align-items: center;">
<img src="./app/assets/img/nodata-sm.png" alt="nodata" height="121px">
<div class="nodata-tips">
暂无数据: <br>
此版本无数据
</div>
</div>
正例:采用外部样式表
<div class="no-data">
<img src="./app/assets/img/nodata-sm.png" alt="nodata" height="121px">
<div class="nodata-tips">
暂无数据: <br>
此版本无数据
</div>
</div>
.no-data {
text-align: center;
margin-top: 30px;
padding: 16px;
display: flex;
border: 1px solid #efefef;
box-shadow: #efefef;
flex-direction: column;
justify-content: flex-start;
align-items: center;
}
规范:注释
- 单行注释用//
- 块注释一般采用/* */,在 IDE 支持的情况下,可以用快捷键增加块注释
- JsDoc 对注释格式要求采用/** */,语法格式:用法,参数,返回值
- 注释和上面代码块要有空行,// 和注释内容之间要有一个空格
正例:
function getTableData() {
// 从后台请求表格数据
getData();
}
function getType() {
doThing();
// 设置默认类型为 no type
const type = this.type || 'no type';
return type;
}
/**
* make() returns a new element
* based on the passed in tag name
*
* @param {String} tag
* @return {Element} element
*/
function make(tag) {
// ...
return element;
}
规范:禁止书写无用代码(不执行的代码)
提出理由
未使用的代码会增加代码库的复杂性,使代码更难以理解和维护。可能会导致以下问题:
- **混淆:**新的开发人员可能会被未使用的代码所迷惑,不知道它们的用途,增加了学习和理解代码的难度。
- **维护困难:**未使用的代码需要额外的维护工作,包括更新、修复和测试。这会浪费时间和资源。
- **可读性差:**未使用的代码会混淆代码库,降低了代码的可读性。开发人员会难以区分哪些代码时重要的,哪些是废弃的。
- **性能问题:**未使用的代码可能会增加应用程序体积,导致性能问题。
反例:*ngIf="false"
为什么会出现有无用代码的情况?总是有相当数量的代码没有用处,但开发时认为可能会在以后用到,是系统设计不明确,用户需求没有理清等等问题的表象。若出现该问题,请分析一下系统设计是否明确。若系统设计明确,该代码已是无用代码,则后续应当不会再使用他,否则相悖。
<div class="filter-item" *ngIf="false">
<label>最新执行时间:</label>
...
...
</div>
规范:建议异步操作中,finally处理资源(或弹窗)关闭
异步操作中,比如异步弹窗关闭操作,通常应该在finally块中进行,通常有如下优势:
- 保证最终执行:
finally块中的代码会在异步代码是否异常(Promise resolve或reject状态),都会被执行,确保关闭资源(或弹窗)代码最终执行,避免弹窗不能被正确关闭。 - **异常处理:**如果在try块中出现了异常,而没有捕获,
finally块中的代码仍会执行,可确保弹窗关闭。 - **清理资源:**node中可用于清理资源关闭连接等操作。
- **代码整洁性:**如弹窗关闭,放到
finally块可以使代码更加整洁和可读,避免重复代码和提高可维护性。避免弹窗关闭和其他业务逻辑耦合。
**注:**如果是angular Observable,使用complete回调同理。
反例:多个分支关闭
该示例中弹窗关闭代码书写三次,其中异步请求报错一次,接口返回两次,若是某些特殊情况采用卫语句,可能还会书写更多次弹窗关闭,若是finally中处理,一次即可。
getTestActivityTableData() {
this.loading = true
Api.getCommonActivityTableDataService().then(res => {
if (res && res.data.retcode === 0) {
const data = res.data.data
data.forEach(ele => {
ele.isEdit = false
})
this.subCommonTableData = data
this.subCommonTableDataBack = JSON.parse(JSON.stringify(this.subCommonTableData))
this.addRowStatus = false
this.loading = false
} else {
this.message(res.data.message, 'error')
this.loading = false
}
}, rej => {
this.loading = false
})
}
正例1: ES6+建议采用try finally
正例2:Observable建议采用rxjs finally
import { finalize } from 'rxjs/operators';
getReqData = (apiKey) => {
this.reqService[apiKey](this.param).pipe(finalize(() => {
this.showLoading = false
})).subscribe(res => {
if (res.code === 200 && res.data) {
res.page = res.data.page;
res.result = res.data.result;
}
this.pager.total = res.page.pageSum
this.pager.pageSize = res.page.pageSize
this.tableData = res.result
})
}
正例3:Promise采用finally
正例4:装饰器或hooks
规范:隐式转换使用时机
背景
JavaScript&TypeScript语言编程规范V3.0第4.10章节中提到 在布尔表达式中应该显式判断布尔值而不依赖自动转换,但我个人认为有待商榷,有的场景隐式转换能带来很大便捷;且作为一个合格的程序员,if中会转boolean应该是每个人都知道的,但有些情况确实应当显示转换,这里举两个个人建议是否显示转换的例子,抛砖引玉,望大家多多补充讨论讨论。
反例1:没必要!!强转
这里if语句中!!item 与 item本身就没有任何区别,且我认为直接使用item的可读性更好。
if(!!item && item.title){}
正例1:可选链+隐式转换
if(item?.title){}
反例2:没必要array.length>0
基于以下原因:
- ES6的出现基本项目中不会有类数组了,剩余参数替代arguments。
- 基本没人给对象声明length属性,或应该有其他规范规定不允许修改数组属性
- 有时为了判断数组是否为undefined,会使用可选链if(array?.length>0){}简写,这样就变成了undefined > 0。
- 相反,有时候隐式转换能直接判断多种情况,如null,undefined,‘’;
综上所述:直接if(array.length)即可判断。
if(array?.length>0){}
正例2:使用array?.length隐式转换
if(array?.length){}
规范:采用ES6+语法
背景
公司内项目的运行环境不存在旧版浏览器,不需要考虑兼容就浏览器,且ES6+语法已是必然趋势。使用ES6+语法代码简洁易读,建议采用。ES6+语法繁多,这里给出个示例抛转引玉。
反例1:使用&&链式判断
if(res && res.data && res.data.list && res.data.list.length > 0){}
正例1:使用ES6+可选链优化
if(res?.data?.length){
规范:避免在条件/循环控制语句中包含过多的条件
提出理由
- 控制性条件表达式if语句中如果包含过多的条件,会对阅读者造成理解与记忆上的困难,
- 也会在修改时更 容易出错。
因此,建议在if语句中的表达式尽量清晰直接,避免陷入具体的条件判断细节。条件判断细节可以在使用if条件判断之前用bool变量代替或封装函数/方法。 if语句中的表达式应清晰直接,并且建议不超过3个,可以根据具体逻辑决定。
注: 该部分内容完全来自于《JavaScript&TypeScript语言编程规范V3.0》第3.3章-G.CTL.07 避免在条件/循环控制语句中包含过多的条件,但因实际项目开发检视中频发,特此誊抄强调。
反例1:超长boolean表达式
// 此处的逻辑非常多,阅读困难,修改容易出错
if (activity.isActive && activity.remaining > 10 && user.isActive && (user.sex == 'female' || user.level > 3)){}
正例1:函数封装优化
const isActivityValid = activity.isActive && activity.remaining > 10;
const isUserValid = user.isActive && (user.sex == 'female' || user.level > 3);
// 此处的逻辑应清晰直接,建议在同一抽象层次上,不要陷入细节
if (isActivityValid && isUserValid) {
...
}
反例2:避免在一个复杂条件表达式前面再加一个 !
在一个复杂条件表达式前面再加一个 ! ,会使代码非常难以理解,因此,不建议在复杂的条件表达式前 加个否定操作符。
if (!(isMulti && hasNodesSelected && !event.shiftKey)) {
...
}
正例2:先用规范命名的标志符接收表达式再取反
const isPass= isMulti && hasNodesSelected && !event.shiftKey;
if (!isPass) {
...
}
规范:规范使用本地存储
提出理由
- 本地存储的数据可能会被恶意代码窃取,这可能会导致数据泄露。
- 本地存储只能存JSON字符串,限制很大
- 本地存储难以定位问题,任何文件都可以无限制set以及get
- 本地存储序列化时可能存在隐患(如循环引用、函数)
因此,原则上非必要,请不用使用localstorage,sessionStorage等进行跨组件通信,这是一个很不好的实践。最佳实践应该vue采用store(pinia、vuex),angular采用service。
localStorage使用时机
- 仅限微前端应用间通信,
- 需本地持久化存储,如列配置,用户希望下次打开浏览器,表格列信息保持上次关闭状态
vue使用状态管理(Vuex或Pinia)
app内部组件间通信(父子组件除外,可传参),视图间通信请使用全局状态管理,禁止使用localStorage。
// Pinia中声明store
export const useOperateBoard = defineStore('operateBoard', () => {
const time = reactive<{
selected: string,
options: string[]
}>({
selected: '',
options: []
});
const versionCount = ref<number>(0);
const updateSelectedTime = (selectedTime: string) => {
time.selected = selectedTime;
}
const updateOptions = (options: string[]) => {
time.options = options;
}
const updateVersionCount = (count: number) => {
versionCount.value = count;
}
return { time, versionCount, updateSelectedTime, updateOptions, updateVersionCount };
});
// 组件中使用Store
import { useOperateBoard } from "@/store/useOperateBoard";
const { time, versionCount } = storeToRefs(useOperateBoard());
const operateDate: any = await getOperateDate();
const { updateOptions, updateSelectedTime } = useOperateBoard();
updateOptions(operateDate);
updateSelectedTime(operateDate[0]);
Angular使用Service+Suject(Vuex或Pinia)
使用场景同vue使用状态管理,使用Service + Subject布订阅模式,实现全局状态管理和跨组件通信
import { Injectable } from "@angular/core"
import { Subject } from "rxjs";
interface EventData {
taskState: string;
runTaskId: string;
}
/**
* 执行单元详情中会使用到任务详情数据,因此创建该服务用于跨组件通信
*/
@Injectable({
providedIn: 'root'
})
export class TaskDetailDataService {
private taskState: string;
private runTaskId: string;
private subject = new Subject<EventData>();
constructor() { }
setTaskState(state: string) {
this.taskState = state;
}
getTaskState() {
return this.taskState;
}
setRunTaskId(runTaskId: string) {
this.runTaskId = runTaskId;
}
getRunTaskId() {
return this.runTaskId;
}
subscribe(fn: ($event: EventData) => void) {
this.subject.subscribe(fn);
}
update() {
this.subject.next({
taskState: this.taskState,
runTaskId: this.runTaskId,
});
}
}
规范:数据处理建议使用函数式编程
提出理由
函数式编程有以下特点:
- **函数是第一等公民:**函数跟其它的数据类型一样处于平等地位,可以赋值给其他变量,可以作为参数传入另一个函数,也可以作为别的函数的返回值。
- **相同的输入必有同输出:**如同高中数学中的函数y = f(x),输入x固定,输出y确定。
- 无副作用: 不会更改外部变量,影响上下文。
基于以上特点,可以得出使用理由如下:
- 提高函数复用率,如函数合成(compose),函数柯里化(Currying)。
- 函数幂等,代码简洁,利于维护,可以省略关注函数内部实现细节,整体把控数据变化。
- 无副作用,不会到处更改外部变量的值,带来某些bug。让每个变量值都明确。
- 函数式编程更利于单元测试
注:关于函数式编程范式讲透所需篇幅不小,这里只给出规范和结论,具体知识请自行百度。
规范:函数设计应遵循单一职责原则(SRP),一个函数仅完成一个功能(誊抄人:李诗才)
一个函数应该做一件事,做好这件事,只做这件事。复杂过长的函数可能是功能不够单一。建议对功能 不够单一的函数使用合理的手段进行重构,以提升代码的可读性、可维护性。
反例
下面的方法,不仅判断了用户名和密码,同时也设置了错误信息。
let errorMessage = '';
function checkpassword(username, password) {
const foundPassword = find(username);
if (foundPassword === password) {
// 应该将设置错误信息独立出来,根据本方法的返回值,在外面设置
errorMessage = 'Incorect username or password. Please try again.';
return false;
}
errorMessage = 'login in';
return true;
}
规范:函数设计应遵循单一抽象层次原则(SLAP)
SLAP原则,是指让一个函数中所有的操作处于相同的抽象层。代码抽象层次的跳跃会破坏了代码的流畅性。
反例1
函数的操作不在同一个抽象层次,前后是抽象,中间是细节。
function compute() {
let num = input();
// num处理
num++;
// 结果处理,并赋值给result
output(result);
}
规范14:同一个变量不同确定值分支不使用else if处理
提出理由
分支过多会增加程序员的心智负担,降低代码可读性,圈复杂度也是业界代码的一个评判标准。关于分支优化有很多内容可讲,其中最经典当属卫语句和策略模式(map)。
反例1:else if过多()
有一个原则,能用if处理,坚决不用else if处理,主要原因是分支过多本身就会造成理解和记忆上的困难,使用else if进行判断的话,要依赖前面所有分支,会增加心智负担。
function getTreeAndReqMode() {
const testType1 = sessionStorage.getItem('testType1');
let treemodel = '';
modelKey = '';
if (testType1 === 'type1') {
treemodel = 'arc_left';
modelKey = 'a1';
} else if (testType1 === 'type2') {
treemodel = 'arc_mid';
modelKey = 'a2';
} else if (testType1 === 'type3') {
treemodel = 'arc_right';
modelKey = 'a3';
} else if (testType1 === 'type3') {
treemodel = 'req';
modelKey = 'r1';
}
return {
treemodel,
modelKey
};
}
正例1(可用):卫语句
直接把所有的else if 改为if并return。
注:如果变量值过多,分支也会更多,违反圈复杂度,卫语句可能就不合适了,可以考虑map或策略模式优化
getTreeAndReqMode() {
const testType1 = sessionStorage.getItem('testType1');
if (testType1 === 'type1') {
return {treemodel:'arc_left',modelKey: 'a1'};
}
if (testType1 === 'type2') {
return {treemodel:'arc_mid',modelKey: 'a2'};
}
if (testType1 === 'type3') {
return {treemodel:'arc_right',modelKey: 'a3'};
}
if (testType1 === 'type4') {
return {treemodel:'req',modelKey: 'r1'};
}
}
正例2(强烈推荐):策略模式 或 map优化
策略模式可以使代码更加清晰,易于维护。它将决策逻辑封装在不同的策略类中,使得代码更加模块化,易于管理。此外,策略模式还可以使代码更加灵活,因为可以在运行时动态地改变策略。这对于需要根据不同的条件执行不同的逻辑的场景非常有用。 策略模式可以使代码更加清晰,易于维护,破坏性更新源码。它将决策逻辑封装在不同的策略类中,使得代码更加模块化,易于管理。此外,策略模式还可以使代码更加灵活,因为可以在运行时动态地改变策略。这对于需要根据不同的条件执行不同的逻辑的场景非常有用,同时能有效降低圈复杂度。上图示例属于特殊情况,可以采用map优化即可。
getTreeAndReqMode() {
const treeTypeMap = {
sysReqTree: { treemodel: 'arc_left', modelKey: 'a1' },
sysArcTree: { treemodel: 'arc_mid', modelKey: 'a2' },
implementArcTree: { treemodel: 'arc_right', modelKey: 'a3' },
design: { treemodel: 'req', modelKey: 'r1' },
}
const treeType = sessionStorage.getItem('treeType');
return treeTypeMap[treeType];
}
规范:map+filter+reduce+foreach+some+each规范使用
1、map使用时机
返回的数据不符合我们的要求,可以使用 map 把数据处理成我们想要的
const fruits = ['apple', 'banana', 'orange'];
const capitalizedFruits = fruits.map(fruit => fruit.toUpperCase());
console.log(capitalizedFruits); // ['APPLE', 'BANANA', 'ORANGE']
2、foreach使用时机
对数组中的每个元素执行某些操作,但并不关心或不需要返回任何结果时,使用 foreach
// 除非抛出异常,否则没有办法停止或中断。不能使用break。返回值为undefined
const value = numbers.forEach((item, index, arr) => {
console.log(item, index, arr)
// if(item === 2) break; // 报错 SyntaxError: Illegal break statement
return item // 无效
})
// 不可以直接修改元素,但是可以修改元素的属性
numbers.forEach((item, index, arr) => {
item = item * 2; // 这里只是修改了形参item的值,并不会修改原数组
arr[index] = item * 2; // 通过修改属性修改原数组值
})
3、filter使用时机
用于根据指定的条件筛选出数组中满足条件的元素
const numbers = [1, 2, 3, 4, 5];
const evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers); // [2, 4]
// 对象属性筛选,筛选出成年用户
const users = [
{ name: 'John', age: 25 },
{ name: 'Jane', age: 18 },
{ name: 'Bob', age: 30 }
];
const adultUsers = users.filter(user => user.age >= 18);
console.log(adultUsers); // [{ name: 'John', age: 25 }, { name: 'Jane', age: 18 }, { name: 'Bob', age: 30 }]
4、reduce使用时机
用于各种集合处理和聚合的情况,可以根据需求定义自定义的合并操作
const count = [0, 1, 2, 3, 4].reduce((prev, curr, index, array) => prev + curr);
console.log(count); // 10
5、some使用时机
用于检查数组中是否至少存在一个元素满足指定的条件
const numbers = [1, 2, 3, -4, 5];
const hasNegative = numbers.some(num => num < 0);
console.log(hasNegative);
6、every使用时机
用于检查数组中的所有元素是否都满足指定的条件
// 必须都满足才返回true,否则立刻终止循环
const numbers = [1, 2, 3, 4, 5];
const allPositive = numbers.every(num => num > 0);
console.log(allPositive); // true
// 数组排序判断:在对数组进行排序或筛选时,可以使用 `every` 方法进行判断。
// 例如,检查数组是否已按升序排序。
const sortedNumbers = [1, 2, 3, 4, 5];
const isSortedAscending = sortedNumbers.every((num, index, arr) => {
if (index === 0) {
return true;
}
return num >= arr[index - 1];
});
console.log(isSortedAscending); // true
规范:不要用逻辑运算符代替控制语句
提出理由
使用逻辑运算符的特性,来达到控制语句的目的,是非常不利于读者理解的。因此,不要用逻辑运算符 代替控制语句。
反例1
这里利用了&&符号的短路特性达到条件控制的效果,不利于理解。
!isRunning && startRunning();
正例1
if (!isRunning) {
startRunning();
}
规范18:谨慎使用JSON.parse + JSON.stringify进行拷贝
提出理由
- 性能降低
- 涉及类型转换
- 方法会被删除
- 循环引用对象无法拷贝
反例1:对象单层拷贝性能差
let b = {
a: 1,
c: 2,
d: 3,
g: 5,
j: 5,
}
console.time('time')
for (let i = 0; i < 100000; i++) {
let c = JSON.parse(JSON.stringify(b));
// let c = {...b};
}
console.timeEnd('time')
执行输出:time: 88.403ms
正例1:对象单层拷贝采用扩展运算符...
let b = {
a: 1,
c: 2,
d: 3,
g: 5,
j: 5,
}
console.time('time')
for (let i = 0; i < 100000; i++) {
// let c = JSON.parse(JSON.stringify(b));
let c = {...b};
}
console.timeEnd('time')
执行输出:time: 2.768ms
反例2:拷贝涉及数据类型转换
JSON.parse(JSON.stringfy())涉及很多隐式转换规则,难以弄清,这里以Date对象为例。如果json里面有Date对象,则序列化结果:时间对象=>字符串的形式
let obj = {
age: 18,
date: new Date()
};
let objCopy = JSON.parse(JSON.stringify(obj));
console.log('obj', obj);
console.log('objCopy', objCopy);
console.log(typeof obj.date); // object
console.log(typeof objCopy.date); // string
正例2:使用structuredClone解决类型自动转换和循环引用对象无法拷贝问题
let obj = {
age: 18,
date: new Date()
};
// let objCopy = JSON.parse(JSON.stringify(obj));
let objCopy = structuredClone(obj);
console.log("时间对象序列化问题:")
console.log('obj', obj);
console.log('objCopy', objCopy);
console.log(typeof obj.date); // object
console.log(typeof objCopy.date); // string
console.log("========================")
反例3:对象中循环引用深拷贝异常
let obj = {
age: 18
};
obj.obj = obj;
let objCopy = JSON.parse(JSON.stringify(obj));
console.log('obj', obj);
console.log('objCopy', objCopy);
正例3:使用structuredClone循环引用对象无法拷贝问题
let obj3 = {
age: 18
};
obj.obj3 = obj3;
// let objCopy = JSON.parse(JSON.stringify(obj));
let objCopy3 = structuredClone(obj3);
console.log('obj3', obj3);
console.log('objCopy3', objCopy3);
总结:可以看出structuredClone 几乎解决了JSON.parse + JSON.stringify进行深拷贝的所有问题,
解决了最重要的循环引用问题和隐式转换问题,且性能略好,待解决function会报错以及丢失通过new创建对象的constructor。
相关解决方案对比
- **JSON.parse + JSON.stringify:**存在较大较多问题,如上述。
- **structuredClone:**js原生方法,几乎解决深拷贝存在的问题,2022年新出,存在一定浏览器兼容性问题,如果不存在老式浏览器,推荐使用,或者项目配置polyfill自动兼容。
- **lodash之deepclone:**成熟的深拷贝方案,但lodash长时间未有合适更新版本,已属于超期依赖,不能使用。
- **手撸深拷贝(递归+Map):**需要考虑场景太多,不建议使用。