穠纖合度的整合 Rails、Webpack、Vuejs

前言

如果您曾經閱讀過小弟的vue + webpack 起手式一文,裡面我曾提到關於容易與傳統 MVC 框架(Rails, ASP.NET MVC)等結合。這篇文章主要就是用來介紹其作法與為什麼我會這麼說。

我先承認吧!我不總是需要使用 SPA 的架構 雖然它很好,但對於很多小型專案,或者我們應該說 UI/UX 設計本身非常單純的專案 - 殺雞焉用牛刀。所以這邊文章的作法針對的是那些專案不是使用 SPA 搭配 API 的架構的人,而是您覺得既有的 MVC 框架對於您的需求開發已經非常足夠且想保留大多數這些框架提供的功能(本文僅針對 Rails 介紹)。

網路上有非常多關於 Rails 與 Webpack 整合的方式,其實都非常不錯。而關於本文只是介紹其中一種小弟認為剛剛好的整合方式,其中許多觀念都不是新創的,都只是整理過去學習經驗而成。

繼續閱讀

Heroku 運行類別、 Procfile、常用指令筆記

Procfile 是一種定義指令是否可以在 Heroku dynos(一種輕量化的容器,可以執行特定用戶設定的指令)上執行的機制。
它遵循著 Unix 的程序模型(process model)。這裡為了簡化概念,我們可以說 dyno 就是執行指令的一個個體。舉例來說一個 web dyno 就意味著執行一個 web server 的程式。希望這樣的說明可以理解這些特殊的名詞概念。

繼續閱讀

關於 delete 變數詭異行為與解釋

問題

發生在當我們撰寫下面程式碼的時候所發現的奇怪行為。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var a = 1;
b = 2;

console.log(a); // 1
console.log(b); // 2

// 使用瀏覽器測試請使用 window 如果是用 node 的 REPL 環境那請改成 global
console.log(window.a); // 1
console.log(window.b); // 2

// 但接下來這麼操作

delete a; // false 不能刪
delete b; // true 可以刪

a; // 1
b; // undefined

解釋

首先根據ECMA-262 §10.3,的定義 Variable environment 是一種特定
型別的 Lexical environment,我們沒辦法透過任何方式直接存取。

一個 Lexical environment 用來記錄執行環境的資訊,可以把它想成是一個物件,我們會把在一個執行環境 Context的變數,函數都存在這個物件的屬性上
針對函數那些定義的參數(Parameter)也會被記錄,舉例來說 function foo (a, b)() 中的 ab 就會被記在 foo 的執行環境資訊中。

一個 Lexical environment 也有一個連結可以連結到外在的 Lexical environment 就是所謂的 scope chain
這個機制可以協助我們取得目前執行環境以外的變數,舉例來說就是 function 裡面可以拿到 global 的變數。

一個 Variable environment 就只是 Lexical environment 的一部份,本質上就是透過 var 宣告在執行環境中的變數或函數。

使用了 var

上面的 a 使用了 var 根據 ECMAScript 定義會被記錄在 Variable environment 根據定義 Variable environment 是不能手動刪除的。
也就是說除非用了 eavl,否則是不能被 delete 的。

1
2
3
4
5
6
7
var a = 1;
delete a; // false
console.log(a); // 1

// 記得清除整個環境
eval('var a = 1; delete a; console.log(a)'); // undefined

沒使用 var

當我們賦值卻沒有用 var,Javascript 會嘗試在 Lexical environment 尋找同名的參考。
最大的不同是 Lexical environment 是嵌套的,就是它可以關聯到外面其他的 Lexical environment
當在本地找不到的時候就會往上層去找,換句話說每個 Lexical environment 都有個爸爸,而最外層的就是 global
所以當我們不使用 var 而宣告一個變數時會開始在各個 scope 尋找同名變數,最終 Javascript 會拿一個 window(global) 的屬性來當作參考。
而物件的屬性是可以刪除的。

結論就是第一個 var 的變數被放在 Variable environment 是不能 delete 的而第二個沒有 var 的變數它是 global 的屬性。

然後你就會問我,那為什麼第一個 a 可以用 window.a 取得,因為全域的 variable object 就是 global(window) 本身。
但誰是屬性誰放在variable environment是有差的,因為程式碼看起來沒差所以會搞死人啊。

Vue.js in Slim 語法的小問題 Slim::Parser::SyntaxError

當我們使用 Vue.js 搭配 slim 時(事實上 Angular 應該也有相同的問題)時

1
2
3
4
5
6
7
8
9
10
div id="app"
p {{message}}

javascript:
new Vue({
el: '#app',
data: {
message: "Hello, Vue.js"
}
})

立馬收到Slim::Parser::SyntaxError的錯誤訊息。

但是改成這樣卻又正常

1
div id="app" {{ message }}

好啦!答案很明顯了就是我們有地方寫錯,讓 slim engine 誤會了。

這邊紀錄一下解法:

補上屬性

第一個最簡單的方式就是幫 p 補上隨意一個屬性

1
2
3
4
5
6
7
8
9
10
div id="app"
p class="" {{ message }}

javascript:
new Vue({
el: '#app',
data: {
message: 'Hello Vue.js',
}
})

加上 [], (), {} 任何一種

1
2
3
4
5
6
7
8
9
10
div id="app"
p () {{ message }}

javascript:
new Vue({
el: '#app',
data: {
message: 'Hello Vue.js',
}
})

使用 |

1
2
3
4
5
6
7
8
9
10
11
div id="app"
p
| {{ message }}

javascript:
new Vue({
el: '#app',
data: {
message: 'Hello Vue.js',
}
})

修改設定

上面的解法都是因為 slim 預設會把 {} () [] 和 tag 後面接的 property=value 當作屬性(attributes)來解析。
所以我們只要把 {} 拿掉就正常了。

新增或修改 config/initializers/slim.rb 加入

1
Slim::Engine.set_options :attr_list_delims => {'(' => ')', '[' => ']'}

開發時期,CarrierWave 重產不同 version 圖片

當增加不同 version 的尺寸需要對已上傳的圖片從新產生新尺寸的圖片時

1
> rails c

輸入

1
2
3
4
Attachment.all.each do |att|
att.image.recreate_versions!
att.save!
end

Heroku 無法 fetch gem

當我們在本機設定 gem 的時候有時候會採用直接從 github 下載的方式

1
gem 'datetimepicker-rails', github: 'zpaulovics/datetimepicker-rails', branch: 'master', submodules: true

不過當我們要把程式碼部署到雲上的主機時,有些時候會碰上該機器無法去 fetch repo 的狀況

這個時候請參考這邊改變設定即可

rake db 常用指令備註

1
2
3
4
5
6
7
8
9
10
11
12
$ rake db:create          # Create the database from DATABASE_URL or config/database.yml for the current Rails.env (use db:create:all to create all dbs in the config)
$ rake db:drop # Drops the database using DATABASE_URL or the current Rails.env (use db:drop:all to drop all databases)
$ rake db:fixtures:load # Load fixtures into the current environment's database
$ rake db:migrate # Migrate the database (options: VERSION=x, VERBOSE=false)
$ rake db:migrate:status # Display status of migrations
$ rake db:rollback # Rolls the schema back to the previous version (specify steps w/ STEP=n)
$ rake db:schema:dump # Create a db/schema.rb file that can be portably used against any DB supported by AR
$ rake db:schema:load # Load a schema.rb file into the database
$ rake db:seed # Load the seed data from db/seeds.rb
$ rake db:setup # Create the database, load the schema, and initialize with the seed data (use db:reset to also drop the db first)
$ rake db:structure:dump # Dump the database structure to db/structure.sql
$ rake db:version # Retrieves the current schema version number

Devise 快速上手

devise 使用筆記

Devise 是一套彈性的驗證機制解決方案,它是根據 Warden 為架構的基礎延伸的。Devise 本身具備

  • 支援 rake
  • 架構在 Rails 之上提供完整的 MVC 方案
  • 提供您可以使用多個 Model 在同一時間登入
  • 模組化,您只需要採用您需要的部份
繼續閱讀

ActionPack 雜記

Action Pack

Action Pack 是整個 Rails 的核心部份,由 ActionDispatch, ActionController, ActionView 組成
ActionDispatch 處理接收到的請求(Requests),即網址的部分,ActionController 負責把請求對應轉換成回應(Responses)
接著 ActionController 調用 ActionView 來處理回應的格式(html, js, json, xml) 等

繼續閱讀

Rails 精簡練習

指令練習

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ rails generate scaffold User name:string bio:text birthday:date
$ rails destroy scaffold User
$ rails g scaffold Post name --skip-assets
$ rails db
$ rails console
$ rails g controller posts # 複數
$ rails g controller posts index new create # 後面可以接 action
$ rails g model Post title body:text
$ rails d model post

$ rake db:setup
$ rake db:migrate
$ rake db:migrate:status
$ rake routes

手刻練習

  1. 建立文章控制器 posts_controller.rb
  2. 加入 resources :posts 路由
  3. 加入 列表 action index
  4. 建立 index view
  5. 加入 新增 action new
  6. 加入 new view 重點在 form_for 表單
  7. 新增 post model rails g model post title body:text
  8. 建立 create action 來儲存 model,記得補上驗證
  9. 建立 show view 顯示單筆資料
  10. Model 可以建立之後把 index view 補上 each do
  11. 把 new view 抽出 _form 然後建立 edit action and update
  12. 補上 destroy action
  13. 加上關聯的 comment 留言 model
  14. 補上 model 關聯的設定
  15. 路由設定
  16. 建立 comments_controller.rb
  17. comments 這個 controller 是依附在 posts 底下,加上巢狀 resources 會得到 /posts/:post_id/comments 所以注意表單 form_for([@post, @post.comments.build]) 的部分
  18. 建立 create action 重點在 @post = Post.find(params[:post_id])@comment = @post.comments.create(params[:comment].permit(:name, :body))
  19. 注意 view 的 _form 部分
  20. view 總共會建立 3 個 _form, _comment, 再修改 posts/show 使用 render “comments/form”
  21. 實作 comments_controller 的 destroy action ,記得要從 @post.commetns.find() -> destroy
  22. models/post.rb 記得 dependent: :destroy

1.

1
$ rails g controller posts

2. 修改 config/routes.rb

1
2
resources :posts
root "posts#index"

3. 加入 action

1
2
3
4
5
6
class PostsController < ApplicationController

def index
end

end

4. 加入 index view

1
<h1>Hello world!</h1>

5. 實作新增 new

1
2
3
4
5
6
class PostsController < ApplicationController
#...

def new
end
end

6. 在 app/views/posts/new.html.erb 新增表單,此時沒有 Model ,因為我們在 form_for 用 symbol

1
2
3
4
5
6
7
8
9
<%= form_for :post, url: posts_path do |f| %>
<%= f.label :title %>
<%= f.text_field :title %>

<%= f.label :body %>
<%= f.text_field :body %>

<%= f.submit %>
<% end %>

此時當我們送出 post 的時候,第一沒有 create 的 action 因為 resources :posts 幫我們加上了 8 個路由,扣掉 put, patch 功能一樣有 7 組不同功能的路由

1
2
3
4
5
6
7
8
get "posts" => "posts#index", :as => "posts" # 取得 posts_path helper
post "posts" => "posts#create", :as => "posts"
get "posts/:id" => "posts#show", :as => "post" # 注意單數,用的時候後面要帶 model 參數才能取得 :id
get "posts/new" => "posts#new", :as => "new_post"
get "posts/:id/edit" => "posts#edit", :as => "edit_post"
put "posts/:id" => "posts#update", :as => "post"
patch "posts/:id" => "posts#update", :as => "post"
delete "posts/:id" => "posts#destroy", :as => "post"

7. 新增 Post Model

1
2
$ rails g model Post title body:test
$ rake db:migrate

8. 在 controller 新增 create action

1
2
3
4
5
6
7
8
9
10
def create
@post = Post.new(post_params)
@post.save
redirect_to @post
end

private
def post_params
params.require(:post).permit(:title, :body)
end

這個時候送出會產生錯誤,因為 redirect_to @post 的關係。一般情況下此時只要有對應的 app/views/posts/show.html.erb 就能 work 了

9. 建立 show action 和 view

1
2
3
4
5
<h1><%= @post.title %></h1>

<p><%= time_ago_in_words(@post.created_at) %></p>

<p><%= @post.body %></p>

10. index 列表

1
2
3
4
<% @posts.each do |post| %>
<h2 ><%= link_to post.title, post_path(post)%></h2>
<p class="date"><%= post.created_at.strftime("%B %d, %Y") %></p>
<% end %>

11. 加入 comment

1
2
$ rails g model Comment name:string body:text post:references
$ rake db:migrate
1
2
3
4
5
6
7
8
9
10
11
12
13
# post.rb

class Post < ActiveRecord::Base
has_many :comments
validates :title, presence: true, length: { minimum: 5 }
validates :body, presence: true
end

# routes.rb

resources :posts do
resources :comments
end

因為現在 Comment model 是關聯在 Post model 下面,所以 @post = Post.find(params[:post_id]) 當我們要在 CommentsController 找 Post 需要 :post_id

1
2
3
4
5
6
7
8
class CommentsController < ApplicationController
def create
@post = Post.find(params[:post_id])
@comment = @post.comments.create(params[:comments].permit(:name, :body))

redirect_to post_path(@post)
end
end

form_for

form_for 的用途是建立一個 html 的 form 表單,讓使用者可以透過 form 把資料傳回後端,新增或者更新某個指定物件的屬性。
這個方法(method)有幾種稍微不同的用法,取決於您想從 Rails 的 Model 中自動對應多少東西,或者說自動處理掉 Model 對應的部分
針對一般情況的 Model,我們透過傳入 form_for 一個字串或 symbol 來表示我們要對應的物件

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
45
46
47
48
49
50
#--- erb

<%= form_for :xx do |f|%>
<%= f.text_field :oo %>
<% end %>

#--- 輸出

<form method="post" accept-charset="UTF-8" action="同一個路由">
<input name="utf8" value="✓" type="hidden">
<input name="authenticity_token" value="EsXGzijdB1Wx8f5FNtO+l8XDEpoA49Ko7nJsZw+Tb5N5BpIQPs3NCw9iO5gwZTkJgyzDm48GHDQCkPwwcFa2WA==" type="hidden">
<input id="xx_oo" name="xx[oo]" type="text">
<form >

#-- 協助記憶

<%= form_for :object do |f| %>
<%= f.text_field :attribute %>
<% end %>

#--- 您也可以單純用 text_field,在 yield 中的 f 變數是一個 FormBuilder 物件,透過與 Model 物件或者 symbol 的定義合作產生表單

#--- erb
<%= text_field :a, :b %>
<%= text_field :person, :name %>
#--- 輸出
<input type="text" id="a_b" name="a[b]">
<input type="text" id="person_name" name="person[name]">

#--- 當 submit 的時候會收到 params[:a][:b], params[:pserson][:name]


#--- 此時如果有一個 @a 的變數傳進來預設就會初始化帶入這些欄位
#--- 範例
#--- view/
<input type="text" id="a_b" name="a[b]">

#--- controller/
class SomeController < ApplicationController
class A
attr_accessor :b
end

def action
@a = A.new
@a.b = "What happen?"
end
end

#--- 如此 view 的 a[b] 就會自動帶入 What happen?

在 form_for 右邊還可以帶入參數

  • url 可以修改 submit 的網址即 action=”url here”
  • namespace 替內部的 input id 再加上特殊的前綴字例如上面本來是 a_b 如果加上 namespce: ‘x’ 就會變成 x_a_b
  • html 其他原生 html 的屬性 e.g. :html => { :multipart => true }
  • 針對表單還有 FormOptionHelper 和 DateHelper 可以針對下拉式選單或日期做處理

注意 form_for 本身不會建立一個獨立的 scope ,意味著您看同時混搭 FormHelper 和 FormTagHelper

1
2
3
4
5
6
<%= form_for :person do |f| %>
<%= f.text_field :first_name %>
<%= text_area :last_name %>
<%= check_box_tag "person[admin]", "1", @person.company.admin? %>
<%= f.submit %>
<% end %>

form_for 搭配 Model

在上面的範例,會根據傳入 form_for 的 symbol 去產生對應的 from 表單 name 屬性,如此一來可以被對應為一個物件,如果您傳入的是字串那麼意思也是一樣的。
我們也可以把 Model 物件本身當作參數傳入,如果 @person 存在且您想編輯它那麼您可以直接使用如下

1
2
3
<%= form_for @person do |f| %>
...
<% end %>

這麼寫的行為幾乎跟您使用 symbol 一樣,不過有些微不同,首先是表單的前綴字即用 model 的 class name , 且 form 會有 id 且會根據 new 或 edit 不同
當然如果不想被綁死您也可以修改

1
2
<%= form_for(@person, as: :client) %>
<% end %>

其次是當該物件已經被初始化或者說有值的時候對應 attributes 的欄位會自動帶入該值,因此如果該 view 已經有個變數 post
也可以這樣寫

1
2
<%= form_for post do |f| %>
<% end %>

在剛剛的範例中,雖然我們沒有明確的指定,但我們還是要使用 :url 來指定 post 的目標。
然而如果我們的物件有透過 resources 指定路由的話路徑就會自動處理
例如

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
<%= form_for @post do |f| %>
<% end %>

# 會等於
<%= form_for @post, as: :post, url: post_path(@post), method: :patch, html: { class: "edit_post", id: "edit_post"} do |f| %>

# 如果 @post 是剛初始化的話
<%= form_for(Post.new) do |f| %>
...
<% end %>

<%= form_for @post, as: :post, url: posts_path, html: { class: "new_post", id: "new_post" } do |f| %>
...
<% end %>

# 當然也可以覆寫

<%= form_for(@post, url: super_post_path) do |f| %>

# 或者設定回應的格式
<%= form_for(@post, format: :json) dp |f| %>

# 針對 namespace 路由 e.g. admin_post_url
<%= form_for([:admin, @post]) do |f|>

# 如果是關聯的子物件屬性
<%= form_for([@post, @comment]) do |f| %>

關掉 id

1
2
3
4
5
<%= form_for(@post) do |f| %>
<%= f.fields_for(:comments, include_id: false) do |cf| %>
...
<% end %>
<% end %>

也可以改用別的 FormBuilder

1
2
3
4
<%= form_for @person, url: { action: "create" }, builder: LabellingFormBuilder do |f| %>
# 照上面的範例如果我們用
<%= render f %>
# 則會 render people/_labelling_form 的樣板

redirect_to 的用法

將瀏覽器重新定向到參數(options)中指定的目標,這個參數可以用下面格式:
(Hash, Model Record, String 搭配 protocol://, String, :back) 總結來說是三種格式

  • Hash - 這種格式是透過搭配 url_for 產生的
1
2
redirect_to url_for(controller: 'posts', action: 'new')
redirect_to :action => "new" # 就算你不加 url_for 預設也會幫您呼叫
  • Record - 一筆紀錄其實就是您取出來的 model 本質上當您 redirct_to @model 時總結來說就是轉址到 model_path(@model)
    對應的路徑即 /posts/:id 就是 “posts#show” (PostsController > show action)。所以雖然我們上面沒有定義 show action 但只要有 view (show.html.erb)就能夠執行是因為 Rails 預設當找不到 action 的時候會直接去找對應 action 的 view。

傳入紀錄的方式在內部是透過 polymorphic_url 來處理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 當我們使用 RESTful 路由時內部的各種狀況範例如下:
#
# # 呼叫 post_url(post)
# polymorphic_url(post) # => "http://example.com/posts/1"
# polymorphic_url([blog, post]) # => "http://example.com/blogs/1/posts/1"
# polymorphic_url([:admin, blog, post]) # => "http://example.com/admin/blogs/1/posts/1"
# polymorphic_url([user, :blog, post]) # => "http://example.com/users/1/blog/posts/1"
# polymorphic_url(Comment) # => "http://example.com/comments"
#
#
# ==== 其他範例
#
# # an Article record
# polymorphic_url(record) # same as article_url(record)
#
# # a Comment record
# polymorphic_url(record) # same as comment_url(record)
#
# # it recognizes new records and maps to the collection
# record = Comment.new
# polymorphic_url(record) # same as comments_url()
#
# # the class of a record will also map to the collection
# polymorphic_url(Comment) # same as comments_url()
  • 字串 - 有使用 protocol:// 例如: http:// 就是直接傳入網址
  • 字串不搭配 protocol 就是 //example.com 會用當前的通訊協定
  • :back - 簡單的說就是 redirect_to(request.env[&quot;HTTP_REFERER&quot;]) 的縮寫
1
2
3
4
5
6
7
8
9
10
11
12
# 完整範例
redirect_to :action => "show", :id => 5
redirect_to post
redirect_to "http://www.rubyonrails.org"
redirect_to "/images/screenshot.jpg"
redirect_to articles_url
redirect_to :back
# 搭配狀態
redirect_to post_url(@post), :status => :found
redirect_to :action=>'atom', :status => :moved_permanently
redirect_to post_url(@post), :status => 301
redirect_to :action=>'atom', :status => 302

routes

routing 模組使 Ruby 具有 rewrite URL 網址的能力,這是一種處理 Request 對應到 controller 及 action 的方式。
主要是用來取代像是 apache 中 mod_rewrite 規則的功能。而在 Rails 中您可以不用動到 Server 的設定,只要設定 config/routes.rb 即可。

建立路由的核心概念就像是列出一張對應表,對應 Requests 。這張對應表告訴系統該怎麼執行,其中必須要遵循一些規則樣式。

1
2
3
4
Rails.application.routes.draw do
# Pattern 1 指定對應的 request 路徑到 controller
# Pattern 2 告訴他們去別的地方
end

其中路由的功能還包含 helper 來協助生成網址

最基本的 Pattern 1 類似於下面範例

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
45
46
47
48
49
50
51
52
53
54
55
get '/products/:id', to: 'products#show' # 把 :id 傳入 params 然後把 request 交給 products_controller 的 show action 處理
get '/products/:id', to: 'products#show', as: 'product' # 對應單數如此一來會產生 product_path helper
# 使用 resources 方式一口氣產出 7 種不同的路由(實際上 patch, put 功能一樣總數有8個)
resources :products # 複數

# 這種 resources 的方式是透過 http method 搭配 url 組成一系列的路由,把 4 組 url + 5 個 http verbs = 7 組功能
# 例如 DELETE /products/17 當 Rails 收到這組 Request 時,就會呼叫 products 的 destroy 並把 params 帶入
get '/photos', to: 'photos#index'
get '/photos' => 'photos#index' # 除了用 :to 還可以直接用 =>
get '/photos/new', to: 'photos#new'
get '/photos/:id', to: 'photos#show'
post '/photos', to: 'photos#create'
get '/photos/:id/edit', to: 'photos#edit'
put '/photos/:id', to: 'photos#update'
patch '/photos/:id', to: 'photos#update'
delete '/photos/:id', to: 'photos#destroy'
# 省略的寫法
match 'messages/show' # 相等於 match 'messages/show' => 'messages#show'
match 'messages' => 'messages#index', :as => 'index'
match "/messages/show/:id" => "messages#show", :constraints => {:id => /\d/} # 限制參數

# 單數的 resource
resources :post
> new_post_path
> edit_post_path
> post_path

# 命名空間的用法
namespace :admin do
resources :photos
end
# 這樣的路徑會加上 admin -> /admin/posts

# 如果單純想把前面沒有 /admin 的路由對應到 Admin::PostsController 則
scope module: 'admin' do
resources :posts
end

# 或者

resources :posts, module: 'admin'

# 當某個 model 底下有子 model 時如
class User < ActiveRecord::Base
has_many :order
end

class Order < ActiveRecord::Base
belongs_to :user
end

# 路由得設法
resources :users do
resources :orders
end

render 方法 文件

渲染回應給瀏覽器的內容

  • 渲染 action (rendering an action)
1
2
3
render :action => "goal"
render :action => "short_goal", :layout => false
render :action => "long_goal", :layout => "spectacular"