iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 8
0
Modern Web

向 Rails 致敬!30天寫一個網頁框架,再拿來做一個 Todo List系列 第 8

[DAY 8] 復刻 Rails - 這裡的 View 還不錯

如果要說一個網站最單純的是什麼,應該就是所謂的 「View」 吧,擺上幾個文字 + 幾張圖片,丟到 Server 上就是一個網站,甚至打開記事本就可以編輯網站內容(現在也是可以),既然是這麼重要的部分,那怎麼可以缺少呢?昨天我們已經做出 Controller,今天就接著把 View 完成,讓整個框架更靈活有趣吧

樣板 ERB

如果你寫過 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

MVC 架構裡面 的 V

首先打開 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,rendercontroller_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!

應該會看到畫面上出現上列文字,酷!

有辦法 default render?

剛剛我們有提到,在 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 才對,那明天就來加上去吧!


上一篇
[DAY 7] 復刻 Rails - 再加一點 Autoloading
下一篇
[DAY 9] 復刻 Rails - 終於有基本雛形了!在 View 上面加點東西
系列文
向 Rails 致敬!30天寫一個網頁框架,再拿來做一個 Todo List30

尚未有邦友留言

立即登入留言