65.9K
CodeProject 正在变化。 阅读更多。
Home

我的 Facebook 好友在哪里?

starIconstarIconstarIconstarIconstarIcon

5.00/5 (12投票s)

2013年10月7日

CPOL

17分钟阅读

viewsIcon

40163

在 Google 地图上定位您的 Facebook 好友 - 一个 Ruby on Rails Web 应用程序

引言

我一直在进行一个项目,涉及Facebook认证、嵌入“点赞”和“帖子”,以及将众筹项目的地点映射到Google地图上。几天前,我想,嘿,如果我能把我的Facebook好友的地点映射到Google地图上呢?结果发现这已经被做过了,但这并不能阻止我想学习如何做到这一点!本文将引导您完成使用Ruby on Rails创建您自己的“好友在哪里”Web应用程序的过程。

Windows下Ruby on Rails开发先决条件

由于我们在Code Project上主要是Windows开发者,所以我们留在Windows平台环境。为此,您需要:

  1. 下载并安装 RailsInstaller
  2. 下载并安装一个不错的IDE,即RubyMine(有一个30天的试用版,但它相当实惠且非常出色)

Facebook开发先决条件

您需要从您的Facebook帐户创建一个应用。为此(请注意,用户界面可能会随时间而变化),请登录Facebook,然后从“齿轮”下拉菜单中选择“创建应用”。

创建应用程序,提供:

  • 一个唯一的应用命名空间
  • 将站点域名设置为“localhost”
  • 将站点URL设置为 https://:3000/
  • 启用沙盒模式

完成此过程后,您将获得一个应用ID和应用密钥。稍后获取用户访问令牌时需要用到它们。

为测试目的查询Facebook的先决条件

要获取好友的位置和家乡信息,我们需要一个用户访问令牌,我们用这些信息来映射好友的所在地。为了测试目的,我们可以手动获取此令牌,但请注意,它每两小时过期一次,因此如果您收到“令牌已过期”的错误,则需要重复此步骤。

访问 https://developers.facebook.com/tools/explorer

然后单击获取访问令牌。将出现一个弹出窗口,从中选择“好友数据权限”,然后勾选“friends_hometown”和“friends_location”。

单击获取访问令牌,这将关闭对话框,此时您的访问令牌将显示在Graph API浏览器中。您可以将此访问令牌复制到您的代码中,以便在测试应用程序时进行临时访问。

测试查询

既然我们在,不妨测试一下我们将要使用的查询。单击“FQL查询”并输入:

SELECT uid, name, pic_square, current_address, current_location, 
hometown_location FROM user WHERE uid IN (SELECT uid2 FROM friend WHERE uid1 = 
me())

然后单击提交按钮。您应该会看到返回一个数组,如果您的好友输入了他们居住地和/或家乡的信息,并且将其设为公开,那么您应该会看到类似如下的内容:

稍后我们将在Ruby中解析这些记录。

连接Facebook的先决条件

Windows缺少SSL信息,这会导致SSL认证失败。要解决此问题,请严格按照以下说明操作:

  1. http://curl.haxx.se/ca/cacert.pem下载到c:\railsinstaller\cacert.pem
  2. 转到计算机 ->高级设置 ->环境变量
  3. 创建一个新的系统变量:
    • 变量:SSL_CERT_FILE
    • 值:C:\RailsInstaller\cacert.pem

出于某种原因,我最初尝试了三次才正确。

GitHub先决条件

这部分是可选的。此项目的源代码可在GitHub上找到,地址为 WhereAreMyFriends。如果您想设置自己的GitHub项目,我所做的就是:

  • 如果您还没有GitHub帐户,请创建一个。
  • 创建一个新的存储库,并包含一个README文件,以便您可以立即克隆该存储库。
  • 从命令行,将存储库克隆到所需的文件夹。我使用“git clone https://github.com/cliftonm/WhereAreMyFriends”克隆了我的。
  • 您也可以直接克隆我的存储库并使用我编写的代码,但您无法将任何更改提交回我。要实现这一点,您需要fork我的存储库,然后发出“pull requests”,如果您希望我将您所做的某些更改合并到我的项目中。

使用Git的先决条件

这部分是可选的,原因有两个:您可以使用Git命令行,也可以使用RubyMine的Git集成来处理存储库。我个人更喜欢使用独立的视觉工具,例如SmartGit/Hg,我发现它是各种Git视觉工具中最好的。

入门

在我们开始实际编码之前,我们还有一些收尾工作要做。

创建初始Rails应用

如果您克隆了我的存储库,请忽略此步骤,因为您可以直接在RubyMine中打开目录。

如果您是从头开始,因为您想了解我是如何编写此应用程序的,那么您需要创建一个Rails应用程序。同样,从命令行,转到您创建项目目录和内容的父目录。如果您从GitHub克隆了一个空的存储库,不用担心,也执行此操作。

在命令行中,输入“rails new WhereAreMyFriends”(或者,如果您给您的项目起了另一个名称,请使用该名称)。这将创建Ruby on Rails应用程序的所有组件。

当您在RubyMine IDE中打开目录时,您应该会看到类似以下内容:

如果您克隆了Git存储库,RubyMine应该已经配置为使用Git作为VCS。我个人更喜欢使用SmartGit/Hg,但您应该知道RubyMine具有内置的Git支持。

我们希望Git忽略RubyMine文件

我们不希望所有RubyMine IDE文件都成为存储库的一部分,因此请打开“.gitignore”文件(位于应用程序的根文件夹中)并添加:

/.idea

这将排除RubyMine创建的整个.idea文件夹。

我们将使用的组件(Gem)

我们需要引入几个组件,因此请编辑Gemfile(位于应用程序文件夹的根目录),并添加:

gem 'gmaps4rails'
gem 'fql'
gem 'slim'
gem 'thin'

更新Gemfile后,在RubyMine中单击工具菜单,然后选择“Bundler...”,然后选择“Install”,然后单击Install按钮(保留可选参数为空)。这将安装gem及其所有依赖项。

这些gem是什么?

gmaps4rails

这是用于与Google Maps(以及OpenLayers,Bing和Mapquest等其他地图)交互的gem

fql

这个gem支持在Ruby中使用Facebook查询语言,我使用它来查询我好友的位置。还有其他选项以及查询Facebook的其他技术,但这是我选择的方法。

Facebook FQL参考文档可在此处找到。

slim

这个gem,根据其网站的说法:“是一个模板语言,其目标是[在]不变得神秘的情况下减少必需部分的语法。”我发现它使HTML更具可读性,减少了尖括号、闭合标签等的杂乱。有一个很棒的在线工具可以在这里将HTML转换为slim。

thin

这个gem是一个比默认的WEBrick服务器快得多的Web服务器。

添加渲染地图所需的所有组件

gmaps4rails gem包含一个安装程序,可以添加显示地图所需的所有JavaScript和CSS。为此,打开命令提示符,然后cd到您的应用程序文件夹。然后键入:

rails generate gmaps4rails:install

创建页面控制器

既然我们还在命令行上,让我们创建控制器和视图。键入:

rails generate controller map_my_friends index

这将创建:

  • 文件“map_my_friends_controller.rb”在app\controllers文件夹中。
  • 文件夹“map_my_friends”在app\views文件夹中。
  • 文件“index.html.erb”在app\views\map_my_friends文件夹中。

删除“index.html.erb”文件,并创建一个名为“index.html.slim”的新文件,以便我们使用slim HTML语法而不是纯HTML。

现在,您的项目树应该显示这些内容:

设置根路由

最后,在我们做任何其他事情之前,让我们将根路由设置到此页面,这样我们就可以通过“localhost:3000”直接访问它。编辑routes.rb文件(位于config文件夹中),并添加:

root to: "map_my_friends#index"

请注意,当我们创建页面控制器时,路由

get "map_my_friends/index"

已自动为我们添加。

开始编码!

现在我们准备进行一些编码了。首先,我们将创建一个基本的模型,“Friend”,来保存有关我们好友的信息。我们可以使用类似于创建控制器的方式使用生成器来完成此操作,但由于它不是标准的解决方案,我更喜欢手动创建文件。

创建“Friend”模型

在RubyMine中,在app\models文件夹下,创建文件“friend.rb”。

class Friend < ActiveRecord::Base
  acts_as_gmappable

  # Fields we get from FB
  attr_accessible :uid, :name, :pic, :address
  # Fields required by gmaps4rails (lat and long also come from FB)
  attr_accessible :gmaps, :latitude, :longitude

  # gmaps4rails methods
  def gmaps4rails_address
    address
  end

  def gmaps4rails_infowindow
    "#{name}"
  end
end

acts_as_gmappable”这一行是一个钩子,它会在地址持久化时生成纬度和经度数据。虽然我无意创建一个可持久化的Friend模型,但gmaps4rails gem在某种程度上与Rails ActiveRecord耦合,而我花了五分钟谷歌搜索和尝试分离它,但都没有成功,这比我希望花费在这方面的时间还要多,因此,我们有一个可持久化的Friend模型。

创建“Friend”表

通常,模型都伴随着其关联的表,因此我们将使用数据库迁移来创建表。由于我们使用sqlite3作为数据库,因此无需处理数据库身份验证问题、数据库服务器等。

在RubyMine中,在“db”文件夹下创建一个名为“migrate”的子文件夹,在该文件夹中创建一个名为“001_create_friends_table.rb”的文件。

class CreateFriendsTable < ActiveRecord::Migration
  def change
    create_table :friends do |t|
      t.string :uid
      t.string :name
      t.string :pic
      t.string :address
      t.float :latitude
      t.float :longitude
      t.boolean :gmaps
      t.timestamps
    end
  end
end

您的项目树现在应该反映这两个新文件:

现在,按Ctrl+F9运行迁移,或者右键单击迁移文件,然后从弹出菜单中选择“运行db:migrate”。

创建我们的Facebook库

在Ruby on Rails代码中,“标准”做法是将渲染页面所需的所有代码直接放入控制器中。因此,通常,您会看到查询Facebook并填充模型的代码放在控制器或模型中。我个人更喜欢将此类代码放入lib文件夹,并提供辅助方法来与支持必要字段的任何模型进行接口。我读过一些文章在这一点上与我的观点不同,认为所有业务逻辑都应该放在模型中。我认为的问题是存在应用程序模型无关的业务逻辑(即,agnostic业务逻辑),例如我们如何与Facebook交互,这不应该放在应用程序的模型中,因为它与应用程序无关。

但是,由于Rails不会自动加载lib文件夹中的代码,因此我们必须对其进行强制加载。另请注意,lib文件夹中的文件不会自动使服务器重新加载Ruby脚本,因此如果您在lib文件夹的文件中进行了更改,则需要重新启动服务器。

首先,编辑app\config文件夹中的application.rb文件,并添加以下行:

config.autoload_paths += %W(#{config.root}/lib/facebook_wrapper)

这告诉Rails我们特别希望包含此文件夹中的文件。

其次,在lib文件夹中创建一个名为“facebook_wrapper”的子文件夹。

第三,在该子文件夹中创建一个名为“facebook_wrapper.rb”的文件。

您的项目结构现在应该如下所示:

现在我们将把类FacebookWrapper包装在一个名为FacebookWrapperModule的模块中,

module FacebookWrapperModule
  class FacebookWrapper
    def ... my functions ...
  end
end

并实现以下函数:

get_fb_friends

此函数以Facebook结构返回好友数组。

def self.get_fb_friends
  options = {access_token: "[your access token goes here]"}
  friends = Fql.execute("SELECT uid, name, pic_square, current_address, 
     current_location, hometown_location FROM user WHERE uid IN (
     SELECT uid2 FROM friend WHERE uid1 = me())", options)

  friends
end

稍后我们将修复硬编码的访问令牌 - 目前,我们只想让它运行起来。

from_fb_friends

此函数将Facebook好友数组转换为我们模型实例的数组,我们将其回调到应用程序以创建每个模型实例。由于Ruby是鸭子类型语言,应用程序只需实现我们期望初始化的属性(properties)或方法 - 我们不需要知道“类型”或像在C#中那样实现接口。此外,通过利用Ruby的回调功能,我们可以要求应用程序自己实例化其模型实例。

def self.from_fb_friends(fb_friends)
  friends = []

  fb_friends.each do |fb_friend|
    location = get_location_or_hometown_address(fb_friend)

    if !location.nil?  # or: unless location.nil?
      friend = yield(fb_friend, location)
      friends << friend
    end
  end

  friends
end

另一个Ruby的习惯是使用“unless”关键字而不是“if !”(如果不是),我个人认为这降低了代码的可读性。我对负逻辑没有意见,说“unless location.nil?”要求我进行一些脑力活动才能理解为“if locations不等于nil”。

get_location_or_hometown_address

最后,我们有一个私有的辅助方法,用于从好友的位置(首选)或家乡(备用)获取地址信息。

private

def self.get_location_or_hometown_address(fb_friend)
  location = fb_friend["current_location"]

  if location.nil?
   location = fb_friend["hometown_location"]
  end

  location
end

请注意,我们从不显式使用“return”关键字。这有一个原因,我将在下文说明。

更新控制器以传递位置信息

接下来,我们将更新map_my_friends_controller.rb文件,即我们index的控制器。首先,我们需要引用我们的facebook_wrapper库助手。这揭示了Ruby的模块和文件处理的一些复杂性。

首先,我们需要告诉Ruby“require”facebook_wrapper.rb文件(它知道如何获取,因为我们将lib\facebook_wrapper添加到auto_load配置路径中)。

require 'facebook_wrapper'

然后,我们需要告诉Ruby我们想使用FacebookWrapperModule中定义的那些对象。

include FacebookWrapperModule

如果我们不这样做,我们必须用“FacebookWrapperModule::”来限定对象。include关键字类似于C#中的using关键字,而module关键字类似于namespace关键字。这里唯一的新东西是动态加载依赖文件facebook_wrapper.rb

index方法的实现收集数组,并提供回调方法来填充每个Facebook结构实例的模型(Friend)实例,最后将该数组格式化为JSON并返回给客户端。

class MapMyFriendsController < ApplicationController
  def index
    fb_friends = FacebookWrapper.get_fb_friends
    @friends = FacebookWrapper.from_fb_friends(fb_friends) { |fb_friend, location|
        friend = Friend.new
        friend.uid = fb_friend["uid"]
        friend.name = fb_friend["name"]
        friend.pic = fb_friend["pic_square"]
        friend.address = location["name"]
        friend.latitude = location["latitude"]
        friend.longitude = location["longitude"]
        friend.gmaps = true

        friend
    }

    @json = @friends.to_gmaps4rails
    respond_to do |format|
      format.html # index.html.erb
      format.json { render json: @friends }
    end
  end
end

请注意,我们在回调代码中没有调用return friend - 如果这样做,它将被视为从调用代码返回,而@friends属性将永远不会被初始化!

使视图渲染地图

编辑index.html.slim文件(位于app\views\map_my_friends文件夹中),将这一行作为文件的全部内容添加:

= gmaps4rails(@json)

测试此功能

如果您运行应用程序(使用当前用户访问令牌),您应该会在Google地图上看到您的好友。

放大地图

但是,我们想要做的是使地图变大,使其占据全屏浏览器窗口的大部分(我所有操作都在全屏模式下进行,这就是我喜欢这个的原因)。为此,请将我们上面创建的行替换为:

= gmaps( :map_options => { :container_class => "my_map_container" }, 
  "markers" => {"data" => @json, 
  "options" => {"auto_zoom" => false} })

并编辑map_my_friends.css.scss(位于app\assets\stylesheets文件夹中),添加:

div.my_map_container {
  margin-top: 30px;
  padding: 6px;
  border-width: 1px;
  border-style: solid;
  border-color: #ccc #ccc #999 #ccc;
  -webkit-box-shadow: rgba(64, 64, 64, 0.5) 0 2px 5px;
  -moz-box-shadow: rgba(64, 64, 64, 0.5) 0 2px 5px;
  box-shadow: rgba(64, 64, 64, 0.1) 0 2px 5px;
  width: 80%;
  height: 80%;
  margin-left:auto;
  margin-right:auto;
}

div.my_map_container #map {
  width: 100%;
  height: 100%;
}

刷新浏览器,您将获得一个更大的地图,该地图会根据浏览器窗口的大小进行调整。

为缩略图弹出窗口添加一些炫酷效果

首先,我们将profile_url添加到我们使用的FQL查询中,并相应地调整我们的模型和控制器。我们还需要添加一个迁移来将此字段添加到Friend表中。

class AddProfileUrlField < ActiveRecord::Migration
  def change
    add_column :friends, :profile_url, :string
  end
end

接下来,我们提供一些HTML在Google Maps信息窗口中渲染:

def gmaps4rails_infowindow
  "<p><a href = '#{profile_url}' target='_blank'>#{name}</a>
   <br>#{address}<br><img src = '#{pic}'/></p>"
end

结果是:

显示:

  • 个人资料的链接,点击后在新窗口中打开,
  • 他们的位置/家乡,以及
  • 他们的Facebook图片。

Omniauth-Facebook

现在我们有了一个基本的应用程序在运行,让我们来处理另一组复杂性,这也将解决恼人的用户访问令牌过期问题。问题在于 - 我们不是收集我们的朋友,而是需要获得授权来收集访问我们网站的任何人的朋友,这意味着我们需要能够让用户使用他们的Facebook登录并授权我们查询他们的数据。

将omniauth-facebook添加到Gemfile

首先,在您文件夹中的Gemfile中添加:

gem 'omniauth-facebook'

现在我们添加到Gemfile中的此项目的gem是:

gem 'gmaps4rails'
gem 'fql'
gem 'slim'
gem 'thin'
gem 'omniauth-facebook'

创建用户模型

这次,让我们使用模型生成器创建用户模型。从RubyMine的工具菜单中,选择“运行Rails生成器”,然后双击“model”。输入Rails生成器的选项:

User provider:string uid:string name:string email:string oauth_token:string

这将创建一个新的迁移文件,因此在db\migrate下找到它,右键单击它并选择“运行'db:migrate'”。

在新创建的用户模型中,添加以下代码:

def self.create_with_omniauth(auth)
  create! do |user|
    user.provider = auth.provider
    user.uid = auth.uid
    user.oauth_token = auth.credentials.token

    if auth.info
      user.name = auth.info.name || ""
      user.email = auth.info.email || ""
    end
  end
end

此代码使用提供的身份验证信息在数据库中创建一个用户。

请注意,我们可以在此处访问存储在oauth_token字段中的用户访问令牌。

设置身份验证

config\initializers文件夹中,创建文件omniauth.rb并添加以下内容:

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :facebook, ENV['FACEBOOK_KEY'], ENV['FACEBOOK_SECRET'],
            :scope => 'friends_location, friends_hometown, user_friends, email',
           :display => 'popup'
end

这会告知omniauth我们正在使用Facebook进行身份验证。请注意“scope”键,其值是friends_locationfriends_hometown,这指定我们对朋友的位置和家乡感兴趣,并且我们需要user_friends以便获取Facebook用户的那些朋友。

设置环境变量

我个人不喜欢环境变量 - 我更喜欢使用一个不存储在Git存储库中的文件。我之前使用了一个local_env.yml文件来以编程方式向ENV集合添加项。

编辑application.rb文件(位于config文件夹中)并添加:

config.before_configuration do
  env_file = File.join(Rails.root, 'config', 'local_env.yml')
  YAML.load(File.open(env_file)).each do |key, value|
    ENV[key.to_s] = value
  end if File.exists?(env_file)
end

此代码向ENV集合添加其他项。现在我们需要创建文件。在config文件夹中,创建文件local_env.yml,其内容如下:

FACEBOOK_KEY: '[your key]'
FACEBOOK_SECRET: '[your secret id]'

请确保在输入您的密钥和secret ID时,保留单引号。

另外,将config/local_env.yml添加到您的.gitignore文件中 - 这可以防止文件添加到您的存储库中。

创建Sessions Controller

app\controllers文件夹中,创建文件sessions_controller.rb,其内容如下:

class SessionsController < ApplicationController

  def new
    redirect_to '/auth/facebook'
  end

  def create
    auth = request.env["omniauth.auth"]
    user = User.where(:provider => auth['provider'], 
                      :uid => auth['uid']).first || User.create_with_omniauth(auth)
    session[:user_id] = user.id
    redirect_to root_url, :notice => "Signed in!"
    end

  def destroy
    session[:user_id] = nil
    redirect_to root_url, notice: 'Signed out!'
  end
end

这处理了三个路由:

  • new - 简单地重定向到Facebook身份验证页面。
  • create - 执行登录,如果uid对于此提供程序(Facebook)是唯一的,则在数据库中注册用户。
  • destroy - 从Facebook注销并从数据库中删除用户的UID,强制用户重新登录(适用于测试和获取新的身份验证令牌)。

创建必要的路由

将以下路由添加到routes.rb文件(在config文件夹中):

match '/auth/:provider/callback' => 'sessions#create'
match '/signout' => 'sessions#destroy'
match '/signin' => 'sessions#new'

更改用户访问令牌以使用oauth_token

在我们的map_my_friends_controller中,我们将传递访问令牌,该令牌是从数据库中通过用户ID获取的,该ID在创建会话时存储。

def index
  user_id = session[:user_id]
  @friends = []

  if !user_id.nil?
    oauth_token = User.find(user_id).oauth_token
    @friends = get_friends(oauth_token)
  end

  @json = @friends.to_gmaps4rails
  respond_to do |format|
    format.html # index.html.erb
    format.json { render json: @friends }
  end
end

请注意,我将get_friends代码分离到一个单独的函数中。在许多Ruby on Rails代码中,您还会看到非常长的函数,其中包含应该被分解的代码块。很容易以“错误”的方式编写代码,因为您处理的是特定、隔离的路由处理函数,但这样做会使代码的可读性和可维护性大大降低。

而在facebook_wrapper.rb中:

def self.get_fb_friends(oauth_token)
  options = {access_token: oauth_token}
  friends = Fql.execute("SELECT uid, name, pic_square, current_address, 
    current_location, hometown_location, 
    profile_url FROM user WHERE uid IN (SELECT uid2 
    FROM friend WHERE uid1 = me())", options)

  friends
end

在视图中更新应用程序以进行登录/注销

在我们的应用程序视图(所有页面通用)中,我们想要提供:

  • 登录/注销通知消息
  • 登录/注销按钮
  • 登录用户的姓名

我们不妨也将此文件转换为“slim”文件,因此删除application.html.erb(位于app\views\layouts文件夹中),并将其替换为slim文件“application.html.slim”。

编辑application.html.erb文件(位于app\views\map_my_friends文件夹中),在文件顶部插入:

doctype
html
  head
    title Where Are My Friends
    = stylesheet_link_tag "application", media: "all"
    = javascript_include_tag "application"
    = csrf_meta_tag
  body
    #container
      #user_nav
        - if current_user
          | Signed in as
          | &nbsp;
          strong= current_user.name
          | &nbsp;
          = link_to "Sign out", signout_path
        - else
          = link_to "Sign in with Facebook", signin_path
      - flash.each do |name, msg|
        = content_tag :div, msg, id: "flash_#{name}"
      = yield
      = yield :scripts

从我们的视图访问用户记录

要访问我们在上面使用的current_user,我们将向application_controller.rb(位于app\controllers文件夹中)添加一个辅助函数:

class ApplicationController < ActionController::Base
  protect_from_forgery

  private

  def current_user
    @current_user ||= User.find(session[:user_id]) if session[:user_id]
  end

  helper_method :current_user
end

还有一堆CSS我没有展示。结果现在是一个可用的网站。

现场体验!

该应用程序托管在此处: http://wherearemyfriends.herokuapp.com/ 试试看!

注意事项

如果您的朋友没有设置当前位置或家乡,或者这些信息被阻止,他们将不会显示在地y图上。我也不会区分当前位置和家乡 - 那将是使用不同标记的一个很好的改进。所以有一些事情我会找时间去处理并更新文章。

此外,处理Facebook应用很有趣。例如,如果我想在我登录后让我的室友试用该网站,虽然我可以从我的网站注销,但我还需要从Facebook注销(通过访问Facebook!),只有这样我才能获得Facebook登录,以便我的室友可以用她的Facebook用户名和密码登录。

致谢

我特别感谢以下几个人,他们不知道他们帮助我把这一切整合在一起!这当然不包括那些花费无数小时编写Ruby、Rails和所有这些惊人gem的人。

历史

  • 2013年10月7日:初始版本
© . All rights reserved.