Rails多子域名场景下Sprockets读取本地Manifest而非CDN文件的问题
config.assets.manifest配置不生效? 我来帮你拆解一下问题的核心,以及对应的解决思路:
核心原因:config.assets.manifest不支持HTTP URL,也不是请求级别的配置
你这里犯了两个关键的误解:
config.assets.manifest只接受本地文件系统路径,不支持远程URL
Sprockets的manifest机制是基于本地文件的——它需要读取本地的.sprockets-manifest-xxx.json文件来建立「原始资产路径」和「带digest的文件名」的映射关系。你把它设置成CDN的HTTP URL,Rails/Sprockets根本无法识别,直接忽略这个配置,继续使用默认的public/assets目录下的本地manifest。config.assets.manifest是全局初始化配置,不支持请求级别的动态切换
和asset_host不同,config.assets.manifest的配置是在Rails启动时就确定的,它并不接受带request参数的Proc。你写的Proc其实根本不会被调用,因为Sprockets在初始化时就会解析这个配置,而不是在每个请求到来时执行。
针对多子域名场景的解决方案
既然每个子域名对应CDN上独立的资产目录和manifest,我们需要换一种思路:在请求时动态选择对应的manifest来生成资产路径。这里有两种可行的方案:
方案一:本地维护每个子域名的manifest,请求时动态加载
适合你已经把每个子域名的manifest同步到本地的情况:
预加载所有子域名的manifest
在config/initializers/assets.rb中,提前加载每个子域名对应的本地manifest:# 假设你的子域名列表可以从配置或数据库获取 SUBDOMAIN_MANIFESTS = {} ['client_a', 'client_b'].each do |subdomain| # 找到该子域名目录下最新的manifest文件 manifest_glob = Rails.root.join("public/assets/#{subdomain}/.sprockets-manifest-*.json") latest_manifest_path = Dir[manifest_glob].max_by { |f| File.mtime(f) } if latest_manifest_path SUBDOMAIN_MANIFESTS[subdomain] = Sprockets::Manifest.new(Rails.application.assets, latest_manifest_path) end end覆盖资产路径生成的helper方法
在app/helpers/application_helper.rb中,重写asset_path方法,根据当前子域名选择对应的manifest:def asset_path(source, options = {}) # 如果当前请求有子域名,且我们预加载了对应的manifest if request.subdomain.present? && SUBDOMAIN_MANIFESTS.key?(request.subdomain) manifest = SUBDOMAIN_MANIFESTS[request.subdomain] digested_asset = manifest.lookup(source) if digested_asset # 拼接CDN路径 protocol = request.ssl? ? 'https' : 'http' "#{protocol}://cdn.domain.com/#{request.subdomain}/#{digested_asset}" else # 找不到对应资产时,回退到默认逻辑 super end else # 没有子域名时,使用默认逻辑 super end end
方案二:动态拉取CDN上的manifest(适合不想同步本地的情况)
如果不想把每个子域名的manifest同步到本地,可以直接从CDN拉取,但要注意加缓存避免性能损耗:
在app/helpers/application_helper.rb中添加:
def remote_manifest(subdomain) # 缓存manifest内容1小时,避免每次请求都去CDN拉取 Rails.cache.fetch("asset_manifest/#{subdomain}", expires_in: 1.hour) do protocol = request.ssl? ? 'https' : 'http' manifest_url = "#{protocol}://cdn.domain.com/#{subdomain}/.sprockets-manifest.json" # 这里用HTTParty举例,你也可以用Net::HTTP或其他HTTP客户端 response = HTTParty.get(manifest_url) response.success? ? JSON.parse(response.body) : nil end end def asset_path(source, options = {}) if request.subdomain.present? manifest = remote_manifest(request.subdomain) if manifest && manifest['assets'].key?(source) protocol = request.ssl? ? 'https' : 'http' "#{protocol}://cdn.domain.com/#{request.subdomain}/#{manifest['assets'][source]}" else super end else super end end
注意:要确保CDN上的
.sprockets-manifest.json可以被你的应用服务器访问到,并且做好缓存策略,避免频繁请求CDN。
内容的提问来源于stack exchange,提问作者Cameron

