Let's talk a little about how a web page is served
At its most basic form, it is a just a pipe of text that is being sent by the server, and interpreted by your web browser.
We built some ruby script files yesterday, but now let's turn it up a few notches.
Open a new file and name it hello.cgi. Did I just give anyone any bad flashbacks? Can someone here explain what CGI is?
CGI is what we did back in the bronze age of the internet. We had graduated from carving HTML into stone tablets, and started putting actual code in our webpages (code code, not mere markup, like html)....
CGI stands for Common Gateway Interface (is more info really necessary??)
Let's put the hashbang at the top and put this in (paste in Flowdock):
#!/usr/bin/env rubyputs"Content-type: text/html<html> <head> <title>Cool Ruby</title> </head> <body> Hello, World! </body></html>"Ruby has a simple built-in webserver that we can use to serve up some files, so let's tell it to run a server on port 5000 to serve this file.
ruby -run -e httpd -- -p 5000 .
If you like, you can save this to a file:
#!/bin/bash
ruby -run -e httpd -- -p 5000 .'
and run it like this:
chmod +x rubyserve
./rubyserve
Windows users, see this forum topic
Open localhost:5000 you'll see the contents of the directory you started the server in. Click on hello.cgi
YAY!
Files with an extension of .cgi and have that hashbang are special, and will be executed by many webservers (as long as they are configured to), so our hello.cgi was executed, and puts'd its string to your web browser. Your web browser saw that first line (with Content-Type), and decided, "Yeah, I can serve text/html"
The web browser knows that the headers end when there is a blank line, so that's why that is there. We can add extra headers there if we wanted to (like expires or cache-control headers).
Let's make it do something a little more interesting.
#!/usr/bin/env rubygreetings=['Mr. President','Your Honor','Your Highness']greeting=greetings.sampleputs"Content-type: text/html<html> <head> <title>Cool Ruby</title> </head> <body> Hello, #{greeting}! <img src='under_construction.gif'> </body></html>"Now, every time we load this page, a random greeting is shown.
How can we deal with user input (params and forms) though?
Let's use the Ruby cgi module just a little, mostly to get the params, but we also get this #header method, too:
(aside about what require does, look at CGI, explain that it is a class that we can now instantiate, with methods that can be called on it)
#!/usr/bin/env rubyrequire'cgi'cgi=CGI.newgreetings=['Mr. President','Your Honor','Your Highness']ifcgi['name'].empty?greeting=greetings.sampleelsegreeting=cgi['name']endputscgi.headerputs"<html> <head> <title>Cool Ruby</title> </head> <body> Hello, #{greeting}! <img s rc='under_construction.gif'> </body></html>"Now, If I put ?name=dave at the end of my URL, I'll see a custom greeting just for me.
Okay, so what if we want to render a whole different page based on the URL?
Lets add some stuff to do that.
#!/usr/bin/env rubyrequire'cgi'cgi=CGI.newputscgi.headerifcgi['page'] == 'about'puts"<html> <head> <title>Cool Ruby</title> </head> <body> This is the about us page. </body> </html>"elseputs"<html> <head> <title>Cool Ruby</title> </head> <body> This is the home page. </body> </html>"endMan, this is cool, but we can't build all our web pages like this. If I were to build a bunch of pages, I'd have to repeat all this boilerplate html code all over again, and if something ever had to change, I'd have to change all this html code.
Let's extract that out into a resuable method.
#!/usr/bin/env rubyrequire'cgi'cgi=CGI.newdefheader"<html><head><title>Cool Ruby</title></head><body>"enddeffooter"</body></html>"endputscgi.headerputsheaderifcgi['page'] == 'about'puts"This is the about us page."elseputs"This is the home page."endputsfooterThis is better, but we can use that yield from yesterday to make things a little nicer:
#!/usr/bin/env rubyrequire'cgi'cgi=CGI.newdeflayout(&block)# the block arg here is not necessary to declareputs"<html><head><title>Cool Ruby</title></head><body>"putsyieldputs"</body></html>"endputscgi.headerlayoutdoifcgi['page'] == 'about'puts"This is the about us page."elseputs"This is the home page."endendLAB: Make link_to Extra credit: Make link_to work with args, or with a block.
Our pages are really more complicated than this, and we'd like to have them in thier own files, so lets break this up a bit.
Create a new file in the same directory, and name it about.html and paste in that content.
Create another file named index.html and use that content.
Back in hello.cgi, let's build a method to render out the file we want, and if it doesn't exist, render the index instead.
#!/usr/bin/env rubyrequire'cgi'cgi=CGI.newdeflayout(&block)puts"<html><head><title>Cool Ruby</title></head><body>"putsyieldputs"</body></html>"enddefrender_view(view_name)putsFile.read("#{view_name}.html")endputscgi.headerlayoutdoifcgi['page'] == 'about'render_view('about')elserender_view('index')endendThat was cool, let's do that with the layout, too. Create a file and name it layout.html.erb, and modify the layout method like this:
deflayout(&block)raw_file=File.read('layout.html.erb')putsERB.new(raw_file).result(binding)endAnd add this line to the top of the file:
require 'erb'
And paste our html in the layout with a yield like so:
<!DOCTYPEhtml>
<html>
<head>
<title>CoolRuby</title> </head>
<body>
Thetimeisnow: <%= Time.now %>
<br>
<%= yield %>
</body>
</html>(let's talk about ERB and binding for a bit, how it is part of the Standard Lib, etc)
Let's convert the view templates to use ERB, too. Rename the index.html and hello.html files to index.html.erb and about.html.erb, and update render_view to use ERB.
defrender_view(view_name)raw_file=File.read("#{view_name}.html.erb")ERB.new(raw_file).result(binding)endMove methods into a new class, instantiate class and start calling methods on that class.
#!/usr/bin/env rubyrequire'cgi'require'erb'cgi=CGI.newclassMyAppdeflayout(&block)raw_file=File.read('layout.html.erb')putsERB.new(raw_file).result(binding)enddefrender_view(view_name)raw_file=File.read("#{view_name}.html.erb")ERB.new(raw_file).result(binding)endendapp=MyApp.newputscgi.headerapp.layoutdoifcgi['page'] == 'about'app.render_view('about')elseapp.render_view('index')endendAn inline class like this kind of works for testing an idea, but let's move this class into its own file named my_app.rb, require it, and use it like so:
Replace the MyApp class with:
require_relative'my_app'app=MyApp.newrequire_relative is new to Ruby 2, so in a lot of older code, you'll probably see this instead:
require File.dirname(__FILE__) + '/my_app'
They are equivalent, though. __FILE__ is a special Ruby variable that means the file path of the script currently being executed.
We can also move the stuff in this layout to a new file, too.
Lab: Make new method in my_app.rb and move either the layout block or the contents of the layout block to it.
Extra Credit: Move that stuff into a new class instead, and call it routes.rb
Extra Credit 2: Look up Ruby's File class and see how you can render a 404 page if the file in cgi['page'] doesn't exist.
Final version (for now):
hello.cgi
#!/usr/bin/env rubyrequire'cgi'require'erb'require_relative'my_app'cgi=CGI.newputscgi.headerapp=MyApp.newapp.layoutdoifcgi['page'] != ''app.render_view(cgi['page'])elseapp.render_view('index')endendmy_app.rb
classMyAppdeflayout(&block)raw_file=File.read('layout.html.erb')putsERB.new(raw_file).result(binding)enddefrender_view(view_name)file_name="#{view_name}.html.erb"ifFile.exist?(file_name)raw_file=File.read(file_name)ERB.new(raw_file).result(binding)elserender_404endenddefrender_404File.read('404.html')enddeflink_to(url,text='', &block)ifblock_given?"<a href='#{url}'>#{yield}</a>"else"<a href='#{url}'>#{text}</a>"endendendlayout.html.erb
<html>
<head>
<title>CoolRuby</title> </head>
<body>
<%= yield %>
</body>
</html>index.html.erb
Thisisthe"Home"page<br>
<%= link_to('http://hotbot.com/','Hotbot') %>about.html.erb
Thisisthe"About Us"page<br>
<%= 'HELLO ' * 5 %>404.html
<h1>404: page not found</h1>