如果要說一個網站最單純的是什麼,應該就是所謂的 「View」 吧,擺上幾個文字 + 幾張圖片,丟到 Server 上就是一個網站,甚至打開記事本就可以編輯網站內容(現在也是可以),既然是這麼重要的部分,那怎麼可以缺少呢?昨天我們已經做出 Controller,今天就接著把 View 完成,讓整個框架更靈活有趣吧
如果你寫過 Rails 一段時間,應該很熟悉 Rails 預設用來產生 Template 的方式是 Embedded Ruby(ERb)
例如像是這樣
<h1><%= Time.now.to_s %></h1>
這種語法讓我們很容易的在 HTML 裡面穿插程式碼,所以今天要做得事情,也會跟 Rails 一樣,用 ERB 當作我們的預設 Template,另外在Rails 5.1 版之後,View 的 templete 已經從 erubis 換成 erubi erubi is included in Rails 5.1, replacing erubis,所以我們這邊就直接使用 erubi
首先打開 mavericks.gemspec,我們在這個檔案最底下加上 erubi
# mavericks/mavericks.gemspec
 
spec.add_runtime_dependency "rack"
# 加上 erubi
spec.add_runtime_dependency "erubi"
接著在 lib/mavericks.rb 這隻檔案裡面,加上 require "mavericks/controller",並且把原本底下的 class Controller 給移除,因為接下來 Controller 程式碼會越來越多,勢必要做分離,整理後的程式碼如下
# mavericks/lib/mavericks.rb
require "mavericks/version"
require "mavericks/routing"
require "mavericks/support"
require "mavericks/dependencies"
# 加上這段
require "mavericks/controller"
module Mavericks
  class Error < StandardError; end
  class Application
    def call(env)
      return favicon if env["PATH_INFO"] == '/favicon.ico'
      return index(env) if env["PATH_INFO"] == '/'
      begin
        klass, act =  get_controller_and_action(env)
        controller = klass.new(env)
        text = controller.send(act)
        [200, {'Content-Type' => 'text/html'},
         [text]]
      rescue
        [404, {'Content-Type' => 'text/html'},
         ['This is a 404 page!!']]
      end
    end
    private
    def index(env)
      begin
        home_klass = Object.const_get('HomeController')
        controller = home_klass.new(env)
        text = controller.send(:index)
        [200, {'Content-Type' => 'text/html'}, [text]]
      rescue NameError
        [200, {'Content-Type' => 'text/html'}, ['This is a index page']]
      end
    end
    def favicon
      return [404, {'Content-Type' => 'text/html'}, []]
    end  
  end
  
  # 移除
  # class Controller
  # .
  # .
  # end
end
接著新增一個 controller.rb 檔案,將搬移過來的 class Controller 貼上,並且新增兩個 mehtod,render 和 controller_name
# mavericks/lib/mavericks/controller.rb
require 'erubi'
module Mavericks
  class Controller
    attr_reader :env
    def initialize(env)
      @env = env
    end
    def render(view_name)
      filename = File.join "app", "views", controller_name, "#{view_name}.html.erb"
      template = File.read filename
      eval(Erubi::Engine.new(template).src)
    end
    def controller_name
      klass = self.class
      klass = klass.to_s.gsub /Controller$/, ""
      Mavericks.to_underscore klass
    end
  end
end
我們先來看看 render 這個程式碼,還記得 Rails 的 Controller 最後都會做 render 嗎?沒印象的話,那是因為如果沒有在 Action 裡面寫上 render, Rails 會自動做預設判斷,而在 render 這個 method 裡面,我們會接收到一個 view_name 的變數,透過預設的檔案路徑去尋找 template,如下面程式碼所示
filename = File.join "app", "views", controller_name, "#{view_name}.html.erb"
這個路徑位置跟 Rails 設計的一樣,也是昨天一直提到的 convention over configuration 模式,我們希望開發者將檔案放在 app/views/ controller_name/action.html.erb,至於檔案名稱如何處理,像是 Controller 的檔名,這些在之前製作 Controller 已經做過就不再多做說明
接著將讀取到的檔案丟給 erubi 做處理
eval(Erubi::Engine.new(template).src)
然後我們回到 just_do 修改我們的檔案,我們將原本只能回傳字串的 Taskks#index 換成 render
# just_do/app/controllers/tasks_controller.rb
class TasksController < Mavericks::Controller
  def index
    @task_name = '向 Rails 致敬!30天寫一個網頁框架,再拿來做一個 Todo List!'
    render 'index'
  end
end
接著新增 index.html.erb template 在相對應的目錄位置
# just_do/app/views/tasks/index.html.erb
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Index</title>
  </head>
  <body>
    <h1>任務的index</h1>
    <%= @task_name %>
  </body>
</html>
最後重整後打開網頁 http://127.0.0.1:3001/tasks/index
任務的index
向 Rails 致敬!30天寫一個網頁框架,再拿來做一個 Todo List!
應該會看到畫面上出現上列文字,酷!
剛剛我們有提到,在 Rails 裡面如果沒有寫 render 的話,預設是會抓到對應的檔案,例如像剛剛的 TasksController#index,就會自動去抓app/views/tasks/index.html.erb,做法不會太難,但我們要先釐清一些觀念,在 Mavericks 中,每一頁 Controller 都是一個物件,所以我們可以加上一個 attribute,來記錄這個 Controller 有沒有執行過 render
# mavericks/lib/mavericks/controller.rb
require 'erubi'
module Mavericks
  class Controller
    # 加上 attribute
    attr_reader :env, :called_render
    def initialize(env)
      @env = env
      # 預設為 false
      @called_render = false
    end
    def render(view_name)
      # 執行過改為 true
      @called_render = true
      filename = File.join "app", "views", controller_name, "#{view_name}.html.erb"
      template = File.read filename
      eval(Erubi::Engine.new(template).src)
    end
    def controller_name
      klass = self.class
      klass = klass.to_s.gsub /Controller$/, ""
      Mavericks.to_underscore klass
    end
  end
end
接著在 Application#call 修改程式碼為
# mavericks/lib/mavericks.rb
def call(env)
  return favicon if env["PATH_INFO"] == '/favicon.ico'
  return index(env) if env["PATH_INFO"] == '/'
  begin
    klass, act =  get_controller_and_action(env)
    controller = klass.new(env)
    text = controller.send(act)
    # 如果沒有執行過 render,就執行 default_render
    text = default_render(controller, act) unless controller.called_render
    [200, {'Content-Type' => 'text/html'},
     [text]]
  rescue
    [404, {'Content-Type' => 'text/html'},
     ['This is a 404 page!!']]
  end
end
最後在底下的加上 default_render
# mavericks/lib/mavericks.rb
private
def default_render(controller, act)
  controller.render(act)
end
然後回到 just_do 把 render 'index' 拿掉後重整看看,如果畫面沒有出錯,代表我們有 default render 了
功能是越來越完整了,但是這個 View 好像少了點什麼,我記得 Rails 不是蠻多 helper 可以用的嗎?例如: link_to 之類的
嗯...,聽起來應該要加點方便開發者使用的 method 才對,那明天就來加上去吧!