Giter VIP home page Giter VIP logo

jhang-jhe-wei.github.io's Introduction

Wells

I am a Ruby on Rails developer, recently living in Taiwan/Taipei I have more than 3 years experience on web development.

Insight

Activity

jhang-jhe-wei.github.io's People

Contributors

jhang-jhe-wei avatar pastleo avatar

Watchers

 avatar

jhang-jhe-wei.github.io's Issues

Pry vs Byebug vs Debugger

Pry

Irb 的替代品,正常使用的方式是先取得 binding 後呼叫 pry

binding.pry

但為了便利性,#pry 被加入進 Object 中,因此目前任何地方都可以被呼叫。

Pry 不是 debugger 工具,它僅是 interactive shell (如同 IRB),因此無法控制接下來的程式運作流程,如單步執行等等。

Byebug

這是真正的 debugger 工具,與 Pry 相比,它能夠使用 Step 運作下一行程式碼,其預設的 interactive shell 是 IRB。

pry-byebug

由於 byebug 預設使用 irb,因此這個套件就是把 byebug 的 interactive 換成 pry。

debugger

這是一個在 Ruby 2.0+ 出現的 gem,據說已經掛掉2年了

但進行在 Ruby 3.1 中出現了新的 gem debug,這是整合過去 debugger 而成的 gem

References

GitBook使用教學

料理食材(內文內容)

  • Gitbook的簡介
  • node、nodejs、npm、nvm的差別
  • 安裝Gitbook
  • 使用Gitbook

誰可以安心食用(適合誰讀)

  • 知道Markdown語法
  • 會操作terminal
  • 你的電腦是Mac

服用完你會獲得什麼

  • 知道Gitbook是什麼
  • 知道如何安裝Gitbook(超重要)
  • 知道如何使用Gitbook

Gitbook的簡介

GitBook是一個非常好用文件管理器,以框架的文件來說,需要的僅有目錄和內容,而Gitbook所產生出的靜態網頁則十分符合這種簡潔的需求,它的呈現就像以下:

image

左邊有一個side bar放文章目錄,中間則是內文

作為一個呈現文件資訊的網頁,這樣子頁面就很足夠了。

安裝Gitbook

  1. 安裝nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.2/install.sh | bash
  1. 使用nvm安裝node v10.23.3(一定要用v10.23.3,不然gitbook會裝不了)
nvm install 10.23.0
  1. 安裝gitbook-cli
npm install -g gitbook-cli

node、nodejs、nvm、npm的差別

這幾個是我當初在安裝nodejs時,常常會搞混的一些東西,因此放在這裡記錄。

  • node v.s nodejs

這兩個是相同的東西,用Ubuntu使用sudo apt-get install -y nodejs安裝node,而MacOSXhomebrew則是使用brew install node,在踩了幾次坑後,我一律都用nvm來下載node,確保不會有名稱錯誤的問題。

  • nvm

nvm是一個管理node版本的工具,就如同rvm是管理ruby版本的工具,要下載特定版本時可以使用nvm install 14或是nvm install node安裝最新版,安裝後可以使用nvm use 14進行版本切換。

  • npm

網路上有許多經由node製作的包,而npm就是用來管理這些包的工具,類似於RubyGem用為管理Ruby的包,如果要安裝特定的包可以使用npm install [email protected],不加後面的@2.3.0就會安裝最新版的包。

建立Gitbook專案

首先先建立一個空的資料夾
mkdir mybook && cd mybook

重開terminal後可能導致你node版本變為預設的最新版,使用nvm use 10.23.3進行切換

  1. 執行gitbook初始化
gitbook init

如果執行正確,你會看到以下訊息:
image

  1. 啟動gitbook服務
gitbook serve

之後瀏覽http://127.0.0.1:4000就會看到以下畫面:
image

完成到這一步代表你的gitbook安裝正確。

編寫Gitbook專案

在你的專案中目前會有三個檔案

  1. README.md
  2. SUMMARY.md
  3. _book/

接著我們打開SUMMARY.md,可以發現裡面含有以下的內容:

# Summary

* [Introduction](README.md)

簡單來說,這是一個表達目錄的檔案,接著我們為其新增一個章節,在該檔案 內加入一行* [Test]](Test.md)

之後再專案中建立一個檔案Test.md,並在其中填入以下內容:

# Test
Hello, World!

完成後開啟gitbook服務gitbook serve,瀏覽器開啟http://127.0.0.1:4000,如果已開啟的話按下Shift + CMD + R整理網頁即可看到變化。

你就可以看到以下畫面:

image

如果你希望TestIntroduction的其中一個章節,你也可以將SUMMARY.md更改為以下:

# Summary

* [Introduction](README.md)
    * [Test](Test.md)

這樣子目錄就會變成這樣:

image

當文件編寫完成後,可以考慮部署至Github Pages,關於如何部署可以參考這裡

加入插件

Gitbook的預設功能雖然已經夠用了,但人類總是貪婪的,而網路上有許多善心人士為了滿足人類的貪婪,於是就做了幾種邪惡的套件,讓用過的人再也回不去單純的Gitbook,如何使用還請繼續看:

  1. 在gitbook專案的根目錄中新增book.json這個檔案
  2. 填入以下內容
    {
    "plugins": [
        "-search", "search-pro","back-to-top-button","chapter-fold","copy-code-button","advanced-emoji","splitter"
        ]
    
    }
    
  3. 至該目錄執行gitbook install,安裝gitbook插件
  4. 開啟gitbook服務gitbook serve
    之後你就可以看到加入插件後的gitbook了

善心人士提供的實用gitbook plug大全

結語

Gitbook是一個寫文件非常好用的利器,近期我在與卡米哥一同編寫kamigo框架的文件時,也是決定使用GitBook作為編寫文件的工具,雖然它十分容易上手,但卻非常難安裝,多虧了網路上許多tral and error的苦主,最終才確定node v10.23.3可以正常安裝,真是感謝!

Ruby 物件導向程式實踐心得

物件導向程式設計

  1. 設計的本質為何?
    1. 設計的本質是為了應付未來的變化
  2. 為何選擇物件導向程式語言?
    1. 物件導向程式的信奉者相信藉由程式描述現實中的物件(Object)與其之間的訊息(Message),可以有效的管理程式碼的依賴關係,從而應付未來的變化
  3. 何時該做設計?
    1. 取決於當下的能力和時間表並做出馬上能得到效益(享受到易於修改)的設計
  4. 如何評估設計的好壞
    1. 透明性:程式碼的修改結果是顯而易見,容易了解有哪些地方會受到影響
    2. 合理性:修改的效益與成本
    3. 可用性:re-usable,不依賴於上下文
    4. 典範性:作為典範讓他人擴充
  5. 設計失敗的原因
    1. 不知道怎麼設計
    2. 一知半解的套用各種 Design pattern,導致設計和實作完全分離時(改不動了)

單一職責的類別

  1. 為何單一職責是屬於物件導向語言的 Principle?
    1. 物件導向語言和程序性語言最大的不同在於資料和行為之間息息相關,為此我們會將相關的資料和行為放在同一個類別中,提高「內聚」。
  2. 為何要追求單一職責?
    1. 一個類別知道得越多,代表依賴的越多,那它就越難重複使用,因為很容易牽一髮動全身,遵守單一職責的類別容易修改且不用擔心後果
  3. 如何判斷物件是否遵守單一職責?
    1. 「高內聚」,這個物件所做的所有事跟與其目標非常相關

    2. 藉由把類別當成一個人來提問

      想像有一個書店 BookStore 類別,並向他提問……

      書店先生,請問你有賣 Ruby 物件導向程式實踐 這本書嗎?
      : 有

      書店先生,請問你的推薦的書籍?
      : Ruby 物件導向程式實踐

      書店先生,請問你有提供什麼食物?
      : ???

  4. 無法切分準確的職責?
    1. 等待更多的資訊,別畫蛇添足

撰寫擁抱變化的程式碼(For Ruby)

  1. 依賴於行為而非資料

    # bad
    puts @var
    -----
    # good
    attr_reader :var
    puts var
  2. 隱藏資料結構

    # bad
    book = data # array
    book[0] # Name
    book[1] # author
    book[2] # publish year
    -----
    # good
    BookModel = Struct.new(:name, :author, :publish_year) <--  data 順序有變時,只需調整這裡
    book = BookModel.new(data)
    book.name
    book.author
    book.publish_year

管理依賴關係

  1. 對於任何訊息,物件會有三種回應方式
    1. 自己本身知道如何回應
    2. 經由繼承回應
    3. 藉由別的物件回應
  2. 依賴重會發生什麼事?
    1. 知道越多,代表依賴越重,當需求出現變化時,修改程式碼的成本會變高
  3. 減少依賴的一些方法?
    1. 使用 keyword argument 取代參數順序

      1. 當 keyword argument 太多的時候,可以用 **arg

        def calculate(**arg)
        	params = default_arg.merge(arg)
        	...
        end
        
        private
        
        def default_arg # 提供參考
        	{
        		unit_price: 0,
        		quantity: 0,
        	}
        end
      2. 遇到不可修改的 class 時,可以用 factory

        class Calculator
        	def initialize(unit_price, quantity, ...) # 改不動
        		...
        	end
        end
        
        module CalculatorWrapper
        	def self.build(hash)
        		Calculator.new(hash[:unit_price], hash[:quantity])
        	end
        end
        
        CalculatorWrapper.build({
        	unit_price: 10,
        	quantity: 10
        })
    2. 依賴注入

      1. 使用依賴注入避免相依於具體的類別,因為我們關心的只有這個物件能不能回應某個行為

      2. 依賴注入可以延伸到另一個觀念「反轉依賴」,導致依賴的方向是可以自由決定的,我們可以得知幾個原則:

        1. 依賴於不容易變化的類別
        2. 具體比抽象更容易變化
        3. 減少依賴將能夠降低修改的成本
        # scenario: 記者報導商家
        # V1
        class Reporter # 永久不變
        	def report(store)
              ...
        	end
        end
        
        reporter.publish(store)
        
        # V2
        class Store # 每天都在變的,method
        	def is_reported_by(reporter)
              ...
        	end
        end
        store.is_reported_by(reporter)
        
        # 假如 Reporter 比 Store 更不易變化(method name 不會變來變去)
        # 該選擇 V2

建立靈活的介面

  1. Domain model 很容易發現,例如:使用者、商家,但之間傳遞的訊息才是構成整個 Application 的核心,而訊息藉由介面在物件中傳遞
    1. UberEat 和 CYBERBIZ 都有 shop 和 customer 類別,但這兩者傳遞的訊息卻不同
  2. 可以藉由順序圖了解訊息的傳遞,可以發現隱藏的物件和設計公共介面
  3. 用「要什麼」取代「如何要」,專注於結果,並且可以使用依賴注入來達成上下文獨立
  4. 當出現 chaining method 時很可能違反「 迪米特法則」,「 迪米特法則」是一套能帶來鬆耦合的守則,表象解是用 delegation,但本質上應該是目前程式碼處於「如何要」的狀態

使用鴨子類型技巧降低成本

  1. 當出現「我知道你是誰,並且我知道你能做什麼」時,通常都可以用鴨子型別變成「我知道你能做什麼」
  2. 當出現以下架構時,通常可以使用鴨子型別降低修改成本
    1. case…when
    2. is_a?
  3. 具體容易釐清但是修改成本高,抽象難懂但是可以輕易擴充
    1. 具體 → prepare_food, prepare_cars
    2. 抽象 → prepare
  4. 基本資料類別也能建立新的抽象方法,這稱為「Monkey patch」

藉由繼承取得行為

  1. 提升抽象而並非下放具體,白話文來說應把子類別的程式碼抽取共同的部分放到父類別

    class Bike
    	...
    	def common_method
    	end
    end
    
    # 新增 MountainBike
    class MountainBike < Bike
    	def specific_method
    		...
    	end
    end
    
    class RoadBike < Bike
    end
    
    # code 該怎麼移動?
  2. 可以使用 raise NotImplementedError 建立 Template 類別

  3. 使用 hook 使子類別和父類別解耦,避免使用 super

    class A
    	def initialize(arg)
    		@name = arg[:name]
    		@age = arg[:arg]
    	end
    end
    
    class B < A
    	def initialize(arg)
    		@weight = arg[:weight]
    		super(arg) # 少這一步就會炸掉
    	end
    end
    
    # V2
    class A
    	def initialize(arg)
    		@name = arg[:name]
    		@age = arg[:arg]
    		local_assign(arg)
    	end
    
    	private
    	def local_assign(arg)
    	end
    end
    
    class B < A
    	def local_assign(arg)
    		@weight = arg[:weight]
    	end
    end

使用模組共用角色行為

  1. 里氏替換原則的進一步解釋,包含何時該用繼承
    1. 繼承條件嚴苛到根本沒有應用的機會

組合物件

  1. 組合(composition)和聚合(aggregate)的差別

    1. 組合代表的是什麼含有什麼
    2. 聚合是一種特殊的組合,被含有的東西含有其自己的獨立生命
  2. 組合、模組和繼承的比較

    1. 繼承:代表的是「is-a」,自動委派(delegation)訊息是繼承最大的特點,若使用得宜,在程式碼的可用性、合理性和典範性的表現上都很突出,因為繼承導致非常強的依賴關係,在大部分的情況下應首先選擇組合。
    2. 模組:代表的是「behavior likes a」,專注的是角色共用的這件事,如:可調度、可列印等等的角色行為
    3. 組合:代表的是「has-a」顧名思義,組合就是將不同功能的小物件組合起來,藉由定義相同的介面,可以自由的抽換其中的小物件,適應各種不同的變化
  3. aggregate 會承擔其 entities 的職責給外部使用, delegrate for enetities ( 自己補充

    1. 跟蒼時的討論的筆記
    2. tree vs graph
    order.update_amount(product_id, amount)
    
    class Order
    	has_many :items
    	def update_amount(product_id, amount)
    		items.select { |i| i.pid == product_id }.update_amount(amount)
    	end
    end
    
    class Item
    	def update_amount
    		...
    	end
    end

設計節省成本的測試

  1. A 物件的輸出等於 B 物件的輸入,A 物件的測試只需測到 A 物件的輸出即可
    1. 避免測到重複的東西
  2. 測試的輸出分為兩種,查詢和命令
    1. 查詢是資料
    2. 命令是 call method
  3. 減少重複或重疊的測試
  4. 測試之所以難寫,很大程度跟主程式的寫法有關係,如依賴注入與替身的關係
  5. BDD 和 TDD 都是先寫測試再實作,差別在於著重點:
    1. BDD: 從外而內
    2. TDD: 從內而外
  6. 使用 shared_test 共用角色行為
  7. 永遠不需要測試私有方法,若私有方法過於龐大,可以考慮抽取成一個新的物件,並增加該物件的測試
  8. 測試繼承時,若父類別為抽象,可以建立一個專用於測試的子類別
  9. 避免測試與主程式脫離,如某物件的 method 已經更名了,但由於 test 中是用 Stub 導致測試依然通過,此時如果該物件已有 expect_response_to(:method) 就可以即時發現原因
  10. 先寫測試的好處在於可以強迫自己使用可重複使用性的程式碼
  11. 如果建立真物件不會太麻煩的話,可以考慮直接注入真物件,而並非建立一個偽物件

Clean Architecture Part3 - Design Principle

前導

  • 這裡介紹的 SOLID 是應對 mid-level (module, component)
    • 將遵從 SOILD 的 modules 想成是一塊塊良好的磚頭,即使如此還是有可能建出混亂的建築

SRP

  • 「每個 function 只做一件事」不是 SRP 強調的

  • 每個 module 只為一個,而且只有一個角色而變化

    • 何謂 module?
      • 可能是同一個程式碼檔案
      • 內聚(cohesive)強制綁定程式碼與單一角色
    • 何謂角色?
      • 指的是同一個群體
  • 經由一些症狀察覺違反 SRP

    • 意外重複
      • 不同的角色使用相同的演算法
    • Merge
      • 會有 Merge 產生,就代表有多人修改同一個檔案,這有可能是因為有不同的角色都使用該檔案
  • 要避免違反 SRP 可以依不同角色行為拆分成單獨的 classes

    例如 Employee 中含有 save , calculate_pay , report_hours ,他們都是給不同角色使用

    • 此時可以將這三個 method 拆成不同的 classes,他們都會依賴於 EmployeeData
      • 但要如何追蹤並且讓別人知道這些 classes 是有關連的
        • 使用 Facade Pattern
        • 由 Facade class 進行 delegating 和 instantiating
    • 以 Rails 來說,是比較偏向將 bussiness rule 放在 model 中,也就是 model 包含 Data 和最重要的 functions,那麼可以將這個 model 當成 Facade class
    • 如何避免一個 class 只有一個 function?
      • 一個 class 只有一個「公共」function 是可能的,其中還會有多個 private function 由 public function 使用
  • SRP 原本是關於 functions 和 classes,但此概念也可以出現在以下兩個層級

OCP

  • 系統的行為應是擴充而非修改,因修改容易導致錯誤
  • 大部分的人覺得這個 Principle 是對於 module 或 class 的設計規範,但他也可以用於系統層面
  • 一個好的架構在實作新需求時,應盡可能將舊扣的變更降低至 0
    • 如何做到?
      • 遵從 SRP,讓事情的變動原因變得單一
      • 管理好依賴關係
        • 確保單向依賴
        • 最終會得出一個 Root,以 Rails 來說應是其中一個 Model
        • 依賴於 interface 而非具體物件(靜態語言)
interface Presenter {
	public void present();
}

class Controller {
  void call(Presenter presenter){}
}

class ScreenPresenter implements Presenter {
  public void present(){}
}

LSP

  • 主要用來規範繼承,但也可以用來評估 interface 和 implementation
  • 只要容忍些微的違反,就會導致整個系統充滿額外的判斷機制

ISP

  • 當使用者直接使用 object 的 method,並且各個使用者皆使用不完全相同的 methods 時,這些 methods 會產生無可避免地相依,這些相依會讓整個 class rebuild 或是 redeploy
    • 這比較常出現在靜態語言,因為其包含了 importinclude 等等的宣告
    • 靜態語言的解法是讓使用者依賴 interface,而 object 則是實做這些 interface
    • 但動態語言沒有這個問題
  • 當依賴於一個 module 其擁有太多你不需要的 functions 時,就容易受到牽連
  • 在 component cohesion 裡會有更詳細的介紹

範例:由於 User1 ~3 需要 import Op 導致就算只變更 Op 裡其中一個 method,也會讓 User1 ~ 3 重新編譯

class Op {
    public void op1() {};
    public void op2() {};
    public void op3() {};
}

class User1 {
    public void doSomething(Op arg) { arg.op1(); }
}

class User2 {
    public void doSomething(Op arg) { arg.op2(); }
}

class User3 {
    public void doSomething(Op arg) { arg.op3(); }
}

若是使用 interface 就不會有此問題

class Op implements Op1, Op2, Op3 {};
interface Op1 {
    public void op1();
}
interface Op2 {
    public void op2();
}
interface Op2 {
    public void op2();
}
class User1 {
    public void doSomething(Op1 arg) { arg.op1(); }
}

class User2 {
    public void doSomething(Op1 arg) { arg.op2(); }
}

class User3 {
    public void doSomething(Op1 arg) { arg.op3(); }
}

DIP

  • 依賴於抽象而非具體
  • 抽象比具體更穩定

Ruby 搭配 Sketchup 學習筆記(五)

前情提要

Sketchup 是一款在建築、都市計畫和遊戲開發都頗有名氣的 3D 建模軟體,而 Ruby 則是一個程式語言,它可以搭配 Sketchup 達成程式化 建模的任務,近期經由系主任引薦,要開發 Sketchup 的 Extension,雖然我寫過 Ruby,但 Sketchup 則是完全沒碰過,於是利用文章來記錄所學的一點一滴。

本篇內容

  • 圖層 Layer
  • 群組 Group
  • 元件 Components
  • 材料 Meterial
  • 圖片

圖層 Layer

圖層(Layer)用來顯示或隱藏特定的圖形,以減少在作業中的干擾,在2021版的 Sketchup 被稱為標記(Tag),它位於視窗的工具列:
image

圖層是屬於 Model 類別之下,以下示範如何建立一個圖層:

# Default code, use or delete...
mod = Sketchup.active_model # Open model
layers = mod.layers # All entities in model

new_layer = layers.add "測試用圖層"
mod.active_layer = new_layer

輸出結果:

image
這邊可以看到有兩個圖層,第一個是預設的,第二個則是我們剛剛新增的。

另外圖層也可以使用以下方法:

  • visible=
  • visible?
  • name=
  • name

群組 Group

假如我們在 Sketchup 中做一個圓柱體:

# Default code, use or delete...
mod = Sketchup.active_model # Open model
ent = mod.entities

circle = ent.add_circle [0, 0, 0], [0, 0, 1], 5
circle_face = ent.add_face circle
circle_face.pushpull -10

輸出結果:

image
對我們來說,這是一整個物件,但對 Sketchup 來說並不是,因為在上方的程式碼中的 circle_face 僅是一個平面,而使用平台拉抬而成的部分並屬於同一個物件,如果使用轉形,就只有表面移動,因此假如要移動整個圓柱體,可以使用 circle_face.all_connected ,不過每次都要這麼打,似乎有點麻煩,因此我們可以使用群組功能將整個圓柱體定義為一個群組:

# Default code, use or delete...
mod = Sketchup.active_model # Open model
ent = mod.entities

circle = ent.add_circle [0, 0, 0], [0, 0, 1], 5
circle_face = ent.add_face circle
circle_face.pushpull -10

circle_group = ent.add_group circle_face.all_connected

t = Geom::Transformation.translation [0, 10, 0]
ent.transform_entities t, circle_group

輸出結果:

image

群組提供了幾個方法:

  • name=
  • description=
  • locked=:是否鎖定,鎖定後不能進行修改
  • explode:將 Group 物件分解
  • deleted?:是否被刪除

群組的特色

將一個或多個物體定義為群組後,可以使用幾個方便的功能對群組整體進行操作,功能如下:

  • copy:複製
  • move!:移動到特定位置
  • transform!:轉形

元件 Components

元件與群組的最大差別在於元件可以跨不同的檔案,而群組只能在其建立的那個檔案。元件有點像 Class 經由定義完成後儲存成 .skp 檔案,要用到的時候在引入之後進行實體化(instance)

建立元件

建立元件有兩種方式。

群組產生元件

# Default code, use or delete...
mod = Sketchup.active_model # Open model
ent = mod.entities

circle = ent.add_circle [0, 0, 0], [0, 0, 1], 5
circle_face = ent.add_face circle
circle_face.pushpull -10

circle_group = ent.add_group circle_face.all_connected

circle_group.to_component

輸出結果:

image
在上方工具列的視窗中可以查看元件,點擊後可以發現此群組已經變為元件了。

使用 DefinitionList 建立元件

DefinitionList 是 active_model 的其中一個方法,詳細做法看以下範例:

# Default code, use or delete...
mod = Sketchup.active_model # Open model
def_list = mod.definitions # 取得 DefinitionList

# 新增一個文件定義
new_def = def_list.add "測試元件"
new_def.description = "這是一個測試元件"

# 只用文件定義的 Entities 建立一個圓柱體
ent = new_def.entities
circle = ent.add_circle [0, 0, 0], [0, 0, 1], 5
circle_face = ent.add_face circle
circle_face.pushpull -10

# 儲存元件定義
save_path = Sketchup.find_support_file "components", ""
new_def.save_as(save_path+"/test.skp")

# Check
puts "已儲存到#{new_def.path}"

image

引入元件

開新的 Sketchup 專案,在 Ruby Console 中輸入以下內容:

def_list = Sketchup.active_model.definitions
file_path = Sketchup.find_support_file "test.skp", "Components"
load_def = def_list.load file_path

這樣子就可以引入該元件了

元件實體化

這邊來示範如何使用元件進行實體化

def_list = Sketchup.active_model.definitions
file_path = Sketchup.find_support_file "test.skp", "Components"
load_def = def_list.load file_path

ent = Sketchup.active_model.entities
t = Geom::Transformation.translation [0,0,0]
ent.add_instance load_def, t

使用元件實體化後的物件,假如元件被修改,該物件也會產生變化,如果要避免此情況可以使用 make_unique,元件還有幾個方法如下:

  • locked
  • name=
  • name
  • explode

材料

Sketchup 的材料並不具備物理資訊,其只負責呈現不同外觀,以下示範如何新增、使用材料:

新增材料

mod = Sketchup.active_model
mats = mod.materials
new_mat = mats.add "新材料"

使用材料

# Default code, use or delete...
mod = Sketchup.active_model # Open model
ent = mod.entities
mats = mod.materials

new_mat = mats.add "新材料"

circle = ent.add_circle [0, 0, 0], [0, 0, 1], 5
circle_face = ent.add_face circle

circle_face.material = new_mat

materials 除了 add 外,還有其他方法:

  • current
  • current=
  • name
  • displat_name
  • materialType

為材料新增顏色

# Default code, use or delete...
mod = Sketchup.active_model # Open model
ent = mod.entities
mats = mod.materials

new_mat = mats.add "新材料"
new_mat.color = [255,0,0]

circle = ent.add_circle [0, 0, 0], [0, 0, 1], 5
circle_face = ent.add_face circle

circle_face.material = new_mat

輸出結果:

image
請注意!其會為表面塗色,但表面取決於向量。

為材料新增材質

材質與顏色類似,唯一不同點在於材質需要圖檔,看以下範例程式碼:

# Default code, use or delete...
mod = Sketchup.active_model # Open model
mats = mod.materials
ent = mod.entities

new_mat = mats.add "新材料"
texture_path = Sketchup.find_support_file "images/wood.jpeg", "Plugins"
new_mat.texture = texture_path
new_mat.color = [255,0,0]

circle = ent.add_circle [0, 0, 0], [0, 0, 1], 5
circle_face = ent.add_face circle

circle_face.material = new_mat

輸出結果:

image
順帶一提,為材料新增材質的同時也可以新增顏色。

也可以將新增變更顏色的材質進行儲存:

mod = Sketchup.active_model # Open model
mats = mod.materials
ent = mod.entities

new_mat = mats.add "新材料"
new_mat.texture = Sketchup.find_support_file "images/wood.jpeg", "Plugins"
new_mat.color = [255,0,0]

circle = ent.add_circle [0, 0, 0], [0, 0, 1], 5
circle_face = ent.add_face circle
circle_face.material = new_mat

twriter = Sketchup.create_texture_writer
twriter.write circle_face, false, "texture.jpg"

twriter.write 第二個參數決定正面還是背面,目前是背面

圖片

以下範例程式碼示範如何展示圖片:

ent = Sketchup.active_model.entities
path = Sketchup.find_support_file "images/carton.jpeg", "Plugins"
img = ent.add_image path, [0, 0, 0], 100

t = Geom::Transformation.rotation [0,0,0],[1,0,0],90.degrees
ent.transform_entities t,img

輸出結果:

image

參考資料

Rails 實作 SQL Injection

本篇內容

  • 介紹
  • 入侵實作
  • 修補方式

介紹

SQL(結構化查詢語言)用於管理資料庫,由於其無法區分值和控制指令, 因此惡意人士可以藉由在值中安插控制指令從而對資料庫進行惡意操作,這種操作稱為 SQL Injection。

入侵實作

本範例使用 Ruby on Rails 6 進行實作。

1. 建立新檔案

使用 Rails 指令新增專案:

rails new demo
cd demo

2. 更改資料庫為 PostgreSQL

更改 Gemfile 中的內容:

- gem 'sqlite3', '~> 1.4'
+ gem 'pg'

複製以下內容,取代 config/database.yml

default: &default
  adapter: postgresql
  encoding: unicode


development:
  <<: *default
  database: development

新增 PostgreSQL 資料庫:

createdb development

3. 產生貼文功能

使用 Rails 指令新增貼文功能:

rails g scaffold post title content
rails db:migrate

並且在 config/routes.rb 中新增以下內容:

root to: "posts#index"

4. 新增搜尋功能

app/views/posts/index.html.erb h1 之下新增以下內容:

<%= form_tag(:posts, method: :get) do%>
  <%= label_tag(:search)%>
  <%= text_field_tag(:search, params[:search])%>
  <%= submit_tag("search")%>
<% end%>

接著把 app/controllers/posts_controller.rb 中 index 的部分替換為以下內容:

 def index
    if(params[:search])
      sql = "SELECT posts.* FROM posts WHERE (posts.title LIKE '%#{params[:search]}');"
      result = ActiveRecord::Base.connection.execute(sql)
      @posts = result.map { |p| OpenStruct.new p }
    else
      @posts = Post.all
    end
end

之所以不能使用Post.where()是因為 Rails 有防止 SQL Injection 的機制,就算使用 Post.where("id = #{params[id]}") 依然會被擋下來。

5. 進行 SQL Injection

此時執行 rails s,之後在瀏覽器輸入 localhost:3000 新增幾個 post 後,就可以看到類似以下的畫面:
image

之後我們在搜尋框中輸入 apple');DELETE FROM posts; --
image

送出後查看log:
image

可以發現它最終組出了以下的 SQL 指令:

SELECT posts.* FROM posts WHERE (posts.title LIKE '%apple');DELETE FROM posts; --');

這段指令可以分為三段:

  1. SELECT posts.* FROM posts WHERE (posts.title LIKE '%apple');
  2. DELETE FROM posts;
  3. --');

第一段是正常執行的指令,只不過因為我們在搜尋框中填入了 apple'); 導致第一段程式碼提前中斷,而第二段就是惡意指令,使我們 posts 這個 table 的資料全部被刪除,而最後一段 -- 則是註解,可以讓其之後的指令皆不被執行。

修補方式

其實 Rails 的防止 SQL Injection 機制很完善,雖然並不保證百分之百不會被攻擊,通常 SQL Ingection 都發生在需要搭配變數的情況,常見的做法就是將所有會影響 SQL 的控制字元進行轉義,以下示範:

-   sql = "SELECT posts.* FROM posts WHERE (posts.title LIKE '%#{params[:search]}');"
+   keyword = ActiveRecord::Base::connection.quote_string(params[:search])
+   sql = "SELECT posts.* FROM posts WHERE (posts.title LIKE '%#{keyword}');"

result = ActiveRecord::Base.connection.execute(sql)
@posts = result.map { |p| OpenStruct.new p }

這會使原本是 "apple');DELETE FROM posts; --" 的文字變成 "apple'');DELETE FROM posts; --" 從而避免 SQL Injection。

另一種更方便的就是用 Array 或是 Hash 傳入:

Post.where("title = ?", "test")
Post.where(title: "test")

這樣子也是有效防止 SQL Injection 的方式。

但就是不要使用以下的做法:

Post.where("title = #{var}")

因為 只要執行 var = "' OR 1='1" 後,上面的程式碼會被轉換成以下的 SQL 指令:

SELECT "posts".* FROM "posts" WHERE (title = '' OR 1='1')

這樣子駭客就能取得所有的資料。

但萬幸的是無法用 ; 注入第二段指令,例如 var = "');DELETE FROM posts;--" 後再執行 Post.where("title = #{var}"),就會產生:

SELECT "posts".* FROM "posts" WHERE (title = '');DELETE FROM posts;--')

雖然會以上的指令有害,但真正運行時會遇到以下此錯誤:

ActiveRecord::StatementInvalid (PG::SyntaxError: ERROR:  cannot insert multiple commands into a prepared statement)

參考資料

我不懂 UI

Text

  1. 大字配小行高 line-height (1.2x),小字配大行高(1.4x)
  2. letter space change to -1 ~ 2% for headings, but it depends on font

Align

  1. don't mix center aligned heading and left aligned body text

text line width

  1. don't puts too much words in one line
    image

Hierarchy

  1. don't use more 2 text size, it can be replace with different subtitle color or font weigth

Space

  1. The height of Space depends on the relationship. for example, heading and its body text is closer than other section

image

https://www.youtube.com/watch?v=88XxC0_zs74

Rails 搭配 Omniauth line

本篇內容

  • 前置作業
  • 安裝相關套件
  • 生成 User Model
  • 設定 Line Login
  • 新增身份驗證
  • 實作登入登出

前置作業

1. 建立新檔案

使用 Rails 指令新增專案:

rails new demo
cd demo

2. 產生空白頁

使用 Rails 指令新增 Controller:

rails g controller home

之後新增 app/views/home/index.html.erb

最後在 config/routes.rb 中新增以下內容:

root to: "home#index"

安裝相關套件

Gemfile 中加入以下內容:

gem 'devise'
gem 'omniauth-line', git: 'https://github.com/etrex/omniauth-line.git'
gem "dotenv-rails", "~> 2.7"

加入後執行 bundle

生成 User Model

請執行以下指令:

rails g devise:install
rails g devise user
rails g migration add_line_login

開啟剛剛新增的 db/migrate/xxxxxxxxx_add_line_login.rb 並在 change 方法中加入以下內容:

add_column :users, :line_id, :string
add_column :users, :name, :string
add_column :users, :image_url, :string

另外,由於我們之後要以 line_id 區分使用者,這導致 email column 的值可能會重複(因為 Devise 預設 email 的值為 '' ),因此需要解決 email 重複的問題,以下新增一個 migration:

rails g migration resolve_users_email_unique

開啟新增的 db/migrate/xxxxxxxxx_resolve_users_email_unique.rb 更改為以下內容:

def up
  change_column :users, :email, :string, null: true, default: nil
end
def down
  change_column_null :users, :email, false, SecureRandom.uuid
end

此段主要為修改 email column,當 user 被建立時,預設值為 nil,以此來避免唯一鍵重複,而 down 中的 SecureRandom.uuid 則是為了避免原本 emailnil 的資料 db rollback 後發生重複的問題。
到了這一步後就可以執行 Database migrate:

rails db:migrate

設定 Line Login

新增 .env 並在其中放入以下內容:

LINE_LOGIN_CHANNEL_ID= 你的 CHANNEL ID
LINE_LOGIN_CHANNEL_SECRET= 你的 CHANNEL SECRET

該資訊可以在 Line Developers 找到,點擊 LINE LOGIN 的 CHANNEL,可以看到以下畫面:
image

LINE LOGIN CHANNEL ID,複製該內容貼至 .env

image

LINE LOGIN CHANNEL SECRET,複製該內容貼至 .env(與 LINE LOGIN CHANNEL ID 同頁面的下方)

image

在 Callback URL 部分填入 https://{your-domain-name}/users/auth/line/callback

config/initializers/devise.rbDevise.setup 區塊中新增以下內容:

config.omniauth :line, ENV['LINE_LOGIN_CHANNEL_ID'], ENV['LINE_LOGIN_CHANNEL_SECRET']

app/models/user.rb 開啟 Omniauthable 功能

devise :database_authenticatable, :registerable,
-        :recoverable, :rememberable, :validatable
+        :recoverable, :rememberable, :validatable,
+       :omniauthable, omniauth_providers: [:line]

app/models/user.rb 新增 from_omniauth 方法

def self.from_omniauth(auth)
    if auth.provider == "line"
        user = User.find_or_create_by(line_id: auth.uid)
        user.update(name: auth.info.name, image_url: auth.info.image)
        user
    end
end

因為 LINE Login 只會傳入 line_id 而沒有 emailpassword,因此 emailpassword 為非必填,在 app/models/user.rb 中加入以下內容:

def email_required?
    false
end

def password_required?
    false
end

新增 omniauth controller

rails g controller OmniauthCallbacks

並在其中填入以下內容:

def line
    user = User.from_omniauth(request.env["omniauth.auth"] )
    sign_in user
    redirect_to root_path
end

config/routes.rb 中加入以下內容:

-   devise_for :users
+   devise_for :users, controllers: {
+        omniauth_callbacks: 'omniauth_callbacks'
+    }

新增身份驗證

app/controllers/application_controller.rb 中加入以下內容:

include Rails.application.routes.url_helpers

def authenticate_user
    return if current_user.present?
    redirect_to user_line_omniauth_authorize_path
end

之後只需要在先登入才能進行動作的 Controller 中加入以下內容:

before_action :authenticate_user

Devise 預設是 before_action :authenticate_user!

image

實作登入登出

完成至上一步已經可以進行 Line Login,但假如希望登入登出這件事並非強制的話,可以增加登入登出的列表。

app/views/layouts/application.html.erb<body> ... </body> 之中加入以下內容:

 <nav>
    <h1>
    <% if current_user.present? %>
        <%= current_user.name %> 您好:
        <%= link_to "登出", destroy_user_session_path, method: :delete  %>
    <% else %>
        <%= link_to "登入", user_line_omniauth_authorize_path %>
    <% end %>
    <h1>
    <hr />
</nav>

這樣子就可以在未登入時可以登入:
image

已登入時可以登出:
image

參考資料

Rails使用Assets Pipeline管理靜態檔案

簡介

Rails專案中可能會有許多靜態的檔案,例如:JavaScript、Stylesheets和圖檔,把所有的靜態檔案都放在public目錄或許是個選擇,但是檔案一多的時候,就不好管理了。

因此為了便於管理這些檔案,Rails提供以下兩種方式:

  1. Webpacker
  2. Assets Pipeline

雖然從Rails 6後預設使用Webpacker來處理Javascript和Asset Pipeline處理CSS,但還是能使用Asset Pipeline管理全部的靜態檔案。

前陣子為了套上模板頁面,因而接觸到Asset Pipeline,所以本篇就來紀錄一下這個過時的Asset Pipeline管理靜態資源的方法吧。

操作步驟

下載模板

本篇文章使用Start Bootstrap-Landing Page作為模板範例,首先請先下載該專案或使用git進行下載

git clone https://github.com/StartBootstrap/startbootstrap-landing-page.git

下載完成後可以進入該資料夾執行npm start,即可瀏覽網頁,如果沒裝npm也沒關係,直接點擊dist/index.html就好

複製所需靜態檔案到專案

接著我們需要把模板的以下內容放到 自己的Rails專案

  • scss/*複製到你的專案/app/assets/stylesheets

*代表所有檔案

  • assets/img/*複製到你的專案/app/assets/images

新增Layout

由於我們下載的是一個首頁的模板,為了不打亂原本的專案,因此我們另外創建一個Layout

  • rails g controller landing
  • 修改landing_controller中的內容
    把此段程式碼刪除
class LandingController < ApplicationController
end

替換成以下

class LandingController < ActionController::Base
    def index
    end
end
  • config/routes中設定根路徑root to: "landing#index"

  • app/views/landing中新增index.html.erb,並且複製模板dist/index.htmlbody部分到index.html.erb

  • app/views/layouts中新增landing.html.erb,複製app/views/layouts/application.html.erb的內容到裡面

  • 把模板dist/index.html中的以下內容全部貼到app/views/layouts/landing.html.erbhead區塊

    <!-- Font Awesome icons (free version)-->
    <script src="https://use.fontawesome.com/releases/v5.15.3/js/all.js" crossorigin="anonymous"><script>
    <!-- Simple line icons-->
    <link href="https://cdnjs.cloudflare.com/ajax/libs/simple-line-icons/2.5.5/csssimple-line-icons.min.css" rel="stylesheet" type="text/css" />
    <!-- Google fonts-->
    <link href="https://fonts.googleapis.com/css?family=Lato:300,400,700,300italic,400italic700italic" rel="stylesheet" type="text/css" />

使用Assets Pipeline

  • 依照下方指示修改app/views/layouts/landing.html.erb
    把此段程式碼刪除
<%= javascript_pack_tag 'application' %>

替換成以下

<%= stylesheet_link_tag 'landing', media: 'all' %>

這一步是讓landing這個layout使用Assets Pipeline而並非Webpacker,並且使用landing.scss作為Stylesheet

  • 複製app/assets/styles.scss的內容到app/assets/landing.scss
  • 刪除app/assets/stylesheets/application.css中的 *= require_tree .

到達這一步後你可以使用rails s來測試看看,你可能會看到以下畫面

修改圖檔URL

會發生這種情況是因為圖片放在assets中Rails會將檔案加密,因而導致找不到圖片,因此我們修改圖檔的路徑,依照不同的檔案屬性,我們也會以不同的方式進行修改。

  1. app/views/layouts/landing.html.erb中,請將background-image: url('assets/img/檔名.jpg')更換為url('<%= asset_path('檔名.jpg')%>')">
  2. app/assets/stylesheets/sections/_call-to-action.scss中,請將url('../assets/img/bg-masthead.jpg')更換為url('<%= asset_path("bg-masthead.jpg") %>'),並且更改檔名為_call-to-action.scss.erb
  3. app/assets/stylesheets/sections/_masthead.scss中,請將url('../assets/img/bg-masthead.jpg')更換為url('<%= asset_path("bg-masthead.jpg") %>'),並且更改檔名為_masthead.scss.erb

之後再次重啟server,一切都會變好了

結語

本篇文章主要是拿來紀錄使用Assets Pipeline的過程,第一次搞的時候是拖累同事一起跟我debug,但是在debug的途中也對於Assets Pipeline漸漸地瞭解了,寫完這篇文章後,接著就要來研究Webpacker啦~

使用Jekyll自架部落格

我是wells,擔任過室內配線的國手,征服了電氣領域後,現在正跨大版圖到資訊界。

料理食材(內文內容)

  • Jekyll的簡介
  • Jekyll的安裝
  • Jekyll的使用
  • Github Page

誰可以安心食用(適合誰讀)

  • 知道Markdown語法
  • 知道Github如何創建repository
  • 對於網站、網頁有一點點概念

服用完你會獲得什麼

  • 知道Jekyll是什麼
  • 知道如何使用Jekyll(初步)
  • 知道如何使用Github Page

基本介紹

Jekyll是一個靜態網頁的生成器,由Ruby程式語言編寫而成的,許多人會使用靜態網頁生成器製作blog,主要是因為直接編寫HTML需要大量與內容無關的格式標籤,用這種方式製作blog效率並不高,而靜態網頁的生成器幾乎都支援Markdown語法,不用編寫又臭又長的html而是使用Markdown這種易讀易寫的標籤語言,因此自然會更受到懶人們(我)的歡迎。

但無論如何,瀏覽器還是只能解讀HTML,所以我們最終還是得把Markdwon轉換為HTML,但試想每次寫完後都要手動拿去轉換是不是有點麻煩呀?

而Jekyll這種靜態網頁的生成器最主要的功用就是協助處理繁瑣重複的動作,使作者可以更加專注內文的編寫,並在有所需求時又不失網頁靈活性。

前置動作

  • 安裝好ruby版本並且確保版本在2.4以上
    ruby --version
  • 確保gem有被安裝(安裝ruby時會自動安裝)
    gem --version
  • 確認gcc/g++/make有被安裝
    gcc -v / g++ -v / make -v

安裝說明

  1. gem install jekyll

    使用gem來安裝jekyll及其插件。

    gem的全名是RubyGems,是用於管理各式Ruby函式庫的管理器,類似於Python中的pip,Ruby函式庫的格式也被稱為gem,其中最有名就是Rails這個gem

  2. jekyll new demo

    安裝完Jekyll後就可以使用new這個指令創建專案。

    這邊的demo請換成自己的專案名稱

  3. bundle install

    前面有提到gem是一個個Ruby的函式庫,如同Ruby或是各類程式語言有版本的編號,gem也會有,而bundler就是一個管理gem版本的gem(有點饒口),bundler主要的功用是確保該專案使用的gem(包含版本)在本地端皆有安裝。

    如果顯示bundle not found,請先執行gem install bundler

  4. bundle exec jekyll serve

    運行jekyll server,其中的serve可以只打一個s,按下Ctrl+C可以關閉server。

    如果前面不加上bundle exec事實上也是可以運行的,此段的用意是為了避免使用了不同的gem版本進行動作。

此時在瀏覽器中輸入http://127.0.0.1:4000/,可以看到以下畫面:

jekyll serve預設使用4000 port,如果要更改,在啟動伺服器時使用bundle exec jekyll serve -P [指定port號]來指定。

Jekyll的設定介紹

這邊來介紹Jekyll的幾個重要設定檔(.yml)和資料夾

YAML

在Jekyll中會看到許多格式是.yml的檔案,那麼這個.yml是什麼呢?

.yml是指YAML格式的檔案,其用來表達資料的序列化,例如以下:

receipt:     Oz-Ware Purchase Invoice
date:        2012-08-06
customer:
  given:   Dorothy
  family:  Gale

由於字串不需要引號包起來,使用YAML就可以很清晰的了解資料的結構,YAML也有一些具有意義的符號,詳情可以查閱維基百科,唯一要特別注意的是YAML的縮排千萬千萬不能使用TAB,我曾經debug一整個下午就因為縮排用TAB......

_config.yml

_config.yml是jekyll中最重要的設定檔,裡面變數儲存了包括root directory、網站名稱......的資料,因為_config.yml裡面的資料是全域性的,因此我們也會把網站擁有者資訊、各式社群連結放在這邊,以下就來介紹幾個常用的變數:

修改_config.yml後Jekyll要重啟server才會被讀取哦!

變數名稱 功用
url 你的domain name
使用GithubPage可能就是https://yourname.github.io
baseurl 你的專案名稱,例如/project-name
假如伺服器含有多個專案時,就會使用這個變數區分不同專案
timezone 設定時區用
但假如server有設定時區的話,就會以server為主

_site/

當我們執行bundle exec jekyll s之後,你會發現檔案目錄多了_site這個資料夾:

可以發現_site/裡面我們寫的Markdown檔案都變成HTML了,這是因為執行bundle exec jekyll s後,內含的指令會把.md的檔案轉換為.html,而_site/也是我們之後要部署到伺服器上的資料夾。

_post/

這個資料夾是我們存放文章的地方,通常每一篇的檔名會以時間和文章名稱命名,例如:2021-04-26-使用Jekyll自架部落格.md,請確保副檔名為.md或是.markdown,不然會沒有辦法轉換為HTML。

在文章開頭的地方我們通常會加入以下內容:

---
title:  "使用Jekyll自架部落格"
date:   2021-04-26 22:52:03 +0800
categories: Tutorial
tags:  [Jekyll,Github-Page]
---

只有titledate是必需的變數,其他的變數都可以自行增減,但前提是你要知道如何操作XD

寫完上面的文章開頭後,接著就可以用Markdown語法開開心心的進行內文書寫了~

_drafts/

這個資料夾是我們存放草稿的地方,預設是沒有這個資料夾的,需要自己新增,相比於_post/_drafts/的文章並不會在你執行bundle exec jekyll s時出現,必須得在後面加入--draft才行。

部署到Github Page

前言

當專案在本機端寫的差不多的時候,你很滿意,但是別人並不知道你的感受......

因為你寫的東西都在你的電腦上,如果你要讓全世界的人都看到,那麼你勢必得把它推到網路上,現在你只要付一點點小錢,購買我們wells網路公司的A方案,就可以享受靜態網站代管服務囉。

如果你不想花錢,當然也可以選擇用Github Page架設免費的靜態網站。

部署動作

前述有提到所有的靜態網頁都在_site中,因此只要部署_site這個資料夾整個網站就可以運作正常,但是這樣子的動作不太好,因為就像是只留卵而不留雞一樣,因此更好的做法是把Jekyll的專案和產生出來的_site都推上同一個repository,之後以不同的分支(Branch)作為區分,以下為部署動作:

  1. 到Github中新建一個repository。

  2. 先把Jekyll專案推上repository。
    在Jekyll專案根目錄執行以下內容:

    git init
    git add .
    git commit -m "提交訊息"
    git remote add origin [你的repository url]
    git push -u origin master
    

    以上動作是把整個專案推上剛剛新建的repository,由於沒有指定分支,因此預設會是master,完成後你就可以在Github上看到剛剛推上去的內容了,但是在這一步你依然無法看到你的網頁。

    你可能發現_site和其他幾個資料夾並沒有在這個repository裏,這是因為.gitignore裡含有這幾個檔案名稱,因此git並不會把這些檔案加入git資料庫中,自然也不會被推到網路上。

  3. 把_site推上repository。

    在終端機運行以下指令

    cd _site
    git init
    git checkout -B gh-pages
    git add .
    git commit -m "提交訊息"
    git remote add origin [你的repository url]
    git push -u origin gh-pages
    

    在本地端要做的事情已經結束了,接著是Github上面的設定:

    首先,到Github Page設定的位置

    在repository上排的選單中點擊最右邊的Settings,之後在左邊的列表中點擊Pages的欄位。


    這張圖與你目前的畫面可能有所差異,但大致上都相同,在Source(紅框)的選項中,選擇_site推上的gh-pages分支,接著選擇/root,按下儲存後你會得到一個URL,點擊後就可以連到你的網站了。

結語

網路上有許多方便寫部落格的網站,如:MediumGoogle blogger,讓你不需要寫任何的程式碼、HTML甚至是Markdown就可以輕鬆建立出一個不錯的部落格,但本人之所以不願意這麼做,除了想不開外,是因為我認為自行架設blog可以學得很多東西,這就等於是一個Side Project,並且身為一位軟體工程師,當別人看到你的blog是來自你自架的網站,不用多說,自然也可以了解到你的實力,有這麼多好處,專屬於你自己的blog還不來架一個嗎?XD

什麼是業務邏輯和商業邏輯

架構圖

mindmap
  root((mindmap))
    Origins
      Long history
      ::icon(fa fa-book)
      Popularisation
        British popular psychology author Tony Buzan
    Research
      On effectiveness<br/>and features
      On Automatic creation
        Uses
            Creative techniques
            Strategic planning
            Argument mapping
    Tools
      Pen and paper
      Mermaid

Model-driven Architecture

https://en.wikipedia.org/wiki/Model-driven_architecture

Wiki 的 Business Object

https://en.wikipedia.org/wiki/Business_object

Clean Architecture 的 Business Logic

Wiki 的 Business Logic

https://en.wikipedia.org/wiki/Business_logic

business logic or domain logic is the part of the program that encodes the real-world business rules that determine how data can be created, stored, and changed

Business Logic 是流程、 Business Rule 是聲明

裡面舉個

business logic != business rule

Wiki 的 Multitier architecture

Tier 和 Layer 是不同的東西
layer is a logical structuring mechanism for the conceptual elements that make up the software solution, while a tier is a physical structuring mechanism for the hardware elements that make up the system infrastructure.[1][2] For example, a three-layer solution could easily be deployed on a single tie

Trivial Validation vs Business Rule

https://www.youtube.com/watch?v=FbYcIqVmGRk&t=332s

這個影片舉了一個 warehouse_product 的例子
其中的程式碼類似於以下

這段程式碼中有兩個條件式,當條件符合時,分別 raise ArgumentErrorInsufficientError
但前者不是 Business Rule,而是 Trivial Validation

class WarehouseProduct
    class InsufficientError << StandardError; end
    
    def shipping_product(quantity)
        raise ArgumentError, 'quantity must bigger than 0' if quantity <= 0
        raise InsufficientError, 'InsufficientError' if quantity > @stock

        apply(quantity)
    end
end

關於 Trivial Validation 和 Business Rule,作者幾種不同的特性

Trivial Validation Business Rule
1 Static Evolve
2 Input/Output layer(Translate) Expand
3 Passed to Business Logic (often) based on state
4 Deterministic

在上述的例子中,為什麼判斷 quantity 不能是負數是 Trivial Validation

  1. 他不會變動,這是一個寫了就不會變的規則,幾乎不會有把 0 變成其他數字的情況(符合第一點)
  2. 在判斷完這個條件式後,我們預期將這個已被驗證的變數傳給商業邏輯(符合第 3 點)
  3. 相同類型的輸入總會得到完全相同的輸出

關於 Input/Output layer 則可以想像成是 Translate,例如把 HTTP Request 的參數轉成 ActionController::Parameters

那對於 Business Rule 呢?
首先 Business Rule 是會變動的,Business Rule 會隨著 Domain、想法或是商業行為而變化
第二點則是可能因應新業務而擴展
最後是 Business Rule 是基於狀態的

接著作者舉的例子很像 Rails 的 Form Object 的應用
將 Trivial Logic 從 Business Logic 分出去

class WarehouseProductForm
    attr_read :quantity
    def initilize(quantity)
        @quantity
    end

    def valid?
        raise ArgumentError, 'quantity must bigger than 0' if quantity <= 0
    end

    def valided_value
        valid?
        quantity
    end
end

class WarehouseProduct
    class InsufficientError << StandardError; end
    
    def shipping_product(form)
        raise InsufficientError, 'InsufficientError' if form.valided_value > @stock

        apply(form.valided_value)
    end
end

之後還舉了個超賣的例子

class WarehouseProduct
    class InsufficientError << StandardError; end
    
    def shipping_product(form)
        raise InsufficientError, 'InsufficientError' if form.valided_value > @stock + Buffer

        apply(form.valided_value)
    end
end

Ruby 搭配 Sketchup 學習筆記(二)

前情提要

Sketchup 是一款在建築、都市計畫和遊戲開發都頗有名氣的 3D 建模軟體,而 Ruby 則是一個程式語言,它可以搭配 Sketchup 達成程式化 建模的任務,近期經由系主任引薦,要開發 Sketchup 的 Extension,雖然我寫過 Ruby,但 Sketchup 則是完全沒碰過,於是利用文章來記錄所學的一點一滴。

本篇內容

  • Ruby code editor 預設內容
  • 使用 Ruby console load .rb 檔案
  • 畫五邊形
  • 多邊形
  • 弧線
  • Sketchup 軸線意義

Ruby code editor 預設內容

還記得上一篇我們最後畫了一條直線,這個程式碼如下:

Sketchup.active_model.entities.add_line [0,0,0],[9,9,9]

但其實 Ruby code editor 有預設的程式碼可以幫我們縮短以上的內容。

在開啟 Ruby code editor 時,你的畫面應該會像這樣:
image

點擊上方工具列的 Edit/Edit Default Code 後你可以看到此畫面
image

不用更改任何內容,當我們再次開啟 Ruby code editor 時,就會有此預設的內容。

下一步我們把原本畫直線的程式碼更改為以下:

ent.add_line [0,0,0],[9,9,9]

放在預設程式碼的下方,執行後結果將會與原本的程式碼相同。

使用 Ruby console load .rb 檔案

使用 Ruby code editor 寫 Ruby 優點是隨開隨寫,但我還是習慣使用外部的編輯器打扣,畢竟快捷鍵還是熟悉的用起來比較順暢,那該如何在 Sketchup 中載入已寫好的 Ruby 檔案呢,接著看下去:

我使用的是 Mac OSX 系統,而 Sketchup 的 Ruby Console 讀取的目錄會是以下路徑

~/Library/Application Support/SketchUp ${版本號}/SketchUp/Plugins

得知路徑後,在該目錄下新增一個 test.rb 後,再到 Ruby Console 執行以下內容就可以載入該檔案

load 'test.rb'

這邊也可以建個資料夾管理檔案,而詳細如何管理那就依個人喜好決定了。

Sketchup 軸線意義

在 Sketchup 上可以看到三種顏色的線,分別是紅、綠和藍,這三條線在傳統的建模軟體代表意義應該是:

  1. 紅:X軸,[1,0,0]
  2. 綠:Y軸,[0,1,0]
  3. 藍:Z軸,[0,0,1]

但實際上 Sketchup 並不是用 XYZ 進行區分,而是用東南西北、上下來區分,如下圖:

不過這只是習慣問題,所以其實沒什麼差。

畫五邊形

在 Sketchup 中假如要畫一個五邊形,會使用線段來完成,而線段被視為邊緣物件(Edges),有幾種做法,以下逐一介紹。

直線

用直線畫五邊形應該是最容易想到的,程式碼如下:

# Default code, use or delete...
mod = Sketchup.active_model # Open model
ent = mod.entities # All entities in model
sel = mod.selection # Current selection

pt1 = [5, 0, 0]
pt2 = [1.5625, -4.75, 0]
pt3 = [-4.0625, -2.9375, 0]
pt4 = [-4.0625, 2.9375, 0]
pt5 = [1.5625, 4.758, 0]

ent.add_line pt1, pt2
ent.add_line pt2, pt3
ent.add_line pt3, pt4
ent.add_line pt4, pt5
ent.add_line pt5, pt1

曲線

曲線其實就是直線的進階版,它可以接受多個點傳入,除了畫出五邊形之外,順便觀察它的 class 與 length,程式碼如下:

# Default code, use or delete...
mod = Sketchup.active_model # Open model
ent = mod.entities # All entities in model
sel = mod.selection # Current selection

pt1 = [5, 0, 0]
pt2 = [1.5625, -4.75, 0]
pt3 = [-4.0625, -2.9375, 0]
pt4 = [-4.0625, 2.9375, 0]
pt5 = [1.5625, 4.758, 0]

curve = ent.add_curve pt1, pt2, pt3, pt4, pt5, pt1

puts "curve是什麼?", curve.class #Array
puts "curve的長度?", curve.length #5

圓形

上述的直線以及曲線都不一定要圍成一個封閉的物件,因此假如要製作五邊形這種封閉的物件,圓形會更適合,程式碼如下:

# Default code, use or delete...
mod = Sketchup.active_model # Open model
ent = mod.entities # All entities in model
sel = mod.selection # Current selection

circle = ent.add_circle [0,0,0], [0,0,1], 10, 5
puts circle.class #Array
puts circle.length #5

這邊可以看到傳入 4 個引數,而 add_circle 的參數定義如下:

add_circle center, normal, radius, num_segments = 24

分別代表:

  1. center:中心點
  2. normal:方向
  3. radius:半徑
  4. num_segments:線段數量。預設為 24。

多邊形

多邊形與圓形幾乎相同,差別在於多邊形的 num_segments 這個參數是沒有預設值,因此不可省略,與圓形比較的程式碼如下:

# Default code, use or delete...
mod = Sketchup.active_model # Open model
ent = mod.entities # All entities in model
sel = mod.selection # Current selection

normal = [0,0,1]
radius = 2

ent.add_ngon [0,0,0], normal, radius, 6
ent.add_circle [5,0,0], normal, radius, 6

ent.add_ngon [10,0,0], normal, radius, 24
ent.add_circle [15,0,0], normal, radius

弧線

弧線十分的麻煩,它的參數定義如下:

add_arc center, xaxis, normal, radius, start_angle, end_angle, num_segments

分別代表:

  1. center:中心點
  2. xaxis:從哪個軸線起算
  3. normal:方向
  4. radius:半徑
  5. start_angle:從哪個角度開始畫
  6. end_angle:在哪個角度結束
  7. num_segments:線段數量

我們執行以下此段程式碼,並且看看結果:

# Default code, use or delete...
mod = Sketchup.active_model # Open model
ent = mod.entities # All entities in model
sel = mod.selection # Current selection

arc1 = ent.add_arc [0,0,0], [0,1,0], [0,0,1], 5, 0, 90.degrees
arc2 = ent.add_arc [0,0,0], [0,1,0], [0,0,-1], 10, 0, 90.degrees
arc3 = ent.add_arc [0,0,0], [0,1,0], [0,0,1], 15, 0, 180.degrees
arc4 = ent.add_arc [0,0,0], [1,1,0], [0,0,-1], 15, 0, 90.degrees
arc4 = ent.add_arc [0,0,0], [0,0,1], [1,0,0], 15, 0, 90.degrees

執行結果:
image

看起來十分的混亂,讓我為它標一下名稱
image

這究竟是怎麼得知的呢!?可以參考「右手定則」,normal 這個參數將決定你大拇指的方向,而四指則是你會弧線繪製的方向,由此一來就可以知道弧線的畫法了最好是

參考資料

Ruby 搭配 Sketchup 學習筆記(一)

前情提要

Sketchup 是一款在建築、都市計畫和遊戲開發都頗有名氣的 3D 建模軟體,而 Ruby 則是一個程式語言,它可以搭配 Sketchup 達成程式化 建模的任務,近期經由系主任引薦,要開發 Sketchup 的 Extension,雖然我寫過 Ruby,但 Sketchup 則是完全沒碰過,於是利用文章來記錄所學的一點一滴。

本篇內容

  • 安裝 Sketchup
  • 使用 Ruby Console 印出第一個 Hello, world!
  • 安裝 Ruby code editor extension
  • 使用 Ruby code editor 畫出第一條直線

安裝 Sketchup

Sketchup 官網 下載 Sketchup
image

下載完成後開啟 Sketchup,第一次使用會要求你登入,即可開始 30 天試用期,之後你會看到以下畫面:
image

這邊我們選擇預設的 簡單/英吋 模型

開啟後你可以看到此畫面:
image

使用 Ruby Console 印出第一個 Hello, world!

在上方工具列中的 擴展程式套件/開發人員 中有 Ruby 控制台,點擊它
image

之後你將可以看到一個對話框,輸入以下內容並送出

puts "Hello, World!"

你將可以看到以下畫面:
image

安裝 Ruby code editor

由於 Ruby console 不適合用來編寫大量的程式碼,因此我們會安裝一個 extension,叫做 Ruby code editor。

點擊上方工具列中的 擴展程式套件/Extension Warehouse ,在搜尋列表中輸入 Ruby code editor,可以看到以下畫面:
image

點擊第一個後安裝。

安裝成功後你將可以看到一個小對話框,點擊後就可以開啟 Ruby code editor:
image

使用 Ruby code editor 畫出第一條直線

點擊 Ruby code editor 的小對話框,你將可以開啟一個編輯器,在其中輸入以下內容

Sketchup.active_model.entities.add_line [0,0,0],[9,9,9]

按下下方的執行按鈕,如下圖:
image

之後你將可以在 sketchup 中看到剛剛畫的那條直線:
image

參考資料

Ruby 搭配 Sketchup 學習筆記(六)

前情提要

Sketchup 是一款在建築、都市計畫和遊戲開發都頗有名氣的 3D 建模軟體,而 Ruby 則是一個程式語言,它可以搭配 Sketchup 達成程式化 建模的任務,近期經由系主任引薦,要開發 Sketchup 的 Extension,雖然我寫過 Ruby,但 Sketchup 則是完全沒碰過,於是利用文章來記錄所學的一點一滴。

本篇內容

  • 對話方塊
  • 功能表
  • 命令
  • 工具列
  • WebDialogs

對話方塊

對話方塊在之前的範例中用作於呈現資訊,但其實它還可以與使用者進行互動。以下是基本的用法:

UI.messagebox "測試訊息"

輸出結果:
image

UI.messagebox 的參數除了接受一個 String 之外,還可以傳入第二個參數決定按鈕形式,以下介紹幾種參數:

  • MB_OK:有一個「確定」按鈕
  • MB_OKCANCEL:有「確定」和「取消」按鈕
  • MB_RETRYCANCEL:有「重試」和「取消」按鈕
  • MB_ABORTRETRYCANCEL:有「放棄」、「重試」和「取消」按鈕
  • MB_YESNO:有「是」和「否」按鈕
  • MB_YESNOCANCEL:有「是」、「否」和「取消」按鈕
  • MB_MULTILINE:顯示多列文字的訊息方塊,有第三個參數,代表訊息方塊的標題

各事件回傳的值皆不同:

  • 「確定」:1
  • 「取消」:2
  • 「重試」:4
  • 「放棄」:3
  • 「是」:6
  • 「否」:7

使用範例:

case UI.messagebox "測試訊息",MB_OKCANCEL
    when 1
        puts "你按下確定"
    when 2
        puts "你按下取消"
end

還有一種對話方塊可以讓使用者輸入資訊,以下示範 BMI 計算:

label = ["身高", "體重"]
defaults = [173, 68]
result = inputbox label, defaults, "輸入你的身高與體重"
bmi = result[1]/((result[0]/100.0)**2)

UI.messagebox "你的BMI是#{bmi}"

輸出結果:

image

image

以下示範如何使用下拉式選單:

label = ["餐點種類"]
default = ["炒飯"]
options = ["炒飯", "炒麵"]
enums = [options.join("|")]
result = inputbox label, default, enums, "今天晚上吃什麼?"

UI.messagebox "今天吃#{result[0]}" if result

輸出結果:
image

image
比較麻煩的就是所有的參數都要塞 Array

功能表

功能表可以新增的位置有兩種:

  1. 上方工具列
  2. 右鍵點擊物件(快顯)

上方工具列

要新增在上方工具列,請先選擇一列,例如: File, Plugins等等,以下示範如何使用:

menu = UI.menu "Plugins"
menu.add_item "按按看" do
    UI.messagebox "這是功能表"
end
submenu = menu.add_submenu "工具列"
submenu.add_item "板手" do
    UI.messagebox "板手"
end
submenu.add_item "螺絲起子" do
    UI.messagebox "螺絲起子"
end
submenu.add_separator
item = submenu.add_item "電動起子" do
    UI.messagebox "電動起子"
end
submenu.set_validation_proc(item){MF_DISABLED}

輸出結果:
image

set_validation_proc 的區塊**有五種常數可以填入:

  • MF_ENABLED
  • MF_DISABLED
  • MF_CHECKED
  • MF_UNCHECKED
  • MF_GRAYED

右鍵點擊物件(快顯)

以下示範如何使用快顯功能表:

UI.add_context_menu_handler do |menu|
    menu.add_item("這是快顯按鈕") do
        UI.messagebox("你點到啦!")
    end
end

輸出結果:

image

命令

當有需要製作大量相同功能的按鈕時,可以考慮使用命令:

UI.menu("Draw").add_item("觸發命令") do
    UI.messagebox("從這裡開始執行我定義的程序")
end

cmd = UI::Command.new("測試新的命令") do
    UI.messagebox("開始執行")
end

UI.menu("Draw").add_item cmd

工具列

還記得裝完 Ruby Code Editor 後會有一個小對話框浮現嗎?那其實就是工具列。以下示範如何製作:

tool_cmd = UI::Command.new("測試工具"){UI.messagebox "這是我第一個工具"}
tool_cmd.large_icon = "carton.jpeg"
tool_cmd.tooltip = "這是提示"
tool_toolbar = UI::Toolbar.new "我的工具列"
tool_toolbar.add_item tool_cmd
tool_toolbar.show

輸出結果:

image

WebDialogs

Sketchup Ruby Api 官方文件建議改為 HTMLDialogs。

以下示範如何建立一個 WebDialogs

wd = UI::WebDialog.new "一個 WebDialog"
wd.show

在新增 WebDialog 時也可以給予其參數:

  1. dialog_title
  2. scrollable
  3. preferences_key:記住 WebDialog 的位置尺寸
  4. width
  5. height
  6. left
  7. top
  8. resizable

而產生出來的物件有以下的方法可使用:

  • set_url
  • set_potition
  • set_size
  • set_file
  • max_height
  • max_width
  • min_height
  • min_width

搭配 HTML

在 WebDialogs 之中可以放入 HTML 和 Javascript,例如以下範例先建立一個 .html 檔,之後進行引入:
新增一個 index.html 在 Sketchup 的 Plugins/mytest 資料夾之下

wd = UI::WebDialog.new "一個 WebDialog"
path = Sketchup.find_support_file "index.html", "Plugins/mytest"
wd.set_file path
wd.show

輸出結果:

image

搭配 Javescript

當 Ruby 想要傳送訊息到 Javascript 時可以使用 wd.execute_script funcName(arg1,arg2),當 Javascript 想要傳送資料到 Ruby 時,可以在 Javascript 使用 window.loaction = "skp:callback_name@callback_data"

Ruby 傳資料給 Javascript

先建立 ex_909.rb,程式碼如以下:

class EntObserver < Sketchup::EntityObserver
  def onChangeEntity(entity)
    f_count = 1
    for v in entity.vertices
      args = "'#{v.position.x.to_s}','#{v.position.y.to_s}','#{v.position.z.to_s}'"
      $wd.execute_script "setPoint#{f_count.to_s}(#{args})"
      f_count += 1
    end
  end

  def onEraseEntity(entity)
    $wd.execute_script "faceDeleted()"
  end
end

ents = Sketchup.active_model.entities
face = ents.add_face [0, 0, 0], [10, 0, 0], [10, 10, 0], [0, 10, 0]

cbs = EntObserver.new
face.add_observer cbs

$wd = UI::WebDialog.new "檢查並顯示端點位置"
path = Sketchup.find_support_file "ex_910.html", "Plugins/mytest"
$wd.set_file path
$wd.show

再建立 ex_910.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script>
        function setPoint1(c1, c2, c3) {
            document.getElementById("pt1").innerHTML = `${c1},${c2},${c3}`
        }

        function setPoint2(c1, c2, c3) {
            document.getElementById("pt2").innerHTML = `${c1},${c2},${c3}`
        }

        function setPoint3(c1, c2, c3) {
            document.getElementById("pt3").innerHTML = `${c1},${c2},${c3}`
        }

        function setPoint4(c1, c2, c3) {
            document.getElementById("pt4").innerHTML = `${c1},${c2},${c3}`
        }

        function faceDeleted() {
            document.getElementById("pt1").innerHTML = "圖形已被刪除"
            document.getElementById("pt2").innerHTML = "圖形已被刪除"
            document.getElementById("pt3").innerHTML = "圖形已被刪除"
            document.getElementById("pt4").innerHTML = "圖形已被刪除"
        }
    </script>
</head>

<body>
    <h1>第一個點:</h1>
    <p id="pt1">在預設位置</p>
    <h1>第二個點:</h1>
    <p id="pt2">在預設位置</p>
    <h1>第三個點:</h1>
    <p id="pt3">在預設位置</p>
    <h1>第四個點:</h1>
    <p id="pt4">在預設位置</p>

</body>

</html>

輸出結果:

image

Javascript 傳資料給 Ruby

請建立 ex_911.rb,並複製以下內容:

wd = UI::WebDialog.new "Face 製造機"
wd.set_size 600, 350
path = Sketchup.find_support_file "ex_912.html", "Plugins/mytest"
wd.set_file path

wd.add_action_callback("create_face") do |dialog, arg|
  if arg.to_s.length == 0
    puts "無效的輸入"
  else
    v = arg.to_s.split(",")
    pt1 = [v[0].strip.to_f, v[1].strip.to_f, v[2].strip.to_f]
    pt2 = [v[3].strip.to_f, v[4].strip.to_f, v[5].strip.to_f]
    pt3 = [v[6].strip.to_f, v[7].strip.to_f, v[8].strip.to_f]
    pt4 = [v[9].strip.to_f, v[10].strip.to_f, v[11].strip.to_f]

    Sketchup.active_model.entities.add_face pt1, pt2, pt3, pt4
  end
end
if RUBY_PLATFORM.index "darwin"
  wd.show_modal
else
  wd.show
end

再建立 ex_912.html ,複製以下內容:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script>
        function sendPoints() {
            var ids = new Array("x1", "y1", "z1", "x2", "y2", "z2", "x3", "y3", "z3", "x4", "y4", "z4");
            var arg = "";
            var entry = "";
            var valid = true;

            for (i in ids) {
                entry = document.getElementById(ids[i]).value
                if (entry.length == 0 || isNaN(entry)) {
                    valid = false;
                } else {
                    arg += `${entry},`
                }
            }
            if (!valid) {
                arg = "";
            }
            window.location = `skp:create_face@${arg}`;
        }
    </script>
</head>

<body>
    <input type="text" id="x1" value="0.0" size="10" maxlength="6">
    <input type="text" id="y1" value="0.0" size="10" maxlength="6">
    <input type="text" id="z1" value="0.0" size="10" maxlength="6">
    <hr>
    <input type="text" id="x2" value="0.0" size="10" maxlength="6">
    <input type="text" id="y2" value="0.0" size="10" maxlength="6">
    <input type="text" id="z2" value="0.0" size="10" maxlength="6">
    <hr>
    <input type="text" id="x3" value="0.0" size="10" maxlength="6">
    <input type="text" id="y3" value="0.0" size="10" maxlength="6">
    <input type="text" id="z3" value="0.0" size="10" maxlength="6">
    <hr>
    <input type="text" id="x4" value="0.0" size="10" maxlength="6">
    <input type="text" id="y4" value="0.0" size="10" maxlength="6">
    <input type="text" id="z4" value="0.0" size="10" maxlength="6">
    <hr>
    <input type="submit" onclick="sendPoints();" value="產生">
</body>

</html>

輸出結果:
image

參考資料

Ruby 使用 Double Splat Operator 修飾參數

簡介

此篇是延伸至上一篇 Ruby 使用 Splat Operator 修飾參數,上篇介紹的是 Splat Operator (*),會將傳入的引數變成一個 Array,而此篇的 Double Splat Operator (**) 則是一個不同的應用,詳細內容就繼續看下去吧。

使用方式

在參數前方加入**,如以下:

def test(**arg)
    ...
end

Double Splat Operator (**)

使用 Double Splat Operator 修飾的參數會有以下幾個特性:

  1. 只能傳入 Hash
  2. 該參數為選填,不傳入時該參數會是{}
  3. 只能傳入一個 Hash,不像 Splat Operator (*)可以傳入多個引數
  4. 該參數需要放在最後
  5. 可以搭配關鍵字參數(Keyword Parameter)

除了 第4點第5點 外,使用 Double Splat Operator 修飾的參數與以下參數並無差別

def test(arg={})
    ...
end

與 Splat Operator (*) 比較

以下比較使用 Splat Operator (*) 和 Double Splat Operator (**) 修飾後的參數差別

功能 Splat Operator (*) Double Splat Operator (**)
類別 Array Hash
可傳入引數的類別 只要能儲存在 Array 之中皆可 只能是 Hash
可傳入引數的數量 無限制 只能是 Hash
無引數傳入時 [] {}
可放置位置 不限,但假如有預設值的參數,必須在其後 必須是最後
假如有 Keyword Parameter 需放置在 Keyword Parameter 前方 一樣放置在最後

使用範例

使用 Splat Operator (*) 和 Double Splat Operator (**) 搭配

我們學校每一年都會舉辦就業博覽會,邀請許多廠商來學校擺攤,今年我大三剛好也有機會去看一下,但是到每一個攤位,都會請我填寫一些個人資料,裡面的資料包含姓名、聯絡方式、學歷和競賽經驗,那我其實就有點好奇,學歷和競賽經驗這種數目不一定的選項,我可以用 Splat Operator (*) 和 Double Splat Operator (**) 寫出來嗎?

def profile(name, email, *educations, **competitions)
    puts "姓名:#{name}"
    puts "電子信箱:#{email}"
    puts "畢業學校:" unless educations.empty?
    educations.each{|education| puts education}
    puts "競賽經驗:" unless competitions.empty?
    competitions.each{|name,award| puts "競賽名稱:#{name} 獎項:#{award}"}
end

profile("wells", "[email protected]", "花蓮高工", "台灣柯基大學", "IOCCC":"冠軍", "國際技能競賽": "冠軍", "英雄聯盟六都爭霸賽":"倒數第一")

輸出:

姓名:wells
電子信箱:[email protected]
畢業學校:
花蓮高工
台灣柯基大學
競賽經驗:
競賽名稱:IOCCC 獎項:冠軍
競賽名稱:國際技能競賽 獎項:冠軍
競賽名稱:英雄聯盟六都爭霸賽 獎項:倒數第一

哦哦!原來真的可以用 Splat Operator (*) 和 Double Splat Operator (**) 寫出來欸!但是調皮的我想要試試看假如沒有 **competitions 這程式會怎麼跑呢?

不使用 Double Splat Operator (**) 的情況

def profile(name, email, *educations)
    puts "姓名:#{name}"
    puts "電子信箱:#{email}"
    puts "畢業學校:" unless educations.empty?
    educations.each{|education| puts education}
end

profile("wells", "[email protected]", "花蓮高工", "台灣柯基大學", "IOCCC":"冠軍", "國際技能競賽": "冠軍", "英雄聯盟六都爭霸賽":"倒數第一")

輸出:

姓名:wells
電子信箱:[email protected]
畢業學校:
花蓮高工
台灣柯基大學
{:IOCCC=>"冠軍", :國際技能競賽=>"冠軍", :英雄聯盟六都爭霸賽=>"倒數第一"}

雖然程式還是可以運作,但是他似乎把最後一個 Hash 也放進 Array 裡了,如果我想要做出與上一版相同的功能,那我就只能用educations.last.each了,但還要處理一些繁瑣的東西,重點是看起來完全不潮!因此假如最後會傳入一個 Hash,我將優先使用 Double Splat Operator (**)。

結語

Double Splat Operator 相比 Splat Operator 我覺得用途就沒有這麼廣泛了,但是相比def test(arg={}),用 Double Splat Operator 就是潮!

參考連結

Kamigo 搭配 Devise 實作會員功能

簡介

前陣子在做實作台科不分系學姊時,發現需要會員機制,但假如直接開一個表單要使用者註冊似乎又有點太麻煩, 因為最終傳到 Server 的每一條訊息都已經帶有使用者資訊,於是和卡米哥討論該怎麼做,沒想到他早就已經做過了,而且還結合了 Devise,十分的方便,真是偉哉偉哉卡米哥。

前提提要

此篇文的前提是使用 Rails 的 Kamigo 框架進行 Line Chatbot 開發,如果還不知道如何使用 Kamigo 可以查看前方的連結,參考該 Repo 的 README.md 進行實作。

操作步驟

  1. 安裝相關 Gem
  2. 生成 User Model
  3. 設定 Devise
  4. 認證 User

安裝相關 Gem

Gemfile 中加入以下內容:

gem 'devise'

之後執行:

bundle install

生成 User Model

請執行以下指令:

rails g devise:install
rails g devise user
rails g migration add_line_login

開啟剛剛新增的 db/migrate/xxxxxxxxx_add_line_login.rb 並在 change 方法中加入以下內容:

add_column :users, :line_id, :string
add_column :users, :name, :string
add_column :users, :image_url, :string

另外,由於我們之後要以 line_id 區分使用者,這導致 email column 的值可能會重複(因為 Devise 預設 email 的值為 '' ),因此需要解決 email 重複的問題,以下新增一個 migration:

rails g migration resolve_users_email_unique

開啟新增的 db/migrate/xxxxxxxxx_resolve_users_email_unique.rb 更改為以下內容:

def up
  change_column :users, :email, :string, null: true, default: nil
end
def down
  change_column_null :users, :email, false, SecureRandom.uuid
end

此段主要為修改 email column,當 user 被建立時,預設值為 nil,以此來避免唯一鍵重複,而 down 中的 SecureRandom.uuid 則是為了避免原本 emailnil 的資料 db rollback 後發生重複的問題。
到了這一步後就可以執行 Database migrate:

rails db:migrate

設定 Devise

由於 User 使用 line_id 進行區分,因此 emailpassword 為非必填,在 app/models/user.rb 加入以下內容:

def email_required?
    false
end

def password_required?
    false
end

認證 User

app/controllers/application_controller.rb 加入以下內容:

include Rails.application.routes.url_helpers
before_action :line_messaging_login

def line_messaging_login
  user = User.from_kamigo(params)
  sign_in user if user.present?
end

app/models/user.rb 加入以下內容:

# params[:source_user_id]
# params[:profile][:displayName]
def self.from_kamigo(params)
  if params[:profile].present? && params[:source_user_id].present?
    line_id = params.dig(:source_user_id)
    name = params.dig(:profile, :displayName)
    user = User.find_or_create_by(line_id: line_id)
    user.update(name: name)
    user
  end
end

完成到這一步後,每次使用者傳訊息至 Line Chatbot 都會判斷 line_id 是否已存在於 users 中,如果沒有會建立,最後的功能就和 Devise 一樣,可以在 controller 中使用 current_user 進行操作。

結語

會員功能就是這麼簡單就做好了,當然這都要歸功於 Kamigo 框架本身就寫得與 Rails 息息相關,目前來說雖然已經完成在 Line Chatbot 中確認使用者,但假如使用者開啟網頁就沒辦法確認了,因此下一篇會介紹使用 Line Login 擴充此會員功能。

參考連結

Kamigo LINE Bot 框架 - 實作簡單的取號機器人

Mysql2 的預設隔離等級

這會 update twice

def execute
  ActiveRecord::Base.transaction do
    shop = Shop.find(1)
    if webest_setting.business_id.present?
      puts "webest_setting.business_id: #{webest_setting.business_id}"
    end
    shop.lock!
    if webest_setting.business_id.nil?
      update_webest_setting
    end
    sleep 10
  end
end

def update_webest_setting
  webest_setting = WebestSetting.first
  webest_setting.business_id = '123'
  webest_setting.save!
end

def webest_setting
  WebestSetting.first
end

2.times { Thread.new { execute } }

update once

def execute
  shop
  ActiveRecord::Base.transaction(isolation: :read_committed) do
    if webest_setting.business_id.present?
      puts "webest_setting.business_id: #{webest_setting.business_id}"
    end
    shop.lock!
    if webest_setting.business_id.nil?
      update_webest_setting
    end
    sleep 5
  end
end

def update_webest_setting
  webest_setting = WebestSetting.first
  webest_setting.business_id = '123'
  webest_setting.save!
end

def webest_setting
  WebestSetting.first
end

def shop
  @shop ||= Shop.find(1)
end

2.times { Thread.new { execute } }

Ruby 使用 Splat Operator 修飾參數

簡介

Splat Operator 是 Ruby 定義方法時很常使用的運算子, 使用 Splat Operator 修飾的參數會將傳入的引入變成一個 Array ,其可以運用在不確定會傳入多少引數的情況,關於其特性和使用範例請繼續看下去。

使用方式

在參數前方加入*,如以下:

def test(*arg)
    ...
end

Splat Operator (*)

使用 Splat Operator 修飾的參數會有以下幾個特性:

  1. 可以傳入不限數目的引數,並將所有傳入的引數變成一個 Array,內容依傳入順序排序
  2. 該參數為選填,假如沒有傳入引數,該參數的變數會是一個空的 Array
  3. 不能有兩個以上 Splat Operator 修飾的參數
  4. 可以在其他參數中間
  5. 假如其他參數有預設值,一定得在有預設值的參數後面,但建議不要用沒必要刁難自己
  6. 假如有指定關鍵字參數(Keyword Parameter),則必須在其前面 ,如def test(*arg, key:)

使用範例

引數數目不確定

還記得以前我還不會講日文的時候(現在也不會),當時遇到了一位日本來的同學,為了表示我的友好我決定用日文跟他溝通,於是我使用一個會在結尾都加上「得斯」的翻譯機(translator),那麼這個翻譯機的程式會是怎樣寫呢?

如果我今天不會 Splat Operator ,我可能會寫成以下:

def translator(arg)
    puts "#{arg}得斯"
end

translator("哈囉")
translator("你好")
translator("我不會講日文")

輸出:

哈囉得斯
你好得斯
我不會講日文得斯

這樣子雖然可以跟日本同學聊上天了,但是這個翻譯機每次只能翻譯一句話,實在有點麻煩,我希望不管我傳入幾句話,它都能夠翻譯,於是我使用 Splat Operator 改寫看看 :

def translator(*arg)
  arg.each do |str|
    puts "#{str}得斯"
  end
end

translator("空巴哇", "台日友好", "我很會講日文")

輸出:

空巴哇得斯
台日友好得斯
我很會講日文得斯

完美!這下子不僅搭上話,還讓日本來的同學覺得我的日文很好,偉哉偉哉,對打 Code 的人來說語言真不是個問題。

收集多出的引數

我常去吃的小吃店,老闆非常不喜歡餐點做到一半時,客人才跟他說不要香菜或是突然要加麵之類的,於是他希望除了基本的餐點外,客人可以一次把他的需求說出來,所以老闆希望你可以幫他製作點餐系統,除了餐點和大小碗外,其他的要求也不知道有多少個,那麼這該怎麼製作呢?

def order(type, size, *notes)
    puts "餐點種類: #{type}"
    puts "大小碗: #{size}"
    puts "其他備註:" unless notes.empty?
    notes.each do |note|
        puts note
    end
end

order("湯麵","大碗","不要加蔥、要辣","不用免洗筷","湯和麵要分開裝")
puts "------------------"
order("乾麵","小碗")

輸出:

餐點種類: 湯麵
大小碗: 大碗
其他備註:
不要加蔥、要辣
不用免洗筷
湯和麵要分開裝
------------------
餐點種類: 乾麵
大小碗: 小碗

製作完成!無論客人的要求有幾百項,這個系統都不會壞掉!但可憐的老闆就沒這麼幸運了

放置在其中參數之間

過年時,小孩子說句吉祥話就可以討到紅包,於是我想到假如寫個說吉祥話的程式,放在有小朋友外型的機器人,它是不是就會變成一個紅包吸引機了!?並沒有

def say_blessing_words(title,*peoples,blessing_words)
    peoples.each do |people|
        puts "#{title}#{people},#{blessing_words}!"
    end
end

say_blessing_words("親愛的","阿貓叔叔","阿太哥哥","美美阿姨","恭喜發財")

輸出:

親愛的阿貓叔叔,恭喜發財!
親愛的阿太哥哥,恭喜發財!
親愛的美美阿姨,恭喜發財!

有了這一個紅包吸引機我已經迫不期待要過年了!!

結語

總結一下 Splat Operator 的使用時機:

  1. 引數的數量不確定
  2. 假如參數的數量確定,但太多時也可以節省數量
  3. 取代預設值為空陣列的參數,例如def test(arg=[])

下一篇會介紹 Double Splat Operator 的用法。

參考連結

Ruby 搭配 Sketchup 學習筆記(三)

前情提要

Sketchup 是一款在建築、都市計畫和遊戲開發都頗有名氣的 3D 建模軟體,而 Ruby 則是一個程式語言,它可以搭配 Sketchup 達成程式化 建模的任務,近期經由系主任引薦,要開發 Sketchup 的 Extension,雖然我寫過 Ruby,但 Sketchup 則是完全沒碰過,於是利用文章來記錄所學的一點一滴。

本篇內容

  • Sketchup 基本結構
  • Sketchup 模組
  • Model 類別物件
  • Entities 類別物件
  • 超級類別 Entity
  • Sketchup 的 Ruby

Sketchup 基本結構

在之前我們畫直線時,曾經使用過以下指令:

Sketchup.active_model.entities.add_line [0,0,0],[9,9,9]

以 Sketchup 的基本結構來拆分的話,可以分為以下三種:

  1. Sketchup 模組(Module)
  2. Model
  3. Entity
  4. Method

那這分別代表什麼意思呢?接著看下去。

Sketchup 模組

Sketchup 模組是 Sketchup 專屬的模組,所以你無法在其他地方呼叫 Sketchup,這個模組是專門用來處理 Sketchup 應用程式的所有資訊,在 Ruby 中任何東西都是物件,因此你可以使用 Sketchup 的幾個指令:

Sketchup.os_language
Sketchup.app_name
Sketchup.version

其中最重要的就是 Sketchup.active_model,呼叫此方法將會回傳目前的 Model 物件,至於 Model 物件是什麼?接著看下去。

Model 類別物件

Model 代表一個 Sketchup 的檔案(*.skp),包括其所有幾何圖形和模型的相關資訊,Sketchup 模組和 Model 模組僅有在剛開啟軟體時才會相同,假如之後有進行變更的話,就會不相同,簡單來說 Model 是 Sketchup 的子集合,可以試試以下指令:

mod = Sketchup.active_model
mod.title
mod.description
mod.path
mod.modified?

Model 是一個大型的容器,其底下包含了六類的容器:

  • 元件定義(.definitions):回傳檔案中ComponentDefinitions物件關聯的元件定義
  • 圖層容器(.layer):回傳檔案中的所有圖層資訊
  • 實體容器(.entities):回傳所有幾何圖形資訊
  • 選項管理員(.options):回傳「模型資訊」的設定選項
  • 材料容器(.materials):回傳使用材料的資訊
  • 檢視容器(.pages):回傳所有場景畫面資訊

Entities 類別物件

還記得之前畫過的直線嗎?

Sketchup.active_model.entities.add_line [0,0,0],[9,9,9]

當我們要畫直線之前,必須要先拿到 Entity 的控制權,也就是 Sketchup.active_model.entities,之後我們才可以用 .add_line 方法去畫直線。

值得一提的是當初我們在畫圓形時 .add_circle,其會回傳由多個 Edges 物件所組成的 Array,而並非單個物件。

超級類別 Entity

在 Entity 之下有許多子類別,其中最重要的就是 Drawingelement,在我們繪製幾何圖形時使用的就是該子類別的方法。
另外 Entity 物件有提供幾個常用的方法:

  • entityID:回傳該物件 ID
  • typename:回傳幾何物件的型態
  • model:回傳此物件所在的 Model
  • parent:回傳此物件的父層
  • deleted?:回傳此物件是否已被刪除
  • valid?:回傳此物件是否有效

請記得假如用 .add_circle 畫出來的物件是 Array,因此不能直接用上述的方法讀取。

Sketchup 的 Ruby

Sketchup 的 Ruby 有幾個有趣的操作,有些認為沒這麼好用的就不記錄了。

  1. 長度的轉換

    由於 Sketchup 有所謂的長度單位,還記得第一篇中,我們使用預設的英吋單位開啟檔案,那假如在程式碼中想要畫公分怎麼辦?

    Sketchup.active_model.entities.add_line [0,0,0],[9.cm,9.cm,9.cm]

    直接在後方加入單位即可,十分的方便。

  2. 陣列

    在 Sketchup 中假如要標示某點,必須以三維空間標示,而這通常都以一個陣列表示,於是 Sketchup 中的 Array 有以下幾種特別的方法:

    | 方法 | 用途 | 範例 |
    | -------- | -------------------------------- | --------------------------------- |
    | x | 回傳陣列中x軸的座標 | pt=[1,2,3];pt.x -> 1 |
    | y | 回傳陣列中y軸的座標 | pt=[1,2,3];pt.y -> 2 |
    | z | 回傳陣列中z軸的座標 | pt=[1,2,3];pt.z -> 3 |
    | on_line? | 判斷陣列代表的點是否在同一直線上 | pt=[3,3,3];line=[[1,1,1],[2,2,2]]; pt.on_line? line ->true|

參考資料

Rails 實作儲存型 XSS

本篇內容

  • 介紹
  • 防範機制
  • 挖坑給自己跳

介紹

儲存型 XSS 是利用動態網站經常從資料庫撈出資料後產生 HTML 的特性進行駭入,最常見的就是有貼文功能的網站,原本該呈現正常的貼文,但惡意人士將 JavaScript 寫入貼文中,導致其他用戶在觀看其貼文時瀏覽器直接執行該段內容裡的 JavaScript。

防範機制

儲存型 XSS 的駭入程序大致如下:

  1. 儲存惡意程式碼進資料庫
  2. 網站以資料庫的資料產生 HTML 傳給用戶
  3. 用戶的瀏覽器解析 HTML 並執行惡意程式碼

依照上方流程最常見的防護手段在第二步,在 Rails 內建中,會自動將 HTML 裡的特殊字符進行轉義,例如以下變化:

原始字元 轉義處理後
" &quot;
& &amp;
' &apos;
< &lt;
> &gt;

如果不相信可以建一個 index.html 並在其放入:

&lt;script&gt;alert(&quot;hack!&quot;)&lt;/script&gt;

<script>alert("hack!")</script>

比較看看最後出來的會是什麼。

另外一種防範手法是內容安全政策(CSP),做法是在 HTML 的 Head 裡加入以下 Meta:

<meta http-equiv="Content-Security-Policy" content="default-src 'self'">

此段規定所有的 JavaScript 皆只能從 <script> 中的 src 屬性讀取,內聯的 JavaScript 皆不會被執行。
而 CSP 也可以設定白名單,詳細作法請參考 Content Security Policy (CSP) 筆記

挖坑給自己跳

在 Rails 中,一般都會使用 *.html.erb ,如果想要在該檔案中遷入變數,就會寫成:

<div>
    <%= post%>
</div>

而其中的 <%= %> 就已經有進行轉義處理,因此就算你在其中放入 JavaScript 它也不會執行:

<div>
    <%= '<script>alert("hack!")</script>'%>
</div>

那假如今天閒來無事,可以試試看在前方加入 raw,那麼它就不會轉義了,例如:

<div>
    <%= raw '<script>alert("hack!")</script>'%>
</div>

輸出結果:
image

使用 raw 會導致惡意程式碼被執行。

參考資料

Ruby 中三種載入程式碼的機制(load, require, autoload)

Load

載入指定的 Ruby 檔案,寫幾次 load 就會執行幾次

# index.rb
puts 'first load'
load 'loader.rb'
puts 'second load'
load 'loader.rb'

# loader.rb
puts <<~TEXT
loading
----------
TEXT

輸出:

first load
loading
----------
second load
loading
----------

require

相比 load,只會執行一次,還有一個類似的叫 require_relative,跟 require 差別在於找檔案的起始點不同
require 會從 $LOAD_PATH 這個環境變數裡找,而相對路徑的 require 則比較適合用 require_relative

# index.rb
puts 'first require'
require './loader.rb'
puts 'second require'
require './loader.rb'

# loader.rb
puts <<~TEXT
loading.......
----------

TEXT

輸出:

first require
loading.......
----------

second require

autoload

autoload 會註冊 module 名稱在當前的 namespace,當之後訪問該 module 時,會使用 require 載入註冊時的檔案路徑

# index.rb
puts 'first autoload'
autoload :A, './loader.rb'
puts 'second autoload'
autoload :A, './loader.rb'

A.hi

# loader.rb
puts <<~TEXT
loading.......
----------

TEXT

module A
  def self.hi
    puts 'hi'
  end
end

輸出:

first autoload
second autoload
loading.......
----------

hi

Clean Architecture - Starting with the Bricks: Programming Paradigms

前導

  1. 這章像在介紹軟體界的「誓約與制約」,提到了三種典範語言為程式碼增加了一些規則,而這些限制可以讓軟體品質更高
    1. Structuring Programming
    2. Object-Oriented Programming
    3. Functional Programming
  2. 可以由上述的限制延伸到整個系統的三個專注點
    1. function
    2. separation of components
    3. data management

Structuring Programming

  1. 增加限制在 Program counter 的控制
    1. imposing discipline on direct transfer of control
  2. 使用 if, else, while 取代 goto
    1. 為何要廢除 goto這篇
  3. 雖然現今的主流語言已經沒有 goto 但還是有像 exception 和 break 等等破壞流程的語法
  4. Edsger Wybe Dijkstra 想要用數學學科中的**歐幾里得定理**找到一個公式可以代表軟體的品質
  5. 所有的程式都是由 sequence, selection, and iteration 組成
    1. sequence, selection 透過枚舉法證明
    2. iteration 透過歸納法證明
  6. 第 5 點的發現看似有機會找到公式解,但最終失敗了,於是開始採取科學的方式證明軟體品質
    1. 數學可以證明對的
    2. 科學是普遍性的假說,如黑天鵝,所以存在可否證性(falsifiability)
  7. 測試無法代表程式正確,僅能透過程式發現錯誤
  8. Structuring Programming 的價值來自於可以由多個可否證性的 unit(function) 所組成
  9. 鼓勵用 module 或是 function 切分系統 (functional decomposition)

補充資料

Object-Oriented Programming

  1. 增加限制在 function call(from pointer)

    1. imposing discipline on indirect transfer of control
  2. 有兩位大神發現可以把 function call stack frame 放在 heap memory 中

    1. stack memory vs heap memory
      1. stack memory is a fixed of size memory

        int val = 0; // compiler can know the size of the programming
      2. 在 heap memory 中的空間不隨著 function return 而釋放,所以需要在用完後需要呼叫 free() 或是由 GC 處理,heap memory 是整個系統共用的

        int* ary = new int[n] // cannot know the size during compilation
  3. 什麼是 OO?

    1. combine data and function
      1. f(o)o.f()
    2. 模擬現實世界
    3. encapsulation, inheritance, and polymorphism
    4. 上述都對於作者來說都不是好的答案
  4. Encapsulation

    1. 並非是 OO 的專有概念

      struct Point {int x;int y;} // C 語言也可以封裝
    2. OO 的封裝並不完美

      1. C++
        1. the header file includes variables (the user doesn’t need to know)
      2. Java/ C#
        1. definition and implementation are in the same file
  5. Inheritance

    1. The goal of Inheritance is the re-usability of already implemented code by grouping common functionality of different classes in so-called base classes —- from How To Implement Inheritance and Polymorphism in C?
    2. 在 C 裡也可以手動完成類似的概念(透過轉型)
  6. Polymorphism

    1. In Polymorphism, we declare an Interface and implement the details in entities of different types —- from How To Implement Inheritance and Polymorphism in C?
    2. STDIN and STDOUT 早已包含 Polymorphism 的概念
      1. 每個 Unix 上的裝置都要實作 5 個 functions,這些 functions 都包含在 FILE Structure

        1. open
        2. close
        3. read
        4. write
        5. seek
        // device definition
        #include "file.h"
        void open(char* name, int mode) { /*...*/ }
        void close() { /*...*/ }
        int read() { /*...*/ }
        void write(char c) { /*...*/ }
        void seek(long index, int mode) { /*...*/ }
        
        struct FILE console = { open, close, read, write, seek }
        
        // somewhere
        extern struct FILE* STDIN;
        STDIN = exist_device
        int getchar() {
        	return STDIN->read();
        }
      2. 透過上述方法可以將每個 device 當成是一個 plugin,這種作法稱為 Plugin architecture

    3. C++ 透過 Virtual method table 找對應的 method 位置
    4. 在 OO 之前,polymorphism 的達成是需要去 init pointer 的,假如忘記 init 而直接用 pointer call function 是很危險的
    5. OO 導入 interface 讓 polymorphism 更安全
      1. initialization
    6. 依賴反轉延伸出依賴方向是可以自由選擇的
      1. business rule, UI, Database
      2. 互相獨立 → 可以讓不同 team 開發
  7. 上述的三個概念,沒有一個是由 OO 發明出來的

  8. OO 帶來的好處

    1. 以系統架構而言,通過使用 polymorphism 脫離程式碼的依賴取回控制權(程式碼的擺放位置變得自由)
      1. OO 讓 polymorphism 的使用變得更安全(可能是透過 .new)
    2. Plugin architecuture 減少依賴,讓各部件可以獨立開發和部屬

Functional Programming

  1. 增加限制在 assignment statement
    1. imposing discipline on assignment statement
  2. immutability architecture
    1. 任何的同步問題都是由可修改的變數而產生的,如 race condition, deadlock
  3. 隔離可變性
    1. 區分 immutable 和 mutable components
    2. immutable component 可以跟多個 mutable component 溝通
    3. mutable component 要用 transactional memory 保護變數
      1. RDMS database
    4. 盡可能地讓所有處理是 immutable 的
      1. pure function
  4. Event Sourcing
    1. 類似區塊鏈的概念
    2. 只有新增讀取,沒有刪除更新
    3. 前提是要有無窮的空間和處理效能

建立 SSH 連線到遠端伺服器

簡介

目前開發的專案都會放在雲端伺服器上,雖然 Heroku 這種微服務很方便,但最常用的還是直接開一個虛擬機,但是每次開一個新的虛擬機都要設定一些東西,而第一步我就會先設定好 SSH,因為沒辦法在本機上連到伺服器作業真的是太痛苦了QQ

設定流程

以下示範新增使用者到使用 SSH 進行無密碼登入:

  1. 新增使用者
  2. 使用 SSH 進行無密碼登入

新增使用者

建立一般使用者

  1. sudo passwd重設sudo密碼
  2. su成為超級使用者
  3. adduser username新增使用者
  4. 將新增的 user 加入 sudo 清單
    1. vim /etc/sudoers開啟sudo列表
    2. 在其中加入以下內容:
      username = ALL(ALL:ALL) ALL
      

建立部署專用使用者

為了安全性考量,會建立一個專門用來部署專案的使用者,這個使用者不會有密碼。

  1. adduser --disabled-password deploy建立一個沒有密碼的使用者

使用 SSH 進行無密碼登入

當完成上一個步驟時,就已經可以用 ssh username@serve_ip 連上了,但由於這種方式需要輸入使用者密碼,像是部署專用使用者就無法連上了,因此可以依照以下步驟完成不用密碼登入。

  1. 確認本地有金鑰,如果沒有執行 ssh-keygen 建立金鑰。
  2. 檢視本地的金鑰 cat ~/.ssh/username-ssh-key.pub,並且複製。

    金鑰通常位於 ~/.ssh/ 這個目錄中,如果沒有就會在執行 ssh-keygen 的當前目錄,username換成你生成金鑰時輸入的使用者名稱。

  3. 把本地的公鑰複製到伺服器使用者的認證金鑰:
    mkdir ~/.ssh
    touch ~/.ssh/authorized_keys
    vim ~/.ssh/authorized_keys # 貼上本地的金鑰
    chmod 700 ~/.ssh # 修改該目錄權限
    chmod 644 ~/.ssh/authorized_keys # 修改該檔案權限
  4. 測試看看ssh username@server_ip

如果不行可以嘗試使用ssh -i ~/.ssh/username-ssh-key username@server_ip

到這一步正常來說就可以在本地以 SSH 的方式連上伺服器了。可喜可樂~可喜可樂~

結語

之前在嘗試建立 SSH 連線到 GCP 時,踩了許多坑,尤其是使用 ssh-copy-id 這種方式,直接讓我連 server 都 Ping 不到了,我也不知道為什麼,感覺應該是被 Ban 掉了,因此此篇紀錄的是目前試下來最穩的方式。

SQL JSON Column

Available version

  • MySQL5.7
  • PostgreSQL 9.2

PostgreSQL

  • JSON type(text + validation)
  • JSONB type

MySQL

  • JSON type(as PG JSONB)

  • What’s the difference between JSON and JSONB in PG

    JSON

    • As Text type

    JSONB

    • Stored in internal format(Binary format), in order to use a key or array index to find a subobject without reading the whole JSON
    • Compare with JSON type has these benefits
      • Validation
      • Can update subobject or nest object by JSON functions
      • Fast to find the subobject or nest object
      • Slightly slower to input

    Semantically-insignificant details(white space…)

    SELECT '{"bar": "baz", "balance": 7.77, "active":false}'::json;
                          json
    -------------------------------------------------
     {"bar": "baz", "balance": 7.77, "active":false}
    (1 row)
    
    SELECT '{"bar": "baz", "balance": 7.77, "active":false}'::jsonb;
                          jsonb
    --------------------------------------------------
     {"bar": "baz", "active": false, "balance": 7.77}
    (1 row)

    Convert Value to numeric

    SELECT '{"reading": 1.230e-5}'::json, '{"reading": 1.230e-5}'::jsonb;
             json          |          jsonb
    -----------------------+-------------------------
     {"reading": 1.230e-5} | {"reading": 0.00001230}
    (1 row)
  • How big about JSON column

    • space: roughly the same as for LONGBLOB or LONGTEXT(approx 4 GB)
      • limited by max-allowed-packet variable
  • How can I use JSON Column

    mysql> CREATE TABLE t1 (jdoc JSON);
    Query OK, 0 rows affected (0.20 sec)
    
    mysql> INSERT INTO t1 VALUES('{"key1": "value1", "key2": "value2"}');
    Query OK, 1 row affected (0.01 sec)
    
    mysql> INSERT INTO t1 VALUES('[1, 2,');
    ERROR 3140 (22032) at line 2: Invalid JSON text:
    "Invalid value." at position 6 in value (or column) '[1, 2,'.
    • Keys must be a string(Rails developers should pay attention)
    • JSON can contain strings or numbers, the JSON null literal, or the JSON boolean true or false literals
    JSON primitive type PostgreSQL type Notes
    string text \u0000 is disallowed, as are unicode escapes representing characters not available in the database encoding
    number numeric NaN and infinity values are disallowed
    boolean boolean Only lowercase true and false spellings are accepted
    null (none) SQL NULL is a different concept
  • How can I search for value in the JSON column

    using JSON Function with JSON path to searching,(e.g. $.name, $[0], ->)

    MySQL(function + JSON path)

    Prefer function

    mysql> SELECT JSON_EXTRACT('{"a": 1, "b": 2, "c": [3, 4, 5]}', '$.*');
    +---------------------------------------------------------+
    | JSON_EXTRACT('{"a": 1, "b": 2, "c": [3, 4, 5]}', '$.*') |
    +---------------------------------------------------------+
    | [1, 2, [3, 4, 5]]                                       |
    +---------------------------------------------------------+
    mysql> SELECT JSON_EXTRACT('{"a": 1, "b": 2, "c": [3, 4, 5]}', '$.c[*]');
    +------------------------------------------------------------+
    | JSON_EXTRACT('{"a": 1, "b": 2, "c": [3, 4, 5]}', '$.c[*]') |
    +------------------------------------------------------------+
    | [3, 4, 5]                                                  |
    +------------------------------------------------------------+
    mysql> SELECT JSON_EXTRACT('{"a": {"b": 1}, "c": {"b": 2}}', '$**.b');
    +---------------------------------------------------------+
    | JSON_EXTRACT('{"a": {"b": 1}, "c": {"b": 2}}', '$**.b') |
    +---------------------------------------------------------+
    | [1, 2]                                                  |
    +---------------------------------------------------------+

    PG(JSON path)

    Prefer operator

    SELECT doc->'site_name' FROM websites
  • How can I update the value in the JSON column

    MySQL

    Partial Update

    mysql> SELECT JSON_SET('"x"', '$[0]', 'a');
    +------------------------------+
    | JSON_SET('"x"', '$[0]', 'a') |
    +------------------------------+
    | "a"                          |
    +------------------------------+
    1 row in set (0.00 sec)

    Merge

    mysql> SELECT
        ->   JSON_MERGE_PRESERVE('{"a": 1, "b": 2}', '{"c": 3, "a": 4}', '{"c": 5, "d": 3}') AS Preserve,
        ->   JSON_MERGE_PATCH('{"a": 3, "b": 2}', '{"c": 3, "a": 4}', '{"c": 5, "d": 3}') AS Patch\G
    *************************** 1. row ***************************
    Preserve: {"a": [1, 4], "b": 2, "c": [3, 5], "d": 3}
       Patch: {"a": 4, "b": 2, "c": 5, "d": 3}

    with Update

    mysql> update t set json_col = json_set(json_col, '$.age', age + 1);
    Query OK, 16 rows affected (13.56 sec)
    Rows matched: 16  Changed: 16  Warnings: 0

    Some limits to Partial Update

    • The column type is JSON
    • Using JSON Update functions
    • The input column and the target column must be in the same column
      • UPDATE mytable SET jcol1 = JSON_SET(jcol2, '$.a', 100)
    • All changes replace existing array or object values with new ones
    • The new value cannot be larger than the old value

When to Use JSON Column?

  • Using JSON for Logging Purposes
  • To Store Permissions and Configurations
  • To Avoid Slow Performance on Highly Nested Data

When To Avoid JSON Data in a Relational Database?

  • You are Not Sure what Data to Store In the JSON Column

  • You Do Not Want to Deal With Complex Queries

    • query with json column
    SELECT
      U1.id AS user1,
      U2.id AS user2,
      U1.jsonInfo->>'name' AS "U1 Name",
      U2.jsonInfo->>'name' AS "U1 Name",
      A1->>'address' AS "address"
    FROM user U1
    inner join user U2 on (U1.id > U2.id)
    cross join lateral jsonb_array_elements(U1.jsonInfo->'addresses') A1
    inner join lateral jsonb_array_elements(U2.jsonInfo->'addresses') A2 on (A1->>'address' = A2->>'address')
    • query with primitive column
    SELECT
      U1.id AS user1,
      U2.id AS user2,
      U1.name AS "U1 Name",
      U2.name AS "U2 Name",
      A1.address AS address
    FROM
      user U1
      INNER JOIN user U2 ON (U1.id > U2.id)
      INNER JOIN user_address A1 ON (U1.id = A1.user_id)
      INNER JOIN user_address A2 ON (U2.id = A2.user_id)
    WHERE
      A1.address = A2.address;
  • You Have a Strongly Typed ORM

    • ActiveRecord

Use case

公司有 Orders 這一個表,在結帳時有時會有需要留備註的功能,但留下備註的人有所不同,例如:店員、店長、顧客、網站管理員……等等,在這種情境下我有幾種做法

  1. 直接開 column 在這個 table 上
    1. 適合小流量、資料少的情況
  2. 開一個 JSON column 在 order 表上
    1. 適合這些備註只需要內容的情況
  3. 開一個備註表,用 poly association
    1. 適合這些備註不只要內容,還需要知道是哪個使用者留言的情況
  4. 為每一個備註都開一張表
    1. 適合這些備註各個差異都很大,且需要複雜關聯的情況

References

Rails Deploy 筆記

簡介

近期 GCP 的免費額度快沒了,身為一位免費仔我決定再開一個帳號進行移機動作,但每次移機都會發現一些詭異的事情導致浪費一整天,故寫下這個筆記希望以後移機可以快一點。

設定流程

開始設定

設定 SSH

請參考 建立ssh連線到遠端伺服器

安裝 RVM

由於 rvm 在 Ubuntu 有專用的軟體包,以下指令為 此篇 擷取:

sudo apt-get install software-properties-common
sudo apt-add-repository -y ppa:rael-gc/rvm
sudo apt-get update
sudo apt-get install rvm
sudo usermod -a -G rvm $USER

安裝完後記得把 RVM 加入環境變數,之後執行 source /home/deploy/.bashrc

安裝 Ruby on Rails

rvm install 2.7.2
gem install bundler
gem install rails -v 6.1.2
rails --version

安裝 NVM

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash

將 NVM 加入環境變數後,執行 source /home/deploy/.bashrc

安裝 Nodejs 和 Yarn

nvm install node
npm install -g yarn

安裝 Database

MySQL

sudo apt-get install mysql-common mysql-client libmysqlclient-dev mysql-server
mysql -u root -p
CREATE DATABASE databasename CHARACTER SET utf8mb4;

如果遇到 ERROR 1698 (28000): Access denied for user 'root'@'localhost' 參考 此篇

PostgreSQL

sudo apt-get install postgresql libpq-dev postgresql-contrib
sudo -u postgres psql
\password
\quit
sudo -u postgres createdb demo-production

安裝 Passenger + Nginx

擷取 此篇 內容,執行以下指令:

sudo apt-get install nginx

sudo apt-get install -y dirmngr gnupg
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 561F9B9CAC40B2F7
sudo apt-get install -y apt-transport-https ca-certificates

sudo sh -c 'echo deb https://oss-binaries.phusionpassenger.com/apt/passenger xenial main > /etc/apt/sources.list.d/passenger.list'
sudo apt-get update

sudo apt-get install -y nginx-extras passenger

安裝完後重啟server sudo service nginx restart

確認安裝成功

sudo /usr/bin/passenger-config validate-install #選擇`Passenger itself`
sudo /usr/sbin/passenger-memory-stats # 確認Nginx和Passenger有在運行

設定 Passenger + Nginx

編輯 /etc/nginx/nginx.conf

    # 讓 Nginx 可以讀到環境變量 PATH,Rails 需要這一行才能調用到 nodejs 來編譯靜態檔案
+   env PATH;

    user www-data;
    worker_processes auto;
    pid /run/nginx.pid;

    events {
    worker_connections 768;
    # multi_accept on;
    }

http {

    # 關閉 Passenger 和 Nginx 在 HTTP Response Header 的版本資訊,減少資訊洩漏
+   passenger_show_version_in_header off;
+   server_tokens       off;

    # 設定檔案上傳可以到100mb,默認只有1Mb超小氣的,上傳一張圖片就爆了
+   client_max_body_size 100m;

    gzip on;
    gzip_disable "msie6";

    # 最佳化 gzip 壓縮
+   gzip_comp_level    5;
+   gzip_min_length    256;
+   gzip_proxied       any;
+   gzip_vary          on;
+   gzip_types application/atom+xml application/javascript application/x-javascript application/json application/rss+xml application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/svg+xml image/x-icon text/css text/xml text/plain text/javascript text/x-component;

    # 打開 passenger 模組
-   # include /etc/nginx/passenger.conf;
+   include /etc/nginx/passenger.conf;

新增/etc/nginx/sites-available/${project-name}這個檔案,並填入以下內容:

server {
    listen 80;
    server_name 1.2.3.4;   # 用你自己的服務器 IP 位置

    root /home/deploy/${project-name}/current/public; # 用你自己的項目名稱位置

    passenger_enabled on;

    passenger_min_instances 1;

    location ~ ^/assets/ {
        expires 1y;
        add_header Cache-Control public;
        add_header ETag "";
        break;
    }
}

執行以下指令:

cd ../sites-enabled/
sudo ln -s ../sites-available/${project-name} .
service nginx restart

新增 Ngork

新增 etc/nginx/sites-available/ngrok,填入以下內容:

server {
  server_name ngrok.yourdomain.com;
  # 這邊設定你要設定的 url
  location / {
      proxy_pass http://localhost:3333/;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header Host $host;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto https; # 如果有設定 https 要加上這行
  }
}

執行以下指令:

cd ../sites-enabled/
sudo ln -s ../sites-available/ngrok .
service nginx restart

Bundle lock 新增 Linux Platform

至專案中執行以下指令:

bundle lock --add-platform x86_64-linux

執行完後要再推到 Repo

加入 Capistrano Gem 至專案

Gemfiledevelopment 區塊中加入以下 Gem:

gem "capistrano-rails"
gem "capistrano-passenger"
gem 'capistrano-rvm'

接著執行以下指令:

bundle install
cap install

設定 Capistrano

編輯 Capfile,加入以下內容

require 'capistrano/rails'
require 'capistrano/passenger'
require "capistrano/rvm"

編輯config/deploy.rb

+  sh "ssh-add"

   # config valid only for current version of Capistrano
   lock "3.8.1"

-  set :application, "my_app_name"
+  set :application, "demo"   # 請用你自己的項目名稱

-  set :repo_url, "[email protected]:me/my_repo.git"
+  set :repo_url, "[email protected]:growthschool/rails-recipes.git"    # 請用你自己項目的git位置

+  set :rvm_custom_path, '/usr/share/rvm'  # only needed if not detected
   # Default branch is :master
   # ask :branch, `git rev-parse --abbrev-ref HEAD`.chomp

   # Default deploy_to directory is /var/www/my_app_name
   # set :deploy_to, "/var/www/my_app_name"
+  set :deploy_to, "/home/deploy/demo"     # 這樣服務器上代碼的目錄位置,放在 deploy 帳號下。請用你自己的項目名稱。

   # Default value for :format is :airbrussh.
   # set :format, :airbrussh

   # You can configure the Airbrussh format using :format_options.
   # These are the defaults.
   # set :format_options, command_output: true, log_file: "log/capistrano.log", color: :auto, truncate: :auto

   # Default value for :pty is false
   # set :pty, true

   # Default value for :linked_files is []
-  # append :linked_files, "config/database.yml", "config/secrets.yml"
+  append :linked_files, "config/database.yml", "config/secrets.yml"

   # Default value for linked_dirs is []
-  # append :linked_dirs, "log", "tmp/pids", "tmp/cache", "tmp/sockets", "public/system"
+  append :linked_dirs, "log", "tmp/pids", "tmp/cache", "tmp/sockets", "public/system"

+  set :passenger_restart_with_touch, true

   # Default value for default_env is {}
   # set :default_env, { path: "/opt/ruby/bin:$PATH" }

   # Default value for keep_releases is 5
-  # set :keep_releases, 5
+  set :keep_releases, 5

編輯 config/deploy/production.rb

+   set :branch, "master"
-   # server "example.com", user: "deploy", roles: %w{app db web}, my_property: :my_value
+   server "demo.wells.tw", user: "deploy", roles: %w{app db web}, my_property: :my_value

設定完成後執行以下指令:

cap production deploy:check

因為還沒有新增 database.yml 所以會報錯。

新增 database.yml

/home/deploy/demo/shared/config 中新增 database.yml,如果使用 PostgreSQL 填入以下內容:

default: &default
  adapter: postgresql
  encoding: unicode
  pool: 25
  host: localhost
  username: postgres
  password: password

production:
  <<: *default
  database: demo-production

新增 secrets.yml

在本地專案執行:

rails secret

將產生的金鑰依照下方格式貼在 Server 的 /home/deploy/demo/shared/config/secrets.yml

production:
  secret_key_base: 把剛剛的亂數key貼上來

建立憑證

參考 此篇 擷取其中內容,執行以下程式:

sudo apt-get update
sudo apt-get install software-properties-common
sudo add-apt-repository ppa:certbot/certbot
sudo apt-get update
sudo apt-get install python-certbot-nginx

產生憑證指令:

sudo certbot --nginx -d hellojcc.tw -d www.hellojcc.tw

註銷憑證指令(有用到再執行):

certbot revoke --cert-path /etc/letsencrypt/archive/${YOUR_DOMAIN}/cert1.pem

安裝 Chromedriver

因為專案需要爬蟲,因此要裝 Chromedriver,執行以下指令:

apt install chromium-chromedriver

常見狀況

相同 Domain Name 但 SSH 連不上

遇到類似以下錯誤訊息:

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
github.com,52.69.186.44 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9ID@       WARNING: POSSIBLE DNS SPOOFING DETECTED!          @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
The ECDSA host key for test.wells.tw has changed,
and the key for the corresponding IP address 34.81.121.249
is unchanged. This could either mean that
DNS SPOOFING is happening or the IP address for the host
and its host key have changed at the same time.
Offending key for IP in /Users/wells/.ssh/known_hosts:9
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the ECDSA key sent by the remote host is
SHA256:EC3r9M29PBWfOLIuF32Ha3meLK/wzu884s7/29tsxGs.
Please contact your system administrator.
Add correct host key in /Users/wells/.ssh/known_hosts to get rid of this message.
Offending ECDSA key in /Users/wells/.ssh/known_hosts:3
ECDSA host key for test.wells.tw has changed and you have requested strict checking.
Host key verification failed.

至本地開啟 known_hosts,並刪除有問題的 Domain

vim  ~/.ssh/known_hosts

假如 Server 上的 RVM 找不到

參考 Capistrano 官方文件,在本地的config/deploy.rb新增以下內容:

set :rvm_custom_path, '/usr/share/rvm'  # only needed if not detected

使用Template美化你的Jekyll專案

料理食材(內文內容)

  • Jekyll的使用
  • Jekyll Template

誰可以安心食用(適合誰讀)

服用完你會獲得什麼

  • 知道如何亂改別人的Code使用Jekyll模板
  • 幫你抓幾隻蟲、踩幾個雷,節省你好幾個小時的時間

挑選Jekyll Template

一個靜態網頁的生成器受歡迎程度除了本身的性能(速度、穩定度)外,有很大的因素取決於社群,關於Jekyll網路上有許多熱心人士提供各式各樣的主題,像是jekyllthemes.org這個網站中就有各式各樣的模板供你選擇,但要特別注意的是不同的模板要求的jekyll版本也是會不同的。

如果網站上的模板都不得你喜好,要自己刻一個也是沒問題的,關於Jekyll的官方文件可以查閱此處

這邊我選擇chirpy這個模板,點進去之後可以再點擊Demo頁面,或是按下前面的chirpy也可以進入此畫面:

這些模板的提供者通常都會把使用說明放在Demo網站上,個人覺得這種做法非常的高效,充分的發揮了這個Demo的價值及教育意義。

使用Theme Gem安裝Jekyll Template

如果想要使用這個模板有很多種方式,這邊我們示範使用Theme Gem的方法

  1. 在Jekyll專案的GemFile中加入以下這段:
    gem "jekyll-theme-chirpy"
  2. 在Jekyll專案的_config.yml中加入以下這段:
    theme: jekyll-theme-chirpy
  3. 使用bundler安裝GemFile中的Gem
    bundle
  4. 下載chirpy的Github專案
    使用git clone https://github.com/cotes2020/chirpy-starter.git下載
  5. 複製專案中的以下內容到你自己的專案
    .
    ├── _data
    ├── _plugins
    ├── _tabs
    ├── _config.yml
    └──  index.html
    
    此時運行bundle exec jekyll s你會發現已經有畫面了,但是卻沒有任何文章。
  6. 在自己的專案中刪除以下文件
    .
    ├──  about.markdown
    └──  index.markdown
    
    此時再重新開啟server,就會出現以下畫面了。
  7. 修改_config.yml中的以下變數:
    • url換成你推上github page的url,例如https://blog.wells.tw
    • avatar大頭貼的圖檔(使用圓框遮罩修改後更佳)
    • timezone換成你所在位置的時區,例如 Asia/Taipei
    • lang換成你的語言,例如Zh-TW

到這一步基本的建置功能就都已經完成了,在_post/中新增文章進行撰寫吧!

更改專案中的favicon

favicon就是在網站名稱左邊的小小圖片,這東西是可以自己定義的

首先先到Real Favicon Generator選擇上傳要當作facicon的圖片,上傳會可以在最下面點擊Generate your Favicons and HTML code,即可得到一個zip的檔案

解壓縮後刪除裡面的兩個東西:

  • browserconfig.xml
  • site.webmanifest

之後把所有的圖片檔案都複製到自己專案中的assets/img/favicons/資料夾中,之後你就可以看到你的網站名稱旁已經有你剛剛放置的icon了。

部署Jekyll專案

在本地端寫好檔案後我們就部署到Github Page上,關於如何部署請看這裡

但...等等!!怎麼有點落差...

這是Demo的網頁

接著...讓我們按下F12來Debug一下

可以發現這裡的錯誤主要的原因是找不到檔案,所以我們來檢查一下網址

http://blog.wells.tw/jekyll-demo/

這一段網址http://blog.wells.tw/是domain name,而後面的jekyll-demo則是專案的名稱,再打開Jekyll專案中的_config.yml


這邊可以發現需要填入專案名稱baseurl是空的,因此我們把它更改為baseurl: /jekyll-demo

jekyll-demo換成你的專案名稱

之後在終端機輸入

git add .
git commit -m "提交訊息"
git push

過幾秒後就可以看到正常的網站囉

結語

本篇文主要介紹如何使用Theme Gem的方式套上模板,但還有更快的方式,就是直接Fork專案XDD

但是自己一步一步完成就是有那種成就感啦!這篇結束後已經有一個blog該有的樣子了,下一篇開始我們來介紹一些對於部署更方便的功能吧!

我對於 Rails 中 Aggregation 的想法

現在的想法是 Rails 處理 models 的關聯非常強
應多善用這個特性在讀取方面
所以 has_many 和 has_one 不會因為不同 Aggregation 就禁止關聯

前幾天你說 aggregate root 是寫入單位
那我們應禁止類似的 code 在非 mode/order.rb 的檔案中出現
order.line_items.build(...)
order.save!

而應該寫出的是
order.build_line_items(...) # 封裝內部行為,也方便日後 override
order.save!

並且還要搭配 autosave 給 aggregation 的 entities(避免 children entities 沒有觸發 changed_for_autosave?)

但此時會有問題,就是 order.build_line_items 在不同 context 可能有不同的行為

為了應付這個情況,可以將 DCI 的概念應用上,不同 context 實作不同的 interaction,例如預購和搶購兩個 context 的 build_line_items 就可能不同

# 預購
module FutureBuy
  module Order
    def build_line_Items
       product = Product.find(id)
       # 不確認庫存
    end
  end
end

# 搶購
module LimitBuy
  module Order
    def build_line_Items
       product = Product.find(id)
       raise StockInsufficientError, 'product 庫存不足' if product.stock <= 0
    end
  end
end

看了蒼時文章裡介紹的 aggregate root 是指有關於讀取的,不清楚寫入是怎麼處理的
https://blog.aotoki.me/posts/2023/10/20/rails-in-practice-aggregate-and-boundary/

Ruby 搭配 Sketchup 學習筆記(四)

前情提要

Sketchup 是一款在建築、都市計畫和遊戲開發都頗有名氣的 3D 建模軟體,而 Ruby 則是一個程式語言,它可以搭配 Sketchup 達成程式化 建模的任務,近期經由系主任引薦,要開發 Sketchup 的 Extension,雖然我寫過 Ruby,但 Sketchup 則是完全沒碰過,於是利用文章來記錄所學的一點一滴。

本篇內容

  • 邊線(Edges)
  • 向量
  • 二維表面
  • 三維物體
  • 文字
  • 轉形

邊線(Edge)

邊線(Edge)是一個物件,還記得之前畫直線跟畫圓形時最大的差別在於畫圓形回傳的是一個 Array,嚴格來說是一個由 Edge 組成的 Array,而 Entity 提供一個方法可以讓我們以其中一個 Edge 得到其相連的全部 Edges,以下為程式碼:

# Default code, use or delete...
mod = Sketchup.active_model # Open model
ent = mod.entities # All entities in model
origin = [0,0,0]
normal = [0,0,1]
radius = 10

polygon = ent.add_ngon origin, normal, radius, 6
entity1 = ent[1] # ent[0] 是 Steve

edges = entity1.all_connected
puts edges.to_s

輸出:

[#<Sketchup::Edge:0x00007f87663ce688>, #<Sketchup::Edge:0x00007f87663ce728>, #<Sketchup::Edge:0x00007f87663ce700>, #<Sketchup::Edge:0x00007f87663ce6d8>, #<Sketchup::Edge:0x00007f87663ce6b0>, #<Sketchup::Edge:0x00007f87663ce750>]
true

經由 .all_connected 我們可以得到整個 Array,這在之後建立表面時十分有用。

Edge 提供了幾個有用的方法:

  • length:取得直線長度(單位為目前設定的度量單位)
  • start:傳回起點(Vertex 物件)
  • end:傳回終點(Vertex 物件)
  • vertices:傳回兩的端點(由 Vertex 物件組成的 Array)
  • other_vertex:提供一個端點作為參數,回傳另一個端點
  • split:分割直線;需傳入該直線上的一個端點,分割後為新直線的端點
  • used_by?:判斷指定的端點是否在直線上

以下程式碼可供測試:

test_line = Sketchup.active_model.entities.add_line [0, 0, 0], [9, 9, 0]
test_line.length # 1' 3/4
new_line = test_line.split [6, 6, 0]
test_line.length # 8 1/2
new_line.length # 4 1/4
test_line.start.position # (0", 0", 0")
vertex2 = test_line.end
vertex2.position # (6", 6", 0")

vertex1 = test_line.other_vertex vertex2
vertex1.position # (0", 0", 0")
new_line.start.position # (6", 6", 0")
new_line.end.position # (9", 9", 0")
test_line.used_by? vertex1 # true
new_line.used_by? vertex1 # false

向量

在 Sketchup 中畫直線時,經由起點和終點可以產生出向量,而在二維表面中,向量則決定該面該朝哪一個方向。例如希望做一個垂直於 X 軸的表面,該向量就會是 [1,0,0]。

二維表面

建立第一個二維表面

要生成二維表面有個前提,就是必須用 Edges 圍成一個封閉的區域,要生成二維表面使用的是 .add_face,之後傳入一個由 Edges 組成的 Array,因此可以先生成一個幾何圖形,之後再用該回傳的物件生成表面。以下示範製作一個矩形表面:

# Default code, use or delete...
mod = Sketchup.active_model # Open model
ent = mod.entities # All entities in model

depth = 50
width = 100

pt1 = [0, 0, 0]
pt2 = [width, 0, 0]
pt3 = [width, depth, 0]
pt4 = [0, depth, 0]

test_face = ent.add_face pt1,pt2,pt3,pt4

執行結果:
image

之後我們可以來看一下產生一個表面對於 Entity 的影響,以下是 Ruby Console:

> ent.size
1
> # Default code, use or delete...
mod = Sketchup.active_model # Open model
ent = mod.entities # All entities in model

depth = 50
width = 100

pt1 = [0, 0, 0]
pt2 = [width, 0, 0]
pt3 = [width, depth, 0]
pt4 = [0, depth, 0]

test_face = ent.add_face pt1,pt2,pt3,pt4

#<Sketchup::Face:0x00007f8765c67b10>
> ent.size
6

可以發現在生成表面後,Entity 的 size 從 1 變成了 6,也就是一次增加了 5 個實體物件,而這 5 個物件分別是:

  1. 表面
  2. 端點1
  3. 端點2
  4. 端點3
  5. 端點4

這 5 個物件是一個陣列,指的是這個平面物件。之後我們先選取的方式來確認這 4 個端點:

  1. 先點擊該平面
  2. 執行以下程式碼:
# Default code, use or delete...
mod = Sketchup.active_model # Open model
sel = mod.selection # Current selection

vertices = sel[0].vertices

UI.messagebox "第一個端點#{vertices[0].position.cm}\n第二個端點#{vertices[1].position.cm}\n第三個端點#{vertices[2].position.cm}\n第四個端點#{vertices[3].position.cm}\n"

結果:
image

Face 物件搭配向量

依照上方的結果,我們可以發現一件事(單位問題請省略),那就是寫入的端點順序與真正的端點順序並不相同,經由「右手四指定則」的四指和該端點的順序相比,可以發現大拇指是朝下的,也就是向量為 [0,0,-1],那該如何修正呢?可以使用以下指令:

test_face.normal # 確認修改之前的向量
test_face.reverse! # 把向量方向反轉
test_face.normal # 再次確認向量

Face 類別的方法

以下紀錄幾個有用的方法

  • area:回傳表面的面積
  • edges:回傳組成表面的 Edges 陣列
  • followme:沿著 Edges 陣列建立 3D 圖形
  • pushpull:用來推拉表面形成 3D 圖形
  • classify_point:回傳指定點在表面上的位置代表值
    • 0:無法判斷
    • 1:點在表面上
    • 2:點在端點上
    • 4:點在邊線上
    • 16:點在表面同一平面的外側或空洞中
    • 32:點不在表面平面上

三維物體

三維物體可以藉由二維表面拉抬而成,以下紀錄兩種方法

pushpull 方法

pushpull 會將該二維表面依向量拉抬。範例程式碼示範如何製作一個立方體:

# Default code, use or delete...
mod = Sketchup.active_model # Open model
ent = mod.entities # All entities in model

test_face = ent.add_face [0,0,0],[10,0,0],[10,10,0],[0,10,0]

test_face.pushpull 10

輸出結果:

image

為什麼是往下的呢?抓出 test_face 的四個 vertex 再用「右手定則」比比看吧。

如果希望把它切角的話,該怎麼做呢?

# Default code, use or delete...
mod = Sketchup.active_model # Open model
ent = mod.entities # All entities in model

test_face = ent.add_face [0, 0, 0], [10, 0, 0], [10, 10, 0], [0, 10, 0]

test_face.reverse!
test_face.pushpull 10

cut_line = ent.add_line [10, 8, 10], [8, 10, 10] # 將表面一分為二

cut_line.faces[1].pushpull -10 # 將第二個表面往下壓

輸出結果:

image

在這個正方體的表面新增一條線,將此表面拆為兩份,並且將第二份往下壓。

followme 方法

followme 與 pushpull 的差別在於其可以依照指定的路徑拉出三維圖形,也因此其可以不用考慮目前表面的向量,試想想你該如何用 pushpull 拉出一個有弧度的圓柱體呢?這一點 followme 輕鬆就能做到。

立方體

同樣先建立一個立方體,這次使用 followme:

# Default code, use or delete...
mod = Sketchup.active_model # Open model
ent = mod.entities # All entities in model

depth = 10
width = 10

pt1 = [0, 0, 0]
pt2 = [width, 0, 0]
pt3 = [width, depth, 0]
pt4 = [0, depth, 0]

test_face = ent.add_face pt1, pt2, pt3, pt4

point1 = [0, 0, 0]; point2 = [0, 0, 10]
path = ent.add_line point1, point2
test_face.followme path

輸出結果:

image

其實還滿好理解的,假如要用 followme 的話,我們只需要給它一條線,之後他就會沿著這條線拉出立體圖了。

立方體切邊

接著來看看假如想要將立方體周邊的角都去掉,該怎麼做:

# Default code, use or delete...
mod = Sketchup.active_model # Open model
ent = mod.entities # All entities in model

test_face = ent.add_face [0, 0, 0], [10, 0, 0], [10, 10, 0], [0, 10, 0]
test_face.pushpull -10

cut = ent.add_line [0, 0, 9], [1, 0, 10]
cut.faces[0].followme test_face.edges

輸出結果:

image
還記得 edges 是什麼嗎?就是表面周圍的邊。

圓柱彎管

在這範例中,我們先建立一個圓形的表面,然後使用三角函數 sin 製作一個弧度路徑,之後使用 followme沿著這條路徑生成,最後再把這條路徑刪除。

# Default code, use or delete...
mod = Sketchup.active_model # Open model
ent = mod.entities # All entities in model

# 製作圓形表面
circle = ent.add_circle [0, 0, 0], [1, 1, 0], 5 # 畫一個圓框
circle_face = ent.add_face circle # 使用圓框製作平面

# 製作路徑
pt = []
for i in 0..135
  pt[i] = [i, 100 * Math::sin(i.degrees), 0]
end
path = ent.add_curve pt

# 沿著路徑生成立體圖形
circle_face.followme path

# 刪除路徑
ent.erase_entities path

輸出結果:

image

球體和甜甜圈

要做出球體的道理很簡單,就跟把硬幣旋轉一樣,原本硬幣是圓形平面,當把它旋轉時,它將變成一個球體,在 Sketchup 中也是類似的做法,請看以下:

# Default code, use or delete...
mod = Sketchup.active_model # Open model
ent = mod.entities # All entities in model

center = [0, 0, 0]
radius = 5
# 製作圓形表面
circle = ent.add_circle center, [1, 1, 0], radius # 畫一個圓框
circle_face = ent.add_face circle # 使用圓框製作平面

# 製作路徑
path = ent.add_circle center, [0, 0, 1], radius+1 # 要比半徑大一點,不然刪不掉

# 沿著路徑生成立體圖形
circle_face.followme path

# 刪除路徑
ent.erase_entities path

輸出結果:

image

我們將二維表面與路徑做垂直,之後直接 followme 就可以得到這個結果。

假如我們把路徑的中心點偏移一點的話...

# Default code, use or delete...
mod = Sketchup.active_model # Open model
ent = mod.entities # All entities in model

center = [0, 0, 0]
radius = 5
# 製作圓形表面
circle = ent.add_circle center, [1, 1, 0], radius # 畫一個圓框
circle_face = ent.add_face circle # 使用圓框製作平面

# 製作路徑
path = ent.add_circle [-5, 0, 0], [0, 0, 1], radius + 1 # 要比半徑大一點,不然刪不掉

# 沿著路徑生成立體圖形
circle_face.followme path

# 刪除路徑
ent.erase_entities path

輸出結果:

image

看呀!它就變成甜甜圈了。

文字

二維文字

二維文字通常用作標示用途,其有 overloading 兩種用法,第一種是傳入兩個參數,第二種是傳入三個參數,以下看比較:

參數數量 第一個參數 第二個參數 第三個參數
2 放置要呈現的文字 文字開頭位置
3 放置要呈現的文字 箭頭開頭位置 箭頭結束位置(空格後文字開頭)

測試看看:

# Default code, use or delete...
mod = Sketchup.active_model # Open model
ent = mod.entities # All entities in model

ent.add_text "這是兩個參數的 add_text!", [0,0,0]
ent.add_text "這是三個參數的 add_text!", [0,0,0],[0,5,5]

輸出結果:

image

除此之外 Text 還有幾個方法可以用:

  • text=:顯示的文字
  • point=:文字起點或箭頭起點
  • vector=:箭頭引出現終點的位置
  • line_weight=:引出線的粗細
  • arrow_type=:箭頭的外觀
    • 0:隱藏箭頭
    • 1:橫線形
    • 2:點形
    • 3:封閉形
    • 4:開放形
  • leader_type=:引出線的樣式
    • 0:不作用
    • 1:檢視型(不會隨著檢視角度變化)
    • 2:圖釘型(隨著檢視角度變化)

三維文字

三維文字通常都是模型的一部分,使用三維文字的方法是 .add_3d_text,當然它還是在 Entity 類別之下,使用這個方法的參數有不少,以下是其參數:

 .add_3d_text(string, alignment, fontname, bold, inalic, height, tolerance, baseZ, filled, extrusion)

基本上還滿好理解,除了幾個比較特別的額外介紹:

  • tolerance:可以把它當作解析度,但這個值越高,品質越差
  • baseZ:Z軸的尺寸
  • filled:是否為實心字
  • extrusion:若為實心字,設定三維文字的深度

範例程式碼:

# Default code, use or delete...
mod = Sketchup.active_model # Open model
ent = mod.entities # All entities in model

string = "Hello, World"

ent.add_3d_text(string, TextAlignLeft, "Arial", true, false, 1.0, 0.0, 0.5, true, 5.0)

執行結果:

image

轉形

轉型可以來進行移動、旋轉和縮放,在 Entities 類別中提供了 transform_entities 的方法,以下是範例:

# Default code, use or delete...
mod = Sketchup.active_model # Open model
ent = mod.entities # All entities in model

test_face = ent.add_face [0, 0, 0], [30, 0, 0], [30, 15, 0], [0, 15, 0]
test_face.pushpull 10

roof_line = ent.add_line [15, 0, 5], [15, 15, 5]

tr = Geom::Transformation.translation [0, 0, 10]
ent.transform_entities tr, roof_line

輸出結果:

image
是不是覺得很神奇,怎麼突然就有個屋頂,依我的理解,此段程式應該是當 roof_line 建立時,因為上 test_face 的表面上,因此它會將該表面畫分為二,當 roof_line 被拉到 [0,0,10] 時,由於物件相連,因此那兩個表面也會被拉抬。

Geom::Transformation 共有三種方法,分別是:

  • translation:轉移
  • ratation:旋轉
  • scaling:縮放

Translation 轉移

Translation 轉移所接受的參數是轉移前後的距離,共有 3 種方式可以執行,以下逐一示範:

# Default code, use or delete...
mod = Sketchup.active_model # Open model
ent = mod.entities # All entities in model

tran_face = ent.add_face [0, 0, 0], [30, 0, 0], [30, 15, 0], [0, 15, 0]

# 第一種
t = Geom::Transformation.new [15, 0, 0]
ent.transform_entities t, tran_face

# 第二種
t = Geom::Transformation.translation [0, 12, 0]
ent.transform_entities t, tran_face

# 第三種
ent.transform_entities [-13, 0, 0], tran_face

最後輸出只會看到結果,假如想到確認每一步,可以按下 ctrl+z 一步一步退回。

Ratation 旋轉

旋轉的需要三個資訊,分別是:

  1. 旋轉的定點
  2. 旋轉的軸
  3. 旋轉的角度

以下示範旋轉的做法:

# Default code, use or delete...
mod = Sketchup.active_model # Open model
ent = mod.entities # All entities in model

tran_face = ent.add_face [0, 0, 0], [30, 0, 0], [30, 15, 0], [0, 15, 0]

# 第一種
t = Geom::Transformation.new [0, 0, 0], [0, 1, 0], 30.degrees
ent.transform_entities t, tran_face

# 第二種
t = Geom::Transformation.rotation [0, 0, 0], [0, 0, 1], 90.degrees
ent.transform_entities t, tran_face

輸出結果:

image
第二個參數一樣可以用「右手定則輔助」。

Scaling 縮放

縮放要特別注意的是除了面積會縮小外,端點也會縮小。

以下示範縮放:

# Default code, use or delete...
mod = Sketchup.active_model # Open model
ent = mod.entities # All entities in model

tran_face = ent.add_face [0, 0, 0], [30, 0, 0], [30, 15, 0], [0, 15, 0]

# 第一種
t = Geom::Transformation.new 0.5
ent.transform_entities t, tran_face

# 第二種
t = Geom::Transformation.scaling 0.5
ent.transform_entities t, tran_face

輸出結果:

image
因為縮小的 0.5 倍兩次,因此為原本的 0.25 倍。

Geom::Transformation.scaling 總共有 overloading 四種定義,以下逐一說明:

參數數量 第一個參數 第二個參數 第三個參數 第四個參數
1 比例
2 新原點 比例
3 X軸比例 Y軸比例 Z軸比例
4 新原點 X軸比例 Y軸比例 Z軸比例

以上共四種,但要特別注意的是新原點也會受到比例縮放的影響。

綜合應用

轉形共有 3 種不同的功用,那假如想要轉移之後旋轉,是不是就要寫兩段呢?其實不用,只要把轉移和移動 Geom::Transformation.new 出來的物件以 * 進行連接即可。

請注意,假如是 total = t_rotation * t_translation的話,會是先轉移再旋轉,也就是右邊的先執行

參考資料

Ruby 使用 Byebug 進行 Debug

簡介

Byebug 是 Ruby 很出名的除錯工具,以 Gem 的方式加入進 Ruby 的專案中,其擁有其他除錯工具的相同功能,如:

  • 步進:程式一行一行執行。
  • 暫停:程式停止在指定處。
  • 交互式編輯環境(REPL):在程式停止的該處進行操作。
  • 追蹤:追蹤變數的值在不同行的差異。

使用方式

  1. 安裝 Byebug
gem install byebug
  1. .rb 檔案最上方加入以下內容:
require "byebug"
  1. 埋入中斷點
  2. 開始執行
    • ruby xxx.rb
      執行到 byebug 的地方後進入 byebug 模式
    • byebug xxx.rb
      自第一行開始進入 byebug 模式

Byebug 操作指令

說明相關指令

指令 別名 範例 說明
help h h 列出全部指令的說明
help cmd h cmd-alias h n 列出 next 這個指令的說明

執行下一步相關指令

指令 別名 範例 說明
next n n 在同一個區塊或方法中往下執行 1 行
  n 3 在同一個區塊或方法中往下執行 3 行
step s s 進入區塊或是方法執行 1 行
  s 3 進入區塊或是方法執行 3 行
continue c c 執行到下一個斷點或是程式結束
  c 43 執行到第 43 行(或是在下一個斷點停止)
continue! c! c! 執行到程式結束(遇到斷點也不會停止)
finish fin fin 執行所有 frame 中的程式(完成由 step 進入的程式碼)

斷點相關指令

指令 別名 範例 說明
info breakpoints i b i b 列出所有斷點的編號、啟用與否和放置位置
break b b 33 在目前檔案的第 33 行新增一個斷點
disable breakpoint dis b dis b 2 關閉編號 2 的斷點(如果沒指定編號則代表關閉所有斷點)
enable breakpoint en b en b 2 開啟編號 2 的斷點(如果沒指定編號則代表開啟所有斷點)
delete del del 1 刪除編號 1 的斷點(如果沒指定編號則代表刪除所有斷點)
condition cond cond 3 i > 3 為編號 3 的斷點新增 i > 3 的判斷式
  cond 3 刪除編號 3 的斷點判斷式

監控相關指令

在程式每次被斷點中斷時都會呈現監控點上的程式碼回傳值。

指令 別名 範例 說明
info display i d i d 列出所有監控的編號、啟用與否和其程式碼
display disp disp arg 新增一個每次被斷點中斷時,都會呈現 arg 數值的監控點
  disp 列出所有監控點的編號、程式碼和其回傳值
disable display dis d dis d 2 關閉編號 2 的監控點
enable display en d en d 2 開啟編號 2 的監控點
undisplay undisp undisp 1 停止顯示編號 2 的監控點(無指定編號則代表停止顯示所有監控點)

變數相關指令

指令 別名 範例 說明
var args v a v a 顯示當前方法的參數名稱和值
var local v l v l 顯示當前範圍(區塊或是方法)的所有變數名稱和值
var instance v i v i 顯示實例化物件中的變數名稱和值
  v i obj 顯示 obj 這個實例化物件中的變數名稱和值
var const v c v c 顯示所有的常數名稱和值
  v c Klass 顯示 Klass 這個 class/module 裡常數名稱和值
var global v g v g 顯示所有的全域變數名稱和值
var all v all v all 顯示所有的變數
eval eval eval i 當變數為 byebug 關鍵字時,可以用這種方式取得值

追蹤和定位相關指令

指令 別名 範例 說明
list= l= l= 顯示目前執行位置的前 5 行和後 4 行程式碼
list l l 8-20 顯示第 8 行到第 20 行的程式碼
  l 顯示往後 10 行的程式碼(再輸入一次 l 會在往後 10 行)
list- l- l- 顯示往前 10 行的程式碼(再輸入一次 l 會在往前 10 行)
where or backtrace w or bt w 顯示目前停止的位置其上下文(由誰呼叫等等)
  bt 顯示目前停止的位置其上下文(由誰呼叫等等)
frame f f 2 往上 2 層查看進入點

操作相關指令

指令 別名 範例 說明
edit ed ed 編輯當前執行的檔案
irb irb irb 開啟 irb
quit q q 離開 byebug
quit! q! q! 強制離開 byebug
restart restart restart 重新開始執行

使用範例

Byebug 後面加判斷式

require "byebug"
10.times.each do |i|
    byebug if i == 3
    puts i
end

輸出:

0
1
2
3
Return value is: nil

[1, 6] in /Users/wells/project/test.rb
   1: require "byebug"
   2:
   3: 10.times.each do |i|
   4:   puts i
   5:   byebug if i == 3
=> 6: end
(byebug)

參考連結

Rails使用 Webpacker 管理靜態檔案

簡介

Rails專案中可能會有許多靜態的檔案,例如:JavaScript、Stylesheets 和圖檔,把所有的靜態檔案都放在public目錄或許是個選擇,但是檔案一多的時候,就不好管理了。

因此為了便於管理這些檔案,Rails 提供以下兩種方式:

  1. Webpacker
  2. Assets Pipeline

雖然從Rails 6後預設使用 Webpacker 來管理 Javascript 並使用 Asset Pipeline 管理 CSS,但是要使用其中一邊管理全部的靜態資源也是可以的,因為上篇已經寫過使用 Assets Pipeline 的方式管理靜態資料,因此本篇介紹使用 Webpacker 的方式來實作。

操作步驟

以下示範使用 Webpacker 搭配已經寫好的模板:

  1. 下載模板
  2. 複製模板必要內容至專案
  3. 安裝 bootstrap
  4. 更改靜態資源的取得路徑
  5. 其他問題

下載模板

本篇文章使用Start Bootstrap-Landing Page作為模板範例,首先請先下載該專案或使用git進行下載

git clone https://github.com/StartBootstrap/startbootstrap-landing-page.git·

下載完成後可以進入該資料夾執行npm start,即可瀏覽網頁,如果沒裝npm也沒關係,直接點擊dist/index.html就好

複製模板必要內容至專案

必要內容包含 scssimagedist/index.html 中的程式碼

  • 複製模板 src/scss 之下所有內容到專案 app/javascript/stylesheets 資料夾(沒有就新增)
  • 複製模板 src/assets/img之下所有圖片到專案 app/javascript/images 資料夾(沒有就新增)

新增 Controller

由於我們下載的是一個首頁的模板,為了不打亂原本的專案,因此我們另外創建一個 Controller

  • 執行rails g controller landing新增 Controller
  • 修改landing_controller中的內容

    把此段程式碼刪除
class LandingController < ApplicationController
end

替換成以下

class LandingController < ActionController::Base
    def index
    end
end
  • config/routes 中設定根路徑 root to: "landing#index"

  • app/views/landing 中新增 index.html.erb,並且複製模板 dist/index.htmlbody 區塊到 index.html.erb

  • app/views/layouts 中新增 landing.html.erb,複製 app/views/layouts/application.html.erb 的內容到裡面

  • 把模板dist/index.html中的以下內容全部貼到app/views/layouts/landing.html.erbhead區塊

    <!-- Font Awesome icons (free version)-->
    <script src="https://use.fontawesome.com/releases/v5.15.3/js/all.js" crossorigin="anonymous"><script>
    <!-- Simple line icons-->
    <link href="https://cdnjs.cloudflare.com/ajax/libs/simple-line-icons/2.5.5/csssimple-line-icons.min.css" rel="stylesheet" type="text/css" />
    <!-- Google fonts-->
    <link href="https://fonts.googleapis.com/css?family=Lato:300,400,700,300italic,400italic700italic" rel="stylesheet" type="text/css" />
  • app/javascript/packs/application.js 之中加入此段,引入模板 scss

import 'stylesheets/styles.scss'

安裝 bootstrap

由於此模板需要 bootstrap,因此必須安裝,安裝步驟參考Rails 6 使用 Bootstrap,但有少許修改,依下方為主

  • 執行yarn add bootstrap@4 jquery popper.js安裝 bootstrap 相關套件
  • app/javascript/packs/application.js之中加入 bootstrap 相關套件
import 'jquery'
import 'popper.js'
import 'bootstrap'
window.jQuery = $ #jquery使用,此範例不加此行也可以正常執行
window.$ = $ #jquery使用,此範例不加此行也可以正常執行
  • 新增 app/javascript/stylesheets/site.scss 檔案,在其中加入以下內容
@import "~bootstrap/scss/bootstrap.scss";
  • app/javascript/packs/application.js之中加入此段,引入 bootstrap
import 'stylesheets/site.scss'
  • 設定 Webpacker,在 config/webpack/environment.js加入以下內容
const { environment } = require('@rails/webpacker')
const webpack = require('webpack');
environment.plugins.append(
    'Provide',
    new webpack.ProvidePlugin({
        $: 'jquery',
        jQuery: 'jquery',
        Popper: ['popper.js', 'default'],
    })
);
module.exports = environment

更改靜態資源的取得路徑

  • app/javascript/packs/application.js加入此段
require.context("../images", true)
  • 更改 app/views/layouts/landing.html.erb 的圖片 url

    將有使用到圖片 url,替換成以下 helper 寫法
asset_pack_path 'media/images/檔名.jpg'
  • app/javascript/stylesheets 資料下的圖檔 url 更改為相對路徑

到了這一步,執行 rails s 就應該可以看到正常的畫面了。

其他設定

如果想要指定在 production 環境運行的話,需要進行以下動作:

  • RAILS_ENV=production webpacker:compile Webpacker重新編譯
  • RAILS_SERVE_STATIC_FILES=true rails server -e production

    config.serve_static_files在預設時是 true ,但在 production 環境下會是 false,這是因為提供靜態資源應為伺服器軟體負責的,如 Apache、Nginx 等。相關連結Rails指南

結語

本篇文章主要是拿來紀錄使用 Webpacker 的過程,寫這篇文得時候我感覺是在做一件挖坑給自己跳的事情,雖然之前有做過,但當時為了趕工,留下了許多技術負債,也因為要寫這篇文,我不得不把每一個坑都跳過一遍,但藉著這個機會也讓我對於 Webpacker 更加了解。

參考連結

Ruby 搭配 Sketchup 學習筆記(八)

前情提要

Sketchup 是一款在建築、都市計畫和遊戲開發都頗有名氣的 3D 建模軟體,而 Ruby 則是一個程式語言,它可以搭配 Sketchup 達成程式化 建模的任務,近期經由系主任引薦,要開發 Sketchup 的 Extension,雖然我寫過 Ruby,但 Sketchup 則是完全沒碰過,於是利用文章來記錄所學的一點一滴。

本篇內容

  • 視景畫面
  • 照相機

視景畫面

視景畫面代表使用者目前畫面上呈現的資訊,以下示範如何取用視景資訊:

view = Sketchup.active_model.active_view

height = view.vpheight.to_s
width = view.vpwidth.to_s

puts "視景尺寸:#{width}:#{height}"

puts "上左:#{view.corner(0)[0]},#{view.corner(0)[1]}。"
puts "上右:#{view.corner(1)[0]},#{view.corner(1)[1]}。"
puts "下左:#{view.corner(2)[0]},#{view.corner(2)[1]}。"
puts "下右:#{view.corner(3)[0]},#{view.corner(3)[1]}。"

center = view.center

puts "視景中心點:#{center[0]},#{center[1]}"

origin = view.screen_coords [0,0,0]
puts "繪圖原點的位置:\n#{origin[0].to_f}\n#{origin[1].to_f}"

輸出結果:

視景尺寸:2880:1504
上左:0,0。
上右:2880,0。
下左:0,1504。
下右:2880,1504。
視景中心點:1440,752
繪圖原點的位置:
958.4791057184447
1012.086470675737

還有一個有趣的用法是讓畫面聚焦在特定物件

view = Sketchup.active_model.active_view
view.zoom Sketchup.active_model.entities[0]

輸出結果:
image

照相機

以下示範變更視景位置:

eye = [100,100,200]
target = [0,0,0]
up = [0,0,1]
my_cam = Sketchup::Camera.new eye, target, up

view = Sketchup.active_model.active_view
view.camera = my_cam

輸出結果:

image
其中的參數分別是:

  • eye:視角位置
  • target:看哪裡
  • up:移動時向量,會決定畫面的角度

參考資料

使用Github Action部署Jekyll專案

料理食材(內文內容)

  • Jekyll deploy
  • Github Action的使用

誰可以安心食用(適合誰讀)

服用完你會獲得什麼

  • 使用Github Action讓你部署Jekyll專案變得更輕鬆
  • 幫你抓幾隻蟲、踩幾個雷,節省你好幾個小時的時間

使用Github Action部署Jekyll專案

在前幾篇文章中,我們了解到假如要部署Jekyll專案,除了專案本身外,還需要把_site/這個資料夾推上Github Page,因此假如能把這件事情變成自動化的話,一切都會輕鬆不少。

因此本篇文章就是要來介紹如何使用Github Action自動部署到Github Page上

安裝步驟

以下步驟以chirpy這個模板為例。

  1. 下載chirpy的Github專案,並且複製其中的tools/test.shtools/deploy.sh到你自己的專案(請確保相對路徑與原專案相同)
  2. 在自己的專案下創建.github/workflows/pages-deploy.yml這個檔案,並且複製該段程式碼到檔案中。

需要注意的是on.push.branches後面接的branch要是你Github Repository的預設branch,如果Ruby版本不同也要修改

  1. 由於Github Action上是Linux作業系統,因此請在自己的專案中運行以下指令:
bundle lock --add-platform ruby
bundle lock --add-platform x86_64-linux
  1. bundle add html-proofer

    由於tools/test.sh是使用html-proofer進行測試,因此請加入這個gem

    如果你的文章含有中文或是非英文,在測試的時候會報錯,因此請在tools/test.sh的底部刪掉 --check-html \這將是欠技術債的開始

  2. 將你的專案推上Github,在Github Repository的功能列可以找到Action,點擊後就可以看到以下畫面

    完成後你切換branch到gh-pages分支,你就可以看到機器人幫你把_site/推上這個分支了

踩坑紀錄

依照以上的步驟通常可以幫你避開一些坑,但坑這種東西是永遠填不完的,最主要的問題在於test沒辦法過,假如你有這種情況可以試試以下動作:

  1. 如果沒有使用Google Analytics,可以註解掉_config.yml中的GA部分
  2. 不要填寫baseurl,這會導致test去找_site$baseurl,文件是預設你的Github Repository名稱是<username>.github.io,因此不會報錯,但假如你調皮的話......我也不知道該怎麼辦QQ

結語

Github Action博大精深,我對他還只是初步的了解,大部分的設定也都是參考網路上的範例,希望之後有機會來好好的研究Github Action。

Zeitwerk 深入淺出

前言

眾所周知,在寫 Rails 時幾乎沒有使用 require 的機會,這是因為 Rails 有 autoloading 的機制

但前提是有照著它的命名規則。

近期要為公司寫個 API Client 的 Gem,於是參考了 Shopify API Ruby 的實作,對其中如何載入不同版本的 API 感到好奇

因為 Rails 的 File Path Naming Convention,檔案的路徑會與 module/classnested 相關
例如: app/controllers/admin/orders_controller.rb 裡面就會有以下的 code

module Admin
  class OrdersController
    ...
  end
module

但在 Shopify API Ruby 中的檔案路徑中卻可以直接使用以下程式碼來呼叫相應版本的 API

shopify-api-ruby gem 檔案結構

lib/shopify_api
...
├── rest
│   └── resources
│       ├── 2023_04
│       │   └── shop.rb
│       ├── 2023_07
│       │   └── shop.rb
│       └── 2023_10
│           └── shop.rb
...

直接呼叫

ShopifyAPI::Shop

而不是

ShopifyAPI::Rest::Resources::2023_04::Shop

到底是怎麼實作的?

這就要從 Rails 的 Autoloading 說起

Rails 的 Autoloading

這邊我們先簡單介紹 Rails 使用的兩種 Autoloader

Rails 在很早期就透過 Autoload 機制去解決這問題了,Rails 6 以前使用的是原生的 autoloader,之後則是 zeitwerk

有興趣比較兩個 autoloader 的差別可以看這篇

本篇文章主要是介紹 zeitwerk

ZeitWerk 是什麼? 有什麼用?

Zeitwerk is an efficient and thread-safe code loader for Ruby.

如同簡述是個 ruby 程式碼的載入器

若還不知道 Ruby 載入程式碼的方法,可以參考這篇 Ruby 中三種載入程式碼的機制(load, require, autoload)

回到主題,Zeitwerk 是什麼?有什麼用?

原本 Ruby 提供的載入功能都是對檔案

若你有 N 個檔案就要寫 N 次 require

我們可以看看不使用 Autoloader 專案 SketchUp/ruby-api-stubs 的載入方式

# lib/sketchup-api-stubs/sketchup.rb
require 'sketchup-api-stubs/stubs/_top_level.rb'
require 'sketchup-api-stubs/stubs/Array.rb'
require 'sketchup-api-stubs/stubs/Geom.rb'
require 'sketchup-api-stubs/stubs/Geom/BoundingBox.rb'
require 'sketchup-api-stubs/stubs/Geom/Bounds2d.rb'
require 'sketchup-api-stubs/stubs/Geom/LatLong.rb'
require 'sketchup-api-stubs/stubs/Geom/OrientedBounds2d.rb'
require 'sketchup-api-stubs/stubs/Geom/Point2d.rb'
require 'sketchup-api-stubs/stubs/Geom/Point3d.rb'
require 'sketchup-api-stubs/stubs/Geom/PolygonMesh.rb'
require 'sketchup-api-stubs/stubs/Geom/Transformation.rb'
require 'sketchup-api-stubs/stubs/Geom/Transformation2d.rb'
require 'sketchup-api-stubs/stubs/Geom/UTM.rb'
require 'sketchup-api-stubs/stubs/Geom/Vector2d.rb'
require 'sketchup-api-stubs/stubs/Geom/Vector3d.rb'
require 'sketchup-api-stubs/stubs/LanguageHandler.rb'
require 'sketchup-api-stubs/stubs/Layout.rb'
require 'sketchup-api-stubs/stubs/Layout/Entity.rb'
require 'sketchup-api-stubs/stubs/Layout/AngularDimension.rb'
require 'sketchup-api-stubs/stubs/Layout/AutoTextDefinition.rb'
require 'sketchup-api-stubs/stubs/Layout/AutoTextDefinitions.rb'
require 'sketchup-api-stubs/stubs/Layout/ConnectionPoint.rb'
require 'sketchup-api-stubs/stubs/Layout/Document.rb'
require 'sketchup-api-stubs/stubs/Layout/Ellipse.rb'
require 'sketchup-api-stubs/stubs/Layout/Entities.rb'
require 'sketchup-api-stubs/stubs/Layout/FormattedText.rb'
require 'sketchup-api-stubs/stubs/Layout/Grid.rb'
require 'sketchup-api-stubs/stubs/Layout/Group.rb'
require 'sketchup-api-stubs/stubs/Layout/Image.rb'
require 'sketchup-api-stubs/stubs/Layout/Label.rb'
require 'sketchup-api-stubs/stubs/Layout/Layer.rb'
require 'sketchup-api-stubs/stubs/Layout/LayerInstance.rb'
require 'sketchup-api-stubs/stubs/Layout/Layers.rb'
require 'sketchup-api-stubs/stubs/Layout/LinearDimension.rb'
require 'sketchup-api-stubs/stubs/Layout/LockedEntityError.rb'
require 'sketchup-api-stubs/stubs/Layout/LockedLayerError.rb'
require 'sketchup-api-stubs/stubs/Layout/Page.rb'
require 'sketchup-api-stubs/stubs/Layout/PageInfo.rb'
require 'sketchup-api-stubs/stubs/Layout/Pages.rb'
require 'sketchup-api-stubs/stubs/Layout/Path.rb'
require 'sketchup-api-stubs/stubs/Layout/Rectangle.rb'
....

看起來真可怕是吧?

使用 Zeitwerk 後這些 require 都可以刪掉了

Zeitwerk 怎麼用?

這裡我們直接看範例

# main.rb
require 'zeitwerk'

loader = Zeitwerk::Loader.new
loader.push_dir("lib")
loader.setup

A.hi

# lib/a.rb
module A
  def self.hi
    puts 'hi'
  end
end

如何設計一個類 Zeitwerk 的 Autoloader 工具

以下程式碼可以在 zeitwerk-POC repo 下載

Basic usage

先來個最基本的範例

# poc_loader.rb
class PocLoader
  attr_reader :autoload_paths

  def initialize
    @autoload_paths = []
  end

  def push_dir(dir)
    @autoload_paths << dir
  end

  def setup
    autoload_paths.each do |dir|
      Dir.glob("#{dir}/**/*.rb").each do |file|
        require_relative file
      end
    end
  end
end

# lib/a.rb
module A
  def self.hi
    puts 'hi'
  end
end

# main.rb
require_relative 'poc_loader'

loader = PocLoader.new
loader.push_dir("lib")
loader.setup

A.hi
# ruby main.rb
# hi

這一版可以載入 lib 底下的 .rb 檔案,但並不能辦到以下幾點:

  1. Reload
  2. Custom root Namespace

下一階段我們來實作 Reload 的功能。

Reload

前面有提到 requireautoload 都只能載入程式碼一次,那有什麼辦法可以跨過這限制呢?
關鍵在於 $LOADED_FEATURES 這個環境變數,它會記得載入過的檔案,因此只要刪掉該檔案就可以重新載入一次

require 'json' # true

require 'json' # false

$LOADED_FEATURES.pop

require 'json' # true

接著我們來改寫 PocLoader 使其能夠支援 Reload

# poc_loader.rb
class PocLoader
  # 前面跟 basic 一樣
  # ...
  def reload
    autoload_paths.each do |dir|
      Dir.glob("#{dir}/**/*.rb").each do |file|
        abs_file = File.expand_path(file)
        $LOADED_FEATURES.delete(abs_file)
      end
    end
    setup
  end
end

# lib/a.rb
module A
  def self.hi
    puts 'hi'
  end
end

# main.rb
require_relative 'poc_loader'

loader = PocLoader.new
loader.push_dir("lib")
loader.setup

while true
  loader.reload
  A.hi
  sleep 1
end
# ruby main.rb
# hi
# hi
# hi
# ----變化 A.hi 的輸出為 hi123
# hi123
# hi123

在這個版本中,執行 main.rb 後,會不斷呼叫 A.hi,從而印出 hi,若此時修改 A.hi 內部的實作,將會讓印出的字串發生改變
不過這版還是有些問題:

  1. Custom root Namespace
  2. 假如檔案被刪除,該 constant 也應該被刪除

下個階段來嘗試解決這問題。

Custom root Namespace

zeitwerk 中,可以指定 Root Namespace,這是什麼意思呢?看一下範例:

require "active_job"
require "active_job/queue_adapters"
loader.push_dir("#{__dir__}/adapters", namespace: ActiveJob::QueueAdapters)

# adapters/my_queue_adapter.rb -> ActiveJob::QueueAdapters::MyQueueAdapter

為了實作這個功能,我們不能再使用 require,必須改用 autoload 才能定義到指定的 Root Namespace 上,以下來看看實作

這是檔案目錄

custom_namespace
├── lib
│   └── car
│       └── parts
│           ├── v1
│           │   └── wheel.rb
│           └── v2
│               └── wheel.rb
├── main.rb
├── monkey_patches.rb
├── poc_loader.rb

程式碼

# monkey_patches
class String
  def remove_rb_extension
    self.gsub(/\.rb$/, '')
  end

  def constantize
    Object.const_get(self)
  end

  def camelize(uppercase_first_letter = true)
    string = self
    if uppercase_first_letter
      string = string.sub(/^[a-z\d]*/) { |match| match.capitalize }
    else
      string = string.sub(/^(?:(?=\b|[A-Z_])|\w)/) { |match| match.downcase }
    end
    string.gsub(/(?:_|(\/))([a-z\d]*)/) { "#{$1}#{$2.capitalize}" }.gsub("/", "::")
  end
end
# lib/car/parts/v2/wheel.rb
module Car
  class Wheel
    def self.hi
      puts 'I am a wheel v2 class'
    end
  end
end

這兩個檔案比較關鍵

# main.rb
require_relative 'poc_loader'
VERSION = "v2"
module Car ;end

loader = PocLoader.new
loader.push_dir("lib/car/parts/#{VERSION}", root_namespace: Car)
loader.setup

while true
  loader.reload
  Car::Wheel.hi
  sleep 1
end
# poc_loader.rb
require_relative './monkey_patches'

class PocLoader
  attr_reader :autoload_paths, :root_namespace

  def initialize
    @autoload_paths = []
  end

  # autoloadPaths store hash arry
  # every element is a hash with key: dir, namespace
  def push_dir(dir, root_namespace: Object)
    autoload_paths << { dir: dir, root_namespace: root_namespace }
  end

  def reload
    unload
    setup
  end

  def setup
    autoload_paths.each do |dir:, root_namespace:|
      list_files(dir) do |abs_path, relat_path|
        cname = relat_path.remove_rb_extension.camelize
        root_namespace.autoload cname, abs_path
      end
    end
  end

  private

  def unload
    autoload_paths.each do |dir:, root_namespace:|
      list_files(dir) do |abs_path, relat_path|
        cname = relat_path.remove_rb_extension.camelize
        $LOADED_FEATURES.delete(abs_path)
      end

      remove_shallow_level_constants(dir, root_namespace)
    end
  end

  def remove_shallow_level_constants(dir, namespace)
    Dir.glob("#{dir}/*").each do |relat_path|
      base_path = relat_path.gsub(/#{dir}\//, '')
      cname = base_path.remove_rb_extension.camelize
      namespace.send(:remove_const, cname)
    end
  end
 
  def list_files(directory)
    Dir.glob(File.join(directory, '**', '*.rb')).each do |file|
      abs_path = File.absolute_path(file)
      relat_path = file.gsub(/#{directory}\//, '')
      yield(abs_path, relat_path) if block_given?
    end
  end
end

在這版本中,我們使用 autoload 取代了 require,並且善用 autoload 可自由決定 namespace 的特性來完成 Custom Root Namespace。

值得一提的是 unload 的部分,除了從 $LOADED_FEATURES 移除絕對路徑外,還使用 Object.remove_const 這個 private method 刪除 namespace 上的 const,以應付 reload 後檔案已刪除的情形

但這個版本還是有個問題:

  1. Implicit Namespace

下一個段落我們來看看這是什麼問題。

Implicit Namespace

看一下官方文件的說明

If a namespace consists only of a simple module without any code, there is no need to explicitly define it in a separate file. Zeitwerk automatically creates modules on your behalf for directories without a corresponding Ruby file.
for instance: suppose a project includes an admin directory:

app/controllers/admin/users_controller.rb -> Admin::UsersController

白話文來說就是檔案中間的 namespace 沒有定義過(因為沒有一個檔案叫做 app/controllers/admin.rb),所以直接載入最底端的檔案app/controllers/admin/users_controller.rb會出現找不到 Admin 的問題

那這點要怎麼解決?

第一種方式最簡單直觀,是直接為每個 folder 定義 module,但這就跟 Lazy loading 的原則相悖。

第二種方式則是確確實實的把 folder 放進 autoload_paths

例如 loader.push_dir('app/controllers/admin'),藉由這種方式讓 autoload 知道要新增一個 Admin 的 namespace

而目前的做法是採用第一種並進行一些優化,拆解做法:

  1. autoload_path 底下的子檔案和資料夾進行排序
  2. 因為排序後子檔案會比資料夾更前面,之後就可以藉由 Object.const_defined? 確定 namespace 是否定義過
  3. 沒定義過的話就自己定義

接著來看看實作吧

檔案結構

implicit_namespace
├── lib
│   ├── car
│   │   └── wheel.rb
│   ├── ship
│   │   └── keel.rb
│   └── ship.rb
├── main.rb
├── monkey_patches.rb
├── poc_loader.rb

程式碼
monkey_patches 就不放了

# main.rb
require_relative 'poc_loader'

loader = PocLoader.new
loader.push_dir("lib")
loader.setup

while true
  loader.reload
  Car::Wheel.hi
  Ship::Keel.hi
  sleep 1
end
# poc_loader.rb
require_relative './monkey_patches'
require 'set'

class PocLoader
  # ...
  def setup
    autoload_paths.each do |dir:, root_namespace:|
      list_files(dir) do |abs_path, relat_path|
        cname = relat_path.remove_rb_extension.camelize
        namespace = define_namespace(cname, root_namespace) # +++
        cname_without_namespace = cname.split('::').last # +++
        namespace.autoload cname_without_namespace, abs_path # +++
      end
    end
  end

  private

  def define_namespace(cname, root_namespace)
    namespaces = cname.split('::')
    # remove last element
    namespaces.pop
    namespaces.each do |namespace|
      unless root_namespace.const_defined?(namespace)
        root_namespace.const_set(namespace, Module.new)
      end
      root_namespace = root_namespace.const_get(namespace)
    end
    root_namespace
  end

  def remove_shallow_level_constants(dir, namespace)
    set = Set.new
    Dir.glob("#{dir}/*").each do |relat_path|
      base_path = relat_path.gsub(/#{dir}\//, '')
      cname = base_path.remove_rb_extension.camelize
      set << cname.split('::').first
    end
    set.each do |cname|
      namespace.send(:remove_const, cname)
    end
  end

  def list_files(directory)
    children_files = Dir.glob(File.join(directory, '**', '*.rb'))
    children_files.sort! # +++
    children_files.each do |file|
      abs_path = File.absolute_path(file)
      relat_path = file.gsub(/#{directory}\//, '')
      yield(abs_path, relat_path) if block_given?
    end
  end
  # ...
end

在這一版中,我們做了一些變更

  1. PocLoader#list_fileschildren_files 排序,讓較少層目錄的檔案優先於較多層的
  2. setup 中多加呼叫 #define_namespace,確保上層的 namespace 已被宣告
  3. 修改 remove_shallow_level_constants,因可能刪除相同的 const 兩次導致錯誤,所以用 Set 確保每個 const 只會被刪一次

到這邊基本上我們已掌握了 Zeitwerk 的核心概念。

剩下是一些有想到但還沒實作的內容

  1. Inflection
  2. Ignore dir
  3. thread-safe
  4. explicitly namespace
  5. multi loader
  6. eager load

技術難題總結

參考作者在 RailsConf 2022 - Opening Keynote: The Journey to Zeitwerk by Xavier Noria 提到的五個技術難題:

  1. Module#autoload 呼叫 Ruby 內建的 require(自 Ruby 1.9 開始 requirerubygems 這個套件處理,詳情可以參考龍哥寫的文章
  2. require 是幕等性的, 只在第一次生效
  3. 沒有 API 能刪除 autoload 定義過的 const
  4. 隱含的 namespace ,例如只有 admin/ 這個資料夾但沒有 admin.rb 的檔案,並且也沒有事先定義 Admin,那要怎麼處理?
  5. 明確的 namespace ,參考下面的扣,我們應該先 autoload 哪個?這是個死結的狀態(作者正在處理的問題)
# car.rb
class Car
 include Wheel
end

# car/wheel.rb
module Car::Wheel
end

除了第 5 點以外基本上我們都解決了。

總結

回到最一開始產生這個疑問的起點,為什麼 shopify-api-ruby 可以依不同版本載入相應的 API 呢?

查看一下他的程式碼

def load_rest_resources(api_version:)
  # Unload any previous instances - mostly useful for tests where we need to reset the version
  @rest_resource_loader&.setup
  @rest_resource_loader&.unload

	...

  version_folder_name = api_version.gsub("-", "_")
  path = "#{__dir__}/rest/resources/#{version_folder_name}"

	...

  @rest_resource_loader = T.let(Zeitwerk::Loader.new, T.nilable(Zeitwerk::Loader))
  T.must(@rest_resource_loader).enable_reloading
  T.must(@rest_resource_loader).ignore("#{__dir__}/rest/resources")
  T.must(@rest_resource_loader).setup
  T.must(@rest_resource_loader).push_dir(path, namespace: ShopifyAPI)
  T.must(@rest_resource_loader).reload
end

再搭配他的檔案結構

shopify-api-ruby gem 檔案結構

shopify-api-ruby/lib/shopify_api/
├── auth
│   └── oauth
├── clients
│   ├── graphql
│   └── rest
├── errors
├── rest
│   └── resources
│       ├── 2022_04 # 含有 shop, customer 等等的 ruby file
│       ├── 2022_07
│       ├── 2022_10
│       ├── 2023_01
│       ├── 2023_04
│       ├── 2023_07
│       └── 2023_10
├── utils
└── webhooks
    └── registrations

這樣子是不是就清楚許多了

References

Git入門教學

料理食材(內文內容)

  • Git簡介
  • Git基本用法

誰可以安心食用(適合誰讀)

  • 會使用瀏覽器的人
  • 有Github帳號的人(沒有請點這裡去辦)
  • 不會懼怕小黑窗(Terminal)的人

服用完你會獲得什麼

  • 會使用基本的Git指令
  • 把你電腦上的專案推上Github

Git簡介

試想在很久以前,你對資訊還不太熟悉,你是否會為了每次編輯而做一份備份的動作呢?這可能讓你的檔案變得跟下圖一樣:

一個月或更久後再回來看,通常就不知道每一個檔案的差別是什麼了,一個一個查閱時,卻又因為這些檔案都含有大量相同的內容,難以找出不同點,於是你只好花費寶貴的青春在做這種沒意義的事情上。

而現在你長大了,為了避免重複年少時的過錯,你想到了一個方法可以解決這樣子的困境,那就是創立一個版本集,每次變更時都會紀錄成一個版本存進版本集中,每一個版本只紀錄與上一版的差異,當需要特定版本的檔案時,只要依照版本集對檔案進行修改,你就可以得到特定版本的檔案了, 而因為這樣做你可以達成:

  1. 減少儲存空間,因為每一版不需要儲存整個檔案。
  2. 容易辨識每一版本的差異,因為沒有重複的內容。
  3. 與別人分工變容易, 因為只需要把彼此不同版本加起來(merge)就好。

這真是個天才的想法!但唯一的缺點大概就是要回到特地版本時,需要依照中間的版本紀錄對檔案做修改,假如有上百個版本,那將會是個災難。

不過沒關係,因為世界上有著和你相同想法的天才已經用程式幫你開發好這一套分散式版本控制系統了,這個名稱叫做"Git"。

分散式是指每位專案參與者的個人電腦都可以擁有一份 Git資料庫,在開發過程中都可以對自己的資料庫進行操作,等到開發到一個階段時再推上遠端的資料庫(例如Github上的資料庫)做同步的動作。

Git跟Github的差別

現在我們已經知道Git是什麼了,那Github呢?

Git和Github大概就像是影片跟youtube的差別,後者就是儲存一大堆Git的網站,但除此之外也提供許多便利的功能,如Github Action、Github Page等等。


Git使用流程

假設你今天已經有一份寫好的檔案,你希望上傳到Github上,讓身邊的人看看你爆肝完成的成品,那麼你該進行以下的動作:

安裝Git

這部分請依照作業系統選擇。

在Github上創建Repository

如還沒有註冊Github帳戶,請點此

當登入後,在個人首頁的右上角按下+之後選擇New Repository

之後會看到這個畫面

填寫一些這個檔案的資料即可。
需要特別介紹的是底下的Initialize this repository with:,可以看到有三個選項,以下逐一介紹:

  1. Add a README.md file

    README.md主要用於說明這個repository是在做什麼的,如果repository的根目錄上含有README.md的話,就會在Github的頁面底部呈現

    就像這樣。

    README.md的編寫需要使用Markdown語法,如果不知道Markdown是什麼的話,請查閱此處

  2. Add .gitignore

    當專案中含有一些敏感資料或是不相關的檔案,如:secret_key,你不希望這些檔案被加入git中,這時你就可以把這些檔案的名稱寫在.gitignore中,這樣子在執行git add時,就不會把這些檔案加入Git資料庫裡了。

  3. Choose a license

    這裡的License是關於專案的授權條款,最常見的是MIT License,代表被授權人有權利使用、複製、修改、合併、出版發行、散布、再授權和/或販售軟體及軟體的副本,及授予被供應人同等權利。

    更多的License介紹可以查看此處

以上三個選項,不勾也可以成功創建新的資料庫。

把本地的專案推到遠端節點

  1. 在專案資料夾中開啟terminal

    • Windows系統可以在專案資料夾中按下右鍵開啟git bash
    • Mac OSX開啟terminal後輸入cd ~/Document/project

    這裡的~/Document/project請換成你專案的路徑。

  2. git init

    於該資料夾中初始化git資料庫。

  3. git add .

    把未被紀錄的文件加入緩存區,git add後與.要有空白,.的意思代表該目錄下所有文件,也可以換成其他檔案的名稱,例如git add index.html

  4. git config --global user.email "[email protected]" | git config --global user.name "yourname"

    此步驟是設定git進行commit的身份,如果你已經有安裝過git了,執行這行指令後,它顯示error: 不能鎖定設定檔案,那八成是你已經設定過了。

  5. git commit -m "本次提交進資料庫的訊息ex.Add set user function..."

    把緩存區的資料存入git資料庫中,如果沒有打-m會進入vim編輯器中,輸入:q後按enter可以離開

    關於vim的進一步操作可以查看此處

  6. git remote add origin [url]

    把遠端資料庫(github)的節點加入至本地資料庫,origin代表遠端節點的名稱,你可以換成任何名稱,例如:github[url]為遠端資料庫的網址,例如你使用github,遠端資料庫的url就會像是https://github.com/Kevin-1215/project1.git

  7. git push -u origin master

    此段是第一次push時會使用到的指令,-u代表的是設定之後push的遠端節點(這邊指定origin)和節點分支(這邊指定master),在第二次後只要使用git push就可以了。

專案修改後重推

假如你推上遠端資料庫後發現 檔案有誤,或是你要做一些修改,那麼你該怎麼做呢?

  1. git add .
  2. git commit -m "修改的內容"
  3. git push

第二次推上遠端資料庫只需要以上的指令就可以完成了。


結語

本篇只有介紹很基本的Git使用流程,未來如果還有機會再來介紹更細部的內容和Git的GUI該如何使用。

Ruby 搭配 Sketchup 學習筆記(七)

前情提要

Sketchup 是一款在建築、都市計畫和遊戲開發都頗有名氣的 3D 建模軟體,而 Ruby 則是一個程式語言,它可以搭配 Sketchup 達成程式化 建模的任務,近期經由系主任引薦,要開發 Sketchup 的 Extension,雖然我寫過 Ruby,但 Sketchup 則是完全沒碰過,於是利用文章來記錄所學的一點一滴。

本篇內容

  • 屬性
  • 模型的屬性
  • 檔案存取

屬性

與實體物件有關的資料都被稱為「屬性(Attributes)」,可以使用以下指令進行操作:

  • set_attribute:新增 key 和 value,有三個參數
  • get_attribute:取得特定 key 的 value,第三個參數可選填,當無該屬性資料時會回傳該值
  • attribute_dictionaries:取得該實體物件所有屬性

例如以下範例:

person = Sketchup.active_model.entities[0]
person.set_attribute "person", "state", "relaxing"

person_state = person.get_attribute "person", "state" #relaxing
person_age = person.get_attribute "person", "age", 18 # 18

attr_dicts = person.attribute_dictionaries
attr_dicts.each {|attr| attr.each_pair{|key, value| puts "#{key}-#{value}"}}
#Owner-
#Status-
#state-relaxing

依照上方範例可以發現與 Hash 相似,但不同點在於其總共有三層,可以想像成是一個由 Hash 組成的 Hash。

Owner 和 Status 是 Sketchup 內建的,可以忽略。

模型的屬性

在之前有提過的 Active_model 之下的其中一個類別 OptionsManager,其用來放置 Sketchup 的所有選項,包括一開始所點選的英吋單位,而 OptionsManager 就像是上方的屬性一樣。以下查看所有的選項及其值:

options_manager = Sketchup.active_model.options
options_manager.each do |options_provider|
    puts options_provider.name
    options_provider.each do |key, value|
        puts "\t#{key}-#{value}"
    end
end

輸出結果:

NamedOptions
PageOptions
	ShowTransition-true
	TransitionTime-2.0
SlideshowOptions
	LoopSlideshow-true
	SlideTime-1.0
UnitsOptions
	LengthPrecision-2
	LengthFormat-0
	LengthUnit-4
	LengthSnapEnabled-true
	LengthSnapLength-0.3937007874015748
	AnglePrecision-1
	AngleSnapEnabled-true
	SnapAngle-15.0
	SuppressUnitsDisplay-false
	ForceInchDisplay-false
	AreaUnit-4
	VolumeUnit-4
	AreaPrecision-2
	VolumePrecision-2

UnitsOptions 中的 LengthUnit 就是指英吋或公尺的單位,也因為它是 Hash,因此可以使用 Hash 的方式進行存取。

檔案存取

Sketchup 相關的副檔名有以下:

  • .skm:材料檔

  • .style:樣式檔

  • .skp:元件定義檔

    在 Sketchup 中開啟檔案就和 Ruby 一樣,也可以用 chmod 進行權限修改。

當檔案位於 Sketchup 預設工作資料夾之下時,可以使用 Sketchup.find_support_file 取得其路徑,而 Sketchup.find_support_files 則可以取得多個檔案位置,例如:

Sketchup.find_support_files "rb", "Plugins"

參考資料

Sofatware Architecture 與 Rails

3-tier architecture != MVC pattern

https://openclassrooms.com/en/courses/5684146-create-web-applications-efficiently-with-the-spring-boot-mvc-framework/6156961-organize-your-application-code-in-three-tier-architecture

MVC 其實都是在 presenter layer
但 Rails 的 Model 還負擔了 business logic layer, data access layer 的職責
這也是 Rails 的 model 被稱為 fat model 的原因

這個 blog 介紹了 7 種改善 fat model 的方法(但我還沒看)
https://codeclimate.com/blog/7-ways-to-decompose-fat-activerecord-models

解決 Github Deploy Key is already in use 問題

簡介

由於最近 Github 開始限制使用 Password(HTTPS) 進行 Push 的行為,因此之後盡量都改使用 SSH 的方式進行 Push,但是之前在設定 Deploy Keys 遇到了以下的錯誤:
image

簡單來說是因為這個帳號已經有其他 Repository 使用該金鑰,但一個帳號少說也有 10 個 Repository,每一個 Repository 都去查未免也太沒有效率,於是記錄一下使用該金鑰找出對應的 Repository。

尋找對應的 Repository

使用方式:

ssh -T -ai ~/.ssh/your_ssh_key [email protected]

範例

例如我希望使用 ~/.ssh/test 這個金鑰進行 Push,但是遇到了前面的問題,於是我可以執行:

ssh -T -ai ~/.ssh/test [email protected]

接著會輸出:

Hi jhang-jhe-wei/jhang-jhe-wei.github.io! You've successfully authenticated, but GitHub does not provide shell access.

jhang-jhe-wei/jhang-jhe-wei.github.io 就是我們訪問到的 Repository,接著就去檢查該 Repository 是否的 Deploy Keys 是否有該金鑰。

推薦方式

該錯誤是由於已經有 Repository 使用了該金鑰,但實際上的情況有可能是整個帳號都用同一個金鑰,對於這種情況可以直接至帳號中的設定(點擊右上角的頭像)填寫 SSH and GPG Keys
image

這樣子就可以使用該金鑰訪問該帳號之下的所有 Repository。

參考資料

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.