img

📖 引言

在经历了第89篇的多环境调试磨炼后,团队深刻认识到:很多Bug不是在测试阶段发现的,而是在代码审查阶段就应该被拦截的。回顾"奇妙科学乐园"的整个开发历程,复盘文档(docs/本次迭代复盘.md)中记录的16个问题中,至少有8个可以通过严格的代码审查提前发现并避免。

本文将从"奇妙科学乐园"项目的真实代码出发,整理一份系统化的代码审查检查表,涵盖命名规范、注释规范、排版格式、函数设计、安全编码红线五大维度。这份检查表不仅适用于ArkTS/HarmonyOS项目,其核心原则也可推广到任何软件项目的代码审查流程中。每一条规则都配有项目中真实的正确/错误代码对比,确保审查者能够快速定位问题。


🎯 学习目标

完成本文后,你将能够:

  • ✅ 掌握ArkTS代码审查的完整检查清单(5大维度、40+检查项)
  • ✅ 理解安全编码红线的边界和违规后果
  • ✅ 学会建立团队级的代码审查流程与角色分工
  • ✅ 能够对照检查表进行系统性代码审查
  • ✅ 理解code-linter.json5配置与安全规则的对应关系

💡 需求分析

代码审查分级体系

审查级别 适用场景 审查范围 审查时间
L1 快速扫描 日常小改动(Bug修复、文案修改) 命名、注释、基本格式 5-10分钟
L2 标准审查 功能开发、组件新增 全部5大维度 30-60分钟
L3 深度审查 架构变更、核心模块重构 全维度 + 安全红线 + 性能 2-4小时
L4 发布审查 版本发布前的最终检查 全量回归 + 安全红线 半天

五大审查维度总览

代码审查五大维度
  │
  ├─ 一、命名规范(10项)
  │    ├─ 变量/常量//函数/文件命名规则
  │    └─ 布尔值/枚举/接口命名约定
  │
  ├─ 二、注释规范(6项)
  │    ├─ 文件头部注释模板
  │    ├─ 函数JSDoc注释
  │    └─ 行内注释与废弃注释规范
  │
  ├─ 三、排版格式(8项)
  │    ├─ 缩进/空行/行宽
  │    ├─ 运算符/括号空格
  │    └─ 尾随逗号/大括号风格
  │
  ├─ 四、函数设计(8项)
  │    ├─ 单一职责/参数数量/函数长度
  │    ├─ 返回值类型/副作用控制
  │    └─ 默认参数/回调层级
  │
  └─ 五、安全编码红线(12项)
       ├─ 资源管理(context/rawfile/Preferences)
       ├─ 异常处理(try-catch/日志输出)
       ├─ 类型安全(any禁用/类型标注)
       └─ 数据安全(敏感信息/输入校验)

功能模块设计

模块 功能描述 技术要点
命名检查 代码命名规范性审查 camelCase/PascalCase/UPPER_SNAKE_CASE
注释检查 文档完整性审查 JSDoc格式、文件头部模板、中文注释
格式检查 代码排版一致性审查 4空格缩进、尾随逗号、120字符行宽
设计检查 函数设计合理性审查 单一职责、参数数量、函数长度
安全检查 安全红线违规审查 any禁用、异常处理、资源释放

🛠️ 核心实现

步骤1: 命名规范检查清单

功能说明

命名是代码审查中最容易发现的问题,也是影响代码可读性的首要因素。"奇妙科学乐园"项目中,我们制定了严格的命名规范,所有变量、常量、类、函数、文件都必须遵循统一的命名约定。

完整代码

// ===== 命名规范检查清单 =====

//1. 变量命名: 小驼峰(camelCase),语义化,禁止拼音和中英混合
const userName = '小科学家';           // ✅ 语义化小驼峰
const categoryList = [];              // ✅ 语义化小驼峰
const currentQuestion = null;         // ✅ 语义化小驼峰

// ❌ const yonghuming = '小科学家';  // 拼音命名
// ❌ const data1 = [];               // 无意义命名
// ❌ const user_xinxi = {};          // 中英混合+下划线

//2. 常量命名: 全大写下划线(UPPER_SNAKE_CASE),按模块加前缀
const MAX_HISTORY_SIZE = 100;          // ✅ 全大写下划线
const DEFAULT_QUIZ_COUNT = 10;         // ✅ 全大写下划线+语义化
const KEY_FAVORITES = 'favorite_topics'; // ✅ 模块前缀KEY_

// ❌ const maxSize = 100;            // 未全大写
// ❌ const STATUS = 'normal';        // 缺少模块前缀

// 项目中的AppConstants展示了规范示例:
export const AppConstants: AppConstantsType = {
    MAX_HISTORY_SIZE: 100,
    MAX_QUIZ_SCORES: 50,
    DEFAULT_QUIZ_COUNT: 10,
    DEFAULT_USER_NAME: '小科学家',
    PREFS_NAME: 'science_app_prefs',
    KEY_FAVORITES: 'favorite_topics',
    KEY_READ_HISTORY: 'read_history',
    // ... 全部遵循 UPPER_SNAKE_CASE
};

//3. 类/组件命名: 大驼峰(PascalCase)
class UserPreferences {}              // ✅ 大驼峰
struct QuizPage {}                     // ✅ 大驼峰
@Component
export struct Card {}                  // ✅ 组件大驼峰

// ❌ class userPreferences {}         // 小驼峰(类名错误)
// ❌ struct quiz_page {}              // 蛇形命名(组件名错误)

//4. 布尔值命名: is/has/can/should/need前缀
@State isVisible: boolean = true;      // ✅ is前缀
@State hasPermission: boolean = false; // ✅ has前缀
const canEdit: boolean = true;         // ✅ can前缀
const needRefresh: boolean = false;    // ✅ need前缀

// ❌ const visible = true;            // 缺少前缀
// ❌ const show = true;               // 语义不明确

// 项目中的真实示例:
@State isLoading: boolean = true;       // ✅ is前缀
@State hasError: boolean = false;       // ✅ has前缀
@State showFeedback: boolean = false;    // ✅ show前缀(UI状态可接受)
@State isCorrect: boolean = false;      // ✅ is前缀

//5. 函数命名: 动词开头
function getUserInfo() {}              // ✅ get开头
function handleSubmit() {}             // ✅ handle开头
function formatDate() {}               // ✅ format开头
function fetchUserList() {}            // ✅ fetch开头

//function userInfo() {}           // 缺少动词
//function data() {}               // 无意义

// 项目中的真实示例:
function loadJsonData<T>() {}          // ✅ load开头
function resolveResource() {}          // ✅ resolve开头
function getRecommendedTopics() {}     // ✅ get开头
function shuffleArray<T>() {}          // ✅ shuffle开头

//6. 文件命名规范
// 组件文件: 大驼峰 → TopicCard.ets, BannerCarousel.ets, QuizOptionItem.ets
// 工具文件: 小驼峰 → Logger.ets, RawFileUtil.ets, RouterUtil.ets, DateUtil.ets
// 模型文件: 大驼峰 → Category.ets, Topic.ets, Experiment.ets, Achievement.ets
// 常量文件: 大驼峰 → AppConstants.ets, RouteUrls.ets
// 页面文件: 大驼峰 → Index.ets, MainTabs.ets, Quiz.ets, Profile.ets

//7. 文件夹命名: 全小写,多单词短横线分隔
// components/base/     → ✅ 小写
// components/common/  → ✅ 小写
// components/home/    → ✅ 小写
// components/quiz/    → ✅ 小写
// components/topic/   → ✅ 小写
// viewmodel/          → ✅ 小写

代码解析

1. 命名审查检查表

序号 检查项 正确示例 错误示例 严重程度
N1 变量使用小驼峰 userName user_name 轻微
N2 常量使用全大写下划线 MAX_HISTORY_SIZE maxHistorySize 中等
N3 常量按模块加前缀 KEY_FAVORITES favorites 轻微
N4 类/组件使用大驼峰 UserPreferences userPreferences 中等
N5 布尔值使用is/has/can前缀 isLoading loading 轻微
N6 函数使用动词开头 getUserName userName 轻微
N7 禁止拼音和中英混合 categoryList fenleiList 中等
N8 禁止无意义命名 recommendedTopics data1 中等
N9 文件名遵循约定 TopicCard.ets topic-card.ets 轻微
N10 文件夹全小写短横线 components/base/ Components/Base/ 轻微

步骤2: 注释规范检查清单

功能说明

注释是代码的"说明书",好的注释能让新成员快速理解代码意图。"奇妙科学乐园"项目要求所有新建文件必须包含文件头部注释,所有公开方法必须包含JSDoc注释。注释统一使用简体中文。

完整代码

// ===== 注释规范检查清单 =====

/*
 * 文件用途:xxx功能模块 - 具体功能描述
 * 创建时间:YYYY-MM-DD
 * 兼容环境:HarmonyOS
 * 版本:v1.0
 * 风险提示:已知风险或注意事项
 */

//1. 文件头部注释(所有新建文件必写)
// 项目中每个文件都遵循此模板:
// - Logger.ets: "日志工具 - 统一日志输出封装"
// - RawFileUtil.ets: "rawfile JSON文件读取工具类,提供统一的数据加载、解析和缓存能力"
// - ScienceData.ets: "科学数据服务 - 从rawfile加载全量数据,提供统一查询接口"
// - QuizEngine.ets: "答题引擎 - 趣味问答核心逻辑"

//2. 公开方法必须包含JSDoc注释
/**
 * 从rawfile目录读取JSON文件内容并解析为指定类型
 * @param context - 应用或UI上下文,用于获取resourceManager
 * @param fileName - rawfile目录下的JSON文件名(含后缀),如 'categories.json'
 * @returns 解析后的JSON对象数组
 * @throws 当文件读取失败或JSON解析失败时抛出异常
 */
export function loadJsonData<T>(context: common.UIAbilityContext | common.Context, fileName: string): T {
    // ...
}

/**
 * 初始化数据服务,从rawfile加载全部数据
 * 加载失败时回退到Mock数据,确保Previewer环境也能展示UI
 * @param context - 应用上下文
 */
init(context: common.UIAbilityContext | common.Context): void {
    // ...
}

/**
 * 根据用户ID查询用户详细信息
 * @param userId - 用户ID
 * @param options - 查询选项
 * @param options.includeRole - 是否包含角色信息
 * @returns 用户详细信息对象
 * @throws {NotFoundError} 用户不存在时抛出
 */
async function getUserDetail(userId: string, options?: { includeRole?: boolean }): Promise<UserDetail> {
    // ...
}

//3. 关键业务行单行中文注释
const RESOURCE_PREFIX = 'app.media.';  // rawfile中Resource引用的字符串标识
const fileCache: Map<string, string> = new Map();  // 内存缓存映射表,避免重复读取同一文件

//4. 复杂逻辑使用代码块注释分隔
// ========== 分类相关方法 ==========

getAllCategories(): Category[] {
    return this.categories;
}

// ========== 文章相关方法 ==========

getAllTopics(): Topic[] {
    return this.topics;
}

// ========== 实验相关方法 ==========

getAllExperiments(): Experiment[] {
    return this.experiments;
}

//5. 禁止英文注释
//// Load categories from rawfile
//// 从rawfile加载分类数据

//6. 禁止无用注释(注释显而易见的代码)
// ❌ let i = 0; // 初始化i为0
// ❌ return result; // 返回结果
// ✅ (不写注释,代码本身足够清晰)

代码解析

1. 注释审查检查表

序号 检查项 审查要点 严重程度
C1 文件头部注释 是否包含用途、时间、环境、版本、风险 中等
C2 公开方法JSDoc 是否包含@param、@returns、@throws 中等
C3 注释语言统一 是否全部为中文,禁止英文注释 轻微
C4 业务行注释 关键逻辑是否有中文单行注释 轻微
C5 废弃代码处理 废弃代码是否注释标注原因而非直接删除 轻微
C6 无用注释检查 是否存在注释显而易见代码的冗余注释 轻微

步骤3: 排版格式检查清单

功能说明

统一的代码排版是团队协作的基础。"奇妙科学乐园"项目严格遵循4空格缩进、120字符行宽、尾随逗号、同一行大括号等格式规范。这些规范与项目的code-linter.json5配置保持一致。

完整代码

// ===== 排版格式检查清单 =====

// ✅ 1. 缩进统一4个空格,禁用Tab
@Component
export struct Card {
    borderRadius: number = 12;           // ✅ 4空格缩进
    backgroundColor: string = '#ffffff';
    borderWidth: number = 1;
    // ...
    build() {
        Column() {                       // ✅ 嵌套层也使用4空格
            this.content();
        }
    }
}

// ❌ 使用Tab缩进(文件中混用Tab和空格)
// ❌ 使用2空格缩进(与项目规范不一致)

// ✅ 2. 不同逻辑块之间空一行
const DOMAIN = 0x0000;
const LOG_TAG = 'ScienceApp';

export class Logger {
    static debug(tag: string, message: string): void {
        // ...
    }

    static info(tag: string, message: string): void {
        // ...
    }
}

// ✅ 3. import分组之间空一行
import { hilog } from '@kit.PerformanceAnalysisKit';

import { resourceManager } from '@kit.LocalizationKit';
import { common } from '@kit.AbilityKit';
import { util } from '@kit.ArkTS';

import { Logger } from './Logger';

// ✅ 4. 对象/数组最后一个元素尾随逗号
const tabItems: TabItem[] = [
    {
        index: 0,
        icon: $r('app.media.icon_home'),
        activeIcon: $r('app.media.tab_home_active'),
        label: '首页',                    // ✅ 最后一个元素也有逗号
    },
];

export const AppConstants: AppConstantsType = {
    MAX_HISTORY_SIZE: 100,
    MAX_QUIZ_SCORES: 50,
    DEFAULT_USER_NAME: '小科学家',       // ✅ 尾随逗号
};

// ✅ 5. 大括号同一行风格
if (this.isLoading) {
    this.SkeletonContent();
} else if (this.hasError) {
    this.ErrorContent();
} else {
    this.NormalContent();
}

// ❌ 大括号换行风格(与项目不一致)
// if (condition)
// {
//     // ...
// }

// ✅ 6. 运算符/括号两侧单个空格
const result = a + b;                    // ✅ 空格
if (condition) {}                        // ✅ 空格
function foo(arg1: string, arg2: number) {} // ✅ 逗号后空格

// ❌ const result=a+b;                  // 缺少空格
// ❌ if(condition){}                     // 缺少空格

// ✅ 7. 单行代码不超过120字符
const result: QuizSession = {
    questions: selectedQuestions,
    currentIndex: 0,
    correctCount: 0,
    isFinished: false,
    startTime: Date.now(),
};

// 超过120字符时合理换行:
// ❌ 单行: const result = someVeryLongFunctionName(parameter1, parameter2, parameter3, parameter4, parameter5);
// ✅ 换行:
const result = someVeryLongFunctionName(
    parameter1,
    parameter2,
    parameter3,
    parameter4,
    parameter5
);

// ✅ 8. 文件末尾保留一个空行
// (本文最后一个字符后面有一个换行符)

代码解析

1. 排版审查检查表

序号 检查项 正确做法 错误做法 严重程度
F1 缩进4空格 使用4个空格 使用Tab或2空格 轻微
F2 逻辑块空行 不同逻辑块间空一行 连续多行无空行分隔 轻微
F3 import分组 三方库/内部模块/样式分组 无序排列 轻微
F4 尾随逗号 最后一个元素加逗号 最后一个元素无逗号 轻微
F5 大括号风格 同一行风格 换行风格 轻微
F6 运算符空格 a + b a+b 轻微
F7 行宽限制 <=120字符 超过120不换行 轻微
F8 文件末尾 保留一个空行 无空行或多个空行 轻微

步骤4: 函数设计检查清单

功能说明

函数设计直接影响代码的可维护性和可测试性。"奇妙科学乐园"项目要求函数遵循单一职责、参数不超过3个、长度不超过50行等设计原则。本节以项目中的真实函数为案例,展示正确和错误的设计对比。

完整代码

// ===== 函数设计检查清单 =====

// ✅ 1. 单一职责: 一个函数只做一件事
// 项目示例: loadJsonData只负责"读取+解析",不负责业务处理
export function loadJsonData<T>(context: common.UIAbilityContext | common.Context, fileName: string): T {
    const cached = fileCache.get(fileName);
    let rawContent: string;
    if (cached !== undefined) {
        rawContent = cached;
    } else {
        // 读取文件
        const resMgr: resourceManager.ResourceManager = context.resourceManager;
        const uint8Array: Uint8Array = resMgr.getRawFileContentSync(fileName);
        rawContent = new util.TextDecoder('utf-8').decodeToString(uint8Array);
        fileCache.set(fileName, rawContent);
    }
    // 解析JSON
    return JSON.parse(rawContent) as T;
}

// ❌ 违反单一职责: 一个函数又读取又解析又缓存又初始化业务对象
// function loadAndProcessCategories(ctx, fileName) {
//     const data = readRawFile(ctx, fileName);
//     const parsed = JSON.parse(data);
//     const resolved = parsed.map(item => resolveResource(item));
//     this.categories = resolved;
//     return resolved;
// }

// ✅ 2. 参数不超过3个,超过使用对象参数
// 项目示例: QuizEngine.startQuiz只接收2个参数
startQuiz(categoryId: string = 'all', questionCount: number = AppConstants.DEFAULT_QUIZ_COUNT): QuizSession {
    // ...
}

// ❌ 参数过多:
// function createUser(name: string, age: number, email: string, phone: string, avatar: string) {}

// ✅ 正确: 使用接口封装多参数
interface CreateUserOptions {
    name: string;
    age: number;
    email: string;
    phone?: string;
    avatar?: string;
}
function createUser(options: CreateUserOptions): void {}

// ✅ 3. 函数有明确的返回值类型标注
startQuiz(categoryId: string = 'all', questionCount: number = 10): QuizSession {
    // ...
}

submitAnswer(optionIndex: number): SubmitResult | null {
    // ...
}

incrementProgress(achievementId: string, amount: number = 1): Achievement | null {
    // ...
}

// ❌ 缺少返回值类型标注(依赖推断):
// function startQuiz(categoryId, questionCount) { ... }

// ✅ 4. 异常必须捕获,不能裸try-catch
// 项目示例: RawFileUtil.ets中的分类异常处理
try {
    const resMgr: resourceManager.ResourceManager = context.resourceManager;
    const uint8Array: Uint8Array = resMgr.getRawFileContentSync(fileName);
    rawContent = new util.TextDecoder('utf-8').decodeToString(uint8Array);
    fileCache.set(fileName, rawContent);
} catch (error) {
    const errMsg = `读取rawfile文件失败: ${fileName}`;
    console.error(errMsg, error);
    throw new Error(errMsg);
}

// ❌ 裸try-catch: 捕获所有异常但什么都不做
// try { loadData(); } catch (e) { }

// ✅ 5. 异步逻辑统一使用async/await
// 项目示例: UserPreferences中的异步方法
async getFavorites(): Promise<number[]> {
    return this.getFavoritesSync();
}

async addFavorite(topicId: number): Promise<boolean> {
    if (this.cache.favorites.includes(topicId)) {
        return false;
    }
    this.cache.favorites.push(topicId);
    this.scheduleSave();
    Logger.debug(TAG, `添加收藏: ${topicId}`);
    return true;
}

// ❌ 回调地狱:
// loadAllData((data) => {
//     processData(data, (result) => {
//         saveResult(result, (success) => {
//             updateUI(success);
//         });
//     });
// });

// ✅ 6. 资源必须在aboutToDisappear中释放
// 项目示例: Index.ets中清理轮询定时器
aboutToDisappear() {
    if (this.loadTimer >= 0) {
        clearInterval(this.loadTimer);  // 清理定时器
    }
}

// ❌ 忘记清理资源:
// aboutToDisappear() {
//     // 没有清理loadTimer,导致内存泄漏
// }

代码解析

1. 函数设计审查检查表

序号 检查项 审查要点 严重程度
D1 单一职责 函数是否只做一件事?函数名能否准确描述功能? 中等
D2 参数数量 参数是否超过3个?超过是否使用对象参数? 轻微
D3 函数长度 函数是否超过50行?超过是否需要拆分? 轻微
D4 返回值类型 是否明确标注返回值类型?禁止依赖推断 中等
D5 异常处理 IO/网络/文件操作是否捕获异常?是否分类处理? 严重
D6 异步风格 是否统一使用async/await?是否禁止回调地狱? 中等
D7 资源释放 定时器/事件监听是否在aboutToDisappear中清理? 严重
D8 默认参数 是否使用默认参数代替函数内判断赋值? 轻微

步骤5: 安全编码红线——不可触碰的禁区

功能说明

安全编码红线是代码审查中最高优先级的检查项。任何违反红线的代码都必须立即修复,不得合入主分支。"奇妙科学乐园"项目的code-linter.json5中已经配置了加密算法安全规则,但安全红线的范围远不止加密算法。本节梳理完整的安全红线清单,每一条都配有项目中的真实案例。

完整代码

// ===== 安全编码红线清单 =====
// 以下规则为全局强制禁止项,违反任何一条都不允许合入代码

// ===== 红线1: 禁止硬编码敏感信息 =====

// ❌ 严重违规: 密码/密钥/Token硬编码
// const API_KEY = 'sk-xxxx1234567890';
// const DB_PASSWORD = 'root123456';
// const JWT_SECRET = 'my-secret-key';

// ✅ 正确: 使用环境变量或配置文件
// 环境变量: 通过 @ohos.app.ability.default.json 或 .env 读取
// 配置文件: 通过 AppConstants 集中管理非敏感配置

// 项目中AppConstants的密码管理:
export const AppConstants = {
    // ⚠️ 注意: PARENT_CONTROL_DEFAULT_PWD是家长控制默认密码
    // 这是一个"已知的默认值",用户可以在设置中修改
    // 严格意义上应该要求用户首次使用时强制修改
    PARENT_CONTROL_DEFAULT_PWD: '1234',
    PREFS_NAME: 'science_app_prefs',
};

// ===== 红线2: 禁止使用eval()等动态执行 =====

// ❌ 严重违规: 动态执行代码
// eval(userInput);
// new Function(userInput);

// ✅ 正确: 使用安全的替代方案
// 数据解析: JSON.parse()
// 条件判断: switch-case / if-else

// ===== 红线3: 禁止使用any类型 =====

// ❌ 严重违规: 使用any绕过类型检查
// function processData(data: any) { ... }
// const result: any = JSON.parse(str);

// ✅ 正确: 使用具体类型或unknown + 类型守卫
function processData(data: unknown): void {
    if (typeof data === 'string') {
        // 类型守卫后安全使用
    }
}

// 项目中的类型安全示例:
function loadJsonData<T>(context: common.UIAbilityContext | common.Context, fileName: string): T {
    // 泛型T提供类型约束
    const parsedData = JSON.parse(rawContent) as T;  // 使用as而非any
    return parsedData;
}

// ===== 红线4: 禁止裸try-catch =====

// ❌ 严重违规: 捕获所有异常但不处理
// try {
//     scienceData.init(ctx);
// } catch (e) {
//     // 静默吞掉异常,问题永远无法被发现
// }

// ✅ 正确: 分类捕获 + 日志输出 + 合理处理
try {
    this.questions = loadJsonData<QuizQuestion[]>(context, 'quizzes.json');
    this.isInitialized = true;
} catch (error) {
    console.error('QuizEngine初始化失败', error);
    throw new Error('问答数据加载失败');
}

// ===== 红线5: 禁止SQL字符串拼接 =====

// ❌ 严重违规: SQL注入风险
// const query = `SELECT * FROM users WHERE name = '${userName}'`;

// ✅ 正确: 使用参数化查询
// const query = 'SELECT * FROM users WHERE name = ?';
// db.query(query, [userName]);

// ===== 红线6: 禁止console.log输出到生产环境 =====

// ❌ 生产环境禁止console.log
// console.log('调试信息');

// ✅ 使用Logger工具类(生产环境可按级别过滤)
Logger.info(TAG, '数据加载完成');
Logger.debug(TAG, `详细调试信息: ${JSON.stringify(data)}`);
Logger.error(TAG, '数据加载失败', error);

// ===== 红线7: 禁止修改props =====

// ❌ 严重违规: 直接修改父组件传入的props
// this.topic.title = '新标题';

// ✅ 正确: 通过emit通知父组件修改
// this.emitChange.emit({ title: '新标题' });

// 项目中的正确做法: Index页面通过AppStorage通知MainTabs切换Tab
goToCategory(category: Category): void {
    AppStorage.setOrCreate<string>('switchToTab', 'topics');  // 通知而非直接修改
    AppStorage.setOrCreate<string>('topicsCategory', category.id);
}

// ===== 红线8: ForEach必须使用唯一key =====

// ❌ 严重违规: 使用index作为key
ForEach(this.items, (item: Item, index: number) => {
    // ...
}, (_: Item, index: number) => index.toString());  // ❌ 使用index

// ✅ 正确: 使用唯一标识作为key
ForEach(this.categories, (category: Category) => {
    GridItem() { /* ... */ }
}, (category: Category) => category.id);  // ✅ 使用category.id

// 项目中的真实示例:
ForEach(this.categories, (category: Category) => {
    // ...
}, (category: Category) => category.id);  // ✅

ForEach(this.recommendedTopics, (topic: Topic) => {
    TopicCard({ topic: topic, onItemClick: (t: Topic) => this.goToTopicDetail(t) });
}, (topic: Topic) => topic.id.toString());  // ✅

// ===== 红线9: 禁止未校验的外部输入 =====

// ❌ 严重违规: 直接使用路由参数不做校验
// const topicId = router.getParams().topicId;  // 可能是undefined
// const detail = scienceData.getTopicById(topicId);  // undefined可能导致异常

// ✅ 正确: 校验外部输入
const params = RouterUtil.getParams();
if (params && params.topicId !== undefined) {
    const topicId = params.topicId as number;
    const topic = scienceData.getTopicById(topicId);
    if (topic) {
        // 安全使用
    }
}

// 项目中的真实示例:
aboutToAppear() {
    const params = RouterUtil.getParams() as Record<string, Object>;
    if (params && params.categoryId) {
        this.currentCategory = params.categoryId as string;  // ✅ 校验后使用
    }
}

// ===== 红线10: 禁止v-html渲染用户输入(XSS防护) =====

// ❌ 严重违规: 直接渲染用户输入的HTML
// Text(`<span>${userInput}</span>`)  // 如果是Web组件

// ✅ 正确: 对用户输入进行转义或使用纯文本
// ArkTS中使用Text组件天然安全(不支持innerHTML)
// 但在Web组件中需注意

// ===== 红线11: code-linter.json5安全规则 =====

// 项目中code-linter.json5已配置的加密算法安全规则:
{
    "rules": {
        "@security/no-unsafe-aes": "error",      // 禁止不安全的AES配置
        "@security/no-unsafe-hash": "error",      // 禁止不安全的哈希算法(MD5/SHA1)
        "@security/no-unsafe-mac": "warn",        // 警告不安全的MAC算法
        "@security/no-unsafe-dh": "error",        // 禁止不安全的DH密钥交换
        "@security/no-unsafe-dsa": "error",       // 禁止不安全的DSA签名
        "@security/no-unsafe-ecdsa": "error",     // 禁止不安全的ECDSA配置
        "@security/no-unsafe-rsa-encrypt": "error", // 禁止不安全的RSA加密
        "@security/no-unsafe-rsa-sign": "error",   // 禁止不安全的RSA签名
        "@security/no-unsafe-rsa-key": "error",     // 禁止不安全的RSA密钥
        "@security/no-unsafe-dsa-key": "error",     // 禁止不安全的DSA密钥
        "@security/no-unsafe-dh-key": "error",      // 禁止不安全的DH密钥
        "@security/no-unsafe-3des": "error"         // 禁止3DES(已不安全)
    }
}

代码解析

1. 安全红线审查检查表

序号 红线项 违规后果 审查方法
S1 禁止硬编码敏感信息 密钥泄露,数据被窃取 全文搜索密码/密钥/Token关键字
S2 禁止eval/动态执行 代码注入攻击 搜索eval、new Function
S3 禁止any类型 运行时类型错误 Lint检查+代码审查
S4 禁止裸try-catch 异常被吞,问题无法定位 检查所有catch块
S5 禁止SQL字符串拼接 SQL注入攻击 搜索模板字符串拼接SQL
S6 禁止console.log 生产环境信息泄露 Lint规则+代码审查
S7 禁止修改props 状态管理混乱,数据不一致 检查@Prop/@Link使用
S8 ForEach唯一key 列表渲染异常,性能问题 检查所有ForEach的key函数
S9 禁止未校验外部输入 崩溃或异常行为 检查路由参数、用户输入
S10 禁止v-html用户输入 XSS跨站脚本攻击 检查Web组件内容渲染
S11 加密算法安全 数据加密形同虚设 code-linter自动检查
S12 禁止过时加密算法 MD5/SHA1/DES已被破解 code-linter自动检查

步骤6: 代码审查流程与角色分工

功能说明

检查表制定之后,需要配套的审查流程才能落地执行。"奇妙科学乐园"项目建立了"提交者自审 + 指定审查者复审"的双人审查机制,确保每行代码至少经过两双眼睛的检查。

完整代码

// ===== 代码审查流程 =====

/**
 * 审查流程(3个阶段)
 *
 * 阶段1: 提交者自审(必做)
 *   - 提交PR前,提交者对照检查表逐项自审
 *   - 确保编译零错误、Lint零警告
 *   - 在PR描述中填写"自审检查清单"章节
 *
 * 阶段2: 指定审查者审查(必做)
 *   - 每个PR必须指定至少1名审查者
 *   - 审查者对照检查表进行系统性审查
 *   - 安全红线项为阻塞项(不通过则不允许合入)
 *
 * 阶段3: 交叉验证(Bug修复时必做)
 *   - Bug修复PR必须包含回归测试结果
 *   - 审查者验证修复点 + 关联模块是否受影响
 *   - 确认修复没有引入新问题
 */

// ===== PR描述模板 =====
/**
 * ## 变更说明
 * [描述本次变更的功能/修复/优化]
 *
 * ## 自审检查清单
 * - [ ] 命名规范: 所有新增变量/函数/文件名符合命名规范
 * - [ ] 注释规范: 新增文件有头部注释,公开方法有JSDoc
 * - [ ] 排版格式: 4空格缩进,尾随逗号,120字符行宽
 * - [ ] 函数设计: 单一职责,参数<=3,有返回值类型标注
 * - [ ] 安全红线: 无any类型,无裸try-catch,ForEach有唯一key
 * - [ ] 编译检查: 编译零错误
 * - [ ] Lint检查: code-linter零warning
 * - [ ] 异常处理: IO操作有try-catch,日志使用Logger
 * - [ ] 资源释放: 定时器/监听器在aboutToDisappear中清理
 *
 * ## 测试环境
 * - [ ] Previewer
 * - [ ] 模拟器/真机
 *
 * ## 关联问题
 * [关联的Issue或复盘问题编号]
 */

代码解析

1. 审查流程对比

环节 ❌ 错误做法 ✅ 正确做法
提交前 直接提交代码,不自查 对照检查表逐项自审
审查 走马观花看一眼 逐条检查表系统性审查
反馈 "LGTM"或无意义评论 指出具体问题,给出修改建议
安全红线 忽略安全规则 安全红线为阻塞项,必须修复
Bug修复 只验证修复点 回归测试关联模块

2. 角色分工

角色 职责 审查重点
提交者 代码自审 + PR描述 编译/Lint/基本规范
审查者 系统性代码审查 全维度检查 + 安全红线
架构师 架构级审查 设计合理性/性能/安全
测试工程师 功能验证 回归测试/边界场景

⚠️ 常见问题与解决方案

问题1: 审查效率太低,每次PR审查耗时过长

现象:
审查者反馈每次代码审查需要1-2小时,严重影响开发节奏。

原因:
审查者没有使用检查表,每次都从头逐行阅读代码,没有重点。

解决方案:

1. 使用本文提供的检查表,按维度逐项检查
2. 优先检查安全红线(阻塞项),再检查其他规范
3. 利用DevEco Studio的code-linter自动检查格式和部分安全规则
4. 小改动(<50行)使用L1快速扫描,5-10分钟完成
5. 大改动分批提交PR,每次PR控制在合理范围内

问题2: 审查意见反复修改仍不通过

现象:
提交者修改后重新提交,审查者又发现新的问题,反复3-4次仍不通过。

原因:
审查者没有一次性给出完整的审查意见,或者提交者修改时引入了新问题。

解决方案:

// 审查者应该:
// 1. 一次性给出所有问题的完整列表
// 2. 按严重程度排序(安全红线 > 编译错误 > 格式问题)
// 3. 每条问题给出具体的修改建议(而非只说"这里有问题"// 提交者应该:
// 1. 仔细阅读每条审查意见
// 2. 修改时不要引入新的格式或规范问题
// 3. 修改完成后重新对照检查表自审

📝 本章小结

核心知识点

本文整理了"奇妙科学乐园"项目的完整代码审查检查表,主要包括:

1. 命名规范(10项)

  • 变量小驼峰、常量全大写下划线、类大驼峰
  • 布尔值is/has/can前缀、函数动词开头
  • 禁止拼音、中英混合、无意义命名

2. 注释规范(6项)

  • 文件头部注释模板(用途/时间/环境/版本/风险)
  • 公开方法JSDoc(@param/@returns/@throws)
  • 统一中文注释,禁止英文和无用注释

3. 排版格式(8项)

  • 4空格缩进、120字符行宽、尾随逗号
  • import分组、大括号同一行、运算符空格

4. 函数设计(8项)

  • 单一职责、参数<=3、长度<=50行
  • 返回值类型标注、分类异常处理、async/await
  • 资源在aboutToDisappear中释放

5. 安全编码红线(12项)

  • 禁止硬编码敏感信息、eval、any、裸try-catch
  • 禁止SQL拼接、console.log、修改props
  • ForEach唯一key、外部输入校验、XSS防护
  • code-linter.json5加密算法安全规则

最佳实践总结

审查三步走

1. 提交者自审 → 对照检查表逐项确认
2. 指定审查者复审 → 安全红线优先,全维度覆盖
3. Bug修复交叉验证 → 修复点 + 关联模块

安全红线零容忍

// 任何违反安全红线的代码都不允许合入
// 即使功能正确,安全风险也是不可接受的
// "先合入后续修复"是不可接受的理由

下一步预告

在下一篇文章中,我们将:

  • 📊 深入分析"奇妙科学乐园"项目全员复盘会的16个问题
  • 🔍 追溯每个问题的根本原因,找到系统性改进方案
  • 📋 建立强制性的开发流程和角色规则

🔗 相关链接


💡 提示: 建议结合项目源码中的 code-linter.json5docs/本次迭代复盘.md 对照阅读,理解安全红线配置与实际问题的对应关系。将本文的检查表打印出来放在工位旁,每次代码审查时对照使用。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐