# [08/09&10_b] Rails實作 新專案Note https://www.xmind.net/m/jMUqLi/ 依照順序 **Route => Conroller => Model => DB =>** M/C **=> View** views呈現畫面就好,要東西用controllers抓 ![](https://i.imgur.com/wAq53f9.png) ## START 1. 在目標目錄 rails new 專案名稱 => 開新專案。 2. 在目錄中新目錄 rails s => 開伺服器。 3. 在目錄中新目錄 rails c => 沙盒。 *在 Rails c 裡面打 .all,只會噴 11 筆資料* 5. 可得到一個local 3000網址。 ## (1) Route:創造路徑 在Rails,每條連結都是資源(resources) 在routes.rb用resources 創造8條路徑+7個action ```ruby= resources :notes ``` 例:路徑可改名articles ```ruby= resources :notes, path: 'articles' ``` 要背,會由上往下找,對了就執行那個路徑。 Resources資源,習慣用複數命名 (RESTful API概念)。路徑對照表可看: * 運作rails s之後,任一不存在的網址 * 終端機指令 $rails route * Controller#Action**其中的new(經由post)會送到create * 凡有 :id 帶入後皆會傳入Controller,形成參數雜湊 **params[:id]** ![](https://i.imgur.com/ZAphoVH.png) Verb的功用: **get 取得頁面;post 寫入資料庫;put 更新;delete 移除** * 列表 GET /notes  * 寫入 POST /notes * 看3號 GET /notes/3 * 新增 GET /notes/3/new * 編輯3號 GET /notes/3/edit * 更新3號 PUT /notes/3 * 刪除3號 DELETE /notes/3 ### 路徑開關(很少用到) 只開部份路徑 ```ruby= resources:notes, only: [:index, :show] ``` 除了此路徑全開 ```ruby= resources:notes, except: [:index] ``` ### 追加功能&路徑 member/collection 例:取消訂單 除了8個路徑之外,想自製路徑取消訂單。 member會針對路徑中的id。 等於這個寫法=delete :cancel, {on: :member} ```ruby= resources:notes, only: [:index, :show] member do delete :cancel #比方:針對ID2號訂單 DELETE /order/2/cancel end ``` collection會及於全部。 ```ruby= resources:notes, only: [:index, :show] collection do delete :cancel #刪除所有訂單 DELETE /order/cancel end ``` ## (2) Con:建立方法 在app/views下建立相應檔案。詳筆記RB-13。建議終端機建立法: ```ruby= $rails g controller notes ``` Rails自動規範:**class名稱蛇式<==>檔案名稱駝峰式**。 Controller內定義方法,可以**放@實體變數 給View用**。 本例手動順序: * 在routes設定get "/notes", to: "notes#index"之後 * 在app/controllers下新增「NotesController.rb」並繼承ApplicationController * 承上,**def index** * 手動新增app/views/notes目錄及index.html ## (3) Model:建立預期傳入的資料 產生Model的過程,要建立一個資料表。 因為Model是抽象概念,資料表/資料庫是實體存在的。所以只建立Model不夠,還要**建立migrate(描述資料表內容)**。 * 預期結構「欄位」「資料型態」 > * **title**:string //到資料庫會變成 varchar(1~?) > * **content**:text //到資料庫會變成 text(…~4G) 可放較多資料 > * **delete**:boolean (default: false) //判斷是否移除 > * **deleted_at**:datetime //判斷移除時間 > * **add_index** //建立索引,讓讀取速度更快(但寫入速度變慢) > 因為隨著資料量遞增,撈資料時間呈線性成長。 > 其他資料型態還有:數字(integer)、文字(text)等等可用 ### 開始建立 Model名稱用**首字大寫單數,對應migrate表格/Views目錄下的同名複數**。 (只有string屬性可以不寫)(欄位生成migration) ```ruby= $rails g model Note title content:text ``` ### 自動產生檔案 * 產生"app/model"下的新rb檔(不用編輯) * 產生 **"db/migrate"下的新rb檔** * 可建立真正的資料表 * 會得到**複數model名的migration!** * 會自動加時間戳記timestamps ### 建立新表格(資料庫具現化) 會在建立在"db/development.sqlite3" ```ruby= $rails db:migrate ``` 詳細設定在"config/databade.yml" **清理內容 rails db:migrate:reset** ## (04) View/Con:建立各功能網頁 利用resources 創造8條路徑中的: > new(.:format) 1. 在views下的相應網頁檔寫link("/對應複數名/new") 1. 到**views新增**同名網頁檔(new.html.erb) 2. 尚不存在create或new,所以到controller定義一個,再到views新增同名網頁檔(同上2步) 若寫這個,可拿到表單p的內容: ```htmlmixed= render html: params["p"] 或 render html: params[:p] ``` ## (05) View/Con:新增一筆資料 新增候選人資料,牽涉到View(設表單)、Controller(設create方法)、Model(被動存入新資料) ### View:建立表單送資料 = 新增一筆資料庫 本動作要建立一筆資料庫 設置表單 其中之: > > > **超連結寫法** > 從"views/notes/index.html.erb" > 連到內建的「new」action建議使用new_note_path方法Helper: > > ```ruby= > <%= link_to "新增筆記", new_note_path %> > ``` > > 各種連結寫法: > ![](https://i.imgur.com/BnRnqYM.png) * **Note.new** //新增一筆文章,只會在記憶體裡,要用 **.save** 才能存進去資料庫。 * **Note.create** //新增一筆文章,不用寫 save 可以直接存進去資料庫。 ### 觀念:params 就像雜湊 是一個**打包form送出的所有參數**的Hash(Key與Value) *在 controller 塞 debugger 可以用 params 印出* ![](https://i.imgur.com/Vq5koRS.png) * 用Key叫值(Value) > h= {name: "kk"} > 印出kk=**h[:name]** ### View:解決invalidAuthenticityToken rails網站的post行為必須帶token,否則無效(防止灌水)。 **方法1:加一行hidden(不建議)** * action 是form送出的地方。 * method 是form送出方式、如get或push。 * value 帶 <%= form_authenticity_token %>。 * **各欄input對應Model的各欄名稱,資料庫抓name。** * submit後form會把資料**包成param**,想單獨render其中的content的值的話: > render html: **params["content"]** > 或 > render html: **params[:content]** ```ruby= <form action="/candidates" method="authenticity_token" value="<%= form_authenticity_token %>"> <input type="text" name="title"> <input type="textarea" name="content"> <input type="submit" value="go!"> </form> ``` **方法2:form_for(內建小幫手form helper)** form_for後面要接一個Model,而此時「Notes」已經是Model的class,所以: * 在MVC架構下,建立行為應移到controller內較佳:將**Note.new先定義在Controller**的new方法裡,再給出「@note」實體變數,外面(view)才拿得到。 (\<%= form_for(@note) do %>) * def new方法中寫(本例title與content是要抓param的變數名稱) > title = params[:title] > content = params[:content] > **@note=Note.new(title: title, content: content)** * form_for會猜@note的路徑,若猜錯,就自己加一個url的雜湊(**@note, url: '/正確路徑'**) * 由於Object-Relational Mapping(ORM) 物件關係對應,應該會自己對應SQL與正確的物件。 **在View=初期表單** * 如此,action會指向/notes,方法是post,且自帶token。 * 各欄input對應Model的各欄名稱,資料庫抓name。 * 本來的name多加一層外包 **note[ ]** ,送出的params會多包一層key(後面透過form_for達成) * ![](https://i.imgur.com/sEsEzEU.png) * 這樣的話,原本 title = params[:title] 與 content = params[:content] 可以縮寫成: * @note = Note.new(**params[:note]**) * 這樣會遇到ForbiddenAttributesError,必須做資料清洗。因此params[:note]要獨立成一個「資料清洗 def」,見後面。 ```ruby= <%= form_for(@note) do %> 標題:<input type="text" name="note[title]"> 內文:<textarea type="text" name="note[content]"></textarea> <input type="submit" value="送出"> <% end %> ``` **在View=進化表單** * do後面可**帶變數(如 |f|)** 以承接yield出的數值,批次製造表單。如: * 本例f是一種FormBuilder物件,可透過 **text_field、text_area 或 submit 方法**做出對應的 <input> 標籤* * form_for會將每一欄包起來(如candidate[name]、candidate[age]等),被params包成一個雜湊: **{candidate: {name: 'aaa', age: '20'}}** ```ruby= <%= form_for(Note.new) do |f|%> <div> <%= f.label :title, "標題" %> <%= f.text_field :title %> </div> <div> <%= f.label :content, "內文" %> <%= f.text_area :content %> </div> <%= f.submit %> <% end %> ``` #### form_for的做事 承上,form_for會自動加給一個key,等於舊寫法的: > **<input type="text" name="note[content]" value="<%= @note.title%>">** #### form_for的行動 form_for 偷偷用了.persisted 這個方法來判斷內容物有沒有料,會回傳布林值, 如果沒料就判斷你是想新增東西,所以會走到 new, 如果有料就判斷你是想修改東西,所以會走到 edit。 ## (6) Con/Model:儲存一筆資料庫 if else 在def new完成之後, 繼續在Controller的create方法中,設定new帶入create之後的動作。 * 第8行的經由form_for打包起來的Note欄位雜湊(title: title, content: content),**塞到new裡面給Model**。 * 因此,最初可以寫成 render html: **params[:note]** * 寫實體變數@note 方便View拿到。而前一步網頁中form_for的()變數,也可改為同一實體變數。 * *在controller的方法(如create)中可加一行debugger,以便在終端機除錯(印 params[:note]。按contiune結束。)* ```ruby= def new @note = Note.new #寫進資料庫 end def create # title = params[:title] #寫進該當參數 # content = params[:content] #寫進該當參數 @note = Note.new(title: title, content: content) #寫進資料庫 接著再改寫進clean_params並獨立為private方法 end if @note.save redirect_to "/notes" #若寫入成功 轉進notes清單 else #若不成功 flash[:notice] = "新增失敗" render :new #借views下的已經form_for的new重新渲染 end end ``` 若不成功 * 在**Model**驗證必填某欄位(本例name) **validates :name, presence: true** * 在controller的else給條件 **render :new** 可在if條件下一行,加入快閃訊息 flash[:notice] = "Candidate created!" ,提示成功。 寫入成功後,可用SQLite打開資料庫,在Browse Data的對應table(本例note)看寫入紀錄。 ### Con:解決ForbiddenAttributesError資料清洗 原因:資料未清洗 解決法: 在create開頭用 **強參數strong parameter**(建立白名單,名單內才給過) 抓(require)出前面hash(只要params裡面的:note)的key之後,只允許(permit)部分欄位過來 ```ruby= clean_params = params.require(:note).permit(:title, :content) @note = Note.new(clean_params) ``` 最後一行可能已在def new替換為實體變數的模樣 @note 參數習慣寫成clean_params #### 包成一個新方法 為方便重複使用,獨立def成新的方法。 ```ruby= private def clean_params params.require(:note).permit(:title, :content) end ``` 原本create的實體變數new所帶參數,跟著改變 ```ruby= note = Note.new(clean_params) ``` ### 終端:檢證儲存成功 * rails c 或 rails console //irb * Note.all //取得所有的 Note 資料像是陣列(**all為類別方法**) * Note.first //抓出第一筆資料 * rails routes //路徑對照表 #### IRB - .any?:有沒有東西 - .errors.any?:有沒有錯誤 - .errors.full_message:錯誤訊息 ## (07) Con/View:展示清單 在Controller透過類別方法all,建議實體變數「複數」命名 常用實體變數:all、where(篩選條件)、order(排序)、limit(限制筆數) ```ruby= def index @notes = Note.all end ``` ### 第一階段=印出每一條 在View的index檔案中, @notes本質上會印出陣列,所以可利用**each迴圈**印出每一條 ```ruby= <table> <tr> <td>Title</td> <% @notes.each do |note| %> <tr> <td> <%= link_to note.title, note_path(note) %> </td> </tr> <% end %> </table> ``` ### 第二階段=每一條內頁 承上,增加個別筆數(note)資料連結,也就是show * 依據內建Helper用note_path * **帶id**才知道要看哪一條,相當於/note/#{note.id} * (note.id)的**id可以省略**,會自己抽id * note_path也可以省略,變成 <%= link_to note.title, note %> > <%= link_to 顯示字樣, 連結位址 %> > ```ruby= <% @notes.each do |note| %> <tr> <td><%= link_to note.title, note_path(note.id) %></td> <td><%= note.content %></td> </tr> <% end %> ``` ### 第三階段=做出「show」 在Controller新增方法「show」,利用參數params抓取id,結合SQL的find_by方法。加上實體變數給View使用。 * .find_by //找不到資料 會給nil * .find //找不到資料 會給ActiveRecord::RecordNotFound ```ruby= def show @note = Note.find_by(id: params[:id]) end ``` 在views目錄下新增show網頁檔: ```ruby= <ul> <h1><%= @note.title %></h1> <li>Title: <%= @note.title %></li> <li>Content: <%= @note.content %></li> </ul> ``` ### 解決NoMethodError找不到資料 預防undefined method,在網頁開頭加入: (專屬括號有等號= 印出,此處不加) ```ruby= <% if @note %> 主內容 <% else %> <h1>找不到資料</h1> <% end %> ``` ### 標出異常 #### 迴圈文字 可在new網頁列表出列出錯誤訊息。 1. 利用專屬if判斷有無錯誤,note.errors, 1. 在錯誤訊息本身,前一行放each迴圈印出full_message到變數(|message|)。 1. 將message丟到列表 \<li> 中印出來。 ```ruby= <% if @note.errors.any? %> <ul> <% @note.errors.full_messages.each do |message| %> <li><%= message %></li> <% end %> </ul> <% end %> ``` #### css上色效果+排版 1. 出現錯誤時,原始碼中的div內會出現新的div **class="field_with_error"**。 2. 在app/assets/stylesheets/ 新增css檔。裡面的application.css會將所有css打包生效(以下兩行的功用,勿刪) > *= require_tree //載入所有scss,若關掉會按英文字母順序載 *= require_self 3. 目錄中個別檔案檔名無所謂 4. 錯誤時,文字欄(type="text")邊框與標籤名變紅。 ```css= .field_with_error{ display: inline-block; input[type="text"]{ border-color: red; border-width:1px; } label{ color: red; } } ``` ## (08) Con/View:編輯/更新清單 建立新網頁edit,直接複製new內容。 1. 根據route路徑表,會去Controller找update方法。 * patch:只換掉其中一個資料 * put:整行換掉 1. 因此在View的網頁index,連結選擇路徑表Helper中的edit_xxx_path(會生成隱藏欄位name="_method" value="patch") ```ruby= <%= link_to "編輯", edit_note_path(note.id) %> ``` 在Controller,定義edit繼承show的大標題寫法(抓資料名) 定義update也繼承create的結構,導入清洗資料,區分成(回列表index)敗(重新編輯edit)。 ```ruby= def edit @note = Note.find_by(id: params[:id]) end def update @note = Note.find_by(id: params[:id]) if @note.update(clean_params) flash[:notice] = "更新了" redirect_to "/notes" else render :edit end ``` ## (09) Con:刪除清單 在View的網頁,連結根據路徑表的Helper,方法要寫,不然就是get。 1. 路徑後面接method。 2. 再接data-confirm防呆。 ```ruby= <%= link_to "刪除", note_path(note), method: 'delete', "data-confirm": "確定嗎?" %> ``` 或用雜湊 ```ruby= <%= link_to "刪除", note_path(note), data: {method: 'delete', confirm: '確定嗎?' } %> ``` 根據route路徑表,會去Controller找destory方法。 ```ruby= def destroy @note = Note.find_by(id: params[:id]) #找到他 @note.destroy #刪除他 flash[:notice] = "刪除了" redirect_to "/notes" #離開他 end ``` ## (10) Con/View:清理及簡化*8/12影片 ### Controller中 重複使用的,獨立成方法def..... ### View中 **Partial Render局部渲染** * 不要在 partial reder裡面用實體變數 * form_for獨立出去成開頭「_」之網頁檔,取消實體變數標記。 * 原網頁呼叫實體變數就好 <%= render "form", note: @note %> https://guides.rubyonrails.org/layouts_and_rendering.html#passing-local-variables **Randered collection** 底限單數網頁,在同名views目錄下。 (複雜 再確認) 原網頁 ## (11) Con:找不到網頁&其他 捕捉錯誤訊息 移到controller的上一層,**在Model**裡的application_record.rb ```ruby= begin 原內容 rescue ActiveRecord::RecordNotFound render file:"public/404.html", status: 404 end ``` **在Model**的application_controller.rb * 讓所有controller 都可以自動用rescue_from這個類別方法 * 如果出現ActiveRecord::RecordNotFound就用record_not_found方法 ```ruby= class ApplicationController < ActionController::Base rescue_from ActiveRecord::RecordNotFound, with: :record_not_found private # 渲染rails提供的404頁面,狀態也同步改成404 def record_not_found render file: "public/404.html", status: 404 end end ``` **在Controller** 在notes_controller.rb * before_action = before_filter,做指定 action 前先撈資料 * before_action 是一個類別方法,是 singleton method ```ruby= before_action :find_note, only: [:show, :edit, :update, :destroy] ``` **在View** 在index.html.erb * method 預設為 get 要改成delete。 * 也可以用 data 包起來 * <%# rel="nofollow" %>叫爬蟲不要跟過來 * 按超連結 link_to 會形成一個表單 ,JS 會把 method 跟 token 塞進來 * id: note.id 可搭配JS使用,取得特定資料。 * destroy 刪除內容,要多加delete方法,不然會走錯路,走到show頁面。 ```ruby= <%# data-method="delete"%> <%= link_to "刪除", note_path(note), method: "delete", data: {confirm: "確定嗎?",id: note.id } %> ``` _form.html.erb * 從@note 改成 note 讓_form.html不要自己亂抓東西,怕會抓到重複的 * 只要根據餵進來的東西在這邊呈現就好 ```ruby= <% if note.errors.any? %> <ul> <% note.errors.full_messages.each do |message| %> <li><%= message %></li> <% end %> </ul> <% end %> ``` edit.html.erb * view 提供的 render(局部渲染 partial render)不能把字串 form 改成符號,跟controller 的 render 不一樣 * 把 edit 的 @note 丟過來再丟到_form.html.erb,最後渲染回來 ```ruby= <%= render "form", note: @note %> ``` new.html.erb * 把 new 的 @note 丟過來再丟到_form.html.erb,最後渲染回來 ```ruby= <%= render "form", note: @note %> ``` ###### tags: `Rails`