BigBing 技术博客

使用subdomain开发和测试rails应用(二)

基于上一篇blog使用subdomain开发和测试rails应用(一),已经知道如何在开发和测试环境中配置和使用subdomain。这篇blog主要结合实例代码,介绍如何在rails应用内处理subdomain。

Blog应用实例

实例代码基于Railscasts上的blog-before,升级了一下gem的版本。

更新后的代码github地址:blog-subdomain

应用场景:一个Blog应用网站,用户可以有自己的blog,每个用户的blog有subdomain,例如:bing.blogs.dev

subdomain的值,在rails中时存放在request的实例中的。

配置routes:

  match '', to: 'blogs#show', constraints: {subdomain: /.+/}

在Blog controller的show action中,使用subdomain来获取blog实例:

  @blog = Blog.find_by_subdomain!(request.subdomain)

在首页上连接各个blog的地址:

  <%= link_to blog.name, root_url(subdomain: blog.subdomain) %>

由于所有的articles都存在同一个表中,所以获取文章的时候,要先通过subdomain获取blog实例,然后通过blog获取articles

  @blog = Blog.find_by_subdomain!(request.subdomain)
  @article = @blog.articles.find(params[:id])

支持www子域名访问

我们希望用户输入www.blogs.dev时访问blog的列表,而不是说找不到subdomain。 通过rails的constrains有两种方法实现:

  • 方法一: 在constrains选项中用lamada
  lambda { |request| request.subdomain.present? && r.subdomain != 'www' }

另外一种使用正则表达式的方法:

  constraints: { subdomain: /^(?!www$)(.+)$/i }
  • 方法二: 定义专门的类,适合于逻辑比较复杂的情况
  class Subdomain
    def self.matches?(request)
    request.subdomain.present? && request.subdomain != "www"
    end
  end

在routes中:

  constraints(Subdomain) do
    match '/' => 'blogs#show'
    # more ...
  end

连接到有www的主页:

<%= link_to 'Home', root_url(subdomain: 'www') %>

连接到无www的主页:

<%= link_to 'Home', root_url(subdomain: false) %>

可以配置默认不使用subdomian:

config.action_controller.default_url_options = { subdomain: false }

总结rails constrains的使用

上面的几段代码,也展示了最常见的rails routes中constrains的使用方法:

还有一种常见的就是限制传入的id参数的格式: resources :photos, :constraints => {:id => /[A-Z][A-Z][0-9]+/}

(完)

使用subdomain开发和测试rails应用(一)

这篇博客总结和展示在开发和测试rails应用的时候如何使用子域名subdomains

开发环境 Developing

使用lvh.me

如果在开发环境中使用的是webrick或者Puma服务器,可以通过lvh.me来访问子域名。例如:

http://sub.lvh.me:3000

lvh.me会被解析到localhost,这样就可以通过默认的3000端口访问子域名(Puma的默认端口是9292)。但是这种方法可能会慢一点。

修改hosts文件

为了减少来回的跳转,可以通过修改hosts文件来访问子域名,在Mac下是/etc/hosts. 例如:

127.0.0.1 sub.virtual.local

然后你还是可以使用这个域名加默认的端口来访问应用:

http://sub.virtural.local:3000

使用Pow(仅限Mac)

Pow本身非常好得支持子域名:安装Pow, 连接应用,然后就可以通过子域名访问应用:

sub.app-name.dev

你可以使用powder gem, 方便安装和管理pow。

测试环境 Testing

Capybara在测试应用时需要访问真实的服务器,所以在测试环境下也需要解决子域名的问题。

CI

最简单的方法就是使用lvh.me

添加几个helper方法,例如:spec/support/subdomains.rb:

def switch_to_subdomain(subdomain)
  # lvh.me总是解析到127.0.0.1
  Capybara.app_host = "http://#{subdomain}.lvh.me"
end

def switch_to_main_domain
  # Capybara.app_host = "http://lvh.me"
end

然后在feature测试中就可以使用:

switch_to_subdomain('sub')

在本机测试

虽然上面的方法在本机也是可行的,但是还是会浪费时间在来回跳转上,下面的方法更加好用。 首先还是跟上面一样修改hosts文件:

127.0.0.1 sub.virtual.local

在本机设置环境变量 (例如:LOCALVIRTUALHOST,推荐使用dotenv-rails gem) 在.env中添加:

LOCAL_VIRTUAL_HOST = virtual.local

然后修改spec/support/subdomains.rb

def local_virtual_host
    ENV['LOCAL_VIRTUAL_HOST'] || 'lvh.me'
end

def switch_to_subdomain(subdomain)
    Capybara.app_host = "http://#{subdomain}.#{local_virtual_host}"
end

def switch_to_main_domain
    Capybara.app_host = "http://#{local_virtual_host}"
end

这样可以使得本机的测试更快一些,还有一个需要注意的地方是,如果你有多个子域名,你不能在hosts文件中使用通配符,例如:*.virtual.local。你需要一个一个得指定子域名。

参考: Developing and testing Rails applications with subdomains

产品环境 Nginx + Unicorn + Eye 的 Rails 应用配置

最近整理了一下个人项目产品环境的配置,加了一些注释,分享出来希望对大家有用。 欢迎大家提改进建议。

关于Ruby的时间

TODO:主要内容

  • UTC & GMT
  • Time
  • Datetime
  • timecop
  • Timecop.freeze(24.hours.from_now)
  • Timezone gem?

ActiveSupport::TimeZone.new(“”).formatted_offset .local(*args)

now.in_time_zone(‘’)

now.change

gem: TZInfo

ActiveSupport::TimeZone The TimeZone class serves as a wrapper around TZInfo::Timezone instances.

  1. Limit the set of zones provided by TZInfo to a meaningful subset of 146 zones
  2. Retrieve and display zones with a friendlier name
  3. Lazily load TZInfo::Timezone instances only when they’re needed.
  4. Create ActiveSupport::TimeWithZone instances via TimeZone’s local, parse, at and now methods.

set config.time_zone in the Rails Application, you can access this TimeZone object via Time.zone:

application.rb:

class Application < Rails::Application config.time_zone = ‘Eastern Time (US & Canada)’ end

Time.zone # => # Time.zone.name # => "Eastern Time (US & Canada)" Time.zone.now # => Sun, 18 May 2008 14:30:44 EDT -04:00 Time.zone.today

in_time_zone

You shouldn’t ever need to create a TimeWithZone instance directly via new. Instead use methods local, parse, at and now on TimeZone instances, and in_time_zone on Time and DateTime instances.

Time.zone = ‘Hawaii’ # => ‘Hawaii’ DateTime.utc(2000).in_time_zone # => Fri, 31 Dec 1999 14:00:00 HST -10:00 Date.new(2000).in_time_zone # => Sat, 01 Jan 2000 00:00:00 HST -10:00

Time.utc(2000).in_time_zone(‘Alaska’) # => Fri, 31 Dec 1999 15:00:00 AKST -09:00 DateTime.utc(2000).in_time_zone(‘Alaska’) # => Fri, 31 Dec 1999 15:00:00 AKST -09:00 Date.new(2000).in_time_zone(‘Alaska’) # => Sat, 01 Jan 2000 00:00:00 AKST -09:00

see all the available time zones, run:

rake time:zones:all

The default time zone in Rails is UTC.

Apply user’s preferred time zone:

# app/controllers/application_controller.rb
around_action :set_time_zone, if: :current_user

private

def set_time_zone(&block)
  Time.use_zone(current_user.time_zone, &block)
end

display times in a specific user’s time zone, we can use Time’s in_time_zone method:

Working with APIs

When working with APIs, it is best to use the ISO8601 standard, which represents date/time information as a string. ISO8601’s advantages are that the string is unambiguous, human readable, widely supported, and sortable.

> timestamp = Time.now.utc.iso8601
=> "2015-07-04T21:53:23Z"

The Z at the end of the string indicates that this time is in UTC, not a local time zone. To convert the string back to a Time instance, we can say:

> Time.iso8601(timestamp)
=> 2015-07-04 21:53:23 UTC
# This is the time on my machine, also commonly described as "system time"
> Time.now
=> 2015-07-04 17:53:23 -0400

# Let's set the time zone to be Fiji
> Time.zone = "Fiji"
=> "Fiji"

# But we still get my system time
> Time.now
=> 2015-07-04 17:53:37 -0400

# However, if we use `zone` first, we finally get the current time in Fiji
> Time.zone.now
=> Sun, 05 Jul 2015 09:53:42 FJT +12:00

# We can also use `current` to get the same
> Time.current
=> Sun, 05 Jul 2015 09:54:17 FJT +12:00

# Or even translate the system time to application time with `in_time_zone`
> Time.now.in_time_zone
=> Sun, 05 Jul 2015 09:56:57 FJT +12:00

# Let's do the same with Date (we are still in Fiji time, remember?)
# This again is the date on my machine, system date
> Date.today
=> Sat, 04 Jul 2015

# But going through `zone` again, and we are back to application time
> Time.zone.today
=> Sun, 05 Jul 2015

# And gives us the correct tomorrow according to our application's time zone
> Time.zone.tomorrow
=> Mon, 06 Jul 2015

# Going through Rails' helpers, we get the correct tomorrow as well
> 1.day.from_now
=> Mon, 06 Jul 2015 10:00:56 FJT +12:00

Rails saves timestamps to the database in UTC time zone. We should always use Time.current for any database queries, so that Rails will translate and compare the correct times.

Post.where("published_at > ?", Time.current)
# SELECT "posts".* FROM "posts" WHERE (published_at > '2015-07-04 17:45:01.452465')

A summary of do’s and don’ts with time zones

DON’T USE

* Time.now
* Date.today
* Date.today.to_time
* Time.parse("2015-07-04 17:05:37")
* Time.strptime(string, "%Y-%m-%dT%H:%M:%S%z")

DO USE

* Time.current
* 2.hours.ago
* Time.zone.today
* Date.current
* 1.day.from_now
* Time.zone.parse("2015-07-04 17:05:37")
* Time.strptime(string, "%Y-%m-%dT%H:%M:%S%z").in_time_zone

Testing time zones

Rails 4.1 added a ActiveSupport::Testing::TimeHelpers module, with three useful methods: travel, travel_back, and travel_to. We can use these methods to freeze time within blocks in our tests.

If using an older version of Rails, there are three gems to help us set and freeze the time in our tests: Timecop, Delorean, and Zonebie. As much as I love the reference to go back_to_the_present, I usually use Timecop.

new_time = Time.zone.parse("2014-10-19 1:00:00")

# With Timecop, we can freeze the time,
Timecop.freeze new_time

# or
Timecop.travel new_time

# but will need to clean up after the spec, and return to current time
Timecop.return

# Alternatively we can use blocks, which only freeze the time inside our block
Time.use_zone("Sydney") do
end

# With Delorean, the syntax is a touch different
Delorean.time_travel_to("1 month ago") do
end
Delorean.back_to_the_present

# And Zonebie sets the time zone to a random one each time we run our tests
Zonebie.set_random_timezone

In summary

  • Always work with UTC.
  • Use Time.current or Time.zone.today.
  • Use testing helper methods of your choice to freeze the time in your tests, preferably by using a block.

参考

https://robots.thoughtbot.com/its-about-time-zones

HTML5 Boilerplate

HTML5 Boilerplate是一个非常流行的,用于快速构建健壮的web应用和站点的前端模板。 它集合了社区中很多开发人员的经验。

Features:

  • 轻量,适合移动应用开发,
  • 包含优化了的Google Analytics的代码片段
  • 包含了Normalize.css,还有一些基本的样式,helpers,media queries, print styles
  • 包含了jQuery和Modernizr
  • 还有实用的常用服务器的配置,比如:Apache, Nginx

可以通过Initializr网站来自定义需要的模板,比如Responsive或者Bootstrap集成后的版本

Html5 Boilerplate和Bootstrap的集成

深入学习基于Html5 Boilerplate的开发