iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 12
0
Modern Web

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

[DAY 12] 復刻 Rails - Request

在很早之前我們曾經提過 Rack 這個東西,也提到在 Rack 裡面有一個很特別的 Hash 叫 ENV,裡面帶有非常多有用的資料,靠著這些資料我們可以來處理與瀏覽器(也就是使用者)之間的互動,而這個互動其實靠的就是 Request 和 Response 來回,今天我們要來處理的就是 Request

建立一個 Request object

在剛建立 Mavericks 時曾經提過 @env,那時候我們只有印出來看看裡面有哪些東西,還有在處理 routing 的時候,也有用到 env["PATH_INFO"],之後我們有一段時間就沒在提起 env,接下來我們會透過 Rack 提供的 Request 來處理送進網站的資訊,請看以下程式碼所示

# mavericks/lib/mavericks/controller.rb

def request
  @request ||= Rack::Request.new(@env)
end

透過 Rack 來 new 一個 Request 物件的同時,我們也把 @env 傳送進去,在這個地方會看到我們使用了像這樣的語法

@request ||= Rack::Request.new(@env)

目的是為了把結果 caches 起來,有很多人常會誤解這樣的語法展開後是這樣

# 這是錯誤的觀念
@request = @request || Rack::Request.new(@env)

但其實應該是這樣

# 這才是對的觀念
@request || @request = Rack::Request.new(@env)

意思是說,如果 @requestnil or false,才去做 || 右邊的行為,這樣就不用每次都呼叫 Rack::Request.new(@env) 來花費效能

另外我們也加了一個 method 叫 params,裡面直接回傳 Rack::Request 已經幫我們處理好的 params

# mavericks/lib/mavericks/controller.rb

def params
  request.params
end

整個 Mavericks 的 Controller 現在就會長這樣

# mavericks/lib/mavericks/controller.rb

require 'erubi'
require "mavericks/file_model"

module Mavericks
  class Controller
    .
    .
    (略)

    def request
      @request ||= Rack::Request.new(@env)
    end

    def params
      request.params
    end
  end
end

接著立刻回到 just_do,來看看怎麼使用

# just_do/app/controllers/tasks_controller.rb
 
def show
  @task = FileModel.find(params['id'])
end

我們將原本 FileModel.find(1) 改成 FileModel.find(params['id']),利用在 Mavericks 處理好的 params 來找尋特定資料,現在打開瀏覽器進入到 http://127.0.0.1:3001/tasks/show?id=1 這頁,會發現我們已經可以透過 request 帶著 params,也就是 ?id=1 來取得特定的資料,如果有其他 JSON 檔案的話,也可以改變 id 來找尋其他資料

除了 params 以外,也來看看 request 裡面可以拿到什麼資訊

# just_do/app/controllers/tasks_controller.rb

def show
  @task = FileModel.find(params['id'])
  @user_agent = request.user_agent
end

這裡我們嘗試取得 user_agent,然後修改一下 View

<!-- just_do/app/views/tasks/show.html.erb -->

<div class="row">
  <div class="col-6">
    <div class="card">
      <div class="card-header">
        任務細節
      </div>
      <div class="card-body">
        <div class="row">
          <div class="col"><%= @user_agent %></div>
        </div>
        <div class="row">
          <label class="col">任務名稱</label>
          <div class="col">
             <%= @task['title']  %>
          </div>
        </div>
        <div class="row">
          <label class="col">任務內容</label>
          <div class="col">
            <%= @task['content']  %>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

如果沒有出錯的話,會在頁面上看到類似像 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6)... 這樣的user agent 資訊

有 Request 當然也有 Responses

其實 Response 在之前我們就處理過了,只是當時我們是用比較 hard-core 的做法,還記得一開始看到的這串陣列嗎?

[200, {'Content-Type' => 'text/html'}, [text]]

但其實我們可以利用 Rack 來「包裝」起來,讓我們先回到 Mavericks 的 Controller

首先我們要將 response 包裝起來,所以要加上一個 response method,同時建立一個 get_response 來檢查有沒有 response

# mavericks/lib/mavericks/controller.rb

def response(text, status = 200, headers = {})
  raise "Already responded!" if @response
  @response = Rack::Response.new(text, status, headers)
end

def get_response
  @response
end

接著修改 render_layout,透過這樣的修改,會先經過 response 的包裝來回應網頁內容,而不是直接回傳

# mavericks/lib/mavericks/controller.rb

def render_layout
  layout = File.read "app/views/layouts/application.html.erb"
  text = eval(Erubi::Engine.new(layout).src)
  response(text)
end

接著將 called_render 拿掉,並且在那一行 attr_reader:called_render 也換成 :content

# mavericks/lib/mavericks/controller.rb

    # 將 :called_render 換成 :content
    attr_reader :env, :content
    
    def initialize(env)
      @env = env
      # - @called_render = false
      @content = nil
    end

    def render(view_name)
      # - @called_render = true
      filename = File.join "app", "views", controller_name, "#{view_name}.html.erb"
      template = File.read filename

      @content =  eval(Erubi::Engine.new(template).src)
    end

回到 call 做一些小修改,修改的內容為用 controller.content 檢查有沒有主動寫 render,如果沒有的話,那就呼叫 default_render

# mavericks/lib/mavericks.rb

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)

        controller.send(act)
        default_render(controller, act) unless controller.content
        controller.render_layout

        if controller.get_response
          controller.get_response.to_a
        else
          [500, {'Content-Type' => 'text/html'},
           ['server error!!']]
        end

      rescue
        [404, {'Content-Type' => 'text/html'},
         ['This is a 404 page!!']]
      end
    end
    # .
    # .
    # (略)

然後測試看看,因為我們只有處理 Response 的部分,所以畫面應該都不會有什麼更動才對,如果都沒有出錯,代表完成了!

好像差不多了耶?

休淡幾勒!我們還沒碰到真正的資料庫耶,這幾天做的 Model 充其量也只是個普通 Model,但根本不是真正的 ORM 呀,明天開始就會進入 Model 比較核心的部分,也就是對資料庫做操作


上一篇
[DAY 11] 復刻 Rails - 更多的 Model 功能
下一篇
[DAY 13] 復刻 Rails - 進入 ORM 前,先了解 Migration
系列文
向 Rails 致敬!30天寫一個網頁框架,再拿來做一個 Todo List30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言