注册
web

接地气的前端代码规范

背景:



  • 技术栈为 vue全家桶
  • 更细节、更符合公司现状的一些约定、规范

优先级 A:必要的


这些规则会帮你规避错误,减少可能会产生的缺陷或者性能隐患。


JavaScript


在使用变量前,必须进行判空,必要时还需进行类型判断;若是对象,建议使用可选链


我们经常会遇到这样的情况:在定义变量时未赋默认值;根据接口返回值进行赋值,因数据等问题导致字段有缺失。若我们在使用这些变量时,未进行必要的判断,理所当然地去使用变量的属性、方法等,轻则导致console上出现一些error信息,再则出现功能无法正确运行,重则直接出现整个系统白屏!
注:在<template>中使用的变量,出现undefined而未进行判空,会导致系统白屏。目前vue@2.6.x还未支持<template>中使用可选链,后续会考虑是否升级到2.7.x。


// 反例
let a, b, c;

a = res.data.data.a;

b = JSON.parse(a);

c = b.includes("1");

// 正例
let a, b, c;

a = res?.data?.data?.a;

if (!!a) {
b = JSON.parse(a);
}

if (Array.isArray(b)) {
c = b.includes("1");
}

必须对接口报错进行处理,至少需进行错误提示


目前系统中对接口错误状态码、错误提示的处理良莠不齐,导致部分接口一旦出错,页面无任何反应,对用户很不友好。



  • 针对接口出现一些错误状态码(如status: 500),后续会在组件库的interceptor中对所有axios进行统一处理,给出错误提示,并往外抛。各个业务层可以对组件库抛出的信息进行进一步的处理,如关闭loading,回退处理等等。
  • 针对接口status: 200``success: false,需要在各个调用接口的地方给出提示语。优先以后端返回为准,否则提示语默认为:系统异常,请联系管理员。
  • 针对接口返回blob文件或其他可能会出现异常的情况,建议使用try...catch来捕获异常。

// 反例
function fetchUser (userId) {
return fetch(`/xxx/xxx/${userId}`);
}

// Promise的实现
function updateUserInfo (userId) {
fetchUser(userId).then(res => {
if (res.data.success) {
doSuccessAction();
}
})
}

// async/await的实现
async function updateUserInfo (userId) {
const res = await fetchUser(userId);
if (res.data.success) {
doSuccessAction();
}
}

// 正例
function fetchUser (userId) {
return fetch(`/xxx/xxx/${userId}`);
}

// Promise的实现
function updateUserInfo (userId) {
fetchUser(userId).then(res => {
if (res.data.success) {
doSuccessAction();
} else {
const errorInfo = res.data.error || "系统异常,请联系管理员";
this.$Message.error(errorInfo);
}
}).catch (error => {
this.$Message.error(error);
});
}

// async/await的实现
async function updateUserInfo (userId) {
try {
const res = await fetchUser(userId);
if (res.data.success) {
doSuccessAction();
} else {
const errorInfo = res.data.error || "系统异常,请联系管理员";
this.$Message.error(errorInfo);
}
} catch (error) {
this.$Message.error(error);
}
}

禁止频繁调用同一个接口,包括循环、监听、或未做节流防抖的按钮等情况下调用接口


频繁调用接口,会产生很多问题,列举如下:



  • 接口耗时长,页面白屏,用户体验不好
  • 对后端服务器造成一定压力
  • 同一个接口,在短时间内同时发出,因为网络延迟等因素,会造成接口返回不一定按照接口发起的顺序,导致最终结果与预期不符

目前代码中会有这些常见情况导致频繁调用,以下给出对应的解决方法:



  • 循环中调用:进行接口聚合,比如原先是每一次给后端一个key,后端返回对应的枚举值,可以改为将这些key组合成数组,一次性请求,获取所有对应的枚举值。
  • 监听中调用:这种情况最大的问题是对watch或者computed的触发场景或次数未知。这个没有统一的解法,需要具体情况具体分析。
  • 按钮中调用:点击按钮后调用接口,是一个特别常见的场景,一般情况下我们不会主动去在接口点击后频繁调用同一个接口,但是要防止用户频繁点击按钮。我们需要在按钮点击后,进入loading状态,或者加上节流或防抖,以避免上述用户操作导致的问题。
  • 表单中调用:在input、select、cascader组件的on-change 事件中调用接口,可以改为在输入框失焦,下拉面板收起时触发,即on-blur、on-open-change、visible-change。

Prop 定义应该尽量详尽,至少指定类型


细致的 prop 定义有两个好处:



  • 它们写明了组件的 API,所以很容易看懂组件的用法;
  • 在开发环境下,如果向一个组件提供格式不正确的 prop,Vue 将会告警,以帮助你捕获潜在的错误来源。

// 这样做只有开发原型系统时可以接受
props: ['status']

props: {
status: String
}

// 更好的做法!
props: {
status: {
type: String,
required: true,
validator: function (value) {
return [
'syncing',
'synced',
'version-conflict',
'error'
].indexOf(value) !== -1
}
}
}

拒绝硬编码值;拒绝魔法数字和字符串;


硬编码值和魔法数字和字符串在编程中往往代表着不好的编码习惯,缺点也很明显:



  1. 值的意义难以了解。
  2. 值需要变动时,需要频繁变更,而且可能要改不只一个地方。

// 反例
for (let i = 0; i < 10; i++) {
//...
}

// 正例
const numApples = 10;
for (let i = 0; i < numApples; i++) {
//...
}


// 反例
<template>
<section class="demo-page">
<span v-if="status === '0'">待付款</span>
<span v-if="status === '1'">待发货</span>
<span v-if="status === '2'">待收货</span>
<span v-if="status === '3'">待评价</span>
</section>
</template>
<script>
export default {
data() {
return {
status: "0",
};
},
...
}
</script>

// 正例
<template>
<section class="demo-page">
<span>{{ statusMap[status] }}</span>
</section>
</template>
<script>
import { getStatusMapApi } from "@/api/index";
export default {
data() {
return {
status: "0",
statusMap:{}
};
},
mounted() {
getStatusMapApi().then(res => {
/* {
"0": 待付款,
"1": 待发货,
"2": 待收货,
"3": 待评价,
} */
this.statusMap = ...
})
}
}
</script>

禁止使用refs.children[i]获取子组件,建议用ref属性;不建议使用ref直接调用子组件的api,以保持组件的独立性


refs是一个对象,持有注册过 ref attribute 的所有 DOM 元素和组件实例。children是当前实例的直接子组件,它并不保证顺序,也不是响应式的。因此使用refs.children[i]获取子组件,是一种不稳定的操作。
ref属性可以访问子组件实例或子元素,但这仅仅是一个直接操作子组件的应急方案;为了保持组件的独立性、稳定性,建议不要直接使用子组件的方法、变量等。


禁止在watch和computed中用$route


由于我们目前都是keep-alive模式,若是在watch和computed中用$route,那么在包括tab页签打开、切换等操作在内的每一次路由变化,都会触发watch和computed,不论是否跟本页面本组件有关系。这样子带来了巨大的性能损耗和一些奇奇怪怪的缺陷产生。


禁止增删改JavaScript 对象或Vue的原型,造成原型污染


原型上的属性可以通过遍历访问到的,原型污染会引起性能消耗或意外BUG。
实际上,大多数在写业务代码的场景下,修改原型的方式都可以采用别的方式替代。


注释要保证详细、完整


推荐使用vscode的koroFileHeader插件,进行快捷注释操作。
文件注释去掉,可以留一个description;
代码有更新,注释记得也要更新;


/**
* @description 这个方法是干嘛用的
* @param {*}
* @return {*}
*/


/**
* @description 这个接口是干嘛用的
* @param {*}
* @see yapi地址
*/


// 这个变量是干嘛用的

工程目录、文件(夹)命名、组件内部命名等需遵循以下内部规范


因篇幅过长,单独整理


CSS


必须为组件样式设置作用域,建议采用scoped属性或者class策略。


设置样式的作用域可以有效确保你的样式只会运用在你想要作用的组件上,而不会造成”污染“。



  • scoped 属性:控制CSS 只作用于当前组件中的元素,需要给<style> 标签加上 scoped属性。
  • class策略:不止要使用 scoped属性,使用唯一的 class 名可以帮你确保那些三方库的 CSS或者其他组件的CSS 不会运用在你自己的 HTML 上。

// 反例
<template>
<span class="title">xxxxxxxxxx</span>
</template>

<style>
.title {
color: red;
}
</style>

// 正例
<template>
<div class="xxx-mgr-page">
<span class="title">xxxxxxxxxx</span>
</div>
</template>

<!-- 使用 `scoped` attribute -->
<style scoped>
.xxx-mgr-page{
.title {
color: red;
}
}
</style>

禁止使用全局选择器、类型选择器等作用范围太大的选择器添加css规则,推荐使用类选择器进行精细化控制。


简单说一下这两种被禁止的选择器:



  • 全局选择器,是由一个星号(*)代指的,它选中了文档中的所有内容。
  • 类型选择器,也叫做”标签名选择器“或者是”元素选择器“,因为它在文档中选择了一个 HTML 标签/元素的缘故。

使用他们添加css规则,会造成以下影响:



  • 作用范围太大,会造成一些不想作用的地方却误伤到了
  • 从性能角度考虑,标签选择器的性能比类选择器要慢

禁止通过css选择器的权重和优先规则来覆盖样式


在项目中,可能一个简单的按钮,它的样式会取决于很多地方很多层:组件库为它定义了最底层、最基本的外观 -> 业务项目中的公共样式为它定义了本项目中的统一样式 -> 页面样式为它定义了布局 -> 具体到这个按钮的样式定义了它的独特样式。正是由于这么多层这么复杂的样式组成,导致在需要更新样式的时候,会出现一些很”偷懒“的做法——通过直接覆盖样式,而不是去找到原先写样式的地方去修改。


// 反例
<template>
<button class="ivu-btn btn-close" style="color: white;">X</button>
</template>

<style>
.btn-close {
color: red;
}
</style>

// 正例
<template>
<button class="ivu-btn btn-close">X</button>
</template>

<style>
.btn-close {
color: white;
}
</style>

使用不常用的js api 和 css attribute,注意确认下浏览器兼容性


本条推荐理由很简单。我们推荐使用了chrome浏览器版本号为80+,那兼容性就需要考虑。常用属性已经验证过了没问题,不常用的就需要自行验证。建议可以通过mdn web docs(developer.mozilla.org/zh-CN/docs/…)来查询。
image.png


超长溢出统一用title,而不是tooltip,以提高性能


推荐理由如下:



  • 由于我们全平台中产品设计倾向于单行文本显示,包括标题文本、下拉表单项、表格单元格等等,一个页面中有可能就有上千个。
  • tooltip是iview组件,样式美观可调整,但包含了多个DOM节点;title是HTML属性,样式无法变更。两者性能差异大。

因为涉及范围之广、两者性能差异之大,所以我们建议用title来处理超长溢出。




优先级 B:推荐的


这些规则能够在绝大多数工程中改善可读性和开发体验。即使你违反了,代码还是能照常运行,但例外应该尽可能少且有合理的理由。


禁止单个vue文件超过1000行,尽量500行;禁止复制黏贴超20行的代码


在平常项目开发中,大家都深深体会到了:一个文件太长,维护起来头很大,开发模式下编译时间也很长;大段相似的代码,很不优雅,若产生问题也很容易只改一处,造成缺陷。
之所以限制1000行、500行、20行,凭以往经验决定;只要有充分理由,可以灵活应变。不断地去抽象,去提炼,去封装,也是很考验开发者的功力,很有助于我们的成长。
注:后续会考虑通过eslint+git-hooks阻止超过1000行的文件被提交。


建议不要在html中有超过两个条件的逻辑判断;在js中不要超过两个并列的if,可以考虑优雅的if-else


html中,不要有超过两句话(尽量一个操作符)的逻辑,否则就用computed
js中,一段逻辑不要超过两个if(如果你是第三个应该评估优化一下),优雅的维护if-else。嵌套的if尽可能减少或者注释清楚判断逻辑


代码优化之后,确定不需要的代码建议直接删除,不确定的代码进行注释并写明注释原因;注释或删除一段代码,要把相关的代码一并处理干净


现有情况是存在很多大段注释的代码,太过冗余杂乱,影响代码阅读,因此建议不需要的代码直接删除。
但又存在部分情况是产品提出的要求暂时隐藏某个功能,后续可能会重新启用,因此只需进行注释即可。建议这种情况下,写明注释原因,供他人后续阅读代码或者优化代码提供指引。
注释或删除一段代码时,现在会存在部分情况下,只删除直接相关代码,其他相关代码放任不管。举个例子,比如产品要求隐藏”保存并启用“功能,最差最直接的做法是隐藏这个按钮就完成,但是发现要获取这个按钮权限,需要watch中调用接口,因此导致这个功能被隐藏了,但是接口调用仍在频繁调用。


布局嵌套尽量不要层级太深;不加没有必要的DOM节点;




优先级 C:小tips


这个分类下的是一些项目开发的小技能、小知识点或业务相关的点。


路由组件一定要有name ,以确保keep-alive生效


<keep-alive>includeexclude prop 允许组件有条件地缓存。匹配首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称 (父组件 components 选项的键值)。匿名组件不能被匹配。


弹窗或其他未激活的tab别在mounted阶段加载,以减小白屏时间


操作闭环、逻辑闭环考虑


举个栗子:



  • 比如弹窗要考虑确定、取消、关闭、重新打开等一系列闭环操作的正确性;
  • 比如详情的新建、查看、编辑;
  • 比如表格分页,考虑页码跳转,分页器大小变化,过滤条件变化时初始化页码等;
  • 比如v-if v-else-if v-else;

使用every和some方法时,记得排除空数组;every返回始终为true,some始终false


let a = [];
a.every(i => { ... }) // true
a.some(i => { ... }) // false

let a = [];
if (a.length > 0) {
a.every(i => { ... }) // true
a.some(i => { ... }) // false
} else {
...
}

对象浅拷贝时,分清对象展开运算符和Object.assign的区别


let aa = { a : 1, b : 2, c : 3};
let bb = Object.assign(aa, {d : 4});

// 修改aa
delete aa.a;
// 结果bb也发生了变化
console.log(bb); // {b: 2, c: 3, d: 4}

let aa = { a : 1, b : 2, c : 3};
// 解法1
let bb = Object.assign({}, aa, {d : 4});
// 解法2
let cc = {...aa, d : 4};
// 修改aa
delete aa.a;
console.log(bb); // {a: 1, b: 2, c: 3, d: 4}
console.log(cc); // {a: 1, b: 2, c: 3, d: 4}

在mounted或created阶段获取路由信息


连续调用接口的方法中如果有路由这种会变化的传参时,不能使用this.$route获取路由,避免执行方法时用户通过点击页签切换路由导致后续接口报错


样式尽可能考虑不同分辨率的自适应,如1366、1920


作者:是秋天啊
来源:juejin.cn/post/7216526817371504697

0 个评论

要回复文章请先登录注册