鸿蒙PC_Dart-Sass-ohos适配全记录
鸿蒙PC开源移植:Dart Sass离线编译适配
欢迎加入开源鸿蒙PC社区: https://harmonypc.csdn.net/
欢迎在PC社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper
摘要: Harmony Sass 把官方 Dart Sass 1.101.6 浏览器运行时接入鸿蒙 PC,通过 ArkTS 编辑器与 ArkWeb 桥接完成离线编译。本文拆解运行时打包、异步任务、多文件 importer、文档 URI、Source Map 和项目监视的实现,提供可复现的六文件示例,并结合模拟器画面和官方行为对比测试,说明适配过程中的关键取舍。
关键词: 鸿蒙PC、开源软件移植、Dart Sass、SCSS、ArkWeb、离线编译
一、从一段 SCSS 到一个可以持续使用的工具
Sass 的最小使用过程很简单:输入带变量和嵌套的样式,得到 CSS。但把它带到鸿蒙 PC 后,真正的工作很快就超出了编译按钮。项目有多个文件时,@use 怎么找到依赖?从系统选择器打开的文档 URI,如何对应报错位置?外部编辑器保存了一个分部文件,哪些入口需要重新编译?CSS 导出到别的目录后,Source Map 还能不能找到源文件?
Harmony Sass 围绕这些问题组织适配。界面采用 ArkTS 双栏编辑器,左侧输入 Sass/SCSS,右侧展示 CSS 与诊断信息;编译由应用内的官方 Dart Sass 运行时完成。源码不需要发送到远程服务,Node.js 只参与开发电脑上的运行时构建和测试,不是应用运行时必须启动的进程。
工程托管在 AtomGit PC 社区:ohos_harmony-sass。本文介绍社区仓库中的实际代码,设备运行部分沿用 2026 年 9 月 5 日保存的记录。下面先讲清楚编译器如何进入 HAP,再讨论多文件项目为什么比单次编译更难。
二、选择官方运行时,把语言与平台适配分开
2.1 为什么选择 Dart Sass 浏览器发行版
Sass 语言的兼容性藏在很多细节里:模块只求值一次,分部文件与普通文件存在解析优先级,目录可以通过 _index.scss 暴露入口,包依赖还涉及 package.json 的导出配置。如果应用自己实现一套只支持变量、嵌套和混入的转换逻辑,很容易在示例上成功,却无法编译真实项目。
因此,项目保留官方 Dart Sass 的语言实现,把需要适配的范围集中在宿主侧:如何传入源码,如何提供已授权的依赖,如何取回 CSS 和错误,如何保存结果。Sass 源码中的 @function、控制流和 sass:* 模块继续由官方编译器处理,应用不重新定义这些语言行为。
固定版本也很关键。tools/package.json 将 sass 固定为 1.101.6、esbuild 固定为 0.28.1,配合锁文件安装依赖。这样测试得到的诊断、格式和弃用信息有明确基准;升级时可以对比同一组输入,而不是在不知情的情况下更换编译器。运行时依赖
2.2 编译器资源如何随 HAP 一起交付
tools/build-runtime.mjs 从 runtime-entry.mjs 开始打包,输出到 entry/src/main/resources/rawfile/sass-runtime.js。其中决定运行环境的配置片段如下:
bundle: true,
platform: 'browser',
format: 'iife',
target: ['es2020'],
minify: true,
legalComments: 'external',
platform: 'browser' 让打包过程采用浏览器环境入口;IIFE 形式使运行时能够通过普通脚本加载。构建脚本同时生成 sass-runtime.html,该页面只负责加载本地脚本,并复制官方许可证和依赖声明。完整构建脚本
这一步解决了两个实际问题。首先,编译器与应用版本绑定,不必在启动后从 CDN 拉取脚本。其次,生成文件有可追溯的输入:修改运行时适配应从 runtime-entry.mjs 开始,再重新构建,不能手工修改压缩后的几兆字节 JavaScript。
三、ArkTS 与 ArkWeb 之间,不能只传一个字符串
3.1 可见编辑器和编译执行环境各司其职
应用页面使用原生 ArkTS 控件显示编辑器,同时保留一个承载编译器的 ArkWeb 组件。下面是 Index.ets 中的实际片段:
Web({
src: $rawfile('sass-runtime.html'),
controller: this.controller
})
.javaScriptAccess(true)
.width(1)
.height(1)
.opacity(0)
.enabled(false)
.onPageEnd((): void => {
this.initializeRuntime();
})
这个组件承担本地脚本执行,用户看到的双栏界面由 ArkTS 绘制。初始化等到页面加载结束后进行,而不是假设创建控制器时脚本就已经可用。initializeRuntime() 读取运行时元数据,检查 sass.info 前缀、版本和异步编译模式,再允许编译。页面源码
这样排查启动问题时就有明确层次:双栏界面能显示,只能说明页面已经进入;还要确认本地 HTML 加载、harmonySass 对象建立、编译器元数据读取成功,才算编译环境准备就绪。
3.2 请求需要携带一个项目快照
单文件演示可以只传源码,真实项目还需要入口路径、入口 URI、文件集合、加载路径、输入语法和编译选项。页面的 compileWithDartSass() 先同步当前编辑内容,再组装 SassCompileRequest,避免用户刚输入的文字还没有进入项目模型就开始编译。
逻辑上,请求包含三组信息:当前入口与内容、可访问的依赖文件、输出和诊断选项。桥接类把它序列化成 JSON 后,生成对运行时的调用。以下方法来自 DartSassRuntime.ets:
static createAsyncProjectCompileScript(request: SassCompileRequest): string {
return `globalThis.harmonySass.startCompileProjectAsync(${JSON.stringify(request)})`;
}
使用 JSON.stringify() 的目的,是正确保留源码里的换行、引号和反斜杠,使参数作为数据进入 JavaScript 调用。这里是 JavaScript 桥接,不是拼接并执行 Shell 命令。桥接类型与实现
3.3 用任务编号跨越异步边界
运行时通过 sass.initCompiler() 和 sass.initAsyncCompiler() 创建可复用编译器。编辑器采用异步编译路径,在 compileProjectResultAsync() 中等待官方 compileStringAsync(),再把 CSS、已加载 URL 和诊断组织为结果。
ArkTS 侧没有把官方编译器对象或 Promise 直接跨环境传递。调用过程是:启动任务,取得 jobId,轮询 pending 或 complete 状态,完成后读取序列化结果。waitForAsyncJob() 设置了 120 秒等待上限,未消费的任务会在清理路径中释放;页面退出也会清理登记的异步任务。
这套机制解决的是结果传递与生命周期管理。释放桥接任务记录不等于强制打断 Dart Sass 正在执行的求值;调用了异步 API,也不能自动推导出编译工作运行在独立 Worker 或线程。文章因此将它称为异步桥接,而不据此宣称大型项目绝不会阻塞。
运行时在 pagehide 中清理任务并释放编译器。复用和释放需要一起设计,否则连续编译会反复初始化,或在页面退出后留下已经没人消费的结果。运行时入口
四、多文件适配的核心:路径能解析,来源也要保留
4.1 虚拟路径与文档 URI 是两种信息
在桌面 Node.js 环境中,编译器可以围绕磁盘路径寻找依赖。鸿蒙应用通过系统文档选择器访问用户授权的文件,ArkWeb 不应自行获得整个文件系统的访问能力。因此,项目由 ArkTS 读取文件内容,建立内存文件集合,再由 importer 提供给官方编译器。
一个文件既有项目中的相对路径,例如 styles/_tokens.scss,也可能有真实文档 URI。前者用于解析模块关系,后者用于保存、错误定位和 Source Map。如果只保留文件名,两个不同目录中的 _tokens.scss 会发生混淆;如果只保留系统 URI,用户熟悉的相对 @use 又无法直接解析。
SassProject.ets 负责组织和规范化项目路径;runtime-entry.mjs 则保留真实来源 URL,并为无物理文件的内容使用 harmony-sass: 虚拟地址。它们共同把“编译器眼中的项目”与“用户授权的文件”联系起来。项目模型
4.2 importer 为什么分成定位和读取两步
对于 @use "styles/tokens",首先需要判断实际模块是哪一个文件,然后才能提供它的内容。运行时 importer 的 canonicalize() 处理模块定位,load() 返回内容、语法和 Source Map 来源。稳定的规范 URL 也让官方编译器能够识别重复引用的同一模块。
真实解析不能机械地补上 .scss。要考虑 _tokens.scss 这样的分部文件、普通文件、目录索引、Sass 与 CSS 候选、传统导入专用文件,以及多个候选同时存在时的歧义。错误的“宽容处理”可能让示例编译成功,却与官方选择了不同的依赖。
项目把这些情况纳入同版本官方对比测试。发生歧义时,应保留相应错误,而不是任意挑一个文件继续编译。这种约束让适配层承担文件供给,语言求值仍交给官方实现。
4.3 pkg: 解析不是在设备上安装 npm
包导入还需要读取已加载的 package.json,按虚拟项目中的 node_modules 位置与导出配置寻找样式入口。项目提供虚拟环境中的 NodePackageImporter 兼容行为,覆盖作用域包、最近依赖目录、exports 条件和嵌套依赖等情况。
这里的 pkg:theme 表示从已经载入的项目文件里解析 theme 包,不会自动联网下载 npm 包,也不能越过授权范围访问其他磁盘目录。package.json 参与解析,但不会作为可编译的样式入口显示。区分这两点,才能正确理解界面中的项目文件数与入口数。
五、用一个完整项目串起模块、混入和包依赖
原来的单段 SCSS 示例只能证明基础计算。为了让读者复现截图中的多文件关系,这里提供一个六文件项目。它按截图展示的模块和输出组织,用于独立复现相同语义,不声称恢复了当时所有源文件的逐字节内容。
目录如下,完整文件也随文章保存在 examples/sass-demo:
sass-demo/
├── main.scss
├── secondary.scss
├── styles/
│ ├── _tokens.scss
│ └── _components.scss
└── node_modules/
└── theme/
├── package.json
└── _index.scss
main.scss 使用变量模块、混入、官方数学模块和包导入:
@use "styles/tokens";
@use "styles/components";
@use "sass:math";
@use "pkg:theme";
.app {
color: tokens.$brand;
width: math.div(30px, 2);
@include components.panel(8px);
border-color: theme.$accent;
}
styles/_tokens.scss 提供颜色变量:
$brand: #0a7bff;
styles/_components.scss 定义接受圆角参数的混入:
@mixin panel($radius) {
border-radius: $radius;
padding: 16px;
}
node_modules/theme/package.json 明确包的 Sass 入口:
{
"name": "theme",
"version": "1.0.0",
"exports": {
".": {
"sass": "./_index.scss"
}
}
}
对应的 node_modules/theme/_index.scss:
$accent: #ff3366;
第二个入口 secondary.scss 用来观察批量编译和依赖变化:
@use "styles/tokens";
.secondary {
color: tokens.$brand;
}
在应用中通过文件夹载入或“添加项目文件”将六个文件加入同一项目,核对相对路径,并选择 main.scss 编译;若仍保留内置示例或其他文件,总数会相应增加。文件缺少时,应先补齐依赖,不要通过删掉 @use 来掩盖导入问题。
在 expanded 格式下,主入口 CSS 应为:
.app {
color: #0a7bff;
width: 15px;
border-radius: 8px;
padding: 16px;
border-color: #ff3366;
}
这里每个结果都有来源:颜色来自 tokens,宽度来自 math.div(),圆角和内边距来自混入,边框颜色来自包依赖。当前入口加载的是 main.scss 与三个依赖样式文件,共四个;package.json 是解析元数据,secondary.scss 是另一个入口,都不应简单计入本次入口的已加载样式文件数。
这个例子还能用于检查增量编译:修改 _tokens.scss 会影响两个入口;只修改 _components.scss 则只影响主入口。把依赖关系设计得明确,才能判断自动编译有没有选择正确范围。
六、连续编辑时,自动编译究竟在做什么
6.1 防抖解决频率,依赖图解决范围
用户连续输入时,不适合每个字符都立即编译。页面 scheduleCompile() 将变化路径放入集合,重新设置 350 毫秒定时器,等连续输入暂时停下来再决定要编译哪些入口。这是当前代码中的调度参数,不是设备上的性能测量结果。
多入口自动编译还会查询 SassWorkspace.affectedEntryPaths()。项目根据编译结果中的 loadedUrls 建立“依赖文件 → 入口集合”的反向关系:修改某个分部文件时,直接找到依赖它的入口。没有已知依赖图,或变化路径无法在图中找到时,函数回退到全部入口,避免漏掉应重编译的内容。工作区实现
已有结果通过 mergeBatchResponse() 与本次结果合并,使没有重编译的入口保留结果。新增、删除、重命名和选项变化可能改变依赖关系,需要触发完整刷新。这比把所有变化都当作普通文本修改更稳妥。
6.2 外部监视需要保护未保存内容
当前文件监视使用定时检查,间隔常量为 1200 毫秒,同时结合目录扫描和文件签名判断变化。它是应用层监视机制,不能等同于已经实现了官方 CLI 的全部 watch 或 --update 协议。
发现外部文件改变后,应用先检查 dirtyPaths。文件在应用内还有未保存编辑时,记录冲突并保留编辑内容;没有本地未保存修改时,才重新读取文件并安排编译。否则,外部一次保存就可能覆盖用户正在输入的代码。
文件短暂不可用也不能立即视为删除。代码会结合连续缺失次数和目录扫描结果判断,避免把一次读取异常当成永久移除。休眠恢复则通过页面状态和检查间隔触发刷新,重新核对项目。上述机制已有实现,但长时间监视与权限恢复的实际表现仍要在目标实体 PC 上验证。
七、CSS 正确只是第一步,诊断和导出也要对齐
7.1 保留错误的来源信息
如果编译失败只返回“编译错误”,用户就无法判断是入口语法问题还是某个依赖缺失。运行时因此将异常转换成结构化信息,包括源码范围、URL、Sass 调用栈和运行时堆栈,警告还包含弃用元数据。ArkTS 页面再把这些信息组织为可读诊断。
quietDeps 也需要正确区分相对文件、加载路径依赖和包依赖。把所有文件都放进内存,并不意味着这些来源在诊断语义上变成同一种类型。现有测试会与官方编译器比较警告和源码范围,防止桥接后只剩 CSS 正确、诊断却失真的情况。
7.2 Source Map 的路径必须跟着导出位置走
编译器生成 Source Map 后,用户可能把 CSS 导出到新的文件夹。此时要同时处理 CSS 尾部的 sourceMappingURL、Map 中的 file 字段,以及 sources 相对于导出位置的关系。若直接把内存中的 Map 原样另存,浏览器调试时可能定位不到源文件。
运行时的 finalizeExports() 处理这些导出关系,支持外部或内嵌 Source Map、相对或绝对源 URL,并对文件名和数据 URI 做相应编码。应用输出管理则保留输入目录层级,登记生成文件的位置,在后续编译时更新或清理这些已登记产物。
导出 CSS 与 Map 是相互关联的操作,但文章不将它描述为文件系统事务。目标目录授权、连续写入和休眠恢复仍受文档提供器行为影响,需要专门检查。工程中的测试会逐字节对比官方 CLI 输出的相关格式,设备测试则负责验证授权和实际文件写入,两者关注不同问题。运行时与导出实现
八、从开发电脑构建,再到鸿蒙环境核对结果
8.1 先单独验证编译运行时
准备 Node.js 20.19 或更高版本,克隆社区工程后,可以先运行不依赖鸿蒙 SDK 的验证:
git clone https://atomgit.com/OpenHarmonyPCDeveloper/ohos_harmony-sass.git
cd ohos_harmony-sass
npm --prefix tools ci
npm --prefix tools run verify
这一步重新生成浏览器运行时,并执行 tools/test-runtime.mjs。测试使用 Node VM 加载打包文件,再与相同版本官方 Sass API 和 CLI 对比,涵盖单文件、多文件、同步异步结果、包解析、Source Map 和诊断。它可以发现适配层错误,但 Node VM 通过并不等于已经验证 ArkWeb 生命周期与设备文件权限。测试源码
本次文章修订在 Windows、Node.js 24.14.0 环境补做了验证:前文六文件示例的两个入口均与官方 Dart Sass 输出一致,主入口返回四个已加载文件;运行时重新打包也成功。但完整 verify 在 Error CSS 的逐字节比较处失败:浏览器运行时诊断使用 /C:/.../src/app.scss 形式,Windows 官方 CLI 使用 C:\...\src\app.scss 形式,错误文本和 CSS 字符串中的路径转义因此不同。
这个结果说明,成功编译时的 CSS 一致与失败诊断的跨宿主格式一致,需要分开验证。本轮没有修改编译器或测试来抹平差异,也没有将 Windows 完整测试记录为通过;涉及该项回归时,应保留失败输出,进一步判断目标平台需要的诊断格式。
8.2 再完成 HAP 工程构建
应用构建需要 DevEco Studio 和 HarmonyOS 6.1.1(API 24)SDK。完整脚本还会安装鸿蒙工程依赖、执行 ArkTS 测试并组装开发 HAP:
bash ./scripts/verify.sh
脚本默认使用 macOS DevEco 路径;非默认安装需要配置相应环境变量。Windows 可以在 DevEco Studio 中打开工程并运行,不应把上述脚本写成所有宿主系统开箱即用的命令。详细步骤见 README.OpenHarmony_CN.md。
工程曾遇到中文路径导致的构建问题,脚本已经提供临时英文目录构建逻辑。若直接从 IDE 构建,仍建议先用英文工程目录复现。开发包输出为:
entry/build/default/outputs/default/entry-default-unsigned.hap
设备是否允许安装该包取决于开发与签名配置。在 DevEco Studio 中选择 entry/default 和目标设备,再完成调试签名与运行,不能把未签名开发包等同于正式发布包。
8.3 现有运行截图证明了什么
2026 年 9 月 5 日的运行记录来自 OpenHarmony-6.1.1.125、API 24、2in1 模拟器。记录保存了代码基线、开发包哈希和设备侧截图,并确认界面与成功状态显示 Dart Sass 1.101.6。

图1:2026年9月5日设备侧 screenCap 采集的完整桌面,保留原生编辑器、鸿蒙桌面和任务栏;这是模拟器实际运行画面。
画面左侧的 main.scss 与右侧 CSS 可以逐项核对:math.div(30px, 2) 得到 15px,panel(8px) 产生圆角和内边距,pkg:theme 提供边框颜色。界面中的“6 个文件·2 个入口”描述项目规模,“已载入 4 个文件”描述当前编译,两者没有矛盾。
这张图直接证明所示多文件编译成功,没有覆盖所有导出权限、长时间监视、休眠恢复和大型项目性能。实体 PC 的最终验证仍需补充,不能把模拟器截图改称真机结果。运行记录
2026 年 9 月 11 日又在本机建立了独立的 MateBook Pro 2in1 模拟器实例,并安装按当前源码构建的开发 HAP。启动后可以确认应用在 PC 横向窗口中加载 ArkTS 编辑器、ArkWeb 运行时和 Dart Sass 1.101.6;本轮截图用于补充 PC 窗口启动证据:

图2:2026年9月11日,在 HarmonyOS 6.1.1 / API 24 MateBook Pro 2in1 模拟器中启动当前开发 HAP。画面展示双栏编辑器、内置 Dart Sass 版本和 PC 横向窗口;截图不代表实体鸿蒙 PC 真机。
本轮还尝试在设备编辑区输入错误语句以补充错误定位截图,但模拟器输入法会把自动化输入插入到已有文本中,无法形成可审计的最小错误案例。因此文章继续以已有运行时对比测试作为诊断证据,不把这次输入失败画面用于证明错误定位或 Source Map 导出已经在 PC 设备上通过。
九、常见问题如何定位到具体层
| 现象 | 先检查什么 | 对应原因与处理方向 |
|---|---|---|
| 编辑器显示,但一直无法编译 | 本地运行时资源、onPageEnd、元数据返回 | 页面启动与编译器就绪是不同阶段,确认版本及异步模式读取成功。 |
单文件正常,@use 找不到模块 | 项目相对路径、分部文件和实际载入集合 | 只打开入口不等于加载全部依赖;先确认路径组织。 |
pkg:theme 失败 | 包目录、package.json 和导出目标 | 应用不会自动下载 npm 包,元数据和样式文件都要载入。 |
| 同名文件导致导入歧义 | importer 候选与官方优先级 | 不任意挑选候选,检查文件组织并对照官方行为。 |
| 外部保存后没有覆盖编辑内容 | 应用内是否有未保存修改 | 可能触发了冲突保护,应核对两个版本后再保存。 |
| CSS 正常,Map 定位错误 | 导出目录、文件名编码、来源 URI | 检查 CSS 与 Map 的关联以及 sources 路径。 |
| 显示任务超时 | 编译耗时、任务状态与页面生命周期 | 超时释放桥接记录不代表中止编译器求值,需分别定位。 |
十、T0 / T1 / T2 与后续适配重点
| 阶段 | 当前实现与验证 | 仍需关注的范围 |
|---|---|---|
| T0:基础编辑编译 | 官方运行时随 HAP 打包,ArkTS 双栏界面通过 ArkWeb 编译;已有模拟器安装启动与编译记录。 | 实体 PC 的启动、键盘、输入法及生命周期回归。 |
| T1:日常项目开发 | 支持 SCSS/Sass/CSS、多文件模块、加载路径、保存、诊断与 CSS 导出;截图直接验证所示多文件入口。 | 不同文档提供器的 URI、文件夹选择和持久授权行为。 |
| T2:持续开发工作流 | 支持包解析、Source Map、批量编译、依赖图增量更新、外部监视和输出管理,配套模型与运行时测试。 | 长时间监视、休眠恢复、大型项目性能与目录权限变化的实体设备验证。 |
当前桥接不接受任意宿主 JavaScript Options.functions、Importer/FileImporter 或 Logger 回调,也不提供完整 Node.js 文件系统入口 API、Embedded Protocol 和 CLI 进程。Sass 语言中的 @function 则正常交给官方编译器执行,二者不能混为一谈。完整能力边界
这次适配的收获,是把“编译器能运行”继续推进到“项目能编辑、依赖能解析、结果能解释”。官方运行时负责语言语义,应用负责授权文件与桌面工作流,二者之间通过明确的数据结构连接。今后升级 Dart Sass 时可以重跑同版本对比;改动鸿蒙文件处理时,可以集中验证 URI、权限和恢复行为。
对于开发工具,稳定的边界比堆积选项更有价值。下一步会继续围绕真实项目和实体鸿蒙 PC 检查这些连接点,让一次成功编译能够延伸为连续编辑、保存和调试的日常使用过程。
参考资料与源码入口
- AtomGit:ohos_harmony-sass:鸿蒙 PC 适配工程。
- Dart Sass 上游:官方编译器来源。
- 运行时打包脚本:本地资源生成与许可证处理。
- 运行时适配与 importer:编译、包解析、异步任务与导出。
- 工作区依赖模型:依赖图和批量结果管理。
- 编译运行说明:环境、HAP 构建与安装。
- 2026年9月5日运行记录:模拟器环境和已有证据。
更多推荐

所有评论(0)