Rails + ES6 + Angular2 前后端分离开发(二)

742 阅读3分钟
原文链接: www.sdk.cn

摘要:本篇文章要讲的主要内容是如何让我们可以使用 capistrano 和 rails assets pipeline 部署 Rails + Angular2 项目。

本篇文章要讲的主要内容是如何让我们可以使用 capistrano 和 rails assets pipeline 部署 Rails + Angular2 项目。

在这之前我们先来了解几个概念:

1. Angular2 组件中使用 templateUrl

我们现在把 hello_world.js 重构成下面这样:

app/assets/angular2/components/hello_world.js

import {Component} from 'angular2/core';

@Component({
	selector: 'helloWorld',
	templateUrl: 'assets/components/hello_world.html',
	styleUrls: ['assets/components/hello_world.css']
})

export class HelloWorldComponent {
	constructor() {
	}
}

app/assets/angular2/components/hello_world.html

Hello World with templateUrl !

app/assets/angular2/components/hello_world.css

h1 {
  color: red;
}

刷新页面我们看到的应该是这样的内容:

2. 关于 Assets Pipeline 的 Fingerprinting 技术及其用途

简单来说 Fingerprinting 是一项根据文件内容来生成文件名的技术。当文件内容发生改变时文件名也会随之改变。

那它到底有什么用处呢? 当文件名是唯一并且依赖于文件内容时,我们可以通过设置HTTP headers 将文件保存到 CDNs,ISPs, 网络设备 或者 浏览器进行缓存。如果下次部署的时候某个文件内容发生了改变,那它对应的 fingerprint 也会改变,这样客户端就会重新请求该文件,这个过程通常又叫做:cache busting

Rails Assets Pipeline insert a hash of the content into the name, usually at the end.

例如我们有一个 css 文件叫 application.css

application-908e25f4bf641868d8683022a5b62f54.css
注:具体请参考 Assets Pipeline Fingerprinting

3. 如何利用Assets Pipeline 的 Fingerprinting 解决 Angular2 中 templateUrl 和 styleUrls 的缓存问题。

我们现在要考虑的问题有两个:

  1. 开发环境下 template(html) 和 style(css)文件是不需要通过 Assets Pipeline 进行编译的,因为会影响开发速度,项目很大的时候对通过 Assets Pipeline 编译太多文件会明显影响速冻。
  2. 生产环境下我们需要将上面代码中的 templateUrl 和 styleUrls 替换成 Assets Pipeline 添加过 Fingerprinting 的文件名。

配置 Assets Pipeline precompile Angular2 组件内所有的 html 和 css 文件

将 config/initializers/assets.rb 改成下面这样:

Rails.application.config.assets.version = '1.0'
Rails.application.config.assets.paths << Rails.root.join("app", "assets", "angular2")

# make assets pipeline precompile all html(template) and css(style) file for angular2 components
Rails.application.config.assets.precompile << Proc.new { |path|
  if path =~ /\.(css|html)\z/
    full_path = Rails.application.assets.resolve(path)
    app_assets_path = Rails.root.join('app', 'assets').to_path

    full_path.starts_with?(app_assets_path + '/angular2/components')
  else
    false
  end
}

执行 precompile:

✗ bundle exec rake assets:precompile

在文件 app/view/home/index.html.erb 末尾添加下面这行代码:

 assets_manifest.assets %>

重启 Rails Server 刷新页面你会看到页面上多出的内容是一个 Hash :

{
  "components/hello_world.css" => 
  "components/hello_world-7c13caf4ede58c40f375232f9e1aa57f44370d81e7f5f302968ee98139437581.css",

  "components/hello_world.html" => 
  "components/hello_world-2fc4fdd447b9722d97e198f16056e5bf4f01705d453e9f9ea69e6bf759842855.html",

  "application.js" => 
  "application-2f4c216d83942d7817a6a5510231868e91eab74e7ab25c651dae90f27abcc647.js",

  "application.css" => 
  "application-f1a3ad384315c3290f52141a03b396b98a93f743dbc123decff7be0c6785679d.css"
}

从上面 Hash 的内容可以看出 assets_manifest.assets 返回的是 assets precompile 前后 文件的对应关系。

我们可以通过在 config/application.rb 中配置 config.assets.manifest 的方式来指定 Assets precompile 上面所述文件对应关系的存储路径:

config.assets.manifest = "#{Rails.root}/config/manifest.json"

执行:

✗ bundle exec rake assets:clean
✗ bundle exec rake assets:precompile
✗ more config/manifest.json

查看 config/manifest.json 的内容会发现里面存储的就是上文所属的precompile 前后文件名的对应关系。

所以 assets_manifest.assets 刚好就是我们所需要的。

自定义:Helper method embed_js

app/helpers/application_helper.rb

  def embed_js
    data = {
      env: Rails.env, 
      assetsManifest: assets_manifest.assets,
      assetHost: Rails.application.config.action_controller.asset_host
    }

    
        window.Rails =  #{data.to_json}

        window._assetPath = function(path) {
          if (window.Rails.env == 'development') {
            return "/assets/" + path
          } else {
            path = "/assets/" + Rails.assetsManifest[path]
            if (window.Rails.assetHost) {
              return window.Rails.assetHost + path
            } else {
              return path
            }
          }
        }

        window._jsUrl = function(js) {
          return window._assetPath(js+'.js')
        }
        window._templateUrl = function(html) {
          return window._assetPath('components/' + html+'.html')
        }
        window._styleUrl = function(css) {
          return window._assetPath(css+'.css')
        }
      
    EOS
  end

然后打开 app/views/layouts/application.html.erb 改成如下代码:



  
    DevOps
 csrf_meta_tags %>
 action_cable_meta_tag %>

 embed_js %>

 stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track' => 'reload' %>
 javascript_include_tag 'application', 'data-turbolinks-track' => 'reload' %>
  

  
 yield %>
  

也就是我们在 application.html.erb 里面加了一行代码: <%= embed_js="" %="">

上面的代码中 embed_js 中我们通过 Ruby 代码成了一段 JS 代码,然后将 JS 代码加载到了页面的 Header 中。

这段 JS 代码中定义了 一个全局变量 Rails ,Rails 的值是一个 Object,三个 key 分别是当前 Rails 的环境,assetsManifest, 以及 assetsHost。

然后定义了四个全局函数,其中 window._assetPath 中先判断当前环境是否为 development, 如果是,则直接返回想对应的 js/html/css 文件,若果不是 development 环境,则 通过 assetsManifest 中的对应关系找出 precompile 之后的文件路径,加载到页面中。

有了这四个函数,我们就可以重构上面的 Hello World 了。

import {Component} from 'angular2/core';

@Component({
	selector: 'helloWorld',
	templateUrl: _templateUrl('hello_world'),
	styleUrls: [_styleUrl('hello_world')]
})

export class HelloWorldComponent {
	constructor() {
	}
}

这样在开发环境下 Angular2 组件直接加载 template 和 style 文件,而通过 capistrano 发布项目时只要执行 Assets Precompile 就会生成 manifest,从而使我们的组件能就加载带 fingerprint 的 html, css 和 js。

至此为止,我们开发和部署 Rails + Angular2 项目的所有配置就完成了。