- 前端
- 构建工具
【免费下载链接】css-blocks
High performance, maintainable stylesheets.
@css-blocks/ember-app是 CSS Blocks 在 Ember 生态中的核心 ember-cli addon,负责将应用内所有 CSS Blocks 内容聚合、经 OptiCSS 优化后拼接进最终样式产物,并生成模板运行时所需的类名数据与测试支持数据。本文以该包的 CHANGELOG 为脉络,结合 README 与仓库源码,完整讲解它的集成方式、ember-cli-build.js配置项、构建管线底层实现,以及 1.2.0 → 1.5.0 的关键能力演进,读完即可在 Ember 应用中正确接入并排查 CSS Blocks 相关问题。
一、这个 addon 在构建管线中的定位
@css-blocks/ember-app是一个面向 Ember 应用的 ember-cli addon,应当作为应用的 dependency 安装。它的职责在 src/index.ts 的注释中有明确交代:
- 把应用(以及依赖的 addon、engine)里所有的 CSS Blocks 内容打包在一起;
- 使用 OptiCSS 对编译后的 CSS 进行优化,并拼接成最终产物;
- 生成一份 JSON 形式的运行时数据(runtime data),供模板重写后解析"该给组件加哪些类名";
- 提供一个运行时 helper 真正把类名写出来;
- 额外生成测试支持数据(test support data)。
关键前提:@css-blocks/ember-app假定所有中间 block 已经由@css-blocks/ember这个 addon 编译成compiledblock.css与block-analysis.json。换句话说,应用和它依赖的 addon/engine 只要有 CSS Blocks 文件,就必须同时依赖@css-blocks/ember,否则本 addon 不会产出任何 CSS 输出。
另一个容易忽略的细节(同样记录在 src/index.ts 的注释中):CSS Blocks 的编译发生在模板树(Template tree)而非样式树中。因为 CSS Blocks 需要同时推理模板与样式,才能完成模板重写;@css-blocks/ember-app则负责在 CSS 树阶段把这些编译结果合并进最终样式。这就是为什么它会同时干预preprocessTree与postprocessTree两个阶段。
二、基本使用:三步接入
按照 README 的说明,接入只需要三步:
- 把
@css-blocks/ember-app添加为应用的依赖; - 如果应用要使用 CSS Blocks 编写样式,同时把
@css-blocks/ember添加为依赖; - 执行
ember build。
输出行为分两种场景:
- 如果应用存在
app/styles/app.css,CSS Blocks 的构建输出会被自动追加到该文件的末尾; - 如果应用使用其他样式文件名(例如经过了预处理器处理),构建时会生成一个
app/styles/css-blocks.css文件,你可以把它 include/import 进预处理器,或与其他文件拼接成应用的最终 CSS。
如果应用使用了 engine,则每个使用@css-blocks/ember的 engine 都要把css-blocksservice 加入依赖。
关于开发构建与生产构建的差异,README 也给出了明确说明:开发构建输出带开发者友好 BEM 类名的 CSS,方便理解;生产构建输出经过拼接、优化和压缩的 CSS 产物。
三、ember-cli-build.js配置选项全解
所有选项通过应用ember-cli-build.js中的css-blocks属性传入,最终由 ember-utils/src/options.ts 定义的CSSBlocksEmberOptions接口解析。下面逐一展开。
3.1output
- 类型:
<string> - 默认值:
"css-blocks.css"(见 options.ts) - 作用:改变 CSS Blocks 样式写入
app/styles目录的文件名。 - 注意:一旦设置该值,样式永远不会自动与
app/styles/app.css拼接。从源码看,该文件名在预处理阶段位于app/styles/${output},后处理阶段位于assets/${output}(见 utils/filepaths.ts)。若传入的值不是字符串,getConfig会直接抛出错误(见 options.ts)。
3.2aliases
- 类型:
<object> - 作用:为 CSS Blocks 文件提供导入别名,键对应别名,值是指向包含 CSS Blocks 文件的目录的绝对路径。
- 示例(来自 README):
{myblocks: path.resolve(__dirname, '../blocks')}会让@block foo from 'myblocks/header.block.css';导入../blocks目录下的header.block.css。 - 实现:默认 importer 是
NodeJsImporter(options.aliases)(见 options.ts)。
3.3analysisOpts
- 类型:模板分析选项。
- 默认:
{}。 - 作用:传给
EmberAnalyzer的模板分析参数。README 明确提示"你大概不需要设置这些",属于高级自定义项。
3.4parserOpts
- 类型:传给 CSS Blocks parser 和 compiler 的选项。
- 默认行为:如果没有设置,这些选项会自动从
css-blocks.config.js配置文件加载(config.searchSync(root),见 options.ts)。使用配置文件的好处是:@css-blocks/cli等工具能与 Ember 应用加载到同一套选项。 - 强制行为:
parserOpts.outputMode会被强制设为OutputMode.BEM_UNIQUE(见 options.ts),rootDir若未设置会被补上应用根目录(见 options.ts)。
3.5optimization
- 类型:传给 OptiCSS optimizer 的选项。
- 默认:
{};其中enabled未显式设置时,默认仅在生产构建(isProduction)时开启(见 options.ts)。 - 作用:可选择性开启/关闭某些具体优化,或通过把
enable设为false显式关闭全部优化。
3.6broccoliConcat
- 类型:
BroccoliConcatOptions | false(完整接口见 ember-utils/src/options.ts)。 - 作用:控制
broccoli-concat在后处理阶段拼接 CSS 的行为,包括inputFiles、outputFile、header、headerFiles、footerFiles、footer、sourceMapConfig、allowNone等。 - 特殊值:设为
false时,broccoli-concat 不会运行,你需要自行追加额外处理,把 CSS Blocks 编译内容加入最终 CSS 产物。 - 默认拼接行为:把
assets/css-blocks.css与assets/<modulePrefix>.css拼接,输出到assets/<modulePrefix>.css,并开启 css sourcemap(mapCommentType: "block"、extensions: ["css"]),详见 src/index.ts。用户提供的sourceMapConfig会被强制修正extensions与mapCommentType两个字段(见 src/index.ts),避免拼接出错。
3.7appClasses
- 类型:
string[] - 默认:
[] - 作用:列出应用 CSS 中使用、可能与优化器冲突的类名。README 建议把所有短类名(约 5 个字符)加入此列表,防止优化器在生成 CSS Blocks 编译输出时复用这些类名。
- 实现:这是
optimization.rewriteIdents.omitIdents.class[]的便捷别名。在 src/broccoli-plugin.ts 的reserveClassnames()中,appClasses会被 push 进omitIdents.class列表,同时rewriteIdents.id被强制设为false(不重写 id),class保留用户设置。若优化被禁用,该选项不生效。
四、构建管线源码级拆解
@css-blocks/ember-app的构建逻辑集中在两个文件:addon 主入口 src/index.ts 与三个 broccoli 插件 src/broccoli-plugin.ts。
4.1 JS 树预处理:收集模板分析产物
在preprocessTree("js")阶段(见 src/index.ts),只有isApp为真的环境会执行核心逻辑:
- 遍历所有启用了 lazy loading 的 lazy engine addon,找到其中依赖
@css-blocks/ember的 addon,取其templateCompiler的模板输出,合并进一个lazy-tree-output子目录; - 用
CSSBlocksApplicationPlugin处理[app.addonTree(), tree, lazyOutput]三棵树的合并结果,产出优化后的模板树(css-blocks:optimized)。
也就是说,lazy engine 的模板分析与 CSS 输出会被并入应用构建,这正是 CHANGELOG 中多处提到"支持 lazy engines"的底层机制。从 1.2.0 的"egregious hack"到 1.2.1 的"slightly less hacky approach",再到preprocessTree中基于a.lazyLoading && a.lazyLoading.enabled === true的显式过滤逻辑,lazy engine 支持是逐步收敛成形的。
4.2 CSS 树预处理:把编译产物搬进样式树
preprocessTree("css")阶段(见 src/index.ts)使用CSSBlocksStylesPreprocessorPlugin(实现见 broccoli-plugin.ts)把 JS 树阶段生成的 CSS Blocks 编译内容复制到 CSS 树中默认位置app/styles/css-blocks.css,并转发优化器生成的类名列表 JSON(app/styles/css-blocks-stylelist.json)。如果插件实例尚未创建(JS 树未跑),会抛出明确的错误提示——这在正常情况下不会发生,因为 JS 树总是先于 CSS 树处理。
4.3CSSBlocksApplicationPlugin:核心编译与优化
这是整个 addon 的心脏(见 broccoli-plugin.ts)。它的build()流程可以概括为:
- 扫描输入树中的
**/*.{compiledblock.css,block-analysis.json},用fs-tree-diff计算增量 patch,没有变化就跳过重编译(首次构建会生成一份空的运行时数据文件,避免运行时错误,见 broccoli-plugin.ts); - 解析配置、构造
BlockFactory与EmberAnalyzer; - 对每个
block-analysis.json反序列化分析结果,收集所有传递依赖的 block,加入 OptiCSS optimizer; - 对每个 block:优先复用未编辑的预编译 CSS(
block.precompiledStylesheetUnedited),否则用BlockCompiler重新编译,然后作为 source 加入 optimizer; - 调用
optimizer.optimize(cssFileName)得到优化后的 CSS,并把 sourcemap 以 base64 内联注释的形式追加到 CSS 末尾(addSourcemapInfoToOptimizedCss,见 broccoli-plugin.ts)——这就是 CHANGELOG 1.4.0 "End-to-end sourcemaps for ember v2 pipeline" 的实现落点; - 写出优化 CSS、优化日志(
<cssFileName>.optimization.log)以及优化器生成的类名清单(app/styles/css-blocks-stylelist.json),后者用于后处理阶段的冲突检测; - 用
RuntimeDataGenerator生成运行时数据appName/services/-css-blocks-data.js;非生产构建额外生成测试支持数据appName/services/-css-blocks-test-support-data.js(见 broccoli-plugin.ts)。
运行时数据的结构由 AggregateRewriteData.ts 定义,包含blockIds、blocks、outputClassnames、styleRequirements、impliedStyles、optimizations等字段(空的初始数据结构可见 broccoli-plugin.ts)。生成逻辑在 RuntimeDataGenerator.ts 中:为每个 block、style 分配全局索引,把优化后的类名映射到outputClassnames数组下标,把样式需求(styleRequirements)与隐含样式(impliedStyles)编码成可供运行时求值的表达式——CHANGELOG 1.3.0 中"Emit attribute groups in the runtime aggregate rewrite data"与"Simplify rewrite for dynamic attribute values"两项 feature 正是在这里落地:属性组(attribute groups)被写入聚合运行时数据,动态属性值的重写被简化为基于 styleId 的索引求值。
4.4 CSS 后处理:冲突检测与拼接
postprocessTree("css")阶段(见 src/index.ts)做两件事:
- 类名冲突检测(仅优化开启时):
CSSBlocksStylesPostprocessorPlugin(见 broccoli-plugin.ts)读取后处理树中的assets/css-blocks-stylelist.json(优化器生成的类名),再用 postcss 解析应用 CSS 中所有.class选择器,找出同时出现在应用 CSS 与优化器输出中的类名。若存在冲突,构建直接抛错,错误信息会列出冲突类名及文件与行列位置——这正是 CHANGELOG 1.5.0 "Add file+loc to class name conflict error" 与 "Class name collision detection" 两项功能的实现;同时会生成assets/app-classes.log供排查。错误信息明确建议:把非 block CSS 中的短类名(约 5 个字符)加入css-blocks.appClasses。 - broccoli-concat 拼接:把
assets/css-blocks.css与assets/<modulePrefix>.css拼成最终产物,然后从树中剔除中间文件(css-blocks.css与优化类名列表 JSON,见 src/index.ts)——对应 CHANGELOG 1.5.0 的 "Prune css-blocks.css from the output after concatenating it"。
4.5 端到端 sourcemap
CHANGELOG 1.4.0 的 "End-to-end sourcemaps for ember v2 pipeline" 意味着 sourcemap 贯穿三个阶段:优化器输出内联 sourcemap(addSourcemapInfoToOptimizedCss)、broccoli-concat 强制开启sourceMapConfig.enabled且修正extensions: ["css"]、mapCommentType: "block"(见 src/index.ts)。这样最终拼接产物能一路映射回原始 block 文件。
五、运行时服务:类名如何落到元素上
运行时数据由应用命名空间下的css-blocksservice 消费,实现在 runtime/app/services/css-blocks.ts 中。CSSBlocksService.classNamesFor()是核心入口,它的求值分三步:
- 直接应用样式:用
StyleEvaluator根据模板重写时传入的参数(styleId、状态、属性值等)计算出直接应用的 styleId 集合; - 隐含样式解析:用
StyleResolver根据styleRequirements补全必须同时应用的样式,并产出隐含类名——这是 CSS Blocks 样式需求(style requirements)与隐含样式(implied styles)的运行时实现; - 优化样式应用:遍历
optimizations数组,用 AND/OR/NOT 布尔表达式(evaluateExpression,见 css-blocks.ts)判断当前 style 集合是否命中某项优化,命中则追加outputClassnames[idx]。
调试支持:service 上有enableDebugMode,开启后会在控制台打印参数、直接/隐含样式名与最终类名;debugExpression能把优化条件表达式还原成可读的 style 名。这些能力与 CHANGELOG 1.2.0 的 "Basic runtime style calculations working"、"Implied style runtime support"、"Enable optimizer and runtime rewriting of optimized styles" 等 feature 一一对应。1.3.0 的 "Extract StyleEvaluator, StyleResolver classes from runtime service" 则是把求值逻辑从 service 中拆分成了独立的类。
六、测试支持:setupCSSBlocksTest()
优化后的类名在不同构建、不同机器上都会变化(这是有意为之,防止测试里硬编码类名),因此 addon 提供了测试工具方法setupCSSBlocksTest(),与ember-qunit和ember-mocha兼容。
6.1 使用规则
- 在集成测试或验收测试中、声明任何 test 之前调用
setupCSSBlocksTest(),且必须在setupTest | setupRenderingTest | setupApplicationTest之后调用。源码中若检测不到this.owner会直接抛错:"setupCSSBlocksTest must be called after setupTest|setupRenderingTest|setupApplicationTest"(见 css-blocks-test-support.ts)。 - 该函数暴露在应用命名空间的 service 上,需要这样导入:
import { setupCSSBlocksTest } from '<appName>/services/css-blocks-test-support';- 调用后,测试中可通过
this.cssBlocks访问 css-blocks service。
6.2 核心 API
测试 service 主要暴露一个方法:this.cssBlocks.getBlock(<pathToBlock>, <blockName>)。
<pathToBlock>必须以应用名、in-repo addon 名或 in-repo engine 名开头(表示它所属的命名空间);<blockName>可选,缺省时返回该 block 文件的默认 block;- 返回一个运行时 block 引用,可通过
.style(<styleName>)查询块内样式; - 最终用原生
element.classList.contains()断言元素上是否存在该样式。
getBlock与TestBlock.style的实现细节见 css-blocks-test-support.ts:测试 service 会先从-css-blocks-test-support-data中按 module 名查到 block 的运行时 guid,再通过-css-blocks-data反查该 guid 下所有 interface 样式名;style()会校验样式名是否存在,不存在时抛出包含可用样式列表的错误。而classNamesFor()被覆写为:在真实运行时类名之前,拼接一个由"代理样式名"(getStyleNames返回的人类可读类名)构成的前缀——这样测试里断言的就是接近源码语义的可读类名。
示例(来自 README 并补充完整):
import { setupCSSBlocksTest } from 'my-very-fine-app/services/css-blocks-test-support'; module('Acceptance | css blocks test', function (hooks) { setupApplicationTest(hooks); setupCSSBlocksTest(hooks); test('visiting /', async function (assert) { await visit('/'); let defaultBlock = this.cssBlocks.getBlock("my-very-fine-app/styles/components/application", "default"); let element = find('[data-test-large-hello]'); assert.ok(element.classList.contains(defaultBlock.style(':scope[size="large]'))); }); });测试数据只在非生产构建生成(见 broccoli-plugin.ts),且只包含属于本仓库(应用及所有 in-repo addon / in-repo engine)的 block,外部依赖 block 不会暴露在测试命名空间中。
七、版本演进主线(1.2.0 → 1.5.0)
结合 CHANGELOG,可以把该 addon 的能力演进梳理为清晰的时间线:
7.1 1.2.0(2020-08-05):运行时体系奠基
这是功能最密集的版本,确立了 addon 的核心架构:
- Feature:Establish ember-app addon(建立本 addon 本体);Basic runtime helper build infrastructure and scaffolding;Basic runtime style calculations working;Implied style runtime support;Deserializing block definition files & analysis in the ember-app;Enable optimizer and runtime rewriting of optimized styles;Optimized css in ember-app build output;Centralize ember config and use it in ember & ember-app。
- Bug Fixes:涵盖声明合并(declaration merging)重写、继承与组合(inheritance/composition)bug、样式需求满足(style requirements)校验、lazy engines 的两轮 hack 修复("Egregious hack to make lazy engines work" 与 "Slightly less hacky approach to working with lazy engines")、只合并存在的
app.css、在更多作用域中查找 broccoli 树路径、复用预编译 CSS 并把编译 CSS 交给优化器、不合并输出文件("Don't merge if an output file is specified")等。
这一版本确立了"模板树编译 → CSS 树聚合 → 运行时数据 + 优化 CSS"的整体形态。
7.2 1.3.0(2020-08-11):运行时结构优化
- Feature:Emit attribute groups in the runtime aggregate rewrite data;Simplify rewrite for dynamic attribute values。
- Bug Fixes:Extract StyleEvaluator, StyleResolver classes from runtime service(把求值逻辑从 service 拆成独立类,即现 runtime/app/services/StyleEvaluator.ts 与 StyleResolver.ts);"Sometimes there's no css blocks output" 的空输出兜底。
7.3 1.4.0(2020-09-04):sourcemap 与拼接可定制
- Feature:End-to-end sourcemaps for ember v2 pipeline;Provide ability to override concat settings(即
broccoliConcat选项的来源);Update concatenation(WIP sourcemaps fix)。 - Bug Fixes:拼写修正(brocolli → broccoli)、PR 反馈跟进。
7.4 1.5.0(2020-09-16):冲突检测与产物收敛
- Feature:Scan app CSS for classes;Class name collision detection;Add file+loc to class name conflict error——即 4.4 节所述的后处理冲突检测体系。
- Bug Fixes:Pick up fix for opticss crash on unknown css declarations(跟随 opticss 修复未知 CSS 声明崩溃);Prune css-blocks.css from the output after concatenating it(拼接后清理中间产物)。
八、常见注意事项(Common Gotchas)
README 中该节目前标注为 "Nothing yet...",但结合源码与 CHANGELOG 可以总结出几条实践中确实存在的注意点:
- 必须同时安装
@css-blocks/ember:本 addon 只聚合与优化,编译靠@css-blocks/ember完成;缺失时不会生成任何 CSS 输出(见 src/index.ts)。 output与自动拼接互斥:设置output后 CSS Blocks 内容不再自动并入app.css,需自行 import。- 短类名冲突:应用普通 CSS 中约 5 字符的短类名要加入
appClasses,否则优化器可能复用它们导致构建期冲突检测报错(见 broccoli-plugin.ts)。 setupCSSBlocksTest的调用顺序:必须在setupTest|setupRenderingTest|setupApplicationTest之后调用,否则直接抛错。broccoliConcat: false的后果:拼接不会执行,必须自行把编译产物并入最终 CSS。- lazy engine:使用
@css-blocks/ember的每个 engine 都要把css-blocksservice 加入依赖。
九、小结
@css-blocks/ember-app是 CSS Blocks 在 Ember 应用侧的"最后一公里":它把@css-blocks/ember在模板树中编译出的内容收拢、优化、拼接为最终 CSS,同时生成驱动模板运行时与测试环境的类名数据。理解它的七个配置项、三个阶段(JS 树预处理 → CSS 树预处理 → CSS 树后处理)以及运行时服务的求值链路,是排障与深度定制的前提;而 1.2.0 到 1.5.0 的演进轨迹,恰好展示了这套架构从运行时奠基、结构优化到冲突检测与产物收敛的完整迭代路径。
- 前端
- 构建工具
【免费下载链接】css-blocks
High performance, maintainable stylesheets.
相关推荐
@css-blocks/ember 版本演进与实现剖析:Ember 构建管线中的 CSS Blocks 集成
@css blocks/ember 版本演进与实现剖析:Ember 构建管线中的 CSS Blocks 集成 @css blocks/ember 是 CSS B
前端构建工具@css-blocks/ember 实战指南:在 Ember 应用中集成 CSS Blocks 构建管线
@css blocks/ember 实战指南:在 Ember 应用中集成 CSS Blocks 构建管线 CSS Blocks 是一套面向 Ember 应用的高
前端构建工具@css-blocks/ember-cli 演进史与实现原理:CSS Blocks 的 Ember/Glimmer 构建集成全解析
@css blocks/ember cli 演进史与实现原理:CSS Blocks 的 Ember/Glimmer 构建集成全解析 @css blocks/em
前端构建工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考