[Rails] routes scope 和 concern 運用

scope

最基本產生在某個資料夾的的routes

namespace :admin do
  resources :articles
end

以index為範例$ rake routes可以看出

method verb path controller#method
admin_articles_path GET /admin/articles admin/articles#index

如果想把 /articles 路徑(path)不帶 /admin 前缀但依然要映射到 Admin::Articles 控制器上,可以用scope

scope module: 'admin' do
  resources :articles
end

得到的index:

method verb path controller#method
articles_path GET /articles admin/articles#index

還有下面幾種寫法可以得到上面相同的index結果:

resources :articles, module: 'admin'
scope '/admin' do
  resources :articles
end

也有一種只改路徑(path)的方法

resources :articles, path: '/admin/articles'
method verb path controller#method
articles_path GET /admin/articles /articles#index

concern

concern其實可以把它當作routes用的變數,宣告後可以給各個地方重複使用,簡單例子:

concern :concerntest do
    resources :articles
end
scope module: "admin" do
    concerns :concerntest
end

得到的index:

method verb path controller#method
articles_path GET /articles admin/articles#index

小總結:
如果真的看不懂或不夠資深還是善用$ rake routes去把它印出來看,再來我覺得還是不要用太多方法去寫,並不會好維護到哪裡去,以中國有嘻哈的角度看寫起來也不會特別的skr,還是第一眼讓大家看懂最重要。

[Rails] routes 高級約束 (Advanced Constraints)

constraints是routes的一個約束方法,官方中文翻譯文件是說約束,但我覺得有點像是專屬於routes的if,如果成立才可以執行,例如最簡單的兩個例子:

match 'photos', to: 'photos#show', via: [:get, :post]

只要http verb不是指定的get或post就不行了

get 'photos/:id', to: 'photos#show', constraints: { id: /[A-Z]\d{5}/ }

一個可以利用正則去驗證id的概念

再來高級約束有什麼厲害的地方?
官方提供了一個很酷的例子,擋ip

class BlacklistConstraint
  def initialize
    @ips = Blacklist.retrieve_ips
  end
 
  def matches?(request)
    @ips.include?(request.remote_ip)
  end
end
 
Rails.application.routes.draw do
  get '*path', to: 'blacklist#index',
    constraints: BlacklistConstraint.new
end

他的遊戲規則是constraints一定會去跑class的matches?得到一個bool來決定可不可以通過該routes

而這個範例利用了class.new的特性先去自動執行initialize得到一個instance value(@ips)然後再自動去執行matches?

request則是內建的物件有很多方法可以用,remote_ip則是其中一個。

而routes設定get ‘*path’(所有路徑)的時候都會觸發,所以remote_ip有包含在Blacklist.retrieve_ips裡面則routes會把你導去啟動blacklist#index黑名單的controller,十分的酷炫。

為什麼研究到這個是因為公司的例子,一個rails專案裡面放兩個產品專案,然後利用routes偵測subdomain去決定要顯示什麼頁面:

()
constraints(Subdomain::B) do
   get /welcome_b  => welcome_b#index”

end
()
module Subdomain
  class A
    def self.matches?(request)
      request.subdomain.in?  ["www", "staging", ""]
    end
  end

  class B
    def self.matches?(request)
      request.subdomain.in? [B, B-staging"]
    end
  end
end

所以直接偵測網址的subdomain裡有出現陣列字串就可以成立,那現在出現了一個問題,我要怎麼開發B網站呢?

localhost預設是一個沒有subdomain的domain,而沒有subdomain所以subdomain等於空字串是成立的,所以把A網站陣列裡的空字串改到B的陣列就可以嘍!

參考文章:
Rails Guides英文
Rails Guides中文
兩天同一篇

iTerm + oh my zsh 美化終端機

iTerm是比內建終端機還好用的終端機工具,因為可以客製化很多東西,搭配oh-my-zsh的套件能讓美觀更進一步,且可以直接套用主題~

假設已經安裝好iTerm了,我們參考oh-my-zsh的官方文件開始執行以下步驟:

首先要先看有沒有安裝好Zsh($zsh —version可以檢查)

如果沒有安裝的話就用curl安裝

$ sh -c "$(curl -fsSL https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)”

安裝完之後重新打開iTerm如果視窗是顯示bash環境請轉換成zsh
$chsh -s /bin/zsh
重開後視窗應該要顯示zsh

安裝oh-my-zsh的東西
首先下載
$ git clone git://github.com/robbyrussell/oh-my-zsh.git ~/.oh-my-zsh

我們要自己生成zsh的設定檔,位置是~/.zshrc,我們直接用官方的template比較省事
$ cp ~/.oh-my-zsh/templates/zshrc.zsh-template ~/.zshrc

我們希望用的是agnoster主題,官方有說明文件
發現要用這個主題要用Solarized顏色和Powerline-patched Meslo字型(https://github.com/powerline/fonts)

然後可以在剛剛的.zshrc檔案裡頂端加入一行
DEFAULT_USER=“xxxx”,xxxx可以輸入自己的英文名字
再來找到一行ZSH_THEME這個是設定主題的,如果被註解掉就把它解開設定主題如下
ZSH_THEME="agnoster”,agnoster就是主題名稱

每次修改完.zshrc可以輸入source ~/.zshrc刷新一下這個檔案再重開iTerm

再來可以去設定字形和顏色,打開iTerm後左上角
iTerm > Preference > Profiles
Colors > 可以設定顏色
Text > Change Font > 可以設定字型,建議設定Menlo for Powerline和14pt

應該就有漂亮的iTerm可以使用了!

從前端轉戰Rails後成為T客邦的一份子

先不討論找到T客邦工作之前的多多少少甘苦談了,這大概是我人生做過最大筆的自我投資,目前也是最棒的投資之一。

其實一開始我的環境建立是蠻不順的XD,因為電腦是沒重灌過,造成很多離職同事裝的套件一直因為權限不讓我用,例如Homebrew之類的,這些還真的是我沒想過會撞到的牆QQ。

沒想到搞了將近兩天(有半天想說讓系統更新一下結果就是一下午...)之後才真的開始玩公司裡的Rails101,熟悉的Xdite教材但其實很老舊,碰了許多小地雷。

但真正比較悲劇的是這裡的新手訓練很有趣是給你一張票,票底下有很多子票就跟著教材的每一個章節跑,但我一開始忘了了就在一個分支一直做下去囧,還好發現的時候重做的速度很快。

那時候我覺得讓我最痛苦的地方大概是很不習慣deploy方面的debug,很多error都要去特定的資料夾看,還常常有看沒有懂,比起來直接開發網站真的是蠻幸福的事情。

101終於都弄完成了之後就是無限的codereview,老實說真的蠻酷的!!我也在兩天之內瘋狂的使用rebase,感覺現在很不害怕git版本控制了呀!

前前後後搞了快半個月才走出新手村,結果我第一張票是一位前輩做完然後叫我重做當練習,雖然不難但其實不是一張算小的票!底下的子票大概有十來個,而且還牽扯到角色權限問題。

所幸的是有很多其他的專案可以參考,所以也不然是要開發什麼全新功能的感覺,但就是要看的懂code,熟悉檔案架構還有認識一堆Gem,這真的蠻瘋狂的,一瞬間太多東西了真的很容易亂掉記後忘前的,開發的思維真的跟我之前前端差蠻多的(不過專案大小也有根本的差距,前公司的專案真的很小XD),還好同事很細心的教導我。

還有不習慣的一點是後端真的要蠻細心,有時候前端硬try自然慢慢的debug就可以拿到要的效果雖然到最後真的不好維護,但在後端的世界有時候想要硬try一個東西真的不是這麼容易,還要想做假資料,先想好各種情況,在檔案架構很大的情況下真的是很怕牽一髮動全身改了A檔案到底會不會影響到BCD檔案,但漸漸的習慣了專案架構之後其實這不算太大的問題了。

再來最可怕的大概就是要了解各種gem了,T客邦很厲害的是自己包了很多gem,那對於新人來說常常一個很特別的methods我要先猜是不是從對的hleper或者model定義而來,再來猜是不是外面的gem,最後再去找是不是T客邦自己有包的gem,如果不是多解一點票可能光要找功能就會花上許久時間。

整個一個月初下來的心得,T客邦真的是一個可以學習碰到很多東西的地方,專案架構整體至少比起我碰到的東西來說是相對很大的,但老舊的東西或多或少還是有自己覺得美中不足的地方想寫得更好,但我想目前更熟悉Rails,gem和deploy上的問題才是首要,一個月下來迅速的累積了不少技術債呀!

[Rails]i18n基本使用

首先安裝rails-i18n

Gemfile
gem "rails-i18n"

$ bundle install

我們所有翻譯檔案都會在config/locals目錄下,都是為.yml的檔案,而檔名不重要,純粹是好分辨而已,這邊就創鈔兩個檔案:
en.yml代表英文翻譯
zh-TW.yml代表中文翻譯

自定義的話例如中文翻譯來說:

zh-TW:

  welcome: "嗨世界!#{username}"

  sport:

    ball: 籃球

而英文翻譯:

en:

  welcome: "Hello world! #{username}"

  sport:

    ball: basketball

使用方法如下:

example.html.erb
t("sport.ball")
t(:sport, :scope => :ball )
t(:welcome, :username => "Kurt")

在application_controller.rb設定如下:

application_controller.rb
before_action :set_locale

def set_locale
  # 可以將 ["en", "zh-TW"] 設定為 VALID_LANG 放到 config/environment.rb 
  if params[:locale] && I18n.available_locales.include?( params[:locale].to_sym )
    session[:locale] = params[:locale]
  end

  I18n.locale = session[:locale] || I18n.default_locale
end

在view中切換版本:

example.html.erb
<%= link_to "中文版", :controller => controller_name, :action => action_name, :locale => "zh-TW" %>
<%= link_to "English", :controller => controller_name, :action => action_name, :locale => "en" %>

[Rails]專案引入vue & 利用Attribute給 vue 資料

我的開發環境:
Rails 5.1以上

引入webpacker和vue


先安裝web packer

Gemfile
gem 'webpacker', '~> 3.5'

$ bundle install
$ bundle exec rails webpacker:install

可以查看webpacker帶有什麼指令:
$ rails webpacker

比起npm我比較喜歡用yarn:
$ bundle exec webpacker:yarn_install
$ yarn install

有個指令可以直接安裝vue:
$ bundle exec webpacker:install:vue

利用Attribute給 vue 資料


先在contoller設定變數:

products_controller.rb
def index
  @products = Product.all
end

再到view裡面把資料帶入DOM:

index.html.erb
<% props = {
  products: @products
  }.to_json
%>

<div id="app" data="<%= props %>"></div>

接下來到vue的檔案application:

app/javascript/packs/application.js
import Vue from 'vue'
import App from '../app.vue'

document.addEventListener('DOMContentLoaded', () => {
  const el = "#app"

  const props = JSON.parse($("#app")[0].getAttribute('data'))
  const app = new Vue({
    el: el,
    render: h => h(App, { props })
  })
})

最後到vue 的component:

app/javascript/app.vue
<script>
export default {
  props: ["products"],
  data: function () {
    return {
      message: "Hello Vue!"
    }
  },
  mounted: function () {
    console.log("products", products)
  }
}
</script>

mounted是vue生成完之後會執行的動做,可以參考vue lifecycle
如果有console出東西就ok嘍!!

這個方式的優點十分明顯,可以省去製作API和串接的麻煩,但就是比較不彈性,真正要做到完全的前後端分離建議還是分成兩個專案,Rails寫API而Vue去做前端實現。

參考文章:
Passing Props to Vue in a Rails View

[Rails]rails 搭配 vue-router(目前無法指定vue-routes)

參考文件:
webpacker

Rails版本5.1以上可以直接創造帶有webpack的新專案
$ rails new rails-vue-todolist --skip-turbolinks --skip-spring --webpack=vue
$ cd rails-vue-todolist

先快速建立todolist
$ rails g scaffold Todolist item:string
$ bundle install

設定router

routes.rb
root "todolists#index"

測試有沒有成功

app/views/layout/application.html.erb
<%= javascript_pack_tag 'hello_vue' %>

更新todolist/index

app/views/todolists/index.html.erb
<div id="hello"></div>
...

$ rails s
到首頁刷新頁面應該就會有了,如果有出現:
You may need an appropriate loader to handle this file type.
可以嘗試下下面三個指令
$ bundle update webpacker
$ yarn upgrade @rails/webpacker
$ bundle exec rails webpacker:install:vue

大多會是版本上的問題,如果直接用rails5.1以上應該沒有問題,如果是用其他版本或者是專案升級情況可能要再去查詢下官方log文件

再來如果玩過Vue.js的人有一種開發模式是把一個id="app"當作入口,所以我們會有一個主要的main.js和主要生成vue的app.vue,此時app/javascript/packs/application.js可以把它當作main.js,再來怎麼規劃檔案就都可以了。

我們循著上面的作法先改rails view剛剛的application.html.erb的tag

app/views/layout/application.html.erb
# 把原本的 <%= javascript_pack_tag 'hello_vue' %> 替換如下


<%= javascript_pack_tag 'application' %>

修改index入口點只留下一行當作vue的入口就好

app/views/todolists/index.html.erb
<div id="app"></div>

修改packs/application.js

app/javascript/packs/application.js
import Vue from 'vue'
import App from '../app.vue'

document.addEventListener('DOMContentLoaded', () => {
  const app = new Vue({
    el: "#app",
    render: h => h(App)
  })
})

現在再重新刷新一次頁面如果有render出Hello Vue!恭喜已經設定好所有一切了!

初創index


接下來我們先直接在app.vue裡面創造假資料做出index畫面:

app/javascript/app.vue
<template>
  <div>
    <h1>Todo Lists</h1>
    <table>
      <thead>
        <tr>
          <th>#</th>
          <th>Item</th>
        </tr>
      </thead>

      <tbody>
        <tr v-for="todo in list" >
          <td>{{ todo.id }}</td>
          <td>{{ todo.item }}</td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script>
export default {
  data: function () {
    return {
      list: [
        { id: 1, item: "Foo" },
        { id: 2, item: "Bar" }
      ]
    }
  }
}
</script>

應該會list出兩個item出來,但我們要去拿rails裡todolists#index的資料,首先我們在rails console新增兩筆:
$ rails c
$ Todolist.create(item: "Test1")
$ Todolist.create(item: "Test2")
$ exit

然後如何從vue的methods裡面拿取資料,可以用vue-resource這個套件。
$ yarn add vue-resource

然後import在app/javascript/application.js裡:

app/javascript/application.js
import VueResource from 'vue-resource'
Vue.use(VueResource);

現在修改app.vue

app/javascript/app.vue
<template>
  <div>
    <h1>Todo Lists</h1>
    <table>
      <thead>
        <tr>
          <th>#</th>
          <th>Item</th>
        </tr>
      </thead>

      <tbody>
        <tr v-for="todo in list" >
          <td>{{ todo.id }}</td>
          <td>{{ todo.item }}</td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script>
export default {
  data: function () {
    return {
      list: []
    }
  },

  created: function() {
   this.fetchTodoLists();
  },

  methods: {
    fetchTodoLists: function() {
       const resource = this.$resource('/todolists.json');
       resource.get()
        .then(response => {
           this.list = response.data
        });
    }
  }
}
</script>

應該會出現剛剛在console創的兩筆資料。

component裡面寫css


webpacker官方文件裡面有規劃資料夾,雖然敘述說會有放js css images的地方但我初創的時候只有放js的資料夾,他的架構如下:
app/javascript:
├── packs:
│ # only webpack entry files here
│ └── application.js
└── src:
│ └── application.css
└── images:
└── logo.svg

照著他的架構創
$ mkdir app/javascript/src
$ touch app/javascript/src/application.css

然後在原本rails的application.html.erb加上連結

app/views/layouts/application.html.erb
<%= stylesheet_pack_tag 'application' %>

就可以在每個.vue檔的component裡面寫css了,例如:

component.vue
<style lang="scss">
  .link {
    color: blue;
    cursor: pointer;
  }
</style>

引入vue-router


再來跟全端開發一樣,rails的gem歸rails的Gemfile,而webpack的歸package.json(Vue也是在這裡面,只不過rails5.1連著專案一起創webpack的話已經內建Vue了!)。

現在安裝vue-router
$ yarn add vue-router

先把router寫在一個js裡面:
$ touch app/javascript/packs/router.js
建立routes

app/javascript/packs/router.js
import Vue from 'vue'

import VueRouter from 'vue-router'
Vue.use(VueRouter)

import todolists_index from './../views/todolist/index.vue'

const router = new VueRouter({
    // 使用 HTML 5 模式

    mode: 'history',
    routes: [
        { path: '/todolists', component: todolists_index },
        { path: '/', redirect: '/todolists' }
    ]
})

export default router

檔名或變數不要取routes避免產生match error

修改packs/application.js把

app/javascript/packs/application.js
import Vue from 'vue'
import App from '../app.vue'

import VueResource from 'vue-resource'
Vue.use(VueResource);

import VueRouter from 'vue-router'
Vue.use(VueRouter)
import router from './router.js'

document.addEventListener('DOMContentLoaded', () => {
  const app = new Vue({
    el: "#app",
    router,
    render: h => h(App)
  })
})

現在創造專給vue的view的資料夾
$ mkdir app/javascript/views
再給todolist一個歸類
$ mkdir app/javascript/views/todolist
最後先做index檔案,根本來的app.vue一模一樣
$ cp app/javascript/app.vue app/javascript/views/todolist/index.vue

最後app.vue簡化如下:

app/javascript/app.vue
<template>
  <div>
    <router-view></router-view>
  </div>
</template>

<script>
export default {
  name: 'app'
}
</script>

就成功import vue-router了!這樣也算是做出前後端分離的概念了,rails去吐api,而vue就專門做前端的東西吧!

但目前遇到的問題是重整頁面會去讀取rilas routes,要怎麼設定待研究

[Rails]RSpec101

純粹ruby測試


安裝在本機
$ gem install rspec
會安裝5~6個東西

接著在資料夾裡下
$ rspec --init
就可以開始玩了

Rails專案裡實作


Gemfile
group :development, :test do
  gem 'byebug', platform: :mri
  gem 'rspec-rails', '~> 3.5.2'
  gem 'rails-controller-testing'
end

$ bundle install
$ bin/rails generate rspec:install
$ rspec
應該可以運作,terminal出現訊息如下:

No examples found.
Finished in 0.0.00028 seconds (files took 0.08784 seconds to load)
0 examples, 0 failures

就代表成功,可以參考Github上的專案RSpec101的CRUD-test分支做出簡單的CRUD test。

[Rails]will_paginate 做分頁

參考官方文件:
will_paginate

安裝:

Gemfile
gem 'will_paginate', '~> 3.1.0'

$ bundle install
$ rails s

在controller裡面加入:

post_controller.rb
Post.paginate(:page => params[:page], :per_page => 30)

per_page是一頁顯示幾筆

當然也可以搭配任何條件:

post_controller.rb
Post.order("created_at DESC").paginate(:page => params[:page], :per_page => 30)

這樣就可以把最新的Post顯示在最前面

最後在view:

post.html.erb
<%= will_paginate @posts %>

大功告成又很簡單~至於css的話也可以參考官方的:
make those pagination links prettier

[Rails]Ransack in header for global search

基本的使用可以先參考這些資料:
ransack官方文件
[Rails]ransack最基本使用

如果照著[Rails]ransack最基本使用做出來之後把它放在header跳到其他頁面會報錯,因為他會找不到@q這個變數,但要在每個controller加上@q又不切實際,最好的辦法就是把它獨立出來吧!

雖然他是一個form表單,但還是用get比較好,post固然也會運作,但如果原地重整頁面的話就會爆炸嘍!

所以我們先做一個router給他

routes.rb
get "search", :to => "search#index"

做一個controller
$ rails g controller search

設定如下:

search_controller.rb
class SearchController < ApplicationController
  def index
    @q = Model.ransack(params[:q])
    @models = @q.result(distinct: true)
  end
end

再來view就是action到這個router就好:

search.html.erb
<%= search_form_for @q, url: search_path do |f| %>
  <%= f.search_field :title_cont %>
  <%= f.submit "搜尋" %>
<% end %>

這樣其實是創立一個獨立的頁面,也就是說如果本來就有model#index的頁面其實search#index的頁面會是獨立且一模一樣的喲!

但此時我們發現只要她去別的頁面會因為別的controller裡面沒有@q這個變數爆error,我們來設訂一個全域變數吧:

search_controller.rb
class ApplicationController < ActionController::Base
  before_action :set_ransack_argument

  def set_ransack_argument
    @q = Model.ransack(params[:q])
  end
end

這樣的效果就可以讓整個網站都可以使用了,那我們也可以把剛剛search_controller.rb的@q = Model.ransack(params[:q])這行拿掉!