Li Yafei
A senior Ruby on Rails developer
李亚飞
For RubyConf China 2016
2016.09.24
产品讨论
扫码体验
公司注册
先做最可能阻塞但可以并行的事情
最终, 我们交了 1200 人民币!!!
技术选型
Ruby on Rails 5
turbolinks5
actioncable
bootstrap 3
font-awesome
figaro
postgres
slim
high_voltage
carriewave & upyun
sidekiq
kaminari
mina
轻前端应用 turbolinks + Rails5 远优于前端框架
产品迭代开发
class User
# other codes
has_many :reader_orders, class_name: 'Order', foreign_key: 'reader_id'
has_many :writer_posts, class_name: 'Post', foreign_key: 'writer_id'
end
// page1.html
- content_for :head do
meta name="turbolinks-cache-control" content="no-cache"
// layouts/application.html.slim
= csrf_meta_tags
= content_for?(:head) ? yield(:head) : ''
// -----------------
// app/views/wechat/writer/readers/index.html.slim
= link_to order.post.short_title, reader_post_path(order.post), data: { turbolinks: false }
更多 turbolinks5 的问题? 请查阅:
零延迟的支付体验
wxpay gem & sjr
function jsApiCall(){
showSpin(<%= @order.id %>);
WeixinJSBridge.invoke(
'getBrandWCPayRequest',
<%= raw @js_pay_hash.to_json %>,
function(res){
if(res.err_msg == "get_brand_wcpay_request:ok") {
}else if( res.err_msg == "get_brand_wcpay_request:cancel" ){
hideSpin();
$.post("<%= cancel_reader_orders_path(order_id: @order.id) %>");
}
}
);
}
function callpay(){
if (typeof WeixinJSBridge == "undefined"){
if( document.addEventListener ){
document.addEventListener('WeixinJSBridgeReady', jsApiCall, false);
}else if (document.attachEvent){
document.attachEvent('WeixinJSBridgeReady', jsApiCall);
document.attachEvent('onWeixinJSBridgeReady', jsApiCall);
}
}else{
jsApiCall();
}
}
callpay();
wxpay & actioncable
# order action cable( order_channel.rb )
class OrderChannel < ApplicationCable::Channel
def subscribed
stream_from "order_for_#{params[:order_id]}"
end
end
# order.rb
class Order
def send_cable_notify
ActionCable.server.broadcast "order_for_#{self.id}", {}
ActionCable.server.broadcast "user_post_for_user_#{self.reader_id}_and_post_#{self.post_id}", {}
end
end
// javascripts/pay_helper.js
function showSpin(order_id){
window.App.order_channel = window.App.cable.subscriptions.
create( {channel: 'OrderChannel', order_id: order_id}, {
received: function(){
$('#spin').spin(false);
Turbolinks.visit();
}
});
$('#spin').css('z-index', 1).spin().css('background-color', 'rgba(0,0,0,0.2)');
};
function hideSpin(){
$('#spin').css('z-index', -1).spin(false).css('background-color', 'transparent');
}
300KB -> 80KB, 加载速度提升 300%
配合 cdn 用户移动端在 700ms 左右打开应用
用户在电脑端打开文章, 扫码购买, 电脑端即时同步阅读
考虑登录与未登录情况
Channel
# user_post_channel.rb
class UserPostChannel < ApplicationCable::Channel
def subscribed
stream_from "user_post_for_user_#{params[:user_id]}_and_post_#{params[:post_id]}"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
Notification
# order.rb
def send_cable_notify
ActionCable.server.broadcast "order_for_#{self.id}", {}
ActionCable.server.broadcast "user_post_for_user_#{self.reader_id}_and_post_#{self.post_id}"
, {}
end
# post_token.rb
class PostToken
redis = Redis.new
@hash = Redis::Namespace.new(ENV['TOKEN_NAMESPACE'], :redis => redis)
@order_token_map = Redis::Namespace.new(ENV['TOKEN_NAMESPACE'] + '_order_token_map', :redis => redis)
@expire_time = 60 * 60 * 24 * 7 * 2
class <<self
def generate
SecureRandom.uuid.tr('-', '')
end
def bind_order(order_id, token)
@order_token_map.set(order_id, token)
@order_token_map.expire(order_id, @expire_time)
end
def is_bind_order?(order_id)
@order_token_map.get(order_id).present?
end
def order(order_id)
token = @order_token_map.get(order_id)
if token
@hash.set(token, true)
@hash.expire(token, @expire_time)
# notify
ActionCable.server.broadcast "order_token_for_#{token}", {}
end
end
end
end
Post Token
测试与发布
Test Mode
# routes.rb
Rails.application.routes.draw do
if ENV['USER_TEST_DEBUG'].present?
get '/test' => 'visitors#test'
get '/test_for_new_user' => 'visitors#test_for_new_user'
get '/test_for/:id' => 'visitors#test_for'
end
end
# OrdersController#pay
if ENV['USER_TEST_DEBUG'].present?
if @order.may_pay?
@order.pay!
order_token_for_order_id
@order.send_cable_notify
end
render js: 'alert("测试模式支付成功");Turbolinks.visit();'
return
end
Test Mode( 2 )
# sidekiq worker
if ENV['USER_TEST_DEBUG'].present?
ServerNotificationWorker.new.perform(title, text)
else
ServerNotificationWorker.perform_async(title, text)
end
CDN
# config/environments/production.rb
config.action_controller.asset_host = 'https://p8020cdn.b0.xxyun.com'
# app/uploaders/image_uploader.rb
class ImageUploader < CarrierWave::Uploader::Base
storage :xxyun
end
某云模板
发布一键化
$ mina deploy
Charts
# chart
gem "chartkick"
gem 'groupdate'
gem 'linux_fortune'
05.16 ~ 06.06
150+ 文章
3000+ 销售额
在 Rails5 轨道上运营你的产品
AD
深圳市
网络技术有限公司
招人中
同时提供: 创业产品 MVP 开发
RubyConf 赞助商, 资料袋有详细介绍
By Li Yafei