前端代码审查(Code Review)
针对目录结构、SCSS规范、JS规范、Vue规范
具体实践规范
1、POST/PUT/DELETE 请求按钮需要添加 loading 状态,防止重复提交。
建议使用 Element UI 提供的button 组件的loading属性,或者自己封装一个 loading 状态的按钮组件。
html
<el-button type="primary" :loading="loading" @click="handleSubmit"> 提交 </el-button>
2、模板上超过两个的判断条件,写成方法或者computed
html
<!--bad-->
<template>
<t-table v-if="satus==1&&orderStatus==2&&isShowTable"/>
</template>
<!--good-->
<template>
<t-table v-if="isChangeAvailiable"/>
</template>
<script>
computed: {
isChangeAvailiable() {
return (
this.satus==1&&this.orderStatus==2&&this.isShowTable
);
},
},
</script>
3、可选链访问数组/对象元素
js
//bad
cosnt obj = {}
cosnt b = obj.a && obj.a.b
console.log(b) // undefined
//good
cosnt obj = {}
cosnt b = obj?.a?.b
console.log(b) // undefined
4、定时器及时清理
js
mounted () {
this.timer = setInterval(() => {
...
}, 1000)
}
destroyed () {
if (this.timer) {
clearInterval(this.timer)
}
}
5、window/body上的监听事件--需要解绑
js
mounted() {
window.addEventListener('resize', this.fun)
}
beforeDestroy () {
window.removeEventListener('resize', this.fun);
}
6、async await 结合使用(调用接口)
js
export default {
created() {
this.getOrderNo()
},
methods:{
async getOrderNo() {
const res = await this.$api.getOrderNo()
if(res.success){
// 成功处理
}
}
}
}
7、使用try...catch...时--错误代码需要提示
js
try {
// 成功处理
} catch (error) {
// 处理异常的代码
this.$message.error(error.message)
}
8、函数有很多参数,需要封装成一个对象
js
// bad--->这个方式参数就必须按顺序传递
const getUserInfo =(name,age,sex,mobile,hobby)=> {
// 函数逻辑
}
// good
const getUserInfo =(userInfo)=> {
// 函数逻辑
const {name,age,sex,mobile,hobby} = userInfo
}
9、简化switch case判断
js
// bad
const counter =(state=0,action)=>{
switch (action.type) {
case 'ADD':
return state + 1
case 'MINUS':
return state - 1
default:
return state
}
}
// good
const counter =(state=0,action)=>{
const step={
'ADD':1,
'MINUS':-1
}
return state + (step[action.type] ?? 0)
}
10、判断条件过多需要提取出来
js
// bad
const checkGameStatus =()=>{
if(status===0||(satuas===1&&isEnd===1)||(isEnd===2)){
// 调用
}
}
// good
const isGaneOver =()=>{
return (status===0||(satuas===1&&isEnd===1)||(isEnd===2))
}
const checkGameStatus =()=>{
if(isGameOver()){
// 调用
}
}
11、if 判断嵌套--->错误前置
js
// bad
const publishPost =(post)=>{
if(isLoggenIn){
if(post){
if(isPostValid()){
doPublishPost(post)
}else{
throw new Error('文章不合法')
}
}else{
throw new Error('文章不能为空')
}
}else{
throw new Error('用户未登录')
}
}
// good
const publishPost =(post)=>{
if(!isLoggenIn){
throw new Error('用户未登录')
}
if(!post){
throw new Error('文章不能为空')
}
if(!isPostValid()){
throw new Error('文章不合法')
}
doPublishPost(post)
}
// bad
const createElement =(item)=>{
if(item.type==='ball'){
cosnt div = document.createElement('div')
div.className = 'ball'
div.style.backgroundColor = item.color
return div
}else if(item.type==='block'){
const div = document.createElement('div')
div.className = 'block'
div.style.backgroundColor = item.color
return div
}else if(item.type==='square'){
const div = document.createElement('div')
div.className = 'square'
div.style.backgroundColor = item.color
return div
}else{
throw new Error('未知元素类型')
}
}
// good
cosnt createElement =(item)=>{
const validTypes = ['ball', 'block', 'image']
if(!validTypes.includes(item.type)){
throw new Error('未知元素类型')
}
cosnt div = document.createElement('div')
div.className = item.type
div.style.backgroundColor = item.color
return div
}
// bad
let commodity = {
phone: '手机',
computer: '电脑',
television: '电视',
gameBoy: '游戏机',
}
function price(name) {
if (name === commodity.phone) {
console.log(1999)
} else if (name === commodity.computer) {
console.log(9999)
} else if (name === commodity.television) {
console.log(2999)
} else if (name === commodity.gameBoy) {
console.log(3999)
}
}
price('手机') // 1999
// good
const commodity = new Map([
['phone', 1999],
['computer', 9999],
['television', 2999],
['gameBoy', 3999],
])
const price = (name) => {
return commodity.get(name)
}
price('phone') // 1999
补充常规的--->目录结构规范:
项目根目录下创建 src 目录,src 目录下创建 api 目录、assets 目录、components 目录、directive 目录、router 目录、store 目录、utils 目录、views 目录。
1、api 目录存放所有页面API。
建议将每个页面的API封装成一个单独的js文件,文件名与页面名称相同(防止增删查改接口命名重复),并且都放在api下的modules目录下。
js
import request from '@/utils/request'
export function afterSaleApplyRefund(data) {
return request({
url: `/web/refundApplyOrder/applyRefund`,
method: 'put',
data
})
}
export function getStoreList(params) {
return request({
url: `/webWaterStore/getMarkStoreTree`,
method: 'get',
params
})
}
....
建议API目录下新建index.js文件,用于统一导出所有API,在main.js引入并将api挂载到vue的原型上
Vue.prototype.$api = api
;在页面直接使用this.$api.xxx
调用接口。
WebPack自动加载配置API(使用require.context)
js
// 自动加载api
const commonApiObj = {}
const finalObj = {}
const modulesApi = require.context('./modules', true, /\.js$/)
modulesApi.keys().forEach(key => {
const newKey = key.replace(/(\.\/|\.js)/g, '')
commonApiObj[newKey] = require(`./modules/${newKey}`)
})
Object.values(commonApiObj).map(x => Object.assign(finalObj, x))
// console.log('所有业务接口--', finalObj)
export default {
...finalObj
}
Vite自动加载配置API(使用import.meta.globEager)
(注册全局api方法 )instance.config.globalProperties.$api = api;
js
// 自动导入modules
const files: any = import.meta.globEager("./modules/*.ts");
let modules: any = {};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Object.entries(files).forEach(([k, v]) => {
Object.assign(modules, v);
});
export default {
...modules
};
js
// useApi
import { ComponentInternalInstance, getCurrentInstance } from "vue";
export default function useApi() {
const { appContext } = getCurrentInstance() as ComponentInternalInstance;
const proxy = appContext.config.globalProperties;
return {
proxy
};
}
js
//页面使用
<script setup lang="ts">
import useApi from "@/hooks/useApi";
const { proxy } = useApi();
const getData = async () => {
const res = await proxy.$api.xxx(接口名);
if (res.success) {
...
}
}
</script>
2、assets 目录存放静态资源,如图片、字体、公共scss等。
3、components 目录存放公共组件(store也可以如下方式自动导入)。
建议将公共组件拆分为基础组件(baseComponents)和业务组件(pageComponents),基础组件存放一些通用的组件,如按钮、输入框、表格等,业务组件存放与具体业务相关的组件,如用户管理组件、权限管理组件等。
基础组件命名方式大驼峰,如:TTable;业务组件命名方式是小驼峰,如:importExcel。
组件文件夹下必须包含index.vue文件,index.vue文件中必须包含组件的name属性,name属性值必须与组件文件夹名一致。
基础组件复用性高,通常情况都是全局注册
components 目录下的index.js--全局导入
js
import Vue from 'vue'
// 全局自动注册baseComponents下的基础组件
const requireComponent = require.context('./baseComponents', true, /\.vue$/)
// 找到组件文件夹下以.vue命名的文件,如果文件名为index,那么取组件中的name作为注册的组件名
requireComponent.keys().forEach(filePath => {
const componentConfig = requireComponent(filePath)
const fileName = validateFileName(filePath)
const componentName = fileName.toLowerCase() === 'index'
? capitalizeFirstLetter(componentConfig.default.name)
: fileName
Vue.component(componentName, componentConfig.default || componentConfig)
})
//首字母大写
function capitalizeFirstLetter (str) {
return str && str.charAt(0).toUpperCase() + str.slice(1)
}
// 对符合'xx/xx.vue'组件格式的组件取组件名
function validateFileName (str) {
return /^\S+\.vue$/.test(str) &&
str.replace(/^\S+\/(\w+)\.vue$/, (rs, $1) => capitalizeFirstLetter($1))
}
全局注册main.js
js
import '@/components/index.js' // 全局基础组件注入
页面组件使用
html
<template>
<div>
<t-table></t-table>
</div>
</template>
4、utils 目录存放公共方法,如全局loading,axios封装,正则校验等。
axios封装(request.js)
js
import axios from 'axios'
import { Notification, MessageBox, Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
export default function (config) {
// 创建axios实例
const service = axios.create({
// baseURL: process.env.VUE_APP_BASE_API,
baseURL: process.env.VUE_APP_BASE_API ,
// 超时 b
timeout: 50000
})
// request拦截器
service.interceptors.request.use(
config => {
getToken() && (config.headers['Authorization'] = getToken())
localStorage.getItem('store_id') && (config.headers['Store-Id'] = localStorage.getItem('store_id'))
config.headers['Content-Type'] = config.headers['Content-Type'] || 'application/json'
// 8080
if (config.type == 'file') {
config.headers['content-type'] = 'application/multipart/form-data'
} else if (config.type == 'form') {
config.headers['Content-type'] = 'application/x-www-form-urlencoded'
}
if (config.method.toLowerCase() === 'get') {
config.data = true
}
return config
},
error => {
console.log(error)
Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(res => {
const code = res.data.code
if (code === 401) {
MessageBox.confirm(
'登录状态已过期,您可以继续留在该页面,或者重新登录',
'系统提示', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
store.dispatch('FedLogOut').then(() => {
if (!window.__POWERED_BY_QIANKUN__) {
// 为了重新实例化vue-router对象 避免bug
location.reload()
} else {
window.location.href = '/'
}
})
})
} else if (code !== 200) {
Notification.error({
title: res.data.msg
})
return Promise.reject('error')
} else {
return res.data
}
},
error => {
console.log('err' + error)
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
return service(config)
}