레일즈에서 제공하는 모듈 generator로써 기본 어플리케이션 안에 서브 어플리케이션을 둘 수 있게 하는 기술입니다.
프로젝트의 일부 기능을 따로 떼내어 모듈화 혹은 서브 어플리케이션 사용하고자 할 때 주로 사용합니다. devise, activeadmin을 비롯한 다양한 gem들을 이 rails engine으로 구현합니다.
가장 큰 장점은 각각의 어플리케이션을 모듈로 관리할 수 있다는 점입니다. 모듈별로 각각 controller, model, views, routing 등 독립적으로 관리할 수 있습니다.
rails plugin new engine_name --mountable
/content/content.gemspec 안의 TODO를 아래와 같이 변경
spec.email = ["abcgy39@naver.com"]
spec.homepage = "https://github.com/Guk0"
spec.summary = "test for rails engine"
spec.description = "test for rails engine"plugin들을 apps 로 옮기고 아래와 같이 젬파일에서 경로 설정
# root gemfile
gem 'content', path: 'apps/content'subdomain 설정
반드시 root "home#index" 위에 선언해야함
# root routes
constraints :subdomain => 'content' do
mount Content::Engine, :at => '/'
end
root "home#index"partial render 할때는 앞에 모듈 이름을 붙여준다
# content 모듈에서 가져올때
<%= render "content/shared/header" %>
# root 모듈에서 가져올때
<%= render "shared/header" %>link_to 에서는 path 앞에 모듈이름을 붙여준다.
# 루트 어플리케이션의 패스 가져올때
main_app.destroy_user_session_path
# content 모듈에서 패스 가져올때
content.destroy_user_session_path
# 같은 모듈 안에서는 앞에 namespace를 생략할 수 있다.
comments_pathhttps://tanzu.vmware.com/content/blog/leave-your-migrations-in-your-rails-engines
해당 모듈 디렉토리에서 rails g model comment 하면 마이그레이션 파일이 해당 디렉토리의 db에서 생성된다. 마이그레이션은 각각의 모듈에서 관리하는게 좋을 것 같고 아래와 같이 각 엔진 설정 파일에서 root app의 Migrate 폴더 설정에 아래와 같이 패스를 추가해준다.
# apps/content/lib/engine.rb
...
initializer :append_migrations do |app|
unless app.root.to_s.match root.to_s
config.paths["db/migrate"].expanded.each do |expanded_path|
app.config.paths["db/migrate"] << expanded_path
end
end
end
...모델 generate시 migration 파일의 table 명은 content_comments 가 된다
What this isolation of the namespace means is that a model generated by a call to bin/rails generate model, such as bin/rails generate model article, won't be called Article, but instead be namespaced and called Blorgh::Article. In addition, the table for the model is namespaced, becoming blorgh_articles, rather than simply articles. Similar to the model namespacing, a controller called ArticlesController becomes Blorgh::ArticlesController and the views for that controller will not be at app/views/articles, but app/views/blorgh/articles instead. Mailers, jobs and helpers are namespaced as well.
하지만 table 명만 그렇게 생성되는거고 모델이나 뭐 그런것들은 앞에 namespace가 안붙고 생성되는 거라 크게 상관은 없을 듯 하다. migration 파일 생성할 때만 주의하면 될 것 같다.
add_column, remove_column, change_column 등에서 테이블명은 content_items와 같이 앞에 네임스페이스를 같이 적어준다,
irb> Content::Comment.find(1)
=> #<Content::Comment id: 1 ...>앞에 모듈명을 꼭 적어줘야 한다.
Content::Comment.create(user: User.first, post_id: 1, title: 1, content: 1)main_app에서 파생되는 형태이면 sub_app의 layout을 main_app의 layout을 사용하는 것도 좋은 방법이다.(script, link 태그 중복 방지)
<!DOCTYPE html>
<html>
<head>
<title>Content</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "application", media: "all" %>
<%= stylesheet_link_tag "content/application", media: "all" %>
</head>
<body>
<%= yield %>
<%# javascript_include_tag('application') %>
<%= javascript_include_tag('content/application') %>
</body>
</html>request_varient를 사용하는 경우 +web 같이 붙어있는 부분을 찾질 못함. 추가적인 content/application_controller.rb 컨트롤러 설정 필요
webpacker 인 경우
https://gist.github.com/pioz/805d0887ebfdde6322c4db4b46b0120f
sprockets 인 경우
# application.html.erb
<%= stylesheet_link_tag "application", media: "all" %>
<%= stylesheet_link_tag "content/application", media: "all" %>
...
<%= javascript_include_tag('application') %>
<%= javascript_include_tag('content/application') %># main_app's menifest.js
# content 모듈의 menifest를 가져옴
//= link content_manifest# content menifest에 아래와 같이 정의
//= link_directory ../stylesheets/content .css
//= link_directory ../javascripts/content .js
//= link content/application.css# application.scss
*= require_tree .
*= require_self
*= require content/comments
*= require content/itemsapplication.js
//= require content/itemsmodule Content
class ApplicationController < ::ActionController::Base
layout 'application'
def current_content_user
if params[:user_id].blank?
User.find(current_user.id)
else
User.find(params[:user_id])
end
end
end
end- ApplicationController를 완전히 분리하여도 된다. 다만 기존에 ApplicationController에 많은 variable이나 method가 들어가고 해당 요소들을 Engine에서도 사용할 것이라면 root 어플리케이션의 ApplicationController를 상속 받아 해당 Engine에서 필요한 설정들만 추가한다.
- views의 layout도 root application의 layout을 그대로 사용한다.
https://github.com/heartcombo/devise/wiki/How-To:-Use-devise-inside-a-mountable-engine#warning
devise는 engine 들에 여러번 인스톨이 불가능함. 또 하나의 모듈에서 정의한 뒤 다른 모듈에서 이를 가져다 쓰는 것이 바람직한 구현 방법일 것 같음.
하지만 각 엔진에서 User모델안에 관계가 추가될 때가 조금 애매함. 만약 엔진 내에서 종속을 갖는 관계를 루트 엔진의 User에 정의하는 것은 종속성을 온전히 나타내지 못하기 때문에 분리하는 것이 좋을 것으로 생각됨.
루트에 devise 인스톨 후 각 엔진에서 User에 대한 관계를 나누고 싶을 때
# content/user.rb
module Content
class User < ::User
has_many :items
end
end
# content/application_controller
...
def current_content_user
if params[:user_id].blank?
User.find(current_user.id)
else
User.find(params[:user_id])
end
end
...- current_user 같은 경우 다시 정의해서 써여한다. devise가 root에 정의되어 있고 current_user는 루트의 application controller에 정의되기 때문에 current_user는 루트의 User를 가져온다.
- root 엔진의 User는 root 어플리케이션의 관계들만 가져오기 때문에 각 엔진에서 정의한 관계는 가져오지 못한다. 따라서 각 엔진에 정의된 User를 사용하기 위해 current_user를 재정의 해야한다.
- current_user를 override하는 방법도 찾아보고 있긴한데 일단은 위와 같이 current_content_user를 정의하여 사용한다.
- model 명 앞에 engine 명 + / 을 달아준다.
content/items:
title: 상품유형
description: 설명
created_at: 생성일
price: 가격
status: 상태
- 위와 같이 적용 시 아래와 같이 label_tag에서 로케일을 반환한다.
f.label :title- I18n 클래스를 이용하여 직접 접근하고 싶을 때는 아래와 같이 모델명 앞에 content/ 을 붙인다.
I18n.t("enum.content/items.status.#{params[:status]}")- nested_attributes 사용시 foreign_key를 인식하지 못하는 문제가 발생한다.
- User - Item 관계에서 Item의 foreign_key를 user_id로 인식하기 때문
- foreign_key: content_user_id 로 관계 제대로 명시
<%= render "#{request.subdomain}/shared/header" %>- subdomain 명을 engine명과 동일하게 설정하면 위와 같은 처리도 가능하다.
- root application은 보통 서브도메인에 두진 않으니 request.subdomain은 ""이다.
has_many :items, foreign_key: "content_user_id"has_many에 있는 관계의 인스턴스들을 가져오려면 반드시 foreign_key를 설정해줘야 함.
params[:target_type].constantize 으로 하면 Content 모듈이 아닌 root 모듈의 모델을 가져옴.
아래 와 같이 const_get으로 사용
Content.const_get(params[:target_type])모델 name을 얻고 싶을 때 일반적으로 target.class.name 를 많이 사용하지만 engine 내에서는 target.class.name을 사용하면 앞에 네임스페이스가 붙게됨(모듈명)
그래서 아래와 같이 model_name.human 사용
target.model_name.humantarget_type을 모듈명과 같이 넣어줘야함.
target_type: "Content::Item"