前端应用编写规范
zKing 2021-04-01 前端规范
摘要
工欲善其事,必先利其器。
本篇是我在 ImageDT
工作期间编写的技术分享文章
# 基础工具配置
# EditorConfig
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
# Prettier
项目的 Prettier 配置建议如下:
{
"singleQuote": true, // 字符串单引号
"bracketSameLine": true, // 括号同行
"trailingComma": "none", // 旧项目不在对象最后保留逗号,新项目建议为 'es5'
"semi": false, // 不加分号,使用 ts 的项目建议设置为 true
"jsxSingleQuote": false, // jsx 使用双引号
"jsxBracketSameLine": true, // jsx 标签 > 号不单独一行
"arrowParens": "avoid" // 函数单个入参不加括号
}
# Eslint
以 nuxt
为主的项目的 Eslint 配置建议如下(基本都按照最佳推荐就可以了):
{
extends: [
'@nuxtjs/eslint-config-typescript',
'plugin:nuxt/recommended',
'eslint:recommended',
'plugin:vue/recommended',
'plugin:prettier-vue/recommended',
]
}
# StyleLint
项目的 StyleLint 配置建议如下
{
"defaultSeverity": "warning",
"extends": ["stylelint-config-recommended", "stylelint-config-recess-order"]
}
# 项目风格约定
# 统一 Vue TS 顺序
目前统一在 /plugins/interface 建立对应的类型文件(实际上并不是很好,但至少有一个地方来处理类型系统)
// 如果用 template 写法的话,建议顺序和原来的一样
ref;
prop;
data;
computed;
watch;
生命周期;
methods;
// 如果使用 TSX 写法的话,那推荐使用以下的顺序
ref;
prop;
data;
computed;
watch;
method;
生命周期;
render;
renderFunc;
# 统一注释处理
为了便于代码阅读和自文档化,坚持用 JSDoc
来编写注释,可以不用那么仔细,但需要用 /** */
来写注释
/** 这是一个测试函数 */
function Test() {}
# 统一路径处理
- 不要为了贪图方便使用相对路径,除了内部库处理外,一致使用别名路径
- 如果要封装一个通用组件的话,组件中的小部件要统一在一个目录下,并且使用相对路径,最后通过
index.js
来暴露组件进行使用和全局安装
# 统一 Option 处理
当前界面的 Option 会有各种组合 name+id || name+value || name+code
,建议使用适配器模式
,统一转换为 label+value
的组合方案
# 统一库生态
确实需要指定好使用的库,最好是用同样生态的产品,比如 Antv+高德地图,就不要又再用 Echarts+百度地图,甚至混搭。另外在一个项目中最好只有一个 UI 框架库,如果一定要引入另外一个,则另外一个需要按需引入
# 其他
- 如果有比较复杂的业务,那建议加多一个
service
层进行处理 - 在必要的时候才需要使用
vuex
- 禁止同名文件,就算是不同类型的文件,也禁止同名
# 代码风格建议
优先 Vue 的风格指南
# 标签名书写风格
空元素的标签,使用单标签名处理
<el-select>
<el-option></el-option>
</el-select>
<!-- 如果 el-option 里面不再嵌套标签,则不用两个标签名,即用单标签表示没有子元素> -->
<el-select>
<el-option />
</el-select>
# 命名风格
const 常量
命名是使用全大写
,如果是enum
类型的话,则使用大驼峰
,里面的 key 值用大写。- 一般变量使用
小驼峰
命名 - 类名使用大驼峰,并且导出的默认模块建议是一个静态类
export const IDT_TEST = 'imagedt'
export enum QuestionType {
CHOICE,
PHOTO,
ATTENDENCE,
TEXT,
VIDEO,
IDT_TEST
}
let idtUser = 'imagedt'
export function getNextRules(){}
export default class ImageTest {}
- 大部分函数命名最好使用
名词+动词
的结构,并且使用小驼峰
命名 - 如果一个函数为另一个函数的辅助函数,需要用 _ 设置前缀表示私有化,私有化函数最好放在文件底部,并且最佳实践是一个纯函数
- 辅助函数需要写对应的注释说明,遵守 JSDOC,辅助函数可以使用
动宾结构
,相对会更清晰一点 - 个人比较建议的是
- 想要获取数据,初始化数据之类的,可以使用动宾结构
- 如果是页面上某个元素触发的钩子,可以使用宾动结构
- 私有化函数最好设置
_
前缀
const isQuestionEdit = true; // 不推荐 X
const questionEditable = true; // 推荐 √
function handleSkuClick() {} // 不推荐 X
function skuClickHandler() {
// 推荐 √
_getRelateInfo();
/** skuClickHandler 辅助函数 */
function _getRelateInfo() {}
}
- 不同数据结构的命名推荐
const editable = true; // 以 able 为后缀表达布尔值,或者为 1 | 0
const skuMap = {}; // 表示一个对象
const skuList = []; // 表示数组
const skuMapper = new Map(); // 表示 Map 对象
const skuSets = new Set(); // 表示 Set 对象
// 拒绝以下写法
const skus = {}; // 在 js 中没有类型推荐的话,根本不知道是对象还是数组 ....
// 另外也不太推荐 skuArr,skuObjects 这样的写法
# Promise 风格
- 使用 Promise 时,reject 需要 new Error 来抛出,resolve 可以随意,但是必须 return
- 捕获到 error 后应该 console.error 进行输出,而不是使用 console.log 输出
- !!!不要在 Promise 中再加入
async
修饰符,需要采取另外的方式来进行代码编写
function testPromise(flag = false) {
return new Promise((resolve, reject) => {
if (flag) {
return reject(new Error("Promise Error"));
} else {
return resolve("pass");
}
});
}
# 条件判断处理
if(key === 0){
console.log(0)
}else if(key === 1){
console.log(1)
}else if(key === 2){
console.log(2)
}else{
console.log('null')
}
// 如果一定要根据上面这样写的话,请先将对应的判断语义化,否则后续维护者很难知道 1,2,3 代表什么
// 根据需要可以提取到外部的文件中再进行导入
enum KeyInfo {
FRONT,
AFTER,
MIDDLE
}
if(key === KeyInfo.FRONT){
console.log(0)
}else if(key === KeyInfo.AFTER){
console.log(1)
}else if(key === KeyInfo.MIDDLE){
console.log(2)
}else{
console.log('null')
}
除非判断条件确实很复杂,当使用超过 2 个 else if
时,建议用 switch
或者 策略模式
或者 多态
来处理,而不是继续写面条式代码,可以看以下简单的示例
switch
let key = KeyInfo.FRONT
enum KeyInfo {
FRONT,
AFTER,
MIDDLE
}
function getLog(key){
switch(key){
case KeyInfo.FRONT: return 0
case KeyInfo.AFTER: return 1
case KeyInfo.MIDDLE: return 2
default: return null
}
}
console.log(getLog(key))
多态
let key = KeyInfo.FRONT
enum KeyInfo {
FRONT,
AFTER,
MIDDLE
}
class LogDetail {
constructor(log){
this.log = log
}
console(){
console.log(this.log)
}
}
function getLog(key){
switch(key){
case KeyInfo.FRONT: return new LogDetail(0)
case KeyInfo.AFTER: return new LogDetail(1)
case KeyInfo.MIDDLE: return new LogDetail(2)
default: return new LogDetail(null)
}
}
const logDetail = getLog(key)
logDetail.console()
卫语句取代嵌套条件表达式
function test(num) {
let result = 0;
if (num > 0) {
if (num >= 10) {
result = 2;
} else {
result = 1;
}
} else {
result = -1;
}
return result;
}
// 卫语句取代嵌套条件表达式
function test(num) {
if (num <= 0) return -1;
if (num >= 10) return 2;
return 1;
}
# 禁止类型耦合
num1 和 num2 为未来会为数字类型,那初始化时应该用 null 或者 undefined,不应该用空字符串
const num1 = ""; // X
const num2 = null; // √
# 使用管道替代循环
使用管道替代循环(也可以说时链式调用),来更加语义化地编写代码,善用 Array
和Object
对应的 ES 方法
- 简单的示例
let arr = [1, 2, 3, 4, 5];
let result = 0;
//===========
for (let i of arr) {
result += i;
}
// ============
result = arr.reduce((accu, cur) => accu + cur, 0);
- 复杂的示例
function acquireData(input) {
const lines = input.split("\n");
let firstLine = true;
const result = [];
for (const line of lines) {
if (firstLine) {
firstLine = false;
continue;
}
if (line.trim() === "") continue;
const record = line.split(",");
if (record[1].trim() === "India") {
result.push({ city: record[0].trim(), phone: record[2].trim() });
}
}
return result;
}
// ================
// 使用‘管道’改写后
function acquireData(input) {
const lines = input.split("\n");
return lines
.slice(1)
.filter(line => line.trim() !== "")
.map(line => line.split(","))
.filter(fields => fields[1].trim() === "India")
.map(fields => ({ city: fields[0].trim(), phone: fields[2].trim() }));
}
# 正确打印 console
很多情况并不是 log
就能表达清楚的,而是需要用到 warn,error
等类型
//打印错误时,需要用 console.error 不要用 console.log
//获取到错误后,最好弹个窗来提醒,看到是接口的错误就可以直接转给后端了
try {
} catch (err) {
console.error(err);
this.$message.error(err.message);
}
// 另外建议将这种错误处理可以挂在原型上,也方便后续统一进行错误信息采集
// TSX 的话其实可以考虑写成装饰器,这样甚至不用写 try catch
try {
} catch (err) {
this.$idt.error(err);
}
// 如果要在线上看到 log 的话,建议将 log 包装起来,然后可以在 localStorage 里存储一个 debug 字段
// debug 为 true 时,才会在控制台上展示对应的 log,可以不直接在打包的时候去掉 log
# 禁止的写法
- 查询出来的数据可能不存在的情况,不要直接进行数据处理
let arr = [{ test: "idt", detail: [] }];
let detail = arr.find(i => i.test === "idt").detail; // X
let findOne = arr.find(i => i.test === "idt");
let idtDetail = findOne.detail || []; // √ 如果不确定有数据的话,可以在这里做一次兼容方案
# 加餐:常用工具
场景 | 工具 |
---|---|
画图 | draw.io |
文档撰写 | confluence + 仓库 Readme.md |
代码规范 | eslint + prettier + stylelint + commitlint + JSDoc + 风格指南 |
常用工具库 | 时间日期处理 - day.js |
工具函数库 - lodash | |
计算精度库 - math.js/numeral | |
编辑器 - quill-editor | |
标框工具 - pixi.js // 简单的场景可以手动用 Dom 渲染 | |
图表库 - echarts | |
地图 - Amap | |
PC 组件库 - element-ui | |
Mobile 组件库 - cube-ui/vant 后续需要统一用一个就好 | |
CSS 处理器 - scss + postcss |
...持续补充中