关于 propshaft
面向 Rails 8 及更高版本应用的 Propshaft 资源管理技能,适合负责静态资源配置和维护的开发者。内容涵盖资源组织、指纹处理、CSS 层以及从 Sprockets 迁移时的差异。
主要功能
- 01组织 Rails 静态资源目录
- 02说明资源指纹与路径引用
- 03使用 @layer 控制 CSS 级联
- 04配置样式表加载与资源预编译
- 05指导从 Sprockets 迁移
适用场景
- 01在 Rails 8 应用中配置资源
- 02排查资源路径或指纹问题
- 03设置 CDN 或生产环境预编译
- 04将应用从 Sprockets 迁移到 Propshaft
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ thinkoodle/ rails-skills --skill propshaft安装后在 Claude Code 中输入/propshaft确认已加载。
附带文件reference.md
详细介绍
Rails Propshaft 资产管道专家
使用 Propshaft 管理 Rails 8+ 应用中的资产——这是一种现代、精简的资产管道,可直接提供文件,无需编译或打包。
何时使用此技能
- 在 Rails 8+ 应用中设置或整理资产
- 从 Sprockets 迁移到 Propshaft
- 修复损坏的资产路径、缺失的样式表或指纹问题
- 配置 CDN、导入映射或资产预编译
- 按照 Propshaft 约定整理 CSS/JS 文件结构
关键思维模型
Propshaft 不是 Sprockets。 立即停止套用 Sprockets 模式:
| Sprockets(旧版 — 切勿使用) | Propshaft(Rails 8 默认) |
|---|---|
//= require 指令 | 不需要 — 所有文件都会自动提供 |
//= require_tree . | 不需要 — 目录会自动包含 |
CSS 中的 asset_path("image.png") | CSS 中的 url("/image.png") |
Sass 辅助函数 image-url("bg.png") | 纯 CSS url("/bg.png") |
manifest.js 文件 | 不需要 — 无清单文件 |
| Sass/SCSS 编译 | 纯 CSS(或使用 cssbundling-rails) |
| 资产编译步骤 | 无需编译 — 文件按原样提供 |
config.assets.compile = true | 不适用 |
Agent 最常犯的错误: 使用 //= require 或 Sprockets 时代的辅助函数。Propshaft 会完全忽略这些内容,它们会以纯文本形式出现在 CSS/JS 中。
理念
- 直接提供文件 — 无需编译、打包或转换
- 指纹处理是核心任务 — Propshaft 为文件生成摘要以实现缓存失效
- CSS
@layer控制层叠 — 不依赖文件加载顺序或清单声明 - 仅使用适合浏览器的资产 — Propshaft 需要浏览器可直接使用的 CSS/JS
- 简单优先于强大 — 需要打包/转译?添加 jsbundling-rails 或 cssbundling-rails
文件组织
标准目录结构
app/assets/
├── stylesheets/
│ ├── application.css # Main entry — may just declare @layer order
│ ├── _global.css # Design tokens (CSS custom properties)
│ ├── reset.css # CSS reset
│ ├── base.css # Base element styles
│ ├── utilities.css # Utility classes
│ └── components/
│ ├── buttons.css
│ ├── cards.css
│ ├── forms.css
│ ├── alerts.css
│ └── navigation.css
├── images/
│ ├── logo.svg
│ └── icons/
│ ├── arrow.svg
│ └── check.svg
└── fonts/
├── inter-regular.woff2
└── inter-bold.woff2
关键规则
app/assets/stylesheets/中的每个.css文件都可以提供- 下划线前缀(
_global.css)仅是约定 — Propshaft 不会特殊处理 - 没有清单文件 — 不同于 Sprockets,无需
manifest.js或带有//= require的application.css - 子目录可正常使用 —
components/cards.css会以components/cards.css提供
使用 @layer 组织 CSS
声明层顺序(关键)
在 application.css 或 _global.css 中声明层叠顺序:
/* app/assets/stylesheets/application.css */
@layer reset, base, components, utilities;
这一行即可控制所有层叠优先级。列表中越靠后的层,优先级越高。
将每个文件包装在对应层中
/* app/assets/stylesheets/reset.css */
@layer reset {
*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; }
}
/* app/assets/stylesheets/components/cards.css */
@layer components {
.card {
padding: var(--space-4);
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
}
}
/* app/assets/stylesheets/utilities.css */
@layer utilities {
.hidden { display: none !important; }
.text-center { text-align: center; }
}
@layer 的重要性
没有 @layer 时,CSS 优先级取决于文件加载顺序,而 Propshaft 不保证该顺序。使用 @layer 后,无论哪个文件先加载,声明的顺序始终生效:
reset— 优先级最低(CSS 重置、box-sizing)base— 元素默认样式(body、标题、链接)components— UI 组件(卡片、按钮、表单)utilities— 优先级最高(可覆盖任何内容)
不属于任何 @layer 的样式具有最高优先级 — 它们会覆盖所有层。请有意地使用这一点。
使用 CSS 自定义属性定义设计令牌
/* app/assets/stylesheets/_global.css */
:root {
/* Colors */
--color-primary: #2563eb;
--color-surface: #ffffff;
--color-border: #e5e7eb;
--color-text: #111827;
--color-text-muted: #6b7280;
/* Spacing */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-4: 1rem;
--space-6: 1.5rem;
--space-8: 2rem;
/* Typography */
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.125rem;
--font-medium: 500;
--font-bold: 700;
/* Borders */
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
}
使用 light-dark() 实现暗色模式:
:root {
color-scheme: light dark;
--color-surface: light-dark(#ffffff, #1f2937);
--color-text: light-dark(#111827, #f9fafb);
}
在布局中加载样式表
:app 符号(推荐)
<%# Loads ALL stylesheets from app/assets/stylesheets/ %>
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
加载指定文件
<%# Load third-party CSS first, then app CSS %>
<%= stylesheet_link_tag "actiontext", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
切勿使用 :all
<%# BAD — loads engine CSS too (e.g., Bulma from mission_control-jobs) %>
<%= stylesheet_link_tag :all %>
<%# GOOD — only your app's stylesheets %>
<%= stylesheet_link_tag :app %>
显式指定文件顺序(必要时)
<%= stylesheet_link_tag "reset", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag "base", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag "main", "data-turbo-track": "reload" %>
资产引用
在视图中(ERB)
<%# Images %>
<%= image_tag "logo.svg", alt: "Company Logo" %>
<%= image_tag "icons/arrow.svg", alt: "Arrow", class: "icon" %>
<%# Stylesheets %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%# Favicon %>
<%= favicon_link_tag "favicon.ico" %>
<%# Generic asset path %>
<%= asset_path("document.pdf") %>
在 CSS 中
/* Use root-relative paths — Propshaft rewrites these to digested URLs */
.hero {
background-image: url("/bg/pattern.svg");
}
.icon-check {
background-image: url("/icons/check.svg");
}
@font-face {
font-family: "Inter";
src: url("/fonts/inter-regular.woff2") format("woff2");
font-weight: 400;
}
Propshaft 会在预编译期间自动将 url("/bg/pattern.svg") 重写为 url("/assets/bg/pattern-abc123.svg")。
在 JavaScript 中
使用 RAILS_ASSET_URL 宏:
// Propshaft transforms this during precompilation
const trashIcon = RAILS_ASSET_URL("/icons/trash.svg");
// Becomes: "/assets/icons/trash-54g9cbef.svg"
使用导入映射处理 JavaScript
Propshaft 负责 CSS/图像/字体。JavaScript 则由 importmap-rails 管理(Rails 8 默认):
app/javascript/
├── application.js # Entry point
└── controllers/ # Stimulus controllers
├── hello_controller.js
└── modal_controller.js
<%# In layout — this is importmap-rails, not Propshaft %>
<%= javascript_importmap_tags %>
# config/importmap.rb
pin "application"
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "stimulus.min.js"
pin_all_from "app/javascript/controllers", under: "controllers"
关键区别: Propshaft 为 JS 文件生成指纹。导入映射将模块名称解析为 URL。两者相辅相成 — Propshaft 不会打包或转换 JS。
图像和字体
图像
放置在 app/assets/images/ 中。使用辅助方法引用:
<%= image_tag "logo.svg", alt: "Logo", width: 200 %>
<%= image_tag "icons/edit.svg", class: "icon", alt: "" %>
在 CSS 中:
.logo { background-image: url("/logo.svg"); }
字体
放置在 app/assets/fonts/ 中。使用 @font-face 声明:
@font-face {
font-family: "Inter";
font-weight: 400;
font-style: normal;
font-display: swap;
src: url("/fonts/inter-regular.woff2") format("woff2");
}
@font-face {
font-family: "Inter";
font-weight: 700;
font-style: normal;
font-display: swap;
src: url("/fonts/inter-bold.woff2") format("woff2");
}
body {
font-family: "Inter", system-ui, sans-serif;
}
部署与预编译
预编译命令
RAILS_ENV=production rails assets:precompile
或者不使用真实密钥:
RAILS_ENV=production SECRET_KEY_BASE_DUMMY=1 rails assets:precompile
此操作会:
- 将加载路径中的所有资源复制到
public/assets/ - 为文件名添加指纹(例如,
application-a1b2c3.css) - 将 CSS 中的
url()引用重写为带摘要的路径 - 生成
.manifest.json,将原始文件名映射到带摘要的文件名
CDN 配置
# config/environments/production.rb
config.asset_host = ENV.fetch("CDN_HOST", nil)
# e.g., CDN_HOST=https://cdn.example.com
缓存标头(Web 服务器)
Nginx:
location ~ ^/assets/ {
expires 1y;
add_header Cache-Control public;
add_header ETag "";
}
Apache:
<Location /assets/>
Header unset ETag
FileETag None
ExpiresActive On
ExpiresDefault "access plus 1 year"
</Location>
生产环境 Cache-Control
# config/environments/production.rb
config.public_file_server.headers = {
"Cache-Control" => "public, max-age=31536000"
}
添加新样式表
-
创建文件:
app/assets/stylesheets/components/modal.css -
使用适当的
@layer包裹:@layer components { .modal { /* ... */ } .modal-overlay { /* ... */ } } -
完成。 Propshaft 会自动包含该文件。无需更新清单,也无需添加 require 指令。
第三方 / 引擎 CSS
对于提供 CSS 的 gem(例如 Action Text、Trix):
<%# Load third-party BEFORE :app so your styles can override %>
<%= stylesheet_link_tag "actiontext", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
其他资源路径
# config/initializers/assets.rb
Rails.application.config.assets.paths << Rails.root.join("vendor/assets/stylesheets")
Rails.application.config.assets.paths << Emoji.images_path
排除不进行摘要处理的路径
# config/initializers/assets.rb
# Useful when using cssbundling-rails with Sass source files
config.assets.excluded_paths = [Rails.root.join("app/assets/stylesheets")]
调试
# List all available assets
bin/rails assets:reveal
# In Rails console
Rails.application.assets.resolver.logical_paths.to_a
# Clear precompiled assets (fix stale development assets)
bin/rails assets:clobber
常见开发问题: 如果资源停止更新,可能是你在开发环境中运行了 assets:precompile。public/assets/ 中的 .manifest.json 会指示 Rails 使用预编译文件。运行 rails assets:clobber 即可修复。
常见错误
- 使用
//= require— Propshaft 会完全忽略 Sprockets 指令 - 使用
image-url()或asset-url()— 这些是 Sass/Sprockets 辅助方法。请使用普通的url("/path") - 使用
:all而不是:app—:all会加载你不需要的引擎 CSS - 缺少
@layer声明 — 没有层时,层叠顺序取决于不可预测的文件加载顺序 - 期待 Sass 编译 — Propshaft 提供的是纯 CSS。要使用 Sass,请使用 cssbundling-rails 或 dartsass-rails
- 在开发环境中预编译 — 这会创建
.manifest.json并冻结资源。运行assets:clobber - CSS 中的 URL 格式错误 — 使用
url("/icons/check.svg"),而不是url("icons/check.svg")(相对于根目录) - 混淆 Propshaft 和 importmap-rails — Propshaft = CSS/图像/字体。Import maps = JavaScript 模块
从 Sprockets 迁移
完整的迁移检查清单请参见此技能目录中的 reference.md。
快速摘要:
bundle remove sprockets sprockets-rails sass-rails- 删除
config/assets.rb和assets/config/manifest.js bundle add propshaft(或升级到包含 Propshaft 的 Rails 8)- 将
//= require替换为空 — 文件会自动加载 - 将
image-url()/asset-url()替换为url("/path") - 将 CSS 中的
asset_path()替换为url("/path") - 添加
@layer声明来控制层叠顺序 - 将 Sass 转换为纯 CSS(或添加 dartsass-rails/cssbundling-rails)