Render JSONP response in Rails

今天在给前端提供一个接口的时候, 由于有跨域的问题, 加之都是 get 请求, 所以就通 过 jsonp 来比较简单的实现。

Rails 中提供了很简便的接口, 通常我们这样返回 json 数据

render json: { hello: "world" }

对于需要加上 json callback 的情况我们只需要多加一个 option 就可以了

render json: { hello: "world" }, callback: params[:callback]

这样当我们通过 ajax 去请求别的域名的数据的时候, 加上一个 callback 参数, 比如 ?callback=abc。这样返回的数据就会被包裹在一个 JavaScript 的方法里, 像下面这样

abc({
  hello: "world"
})

这时前端同学反应希望返回的数据是 abc && abc() 的形式, 这种形式对于当服务端返回一个客户端 不识别的方法有着更好的容错处理, 看起来觉得很简单, 把返回的数据包装一下就可以了呗。做起来发现 有问题了。

开始我是这样解决的

render json: "#{callback} && #{callback}(#{json})"

问题一出现了, 里面的那些 JSON 数据格式不对, 返回的直接是 Ruby 的 Hash 的数据格式 (突然我 领悟了一些人说 Node.js 一大好处是统一前后端的语言, 当时还一直不明白这统一有毛用啊)。那就 encode 一下呗

json = ActiveSupport::JSON.encode json
render json: "#{callback} && #{callback}(#{json})"

这下总可以了吧, 结果客户端又抱错了, 看了报错信息我一下就懵逼了, Mime Type 不对。不应该用 application/json。可是 Rails 那个方法不也是 render json 吗。OK, 菜菜不在, 只能自己查 源码了。

# rails/actionpack/lib/action_controller/metal/renderers.rb line 115
add :json do |json, options|
  json = json.to_json(options) unless json.kind_of?(String)

  if options[:callback].present?
    if content_type.nil? || content_type == Mime::JSON
      self.content_type = Mime::JS
    end

    "/**/#{options[:callback]}(#{json})"
  else
    self.content_type ||= Mime::JSON
    json
  end
end

原来对于传入 callback 参数的这种, 虽然我们写的是 render json, 但是其实返回的 content_type 是 application/js。jsonp 需要的数据就是这样的类型滴, 不然那个 JS 方法如何执行呢。