Kurt Hsu's blog

The Rails developer in taiwan.


  • 首頁

  • 標籤

  • 分類

  • 歸檔

[CS50]2019哈佛大學程式課程Week9

發表於 2020-10-02 分類於 CS50
  • 2019年的CS50: 只有英文字幕
  • 導讀哈佛大學程式課程: 胡立的中文導讀
    注意: 中文導讀不確定是幾年的, 有可能會有些許對不上或順序不對, 但最後重點都會講到

系列文章:

  • [CS50]2019哈佛大學程式課程Week0
  • [CS50]2019哈佛大學程式課程Week1
  • [CS50]2019哈佛大學程式課程Week2

最後一堂課大概就是總回顧, 然後這個課程背後有一個團隊在 support, 這邊大概紀錄一下心得吧

這一系列的課總共 10 堂, 每年可能內容和順序不太一樣, 前三堂我是真的看英文版很吃力的去聽懂他們在講什麼, 後來有人翻譯 2018 年的課程有中文字幕, 雖然不一地正確但省力很多, 所以其實最後七堂都是 2018 的內容

當初會想看這個文章是因為自己是非本科系的工程師, 目前大約也三年多經驗了, 總覺得自己電腦知識非常的不足, 實務上確實是用
stackoverflow 來解決大部分的問題, 不過常常有時候很好奇 coding 世界到底是怎麼運作出來的, 有些英文專有名詞真的不是很懂, 花費了非常多時間但一切對我來說還是感覺很像魔法一樣, 這就讓我很沒有安全感

但我得說這十堂課對於自己目前工作, 至少在台灣也許幫助不是很大, 但看完會有一種 oh~ 原來我手上的工具是這樣發展出來的感覺, 有些自己在工作的項目上真的會比較有安全感, 就像之前看完 Dave Thomas 解說 ruby 物件導向的世界觀, 自己在寫 class 或想程式碼怎麼運作的時候就會對自己比較有信任感, 物件導向可以參考我之前寫的這篇[Ruby] 物件導向

站在巨人的肩膀上真的很方便, 但想了解巨人的話推薦可以看 CS50 囉!!

小補充:
2018 有中文字幕的連結在這裡 CS50 Lectures 2018

[CS50]2019哈佛大學程式課程Week8

發表於 2020-10-02 分類於 CS50
  • 2019年的CS50: 只有英文字幕
  • 導讀哈佛大學程式課程: 胡立的中文導讀
    注意: 中文導讀不確定是幾年的, 有可能會有些許對不上或順序不對, 但最後重點都會講到

系列文章:

  • [CS50]2019哈佛大學程式課程Week0
  • [CS50]2019哈佛大學程式課程Week1
  • [CS50]2019哈佛大學程式課程Week2

database

我們紀錄使用者的資訊可以用 cookie or csv 檔 text 檔之類的, 但這很沒有安全性, 所以可以記錄在 database 裡面

data type

存的資料有很多種 type, 每一種佔用的記憶體不同, 例如 integer 和 bigint 都是存數字, 甚至 bigint 因為用的記憶體空間較大, 可以存遠比 integer 更大的數字, 但就是效能比較不好
現在的 sql 也都很聰明, 例如字串就會依據輸入的字元多寡而去決定該用多少記憶體, 但 default 值有上限, 除非自己定義更多的空間, 例如在 sqlite string 預設最多 255 個字元, 可以設更少或更多, 取決用途
最後影響效能的還有 type 存在記憶體的方式不一樣, 例如 varchar 就會比 char 慢

注意

要注意 race condition, 善用 lock 讓當前的 table 暫時不能給別的使用者更新
要注意 database injection, database 會盲目地執行使用者輸入的字串, 所以程式碼要執行的 sql 指令最好是動態產生有做過一些處理的

課程上還有介紹蠻多基礎的東西, 但自己只記錄一些自己還不知道或者比較重要的

[CS50]2019哈佛大學程式課程Week7

發表於 2020-10-02 分類於 CS50
  • 2019年的CS50: 只有英文字幕
  • 導讀哈佛大學程式課程: 胡立的中文導讀
    注意: 中文導讀不確定是幾年的, 有可能會有些許對不上或順序不對, 但最後重點都會講到

系列文章:

  • [CS50]2019哈佛大學程式課程Week0
  • [CS50]2019哈佛大學程式課程Week1
  • [CS50]2019哈佛大學程式課程Week2

web programming

python 可以載入套件直接開啟 web server 和 render html, 就可以開始開發 web 了
html 裡的雙大括號則是 python 給的動態參數

介紹了 get url 參數和 post form 的參數拿法是不一樣的
不斷地重複寫 html 很冗長, 以後維護也要所有檔案都更新, 所以用在最外部的 html body 內去動態 render 其他 html, 就不用一直重複寫 header 那些, 可以專注開發在想要開發的東西上面

但由於 python 和 web server 主要都沒有存資料的功能, 所以 server 一重開剛剛的資料都會消失, 在這堂課還沒介紹 sql 之前可以在表單送出之後把資料寄到自己的信箱 or 存在 csv 檔裡

html 裡可以寫簡單的驗證, 也有許多 ui framework 可以用, 例如 bootstrap
後續還示範收尋功能, 可以簡單用關鍵字查英文單字(單字庫是先寫死的)

[CS50]2019哈佛大學程式課程Week6

發表於 2020-10-01 更新於 2020-10-02 分類於 CS50
  • 2019年的CS50: 只有英文字幕
  • 導讀哈佛大學程式課程: 胡立的中文導讀
    注意: 中文導讀不確定是幾年的, 有可能會有些許對不上或順序不對, 但最後重點都會講到

系列文章:

  • [CS50]2019哈佛大學程式課程Week0
  • [CS50]2019哈佛大學程式課程Week1
  • [CS50]2019哈佛大學程式課程Week2

python

python 是比起 C 是更高級的語言, 但他本身就有更多東西可以用, 例如 string 有的沒的, 當然它的效能會比 C 弱一些, 不過取而代之的是更好的閱讀性, 接下來稍微介紹一些與 C 不一樣的地方, 另外 python 依然也有溢出的問題

如何取決

除了介紹 python 的東西, 最後面比對了 python 和 C 的不同
其實最大的不同就是執行時間, python 大約是 C 的兩倍(不過可能有其他因素例如 IDE 等), 總之 python 雖然比 C 好開發很多, 但終究沒辦法像 C 一樣快甚至直接調控很底層的東西, 不過現在在台灣大多都在開發 web, 其實這一點執行速度 python 就很夠了, 加上更利於團隊去維護開發, 不過今天是超級 big data 就不一定了, 所以挑選合適的語言合適的工具去解決問題也是很重要的一環

[CS50]2019哈佛大學程式課程Week5

發表於 2020-10-01 更新於 2020-10-02 分類於 CS50
  • 2019年的CS50: 只有英文字幕
  • 導讀哈佛大學程式課程: 胡立的中文導讀
    注意: 中文導讀不確定是幾年的, 有可能會有些許對不上或順序不對, 但最後重點都會講到

系列文章:

  • [CS50]2019哈佛大學程式課程Week0
  • [CS50]2019哈佛大學程式課程Week1
  • [CS50]2019哈佛大學程式課程Week2

TCP/IP

server 和 server 之間傳遞訊息的協定, 目前有 4 個 8 位數共 32 位數的數字組成, 總共可以有超過 40 億的組合
但現在世界上已經遠遠超過 40 億個獨立的設備, 遠遠不夠用, 所以直到現在已經出現了 IPv6 的版本使用 128 位數

現在全球的 HTTP 協定還會多加 port 80 代表是網際網路訊息, port 25 則是 SMTP 專門用來發送 E-mail
加上 port 會取決於會走什麼協定, 例如現在的 HTTPS port 是 443

現在網址不可能輸入純數字的 IP, 所以 DNS(Domain name system) 會幫忙轉址, 這樣也可以讓網址擁有可讀性

網址的 www 代表主機名稱, 或稱子網域 or 次網域(Subdomain), 現在有許多網址的會省略 www, 對於網址能否正確導向網站並沒有差別
更多的資訊可以參考什麼是 URL 網址 IP ? 網域 Domain 中文 意思是什麼?

網址的 http 則是剛剛提到的協定, 所以不用在 IP 後面加上 port(127.0.0.0:80 這個 80 就不用了)

http 協定

通常 client request 內容如下:

1
2
3
GET / HTTP/1.1
HOST example.com
...

server response 內容如下:

1
2
3
HTTP 200 OK
Content-Type text/html
...

影片的範例看到 response header 看到了

1
2
3
4
HTTP 301 Moved Permanenetly
...
Location: https://www.other.website.com
...

除了導向, https 協定可以安全的不讓別人看到 or 攔截這個 request

後面陸續稍微介紹 html, css, js 就不記錄了

[CS50]2019哈佛大學程式課程Week4

發表於 2020-10-01 更新於 2020-10-02 分類於 CS50
  • 2019年的CS50: 只有英文字幕
  • 導讀哈佛大學程式課程: 胡立的中文導讀
    注意: 中文導讀不確定是幾年的, 有可能會有些許對不上或順序不對, 但最後重點都會講到

系列文章:

  • [CS50]2019哈佛大學程式課程Week0
  • [CS50]2019哈佛大學程式課程Week1
  • [CS50]2019哈佛大學程式課程Week2

data structures

可先參考別人的鐵人解說: Data Structure
大概就是一個封裝的概念

再來演練如何有效的使用記憶體, 由於對 C 不熟大概所以記錄一下有使用的關鍵字

  • struct
  • malloc
  • realloc
  • sizeof
  • free

最後還用一些例子解釋演算法, 有興趣的人可直接看最後 20 分鐘

小心得

C 真的好接近底層, 什麼都要先宣告好甚至記憶體怎麼分配和使用
數學家和開發演算法的人太有才了XD

[CS50]2019哈佛大學程式課程Week3

發表於 2020-10-01 更新於 2020-10-02 分類於 CS50

這篇是 week3, 開頭先分享資源和系列文章連結:

  • 2019年的CS50: 只有英文字幕
  • 導讀哈佛大學程式課程: 胡立的中文導讀
    注意: 中文導讀不確定是幾年的, 有可能會有些許對不上或順序不對, 但最後重點都會講到

系列文章:

  • [CS50]2019哈佛大學程式課程Week0
  • [CS50]2019哈佛大學程式課程Week1
  • [CS50]2019哈佛大學程式課程Week2

這篇講蠻多 C 語言底層的東西, 我只記錄覺得對我比較有用的筆記

IDE

IDE 上網查了一下全名叫 Integrated Development Environmen, 中文翻譯是整合開發環境, 可以直接參考別人的文章了解

  • 【名詞解釋】認識 IDE 整合開發環境
  • 全世界最受歡迎的整合開發環境(IDE) 2004-2020 可以看到現在流行的 IDE

通常在台灣溝通上說用哪一種編輯器就可以了
好的編輯器通常會包含語法高亮, 語法檢查, 有終端機直接下指令, 可以直接在 code 下個斷點, 就不一定要一直 printf or bindind.pry or 打開遊覽器看 console log 之類的, 但開發習慣還是取決於自己囉

記憶體位置

之前有說過每個字元都有自己的記憶體, 如果把一連串的東西 assign 給一個變數只是把該變數的 address point to 該記憶體
如果拿第一個變數 assign 給第二個變數, 第二個變數也會拿到記憶體位置, 這時候改變原本的東西兩個變數的內容都會變化
我想這段是在介紹 C 的一些 method 和 deep clone 在記憶體位置層是怎麼發生的
最後要注意不斷的創新記憶體效能會變低, C 可以用 free 釋放該變數所佔的記憶體

[CS50]2019哈佛大學程式課程Week2

發表於 2020-06-03 更新於 2020-10-02 分類於 CS50

這篇是 week2, 開頭先分享資源和系列文章連結:

  • 2019年的CS50: 只有英文字幕
  • 導讀哈佛大學程式課程: 胡立的中文導讀
    注意: 中文導讀不確定是幾年的, 有可能會有些許對不上或順序不對, 但最後重點都會講到

系列文章:

  • [CS50]2019哈佛大學程式課程Week0
  • [CS50]2019哈佛大學程式課程Week1
  • [CS50]2019哈佛大學程式課程Week3
  • [CS50]2019哈佛大學程式課程Week4
  • [CS50]2019哈佛大學程式課程Week5
  • [CS50]2019哈佛大學程式課程Week6
  • [CS50]2019哈佛大學程式課程Week7
  • [CS50]2019哈佛大學程式課程Week8
  • [CS50]2019哈佛大學程式課程Week9

執行寫有程式碼的檔案到底發生了什麼事情

大概是說我們知道 clang 可以 compile .c file 然後可以執行它, 過程中就是把我們的 code 編譯成電腦懂的 binery 語言, 已經抽象到統稱他為 compile, 但其實程序做了不少事情, 以下為四個步驟:

  • preprocessing
  • compiling
  • assembling
  • linking

preprocessing

電腦基本上只懂得 binery 的語言, 就是一堆 010101 的東西, 所以需要有 compiler 這個過程翻譯

課程上介紹最基本的範例是用 clang 去 compile 一個 C 語言的檔案, 即可執行它, 例如:
$ clang -o hello hello.c #=> same as $ make hello
然後就可以執行 compile 出來的檔案
$ ./hello

現在檔案只要在開頭

1
2
#include <cs50.h>
#include <stdio.h>

compile 後就可以在裡面寫上 get_string 的方法, 可以注意到執行指令上會有一段 -lcs50, 其中 l 代表 libery, 這代表加上了 cs50 這個套件(libery)的程式碼, 實際上發生的事情是引入了許多 cs50 binery 的程式碼

compiling

clang 會把 .c file 先編譯成 assembly language, 基本上人類已經看不太懂, 是給 CUP 看的

assembling

會把 assembly language 在轉換成完全的電腦語言 binery

linking

剛剛在 preprocessing 步驟有提到 .c file 裡面開頭貼上 #include <cs50.h> 引入了許多 cs50 binery 的程式碼, 到這裡會把所有的 binery 程式碼連結成一塊, 所以對我們來說真正有了 get_string 可以直接 coding

電腦如何運算

電腦有個硬件叫 CPU 可以專做運算

籠統地來說可以先可以想像 CPU 裡面有三種空間, 且空間內有很多儲存格
空間1 -> 寫入固定的東西在裡面
空間2 -> 先空著準備拿來做運算
空間3 -> 專門做運算

假設今天要計算 33 等於多少
3
3 = 3 + 3 + 3
3 是放在空間1某個儲存格的值, 正常是個 binery, 電腦已經幫我們翻譯成 3 這個數字

step1. 空間2會挪出一個儲存格先放空間1的 3
step2. 再拿一個空間3先算出第一個 3 + 3 放回剛剛空間2的那一個儲存格
step3. 重複第二個步驟現在空間2的那一個儲存格就是我們要的答案

你可以想像我們人腦背 9 * 9 是離所當然的事情, 為什麼電腦這麼笨要計算這麼多次這麼久
但其實以現在的 CPU 來說一秒可以執行幾億個 step, 他要計算 3.14^10 也不過是一瞬間的事情了

最後這個例子只是想用白話解釋一下, 真正運作可能不是這樣子的

Array

Array 可以把他視為陣列的變數, 如剛剛說的儲存格, 譬如說一個字串 ‘Kurt’ 可以把他想像成有四個儲存格組成 ‘Kurt’ 這個字串, 而讀取每一個字串的方法如下(我用 ruby 示範):

1
2
3
4
5
str = 'Kurt'
puts str[0] # => 'K'
puts str[1] # => 'u'
puts str[2] # => 'r'
puts str[3] # => 't'

如何有計算字串的長度可以使用

如同第一個主題所說引入別人的 libery(例如 cs50.h) 之後就有些 method 可以使用, 加上剛剛 Array 的特性, 影片用 C 語言做一個示範:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <cs50.h>
#include <stdio.h>
#include <string.h>

int main(viod)
{
string s = get_sting("Name: ");

int n = 0;
# '\0' 代表字串結束
while(s[n] != '\0')
{
n++
}

pringf('The length of your name is %i\n, n')
}

所以這個檔案可以計算字串長度了!

C 語言本身是沒有計算長度的方法的, 只是有人像上面範例一樣做出一些方法, 並且整理得更好, 然後在別的檔案 include 進來就可以使用了

計算法

如果要排列一群數字有三種方法:

  1. 從頭到尾每兩個數字比對, 比較小的排前面 (最花時間)
  2. 每次找出最小的數字直接放到最前面 (還是蠻花時間)
  3. 每一次兩組比對, merge 後第二次再拿兩組比對, 每次比對最小的數字放前面 (花的時間指數下降)

第三種方法文字有點難解釋XD

總之可以想像當一個數字在做移動時就會消耗一個記憶體, 三個方法每一回合執行的時間如果都固定的話最後一個方法執行的回合最少之外移動的也是最少

這都是許多天才想出來讓我們電腦運算越來越快的一個例子

其他穿插的紀錄

有時候會突然講一些小東西, 我沒辦法連貫所以記錄下來

  1. 不論成功失敗永遠都要回傳東西, 成功或錯誤訊息 or boolean
  2. 雖然電腦計算很快, 但過多的 loop 或過少的變數設計在程式碼越多和越複雜的情況下效能還是會差異的可怕
  3. 加密的東西需要一把鑰匙另一頭人才可以解密, 現在電腦許多的協議以及工具已經可以做到安全性蠻高的

[Rails] 簡介 before, after and around actions(filters)

發表於 2020-05-27 更新於 2020-05-29 分類於 Rails

最近在看 I18n 的文件, 突然看到了 around_action, 然後才發現我對這塊的理解不到這麼熟悉所以記錄一下, 搭配官方文件和一篇外國文章就能解釋得非常清楚了, 那篇外國文章範例非常好, 基本上我就直接沿用他的範例了~

關鍵字 active controller filter

在 rails action_controller 的官方文件裡面, before_action 那些都稱為 filter, 不過查詢 before_action 或者 before_filter 都可以找到很多資訊, filter 是舊的 helper 名稱在 rails v2.3.8 已經被遺棄了

所有 filters 官方文件, 就是現在的 before_action 那些
before_filter 官方文件, 名稱在 rails v2.3.8 已經被遺棄

接下來 before_action, after_action 等等都統稱為 filters

before_action

剪貼一些官方敘述和自己的見解

  • If a “before” filter renders or redirects, the action will not run. If there are additional filters scheduled to run after that filter, they are also cancelled.

    如果在 before_action 提早 renders 或 redirects, 接下來所有的 action 或 filters 都會被中斷不會執行

  • Calling the same filter multiple times with different options will not work, since the last filter definition will overwrite the previous ones.

    同名 filter 的 options(only, except等) 會覆蓋掉前一個的

after_action

剪貼一些官方敘述和自己的見解

  • “after” filters cannot stop the action from running.

    after_action 不能中斷 action, 畢竟是 action 成功執行完之後才會跑 after_action

  • “after” filters are executed only after a successful action, but not when an exception is raised in the request cycle.

    after_action 一定要在 action 成功執行完之後才會執行, 如果在 action except 或 raise 掉了也不會執行

around_action

剪貼一些官方敘述和自己的見解

  • “around” filters are responsible for running their associated actions by yielding, similar to how Rack middlewares work.

    around_action 可以像 Rack middlewares 層一樣處理執行 action 前或後的流程, 在 around_action block 裡面用 yield 執行 action

  • Note that an “around” filter also wraps rendering.

    around_action 會包含 rendering, 也就是說即使 action call render 也會把剩下的程式碼執行完畢
    也就是說 yield 之前的 code 都像是 before_action, yield 之後的 code 都像是 after_action

一些執行 filter 的方式

常見 filter 執行方式

1
2
3
4
5
before_action :method_in_controller

def method_in_controller
puts 'Run filter'
end

直接執行 filter 的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
before_action do |controller|
controller.send(:method_in_controller)
end
before_action { |controller| controller.send(:method_in_controller) }

around_action do |controller, action|
controller.send(:method_in_controller)
action.call
end

def method_in_controller
puts 'Run filter'
end

注意因為 filter 執行的 scope 不是在 controller 裡面, 所以要用傳進來的 controller 變數 send controller 的方法

不過除非要處理的事情真的極其簡單, 不然不建議這種寫法

注意 block argument

要注意一下 call method 方式的參數, action 前面要加&代表他是個 block, 比較兩個差異

1
2
3
4
5
6
7
8
9
10
11
around_action do |controller, action|
controller.send(:method_in_controller)
action.call
end

around_action :other_method_in_controller

def other_method_in_controller(&action)
method_in_controller
action.call # same as yield
end

實作範例 & 執行順序

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
class ApplicationController < ActionController::API
before_action { puts "Calling before_action 1" }
before_action { puts "Calling before_action 2" }

around_action do |controller, block|
puts "Calling around_action 1 - before yield"
block.call
puts "Calling around_action 1 - after yield"
end
around_action do |controller, block|
puts "Calling around_action 2 - before yield"
block.call
puts "Calling around_action 2 - after yield"
end

after_action { puts "Calling after_action 1" }
after_action { puts "Calling after_action 2" }
end

class PagesController < ApplicationController
def test
puts "Executing action"
head :ok
end
end

# after excute PagesController#test will output below:
# =>
# Calling before_action 1
# Calling before_action 2
# Calling around_action 1 - before yield
# Calling around_action 2 - before yield
# Executing action
# Calling after_action 2
# Calling after_action 1
# Calling around_action 2 - after yield
# Calling around_action 1 - after yield

會發現兩個現象:

  1. 越晚出現的 before_action 越晚執行, 包括 around_action yield 前的程式碼
  2. 越晚出現的 around_action 越早執行, 包括 around_action yield 後的程式碼

繼承的執行順序

現在 PagesController 也自己也有 filters:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class PagesController < ApplicationController
before_action -> { puts "Calling before_action 3" }
after_action -> { puts "Calling after_action 3" }
# ...same
end

# after excute PagesController#test will output below:
# =>
# Calling before_action 1
# Calling before_action 2
# Calling around_action 1 - before yield
# Calling around_action 2 - before yield
# Calling before_action 3
# Executing action
# Calling after_action 3
# Calling after_action 2
# Calling after_action 1
# Calling around_action 2 - after yield
# Calling around_action 1 - after yield

父 class 的東西都先加入排程, 其餘就跟剛剛的現象一樣

prepend filters

比 before_action 更高的權重就是有前綴 prepend, 這個 filter 會先執行, 也有三種:

  • prepend_before_action
  • prepend_after_action
  • prepend_around_action

更新剛剛的 PagesController 來試試看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class PagesController < ApplicationController
prepend_before_action -> { puts "Calling before_action 3" }
prepend_after_action -> { puts "Calling after_action 3" }
# ...same
end

# after excute PagesController#test will output below:
# =>
# Calling before_action 3
# Calling before_action 1
# Calling before_action 2
# Calling around_action 1 - before yield
# Calling around_action 2 - before yield
# Executing action
# Calling after_action 2
# Calling after_action 1
# Calling around_action 2 - after yield
# Calling around_action 1 - after yield
# Calling after_action 3

兩個特性注意:

  1. 有前綴的 filters 順序跟沒有前綴的一樣
  2. 有前綴的 filters before 會更先執行, 反之 after 會更後執行

更新順序

如先前提到的重複寫 filters 會有兩個行為:

  1. 只會執行一次
  2. 最後的會排程會覆蓋掉前面的

所以除了在同一個 controller 可以排執行順序之外也可以利用繼承的特性去排列:

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
class ApplicationController < ActionController::API
before_action :before_1
before_action :before_2
before_action :before_3

private

def before_1
puts "Calling before_action 1"
end

def before_2
puts "Calling before_action 2"
end

def before_3
puts "Calling before_action 3"
end
end

class PagesController < ApplicationController
before_action :before_1
before_action :before_3
# ...same
end

# after excute PagesController#test will output below:
# =>
# Calling before_action 2
# Calling before_action 1
# Calling before_action 3

最後執行 PagesController#test 時, 同名的 filters 會覆蓋掉先前 ApplicationController 同名 filters 的排程

小結

充分的了解 filters 後, 我覺得專案裡還是不要有過多的 filters, 不然有些狀況會顯得很通靈, 而且真的會很難除錯, 要一層一層 class 把 filters 都印出來看, 如果真的要寫這麼多建議寫好測試

再來可以了解到如果程式碼是 filters 有問題, 如果連 action 一開始都 binding 跑步進去的話肯定是 before_action 或者 around_action 有問題, 因為只有這兩個可以中斷 action 執行

參考文章

官方文件 filters
Ordering of Filters in Rails Controllers

[Rails] I18n 語系設定

發表於 2020-05-27 更新於 2020-05-29 分類於 Rails , i18n

記錄一下 I18n 設定和轉換語系的方式, 至少看得懂專案裡 Rails 如何使用 I18n 的也挺重要

config 基本設定

config/application.rb
1
2
3
4
5
6
7
8
9
10
module YourRepo
class Application < Rails::Application
# 設定從哪裡讀取 .yml 翻譯檔
config.i18n.load_path += Dir[Rails.root.join('lib', 'locale', '*.{rb,yml}')]
# 允許哪些語系
config.i18n.available_locales = [:en, :zh]
# 預設語系, 不設定的話 default 是 :en
config.i18n.default_locale = :zh
end
end

locale vs with_locale

locale 和 with_locale 都可以改變當前的語系, locale 還可以拿到當前語系, 先基本了解一下他們的特性:

1
2
3
4
5
6
7
I18n.locale #=> :en

I18n.with_locale(:fr) do
I18n.locale #=> :fr
end

I18n.locale #=> :en

官方的敘述如下:

1
I18n.locale can leak into subsequent requests served by the same thread/process if it is not consistently set in every controller. For example executing I18n.locale = :es in one POST requests will have effects for all later requests to controllers that don't set the locale, but only in that particular thread/process. For that reason, instead of I18n.locale = you can use I18n.with_locale which does not have this leak issue.

意思大概是說, 用 I18n.locale 設定語系的話之後的『一些』 requests 會是該語系, 但執行幾個 requests 之後又會變成 default_locale, 這會延伸一個問題會在主題『利用 cookie 轉換語系紀錄』

利用 URL query + cookie 轉換語系

我們可以在 html 用 link_to 帶上參數去轉換語系, html 如下:

1
2
<%= link_to '中文版', locale_change_path(locale: 'zh') %>
<%= link_to 'English', locale_change_path(locale: 'en') %>

再來在 controller 裡面:

locale_controller.rb
1
2
3
4
5
6
7
8
9
10
class LocaleController < ApplicationController
def change
if params[:locale] && I18n.available_locales.include?( params[:locale].to_sym )
session[:locale] = params[:locale]
end

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

這時候雖然發現成功改變了語系, 但只要重新刷新或導向幾次頁面之後就會發現語系又回到 default_locale 了, 這就是剛剛在主題『locale vs with_locale』最後提到的問題, 簡單的解決方法就做一個 around_action 一直保持語系正確即可, 通常在 application_controller 做:

application_controller.rb
1
2
3
4
5
6
7
8
class ApplicationController < ActionController::Base
around_action :switch_locale

def switch_locale(&action)
locale = session[:locale] || I18n.default_locale
I18n.with_locale(locale, &action)
end
end

利用 URL Domain 切換語系

例如有以下 Domain 並指定指定語系

  • blog.kurthsu.com => en
  • blog.kurthsu.tw => zh-TW
  • blog.kurthsu.jp => jp
application_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ApplicationController < ActionController::Base
around_action :switch_locale

LOCALE_LIST = {
'com' => :en,
'tw' => 'zh-TW',
'jp' => :jp
}

def switch_locale(&action)
locale = parsed_locale_from_tld || I18n.default_locale
I18n.with_locale(locale, &action)
end

def parsed_locale_from_tld
LOCALE_LIST[request.host.split('.').last]
end
end

最後如果是連到中文版的 link_to 改為:

1
<%= link_to '中文版', "blog.kurthsu.tw#{request.env['PATH_INFO']}" %>

不過這感覺要用點財力購買完整系列的 domain, 雖然我想也是可以用 CNAME 做些處理

利用 URL path 切換語系

這邊將利用 routes 切分指定語系並且用 books 當範例

  • blog.kurthsu.com/en/books => en
  • blog.kurthsu.com/zh-TW/books => zh-TW

先設定 default_url_options 在 ApplicationController

application_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
class ApplicationController < ActionController::Base
around_action :switch_locale

def switch_locale(&action)
locale = parsed_locale_from_tld || I18n.default_locale
I18n.with_locale(locale, &action)
end

def default_url_options
{ locale: I18n.locale }
end
end

這邊會先執行 around_action, 如果目前的 I18n.locale 為 en
此時所有的 URLs 路徑都會包含 query => ?locale=en, 例如:

  • blog.kurthsu.com/?locale=en
  • blog.kurthsu.com/books?locale=en

再來更新 routes, 利用網址可以挖洞給 params 的特性把網址變漂亮

routes.rb
1
2
3
4
5
6
7
Rails.application.routes.draw do
root to: 'root_path#index'

scope path: '/:locale' do
resources :books
end
end

網址除了 root_path 成功的變成我們要的:

  • blog.kurthsu.com/?locale=en
  • blog.kurthsu.com/en/books

也可以用 option 的寫法:

routes.rb
1
2
3
4
5
6
7
Rails.application.routes.draw do
root to: 'root_path#index'

scope path: '(:locale)', locale: /en|zh-TW/ do
resources :books
end
end

這樣如果網址像下面這樣缺少 locale 也不會噴 Routing Error, 然後會採用 I18n.default_locale

  • blog.kurthsu.com/books

最後 root_path 也想變漂亮變成:

  • blog.kurthsu.com/en
  • blog.kurthsu.com/zh-TW
    在 root to: 前面新增一個 route
    routes.rb
    1
    2
    3
    4
    5
    6
    7
    8
    Rails.application.routes.draw do
    get '/:locale', to: 'root_path#index'
    root to: 'root_path#index'

    scope path: '(:locale)', locale: /en|zh-TW/ do
    resources :books
    end
    end

但 link_to root_path 回傳的網址依然會是

  • /?locale=en
  • /?locale=zh-TW

最後小提醒這種方式最好一直保持 I18n.locale 的乾淨不要去頻繁的改變它, 想用 helper 產生指定語系的網址直接給參數即可:

  • books_path(locale: ‘en’)
  • books_path(locale: ‘zh-TW’)

小結

還有一些方法, 例如 http header 或者把使用者的語系存在欄位裡面以便登入時可以切換適用於他的語系, 總之可以在範例中 around_action :switch_locale 判斷該用哪個語系即可

以前比較常用『URL query + cookie 轉換語系』這招但官方不建議了, 因為同一個網址複製過去給別人看到的語系可能會不同, 看到的東西也就不同, 『URL path 切換語系』則不會有這個問題, 而且簡單又沒什麼成本, 蠻推薦的

I18n 系列:

[Rails] I18n 基本使用] [Rails] I18n default scope]

參考文章

官方文件 Managing the Locale across Requests

12…18

Kurt Hsu

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