propshaft 是在 Rails 7 发布后,用于取代之前的 sprockets 而新创建的一个用于管理静态资源 (CSS / JS / Images)。相比 sprockets 的丰富功能,propshaft 显得十分轻量极,代码量也比 sprockets 少很多。这和 Rails 7 推荐的前端管理方案是一致的。

在这篇文章里简要的分析一下 propshaft 库里的代码。

首先页面中使用了 Rails 提供的方法

<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>

生成的 HTML 标签如下:

<link rel="stylesheet" href="/assets/application-f00b069f5cd8e28f9b699ca70bb2b5cdb20b8698.css" data-turbo-track="reload" />
<script src="/assets/application-6ebdcbdb87df2810d072c43df65ad72771c6133b.js" data-turbo-track="reload" defer="defer"></script>

那么这样的文件 URL 路径是怎么构建出来的呢?Propshaft 中的 helper.rb 中的方法覆盖了 Rails 中的方法

def compute_asset_path(path, options = {})
  Rails.application.assets.resolver.resolve(path) || raise(MissingAssetError.new(path))
end

此方法将对文件名进下如下的转换:

application.css -> /assets/application-f00b069f5cd8e28f9b699ca70bb2b5cdb20b8698.css
logo.png -> /assets/logo-5ac8bf8b1c5f89160e89e9c9b5e4e76824de113e.png

Rails 中的相关代码如下:

# Maps asset types to public directory.
ASSET_PUBLIC_DIRECTORIES = {
	audio:      "/audios",
	font:       "/fonts",
	image:      "/images",
	javascript: "/javascripts",
	stylesheet: "/stylesheets",
	video:      "/videos"
}

# Computes asset path to public directory. Plugins and
# extensions can override this method to point to custom assets
# or generate digested paths or query strings.
def compute_asset_path(source, options = {})
	dir = ASSET_PUBLIC_DIRECTORIES[options[:type]] || ""
	File.join(dir, source)
end
alias :public_compute_asset_path :compute_asset_path

Rails 中生成的 HTML 代码如下,基本上没有什么用,一般都是通过插件重写此方法。

<link rel="stylesheet" href="/stylesheets/application.css" data-turbo-track="reload" />
<script src="/javascripts/application.js" data-turbo-track="reload" defer="defer"></script>

好了,前面解释明白了相关的 assert helper 用法,以及它们生成的 HTML 代码。那 Rails 框架实际上是怎么响应这些 assets 的请求呢?

在 propshaft/railtie.rb 中看到 /assets/ 前缀的 URL 请求都由 propshaft/server.rb 处理,它是一个简单的 rack middleware

# lib/propshaft/railtie.rb
config.after_initialize do |app|
	config.assets.server = Rails.env.development? || Rails.env.test?
	app.assets = Propshaft::Assembly.new(app.config.assets)
	if config.assets.server
		app.routes.prepend do
			mount app.assets.server => app.assets.config.prefix
		end
	end
end

rack middleware 在 Propshaft::Assembly 中创建,它指向 Propshaft::Server

# lib/propshaft/assembly.rb
class Propshaft::Assembly
	def server
    Propshaft::Server.new(self)
  end
end

检查是否存在 public/assets/.manifest.json Yes: Propshaft::Resolver::Static No: Propshaft::Resolver::Dynamic