HarmonyOS应用<奇妙科学乐园>开发第90篇:代码审查检查表——编码规范与安全红线

📖 引言
在经历了第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个问题
- 🔍 追溯每个问题的根本原因,找到系统性改进方案
- 📋 建立强制性的开发流程和角色规则
🔗 相关链接
- 项目源码: Atomgit仓库
- 前置文章: 第89篇: 调试技巧
- 相关文章: 第78篇: 编译错误连锁修复
- 相关文档: 项目复盘报告
- 相关配置: code-linter.json5
💡 提示: 建议结合项目源码中的 code-linter.json5 和 docs/本次迭代复盘.md 对照阅读,理解安全红线配置与实际问题的对应关系。将本文的检查表打印出来放在工位旁,每次代码审查时对照使用。
更多推荐

所有评论(0)