前端开发规范
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 // 不符合PascalCase2. 代码格式
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 组件结构
- 按以下顺序组织组件代码:
- name
- components
- props
- data
- computed
- watch
- 生命周期钩子
- 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
- 代码分割
- 压缩混淆
环境配置
- 区分开发、测试、生产环境
- 使用环境变量
- 配置错误监控
