Skip to content

前端开发规范

6126字约20分钟

开发规范前端阿里巴巴代码规范

2024-11-16

前言

本规范基于阿里巴巴前端开发手册,结合现代前端开发最佳实践制定。规范的目的是提高代码质量,增强代码可读性,提升团队协作效率。

1. 命名规范

1.1 文件命名

  • 使用 kebab-case 命名法
  • 组件文件使用 PascalCase
  • 工具文件使用 camelCase
// 正例
user-profile.vue          // 页面组件
UserProfile.vue           // 组件文件
user-service.js           // 服务文件
utils.js                  // 工具文件
api-config.js             // API配置文件

// 反例
userProfile.vue           // 不符合kebab-case
user_profile.vue          // 不符合kebab-case
User-profile.vue          // 混合命名

1.2 变量命名

  • 使用 camelCase 命名法
  • 常量使用 UPPER_SNAKE_CASE
  • 布尔值使用 is/has/can 前缀
// 正例
const userName = 'admin';
const userAge = 25;
const isActive = true;
const hasPermission = false;
const canEdit = true;
const MAX_RETRY_COUNT = 3;
const DEFAULT_PAGE_SIZE = 20;
const API_BASE_URL = 'https://api.example.com';

// 反例
const username = 'admin';        // 不符合camelCase
const user_age = 25;             // 不符合camelCase
const active = true;             // 布尔值没有前缀
const permission = false;        // 布尔值没有前缀
const maxRetryCount = 3;         // 常量应该大写

1.3 组件命名

  • 组件名使用 PascalCase
  • 基础组件以 Base 开头
  • 单例组件以 The 开头
// 正例
export default {
  name: 'UserProfile'
}

// 基础组件
BaseButton.vue
BaseInput.vue
BaseModal.vue

// 单例组件
TheHeader.vue
TheFooter.vue
TheSidebar.vue

// 反例
export default {
  name: 'userProfile'     // 不符合PascalCase
}

// 组件文件名
baseButton.vue            // 不符合PascalCase
theHeader.vue             // 不符合PascalCase

2. 代码格式

2.1 缩进

  • 使用 2 个空格缩进
  • 禁止使用 tab 字符
  • 保持一致的缩进风格
// 正例
function getUserInfo(userId) {
  if (userId) {
    return fetch(`/api/users/${userId}`)
      .then(response => response.json())
      .then(data => {
        return data;
      });
  }
  return null;
}

// 反例
function getUserInfo(userId) {
	if (userId) {                    // 使用tab缩进
		return fetch(`/api/users/${userId}`)
			.then(response => response.json())
			.then(data => {
				return data;
			});
	}
	return null;
}

2.2 分号

  • 语句末尾必须加分号
  • 函数声明和类声明不加分号
  • 保持一致性
// 正例
const userName = 'admin';
const userAge = 25;

function getUserInfo() {
  return { name: userName, age: userAge };
}

class UserService {
  constructor() {
    this.baseUrl = '/api/users';
  }
  
  getUsers() {
    return fetch(this.baseUrl);
  }
}

// 反例
const userName = 'admin'           // 缺少分号
const userAge = 25                 // 缺少分号

function getUserInfo() {            // 函数声明不加分号是正确的
  return { name: userName, age: userAge }
}                                  // 但函数体中的return语句应该加分号

2.3 引号

  • 统一使用单引号
  • 模板字符串使用反引号
  • 避免使用双引号
// 正例
const message = 'Hello World';
const userName = 'admin';
const apiUrl = `/api/users/${userId}`;
const fullName = `${firstName} ${lastName}`;

// 反例
const message = "Hello World";     // 使用双引号
const userName = "admin";           // 使用双引号
const apiUrl = '/api/users/' + userId;  // 不使用模板字符串

2.4 空格和换行

  • 运算符左右加空格
  • 逗号后面加空格
  • 对象和数组的括号内加空格
  • 函数参数之间加空格
// 正例
const result = a + b;
const user = { name: 'admin', age: 25 };
const numbers = [1, 2, 3, 4, 5];

function createUser(name, age, email) {
  return { name, age, email };
}

// 反例
const result=a+b;                  // 运算符左右没有空格
const user={name:'admin',age:25};  // 对象括号内没有空格
const numbers=[1,2,3,4,5];        // 数组括号内没有空格

function createUser(name,age,email) {  // 参数之间没有空格
  return {name,age,email};
}

3. 注释规范

3.1 文件注释

  • 文件开头添加文件描述
  • 包含作者、创建时间等信息
  • 说明文件的主要功能和用途
/**
 * 用户管理组件
 * 
 * 提供用户的增删改查功能,包括用户列表展示、用户信息编辑、
 * 用户权限管理等核心功能。
 * 
 * @author 张三
 * @date 2024-11-16
 * @version 1.0.0
 * @since 1.0.0
 */

import { ref, reactive, onMounted } from 'vue';
import { getUserList, createUser, updateUser, deleteUser } from '@/api/user';

export default {
  name: 'UserManagement',
  // 组件实现...
};

3.2 函数注释

  • 使用 JSDoc 规范
  • 包含功能描述、参数、返回值说明
  • 对于复杂的业务逻辑,应该详细说明实现思路
/**
 * 获取用户信息
 * 
 * 根据用户ID从服务器获取用户详细信息,包括基本信息、权限信息等。
 * 如果用户不存在,返回null。
 * 
 * @param {number} userId - 用户ID,必须大于0
 * @param {boolean} [includePermissions=false] - 是否包含权限信息
 * @returns {Promise<Object|null>} 用户信息对象,如果用户不存在返回null
 * @throws {Error} 当userId无效或网络请求失败时抛出
 * 
 * @example
 * // 获取用户基本信息
 * const user = await getUserInfo(123);
 * 
 * // 获取用户信息,包含权限
 * const userWithPermissions = await getUserInfo(123, true);
 */
async function getUserInfo(userId, includePermissions = false) {
  // 参数验证
  if (!userId || userId <= 0) {
    throw new Error('用户ID必须大于0');
  }
  
  try {
    const url = `/api/users/${userId}?includePermissions=${includePermissions}`;
    const response = await fetch(url);
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    
    const user = await response.json();
    return user;
  } catch (error) {
    console.error('获取用户信息失败:', error);
    throw error;
  }
}

3.3 代码注释

  • 复杂逻辑必须添加注释,说明实现思路
  • 注释要简洁明了,避免废话
  • 对于算法、业务规则等复杂逻辑,应该详细注释
// 计算用户积分
// 积分规则:基础积分100 + 注册天数 * 2 + 活跃天数 * 1
const baseScore = 100;
const registerDays = Math.floor((Date.now() - user.registerTime) / (24 * 60 * 60 * 1000));
const activeDays = user.activeDays;
let totalScore = baseScore + registerDays * 2 + activeDays;

// 根据用户等级调整积分
if (user.level === 'VIP') {
  totalScore = Math.floor(totalScore * 1.5); // VIP用户积分1.5倍
} else if (user.level === 'SUPER_VIP') {
  totalScore = totalScore * 2; // 超级VIP用户积分2倍
}

// 使用防抖优化搜索功能
// 避免用户输入过程中频繁发送请求
const debouncedSearch = debounce(async (keyword) => {
  if (keyword.trim().length < 2) {
    return []; // 关键词太短,不搜索
  }
  
  try {
    const results = await searchUsers(keyword);
    return results;
  } catch (error) {
    console.error('搜索失败:', error);
    return [];
  }
}, 300); // 300ms防抖延迟

// 防抖函数实现
function debounce(func, delay) {
  let timeoutId;
  return function (...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => func.apply(this, args), delay);
  };
}

3.4 组件注释

  • 组件props要有详细的说明
  • 组件事件要有说明
  • 组件的使用示例要有注释
export default {
  name: 'UserTable',
  
  props: {
    // 用户数据列表
    users: {
      type: Array,
      required: true,
      default: () => []
    },
    
    // 是否显示操作列
    showActions: {
      type: Boolean,
      default: true
    },
    
    // 每页显示数量
    pageSize: {
      type: Number,
      default: 20,
      validator: value => value > 0 && value <= 100
    }
  },
  
  emits: {
    // 用户选择事件
    'user-select': (user) => {
      return user && typeof user.id === 'number';
    },
    
    // 用户删除事件
    'user-delete': (userId) => {
      return userId && userId > 0;
    }
  },
  
  // 组件实现...
};

4. Vue 组件规范

4.1 组件结构

  • 按以下顺序组织组件代码:
    1. name
    2. components
    3. props
    4. data
    5. computed
    6. watch
    7. 生命周期钩子
    8. methods
export default {
  name: 'UserManagement',
  
  components: {
    UserTable: () => import('@/components/UserTable.vue'),
    UserForm: () => import('@/components/UserForm.vue'),
    BaseButton: () => import('@/components/BaseButton.vue')
  },
  
  props: {
    // props定义...
  },
  
  data() {
    return {
      // 响应式数据...
    };
  },
  
  computed: {
    // 计算属性...
  },
  
  watch: {
    // 侦听器...
  },
  
  mounted() {
    // 组件挂载后执行...
  },
  
  methods: {
    // 方法定义...
  }
};

4.2 Props 定义

  • 必须指定类型
  • 提供默认值
  • 添加验证规则
  • 使用 camelCase 命名
export default {
  props: {
    // 用户数据
    user: {
      type: Object,
      required: true,
      validator: (value) => {
        return value && typeof value.id === 'number' && value.username;
      }
    },
    
    // 是否可编辑
    editable: {
      type: Boolean,
      default: false
    },
    
    // 用户状态
    status: {
      type: String,
      default: 'active',
      validator: (value) => {
        return ['active', 'inactive', 'pending'].includes(value);
      }
    },
    
    // 用户角色列表
    roles: {
      type: Array,
      default: () => [],
      validator: (value) => {
        return Array.isArray(value) && value.every(role => typeof role === 'string');
      }
    }
  }
};

4.3 事件命名

  • 使用 kebab-case 命名
  • 避免缩写
  • 事件名要有意义
// 正例
// 使用 kebab-case 命名事件
this.$emit('user-select', selectedUser);
this.$emit('user-delete', userId);
this.$emit('form-submit', formData);
this.$emit('search-change', keyword);
this.$emit('page-change', pageNumber);

// 反例
// 使用缩写或不符合命名规范
this.$emit('select', selectedUser);        // 缩写
this.$emit('delete', userId);              // 缩写
this.$emit('submit', formData);            // 缩写
this.$emit('search', keyword);             // 缩写
this.$emit('page', pageNumber);            // 缩写

4.4 组件通信

  • 使用 props 向下传递数据
  • 使用 events 向上传递数据
  • 复杂状态使用 Vuex 或 Pinia
  • 避免使用 $parent 和 $children
// 父组件
<template>
  <div class="user-management">
    <UserTable 
      :users="users"
      :loading="loading"
      @user-select="handleUserSelect"
      @user-delete="handleUserDelete"
    />
  </div>
</template>

<script>
export default {
  data() {
    return {
      users: [],
      loading: false
    };
  },
  
  methods: {
    handleUserSelect(user) {
      this.selectedUser = user;
      this.showUserDetail = true;
    },
    
    handleUserDelete(userId) {
      this.$confirm('确定要删除这个用户吗?').then(() => {
        this.deleteUser(userId);
      });
    }
  }
};
</script>

// 子组件
<template>
  <div class="user-table">
    <table>
      <tbody>
        <tr 
          v-for="user in users" 
          :key="user.id"
          @click="$emit('user-select', user)"
        >
          <td>{{ user.username }}</td>
          <td>{{ user.email }}</td>
          <td>
            <BaseButton 
              @click.stop="$emit('user-delete', user.id)"
              type="danger"
              size="small"
            >
              删除
            </BaseButton>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script>
export default {
  props: {
    users: {
      type: Array,
      required: true
    }
  },
  
  emits: ['user-select', 'user-delete']
};
</script>

4.5 生命周期钩子

  • 合理使用生命周期钩子
  • 在 created 中初始化数据
  • 在 mounted 中进行DOM操作
  • 在 beforeDestroy 中清理资源
export default {
  data() {
    return {
      users: [],
      loading: false,
      timer: null
    };
  },
  
  created() {
    // 初始化数据,不依赖DOM
    this.initializeData();
  },
  
  mounted() {
    // DOM挂载完成后执行
    this.setupEventListeners();
    this.startAutoRefresh();
  },
  
  beforeDestroy() {
    // 组件销毁前清理资源
    this.cleanup();
  },
  
  methods: {
    initializeData() {
      this.loadUsers();
    },
    
    setupEventListeners() {
      // 设置事件监听器
      window.addEventListener('resize', this.handleResize);
    },
    
    startAutoRefresh() {
      // 启动自动刷新
      this.timer = setInterval(() => {
        this.refreshData();
      }, 30000);
    },
    
    cleanup() {
      // 清理定时器和事件监听器
      if (this.timer) {
        clearInterval(this.timer);
        this.timer = null;
      }
      
      window.removeEventListener('resize', this.handleResize);
    }
  }
};

5. CSS 规范

5.1 选择器命名

  • 使用 BEM 命名法(Block, Element, Modifier)
  • 避免过深的选择器嵌套(不超过3层)
  • 使用语义化的类名
/* 正例:使用 BEM 命名法 */
.user-profile {                    /* Block */
  padding: 20px;
  background: #fff;
}

.user-profile__avatar {            /* Element */
  width: 80px;
  height: 80px;
  border-radius: 50%;
}

.user-profile__avatar--large {     /* Modifier */
  width: 120px;
  height: 120px;
}

.user-profile__info {              /* Element */
  margin-left: 20px;
}

.user-profile__name {              /* Element */
  font-size: 18px;
  font-weight: bold;
}

.user-profile__name--highlighted { /* Modifier */
  color: #1890ff;
}

/* 反例:过深的选择器嵌套 */
.user-management .user-list .user-item .user-info .user-name {
  color: #333;
}

/* 反例:不符合 BEM 命名法 */
.userProfile {                     /* 不符合kebab-case */
  padding: 20px;
}

.user_profile {                    /* 使用下划线 */
  padding: 20px;
}

5.2 属性顺序

  • 布局属性:display, position, top, right, bottom, left, z-index
  • 盒模型:width, height, margin, padding, border
  • 文字:font, line-height, text-align, color
  • 视觉:background, opacity, transform, transition
/* 正例:属性顺序规范 */
.user-card {
  /* 布局属性 */
  display: flex;
  position: relative;
  z-index: 1;
  
  /* 盒模型 */
  width: 300px;
  height: 200px;
  margin: 20px;
  padding: 16px;
  border: 1px solid #e8e8e8;
  border-radius: 8px;
  
  /* 文字 */
  font-size: 14px;
  line-height: 1.5;
  text-align: center;
  color: #333;
  
  /* 视觉 */
  background: #fff;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  transition: all 0.3s ease;
}

/* 反例:属性顺序混乱 */
.user-card {
  color: #333;
  padding: 16px;
  display: flex;
  background: #fff;
  width: 300px;
  margin: 20px;
  font-size: 14px;
}

5.3 响应式设计

  • 使用 rem/em 单位,避免使用 px
  • 设置合理的断点
  • 优先使用 flexbox 布局
  • 使用 CSS Grid 进行复杂布局
/* 基础设置 */
html {
  font-size: 16px; /* 1rem = 16px */
}

/* 响应式断点 */
/* 移动端优先 */
.container {
  width: 100%;
  padding: 0 1rem; /* 16px */
  margin: 0 auto;
}

/* 平板 */
@media (min-width: 768px) {
  .container {
    max-width: 720px;
    padding: 0 1.5rem; /* 24px */
  }
}

/* 桌面端 */
@media (min-width: 1024px) {
  .container {
    max-width: 960px;
    padding: 0 2rem; /* 32px */
  }
}

/* 大屏幕 */
@media (min-width: 1200px) {
  .container {
    max-width: 1140px;
  }
}

/* 使用 Flexbox 布局 */
.user-list {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

@media (min-width: 768px) {
  .user-list {
    flex-direction: row;
    flex-wrap: wrap;
  }
  
  .user-item {
    flex: 0 0 calc(50% - 0.5rem);
  }
}

@media (min-width: 1024px) {
  .user-item {
    flex: 0 0 calc(33.333% - 0.667rem);
  }
}

/* 使用 CSS Grid 进行复杂布局 */
.dashboard {
  display: grid;
  grid-template-columns: 1fr;
  grid-gap: 1rem;
  padding: 1rem;
}

@media (min-width: 768px) {
  .dashboard {
    grid-template-columns: 1fr 1fr;
    grid-template-rows: auto auto;
  }
  
  .dashboard__header {
    grid-column: 1 / -1;
  }
  
  .dashboard__sidebar {
    grid-row: 2 / 3;
  }
}

@media (min-width: 1024px) {
  .dashboard {
    grid-template-columns: 250px 1fr 300px;
    grid-template-rows: auto 1fr;
  }
  
  .dashboard__header {
    grid-column: 1 / -1;
  }
  
  .dashboard__sidebar {
    grid-column: 1 / 2;
    grid-row: 2 / 3;
  }
  
  .dashboard__main {
    grid-column: 2 / 3;
    grid-row: 2 / 3;
  }
  
  .dashboard__widgets {
    grid-column: 3 / 4;
    grid-row: 2 / 3;
  }
}

5.4 CSS 变量和主题

  • 使用 CSS 变量定义主题色彩
  • 支持明暗主题切换
  • 使用语义化的变量名
/* 定义 CSS 变量 */
:root {
  /* 主色调 */
  --primary-color: #1890ff;
  --primary-hover: #40a9ff;
  --primary-active: #096dd9;
  
  /* 文字颜色 */
  --text-primary: #262626;
  --text-secondary: #595959;
  --text-disabled: #bfbfbf;
  
  /* 背景颜色 */
  --bg-primary: #ffffff;
  --bg-secondary: #fafafa;
  --bg-tertiary: #f5f5f5;
  
  /* 边框颜色 */
  --border-color: #d9d9d9;
  --border-color-light: #f0f0f0;
  
  /* 间距 */
  --spacing-xs: 0.25rem;  /* 4px */
  --spacing-sm: 0.5rem;   /* 8px */
  --spacing-md: 1rem;     /* 16px */
  --spacing-lg: 1.5rem;   /* 24px */
  --spacing-xl: 2rem;     /* 32px */
  
  /* 圆角 */
  --border-radius-sm: 4px;
  --border-radius-md: 8px;
  --border-radius-lg: 16px;
  
  /* 阴影 */
  --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
  --shadow-md: 0 4px 8px rgba(0, 0, 0, 0.1);
  --shadow-lg: 0 8px 16px rgba(0, 0, 0, 0.1);
}

/* 暗色主题 */
[data-theme="dark"] {
  --primary-color: #177ddc;
  --primary-hover: #1890ff;
  --primary-active: #0958b5;
  
  --text-primary: #ffffff;
  --text-secondary: #a6a6a6;
  --text-disabled: #595959;
  
  --bg-primary: #141414;
  --bg-secondary: #1f1f1f;
  --bg-tertiary: #262626;
  
  --border-color: #434343;
  --border-color-light: #303030;
}

/* 使用 CSS 变量 */
.button {
  background: var(--primary-color);
  color: #fff;
  padding: var(--spacing-sm) var(--spacing-md);
  border-radius: var(--border-radius-sm);
  border: none;
  cursor: pointer;
  transition: background-color 0.3s ease;
}

.button:hover {
  background: var(--primary-hover);
}

.button:active {
  background: var(--primary-active);
}

.card {
  background: var(--bg-primary);
  border: 1px solid var(--border-color);
  border-radius: var(--border-radius-md);
  padding: var(--spacing-md);
  box-shadow: var(--shadow-sm);
}

.text-primary {
  color: var(--text-primary);
}

.text-secondary {
  color: var(--text-secondary);
}

6. JavaScript 规范

6.1 变量声明

  • 优先使用 const,其次使用 let
  • 避免使用 var
  • 使用解构赋值简化代码
// 正例
// 优先使用 const
const API_BASE_URL = 'https://api.example.com';
const DEFAULT_CONFIG = {
  timeout: 5000,
  retries: 3
};

// 使用 let 声明会变化的变量
let currentUser = null;
let isLoading = false;

// 使用解构赋值
const { name, age, email } = user;
const [first, second, ...rest] = numbers;

// 反例
// 使用 var,作用域混乱
var userName = 'admin';
var userAge = 25;

// 不使用解构赋值
const name = user.name;
const age = user.age;
const email = user.email;

6.2 函数定义

  • 优先使用箭头函数
  • 避免使用 arguments 对象
  • 使用默认参数和剩余参数
  • 使用函数式编程思想
// 正例
// 箭头函数
const getUserInfo = async (userId) => {
  try {
    const response = await fetch(`/api/users/${userId}`);
    return await response.json();
  } catch (error) {
    console.error('获取用户信息失败:', error);
    throw error;
  }
};

// 默认参数
const createUser = (name, age = 18, email = '') => {
  return { name, age, email };
};

// 剩余参数
const sum = (...numbers) => {
  return numbers.reduce((total, num) => total + num, 0);
};

// 高阶函数
const withLoading = (asyncFn) => {
  return async (...args) => {
    isLoading.value = true;
    try {
      const result = await asyncFn(...args);
      return result;
    } finally {
      isLoading.value = false;
    }
  };
};

// 反例
// 使用 function 关键字,不够简洁
function getUserInfo(userId) {
  // 函数实现
}

// 使用 arguments 对象
function sum() {
  let total = 0;
  for (let i = 0; i < arguments.length; i++) {
    total += arguments[i];
  }
  return total;
}

6.3 异步处理

  • 使用 async/await 替代 Promise.then()
  • 避免回调地狱
  • 合理使用 Promise.all() 和 Promise.race()
  • 错误处理要完善
// 正例
// 使用 async/await
async function fetchUserData(userIds) {
  try {
    const promises = userIds.map(id => fetch(`/api/users/${id}`));
    const responses = await Promise.all(promises);
    
    const users = await Promise.all(
      responses.map(response => response.json())
    );
    
    return users;
  } catch (error) {
    console.error('获取用户数据失败:', error);
    throw new Error('获取用户数据失败');
  }
}

// 使用 Promise.allSettled 处理部分失败的情况
async function fetchUserDataWithFallback(userIds) {
  const promises = userIds.map(async (id) => {
    try {
      const response = await fetch(`/api/users/${id}`);
      return { status: 'fulfilled', value: await response.json() };
    } catch (error) {
      return { status: 'rejected', reason: error };
    }
  });
  
  const results = await Promise.allSettled(promises);
  return results;
}

// 使用 Promise.race 实现超时控制
async function fetchWithTimeout(url, timeout = 5000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);
  
  try {
    const response = await fetch(url, {
      signal: controller.signal
    });
    clearTimeout(timeoutId);
    return await response.json();
  } catch (error) {
    clearTimeout(timeoutId);
    if (error.name === 'AbortError') {
      throw new Error('请求超时');
    }
    throw error;
  }
}

// 反例
// 回调地狱
function fetchUserData(userIds, callback) {
  const results = [];
  let completed = 0;
  
  userIds.forEach((id, index) => {
    fetch(`/api/users/${id}`)
      .then(response => response.json())
      .then(user => {
        results[index] = user;
        completed++;
        
        if (completed === userIds.length) {
          callback(null, results);
        }
      })
      .catch(error => {
        callback(error);
      });
  });
}

// 没有错误处理
async function fetchUserData(userIds) {
  const promises = userIds.map(id => fetch(`/api/users/${id}`));
  const responses = await Promise.all(promises);
  const users = await Promise.all(
    responses.map(response => response.json())
  );
  return users; // 没有错误处理
}

6.4 模块化

  • 使用 ES6 模块语法
  • 合理组织模块结构
  • 避免循环依赖
  • 使用命名导出和默认导出
// 正例
// 使用命名导出
export const API_BASE_URL = 'https://api.example.com';
export const DEFAULT_CONFIG = { timeout: 5000 };

export function formatDate(date) {
  return new Date(date).toLocaleDateString();
}

export class UserService {
  constructor() {
    this.baseUrl = API_BASE_URL;
  }
  
  async getUsers() {
    const response = await fetch(`${this.baseUrl}/users`);
    return response.json();
  }
}

// 默认导出
export default UserService;

// 反例
// 使用 CommonJS 语法
module.exports = UserService;

// 混合使用
export default UserService;
module.exports = UserService;

6.5 错误处理

  • 使用 try-catch 捕获异步错误
  • 创建自定义错误类
  • 提供有意义的错误信息
  • 记录错误日志
// 自定义错误类
class ApiError extends Error {
  constructor(message, statusCode, details = null) {
    super(message);
    this.name = 'ApiError';
    this.statusCode = statusCode;
    this.details = details;
    this.timestamp = new Date().toISOString();
  }
}

class ValidationError extends Error {
  constructor(message, field, value) {
    super(message);
    this.name = 'ValidationError';
    this.field = field;
    this.value = value;
  }
}

// 错误处理工具函数
const handleApiError = (error) => {
  if (error instanceof ApiError) {
    console.error(`API错误 ${error.statusCode}:`, error.message);
    
    // 根据状态码处理不同错误
    switch (error.statusCode) {
      case 401:
        // 未授权,跳转到登录页
        router.push('/login');
        break;
      case 403:
        // 禁止访问,显示权限不足
        showMessage('权限不足', 'error');
        break;
      case 500:
        // 服务器错误,显示错误信息
        showMessage('服务器错误,请稍后重试', 'error');
        break;
      default:
        showMessage(error.message, 'error');
    }
  } else {
    console.error('未知错误:', error);
    showMessage('系统错误,请联系管理员', 'error');
  }
};

// 使用示例
async function createUser(userData) {
  try {
    // 参数验证
    if (!userData.name || userData.name.trim().length < 2) {
      throw new ValidationError('用户名至少2个字符', 'name', userData.name);
    }
    
    if (!userData.email || !isValidEmail(userData.email)) {
      throw new ValidationError('邮箱格式不正确', 'email', userData.email);
    }
    
    const response = await fetch('/api/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(userData)
    });
    
    if (!response.ok) {
      const errorData = await response.json();
      throw new ApiError(
        errorData.message || '创建用户失败',
        response.status,
        errorData
      );
    }
    
    return await response.json();
  } catch (error) {
    handleApiError(error);
    throw error;
  }
}

7. 性能优化

7.1 代码分割

  • 使用动态导入实现代码分割
  • 路由懒加载减少初始包大小
  • 组件按需加载提升性能
// 正例
// 路由懒加载
const routes = [
  {
    path: '/users',
    name: 'UserManagement',
    component: () => import('@/views/UserManagement.vue')
  },
  {
    path: '/reports',
    name: 'Reports',
    component: () => import('@/views/Reports.vue')
  }
];

// 组件按需加载
const UserTable = () => import('@/components/UserTable.vue');
const UserForm = () => import('@/components/UserForm.vue');

// 条件加载
const HeavyComponent = defineAsyncComponent(() => {
  if (process.env.NODE_ENV === 'development') {
    return import('@/components/HeavyComponent.vue');
  } else {
    return import('@/components/HeavyComponent.min.vue');
  }
});

// 反例
// 静态导入,增加初始包大小
import UserManagement from '@/views/UserManagement.vue';
import Reports from '@/views/Reports.vue';

7.2 资源优化

  • 图片懒加载减少初始加载时间
  • 使用 CDN 加速资源加载
  • 压缩静态资源减少传输大小
  • 使用 WebP 等现代图片格式
// 图片懒加载
const LazyImage = {
  props: {
    src: String,
    alt: String,
    placeholder: {
      type: String,
      default: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZjBmMGYwIi8+PC9zdmc+'
    }
  },
  
  data() {
    return {
      isLoaded: false,
      observer: null
    };
  },
  
  mounted() {
    this.setupIntersectionObserver();
  },
  
  beforeDestroy() {
    if (this.observer) {
      this.observer.disconnect();
    }
  },
  
  methods: {
    setupIntersectionObserver() {
      this.observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
          if (entry.isIntersecting) {
            this.loadImage();
            this.observer.unobserve(entry.target);
          }
        });
      });
      
      this.observer.observe(this.$el);
    },
    
    loadImage() {
      const img = new Image();
      img.onload = () => {
        this.isLoaded = true;
      };
      img.src = this.src;
    }
  },
  
  template: `
    <img 
      :src="isLoaded ? src : placeholder"
      :alt="alt"
      :class="{ 'lazy-loaded': isLoaded }"
    />
  `
};

// 使用示例
<template>
  <div class="user-avatars">
    <LazyImage 
      v-for="user in users"
      :key="user.id"
      :src="user.avatar"
      :alt="user.name"
    />
  </div>
</template>

7.3 渲染优化

  • 合理使用 v-show 和 v-if
  • 避免在模板中使用复杂表达式
  • 使用 key 优化列表渲染
  • 使用 computed 缓存计算结果
// 正例
export default {
  data() {
    return {
      users: [],
      showUserForm: false,
      searchKeyword: ''
    };
  },
  
  computed: {
    // 使用 computed 缓存过滤结果
    filteredUsers() {
      if (!this.searchKeyword) {
        return this.users;
      }
      
      const keyword = this.searchKeyword.toLowerCase();
      return this.users.filter(user => 
        user.name.toLowerCase().includes(keyword) ||
        user.email.toLowerCase().includes(keyword)
      );
    },
    
    // 缓存计算结果
    userStats() {
      const total = this.users.length;
      const active = this.users.filter(u => u.status === 'active').length;
      const inactive = total - active;
      
      return { total, active, inactive };
    }
  },
  
  template: `
    <div class="user-management">
      <!-- 使用 v-show 避免频繁创建销毁 -->
      <div v-show="showUserForm" class="user-form">
        <UserForm @submit="handleSubmit" @cancel="showUserForm = false" />
      </div>
      
      <!-- 使用 v-if 避免渲染不需要的内容 -->
      <div v-if="users.length === 0" class="empty-state">
        暂无用户数据
      </div>
      
      <!-- 使用 key 优化列表渲染 -->
      <div v-else class="user-list">
        <div 
          v-for="user in filteredUsers"
          :key="user.id"
          class="user-item"
        >
          <span>{{ user.name }}</span>
          <span>{{ user.email }}</span>
          <span>{{ user.status }}</span>
        </div>
      </div>
      
      <!-- 使用 computed 避免重复计算 -->
      <div class="user-stats">
        <span>总数: {{ userStats.total }}</span>
        <span>活跃: {{ userStats.active }}</span>
        <span>非活跃: {{ userStats.inactive }}</span>
      </div>
    </div>
  `
};

// 反例
export default {
  template: `
    <div class="user-management">
      <!-- 在模板中使用复杂表达式 -->
      <div class="user-stats">
        <span>总数: {{ users.length }}</span>
        <span>活跃: {{ users.filter(u => u.status === 'active').length }}</span>
        <span>非活跃: {{ users.filter(u => u.status !== 'active').length }}</span>
      </div>
      
      <!-- 没有使用 key -->
      <div v-for="user in users" class="user-item">
        {{ user.name }}
      </div>
      
      <!-- 频繁切换使用 v-if -->
      <div v-if="showUserForm" class="user-form">
        <UserForm />
      </div>
    </div>
  `
};

7.4 内存管理

  • 及时清理事件监听器
  • 避免内存泄漏
  • 使用 WeakMap 和 WeakSet
  • 合理使用闭包
// 正例
export default {
  data() {
    return {
      resizeHandler: null,
      scrollHandler: null,
      timer: null
    };
  },
  
  mounted() {
    // 绑定事件监听器
    this.resizeHandler = this.handleResize.bind(this);
    this.scrollHandler = this.handleScroll.bind(this);
    
    window.addEventListener('resize', this.resizeHandler);
    window.addEventListener('scroll', this.scrollHandler);
    
    // 启动定时器
    this.timer = setInterval(() => {
      this.updateData();
    }, 30000);
  },
  
  beforeDestroy() {
    // 清理事件监听器
    if (this.resizeHandler) {
      window.removeEventListener('resize', this.resizeHandler);
      this.resizeHandler = null;
    }
    
    if (this.scrollHandler) {
      window.removeEventListener('scroll', this.scrollHandler);
      this.scrollHandler = null;
    }
    
    // 清理定时器
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = null;
    }
  },
  
  methods: {
    handleResize() {
      // 处理窗口大小变化
    },
    
    handleScroll() {
      // 处理滚动事件
    },
    
    updateData() {
      // 更新数据
    }
  }
};

// 使用 WeakMap 避免内存泄漏
const userCache = new WeakMap();

function cacheUserData(user, data) {
  userCache.set(user, data);
}

function getUserData(user) {
  return userCache.get(user);
}

// 反例
export default {
  mounted() {
    // 直接绑定方法,无法清理
    window.addEventListener('resize', this.handleResize);
    
    // 定时器没有清理
    setInterval(() => {
      this.updateData();
    }, 30000);
  },
  
  // 没有 beforeDestroy 钩子清理资源
};

8. 安全规范

8.1 XSS 防护

  • 对用户输入进行转义
  • 使用 v-html 时要谨慎
  • 设置 CSP 策略
  • 使用安全的 DOM API
// 正例
// 使用 v-text 而不是 v-html
<template>
  <div>
    <!-- 安全:使用 v-text 自动转义 -->
    <span v-text="userInput"></span>
    
    <!-- 安全:使用 {{ }} 插值自动转义 -->
    <span>{{ userInput }}</span>
    
    <!-- 危险:使用 v-html 需要手动转义 -->
    <div v-html="sanitizedHtml"></div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      userInput: '<script>alert("xss")</script>'
    };
  },
  
  computed: {
    // 手动转义 HTML
    sanitizedHtml() {
      return this.escapeHtml(this.userInput);
    }
  },
  
  methods: {
    // HTML 转义函数
    escapeHtml(text) {
      const div = document.createElement('div');
      div.textContent = text;
      return div.innerHTML;
    },
    
    // 更安全的 HTML 转义
    sanitizeHtml(html) {
      const allowedTags = ['b', 'i', 'em', 'strong', 'a'];
      const allowedAttributes = ['href'];
      
      // 使用 DOMPurify 等库进行安全过滤
      return DOMPurify.sanitize(html, {
        ALLOWED_TAGS: allowedTags,
        ALLOWED_ATTR: allowedAttributes
      });
    }
  }
};
</script>

// 设置 CSP 策略
// 在 index.html 中添加
<meta http-equiv="Content-Security-Policy" 
      content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';">

// 反例
// 直接使用 v-html,存在 XSS 风险
<template>
  <div v-html="userInput"></div> <!-- 危险! -->
</template>

8.2 CSRF 防护

  • 使用 token 验证
  • 设置 SameSite 属性
  • 验证请求来源
  • 使用双重提交验证
// 正例
// 使用 CSRF Token
const apiClient = {
  baseURL: '/api',
  csrfToken: null,
  
  async init() {
    // 获取 CSRF Token
    const response = await fetch('/api/csrf-token');
    const data = await response.json();
    this.csrfToken = data.token;
  },
  
  async request(url, options = {}) {
    if (!this.csrfToken) {
      await this.init();
    }
    
    const config = {
      ...options,
      headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': this.csrfToken,
        ...options.headers
      },
      credentials: 'same-origin' // 包含 cookies
    };
    
    const response = await fetch(this.baseURL + url, config);
    
    // 检查 CSRF Token 是否过期
    if (response.status === 403) {
      await this.init();
      // 重试请求
      return this.request(url, options);
    }
    
    return response;
  }
};

// 使用示例
async function createUser(userData) {
  try {
    const response = await apiClient.request('/users', {
      method: 'POST',
      body: JSON.stringify(userData)
    });
    
    if (!response.ok) {
      throw new Error('创建用户失败');
    }
    
    return await response.json();
  } catch (error) {
    console.error('创建用户失败:', error);
    throw error;
  }
}

// 设置 SameSite 属性
// 在服务器端设置 cookie
// Set-Cookie: sessionId=abc123; SameSite=Strict; Secure; HttpOnly

// 反例
// 没有 CSRF 防护
async function createUser(userData) {
  const response = await fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(userData)
  });
  return response.json();
}

8.3 输入验证

  • 前端验证不能替代后端验证
  • 使用白名单验证
  • 对特殊字符进行转义
  • 限制输入长度和格式
// 正例
// 输入验证工具
const validators = {
  // 用户名验证
  username: {
    pattern: /^[a-zA-Z0-9_]{3,20}$/,
    message: '用户名只能包含字母、数字、下划线,长度3-20位'
  },
  
  // 邮箱验证
  email: {
    pattern: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
    message: '请输入正确的邮箱格式'
  },
  
  // 手机号验证
  phone: {
    pattern: /^1[3-9]\d{9}$/,
    message: '请输入正确的手机号格式'
  },
  
  // 密码验证
  password: {
    pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d@$!%*?&]{8,}$/,
    message: '密码必须包含大小写字母和数字,长度至少8位'
  }
};

// 验证函数
function validateInput(value, type) {
  const validator = validators[type];
  if (!validator) {
    return { isValid: true, message: '' };
  }
  
  if (!validator.pattern.test(value)) {
    return { isValid: false, message: validator.message };
  }
  
  return { isValid: true, message: '' };
}

// 使用示例
export default {
  data() {
    return {
      form: {
        username: '',
        email: '',
        phone: '',
        password: ''
      },
      errors: {}
    };
  },
  
  methods: {
    validateField(field) {
      const { isValid, message } = validateInput(this.form[field], field);
      
      if (isValid) {
        this.$delete(this.errors, field);
      } else {
        this.$set(this.errors, field, message);
      }
      
      return isValid;
    },
    
    validateForm() {
      const fields = Object.keys(this.form);
      const results = fields.map(field => this.validateField(field));
      return results.every(result => result);
    },
    
    async submitForm() {
      if (!this.validateForm()) {
        this.$message.error('请检查表单输入');
        return;
      }
      
      try {
        await createUser(this.form);
        this.$message.success('用户创建成功');
      } catch (error) {
        this.$message.error('用户创建失败');
      }
    }
  }
};

// 反例
// 没有输入验证
export default {
  methods: {
    async submitForm() {
      // 直接提交,没有验证
      await createUser(this.form);
    }
  }
};

8.4 敏感信息保护

  • 不在前端存储敏感信息
  • 使用 HTTPS 传输
  • 敏感操作需要二次验证
  • 实现安全的登出机制
// 正例
// 安全的用户信息管理
class UserManager {
  constructor() {
    this.user = null;
    this.token = null;
  }
  
  // 登录
  async login(credentials) {
    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(credentials)
      });
      
      if (!response.ok) {
        throw new Error('登录失败');
      }
      
      const data = await response.json();
      
      // 只存储必要的用户信息,不存储敏感信息
      this.user = {
        id: data.user.id,
        username: data.user.username,
        email: data.user.email,
        role: data.user.role
      };
      
      // Token 存储在 httpOnly cookie 中,前端不直接访问
      this.token = data.token;
      
      // 存储到 localStorage(可选)
      localStorage.setItem('user', JSON.stringify(this.user));
      
      return this.user;
    } catch (error) {
      console.error('登录失败:', error);
      throw error;
    }
  },
  
  // 登出
  async logout() {
    try {
      // 调用登出 API
      await fetch('/api/logout', {
        method: 'POST',
        credentials: 'same-origin'
      });
    } catch (error) {
      console.error('登出失败:', error);
    } finally {
      // 清理本地数据
      this.user = null;
      this.token = null;
      localStorage.removeItem('user');
      
      // 跳转到登录页
      router.push('/login');
    }
  },
  
  // 检查登录状态
  async checkAuth() {
    try {
      const response = await fetch('/api/auth/check', {
        credentials: 'same-origin'
      });
      
      if (!response.ok) {
        throw new Error('未登录');
      }
      
      const data = await response.json();
      this.user = data.user;
      return true;
    } catch (error) {
      this.user = null;
      return false;
    }
  }
}

// 使用示例
const userManager = new UserManager();

// 路由守卫
router.beforeEach(async (to, from, next) => {
  if (to.meta.requiresAuth) {
    const isAuthenticated = await userManager.checkAuth();
    if (!isAuthenticated) {
      next('/login');
    } else {
      next();
    }
  } else {
    next();
  }
});

// 反例
// 不安全的信息存储
class UserManager {
  constructor() {
    this.user = null;
    this.password = null; // 存储密码,不安全!
  }
  
  login(credentials) {
    this.user = credentials;
    this.password = credentials.password; // 明文存储密码
    localStorage.setItem('password', credentials.password); // 存储到 localStorage
  }
}

总结

遵循以上规范可以提高代码质量,增强代码可读性和可维护性,同时确保应用的安全性和性能。规范需要根据项目实际情况进行调整和完善。记住:安全无小事,性能无止境,代码质量是团队协作的基础。

9. 测试规范

单元测试

  • 使用 Jest 或 Mocha
  • 测试覆盖率不低于 80%
  • 测试用例要完整

集成测试

  • 测试组件间交互
  • 测试路由跳转
  • 测试 API 调用

10. 部署规范

构建优化

  • 使用 Tree Shaking
  • 代码分割
  • 压缩混淆

环境配置

  • 区分开发、测试、生产环境
  • 使用环境变量
  • 配置错误监控
贡献者: Yibz