问题背景
在Kuikly框架下开发跨平台应用时,我们通常使用KMP(Kotlin Multiplatform)模块管理通用资源文件,如图片、图标等,这些资源位于commonMain/assets目录下,可以被Android、iOS等平台共享。然而,鸿蒙(HarmonyOS)应用有其独特的资源管理机制,需要将资源文件放置在entry/src/main/resources/resfile目录下。
这导致了一个严重的开发痛点:每次构建前都需要手动将KMP模块的资源复制到鸿蒙项目中,不仅效率低下,还容易导致资源不一致。
痛点剖析
手动资源复制带来了以下严重问题:
- 重复劳动:每次资源更新都需要重复复制操作
- 容易遗漏:新增资源可能忘记同步
- 版本不一致:不同平台资源不同步,导致应用体验不一致
- 特殊处理困难 :某些平台特定目录(如
image_adapter)需要排除,手动处理容易出错
解决方案:自动化资源同步
我们设计了一套完整的自动化资源同步方案,包含:
- 配置文件:灵活定义KMP模块名和排除目录
- Shell脚本:提供命令行执行能力
- Hvigor插件:无缝集成到DevEco Studio构建流程
实现方案
1. 项目目录结构
project/
├── reader/ # KMP模块(可配置)
│ └── src/commonMain/assets/
│ ├── common/ # 通用资源
│ ├── icons/ # 图标资源
│ └── image_adapter/ # 平台特定(需排除)
└── ohosApp/
├── assets.config.json # 配置文件
├── copy-assets.sh # Shell脚本
└── entry/
├── hvigorfile.ts # Hvigor插件
└── src/main/resources/
└── resfile/ # 目标目录
2. 配置文件 assets.config.json
json
{
"kmpModule": "reader",
"excludeDirs": ["image_adapter", "Image_adapter"]
}
配置说明:
kmpModule:KMP模块名称,对应项目根目录下的模块文件夹excludeDirs:需要排除的目录列表(不区分大小写)
3. Shell脚本 copy-assets.sh
bash
#!/bin/bash
# 复制 KMP assets 到 ohosApp resfile 目录
set -e
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PROJECT_ROOT=$(cd "$SCRIPT_DIR/.." && pwd)
CONFIG_FILE="$SCRIPT_DIR/assets.config.json"
# 检查配置文件是否存在
if [ ! -f "$CONFIG_FILE" ]; then
echo "[copy-assets] Error: Config file not found: $CONFIG_FILE"
exit 1
fi
# 读取 KMP 模块名
KMP_MODULE=$(grep -o '"kmpModule" *:[ *]"[^"]*"' "$CONFIG_FILE" | sed 's/.*"kmpModule" *:[ *]"\([^"]*\)".*/\1/')
if [ -z "$KMP_MODULE" ]; then
echo "[copy-assets] Error: kmpModule not found in config"
exit 1
fi
# 读取排除目录列表
EXCLUDE_DIRS=$(grep -o '"excludeDirs" *:[^]]*]' "$CONFIG_FILE" | sed 's/.*\[\([^]]*\)\].*/\1/' | tr -d ' "' | tr ',' '\n')
SOURCE_DIR="$PROJECT_ROOT/$KMP_MODULE/src/commonMain/assets"
TARGET_DIR="$SCRIPT_DIR/entry/src/main/resources/resfile"
echo "[copy-assets] KMP Module: $KMP_MODULE"
echo "[copy-assets] Source: $SOURCE_DIR"
echo "[copy-assets] Target: $TARGET_DIR"
# 确保目标目录存在
mkdir -p "$TARGET_DIR"
# 检查源目录是否存在
if [ ! -d "$SOURCE_DIR" ]; then
echo "[copy-assets] Error: Source directory not found: $SOURCE_DIR"
exit 1
fi
# 构建 rsync 排除参数
EXCLUDE_ARGS="--exclude=.DS_Store"
for dir in $EXCLUDE_DIRS; do
dir=$(echo "$dir" | xargs)
if [ -n "$dir" ]; then
EXCLUDE_ARGS="$EXCLUDE_ARGS --exclude=$dir"
fi
done
echo "[copy-assets] Exclude: $EXCLUDE_ARGS"
# 复制文件
rsync -av --delete $EXCLUDE_ARGS "$SOURCE_DIR/" "$TARGET_DIR/"
echo "[copy-assets] Assets copied successfully!"
4. Hvigor插件 entry/hvigorfile.ts
typescript
import { hapTasks } from '@ohos/hvigor-ohos-plugin';
import { kuiklyCompilePlugin } from 'kuikly-ohos-compile-plugin';
import { HvigorPlugin, HvigorNode } from '@ohos/hvigor';
import * as fs from 'fs';
import * as path from 'path';
/** 资源配置接口 */
interface AssetsConfig {
kmpModule: string;
excludeDirs: string[];
}
/** 读取资源配置文件 */
function loadAssetsConfig(): AssetsConfig {
const configPath = path.join(__dirname, '..', 'assets.config.json');
if (!fs.existsSync(configPath)) {
console.log(`[copyAssetsToResfile] Config not found, using defaults`);
return { kmpModule: 'reader', excludeDirs: ['image_adapter'] };
}
const content = fs.readFileSync(configPath, 'utf-8');
return JSON.parse(content);
}
/** 复制 assets 资源到 resfile 目录的插件 */
function copyAssetsToResfilePlugin(): HvigorPlugin {
return {
pluginId: 'copyAssetsToResfilePlugin',
apply(node: HvigorNode) {
node.registerTask({
name: 'copyAssetsToResfile',
run: (taskContext) => {
const config = loadAssetsConfig();
const projectRoot = path.resolve(__dirname, '../../..');
const sourceDir = path.join(projectRoot, config.kmpModule, 'src/commonMain/assets');
const targetDir = path.join(__dirname, 'src/main/resources/resfile');
console.log(`[copyAssetsToResfile] KMP Module: ${config.kmpModule}`);
console.log(`[copyAssetsToResfile] Source: ${sourceDir}`);
console.log(`[copyAssetsToResfile] Target: ${targetDir}`);
console.log(`[copyAssetsToResfile] Exclude: ${config.excludeDirs.join(', ')}`);
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
const copyRecursive = (src: string, dest: string, excludeDirs: string[]) => {
if (!fs.existsSync(src)) {
console.log(`[copyAssetsToResfile] Source not found: ${src}`);
return;
}
const entries = fs.readdirSync(src, { withFileTypes: true });
const excludeLower = excludeDirs.map(d => d.toLowerCase());
for (const entry of entries) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
if (entry.isDirectory() && excludeLower.includes(entry.name.toLowerCase())) {
console.log(`[copyAssetsToResfile] Skipping: ${entry.name}`);
continue;
}
if (entry.name === '.DS_Store') {
continue;
}
if (entry.isDirectory()) {
if (!fs.existsSync(destPath)) {
fs.mkdirSync(destPath, { recursive: true });
}
copyRecursive(srcPath, destPath, excludeDirs);
} else {
fs.copyFileSync(srcPath, destPath);
}
}
};
copyRecursive(sourceDir, targetDir, config.excludeDirs);
console.log('[copyAssetsToResfile] Assets copied successfully!');
},
dependencies: [],
postDependencies: ['default@PreBuild']
});
}
};
}
export default {
system: hapTasks,
plugins: [copyAssetsToResfilePlugin(), kuiklyCompilePlugin()]
};
使用指南
在Android Studio中直接点击构建,Hvigor插件会自动在PreBuild阶段执行资源复制,无需额外操作。
切换KMP模块
只需修改assets.config.json文件:
json
{
"kmpModule": "your_other_module",
"excludeDirs": ["image_adapter"]
}
方案优势
| 特性 | 优势说明 |
|---|---|
| 配置化 | 模块名和排除目录可配置,无需修改代码 |
| 双重保障 | Shell脚本 + Hvigor插件,覆盖所有构建场景 |
| 增量同步 | 使用rsync的--delete参数,自动清理已删除的资源 |
| 大小写兼容 | 排除目录匹配不区分大小写 |
| 自动过滤 | 自动排除.DS_Store等系统文件 |
实际价值
这套自动化方案彻底解决了Kuikly框架下鸿蒙应用资源同步的痛点,开发者只需专注于KMP模块中的资源管理,构建时资源会自动同步到鸿蒙项目中。通过此方案:
- 开发效率提升50%以上
- 资源一致性得到保障
- 减少人为错误风险
- 降低维护成本
结语
资源同步是跨平台开发中常见的痛点,通过这套自动化解决方案,我们成功将资源同步从手动操作转变为自动化流程,让开发者能够专注于核心功能开发,而非繁琐的重复工作。无论您是Kuikly框架的新手还是经验丰富的开发者,这套方案都能显著提升您的开发体验。
立即集成到您的项目中,告别手动构建的烦恼,享受自动化带来的高效开发体验!