Kurt Hsu's blog

The Rails developer in taiwan.


  • 首頁

  • 標籤

  • 分類

  • 歸檔

[Rails] I18n 基本使用

發表於 2018-05-14 更新於 2020-05-28 分類於 Rails , i18n

首先安裝rails-i18n

Gemfile
1
gem "rails-i18n"

$ bundle install

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

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

1
2
3
4
zh-TW:
welcome: "嗨世界!#{username}"
sport:
ball: 籃球

而英文翻譯:

1
2
3
4
en:
welcome: "Hello world! #{username}"
sport:
ball: basketball

使用方法如下:

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

在application_controller.rb設定如下:

application_controller.rb
1
2
3
4
5
6
7
8
9
10
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
1
2
<%= link_to "中文版", :controller => controller_name, :action => action_name, :locale => "zh-TW" %>
<%= link_to "English", :controller => controller_name, :action => action_name, :locale => "en" %>

I18n 系列:

[Rails] I18n default scope] [Rails] I18n 語系設定]

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

發表於 2018-05-14 更新於 2019-08-22 分類於 Rails , Vue

我的開發環境:
Rails 5.1以上

引入webpacker和vue


先安裝web packer

Gemfile
1
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
1
2
3
def index
@products = Product.all
end

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

index.html.erb
1
2
3
4
5
6
<% props = {
products: @products
}.to_json
%>

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

接下來到vue的檔案application:

app/javascript/packs/application.js
1
2
3
4
5
6
7
8
9
10
11
12
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
1
2
3
4
5
6
7
8
9
10
11
12
13
<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)

發表於 2018-05-12 更新於 2019-08-21 分類於 Rails , Vue

參考文件:
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
1
root "todolists#index"

測試有沒有成功

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

更新todolist/index

app/views/todolists/index.html.erb
1
2
<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
1
2
3
# 把原本的 <%= javascript_pack_tag 'hello_vue' %> 替換如下

<%= javascript_pack_tag 'application' %>

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

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

修改packs/application.js

app/javascript/packs/application.js
1
2
3
4
5
6
7
8
9
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<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
1
2
import VueResource from 'vue-resource'
Vue.use(VueResource);

現在修改app.vue

app/javascript/app.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<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
1
<%= stylesheet_pack_tag 'application' %>

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

component.vue
1
2
3
4
5
6
<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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
1
2
3
4
5
6
7
8
9
10
11
<template>
<div>
<router-view></router-view>
</div>
</template>

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

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

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

[Rails]RSpec101

發表於 2018-05-11 更新於 2020-03-07 分類於 Rails , RSpec

純粹ruby測試


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

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

Rails專案裡實作


Gemfile
1
2
3
4
5
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出現訊息如下:

1
2
3
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 做分頁

發表於 2018-05-10 更新於 2019-08-21 分類於 Rails , Gem , will_paginate

參考官方文件:
will_paginate

安裝:

Gemfile
1
gem 'will_paginate', '~> 3.1.0'

$ bundle install
$ rails s

在controller裡面加入:

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

per_page是一頁顯示幾筆

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

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

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

最後在view:

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

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

[Rails]Ransack in header for global search

發表於 2018-05-10 更新於 2019-12-29 分類於 Rails , Gem , Ransack

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

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

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

所以我們先做一個router給他

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

做一個controller
$ rails g controller search

設定如下:

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

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

search.html.erb
1
2
3
4
<%= 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
1
2
3
4
5
6
7
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])這行拿掉!

[Rails]操作遠端的 rake 和 console

發表於 2018-05-09 更新於 2019-08-21 分類於 Rails

原本只是想利用capistrano-rails-db 這個套件輕鬆在本地端下指令就好,但rails5有新增一個保護機制,只要drop或者reset會被擋住,只要不要有動到就資料的動作其他其實也蠻正常的,像是cap deploy:db:seed之類的。

是什麼保護機制可以參考這篇:
防止Production数据库被意外清空 | Rails 5

情況:
我是用Capistrano部署在機台上的專案web server是Nginx

如何執行rake


其實直接進到機台操作還是最簡單直接的,首先進到專案裡的current資料夾

如果要reset或drop db的話請先關閉web server:
$ service nginx stop

再來基本上都可以動rake db的指令了,例如reset:
$ RAILS_ENV=production bin/rake db:reset DISABLE_DATABASE_ENVIRONMENT_CHECK=1
DISABLE_DATABASE_ENVIRONMENT_CHECK就是rails5的保護機制,可以參考上面那篇。

都控制玩db之後重新開啟即可:
$ service nginx start

如何進入console


一樣到專案裡的current資料夾
$ bundle exec rails c production
就可以嘍!

[Rails]Capistrano run seed with remote & error File exists

發表於 2018-05-06 更新於 2019-12-29 分類於 Rails , Gem , Capistrano

參考文件:
Rails 部署 Capistrano 如何使用seed档
capistrano-rails-db

使用


假設已經使用了Capistrano部署完,如沒有參考:
[Rails] Capistrano 部署rails專案到 linode

再來開始使用套件

Gemfile
1
2
3
gem 'capistrano',  '~> 3.1'
gem 'capistrano-rails', '~> 1.1'
gem 'capistrano-rails-db'

Capfile
1
2
require 'capistrano/rails'
require 'capistrano/rails/db'

官方文件上是寫cap deploy:db:seed,我使用如下:
$ cap production deploy:db:seed
就可以了,其他指令應該依樣畫葫蘆就好。

Error


我的seed檔有包含創造圖片,使用paperclip這個套件,因為之前設定deploy.rb檔錯誤發生error,敘述如下:

我把

deploy.rb
1
append :linked_dirs, "log", "tmp/pids", "tmp/cache", "tmp/sockets", "public/system"

設定成

deploy.rb
1
append :linked_dirs, "log", "tmp/pids", "tmp/cache", "tmp/sockets", "public/system/000/000"

並且$ cap production deploy過

造成run $ cap production deploy:db:seed的時候release出錯:
01 Errno::EEXIST: File exists @ dir_s_mkdir - /home/deploy/movie_review/releases/20180506145631/public/system/movies/images/000/000

所以要手動到機台上面進去這個資料夾把/home/deploy/movie_review/releases/20180506145631/public/system/movies/images/000/
刪除即可

[Rails]form_for preview image with javascript

發表於 2018-05-06 更新於 2020-05-31 分類於 Rails , JavaScript

原理是利用Javascript的FileReader製作一個DOM,再利用CSS把舊的圖片藏起來。

view.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<%= form_for @profile do |f| %>
<div class="form-group" style="width: 300px;">
<%= f.file_field :image, required: true, accept:'image/png,image/jpeg', id: :image %>
</div>

<div id="image_preview"></div>

<% if @profile.image.present? %>
<%= image_tag @profile.image.url(:medium), :id => "old_image" %>
<% end %>

<div class="form-group" style="width: 300px;">
<%= f.submit "上傳", class: "btn btn-lg btn-primary" %>
</div>
<% end %>

<script type="text/javascript">
$(function() {
$('#image').on('change', function(event) {
let files = event.target.files;
let image = files[0]
let reader = new FileReader();
reader.onload = function(file) {
let img = new Image(400, 600);
img.src = file.target.result;
$('#image_preview').html(img);
$('#old_image').css("display", "none");
}
reader.readAsDataURL(image);
});
});
</script>

1.上傳圖片後會觸發change這個function
2.event.target可以拿到id=”image”這個tag
3.event.target.file[0]則拿到這個檔案的本體File 格式的物件
4.創造一個FileReader
5.註冊一個FileReader的onload事件,只要拿FileReader做事情就會觸發onload事件
6.用readAsDataURL讀取File本體,完成後 result 將以 data: URL 格式(base64 編碼)的字串來表示讀入的資料內容
7.觸發的onload變數file是剛剛readAsDataURL讀取出來的東西
8.創造一個Image DOM 尺寸是400px * 600px
9.file.target.result是剛剛readAsDataURL讀取File本體回傳的data: URL 格式
10.利用html()把它放進 $(‘#image_preview’)裡面
11.css把舊的$(‘#old_image’)藏起來

<%= image_tag @profile.image.url(:medium), :id => “old_image” %>是用paperclip這個套件做的

小補充:
Blob是一個檔案(原始資料)的不可變物件,剛剛的File 格式的物件其實也是一種特殊的Blob,所以也可以用在任何接受 Blob 物件的地方,例如FileReader。

參考文件:
How to preview uploaded image instantly with paperclip in ruby on rails
MDN FileReader
MDN Blob
MDN Image()

[Rails]form_for select簡易使用

發表於 2018-05-05 更新於 2019-08-21 分類於 Rails

先創造一個最基本的select:

view.html.erb
1
2
3
<%= form_for @profile do |f| %>
<%= f.select(:age, options_for_select(["6+", "12+", "15+", "18+"]), {}, { :class => ’style'}) %>
<% end %>

我們建議把options寫在model:

Profile.rb
1
AGE_CLASS = ["6+", "12+", "15+", "18+"].freeze

常數全部大寫是個好習慣
如此簡單的字串array我們也可以改寫為:

Profile.rb
1
AGE_CLASS = %w(6+ 12+ 15+ 18+).freeze

把options放到view上:

view.html.erb
1
2
3
<%= form_for @profile do |f| %>
<%= f.select(:age, options_for_select(Profile::AGE_CLASS), {}, { :class => ’style'}) %>
<% end %>


如果是想用key, value的話options改為:

Profile.rb
1
AGE_CLASS = [["普遍級","6+"], ["保護級","12+"], ["輔導級","15+"], ["限制級","18+"]].freeze


如果想要類似placeholder的空值的話利用include_blank:

view.html.erb
1
2
3
<%= form_for @profile do |f| %>
<%= f.select(:age, options_for_select(Profile::AGE_CLASS), {:include_blank => "請選擇"}, { :class => ’style'}) %>
<% end %>


但這樣似乎會讓edit帶入不了表單之前的值,所以我們用預設值selected:

view.html.erb
1
2
3
<%= form_for @profile do |f| %>
<%= f.select(:age, options_for_select(Profile::AGE_CLASS, :selected => @profile.age), {:include_blank => "請選擇"}, { :class => ’style'}) %>
<% end %>

這樣如果像是新增或者是先前表單的值並不在options裡面則會跳回include_blank

如果想用html先驗證必填的話加入required:

view.html.erb
1
2
3
<%= form_for @profile do |f| %>
<%= f.select(:age, options_for_select(Profile::AGE_CLASS, :selected => @profile.age), {:include_blank => "請選擇"}, { :class => ’style', :required => true}) %>
<% end %>


如果想讓某個options不能選擇的話加入disabled

view.html.erb
1
2
3
<%= form_for @profile do |f| %>
<%= f.select(:age, options_for_select(Profile::AGE_CLASS, :selected => @profile.age, :disabled => "18+"), {:include_blank => "請選擇"}, { :class => ’style', :required => true}) %>
<% end %>

甚至可以在裡面寫ruby code,但建議邏輯判斷不要寫在view裡就是了,可以善用helper!

1…567…18

Kurt Hsu

Progress One Percent Every Day
171 文章
55 分類
163 標籤
RSS
© 2020 Kurt Hsu
由 Hexo 強力驅動 v3.8.0
|
主題 – NexT.Muse v7.3.0