Dev Talks

用 Rails Engine 將功能分享給其他應用程式

Ken Chen, Rails 工程師 Nov 1, 2022

在工作上遇到好幾個專案需要共用程式碼的案例,先前的維護者解決的方式,是寫一段程式去將主要專案裡的某些檔案複製到其他專案上,這樣的作法看起來確實是一個簡單暴力快速的方法,但身為一個接手維護這幾個專案的人來說,這一年間不知道吃過多少苦頭。

反過來想,有沒有什麼替代的方案可以解決這個問題呢? 在聽過公司前輩們的建議之後,得知了 Rails Engine 這樣的東西很適合來解決這種問題,特別是在 Ruby on Rails 上。

Rails Engine 是什麼?

Rails Engine 可以看作是一個小型的 Rails 應用程式,Rails 應用程式其實也能看做是一個增強版的 Rails Engine,所以本質上可以把 Rails Engine 跟 Rails 應用程式看作是同樣的東西,所以將 Rails Engine 掛載到 Rails 應用程式上,則這個 Rails Engine 將提供它的功能給這個 Rails 應用程式(host applications)。

實作 Rails Engine

本次的示範將 Cat 這個 model 的 CRUD 從原來的 Rails app 分離至 Rails Engine,並讓 Rails app 掛載 Rails Engine 來獲得此功能。

1.假設現在專案裡有一個對貓咪(Cat) CRUD 的功能

用 rails g scaffold Cat name:string cute_level:integer 產生出 Cat 的 CRUD,這時開啟 rails server 並進入 localhost:3000/cats 頁面,就會有可以編輯 Cat 的功能

2.建立出一個名為 cat_engine 的 Rails Engine!

下指令 rails plugin new cat_engine --full ,這樣就會產生出一個可以和 host application 共用 model、controller、view 的 engine 了,若是使用 --mountable 則會產生出自己擁有獨立 namespace 的 model、controller、view ,現在我們要的情境比較偏向第一種。

3. 將 cat_engine 掛載到應用程式上!

在 host application 的 Gemfile 裡加上 gem ‘cat_engine’, path: ‘ENGINE PATH’ , path 需要設定成 engine 的所在位置(在本機的所在位置),接著在終端機執行 bundle install ,接著就會看到這樣的訊息

You have one or more invalid gemspecs that need to be fixed.
The gemspec at /mnt/c/projects/cat_engine/cat_engine.gemspec is not valid. Please fix this gemspec.
The validation error was 'metadata['homepage_uri'] has invalid link: "TODO"'

4.設定 gemspec

engine 剛新建出來的 xxx.gemspec 應該會像下面這樣

require_relative "lib/cat_engine/version"

Gem::Specification.new do |spec|
  spec.name        = "cat_engine"
  spec.version     = CatEngine::VERSION
  spec.authors     = ["kenaser12345"]
  spec.email       = ["kenaser12345@gmail.com"]
  spec.homepage    = "TODO"
  spec.summary     = "TODO: Summary of CatEngine."
  spec.description = "TODO: Description of CatEngine."
    spec.license     = "MIT"
  
  # Prevent pushing this gem to RubyGems.org. To allow pushes either set the "allowed_push_host"
  # to allow pushing to a single host or delete this section to allow pushing to any host.
  spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"

  spec.metadata["homepage_uri"] = spec.homepage
  spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here."
  spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."

  spec.files = Dir.chdir(File.expand_path(__dir__)) do
    Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"]
  end

  spec.add_dependency "rails", ">= 7.0.3"
end

把所有 TODO 都做完設定,若不知道如何設定可以參考 Devise 的 gemspec

Gem::Specification.new do |spec|
  spec.name        = "cat_engine"
  spec.version     = CatEngine::VERSION
  spec.authors     = ["kenaser12345"]
  spec.email       = ["kenaser12345@gmail.com"]
  spec.homepage    = "https://github.com/kenaser12345/cat_engine"
  spec.summary     = "Engine for creating cute cats"
  spec.description = "Engine for creating cute cats"
  spec.license     = "MIT"
  spec.metadata["homepage_uri"] = "https://github.com/kenaser12345/cat_engine"
  spec.metadata["source_code_uri"] = "https://github.com/kenaser12345/cat_engine"
  spec.metadata["changelog_uri"] = "https://github.com/kenaser12345/cat_engine/blob/main/CHANGELOG.md"

  spec.files = Dir.chdir(File.expand_path(__dir__)) do
    Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md", "CHANGELOG.md"]
  end

  spec.add_dependency "rails", ">= 7.0.3"
end

設定完之後到 host application 再次執行 bundle install ,就會成功安裝上去。

5.將 Cat CRUD 功能轉移到 cat_engine 上

將第一步用 scaffold 建立的 Cat CRUD 功能(Model、Controller、View、Route)都移到 cat_engine 裡面對應的位置,先啟動 rails server 確認功能已經移除之後,host application 再次執行 bundle install 更新 cat_engine,啟動 rails server 就可以看到原本的畫面囉~

部屬應用程式

gem 'cat_engine', path: '../cat_engine'

在本地開發時主要應用程式以及 engine 都在本機,所以在 Gemfile 裡可以指定本機位置來安裝 engine,但當要部署應用程式時在 VM 裡通常來說只會有主要的應用程式,不應該再用指定本機位置的方式來安裝 engine 而是用 git 的方式從 Github repository 安裝。

這個時候就要考慮這個 repository(engine) 是要設為 public 或是 private,取決於想不想將 engine 的內容公開。

Public Repository

若是 public repository 只需要將 Gemfile 改成 gem 'cat_engine', git: 'https://github.com/你的GITHUB帳號/Repository名稱.git',在 Github 專案頁面也能找到這個 URL

Private Repository

若 engine 是 private repository 可以使用 Github access tokens 的方式來通過 HTTPS 認證。

1.到 Github access tokens 頁面製作 access token,這次的用途是為了 bundle private gem(repository) 所以 scopes 這邊只需要選 repo,也可以調整 token 的時效。

2.以 HTTPS 方式 bundle gem gem 'cat_engine', git: 'https://github.com/你的GITHUB帳號/Repository名稱.git'

在 VM 設置環境變數 BUNDLE_GITHUB__COM 成 access token 這樣便可以成功安裝 engine 了。

總結

1.這次示範的例子是用 rails plugin new ENGINE_NAME --full 產生出沒有 name space 可以直接使用在被掛載的 app 上,若不希望 engine 內容與 app 原來的功能混在一起可以使用 ``–mountable` 來做,可以參考 Rails 官方的教學

2.Gemfile 中包含 private gem 時執行 bundle install 時會需要認證不管是用 HTTP 或是 SSH 的方式,建議不要將敏感資訊放進專案中,像這樣的方式可能就會有點危險, gem 'my_private_gem', git: 'https://USERNAME:PASSWORD@github.com/你的GITHUB帳號/Repository名稱.git', 建議可以使用 access token 的方式來認證避免危險。

參考資料

Rails Engine 官方文件

stackoverflow pull from a private github repository


分享