我的 Facebook 好友在哪里?





5.00/5 (12投票s)
在 Google 地图上定位您的 Facebook 好友 - 一个 Ruby on Rails Web 应用程序
引言
我一直在进行一个项目,涉及Facebook认证、嵌入“点赞”和“帖子”,以及将众筹项目的地点映射到Google地图上。几天前,我想,嘿,如果我能把我的Facebook好友的地点映射到Google地图上呢?结果发现这已经被做过了,但这并不能阻止我想学习如何做到这一点!本文将引导您完成使用Ruby on Rails创建您自己的“好友在哪里”Web应用程序的过程。
Windows下Ruby on Rails开发先决条件
由于我们在Code Project上主要是Windows开发者,所以我们留在Windows平台环境。为此,您需要:
- 下载并安装 RailsInstaller
- 下载并安装一个不错的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认证失败。要解决此问题,请严格按照以下说明操作:
- 将 http://curl.haxx.se/ca/cacert.pem下载到c:\railsinstaller\cacert.pem。
- 转到计算机 ->高级设置 ->环境变量。
- 创建一个新的系统变量:
- 变量:
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_location
和friends_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
|
strong= current_user.name
|
= 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的人。
- Abram,他提供了调整google地图大小的代码。
- Ryan Bates,他提供了关于omniauth-facebook的精彩RailsCast。
- Ralphos,他提供了omniauth-facebook-example代码。
- DevDude,他提供了SSL证书错误问题的解决方案。
历史
- 2013年10月7日:初始版本