HarmonyOS开发:Lint规则
HarmonyOS开发:Lint规则
📌 核心要点:Lint是代码规范的执行者——没有Lint的规范只是建议,有了Lint的规范才是规则。
背景与动机
团队定了编码规范,写了个文档发到群里,“大家按这个来”。结果呢?一周后没人看,一个月后没人记得,三个月后代码风格五花八门——有人用分号,有人不用;有人用单引号,有人用双引号;有人函数名驼峰,有人函数名下划线。
为什么编码规范总是执行不下去?因为靠人自觉遵守的规则,等于没有规则。你Code Review的时候指出格式问题,开发者觉得你吹毛求疵;你不指出来,代码风格就越来越乱。
Lint工具解决的就是这个问题——把规范变成机器检查。格式不对?Lint报错。命名不规范?Lint报错。复杂度太高?Lint报错。不用你开口,机器替你执法,谁都没意见。
鸿蒙项目的Lint还有一层特殊意义:ArkTS的语法限制比TypeScript多,很多TypeScript里合法的写法在ArkTS里不行。Lint可以在编码阶段就提醒你,不用等到编译报错才发现。
核心原理
Lint的工作流程
graph TD
A[源代码.ets/.ts] --> B[解析器Parser]
B --> C[AST抽象语法树]
C --> D[规则引擎]
E[规则配置.eslintrc] --> D
F[自定义规则插件] --> D
D --> G[遍历AST节点]
G --> H[规则匹配]
H --> I{匹配到问题?}
I -->|是| J[生成Lint报告]
I -->|否| K[通过检查]
J --> L[输出错误/警告]
classDef sourceStyle fill:#FF6B6B,stroke:#C0392B,color:#fff,font-weight:bold
classDef processStyle fill:#4ECDC4,stroke:#1ABC9C,color:#fff
classDef decisionStyle fill:#6C5CE7,stroke:#8E44AD,color:#fff
classDef resultStyle fill:#FFE66D,stroke:#F39C12,color:#333
class A,E,F sourceStyle
class B,C,D,G,H processStyle
class I decisionStyle
class J,K,L resultStyle
Lint规则的分类体系
graph LR
A[Lint规则体系] --> B[格式规范]
A --> C[代码质量]
A --> D[最佳实践]
A --> E[ArkTS专项]
B --> B1[缩进/空格]
B --> B2[引号风格]
B --> B3[分号使用]
B --> B4[行长度限制]
C --> C1[复杂度控制]
C --> C2[重复代码检测]
C --> C3[未使用变量]
C --> C4[类型安全]
D --> D1[错误处理规范]
D --> D2[异步模式]
D --> D3[命名规范]
D --> D4[组件规范]
E --> E1[装饰器使用]
E --> E2[状态管理规范]
E --> E3[生命周期规范]
E --> E4[并发安全]
classDef mainStyle fill:#FF6B6B,stroke:#C0392B,color:#fff,font-weight:bold
classDef subStyle fill:#4ECDC4,stroke:#1ABC9C,color:#fff
classDef detailStyle fill:#FFE66D,stroke:#F39C12,color:#333
class A mainStyle
class B,C,D,E subStyle
class B1,B2,B3,B4,C1,C2,C3,C4,D1,D2,D3,D4,E1,E2,E3,E4 detailStyle
代码实战
基础用法:ESLint配置鸿蒙项目
鸿蒙项目使用ESLint作为Lint引擎,配合ArkTS专用插件:
// .eslintrc.arkts.js - 鸿蒙项目ESLint配置
module.exports = {
// 根配置,防止向上查找
root: true,
// 指定解析器
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
project: './tsconfig.json',
// ArkTS特有的解析选项
extraFileExtensions: ['.ets'],
},
// 插件
plugins: [
'@typescript-eslint',
// 鸿蒙专用Lint插件(如果有)
// 'arkts-lint',
],
// 继承的规则集
extends: [
'eslint:recommended',
'@typescript-eslint/recommended',
'@typescript-eslint/recommended-requiring-type-checking',
// 鸿蒙推荐规则集
// 'arkts-lint/recommended',
],
// 全局变量
globals: {
// 鸿蒙全局API
hilog: 'readonly',
router: 'readonly',
promptAction: 'readonly',
},
// 规则配置
rules: {
// === 格式规范 ===
'indent': ['error', 2], // 2空格缩进
'quotes': ['error', 'single', { avoidEscape: true }], // 单引号
'semi': ['error', 'always'], // 必须分号
'max-len': ['warn', {
code: 120, // 行宽120
ignoreStrings: true, // 忽略字符串
ignoreTemplateLiterals: true, // 忽略模板字符串
}],
'comma-dangle': ['error', 'always-multiline'], // 多行尾逗号
// === 代码质量 ===
'complexity': ['warn', { max: 15 }], // 圈复杂度上限15
'max-lines-per-function': ['warn', {
max: 80, // 函数不超过80行
skipBlankLines: true,
skipComments: true,
}],
'max-depth': ['warn', { max: 4 }], // 嵌套深度不超过4
'no-duplicate-imports': 'error', // 禁止重复导入
'no-unused-vars': 'off', // 关闭基础规则
'@typescript-eslint/no-unused-vars': ['error', { // 使用TS版本
argsIgnorePattern: '^_', // _开头的参数允许未使用
varsIgnorePattern: '^_',
}],
// === 类型安全 ===
'@typescript-eslint/no-explicit-any': 'error', // 禁止any
'@typescript-eslint/no-non-null-assertion': 'warn', // 警告非空断言
'@typescript-eslint/prefer-nullish-coalescing': 'error', // 使用??代替||
'@typescript-eslint/prefer-optional-chain': 'error', // 使用可选链
'@typescript-eslint/strict-boolean-expressions': ['warn', {
allowString: false, // 禁止字符串隐式转布尔
allowNumber: false, // 禁止数字隐式转布尔
allowNullableObject: true, // 允许对象判空
}],
// === 最佳实践 ===
'no-console': ['warn', { allow: ['warn', 'error'] }], // 禁止console.log
'no-throw-literal': 'error', // throw必须抛出Error对象
'prefer-promise-reject-errors': 'error', // reject必须用Error
'no-return-await': 'error', // 不需要return await
'@typescript-eslint/return-await': ['error', 'in-try-catch'], // try-catch中需要await
// === ArkTS专项 ===
// 以下规则需要arkts-lint插件支持
// 'arkts-lint/no-any': 'error', // 禁止any类型
// 'arkts-lint/no-dynamic-property': 'error', // 禁止动态属性
// 'arkts-lint/state-decorator-usage': 'error', // 状态装饰器规范
// 'arkts-lint/component-lifecycle': 'warn', // 生命周期规范
// 'arkts-lint/no-eval': 'error', // 禁止eval
},
// 覆盖配置:针对不同目录使用不同规则
overrides: [
{
// 测试文件放宽规则
files: ['**/*.test.ets', '**/*.spec.ets', '**/ohosTest/**'],
rules: {
'max-lines-per-function': 'off',
'complexity': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
},
{
// Mock数据文件放宽规则
files: ['**/mock/**', '**/fixtures/**'],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'max-len': 'off',
},
},
],
};
进阶用法:自定义Lint规则
团队有特殊的编码规范?内置规则覆盖不了?写自定义规则:
// custom-rules/no-direct-state-mutation.ts
// 自定义规则:禁止在组件外部直接修改@State属性
import { ESLintUtils } from '@typescript-eslint/utils';
const createRule = ESLintUtils.RuleCreator(
(name) => `https://your-team.github.io/lint-rules/${name}`
);
export const noDirectStateMutation = createRule({
name: 'no-direct-state-mutation',
meta: {
type: 'problem',
docs: {
description: '禁止在组件外部直接修改@State装饰的属性',
recommended: 'error',
},
messages: {
noExternalMutation:
'不允许在组件外部直接修改@State属性 "{{propName}}"。' +
'应通过组件提供的方法或@Link进行修改。',
},
schema: [], // 无额外配置
},
defaultOptions: [],
create(context) {
// 收集当前文件中所有@State属性
const stateProperties = new Map<string, string>(); // 属性名 -> 组件名
return {
// 第一步:收集@State属性
'PropertyDefinition'(node: any) {
const decorators = node.decorators;
if (!decorators) return;
const isState = decorators.some((dec: any) => {
const expr = dec.expression;
return expr.type === 'Identifier' && expr.name === 'State';
});
if (isState && node.key.type === 'Identifier') {
stateProperties.set(node.key.name, '');
}
},
// 第二步:检查赋值操作
'AssignmentExpression'(node: any) {
const left = node.left;
// 检查 obj.prop = value 形式
if (left.type === 'MemberExpression') {
const property = left.property;
if (property.type === 'Identifier' &&
stateProperties.has(property.name)) {
// 检查是否在组件方法内
const scope = context.getScope();
const inComponent = scope.type === 'method' ||
scope.type === 'function';
// 简化判断:如果在函数外赋值@State属性,报错
// 实际实现需要更精确的作用域分析
context.report({
node,
messageId: 'noExternalMutation',
data: {
propName: property.name,
},
});
}
}
},
};
},
});
// custom-rules/prefer-lazy-foreach.ts
// 自定义规则:大数据列表推荐使用LazyForEach
import { ESLintUtils } from '@typescript-eslint/utils';
const createRule = ESLintUtils.RuleCreator(
(name) => `https://your-team.github.io/lint-rules/${name}`
);
export const preferLazyForEach = createRule({
name: 'prefer-lazy-foreach',
meta: {
type: 'suggestion',
docs: {
description: '在List/Grid组件中推荐使用LazyForEach替代ForEach',
recommended: 'warn',
},
messages: {
preferLazyForEach:
'在List/Grid组件中使用ForEach可能导致性能问题。' +
'数据量较大时请使用LazyForEach,支持按需加载。',
},
schema: [{
type: 'object',
properties: {
minItemCount: {
type: 'number',
default: 50, // 默认50条以上建议使用LazyForEach
},
},
additionalProperties: false,
}],
},
defaultOptions: [{ minItemCount: 50 }],
create(context, [options]) {
return {
// 检测ForEach在List/Grid中的使用
'CallExpression'(node: any) {
const callee = node.callee;
// 判断是否是ForEach调用
if (callee.type !== 'Identifier' || callee.name !== 'ForEach') {
return;
}
// 向上查找是否在List或Grid组件内
let parent = node.parent;
let inListComponent = false;
while (parent) {
if (parent.type === 'CallExpression' &&
parent.callee.type === 'Identifier') {
const name = parent.callee.name;
if (name === 'List' || name === 'Grid' ||
name === 'WaterFlow' || name === 'Swiper') {
inListComponent = true;
break;
}
}
parent = parent.parent;
}
if (inListComponent) {
context.report({
node,
messageId: 'preferLazyForEach',
});
}
},
};
},
});
完整示例:团队Lint规范配置包
把Lint配置封装成npm包,团队所有项目统一使用:
// eslint-config-harmony/index.js
// 团队共享的Lint配置包
module.exports = {
// 基础配置(所有项目通用)
base: {
root: true,
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
extraFileExtensions: ['.ets'],
},
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'@typescript-eslint/recommended',
],
rules: {
// 格式规范
'indent': ['error', 2],
'quotes': ['error', 'single'],
'semi': ['error', 'always'],
'max-len': ['warn', { code: 120 }],
'comma-dangle': ['error', 'always-multiline'],
'no-trailing-spaces': 'error',
'eol-last': ['error', 'always'],
// 代码质量
'complexity': ['warn', { max: 15 }],
'max-lines-per-function': ['warn', { max: 80 }],
'max-depth': ['warn', { max: 4 }],
'max-params': ['warn', { max: 5 }],
'no-duplicate-imports': 'error',
// 类型安全
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-non-null-assertion': 'warn',
'@typescript-eslint/prefer-nullish-coalescing': 'error',
'@typescript-eslint/prefer-optional-chain': 'error',
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
}],
},
},
// 严格配置(核心模块使用)
strict: {
extends: ['./index.js'].concat([
'@typescript-eslint/recommended-requiring-type-checking',
]),
rules: {
// 更严格的质量要求
'complexity': ['error', { max: 10 }],
'max-lines-per-function': ['error', { max: 60 }],
'max-depth': ['error', { max: 3 }],
// 更严格的类型检查
'@typescript-eslint/no-unsafe-assignment': 'error',
'@typescript-eslint/no-unsafe-call': 'error',
'@typescript-eslint/no-unsafe-member-access': 'error',
'@typescript-eslint/no-unsafe-return': 'error',
},
},
// 宽松配置(测试代码使用)
test: {
extends: ['./index.js'],
rules: {
'max-lines-per-function': 'off',
'complexity': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
},
},
};
// package.json - 发布为npm包
// {
// "name": "@your-team/eslint-config-harmony",
// "version": "1.0.0",
// "main": "index.js",
// "peerDependencies": {
// "@typescript-eslint/eslint-plugin": "^6.0.0",
// "@typescript-eslint/parser": "^6.0.0",
// "eslint": "^8.0.0"
// }
// }
// 项目中使用团队配置
// .eslintrc.js
// 基础项目
module.exports = {
extends: ['@your-team/eslint-config-harmony/base'],
};
// 核心模块
module.exports = {
extends: ['@your-team/eslint-config-harmony/strict'],
};
// 测试代码
module.exports = {
extends: ['@your-team/eslint-config-harmony/test'],
};
// package.json - Lint脚本配置
{
"scripts": {
// 运行Lint
"lint": "eslint entry/src/main/ets --ext .ets,.ts",
// 自动修复
"lint:fix": "eslint entry/src/main/ets --ext .ets,.ts --fix",
// 检查特定规则
"lint:complexity": "eslint entry/src/main/ets --ext .ets,.ts --rule 'complexity: error'",
// 生成Lint报告
"lint:report": "eslint entry/src/main/ets --ext .ets,.ts -f json -o lint-report.json",
// Pre-commit钩子(配合husky)
"lint:staged": "eslint --ext .ets,.ts"
},
// 依赖
"devDependencies": {
"eslint": "^8.50.0",
"@typescript-eslint/eslint-plugin": "^6.7.0",
"@typescript-eslint/parser": "^6.7.0",
"@your-team/eslint-config-harmony": "^1.0.0"
}
}
踩坑与注意事项
坑1:Lint规则太多,开发者体验差
一保存文件就满屏红线,开发者心态崩了,直接eslint-disable全关。
解决方案:
- 区分error和warning:格式问题用warning,类型问题用error
- 渐进式引入:先开启核心规则,团队适应后再加更多
- 自动修复优先:能自动修复的问题不要手动改,配置保存时自动格式化
- Pre-commit只检查error级别,warning不阻塞提交
坑2:.ets文件解析器兼容问题
ESLint默认不支持.ets扩展名,需要额外配置。
解决方案:
// .eslintrc.js中必须配置
{
parserOptions: {
extraFileExtensions: ['.ets'], // 告诉解析器支持.ets
},
// 或在overrides中处理
overrides: [{
files: ['**/*.ets'],
parser: '@typescript-eslint/parser',
}],
}
坑3:团队Lint配置不一致
A项目用一套配置,B项目用另一套,新人换项目就懵了。
解决方案:
- Lint配置封装成npm包,所有项目统一引用
- 配置变更走版本管理,项目按需升级
- CI中检查Lint配置是否是最新版本
坑4:自定义规则维护成本高
自己写的Lint规则,过几个版本就跑不动了。
解决方案:
- 自定义规则尽量简单,只做AST层面的模式匹配
- 复杂的业务逻辑检查用脚本实现,不要硬塞进Lint
- 定期review自定义规则的必要性,该删就删
坑5:Lint与Prettier冲突
ESLint的格式规则和Prettier的格式化结果不一致,保存时格式化来格式化去。
解决方案:
- 格式化全部交给Prettier,ESLint只管代码质量
- 使用
eslint-config-prettier关闭ESLint中与Prettier冲突的规则 - 不要同时配置ESLint的格式规则和Prettier
HarmonyOS 6适配说明
HarmonyOS 6在Lint工具链方面的改进:
-
ArkTS Lint官方插件:华为官方发布了
@ohos/eslint-plugin-arkts,内置30+条ArkTS专用规则,覆盖装饰器规范、状态管理、生命周期等领域。之前需要自己写的规则,现在官方提供了。 -
DevEco Studio内置Lint:IDE内置了Lint检查,不再需要单独安装和配置ESLint。开箱即用,但仍然支持自定义扩展。
-
Lint规则与API版本关联:新版本的Lint规则可以检查API版本兼容性——你用了API 12的接口,但项目最低支持API 10,Lint会提醒你。
-
自动修复增强:更多的规则支持自动修复(
--fix),包括ArkTS特有的类型注解补全、装饰器参数规范等。 -
Lint性能优化:大型项目的Lint速度提升了约40%,增量Lint只需要1-2秒。
总结
Lint是编码规范的自动化执行者。没有Lint,规范就是一纸空文;有了Lint,规范才是真正的规则。
核心要点:
- 格式归Prettier,质量归ESLint:不要让两个工具打架
- 团队配置统一:封装成npm包,所有项目共用
- 渐进式引入:不要一次开太多规则,团队受不了
- 自动修复优先:能自动修的不要手动改
- 自定义规则要克制:维护成本高,能不用就不用
| 维度 | 评分 | 说明 |
|---|---|---|
| 学习难度 | ⭐⭐⭐ | 配置项多,自定义规则需要AST知识 |
| 使用频率 | ⭐⭐⭐⭐⭐ | 每次保存文件都在跑 |
| 重要程度 | ⭐⭐⭐⭐ | 规范的执行者,团队协作的基础 |
- 点赞
- 收藏
- 关注作者
评论(0)